4.1 SQL注入漏洞
SQL注入漏洞可能是被人知道最多的漏洞,哪怕再没有接触到安全的程序员,多多少少会对这个词有所耳闻,它也是目前被利用得最多的漏洞。根据笔者维护公司waf时统计的数据,它的攻击次数占总攻击拦截的一半以上。SQL注入漏洞的原理非常简单,由于开发者在编写操作数据库代码时,直接将外部可控的参数拼接到SQL语句中,没有经过任何过滤就直接放入数据库引擎执行。
由于SQL注入是直接面对数据库进行攻击的,所以它的危害不言而喻,通常利用SQL注入的攻击方式有下面几种:一是在权限较大的情况下,通过SQL注入可以直接写入webshell,或者直接执行系统命令等。二是在权限较小的情况下,也可以通过注入来获得管理员的密码等信息,或者修改数据库内容进行一些钓鱼或者其他间接利用。
针对SQL注入漏洞的利用工具也是越来越智能,sqlmap是目前被使用最多的注入工具,这是一款国外开源的跨平台SQL注入工具,用Python开发,支持多种方式以及几乎所有类型的数据库注入,对SQL注入漏洞的兼容性也非常强。
既然SQL注入是被利用最多的漏洞,因此它也是被研究最深的漏洞,针对不同的漏洞代码情况和运行环境,有多种的利用方式,如普通注入、盲注、报错注入、宽字节注入、二次注入等,但是它们的原理都是大同小异的,下面笔者会介绍怎么挖掘到这些注入漏洞。
4.1.1 挖掘经验
SQL注入经常出现在登录页面、获取HTTP头(user-agent/client-ip等)、订单处理等地方,因为这几个地方是业务相对复杂的,登录页面的注入现在来说大多是发生在HTTP头里面的client-ip和x-forward-for,一般用来记录登录的IP地址,另外在订单系统里面,由于订单涉及购物车等多个交互,所以经常会发生二次注入。我们在通读代码挖掘漏洞的时候可以着重关注这几个地方。
4.1.1.1 普通注入
这里说的普通注入是指最容易利用的SQL注入漏洞,比如直接通过注入union查询就可以查询数据库,一般的SQL注入工具也能够非常好地利用。普通注入有int型和string型,在string型注入中需要使用单或双引号闭合,下面简单演示普通注入漏洞,后面所有测试SQL注入漏洞的数据表中数据都如图4-1所示。
图 4-1
测试代码如下:
< ? php
$uid=$_GET['id'] ;
$sql="SELECT * FROM userinfo where id=$uid" ;
$conn=mysql_connect ( 'localhost' , 'root' , '123456' );
mysql_select_db ( "test" , $conn );
$result=mysql_query ( $sql , $conn );
print_r ( ' 当前 SQL 语句: '.$sql.'<br /> 结果: ' );
print_r ( mysql_fetch_row ( $result ));
测试代码中GET id参数存在SQL注入漏洞,测试方法如图4-2所示。
图 4-2
从截图可以看到原本的SQL语句已被注入更改,使用了union查询到当前用户。
从上面的测试代码中可以发现,数据库操作存在一些关键字,比如select from、mysql_connect、mysql_query、mysql_fetch_row等,数据库的查询方式还有update、insert、delete,我们在做白盒审计时,只需要查找这些关键字,即可定向挖掘SQL注入漏洞。
4.1.1.2 编码注入
程序在进行一些操作之前经常会进行一些编码处理,而做编码处理的函数也是存在问题的,通过输入转码函数不兼容的特殊字符,可以导致输出的字符变成有害数据,在SQL注入里,最常见的编码注入是MySQL宽字节以及urldecode/rawurldecode函数导致的。
1.宽字节注入
在使用PHP连接MySQL的时候,当设置“set character_set_client=gbk”时会导致一个编码转换的注入问题,也就是我们所熟悉的宽字节注入,当存在宽字节注入漏洞时,注入参数里带入%df%27,即可把程序中过滤的\(%5c)吃掉。举个例子,假设/1.php?id=1里面的id参数存在宽字节注入漏洞,当提交/1.php?id=-1’and 1=1%23时,MySQL运行的SQL语句为select*from user where id=’1\’and 1=1#’很明显这是没有注入成功的,我们提交的单引号被转义导致没有闭合前面的单引号,但是我们提交/1.php?id=-1%df’and 1=1%23时,这时候MySQL运行的SQL语句为:
select * from user where id='1 運 ' and 1=1#'
这是由于单引号被自动转义成\',前面的%df和转义字符\反斜杠(%5c)组合成了%df%5c,也就是“運”字,这时候单引号依然还在,于是成功闭合了前面的单引号。
出现这个漏洞的原因是在PHP连接MySQL的时候执行了如下设置:
set character_set_client=gbk
告诉MySQL服务器客户端来源数据编码是GBK,然后MySQL服务器对查询语句进行GBK转码导致反斜杠\被%df吃掉,而一般都不是直接设置character_set_client=gbk,通常的设置方法是SET NAMES'gbk',但其实SET NAMES'gbk'不过是比character_set_client=gbk多干了两件事而已,SET NAMES'gbk'等同于如下代码:
SET
character_set_connection='gbk' ,
character_set_results='gbk' ,
character_set_client=gbk
这同样也是存在漏洞的,另外官方建议使用mysql_set_charset方式来设置编码,不幸的是它也只是调用了SET NAMES,所以效果也是一样的。不过mysql_set_charset调用SET NAMES之后还记录了当前的编码,留着给后面mysql_real_escape_string处理字符串的时候使用,所以在后面只要合理地使用mysql_real_escape_string还是可以解决这个漏洞的,关于这个漏洞的解决方法推荐如下几种方法:
1)在执行查询之前先执行SET NAMES'gbk',character_set_client=binary设置character_set_client为binary。
2)使用mysql_set_charset('gbk')设置编码,然后使用mysql_real_escape_string()函数被参数过滤。
3)使用pdo方式,在PHP5.3.6及以下版本需要设置setAttribute(PDO::ATTR_EMULATE_PREPARES,false);来禁用prepared statements的仿真效果。
如上几种方法更推荐第一和第三种。
下面对宽字节注入进行一个简单测试。
测算代码如下:
< ? php
$conn=mysql_connect ( 'localhost' , 'root' , '123456' );
mysql_select_db ( "test" , $conn );
mysql_query ( "SET NAMES 'gbk'" , $conn );
$uid=addslashes ( $_GET['id'] );
$sql="SELECT * FROM userinfo where id='$uid'" ;
$result=mysql_query ( $sql , $conn );
print_r ( ' 当前 SQL 语句: '.$sql.'<br /> 结果: ' );
print_r ( mysql_fetch_row ( $result ));
mysql_close ();
当提交/1.php?id=%df'union select 1,2,3,4%23时,成功注入的效果如图4-3所示。
图 4-3
对宽字节注入的挖掘方法也比较简单,只要搜索如下几个关键字即可:
SET NAMES
character_set_client=gbk
mysql_set_charset ( 'gbk' )
2.二次urldecode注入
只要字符被进行转换就有可能产生漏洞,现在的Web程序大多都会进行参数过滤,通常使用addslashes()、mysql_real_escape_string()、mysql_escape_string()函数或者开启GPC的方式来防止注入,也就是给单引号(')、双引号(")、反斜杠(\)和NULL加上反斜杠转义。如果某处使用了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-4所示。
图 4-4
既然知道了原理主要是由于urldecode使用不当导致的,那我们就可以通过搜索urldecode和rawurldecode函数来挖掘二次urldecode注入漏洞。
4.1.1.3 espcms搜索注入分析
这里以一个笔者在2013年发现的一个小CMS程序espcms搜索注入的漏洞为例,我们目前尽量以相对好理解的漏洞来举例。
漏洞在interface/search.php文件和interface/3gwap_search.php文件in_taglist()函数都存在,一样的问题,以interface/search.php为例说明:
打开文件看到如下代码:
function in_taglist () {
parent :: start_pagetemplate ();
include_once admin_ROOT . 'public/class_pagebotton.php' ;
$page = $this->fun->accept ( 'page' , 'G' );
$page = isset ( $page ) ? intval ( $page ) : 1 ;
$lng = ( admin_LNG == 'big5' ) ? $this->CON['is_lancode'] : admin_LNG ;
$tagkey = urldecode ( $this->fun->accept ( 'tagkey' , 'R' ));
$takey = $this->fun->inputcodetrim ( $tagkey );
$db_where = ' WHERE lng=\'' . $lng . '\' AND isclass=1' ;
if ( empty ( $tagkey )) {
$linkURL = $_SERVER['HTTP_REFERER'] ;
$this->callmessage ( $this->lng['search_err'] , $linkURL , $this->lng ['gobackbotton'] );
}
if (! empty ( $tagkey )) {
$db_where.=" AND FIND_IN_SET ( '$tagkey' , tags ) " ;
}
其中:
$tagkey = urldecode ( $this->fun->accept ( 'tagkey' , 'R' ));
这行代码得到$_REQUEST['tagkey']的值,由于$tagkey变量使用了urldecode,从而可以绕过GPC:
$db_where.=" AND FIND_IN_SET ( '$tagkey' , tags ) " ;
经过判断$tagkey不为空则拼接到SQL语句中,导致产生注入漏洞。
4.1.2 漏洞防范
SQL注入漏洞虽然是目前最泛滥的漏洞,不过要解决SQL注入漏洞其实还比较简单。在PHP中可以利用魔术引号来解决,不过魔术引号在PHP 5.4后被取消,并且gpc在遇到int型的注入时也会显得不那么给力了,所以通常用得多的还是过滤函数和类,像discuz、dedecms、phpcms等程序里面都使用过滤类,不过如果单纯的过滤函数写得不够严谨,也会出现绕过的情况,像这三套程序就都存在绕过问题。当然最好的解决方案还是利用预编译的方式,下面就来看看这三种方式的使用方法。
4.1.2.1 gpc/rutime魔术引号
通常数据污染有两种方式,一种是应用被动接收参数,类似于GET、POST等;还有一种是主动获取参数,类似于读取远程页面或者文件内容等。所以防止SQL注入的方法就是要守住这两条路。在本书第2章第3节介绍了PHP的核心配置,里面详细介绍了GPC等魔术引号配置的方法,magic_quotes_gpc负责对GET、POST、COOKIE的值进行过滤,magic_quotes_runtime对从数据库或者文件中获取的数据进行过滤。通常在开启这两个选项之后能防住部分SQL注入漏洞被利用。为什么说是部分,因为我们之前也介绍了,它们只对单引号(')、双引号(")、反斜杠(\)及空字符NULL进行过滤,在int型的注入上是没有多大作用的。
PHP 4.2.3以及之前的版本可以在任何地方设置开启,即配置文件和代码中,之后的版本可以在php.ini、httpd.conf以及.htaccess中开启。
4.1.2.2 过滤函数和类
过滤函数和类有两种使用场景,一种是程序入口统一过滤,像框架程序用这种方式比较多,另外一种是在程序进行SQL语句运行之前使用,除了PHP内置的一些过滤单引号等函数外,还有一些开源类过滤union、select等关键字。
1.addslashes函数
addslashes函数过滤的值范围和GPC是一样的,即单引号(')、双引号(")、反斜杠(\)及空字符NULL,它只是一个简单的检查参数的函数,大多数程序使用它是在程序的入口,进行判断如果没有开启GPC,则使用它对$_POST/$_GET等变量进行过滤,不过它的参数必须是string类型,所以曾经某些程序使用这种方式对输入进行过滤时出现了绕过,比如只遍历$_GET的值,当时并没有考虑到$_GET的值也是一个数组。我们来看一个例子如下:
< ? php
$str ? = ? "phpsafe ’ " ;
echo ? addslashes ( $str );? >
上面的例子输出:phpsafe\'。
2.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\''
3.intval等字符转换
上面我们提到的过滤方式,在int类型注入时效果并不好,比如可以通过报错或者盲注等方式来绕过,这时候intval等函数就起作用了,intval的作用是将变量转换成int类型,这里举例intval是要表达一种方式,一种利用参数类型白名单的方式来防止漏洞,对应的还有很多如floatval等。
应用举例如下:
< ? php
$id=intval ( "1 union select " );
echo $id ;
以上代码输出:1
4.1.2.3 PDO prepare预编译
如果之前了解过.NET的SqlParameter或者java里面的prepareStatement,那么就很容易能够理解PHP 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 ));