[GXYCTF2019]禁止套娃

张开发
2026/4/21 5:10:15 15 分钟阅读

分享文章

[GXYCTF2019]禁止套娃
BUUOJ Web《禁止套娃》详细解析从 .git 泄露到无参数函数利用拿下 flag一、题目分析引导思路打开题目首页页面上只有一句话flag在哪里呢这类页面的特点是前端几乎没有可操作功能看起来像“什么都没有”。遇到这种题第一反应不要急着猜漏洞而是先做基础观察有没有隐藏参数有没有源码泄露有没有常见敏感文件可以直接访问我先试了几个常见路径结果发现这个地址可以访问/.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内容如下?phpincludeflag.php;echoflag在哪里呢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臭弟弟);}}?先把这段代码按语法读一遍这段代码从上往下看其实不难。includeflag.php;这句的意思是把 flag.php 包含进来执行。也就是说flag.php 里的内容已经被加载到了当前页面里。echoflag在哪里呢br;这句只是往页面输出一句提示。if(isset($_GET[exp])){这里说明题目的输入点是 GET 参数 exp。也就是访问时可以带上/?expxxxx接下来就是三层过滤全部通过后最后才会执行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这里可以理解成替换为空。也就是说这一句的意思其实是用正则去匹配 exp把所有匹配到的部分删掉看最后是不是只剩下一个分号 ;关键在这个正则/[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 里出现以下任意一个片段就拦截etnainfodecbinhexoctpilog这里要注意它拦的不是“完整函数名”而是“字符串片段”。比如phpinfo();会被拦因为里面有 info。所以到这里题目的限制就很清楚了入口是 exp最后会进 eval()不能用常见协议包装器payload 必须是纯“函数嵌套”结构还不能出现若干黑名单关键字那接下来思路就自然出来了既然不能直接写字符串也不能直接写普通表达式那就去找无参数函数先构造出有用的字符串再一步步拿到目标文件。———三、漏洞验证过程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 )这一步说明两件事代码执行已经打通了当前目录下确实有 flag.php5. 先看看反转后第一个文件是谁输入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/?expreadfile(next(array_reverse(scandir(current(localeconv())))));这条链子的意思是localeconv() 返回数组current(localeconv()) 取出 .scandir(.) 列当前目录array_reverse(…) 反转数组next(…) 取反转后第二个元素readfile(…) 读取这个文件而反转后的第二个元素正好就是 flag.php。———五、获取flag访问最终 payload 后页面回显?php$flagflag{601f38c7-d402-4d1d-be36-e65ab5809d08};?所以本题 flag 为flag{601f38c7-d402-4d1d-be36-e65ab5809d08}———六、知识点总结一定要写好这题主要考两个点.git 源码泄露以及受限条件下的 eval 代码执行。.git 泄露的价值很大因为很多题前端信息少真正的漏洞点都藏在源码里。看到首页没东西的时候先去看 .git、备份文件、临时文件往往比盲试 payload 更有效。这题里的 eval 并不是“直接送命令执行”因为它前面加了很多限制。最关键的是第二层正则它不是单纯黑名单而是在限制语法结构只允许“函数套函数”的写法存在。这个思路在 CTF 里很常见出题人不是不让你执行而是逼你换一种执行方式。以后遇到类似题可以这样判断先看源码里有没有 eval、assert、preg_replace 这类危险点再看过滤是在拦“关键字”还是在拦“语法结构”如果不能写引号、数字、变量、普通参数就要考虑无参数函数链如果能拿到目录列表就优先想办法从返回结果里“借字符串”而不是手写文件名这题最值得记住的一点不是某个函数名而是这个思路当 payload 被限制得很死时先别急着想“执行什么命令”而是先想“我还能合法写出什么语法”。

更多文章