浅析PHP代码审计中的SQL注入漏洞
- 1.概述
- 2.普通注入
- 3.编码注入
- 宽字节注入
- 二次urldecode注入
- 4.漏洞防范
- gpc/rutime魔术引号
- 过滤函数和类
- addslashes函数
- mysql_[real_]escape_string函数
- intval等字符转换
- PDO prepare预编译
1.概述
SQL注入的攻击方式有下面几种:
- 在权限较大的情况下,通过SQL注入可以直接写入webshell,或者直接执行系统命令等
- 在权限较小的情况下,也可以通过注入来获得管理员的密码等信息,或者修改数据库内容进行一些钓鱼或者其他间接利用
登录页面的注入现在来说大多是发生在HTTP头里面的client-ip
和x-forward-for
,一般用来记录登录的IP地址,另外在订单系统里面,由于订单涉及购物车等多个交互,所以经常会发生二次注入。我们在通读代码挖掘漏洞的时候可以着重关注这几个地方
2.普通注入
这里说的普通注入是指最容易利用的SQL注入漏洞,比如直接通过注入union查询就可以查询数据库,一般的SQL注入工具也能够非常好地利用。普通注入有int型和string型
数据库操作存在一些关键字,比如select from、mysql_connect、mysql_query、mysql_fetch_row
等,数据库的查询方式还有update、insert、delete
,我们在做白盒审计时,只需要查找这些关键字,即可定向挖掘SQL注入漏洞
3.编码注入
在SQL注入里,最常见的编码注入是MySQL宽字节
以及urldecode/rawurldecode
函数导致的
宽字节注入
在使用PHP连接MySQL的时候,当设置“set character_set_client=gbk
”时会导致一个编码转换的注入问题,也就是我们所熟悉的宽字节注入
关于这个漏洞的解决方法推荐如下几种方法:
- 在执行查询之前先执行
SET NAMES 'gbk',character_set_client=binary
设置character_set_client
为binary
- 使用
mysql_set_charset('gbk')
设置编码,然后使用mysql_real_escape_string()
函数被参数过滤 - 使用pdo方式,在PHP5.3.6及以下版本需要设
setAttribute(PDO::ATTR_EMULATE_PREPARES,false);
来禁用prepared statements
的仿真效果
如上几种方法更推荐第一和第三种
对宽字节注入的挖掘方法也比较简单,只要搜索如下几个关键字即可:
SET NAMES
character_set_client=gbk
mysql_set_charset('gbk')
二次urldecode注入
如果某处使用了urldecode
或者rawurldecode
函数,则会导致二次解码生成单引号而引发注入。原理是我们提交参数到WebServer时,WebServer会自动解码一次,假设目标程序开启了GPC,我们提交/1.php?id=1%2527
因为我们提交的参数里面没有单引号,所以第一次解码后的结果是id=1%27
(%25
解码的结果是%
)
如果程序里面使用了urldecode
或者rawurldecode
函数来解码id参数,则解码后的结果是id=1’
成功出现引发注入
测试代码:
<?php
$a=addslashes($_GET['p']);
$b=urldecode($a);
echo '$a='.$a;
echo '<br />';
echo '$b='.$b;
4.漏洞防范
gpc/rutime魔术引号
magic_quotes_gpc
负责对GET、POST、COOKIE的值进行过滤,magic_quotes_runtime
对从数据库或者文件中获取的数据进行过滤。通常在开启这两个选项之后能防住部分SQL注入漏洞被利用
为什么说是部分,因为我们之前也介绍了,它们只对单引号(')、双引号(")、反斜杠(\)及空字符NULL进行过滤,在int型的注入上是没有多大作用的
过滤函数和类
addslashes函数
addslashes
函数过滤的值范围和GPC是一样的,即单引号(')、双引号(")、反斜杠(\)及空字符NULL,它只是一个简单的检查参数的函数,大多数程序使用它是在程序的入口,进行判断如果没有开启GPC,则使用它对$_POST/$_GET
等变量进行过滤,不过它的参数必须是string类型
mysql_[real_]escape_string函数
mysql_escape_string
和mysql_real_escape_string
函数都是对字符串进行过滤,在PHP4.0.3以上版本才存在,如下字符受影响【\x00】【\n】【\r】【\】【'】【"】【\x1a】,两个函数唯一不一样的地方在于mysql_real_escape_string
接受的是一个连接句柄并根据当前字符集转义字符串,所以推荐使用mysql_real_escape_string
使用举例:
<?php
$con = mysql_connect("localhost", "root", "123456");
$id = mysql_real_escape_string($_GET['id'],$con);
$sql="select * from test where id='".$id."'";
echo $sql;
当请求该文件?id=1’
时,上面代码输出:select*from test where id='1\''
intval等字符转换
intval
的作用是将变量转换成int类型,这里举例intval
是要表达一种方式,一种利用参数类型白名单的方式来防止漏洞,对应的还有很多如floatval
等
应用举例如下:
<?php
$id=intval("1 union select ");
echo $id;
以上代码输出:1
PDO prepare预编译
我们先来看一段代码:
<?php
dbh = new PDO("mysql:host=localhost; dbname=demo", "user", "pass");
$dbh->exec("set names 'gbk'");
$sql="select * from test where name = ? and password = ?";
$stmt = $dbh->prepare($sql);
$exeres = $stmt->execute(array($name, $pass));
上面这段代码虽然使用了pdo的prepare方式来处理sql查询,但是当PHP版本<5.3.6
之前还是存在宽字节SQL注入漏洞,原因在于这样的查询方式是使用了PHP本地模拟prepare,再把完整的SQL语句发送给MySQL服务器,并且有使用set names 'gbk'
语句,所以会有PHP和MySQL编码不一致的原因导致SQL注入,正确的写法应该是使用ATTR_EMULATE_PREPARES
来禁用PHP本地模拟prepare,代码如下:
<?php
dbh = new PDO("mysql:host=localhost; dbname=demo", "user", "pass");
$dbh->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
$dbh->exec("set names 'utf8'");
$sql="select * from test where name = ? and password = ?";
$stmt = $dbh->prepare($sql);
$exeres = $stmt->execute(array($name, $pass));