PHP安全问题

乌云漏洞平台上发现很多开源PHP程序存在漏洞。大部分漏洞都是因为对PHP中的变量过滤不够严格和未转义造成,直接使用了$_GET,$_PUT,$_COOKIE传过来的值,造成了sql注入(sql injection)和跨站攻击(xss)。事实上这些漏洞完全可以避免的。
先说sql注入。通常注入情况发生在参数过滤不严谨和拼接sql,所以尽量不要用sql拼接,尽量使用PDO或Mysqli。先看下面一段代码(test.php):

$database="test";
$duser="root";
$dpassword="*********";
$server="localhost";
$link=mysql_connect($server,$duser,$dpassword) or die("连接不上服务器:".mysql_error());
mysql_select_db($database,$link) or die("不能选择数据库");

$id=$_GET['id'];
$sql="select id,account,nickname,password from user where id='".$id."'";
echo $sql;
$res=mysql_query($sql);
$arr=mysql_fetch_array($res,MYSQL_ASSOC);

然后访问http://127.0.0.1/test.php?id=1,获取用户信息。这是可以通过构造参数进行攻击比如

//访问:http://127.0.0.1/test.php?id=1%27%20OR%201%20=%201--%27
//将可以查询出所有的表数据。
$sql="select id,account,nickname,password from user where id='1' OR 1 = 1--''";

//访问:http://127.0.0.1/test.php?id=1%27%20UNION%20ALL%20%28SELECT%20NULL,NULL,NULL,%27%3C?php%20system%28$_GET[%22command%22]%29;%20?%3E%27%20INTO%20OUTFILE%20%27D:/www/shell.php%27%29%20--%20-%27
//在知道网站路径情况下,在网站目录下生成shell.php,将能够在对用户的网站进行挂马,提权的操作。
$sql="select id,account,nickname,password from user where id='1' UNION ALL (SELECT NULL,NULL,NULL,'' INTO OUTFILE 'D:/www/shell.php') -- -''";

甚至还可以进行表数据的增加,修改,删除,数据库的删除!所以永远不要相信用户的输入,永远要过滤输入,转义输出。对传递过来的数字型变量要进行严格的匹配,对字符串型的变量进行长度限制。注意过滤用户传递过来的变量是否包含引号,/*,#,—等特殊字符或转义字符,script,drop,不要给用户超级权限,最好进行分隔。

//使用addslashes过滤单引号,双引号和斜杠
$uname = addslashes( $_GET['id'] );
$query = 'SELECT username FROM users WHERE id = "' . $uname . '";

//使用mysql_real_escape_string过滤特殊字符,比如\x00, \n, \r, \, ', ",\x1a
//mysql_real_escape_string使用的是本地单字节字符集,而我们传递多字节编码的变量时,有可能还是会造成SQL注入漏洞
//如chr(0xbf) . chr(0x27),所以mysql服务端与php客户端通信尽量使用utf-8而不是gbk
$uname = mysql_real_escape_string( $_GET['id'] );
$query = 'SELECT username FROM users WHERE id = "' . $uname . '";

//使用is_numeric检查数字型变量
$id = $_GET['id'];
( is_numeric( $id ) ? TRUE : FALSE );

//使用sprintf格式化数字型变量
$id = $_GET['id'];
$query = sprintf("SELECT username FROM users WHERE id = '%d' ", $id);

//使用htmlentities($var, ENT_QUOTES)转义所有的html标记
$id = $_GET['id'];
$id = htmlentities( $id, ENT_QUOTES, 'UTF-8' );
 
$query = 'SELECT username FROM users WHERE id = "' . $id . '"';

//使用filter_var过滤script标签,防止xss
//$str = strip_tags($input);
$str = filter_var($input, FILTER_SANITIZE_STRING);

如果PHP支持PDO可以的话,就不要拼接sql,尽量使用PDO或Mysqli。使用PDO要在PDO的DSN中指定charset属性,要设置PDO::ATTR_EMULATE_PREPARES参数为false,禁止php进行本地转义,造成双字节转义攻击,如果没有必要,msyql服务端与php客户端应该都是用utf-8进行通信,而不是gbk。

$pdo = new PDO("mysql:host=192.168.0.1;dbname=test;","root");
$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
 
$st = $pdo->prepare("select * from info where id =? and name = ?");
$id = 21;
$name = 'zhangsan';
 
$st->bindParam(1,$id);
$st->bindParam(2,$name);
$st->execute();
$st->fetchAll();

除了过滤,变量务必初始化,不要自动获得变量,用户将可以构造任何变量进行攻击。
不要这样随意,将造成文件包含攻击

<?php
  if($_GET['page']) {//if(file_exists($_GET['page'])) {非严格的检测 http://localhost/index.php?page=/etc/passwd%00
    include($_GET['page']);
  }
?>

要进行严格的匹配,或者做映射

<?php
  if($_GET['page'] == 'news') {
    include('news.php');
  }
?>

关闭浏览器输出的错误报告,避免泄漏文件目录路径,错误报告应输出到日记文件中

#php.ini
display_errors = 'off'

#apache2.conf
php_flag  display_errors  off

php当中

ini_set('display_errors', false);

页面的访问要设置权限,特别是执行查询的或者录入的页面,简单设置如下

define('APP_ADMIN', '1');
if(!defined('APP_ADMIN')) exit;

应该做一个更为完善的权限系统,如基于角色的权限管理系统,最好细致到页面(元素)。
eval函数针对php安全有很大杀伤力,为了防止挂马,应禁用eval函数。
下载的页面必须设置下载目录,对要下载的文件进行检测或做目录权限控制,生成临时下载链接,不允许下载任意的文件。对于系统配置文件,日记文件等要做分离,建议像zend framework那样设置web目录,程序与web目录分开,避免泄漏源码等。不要将.svn文件直接放置在web目录,会导致源码泄露。
上传的页面必须进行类型检测,不要只截取扩展名,设置权限,不允许可执行文件。这里是网友写的一个文件类型检查类

/*通过文件名,获得文件类型*
 *@author chengmo QQ:8292669*
 *@copyright <a href="http://www.cnblogs.com/chengmo">http://www.cnblogs.com/chengmo</a> 2010-10-17
 *@version 0.1
 *$filename="d:/1.png";echo cFileTypeCheck::getFileType($filename); 打印:png
 */
class cFileTypeCheck
{
    private static $_TypeList=array();
    private static $CheckClass=null;
    private function __construct($filename)
    {
        self::$_TypeList=$this->getTypeList();
    }
 
    /**
     *处理文件类型映射关系表*
     *
     * @param string $filename 文件类型
     * @return string 文件类型,没有找到返回:other
     */
    private function _getFileType($filename)
    {
        $filetype="other";
        if(!file_exists($filename)) throw new Exception("no found file!");
        $file = @fopen($filename,"rb");
        if(!$file) throw new Exception("file refuse!");
        $bin = fread($file, 15); //只读15字节 各个不同文件类型,头信息不一样。
        fclose($file);
         
        $typelist=self::$_TypeList;
        foreach ($typelist as $v)
        {
            $blen=strlen(pack("H*",$v[0])); //得到文件头标记字节数
            $tbin=substr($bin,0,intval($blen)); ///需要比较文件头长度
             
            if(strtolower($v[0])==strtolower(array_shift(unpack("H*",$tbin)))) 
            {
                return $v[1];
            }
        }
        return $filetype;
    }
     
    /**
     *得到文件头与文件类型映射表*
     *
     * @return array array(array('key',value)...)
     */
    public function getTypeList()
    {
        return array(array("FFD8FFE1","jpg"),
        array("89504E47","png"),
        array("47494638","gif"),
        array("49492A00","tif"),
        array("424D","bmp"),
        array("41433130","dwg"),
        array("38425053","psd"),
        array("7B5C727466","rtf"),
        array("3C3F786D6C","xml"),
        array("68746D6C3E","html"),
        array("44656C69766572792D646174","eml"),
        array("CFAD12FEC5FD746F","dbx"),
        array("2142444E","pst"),
        array("D0CF11E0","xls/doc"),
        array("5374616E64617264204A","mdb"),
        array("FF575043","wpd"),
        array("252150532D41646F6265","eps/ps"),
        array("255044462D312E","pdf"),
        array("E3828596","pwl"),
        array("504B0304","zip"),
        array("52617221","rar"),
        array("57415645","wav"),
        array("41564920","avi"),
        array("2E7261FD","ram"),
        array("2E524D46","rm"),
        array("000001BA","mpg"),
        array("000001B3","mpg"),
        array("6D6F6F76","mov"),
        array("3026B2758E66CF11","asf"),
        array("4D546864","mid"));
    }
 
 	
    public static function getFileType($filename)
    {
        if(!self::$CheckClass) self::$CheckClass=new self($filename);
        $class=self::$CheckClass;
        return $class->_getFileType($filename);
    }
	
}

$filename="22.jpg";
echo $filename,"t",cFileTypeCheck::getFileType($filename),"rn";
$filename="11.doc";
echo $filename,"t",cFileTypeCheck::getFileType($filename),"rn";

对于暴力破解设置验证码,或者限制ip次数。

参考链接:
SQL Injection Attacks by Example
SQL Injection
Injection
PHP SQL Injection
PHP+MySQL环境下SQL Injection攻防总结
PDO防注入原理分析以及使用PDO的注意事项
SQL injection that gets around mysql_real_escape_string()
MySQL远程提权php版
XSS Filter Evasion Cheat Sheet
php通过文件头检测文件类型通用类(zip,rar…)
php 验证码【倾斜,正弦干扰线,黏贴,旋转

发表评论

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