PHP代码审计-代码执行与命令执行

0x00 代码执行与命令执行的区别

  • 代码执行是指用户通过客户端提交可执行命令的代码,由于服务器没有对其函数的参数做有效过滤,导致系统执行了非法命令代码

  • 命令执行是指被攻击者利用从而在系统中执行任意指令的漏洞,通过调用操作系统命令(相当于ipconfig等执行命令)

0x01 代码执行

代码执行漏洞产生的原因,在系统设计和开发过程中,为了给用户带来各种各样的功能,以及业务的便利性,并没有注意程序代码所用的函数,实现的功能、执行的流程等方面是否安全,所以产生的原因就是,没有对整体的架构,纳入安全考虑,才导致的代码执行。

1.代码执行函数

代码执行,就是通过参数、变量等制定可以执行的代码,参数和变量中,不是数字、字符串等,而是恶意的代码,从而导致了代码执行。在PHP语言和框架中,有很多代码执行函数,如下

动态函数,这些函数的参数都是用户可控的,如果没有进行有效的过滤,就会触发代码执行

函数 含义
eval() 把字符串作为PHP代码执行
assert() 检查一个断言是否为 false
preg_replace() 执行一个正则表达式的搜索和替换
call_user_func() 把第一个参数作为回调函数调用
call_user_func_array() 调用回调函数,并把一个数组参数作为回调函数的参数
create_function() 创建匿名(lambda 风格)函数
array_map() 为数组的每个元素应用回调函数

通过文件包含函数,代码执行

函数 含义
include() 包含多次
include_once() 包含一次
require() 包含多次
require_once() 包含一次
file_get_contents() 将整个文件读入一个字符串
file_put_contents() 将一个字符串写入文件
fwrite() 写入文件(可安全用于二进制文件)

2.preg_replace

1
2
3
4
5
6
7
preg_replace(
string|array $pattern,
string|array $replacement,
string|array $subject,
int $limit = -1,
int &$count = null
): string|array|null

第一个参数是要搜索的模式,可以是字符串或一个字符串数组

第二个参数是用于替换的字符串或字符串数组

第三个参数是要搜索替换的目标字符串或字符串数组

漏洞成因

当第一个参数 patern 存在 /e 模式修饰符且 PHP 配置中的magic_quotes_gpc=Off时,函数会将其第二个参数值当做 PHP 代码进行解析执行,该函数的三个参数都是用户可控的,因此,是产生代码执行漏洞的关键。

参数利用

第一个参数

对PHP5.6及之前的框架,新建一个 test1.php 脚本

1
2
3
<?php
preg_replace("/<php>(.*?)".$_GET['reg'], '\\1', '<php>phpinfo()</php>');
?>

访问 URL 地址触发了该代码执行漏洞

1
http://127.0.0.1/test1.php?reg=%3C\/php%3E/e

image-20211223184634876

第二个参数利用

新建 test2.php

1
2
3
<?php
preg_replace("/anyingv/e", $_GET['test'], 'anyingv_test');
?>

访问 URL

1
http://127.0.0.1/test2.php?test=phpinfo()

image-20211223185528777

第三个参数

新建 test3.php

1
2
3
<?php
preg_replace("/<php>(.*?)<\/php>/e", '\\1', $_GET['test']);
?>

访问URL

1
http://127.0.0.1/test3.php?test=<php>phpinfo()</php>

image-20211223190010801

2.call_user_func

1
call_user_func(callable $callback, mixed $parameter = ?, mixed $... = ?): mixed

第一个参数 callback 是被调用的回调函数,其余参数是回调函数的参数。

1
2
3
4
<?php
$b = "phpinfo()";
call_user_func($_GET['test'], $b)
?>

访问 URL

image-20211223190511841

3.array_map()

1
array_map(callable $callback, array $array, array ...$arrays): array

第一个参数是回调函数,第二个参数是要处理的数组

新建test5.php

1
2
3
<?php
array_map($_GET['test'], array(0,1,2));
?>

访问 URL

image-20211223191049749

4.动态函数 $a($b) 与 assert()

动态函数$a($b) 的功能是将 $a 替换为 $_GET['a'],将$b 替换为 $_GET['b'],动态函数在不当使用时造成的代码执行漏洞。

新建 test6.php

1
2
3
<?php
$_GET['a']($_GET['b']);
?>

image-20211223191637056

assert() 函数判断一个表达式是否成立,返回 true 或 false,当其参数为多个字符组成的字符串时,该函数首先将字符串当做PHP代码执行,并将代码执行返回的结果作为表达式判断是否有效。

新建 test7.php

1
2
3
<?php
assert(phpinfo());
?>

image-20211223191927853

直接触发嵌入的phpinfo()代码

5.反序列化代码执行与eval()

在反序列化结束时,如果未对用户输入的序列化字符串进行检测,导致攻击者可以控制反序列化过程,从而导致代码执行,目录遍历,SQL注入等不可控的后果,因为语言的特征,在反序列化过程中有可能会触发对象中的魔术方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
__construct()//类的构造函数
__destruct() //类的析构函数
__call() //在对象中调用一个不可访问方法时调用
__callStatic() //用静态方式中调用一个不可访问方法时调用
__get() //获得一个类的成员变量时调用
__set() //设置一个类的成员变量时调用
__isset() //当对不可访问属性调用isset()或empty()时调用
__unset() //当对不可访问属性调用unset()时被调用。
__sleep() //执行serialize()时,先会调用这个函数
__wakeup() //执行unserialize()时,先会调用这个函数
__toString() //类被当成字符串时的回应方法
__invoke() //调用函数的方式调用一个对象时的回应方法
__set_state() //调用var_export()导出类时,此静态方法会被调用。
__clone() //当对象复制完成时调用
__autoload() //尝试加载未定义的类
__debugInfo() //打印所需调试信息

eval()函数把参数的字符串值当做 PHP 代码来执行,当字符串是合法的 PHP 代码且以分号作为行结尾时,代码在服务端的运行效果与通过该函数触发执行效果相同。

1
2
3
4
5
6
<?php
$a='abc';
$b='cdf';
eval('$a=$b;');
echo $a;
?>

最终结果为 cdf

触发反序列化漏洞,需要具备两个条件

  • unserialize() 函数的参数可控
  • PHP 代码文件中存在可利用的类,类中有魔术方法

新建 test8.php

1
2
3
4
5
6
7
8
9
10
<?php
class Test{
public $var='';
function __destruct()
{
eval($this->var);
}
}
unserialize($_GET['test']);
?>

创建payload.php

先访问 payload页面,获取payload,然后将payload输入到 test中触发了反序列化漏洞

image-20211223194733034

0x02 命令执行

命令执行漏洞,因为没有对用户传入或构造的参数进行有效的安全检测,从而执行了参数中嵌入的系统操作命令或调用了触发系统命令的函数,是的攻击者可以在目标系统中不受限制地执行系统命令,如 关机执行,查询IP,删除,移动,等常见的dos或者Linux命令。

1.命令执行函数

详情可参考 https://www.php.net/manual/zh/

命令 含义
exec() 执行一个外部程序
system() 执行外部程序,并且显示输出
shell_exec() 通过 shell 环境执行命令,并且将完整的输出以字符串的方式返回
passthru() 执行外部程序并且显示原始输出
popen() 打开进程文件指针
proc_open() 执行一个命令,并且打开用来输入/输出的文件指针
pcntl_exec() 在当前进程空间执行指定程序

2.管道符

命令执行漏洞需要利用操作系统提供的管道符功能,管道符的意思就是,将前一个命令的输出,作为后一个命令的输入。

windows 和 Linux 环境下的符号相同,含义不相同

命令 windows Linux
, 执行完前面的命令再执行后面的命令
& 前面的命令为假则直接执行后面的命令
为真,则在前面的命令执行后再执行后面的命令
前面的命令为假则直接执行后面的命令
为真,则在前面的命令执行后再执行后面的命令
&& 前面的命令为假则直接出错,后面的命令也不执行;
反之,前面的命令执行成功后才执行后面的命令
前面的命令为假则直接出错,后面的命令也不执行;
反之,前面的命令执行成功后才执行后面的命令
` ` 前面的命令不执行,直接执行后面的命令
` `

3.命令注入绕过

我们在代码审计的时候,要构造payload的,有些情况下,需要绕过一些过滤,具体绕过的方法可以参考这篇文章 命令执行的一些绕过技巧,这里我就列举一些简单的绕过方式。

Linux-空格绕过

1
2
3
4
5
6
7
8
9
$IFS
$IFS$1
${IFS}
$IFS$9
< 比如cat<a.tct:表示cat a.txt
<>
{cat,flag.php} //用逗号实现了空格功能,需要用{}括起来
%20
%09

Linux-常用字符绕过

1
2
3
4
5
6
7
& 表示将任务置于后台执行
; 多行语句用换行区分代码快,单行语句一般要用到分号来区分代码块
&& 只有在 && 左边的命令返回真(命令返回值 $? == 0),&&右边的命令才会被执行。
|| 只有在 || 左边的命令返回假(命令返回值 $? == 1),||右边的命令才会被执行。
%0a
%0d
| (管道符)

windows-set命令

1
2
3
set a=1 
echo a
echo %a%

image-20211223203054319

set 绕过利用

1
2
3
4
set a=who
set b=ami
%a%%b% //正常执行whoami
call %a%%b% //正常执行whoami

image-20211223203028002

windows-逻辑运算符

1
2
3
4
whoami | ping www.baidu.com
whoami || ping www.baidu.com
qwe & ping www.baidu.com
qwe && ping www.baidu.com

4.system()

system(),函数执行参数指定的系统命令,并且输出执行结果

新建test.php

1
2
3
<?php
system('ping 127.0.0.1'.$_REQUEST['test']);
?>

payload

1
http://127.0.0.1/test.php?test=|whoami

image-20211224145438768

5. ``` `

1
2
3
4
5
6

```php
<?php
echo '<pre>'; #格式化语句
echo `ping 127.0.0.1 {$_REQUEST['test']}`;
?>

image-20211224153922432

6.exec()

exec() 函数有两个参数,默认情况下,会返回第一个参数指定的命令运行结果的最后一行,第二个参数有效时,会将返回结果追加到第二个从参数的值后面

新建test2.php

1
2
3
4
5
<?php
echo '<pre>'; #格式化语句
exec('ping '.$_REQUEST['test'], $output);
print_r($output);
?>

image-20211224155751521

%26是&的urlencode 编码

6.shell_exec()

shell_exec() 函数通过shell执行参数指定的命令,输出命令返回结果的完整字符串。

在 Linux 下新建 test.php

1
2
3
4
<?php
echo '<pre>';
print_r(shell_exec("ls".$_REQUEST['test']));
?>

image-20211224161426089

7.passthru()

passthru()函数调用参数指定的命令,把命令运行的结果原样地输出到标准输出设备上

windows下新建 test3.php

1
2
3
<?php
passthru($_REQUEST['test']);
?>

image-20211224172026715

0x03 修复建议

  1. 严格过滤用户输入的数据,禁止执行非预期系统命令。
  2. 减少或不使用代码或命令执行函数
  3. 客户端提交的变量在放入函数前进行检测
  4. 减少或不使用危险函数

总结

在审计代码执行和命令执行时,主要的漏洞点,还是函数本身,我们可以通过搜索的方式来搜索函数,分析代码,看其是否过滤,然后通过函数的特征,进行构造payload 绕过,是否能命令执行/代码执行。

参考

https://www.php.net/manual/zh/index.php

https://www.anquanke.com/post/id/229611#h3-1

https://www.cnblogs.com/iAmSoScArEd/p/10651947.html#auto_id_5