PHP 安全编程建议

澳门新葡亰赌995577 6

前言

PHP是一种通用的开源脚本语言,它的语法混合了C,Java,以及Perl等优秀语言的语法。除此之外,它还提供了大量的函数库可供开发人员使用。但是,如果使用不当,PHP也会给应用程序带来非常大的安全风险。

在这篇文章中,我们将会对PHP应用程序中经常会出现的一些问题进行深入地分析,尤其是当我们使用“==”(比较运算符)来进行字符串比较时,可能会出现的一些安全问题。虽然近期有很多文章都围绕着这一话题进行过一些探讨,但我决定从“黑盒测试”的角度出发,讨论一下如何利用这个问题来对目标进行渗透和攻击。首先,我会对引起这个问题的根本原因进行分析,以便我们能够更加深入地理解其工作机制,这样才可以保证我们能够尽可能地避免这种安全问题的发生。

简介

要提供互联网服务,当你在开发代码的时候必须时刻保持安全意识。可能大部分
PHP 脚本都对安全问题都不在意,这很大程度上是因为有大量的无经验程序员在使用这门语言。但是,没有理由让你因为对你的代码的不确定性而导致不一致的安全策略。当你在服务器上放任何涉及到钱的东西时,就有可能会有人尝试破解它。创建一个论坛程序或者任何形式的购物车,被攻击的可能性就上升到了无穷大。

澳门新葡亰赌995577 1

问题的描述

在2011年,PHP官方漏洞追踪系统发现,当字符串与数字在进行比较的时候,程序会出现某些非常奇怪的现象。从安全的角度出发,这个问题实际上并不能算是一个安全问题。比如说,你可以看到下面这段代码:

澳门新葡亰赌995577 2

实际上,当使用类似“==”这样的比较运算符进行操作时,就会出现这样的情况。上面这个例子中出现的问题不能算是一个漏洞,因为它是PHP所提供的一种名为“类型转换”的功能。从本质上来分析,当我们使用特定的比较运算符(例如==
, !=,
<>)来进行操作时,PHP首先会尝试去确定参与比较的数据类型。但是这样的一种类型转换机制将有可能导致计算结果与我们预期的结果有较大出入,而且也会带来非常严重的安全问题。安全研究专家在该问题的完整披露报告中写到:这种类型转化机制将有可能导致权限提升,甚至还会使程序的密码验证过程变得不安全。

Gynvael写过一篇关于这一话题的经典文章,PHP等号运算符“==”所涵盖的数据类型非常广泛,我们给大家提供了一个较为完整的比较参考列表,并给出了一些示例,具体内容如下所示:

澳门新葡亰赌995577 3

正如你所看到的,当我们使用“==”来比较这些数字字符串时,参与比较的就是字符串中数字的实际大小,从安全的角度出发,这就是一个非常有趣的问题了。在这种情况下,你可以使用科学计数法来表示一个数字,并将其放在一个字符串中,PHP将会自动把它作为一个数字类型来处理。我们之所以会得到这样的输出类型,是因为PHP使用了一种哈希算法(通常使用十六进制数值表示)来进行处理。比如说,如果一个数字为0,那么在进行松散比较的过程中,PHP会自动对其类型进行转换,但其值永远为0。对于一个给定的散列算法而言,密码就有可能会变成可以被替换的了。比如说,当密码的哈希值被转换成使用科学计数法来表示的数字时,将有可能正好与其他的密码哈希相匹配。这样一来,即使是一个完全不同的密码,也有可能可以通过系统的验证。但有趣的是,当某些采用科学计数法表示的数字在进行比较的时候,结果可能会让你意想不到:

澳门新葡亰赌995577 4

背景

为了确保你的 web 内容安全,这里有一些常规的安全准则:

从“黑盒测试”的角度出发来考虑这个问题

从静态分析的角度来看,这些安全问题就显得有些普通了。但如果我们从黑盒的角度来看待这些问题,我们能够得到什么样的启发呢?对于应用程序中的任何用户账号而言,如果应用程序使用了当前最为流行的哈希散列算法(例如SHA1和MD5)来对密码进行处理,而你在对密码哈希进行验证的时候使用了PHP的松散比较,那么此时就有可能出现安全问题。我们现在可以考虑进行一次典型的渗透测试,你可以创建一个普通的账号,将密码设置成哈希值类似的其中一个密码,然后使用其他的密码进行登录操作。很明显,系统的安全性完全取决于你所使用的散列算法。所以,我们假设你没有在散列算法中使用“Salt”值,那么你至少得使用两种不同的散列算法来对密码进行处理。

现在,在我们去对这些密码组合进行研究之前,我们还应该考虑到一点——即密码的要求。因为我们在对这些密码和散列算法进行分析之前,首先得确保我们所设置的初始密码复合了密码复杂度的要求,否则我们的分析和研究将会没有任何的意义。因此,我们得确保我们的密码长度至少为八个字符,密码中包含有大小写字母,数字,以及至少一个特殊字符:具体如下所示:

import random
import hashlib
import re
import string
import sys
prof = re.compile("^0+ed*$") # you can also consider: re.compile("^d*e0+$")
prefix = string.lower(sys.argv[1])+'!'+string.upper(sys.argv[1])+"%s"
num=0
while True:
    num+=1
    b = hashlib.sha256(prefix % num).hexdigest()
    if (b[0]=='0' and prof.match(b)):
        print(prefix+str(num),b)

为此,我专门编写了一个Python脚本,虽然我没有竭尽全力去优化这个脚本的性能,但是在PyPy编译器的帮助下,这个精心编写的脚本可以在我的AMD
FX8350所有可用的CPU核心中稳定运行。除此之外,我还使用到了hashlib库中的散列函数,而且为了避免遇到Python
GIL的进程同步问题,我还生成了独立的进程来对密码数据进行处理。不仅如此,我还使用了非常复杂的技术来为每一个密码生成不同的前缀,正如上面这段代码所示。

别相信表单

攻击表单很简单。通过使用一个简单的 JavaScript
技巧,你可以限制你的表单只允许在评分域中填写 1 到 5
的数字。如果有人关闭了他们浏览器的 JavaScript
功能或者提交自定义的表单数据,你客户端的验证就失败了。

用户主要通过表单参数和你的脚本交互,因此他们是最大的安全风险。你应该学到什么呢?在
PHP 脚本中,总是要验证 传递给任何 PHP
脚本的数据。在本文中,我们向你演示了如何分析和防范跨站脚本(XSS)攻击,它可能会劫持用户凭据(甚至更严重)。你也会看到如何防止会玷污或毁坏你数据的
MySQL 注入攻击。

分析结果

在经过了一个多小时的分析之后,我得到了四个密码的SHA1值。令我感到惊讶的是,得到四个密码的MD5值所需的时间竟然更短。

密码的计算结果十分相似,具体如下所示:

澳门新葡亰赌995577 5

你可以随意选取两个密码来进行对比,对比的演示结果如下:

澳门新葡亰赌995577 6

如果你无法得到如上图所示的计算结果,那么你应该感到幸运。你可以尝试将用户名和密码捆绑在一起,然后使用带“salt”值的散列算法来进行计算。你只需要修改一小部分代码即可实现,点击“这里”获取修改后的脚本。

别相信用户

假定你网站获取的每一份数据都充满了有害的代码。清理每一部分,即便你相信没有人会尝试攻击你的站点。

解决方案

PHP给我们提供了一个解决方案,如果你想要对比哈希值,你应该使用password_verify()或hash_equals()这两个函数。它们会对数据进行严格比较,并排除一些其他的干扰因素。但是请你注意,hash_equals()函数也可以用于字符串的比较。

关闭全局变量

你可能会有的最大安全漏洞是启用了 register_globals
配置参数。幸运的是,PHP 4.2 及以后版本默认关闭了这个配置。如果打开了
register_globals,你可以在你的 php.ini 文件中通过改变
register_globals 变量为 Off 关闭该功能:

register_globals = Off

新手程序员觉得注册全局变量很方便,但他们不会意识到这个设置有多么危险。一个启用了全局变量的服务器会自动为全局变量赋任何形式的参数。为了了解它如何工作以及为什么有危险,让我们来看一个例子。

假设你有一个称为 process.php
的脚本,它会向你的数据库插入表单数据。初始的表单像下面这样:

<input name="username" type="text" size="15" maxlength="64">

运行 process.php 的时候,启用了注册全局变量的 PHP 会将该参数赋值到
$username 变量。这会比通过 $_POST[‘username’]
$_GET[‘username’]
访问它节省击键次数。不幸的是,这也会给你留下安全问题,因为 PHP
会设置该变量的值为通过 GET 或 POST
的参数发送到脚本的任何值,如果你没有显示地初始化该变量并且你不希望任何人去操作它,这就会有一个大问题。

看下面的脚本,假如 $authorized 变量的值为
true,它会给用户显示通过验证的数据。正常情况下,只有当用户正确通过了这个假想的
authenticated_user() 函数验证,$authorized
变量的值才会被设置为真。但是如果你启用了
register_globals,任何人都可以发送一个 GET 参数,例如 authorized=1
去覆盖它:

<?php
// Define $authorized = true only if user is authenticated
if (authenticated_user()) {
    $authorized = true;
}
?>

这个故事的寓意是,你应该从预定义的服务器变量中获取表单数据。所有通过
post 表单传递到你 web 页面的数据都会自动保存到一个称为 $_POST
的大数组中,所有的 GET 数据都保存在 $_GET
大数组中。文件上传信息保存在一个称为 $_FILES
的特殊数据中。另外,还有一个称为 $_REQUEST 的复合变量。

要从一个 POST 方法表单中访问 username 字段,可以使用
$_POST[‘username’]。如果 username 在 URL 中就使用
$_GET[‘username’]。如果你不确定值来自哪里,用
$_REQUEST[‘username’]

<?php
$post_value = $_POST['post_value'];
$get_value = $_GET['get_value'];
$some_variable = $_REQUEST['some_value']; 
?>

$_REQUEST 是 $_GET、$_POST、和 $_COOKIE
数组的结合。如果你有两个或多个值有相同的参数名称,注意 PHP
会使用哪个。默认的顺序是 cookie、POST、然后是 GET。

分析结论

虽然我们的分析步骤执行起来有些过于复杂,但是从黑盒测试的角度出发,我们所描述的方法也许可以给大家提供一些有价值的信息。如果某个应用程序中的密码采用了这样的一种验证机制,那么它所带来的安全问题将会超出PHP数据类型转换本身所存在的问题。

推荐安全配置选项

这里有几个会影响安全功能的 PHP
配置设置。下面是一些显然应该用于生产服务器的:

  • register_globals 设置为 off
  • safe_mode 设置为 off
  • error_reporting 设置为
    off。如果出现错误了,这会向用户浏览器发送可见的错误报告信息。对于生产服务器,使用错误日志代替。开发服务器如果在防火墙后面就可以启用错误日志。(LCTT
    译注:此处据原文逻辑和常识,应该是“开发服务器如果在防火墙后面就可以启用错误报告,即
    on。”)
  • 停用这些函数:system()、exec()、passthru()、shell_exec()、proc_open()、和
    popen()。
  • open_basedir 为 /tmp(以便保存会话信息)目录和 web
    根目录,以便脚本不能访问这些选定区域外的文件。
  • expose_php 设置为 off。该功能会向 Apache 头添加包含版本号的 PHP
    签名。
  • allow_澳门新葡亰赌995577,url_fopen 设置为
    off。如果你能够注意你代码中访问文件的方式-也就是你验证所有输入参数,这并不严格需要。
  • allow_url_include 设置为
    off。对于任何人来说,实在没有明智的理由会想要访问通过 HTTP
    包含的文件。

一般来说,如果你发现想要使用这些功能的代码,你就不应该相信它。尤其要小心会使用类似
system() 函数的代码-它几乎肯定有缺陷。

启用了这些设置后,让我们来看看一些特定的攻击以及能帮助你保护你服务器的方法。

问题远不止于此

这个问题给我们带来的影响远远不止于此。攻击者可以将这些密码添加到字典文件中,然后对应用程序中的所有用户进行暴力破解攻击。而且,如果应用程序的密码恢复机制中存在不安全的因素,攻击者还有可能对目标账号进行不限次数的攻击,直到攻击成功为止。

SQL 注入攻击

由于 PHP 传递到 MySQL 数据库的查询语句是用强大的 SQL
编程语言编写的,就有了某些人通过在 web 查询参数中使用 MySQL 语句尝试 SQL
注入攻击的风险。通过在参数中插入有害的 SQL
代码片段,攻击者会尝试进入(或破坏)你的服务器。

假如说你有一个最终会放入变量 $product 的表单参数,你使用了类似下面的 SQL
语句:

$sql = "select * from pinfo where product = '$product'";

如果参数是直接从表单中获得的,应该使用 PHP
自带的数据库特定转义函数,类似:

$sql = 'Select * from pinfo where product = '"' 
       mysql_real_escape_string($product) . '"';

如果不这样做的话,有人也许会把下面的代码段放到表单参数中:

39'; DROP pinfo; SELECT 'FOO

那么 $sql 的结果就是:

select product from pinfo where product = '39'; DROP pinfo; SELECT 'FOO'

由于分号是 MySQL 的语句分隔符,数据库会运行下面三条语句:

select * from pinfo where product = '39'
DROP pinfo
SELECT 'FOO'

好了,你丢失了你的表。

注意实际上 PHP 和 MySQL 不会运行这种特殊语法,因为 mysql_query()
函数只允许每个请求处理一个语句。但是,一个子查询仍然会生效。

要防止 SQL 注入攻击,做这两件事:

  • 总是验证所有参数。例如,如果需要一个数字,就要确保它是一个数字。
  • 总是对数据使用 mysql_real_escape_string()
    函数转义数据中的任何引号和双引号。

注意:要自动转义任何表单数据,可以启用魔术引号(Magic
Quotes)。

一些 MySQL 破坏可以通过限制 MySQL 用户权限避免。任何 MySQL
账户可以限制为只允许对选定的表进行特定类型的查询。例如,你可以创建只能选择行的
MySQL
用户。但是,这对于动态数据并不十分有用,另外,如果你有敏感的用户信息,可能某些人能访问其中一些数据,但你并不希望如此。例如,一个访问账户数据的用户可能会尝试注入访问另一个人的账户号码的代码,而不是为当前会话指定的号码。

源代码

点击“这里”获取Python源码。

防止基本的 XSS 攻击

XSS 表示跨站脚本。不像大部分攻击,该漏洞发生在客户端。XSS
最常见的基本形式是在用户提交的内容中放入 JavaScript 以便偷取用户 cookie
中的数据。由于大部分站点使用 cookie 和 session
验证访客,偷取的数据可用于模拟该用户-如果是一个常见的用户账户就会深受麻烦,如果是管理员账户甚至是彻底的惨败。如果你不在站点中使用
cookie 和 session
ID,你的用户就不容易被攻击,但你仍然应该明白这种攻击是如何工作的。

不像 MySQL 注入攻击,XSS 攻击很难预防。Yahoo、eBay、Apple、以及
Microsoft 都曾经受 XSS 影响。尽管攻击不包含 PHP,但你可以使用 PHP
来剥离用户数据以防止攻击。为了防止 XSS
攻击,你应该限制和过滤用户提交给你站点的数据。正是因为这个原因,大部分在线公告板都不允许在提交的数据中使用
HTML 标签,而是用自定义的标签格式代替,例如 [b]
[linkto]

让我们来看一个如何防止这类攻击的简单脚本。对于更完善的解决办法,可以使用
SafeHTML,本文的后面部分会讨论到。

function transform_HTML($string, $length = null) {
// Helps prevent XSS attacks
    // Remove dead space.
    $string = trim($string);
    // Prevent potential Unicode codec problems.
    $string = utf8_decode($string);
    // HTMLize HTML-specific characters.
    $string = htmlentities($string, ENT_NOQUOTES);
    $string = str_replace("#", "&#35;", $string);
    $string = str_replace("%", "&#37;", $string);
    $length = intval($length);
    if ($length > 0) {
        $string = substr($string, 0, $length);
    }
    return $string;
}

这个函数将 HTML 特定的字符转换为 HTML
字面字符。一个浏览器对任何通过这个脚本的 HTML
以非标记的文本呈现。例如,考虑下面的 HTML 字符串:

<STRONG>Bold Text</STRONG>

一般情况下,HTML 会显示为:Bold Text

但是,通过 transform_HTML()
后,它就像原始输入一样呈现。原因是处理的字符串中的标签字符串转换为 HTML
实体。transform_HTML() 的结果字符串的纯文本看起来像下面这样:

<STRONG>Bold Text</STRONG>

该函数的实质是 htmlentities() 函数调用,它会将 <、>、和 & 转换为
<>、和
&。尽管这会处理大部分的普通攻击,但有经验的 XSS
攻击者有另一种把戏:用十六进制或 UTF-8 编码恶意脚本,而不是采用普通的
ASCII 文本,从而希望能绕过你的过滤器。他们可以在 URL 的 GET
变量中发送代码,告诉浏览器,“这是十六进制代码,你能帮我运行吗?”
一个十六进制例子看起来像这样:

<a href="http://host/a.php?variable=%22%3e%20%3c%53%43%52%49%50%54%3e%44%6f%73%6f%6d%65%74%68%69%6e%67%6d%61%6c%69%63%69%6f%75%73%3c%2f%53%43%52%49%50%54%3e">

浏览器渲染这个信息的时候,结果就是:

<a href="http://host/a.php?variable="> <SCRIPT>Dosomethingmalicious</SCRIPT>

为了防止这种情况,transform_HTML() 采用额外的步骤把 # 和 %
符号转换为它们的实体,从而避免十六进制攻击,并转换 UTF-8 编码的数据。

最后,为了防止某些人用很长的输入超载字符串从而导致某些东西崩溃,你可以添加一个可选的
$length 参数来截取你指定最大长度的字符串。

使用 SafeHTML

之前脚本的问题比较简单,它不允许任何类型的用户标记。不幸的是,这里有上百种方法能使
JavaScript 跳过用户的过滤器,并且要从用户输入中剥离全部
HTML,还没有方法可以防止这种情况。

当前,没有任何一个脚本能保证无法被破解,尽管有一些确实比大部分要好。有白名单和黑名单两种方法加固安全,白名单比较简单而且更加有效。

一个白名单解决方案是 PixelApes 的 SafeHTML 反跨站脚本解析器。

SafeHTML 能识别有效 HTML,能追踪并剥离任何危险标签。它用另一个称为
HTMLSax 的软件包进行解析。

按照下面步骤安装和使用 SafeHTML:

  1. 到 下载最新版本的
    SafeHTML。
  2. 把文件放到你服务器的类文件夹。该文件夹包括 SafeHTML 和 HTMLSax
    功能所需的所有东西。
  3. 在脚本中 include SafeHTML 类文件(safehtml.php)。
  4. 创建一个名为 $safehtml 的新 SafeHTML 对象。
  5. 用 $safehtml->parse() 方法清理你的数据。

这是一个完整的例子:

<?php
/* If you're storing the HTMLSax3.php in the /classes directory, along
   with the safehtml.php script, define XML_HTMLSAX3 as a null string. */
define(XML_HTMLSAX3, '');
// Include the class file.
require_once('classes/safehtml.php');
// Define some sample bad code.
$data = "This data would raise an alert <script>alert('XSS Attack')</script>";
// Create a safehtml object.
$safehtml = new safehtml();
// Parse and sanitize the data.
$safe_data = $safehtml->parse($data);
// Display result.
echo 'The sanitized data is <br />' . $safe_data;
?>

如果你想清理脚本中的任何其它数据,你不需要创建一个新的对象;在你的整个脚本中只需要使用
$safehtml->parse() 方法。

发表评论

电子邮件地址不会被公开。 必填项已用*标注

相关文章

网站地图xml地图