携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第19天,点击查看活动详情
SQL 注入是网络攻击中最为常见的攻击方式,通过向服务器端发送恶意的 SQL 语句或 SQL 语句片段注入到服务器端的数据库查询逻辑中,改变原有的查询逻辑,从而实现类恶意读取服务器数据库数据,攻击者甚至可以利用数据库内部函数或缺陷提升权限,从而获取服务器权限。
- 用户后台系统登陆注入 1.1 登陆位置注入测试 后台登陆系统注入在前些年是非常常见的,我们通常会使用' or '1'='1 之类的注入语句来构建一个查询结果永为真的 SQL,俗称:万能密码。
示例 - 用户登陆注入代码:
<%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ page import="java.sql.Connection" %> <%@ page import="java.sql.DriverManager" %> <%@ page import="java.sql.ResultSet" %> <%@ page import="java.util.HashMap" %> <%@ page import="java.util.Map" %>
<%
// MYSQL sys_user示例表,测试时请先创建对应的数据库和表
//
// CREATE TABLE sys_user (
// id int(9) unsigned NOT NULL AUTO_INCREMENT COMMENT '用户ID',
// username varchar(16) NOT NULL COMMENT '用户名',
// password varchar(32) NOT NULL COMMENT '用户密码',
// user_avatar varchar(255) DEFAULT NULL COMMENT '用户头像',
// register_time datetime DEFAULT NULL COMMENT '注册时间',
// PRIMARY KEY (id),
// UNIQUE KEY idx_sys_user_username (username) USING BTREE
// ) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8 COMMENT='系统用户表'
//
// INSERT INTO sys_user VALUES ('1', 'admin', '123456', '/res/images/avatar/default.png', '2020-05-05 17:21:27'), ('2', 'test', '123456', '/res/images/avatar/default.png', '2020-05-06 18:27:10'), ('3', 'root', '123456', '/res/images/avatar/default.png', '2020-05-06 18:28:27'), ('4', 'user', '123456', '/res/images/avatar/default.png', '2020-05-06 18:31:34'), ('5', 'rasp', '123456', '/res/images/avatar/default.png', '2020-05-06 18:32:08');
%>
<% String sessionKey = "USER_INFO"; Object sessionUser = session.getAttribute(sessionKey);
// 退出登陆
if (sessionUser != null && "exit".equals(request.getParameter("action"))) {
session.removeAttribute(sessionKey);
out.println("<script>alert('再见!');location.reload();</script>");
return;
}
Map<String, String> userInfo = null;
// 检查用户是否已经登陆成功
if (sessionUser instanceof Map) {
userInfo = (Map<String, String>) sessionUser;
out.println("<p>欢迎回来:" + userInfo.get("username") + ",ID:" + userInfo.get("id") + " \r<a href='?action=exit'>退出登陆</a></p>");
return;
}
String username = request.getParameter("username");
String password = request.getParameter("password");
// 处理用户登陆逻辑
if (username != null && password != null) {
userInfo = new HashMap<String, String>();
ResultSet rs = null;
Connection connection = null;
try {
Class.forName("com.mysql.jdbc.Driver");
connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/javaweb-bbs", "root", "root");
String sql = "select id,username,password from sys_user where username = '" + username + "' and password = '" + password + "'";
System.out.println(sql);
rs = connection.prepareStatement(sql).executeQuery();
while (rs.next()) {
userInfo.put("id", rs.getString("id"));
userInfo.put("username", rs.getString("username"));
userInfo.put("password", rs.getString("password"));
}
// 检查是否登陆成功
if (userInfo.size() > 0) {
// 设置用户登陆信息
session.setAttribute(sessionKey, userInfo);
// 跳转到登陆成功页面
response.sendRedirect(request.getServletPath());
} else {
out.println("<script>alert('登陆失败,账号或密码错误!');history.back(-1)</script>");
}
} catch (Exception e) {
out.println("<script>alert('登陆失败,服务器异常!');history.back(-1)</script>");
} finally {
// 关闭数据库连接
if (rs != null)
rs.close();
if (connection != null)
connection.close();
}
return;
}
%>
Login TestPassword:
id username password user_avatar register_time 1 admin 123456 /res/images/avatar/default.png 2020-05-05 17:21:27 2 test 123456 /res/images/avatar/default.png 2020-05-06 18:27:10 访问示例中的后台登陆地址:http://localhost:8000/modules/jdbc/login.jsp,如下图:
攻击者通过在密码参数处输入:'=0#即可使用 SQL 注入的方式改变查询逻辑,绕过密码认证并登陆系统,因此用于检测用户账号密码是否存在的 SQL 语句变成了:
select id,username,password from sys_user where username = 'admin' and password = ''=0#'
其中的 password 的值预期是传入用户密码,但是实际上被攻击者传入了可改变查询逻辑的 SQL 语句,将运算结果改变为 true,从而攻击者可以使用错误的用户及密码登陆系统,如下图:
毫无疑问因为攻击者输入的信息足够的短小简洁,但是对于用户网站系统来说却有极强的杀伤性,绝大多数的 WAF 或者 RASP 产品都无法精准辨别'=0#的威胁性,无法正确做到精准防御。
万能密码登陆注入原理图解:
- 文章详情页注入 通常情况下在用户系统发布文章后会在数据库中产生一条记录,并生成一个固定的文章 ID,用户浏览文章信息只需要传入文章 ID,即在后端通过文章 ID 查询文章详情信息。
示例 - 存在 SQL 注入的文章详情代码:
<%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ page import="java.sql.Connection" %> <%@ page import="java.sql.DriverManager" %> <%@ page import="java.sql.ResultSet" %> <%@ page import="java.util.HashMap" %> <%@ page import="java.util.Map" %>
<%
// MYSQL sys_article示例表,测试时请先创建对应的数据库和表
// CREATE TABLE sys_article (
// id int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '文章ID',
// user_id int(9) NOT NULL COMMENT '用户ID',
// title varchar(100) NOT NULL COMMENT '标题',
// author varchar(16) NOT NULL COMMENT '作者',
// content longtext NOT NULL COMMENT '文章内容',
// publish_date datetime NOT NULL COMMENT '发布时间',
// click_count int(11) unsigned NOT NULL DEFAULT '0' COMMENT '文章点击数量',
// PRIMARY KEY (id),
// KEY index_title (title) USING BTREE
// ) ENGINE=InnoDB AUTO_INCREMENT=100002 DEFAULT CHARSET=utf8 COMMENT='系统文章表';
//
// INSERT INTO sys_article VALUES ('100000', '1', '东部战区陆军:丢掉幻想,准备打仗!', 'admin', '<p style="font-family:PingFangSC-Regular, 微软雅黑, STXihei, Verdana, Calibri, Helvetica, Arial, sans-serif;font-size:16px;text-indent:32px;background-color:#FFFFFF;">\n 中国人民解放军东部战区陆军微信公众号“人民前线”4月15日发布《丢掉幻想,准备打仗! 》,以下为文章全文:\n
<% String id = request.getParameter("id"); Map<String, Object> articleInfo = new HashMap<String, Object>(); ResultSet rs = null; Connection connection = null;
if (id != null) {
try {
Class.forName("com.mysql.jdbc.Driver");
connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/javaweb-bbs", "root", "root");
String sql = "select * from sys_article where id = " + id;
System.out.println(sql);
rs = connection.prepareStatement(sql).executeQuery();
while (rs.next()) {
articleInfo.put("id", rs.getInt("id"));
articleInfo.put("user_id", rs.getInt("user_id"));
articleInfo.put("title", rs.getString("title"));
articleInfo.put("author", rs.getString("author"));
articleInfo.put("content", rs.getString("content"));
articleInfo.put("publish_date", rs.getDate("publish_date"));
}
} catch (Exception e) {
e.printStackTrace();
} finally {
// 关闭数据库连接
if (rs != null)
rs.close();
if (connection != null)
connection.close();
}
}
%>
<%=articleInfo.get("title")%><%=articleInfo.get("title")%>
作者:<%=articleInfo.get("author")%> - <%=articleInfo.get("publish_date")%>
id user_id title author content publish_date click_count 100000 1 东部战区陆军:丢掉幻想,准备打仗! admin 文章内容 2020-04-19 17:35:06 4 100001 1 面对战争,时刻准备着! admin 文章内容 2020-04-19 17:37:40 17 访问示例程序并传入参数 id=100001 后会显示文章详情,请求:http://localhost:8000/modules/jdbc/article.jsp?id=100001,如下图:
2.1 union select 类型的 SQL 注入攻击测试 攻击者在 ID 处构造并传入恶意的 SQL 注入语句后,可以轻松的读取出数据库信息,如将请求中的 id 参数值改为 100001 and 1=2 union select 1,2,user(),version(),database(),6,7, 服务器端将会返回数据库名称、请求:http://localhost:8000/modules/jdbc/article.jsp?id=100001%20and%201=2%20union%20select%201,2,user(),version(),database(),6,7,如下图:
由于攻击的 Payload 中包含了 union、select、user()、version()、database() 敏感关键字,大部分的 WAF 都能够识别此类 SQL 注入。
2.2 算数运算结果探测型攻击测试 但如果攻击者将注入语句改为检测语句:100001-1 的时候页面会输出文章 id 为 100000 的文章,由于 id 参数存在注入,数据库最终查询到的文章 id 为 100001-1 也就是 id 为 100000 的文章,请求:http://localhost:8000/modules/jdbc/article.jsp?id=100001-1,如下图:
几乎可以绕过 99% 的 WAF 和大部分的 RASP 产品了,此类 SQL 注入攻击属于不具有攻击性的探测性攻击。
2.3 数据库函数型攻击测试 部分攻击者使用了数据库的一些特殊函数进行注入攻击,可能会导致 WAF 无法识别,但是 RASP 具备特殊函数注入攻击的精准检测和防御能力。
例如上述示例中攻击者传入的 id 参数值为:(100001-1) 或者 (100001) 用于探测数据表中是否存在 id 值为 100000 的文章,请求:http://localhost:8000/modules/jdbc/article.jsp?id=(100001),如下图:
或者传入的 id 参数值为:(select 100000) 来探测数据库是否存在 id 值为 100000 的文章,请求:http://localhost:8000/modules/jdbc/article.jsp?id=(select%20100000),如下图:
大多数数据库支持使用 () 来包裹一个整数型的字段值,但是 99% 的 WAF 和极大多数的 RASP 产品是无法识别此类型的注入攻击的。