实验环境
漏洞介绍
认识SQL注入漏洞
SQL注入漏洞可以说是在企业运营中会遇到的最具破坏性的漏洞之一,它也是目前被利用得最多的漏洞。
要学会如何防御SQL注入,我们首先要对他的原理进行了解。
SQL注入(SQLInjection)是这样一种漏洞:当我们的Web app在向后台数据库传递SQL语句进行数据库操作时。
如果对用户输入的参数没有经过严格的过滤处理,那么恶意访问者就可以构造特殊的SQL语句,直接输入数据库引擎执行,获取或修改数据库中的数据。
漏洞危害
1.直接就造成数据库中的数据泄露
2.如果数据库连接用户具备高权限,则可能导致恶意访问者获取服务器控制权
3.众多安全事件的切入点。
相关百度漏洞案例
WooYun-2015-157024 某百度音频系统注入间接导致GETSHELL可内网渗透。
恶意访问者发现一处注入漏洞后,随后该恶意访问者通过信息搜集,发现一处信息泄漏,得到了网站目录的绝对路径。
该恶意访问者想通过into_outfile
写入获取shell
权限。发现PHP后端具有基本的防御,'
单引号被转义。导致写shell时语法错误,无法突破后,恶意访问者开始调整恶意访问角度。
不甘放弃的恶意访问者开始使用Mysql load_file函数
读取网站程序源码审计,终于恶意访问者发现一处二次注入已绕过单引号的过滤。通过构造够到语句尝试GETSHELL。
恶意访问者利用成功,写入了恶意PHP文件,获得了一个SHELL权限,且该服务器处于百度内网,可进一步内网渗透,提权,嗅探等操作,按照目前恶意访问者拥有的权限可以对网站,甚至数据库进行破坏操作,该漏洞被白帽子提交修复。但是如果落入恶意的“黑客”手中,进一步内网渗透,可能造成更多服务器,敏感数据沦陷!
- 根据本案例思考,良好的安全编码习惯,敏感信息处理,就相对避免恶意访问者多种恶意访问。
实验步骤
漏洞分析与防护
Part 1:简单漏洞实例
首先,我们打开浏览器输入测试平台的地址,选择SQL注入漏洞进行分析。
源码分析
该页面中是一个简单的漏洞源码示例,在接收到用户的提交数据后,对输入的内容没有任何过滤。所以我们通过简单的SQL注入语句构造就可以对数据库中的内容进行操作。我们先分析该页面的源代码:
php include("sql-connections/sql-connect.php"); error_reporting(0); $id = isset($_GET['id']) ? $_GET['id']:'1'; $sql="SELECT * FROM news WHERE id='$id'"; //用户输入完全未过滤,直接带入SQL语句,导致SQL注入 $result=mysql_query($sql); $row = mysql_fetch_array($result); if($row) { echo "
'. $row['title'].' |
' .$row['content'].' |
"; echo ''; print_r(mysql_error()); echo ""; } ?>
在这段代码中,通过变量"$sql"
来执行相应的SELECT语句,但是对用户输入的变量"$id"
并没有任何处理,造成了SQL注入漏洞的产生。接下来,我们通过简单的测试,验证此处SQL注入的存在。
漏洞验证
- 首先,我们在输入的末端添加一个 ‘ ' ’ 单引号,对输入内容进行闭合,并在之后添加判断语句:
' and 1=1 --+ //"--+或者#为注释符,用于截断原SQL语句末尾原有的单引号"
注入代码= ?id=1' and 1=1 --+
- 实现单引号绕过,服务端在接收到这段输入内容之后,执行的SQL语句就变为了:
select * from users where id='1' and 1=1 --+ //由于"and 1 = 1"是恒真的,所有查询的结果正常显示
注入代码= ?id=1' and 1=2 --+
这时,我们对输入的内容稍微修改,输入'and 1=2 --+
服务端执行的SQL语句就变为了:
select * from users where id='1' and 1=2 --+ //此时,由于and 1=2恒为假,所以并没有返回信息,所以确定存在注入漏洞。
在验证漏洞存在之后,我们就可以进行各种语句的构造,操作数据库中的内容。以下实验内容,大家可以任意尝试。
注入代码= ?id=1' order by 4 --+
- 判断字段长度
'Order by 4
服务端执行语句为:select * from users where id='1' order by 4 --+
通过Order by 函数判断列长度,由报错信息判断列长度小于4,继续向下尝试。
注入代码= ?id=1' order by 3 --+
- 再次判断字段长度
Order by 3
服务端执行语句为:select * from users where id='1' order by 3 --+
- 通过
Order by
函数判断列长度,无报错信息判断列长度等于3,根据收集信息进一步注入。
注入代码
= ?id=1'and 1=2 union select 1,user(),database() --+
- 获取敏感信息
'and 1=2 union select 1,user(),database() --+
服务端执行语句为:select * from users where id='1' and 1=2 union select 1,user(),database() --+
爆出服务端MySQL当前用户名,当前数据库名,可以利用数据库名进一步获取表名。
- 注入获取表名
注入代码
= ?id=1'and 1=2 union select 1,2,group_concat(table_name) from information_schema.tables where table_schema=database() --+
服务端执行语句为: select * from users where id='1' and 1=2 union select 1,2,group_concat(table_name) from information_schema.tables where table_schema=database() --+
默认MySQL information_schema数据库中保存了数据库所有数据库表和列的信息,因此我用利用这个特性去查询表和列名。 获取到表名后,依旧利用information_schema数据库查询列名。
注入代码
= ?id=1' and 1=2 union select 1,2,group_concat(column_name) from information_schema.columns where table_name='users' --+
服务端执行语句为: select * from users where id='1'and 1=2 union select 1,2,group_concat(column_name) from information_schema.columns where table_name='users' --+
获取USER表,列名称。
注入代码
='and 1=2 union select 1,group_concat(name),group_concat(pass) from users --+
- 注入获取用户数据 服务端执行语句为:
select * from users where id='1' and 1=2 union select 1,group_concat(name),group_concat(pass) from users --+
通过group_concat获取所有用户数据,最终实现脱裤(得到数据库中的数据)。
Part2 漏洞防护总结
最佳防御代码 策略
PDO prepare参数化语句
PHP数据对象(PHP Date Object PDO)框架最常用的数据库接口之一,通过使用问号占位符来支持参数化语句。
安全示例代码1
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方式处理查询,但是当PHP版本在5.3.6之前还是存在宽字节注入漏洞,原因在于使用了本地模拟Prepare,在把完整SQL语句发送给服务器,并且编码设置了GBK,所以会有PHP和MYSQL编码不一致原因导致注入,解决方法是 使用
ATTR_EMULATE_PREPARES
来禁用PHP本地模拟prepare。
安全示例代码2
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)); ?>
这里使用某CMS SQL注入防护系统代码,在安全编码的习惯基础上,在向数据库发送执行请求时,所有请求必须经过防注入函数二次验证,最大程度上避免了SQL注入漏洞的产生。
- 注意 : 不要忽略X-Forwarded-For,Header,Refferer参数 包括从数据库取出时也要经过安全函数过滤,因为这些攻击者完全可以通过抓包修改这些参数,一切输入甚至输出都可能是有害的。
private static function _do_query_safe($sql) { $sql = str_replace(array('\\\\', '\\\'', '\\"', '\'\''), '', $sql); $mark = $clean = ''; if (strpos($sql, '/') === false && strpos($sql, '#') === false && strpos($sql, '-- ') === false && strpos($sql, '@') === false && strpos($sql, '`') === false) { $clean = preg_replace("/'(.+?)'/s", '', $sql); } else { $len = strlen($sql); $mark = $clean = ''; for ($i = 0; $i < $len; $i++) { $str = $sql[$i]; switch ($str) { case '`': if(!$mark) { $mark = '`'; $clean .= $str; } elseif ($mark == '`') { $mark = ''; } break; case '\'': if (!$mark) { $mark = '\''; $clean .= $str; } elseif ($mark == '\'') { $mark = ''; } break; ... } $clean .= $mark ? '' : $str; } } if(strpos($clean, '@') !== false) { return '-3'; } $clean = preg_replace("/[^a-z0-9_\-\(\)#\*\/\"]+/is", "", strtolower($clean)); if (self::$config['afullnote']) { $clean = str_replace('/**/', '', $clean); } ... return 1; }
设计输入验证和处理策略
输入验证是一种在保护应用程序安全方面很有用的工具。不过,他只能作为深度防御策略(包含多个防护层以保护应用程序的总体安全)的一个子部分。
在应用程序输入层使用白名单输入验证以便验证所有用户输入都符合应用要接收的内容。应用只允许接收符合期望格式的输入
在客户端浏览器上同样执行白名单输入验证,这样可以防止为用户输入不可接收的数据时服务器和浏览器的往返传递。不能将该操作作为安全控制手段,因为攻击者可以修改来自用户浏览器的所有数据。
在Web应用防火墙(WAF)层使用黑名单和白名单输入验证(以漏洞“签名”和“有经验”行为的形式)以便提供入侵检测/阻止功能和监控应用攻击。
在应用程序中自始自终地使用参数化语句以保证执行安全地SQL执行。
在数据库中使用编码技术以便在动态SQL中使用输入时安全地对其编码。
在使用从数据库中提取数据之前恰当地对其进行编码。例如将浏览器中显示的数据针对跨站脚本进行编码。
推荐防护方案
领域驱动的安全性
- SQL注入之所以发生,是因为我们的应用程序不正确地将数据在不同表示方式之间进行映射。
- 通过将数据封装到有效值对象中,并限制对原始数据的访问,我们就可以控制对数据的使用
使用参数化语句
- 动态SQL(或者将SQL查询组装成包含用户控制的输入字符串并提交给数据库)是引发SQL注入漏洞的主要原因。
- 应该使用参数化语句(也称为预处理语句)而非动态SQL来安全地组装SQL查询
- 在提供数据时可以只使用参数化语句,但却无法使用参数化语句来提供SQL关键字或表示符(比如表名或列名)。
验证输入
- 尽可能坚持使用白名单输入验证(只接受期望的已知良好的输入)。
- 确保验证应用收到的所有受用户控制的输入类型,大小,范围和内容。
- 只有当无法使用白名单输入验证时才能使用黑名单输入验证(拒绝已知不良的或基于签名的输入)
- 绝不能单独只使用黑名单检验数据,至少应该总是将它与输出编码技术一起结合使用。
编码输出
- 确保对包含用户可控制输入的查询进行正确的编码以防止使用单引号或其他字符来修改查询。
- 如果正在使用LIKE子句,请确保LIKE中的通配符恰当编码。
- 在使用从数据库接收到的数据之前确保已经对数据中的敏感内容进行了恰当的输入验证和输出编码。
规范化
- 将输入解码或变为规范格式后才能执行输入验证过滤器和输出编码
- 任何单个字符都存在多种表示及编码方法。
- 尽可能使用白名单输入验证并拒绝非规范格式的输入。
通过设计避免SQL注入
- 使用存储过程以便在数据库层拥有较细粒的许可。
- 可以使用数据访问抽象层来对整个应用施加安全的数据访问。
- 设计时,请考虑对敏感信息进行附加的控制。
其他防御方式
GPC/RUNTIME魔术引号
通常数据污染有两种方式,一种是应用被动接受参数,类似于GET,POST等;还有一种是主动获取参数,类似于读取远程页面或者文件内容等。所以防止SQL注入的方法就是要守住这两条路。
magic_quotes_gpc负责对GET,POST,COOKIE的值进行过滤,magic_quotes_runtime对从数据库或者文件中获取的数据进行过滤。通常在开启这两个选项之后能防住部分SQL注入漏洞被利用,因为我们之前也介绍了,在某些环境下存在绕过,在INT型注入上是没有多大作用的。
在PHP.INI配置GPC RUTIME启用状态
测试代码如下:
echo ($_GET['ichunqiu']); ?>
- GPC关闭状态下,可以看到GET请求未经过过滤显示。
& GPC开启状态,自动对GET获取的数据进行了转义。
过滤函数和类
在PHP5.4之前,可以利用魔术引号来解决部分SQL注入的问题。而GPC在面对INT型注入时,也无法进行很好的防御。所以在通常的工作场景中,用得多的还是过滤函数和类。 不过如果单纯的过滤函数写得不够严谨,也会出现绕过的情况。这时候可以使用预编译语句来绑定变量,一般情况下这是防御SQL注入的最佳方式。
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","root"); $id = mysql_real_escape_string($_GET['ichunqiu'],$con); echo "$id"; ?>
intval等字符转换
上面我们提到的过滤方式,在int类型注入时效果并不好,比如可以通过报错或者忙注等方式来绕过,这时候intval等函数就起作用了,intval的作用是将变量转换成int类型,这里举例intval是要表达一种方式,一种利用参数类型白名单的方式来防止漏洞。
- 代码示例 1
php $id=intval($id); if($id) { $sql="select * from users where uid=".$id; mysql_query($sql,$con); }
测试效果如下: 将数据强制转换成为INT,避免了闭合单引号绕过的风险。
- 代码示例 2
$con = mysql_connect("localhost","mysql_user","mysql_pwd"); $id=$_GET['id']; //字符型的使用mysql_real_escape_string进行处理即:$id=mysql_real_escape_string($id); //如果使用了odp框架,字符型的可调用escapeString()方法进行过滤 $id=intval($id); sql="select username,password from users where uid=".$id; mysql_query($sql,$con);
在使用intval过滤时,不要使用if(intval($id)),这样会过滤失效,而应该使用
$id=intval($id); if($id) { $sql="select username,password from users where uid=".$id; mysql_query($sql,$con); }
思考
SQL注入使用内置安全转义函数过滤。