0.前言
本篇博文是對SQL手工注入進行基礎知識的講解,更多進階知識請參考進階篇(咕咕),文中有誤之處,還請各位師傅指出來。學習本篇之前,請先確保以及掌握了以下知識:
- 基本的SQL語句
- HTTP的GET、POST請求,URL編碼
文中所有例題選自sqlilab,可以先配置好一起邊看邊操作。因為虛擬機炸了,所以我自己搭建了一個簡陋的平台,sqlilab可自行進行搭建練習,在后面的博客也會寫一些關於sqlilab的wp。
1.准備工作
在開始SQL注入之前,我們首要需要了解SQL注入的原理,對於一個新安裝好的MySql數據庫,你至少會包含三個已經建立好的數據庫分別是user、infomation_schema、performance_schema如下圖所示
而SQL注入要干的,這些系統數據庫中存儲了MySql各個數據庫的屬性以及用戶信息,我們要做的就是繞過過濾再來利用這些系統表進行查詢。
了解了這個之后我們還要了解一點就是PHP的GET方法和POST方法傳參的區別,如果使用GET方法,則會自動進行一次url解碼,例如傳入%23實際得到‘#’,而POST則會將數據原封不動的傳輸。下面我們開始進入正式的SQL注入階段。
2.判斷注入類型
一般的對於SQL的查詢語句,有字符型和數值型查詢,而這兩者的區別就是是否有單引號,這決定了我們接下來應該如何構造SQL注入語句。
判斷方法有如下幾種,
- +-數值,如果是數值型的,你可以嘗試使用1+1,1+2,這樣的語句,例如
看網頁回顯是否正確。
- and 1=1,and 1=2,直接在后面添加“1 and 1=1”和“1 and 1=2”(前面有個空格)來進行查詢,若1=1回顯正確而1=2回顯錯誤則為數值型。
- 加‘#,在后面添加'#進行查詢,若回顯正確則表明為字符型。
除了上述幾種方法還可以用其他方法進行判斷,但原理都是構造SQL語句進行判斷。
3.查列數
通過上述方法知道了注入類型之后,我們就可以進行下一步的操作了,在這里我搭建了一個簡易的存在字符型查詢漏洞的頁面,大家可以在本地搭建一下一邊學習一邊練習。
SQL代碼如下:

1 /* 2 Navicat MySQL Data Transfer 3 4 Source Server : Mysql 5 Source Server Version : 50553 6 Source Host : localhost:3306 7 Source Database : test 8 9 Target Server Type : MYSQL 10 Target Server Version : 50553 11 File Encoding : 65001 12 13 Date: 2019-09-18 23:17:53 14 */ 15 16 SET FOREIGN_KEY_CHECKS=0; 17 18 -- ---------------------------- 19 -- Table structure for secret_table 20 -- ---------------------------- 21 DROP TABLE IF EXISTS `secret_table`; 22 CREATE TABLE `secret_table` ( 23 `fl4g` varchar(32) DEFAULT NULL 24 ) ENGINE=MyISAM DEFAULT CHARSET=utf8; 25 26 -- ---------------------------- 27 -- Records of secret_table 28 -- ---------------------------- 29 INSERT INTO `secret_table` VALUES ('flag_is_here'); 30 31 -- ---------------------------- 32 -- Table structure for student 33 -- ---------------------------- 34 DROP TABLE IF EXISTS `student`; 35 CREATE TABLE `student` ( 36 `id` int(11) NOT NULL AUTO_INCREMENT, 37 `name` varchar(255) DEFAULT NULL, 38 `class` varchar(255) DEFAULT NULL, 39 `age` int(11) DEFAULT NULL, 40 `note` varchar(16) DEFAULT '', 41 PRIMARY KEY (`id`) 42 ) ENGINE=MyISAM AUTO_INCREMENT=9 DEFAULT CHARSET=utf8; 43 44 -- ---------------------------- 45 -- Records of student 46 -- ---------------------------- 47 INSERT INTO `student` VALUES ('1', 'zhangsan', 'nss', '18', ''); 48 INSERT INTO `student` VALUES ('2', 'lisi', 'nss', '19', ''); 49 INSERT INTO `student` VALUES ('3', 'wangwu', 'nss2', '20', ''); 50 INSERT INTO `student` VALUES ('4', 'zhaoliu', 'nss2', '21', ''); 51 INSERT INTO `student` VALUES ('5', 'sunqi', 'nss2', '22', ''); 52 INSERT INTO `student` VALUES ('6', 'qianba', 'nss', '23', ''); 53 INSERT INTO `student` VALUES ('7', 'liujiu', 'nss', '24', '');
PHP代碼如下:

1 <?php 2 if (!empty($_POST['st'])) { 3 $conn = mysqli_connect("localhost","root","root","test"); 4 $name = $_POST['name']; 5 $sql = "select * from student where name='".$name."';"; 6 7 $result = mysqli_query($conn,$sql); 8 9 echo "<table border='1'> 10 <tr> 11 <th>Id</th> 12 <th>Name</th> 13 <th>Class</th> 14 <th>Age</th> 15 <th>Note</th> 16 </tr>"; 17 18 while($row = mysqli_fetch_array($result)) { 19 echo "<tr>"; 20 echo "<td>" . $row['id'] . "</td>"; 21 echo "<td>" . $row['name'] . "</td>"; 22 echo "<td>" . $row['class'] . "</td>"; 23 echo "<td>" . $row['age'] . "</td>"; 24 echo "<td>" . $row['note'] . "</td>"; 25 echo "</tr>"; 26 } 27 echo "</table>"; 28 29 mysqli_close($conn); 30 } 31 ?> 32 33 <!doctype html> 34 <!-- flag in SQL --> 35 <form action="" method="POST"> 36 <input type="text" placeholder="Input A Name" name="name" value=""> 37 <button type="submit" name="st" value='1'>提交</button> 38 </form> 39 <div style="position: absolute;bottom:0;width:100%;display:flex;flex-direction:column;"> 40 <hr > 41 <a style="align-self:center;" href="./source.txt">Source</a> 42 </div>
通過上面的判斷我們知道是字符型注入,現在我們需要查出這個數據表的列數來為后面的聯合查詢做鋪墊。
當查詢語句最后為where xx 的時候我們使用order by num;
當查詢語句最后為limit xx的時候我們使用into @,@;
對於第一種order by num; num代表數值,語義就是以第幾列進行排序,當列不存在是就會報錯,我們就可以用二分的方法找出正確的列數。如下
對於lisi' order by 6#,lisi是數據庫中的正常數據,單引號是為了閉合前面的select語句,#是mysql的單行注釋語句,提示報錯,換成5則正確,說明該表有5列。
對於limit xx的情況,我會在另一篇額外講解。
4.確定字段位置
當我們獲得表的列數之后,就可以通過聯合查詢獲得數據庫的信息,但在此之前,我們還需要確定每個字段顯示在網頁上的位置,方便查看后面的數據。
對於例題,我們知道列數為5之后,構造參數lisi' and 1=2 union select 1,2,3,4,5#即可知道每個字段的位置
完整的SQL語句就是select * from student where name = ‘lisi’ and 1=2 union select 1,2,3,4,5#';
and 1=2是為了避免一些只顯示一行的頁面過濾掉后面的聯合查詢的數據,所以讓前面的查詢條件永遠為false,這里你不加這個,但為了方便我們都加上這個。
union就是合並操作,完整語義就是使用union合並兩個select的結果集,前者為空,后者結果為1,2,3,4,5,所以就會把這些結果合並然后顯示到對應的字段。
5.獲取數據庫信息
當列數和字段位置都知道后,我們就可以通過進一步的查詢來獲取數據庫信息了,下面提供一些常用的數據庫函數。
database():查看當前數據庫名稱
version():查看數據庫版本信息
user():返回當前數據庫連接的用戶
char():將ASCII碼轉化成字符,用於分隔每個字段的內容
使用方法就是將函數放在列的位置,例如提交lisi' and 1=2 union select 1,user(),database(),4,5#,結果如下:
同樣在最開始的時候我們提到了MySql的幾個系統表,我們也可以從這些系統表中獲取所有表名,列數,字段名等數據。
例如查詢所有表名,提交lisi' and 1=2 union select 1,2,3,4,TABLE_NAME from INFORMATION_SCHEMA.TABLES where TABLE_SCHEMA='test,結果如下
第五個位置也就是查詢INFORMATION_SCHEMA.TABLES表中數據庫為test的所有表名,MySql的表屬性會存儲在這個表中,因為后面還有一個單引號,所以這里右邊就不要單引號了。
這樣我們就查詢到了所有的表名,現在我們發現有個表叫做secret_table,猜測flag隱藏在其中,那么我們再來獲取這個表所有的字段名。
提交lisi' and 1=2 union select 1,2,3,4,COLUMN_NAME from INFORMATION_SCHEMA.COLUMNS where TABLE_NAME='secret_table。結果如下
語義同上,這里就不在做解釋了,現在我們可以看到這個表有一個叫fl4g的字段,那flag就藏在這里沒錯了。
接下來要做的就簡單了,我們只需要提交lisi' and 1=2 union select 1,2,3,4,fl4g from secret_table#
到此我們就找到了最終的flag。
6.總結
SQL手工注入的基礎知識道馳就結束了,推薦看完了這篇再去學習SQLmap的知識,可以很快上手也可以了解其本質的東西,不推薦直接學習SQLmap成為腳本小子。
再梳理一遍上述知識,首先找到注入點,注入點一般是網頁的某個提供查詢的地方,然后確定是字符型還是數值型,當確定了注入類型之后,就是進行確定一些數據表的信息,方便后面的盲注,最后在從這些注入點得到我們想要的信息。
對於CTF題目來說,一般不是讓你盲注,會將代碼給你,這時候注入點肯定是會有諸多的過濾,這時候我們就不能直接執行上述語句了,我們就需要構造一些語句去繞過執行,但最終要達到的效果和上述內容是一致的。
對於更多的SQL注入知識,以及一些繞過技巧我將會在后面的進階篇詳解闡述。