b01lers CTF 2025

比赛地址:b01lers CTF 2025

比赛时间:19 Apr 2025 07:00 CST - 21 Apr 2025 07:00 CST

复现的题目用🔁标注

Web

when

Challenge

web/whenbeginner

author: tillvit

the sunk cost fallacy

https://when.atreides.b01lersc.tf/

app.ts

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
import express from 'express'
import { rateLimit } from 'express-rate-limit'
import path from "path"

const limiter = rateLimit({
windowMs: 60 * 1000,
limit: 60, // 60 per minute
standardHeaders: 'draft-7',
legacyHeaders: false,
skip: (req, res) => {
return req.path != "/gamble"
}
})

const app = express()

app.use(limiter)
app.use(express.static(path.join(__dirname, 'static')))

async function gamble(number: number) {
return crypto.subtle.digest("SHA-256", Buffer.from(number.toString()))
}

app.post('/gamble', (req, res) => {
const time = req.headers.date ? new Date(req.headers.date) : new Date()
const number = Math.floor(time.getTime() / 1000)
if (isNaN(number)) {
res.send({
success: false,
error: "Bad Date"
}).status(400)
return
}
gamble(number).then(data => {
const bytes = new Uint8Array(data)
if (bytes[0] == 255 && bytes[1] == 255) {
res.send({
success: true,
result: "1111111111111111",
flag: "bctf{fake_flag}"
})
} else {
res.send({
success: true,
result: bytes[0].toString(2).padStart(8, "0") + bytes[1].toString(2).padStart(8, "0")
})
}
})
});

app.listen(6060, () => {
console.log(`Started express server`);
});

Solution

检查 /gamble 路由的逻辑发现服务端会检查请求头中的 Date 字段。如果存在,则将其解析为时间戳;否则使用当前时间。时间戳会被转换为秒级时间(Math.floor(time.getTime() / 1000)),然后将时间戳传递给 gamble 函数,该函数计算时间戳的 SHA-256 哈希值。如果前两个字节的值分别为 255255(即二进制表示为 1111111111111111),服务器会就返回 flag ,否则服务器会返回前两个字节的二进制表示。

因此我们要做的就是伪造一个 Date 请求头,使得时间戳经过 SHA-256 哈希计算后前两个字节恰好是 255255

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 requests
import hashlib
from datetime import datetime

# Step 1: 寻找符合条件的时间戳
def find_valid_timestamp():
# 把上限设置大一点确保有解
for timestamp in range(0, 9999999999999999):
# 计算时间戳的 SHA-256 哈希值
sha256_hash = hashlib.sha256(str(timestamp).encode()).digest()
# 检查是否符合条件
if sha256_hash[0] == 0xFF and sha256_hash[1] == 0xFF:
return timestamp
return None

# Step 2: 发送请求
def send_forged_request(timestamp):
url = "https://when.atreides.b01lersc.tf/gamble"
# 把时间戳转换日期格式
date_header = datetime.utcfromtimestamp(timestamp).strftime('%a, %d %b %Y %H:%M:%S GMT')
headers = {"Date": date_header}
return requests.post(url, headers=headers).json()

if __name__ == "__main__":
valid_timestamp = find_valid_timestamp()

if valid_timestamp:
print(send_forged_request(valid_timestamp))
else:
print("没找到符合条件的时间戳")

得到以下响应

1
{'success': True, 'result': '1111111111111111', 'flag': 'bctf{ninety_nine_percent_of_gamblers_gamble_81dc9bdb}'}
1
bctf{ninety_nine_percent_of_gamblers_gamble_81dc9bdb}

trouble at the spa

Challenge

web/trouble at the spa

author: ky28059

I had this million-dollar app idea the other day, but I can’t get my routing to work! I’m only using state-of-the-art tools and frameworks, so that can’t be the problem… right? Can you navigate me to the endpoint of my dreams?

https://ky28060.github.io/

trouble_at_the_spa.zip

Solution

关键代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { BrowserRouter, Routes, Route } from 'react-router';

// Pages
import App from './App.tsx';
import Flag from './Flag.tsx';

import './index.css';


createRoot(document.getElementById('root')!).render(
<StrictMode>
<BrowserRouter>
<Routes>
<Route index element={<App />} />
<Route path="/flag" element={<Flag />} />
</Routes>
</BrowserRouter>
</StrictMode>
);

通过浏览器扩展(Tampermonkey)注入脚本,强制触发路由跳转

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// ==UserScript==
// @name Force React Router Navigation
// @author Aristore
// @match https://ky28060.github.io/*
// ==/UserScript==

(function() {
'use strict';

// 模拟访问 /flag
window.history.pushState({}, '', '/flag');

// 触发 React Router 的路由更新
const event = new Event('popstate');
window.dispatchEvent(event);
})();

然后直接访问 https://ky28060.github.io/ 就会跳转到 https://ky28060.github.io/flag

b01lersCTF2025-1

1
bctf{r3wr1t1ng_h1st0ry_1b07a3768fc}

Reverse

class-struggle

Challenge

rev/class-strugglebeginner

author: CygnusX & ky28059

I miss the good old days before OOP, when we lived in a classless, stateless society…

marx.c

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
#include <stdio.h>
#include <string.h>

#define The
#define history
#define of
#define all
#define hitherto struct tbnlw{int srlpn;}tbnlw_o;
#define existing unsigned char tfkysf(unsigned char j,int
#define society
#define is vfluhzftxror){vfluhzftxror&=7;
#define the
#define class
#define struggles (void)tbnlw_o
#define Freeman srlpn;
#define and
#define slave (void)((void)0
#define patrician 0);
#define plebeian (void)((void)0
#define lord 0);
#define serf (void)((void)0
#define guild
#define master 0);
#define journeyman (void)((void)0
#define in
#define a
#define word 0);(void)((void)0
#define oppressor 0);
#define oppressed (void)((void)0
#define stood 0);
#define constant return (j<<vfluhzftxror)|
#define opposition (j>>(8-vfluhzftxror));}
#define to unsigned char b(unsigned
#define one char j,int vfluhzftxror){
#define another (void)((void)0
#define carried 0);
#define on vfluhzftxror&=7;return(j
#define an >>vfluhzftxror)|(j<<(8-vfluhzftxror));
#define uninterrupted (void)((void)0
#define now
#define hidden 0);(void)((void)0
#define open 0);(void)((void)0
#define fight
#define that
#define each 0);}
#define time unsigned char jistcuazjdma(unsigned char jistcuazjdma,int mpnvtqeqmsgc){
#define ended (void)((void)0
#define either 0);
#define revolutionary jistcuazjdma^=(
#define reconstitution mpnvtqeqmsgc *
#define at 37);
#define large (void)((void)0
#define or 0);
#define common jistcuazjdma=
#define ruin tfkysf(jistcuazjdma,(mpnvtqeqmsgc+3)%7);
#define contending (void)tbnlw_o
#define classes
#define In
#define earlier srlpn;
#define epochs (void)((void)0
#define we
#define find 0);
#define almost
#define everywhere jistcuazjdma+=42;
#define complicated return jistcuazjdma;}int
#define arrangement evhmllcbyoqu(const
#define into char
#define various *g){
#define orders (void)((void)0
#define manifold 0);
#define gradation const unsigned char gnmupmhiaosg[]={0x32,0xc0,0xbf,0x6c,0x61,0x85,0x5c,0xe4,0x40,0xd0,0x8f,0xa2,0xef,0x7c,0x4a,0x2,0x4,0x9f,0x37,0x18,0x68,0x97,0x39,0x33,0xbe,0xf1,0x20,0xf1,0x40,0x83,0x6,0x7e,0xf1,0x46,0xa6,0x47,0xfe,0xc3,0xc8,0x67,0x4,0x4d,0xba,0x10,0x9b,0x33};int
#define social guxytjuvaljn=strlen(g);
#define rank (void)tbnlw_o
#define ancient srlpn;
#define Rome if(guxytjuvaljn!=sizeof
#define have (gnmupmhiaosg)){
#define patricians (void)((void)0
#define knights 0);(void)((void)0
#define plebeians 0);(void)((void)0
#define slaves 0);(void)(0?0
#define Middle 0);
#define Ages (void)((void)0
#define feudal
#define lords 0);(void)((void)0
#define vassals 0);(void)((void)0
#define masters 0);(void)((void)0
#define journeymen 0);(void)((void)0
#define apprentices 0);(void)((void)0
#define serfs 0);(void)(0?0
#define these 0);(void)((void)0
#define again 0);(void)((void)0
#define subordinate 0);
#define gradations (void)tbnlw_o
#define modern srlpn;
#define bourgeois return 0;}for(int mpnvtqeqmsgc=0;mpnvtqeqmsgc<guxytjuvaljn;mpnvtqeqmsgc++){
#define has
#define sprouted unsigned char z=jistcuazjdma(g[mpnvtqeqmsgc],
#define from mpnvtqeqmsgc);unsigned
#define ruins char e=b((z&0xF0)|((~z)&0x0F),
#define not mpnvtqeqmsgc%8);if(e!=gnmupmhiaosg
#define done [mpnvtqeqmsgc]){
#define away return 0;}}
#define with return 1;
#define antagonisms (void)tbnlw_o
#define It srlpn;
#define but }int main(void){
#define established (void)((void)0
#define new
#define conditions 0);
#define oppression (void)((void)0
#define forms 0);
#define struggle char cmdcnwrnjxlp[64];printf("Please input the flag: ");fgets(cmdcnwrnjxlp,sizeof(cmdcnwrnjxlp),stdin);
#define place char*nl=strchr(cmdcnwrnjxlp,'\n');if(nl){*nl=0;}if(evhmllcbyoqu(cmdcnwrnjxlp)){puts("Correct!");}else{
#define old puts("No.");}
#define ones return 0;}

The history of all hitherto existing society is the history of class struggles.

Freeman and slave, patrician and plebeian, lord and serf, guild-master and
journeyman, in a word, oppressor and oppressed, stood in constant opposition
to one another, carried on an uninterrupted, now hidden, now open fight, a
fight that each time ended, either in a revolutionary reconstitution of society
at large, or in the common ruin of the contending classes.

In the earlier epochs of history, we find almost everywhere a complicated
arrangement of society into various orders, a manifold gradation of social
rank. In ancient Rome we have patricians, knights, plebeians, slaves: in the
Middle Ages, feudal lords, vassals, guild-masters, journeymen, apprentices,
serfs: in almost all of these classes, again, subordinate gradations.

The modern bourgeois society that has sprouted from the ruins of feudal society
has not done away with class antagonisms. It has but established new classes,
new conditions of oppression, new forms of struggle in place of the old ones

Solution

ChatGPT 一把梭

这道题通过一系列宏定义把「输入字符串 g [i]」经过三步变换后,与给定的字节数组比较。我们要做的是把这三步变换反过来,从目标数组还原出原始的 g [i]。

一、理解正向变换

对于每个索引 i 和字符 j = g[i],正向变换分三步:

  1. 异或与旋转

    1
    2
    j ^= (i * 37);                // XOR 运算,结果截断到 8 位
    j = rol(j, (i + 3) % 7); // 向左循环移位 r = (i+3)%7
  2. 加常数 42

    1
    j = (j + 42) & 0xFF;          // 加法并截断到 8 位
  3. 提取高低 4 位后再旋转

    1
    2
    x = (j & 0xF0) | ((~j) & 0x0F);  // 高 4 位保留,低 4 位取 ~j 的低 4 位
    e = ror(x, i % 8); // 向右循环移位 r = i%8,得到最终字节

    最终的 e 就存储在给定数组中。

二、反向还原算法

  1. 逆向第二次旋转

    1
    2
    # 已知 e 和 r2=i%8
    x = rol(e, r2) # 旋转方向相反:向左旋转 r2 位
  2. 逆向高低位提取
    x = (z & 0xF0) | ((~z) & 0x0F),可解得

    1
    2
    3
    4
    z_high = x & 0xF0
    x_low = x & 0x0F
    z_low = 0xF - x_low # 因为 (~z_low)&0x0F = x_low ⇒ z_low = 0xF - x_low
    z = z_high | z_low
  3. 逆向加 42

    1
    t1 = (z - 42) & 0xFF
  4. 逆向第一次旋转

    1
    2
    r1 = (i + 3) % 7
    t2 = ror(t1, r1) # 向右循环移位的逆运算
  5. 逆向异或

    1
    j = t2 ^ ((i * 37) & 0xFF)

    得到的 j 就是当时的 g[i]

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
# 8 位循环移位函数
def rol(v, r): return ((v << r) & 0xFF) | (v >> (8 - r))
def ror(v, r): return (v >> r) | ((v << (8 - r)) & 0xFF)

# 给定的目标数组
cipher = [
0x32,0xc0,0xbf,0x6c,0x61,0x85,0x5c,0xe4,
0x40,0xd0,0x8f,0xa2,0xef,0x7c,0x4a,0x02,
0x04,0x9f,0x37,0x18,0x68,0x97,0x39,0x33,
0xbe,0xf1,0x20,0xf1,0x40,0x83,0x06,0x7e,
0xf1,0x46,0xa6,0x47,0xfe,0xc3,0xc8,0x67,
0x04,0x4d,0xba,0x10,0x9b,0x33
]

flag_bytes = []
for i, e in enumerate(cipher):
# 1. 逆向第二次循环右移
x = rol(e, i % 8)
# 2. 逆向高低位混合
z = (x & 0xF0) | (0xF - (x & 0x0F))
# 3. 逆向加 42
t1 = (z - 42) & 0xFF
# 4. 逆向第一次循环左移
t2 = ror(t1, (i + 3) % 7)
# 5. 逆向异或
j = t2 ^ ((i * 37) & 0xFF)
flag_bytes.append(j)

flag = bytes(flag_bytes).decode('ascii')
print(flag)

运行后即可得到 flag

1
bctf{seizing_the_m3m3s_0f_pr0ducti0n_32187ea8}