管中窥豹——框架下的SQL注入 Java篇
背景
SQL注入漏洞应该算是很有年代感的漏洞了,但是现在依然活跃在各大漏洞榜单中,究其原因还是数据和代码的问题。
SQL 语句在DBMS系统中作为表达式被解析,从存储的内容中取出相应的数据, 而在应用系统中只能作为数据进行处理。
各个数据库系统都或多或少的对标准的SQL语句进行了扩展
Oracle的PL/SQL
SQL Server的存储过程
Mysql也作了扩展(PS:不过我不知道这扩展叫什么名字
既然问题很清楚是什么了,大佬们的解决方案也不会慢——预编译和ORM框架
从我目前来感觉来看,就是封装,把你可能用到的语句封装起来,明确你数据的位置,再根据SQL语句的语法防止数据影响到真正的语义
ORM框架与预编译
预编译
预编译的指令方式用起来多少有点繁琐,大部分都会采用相关的ORM框架来解决问题,但是多少需要了解,另外呢,再尝试编写sql的转义器的时候,我估计我还得读读这些底层的实现作为参考,原因嘛,自然是场景几乎一致,老司机的东西肯定比我拍脑袋的强(PS:实际上我需要的太简单了,预编译对不同类型均有不同的处理)。
JAVA
// Java.sql 包
PreparedStatement preparedStatement=connection.prepareStatement("SELECT * FROM users WHERE name =?;"); // ?号为占位符,表示此处有输入的变量
preparedStatement.setString(1,name); // 通过set的方式设置变量
C
涉及的类,分别是sqlParameter、DataAdapter、
// 参考:https://www.cnblogs.com/wangwangwangMax/p/5551614.html
public string Getswhere()
{
StringBuilder sb = new StringBuilder();
sb.Append("select ID,username,PWD,loginname,qq,classname from Users where 1=1");
//获取到它的用户名
string username = TxtUserName.Text.Trim();
if (!string.IsNullOrEmpty(username))
{
//sb.Append(string.Format("and username='{0}'", username));
//防SQL注入,通过@传参的方式
sb.Append(string.Format("and username=@username"));
//怎么把值传进去,通过sqlParameter数组
//SqlParameter[] para = new SqlParameter[]
//{
// //创建一个SqlParameter对象(第一个传名称,第二个传值)
// new SqlParameter("@username",username)
//};
// para[0]表示数组对象的第一个里面添加
//para[0] = new SqlParameter("@username",username);
para.Add(new SqlParameter("@username", username));
}
if(ddlsclass.SelectedIndex>0)
{
//sb.Append(string.Format("and ClassName='{0}'", ddlsclass.SelectedValue));
sb.Append(string.Format("and ClassName=@ClassName"));
//para[1] = new SqlParameter("@ClassName",ddlsclass.SelectedValue);
para.Add(new SqlParameter("@ClassName", ddlsclass.SelectedValue));
}
return sb.ToString();
}
ORM框架
Java
Java下目前基本上都是采用了mybatis框架进行处理了吧,反正我目前接触到的都是这个。
mybatis
在java代码调用mapper的方法,实现数据库查询,框架将查询的结果映射到xml文件中配置的结果集上,详细的底层原理可以查看图片下方的原文链接。
参考:https://blog.csdn.net/luanlouis/article/details/40422941
当然除了xml配置文件的方式,还支持注解,不过目前接触到的主流都是xml,偶尔有在代码中看到几行简单查询的注解。
一般而言${}表示动态拼接——容易导致SQL注入,#{}表示参数绑定——不会导致SQL注入 (后文会尝试从mybatis框架上看看到底什么区别)
xml文件一个个去写,其实也是蛮大的工作量,当然大佬们已经想到这个问题了,基本上都会采用相关的插件来生成一个能满足基本需求的xml文件、mapper类以及实体类(处理输入和输出)
目前我接触到的有两个
mybatis-generator (maven的插件)
idea mybatis-generator (idea的插件)
mybatis-generator (maven的插件)
需要配置 generatorConfig.xml (包含了jdbc的账号和密码,一般会放在resouces目录下)
PS: 可以关注的信息泄露的点
生成的实体类包括 tableName 和tableNameExample
tableNameExample作为查询的条件输入类,tableName主要用于结果输出类,两者在功能上做了分离
/**
* This method was generated by MyBatis Generator.
* This method corresponds to the database table asset_group
*
* @mbg.generated Fri Aug 10 18:44:32 CST 2018
*/
List selectByExample(AssetGroupExample example);
/**
* This method was generated by MyBatis Generator.
* This method corresponds to the database table asset_group
*
* @mbg.generated Fri Aug 10 18:44:32 CST 2018
*/
AssetGroup selectByPrimaryKey(Integer id);
tableNameExample作为条件的实现,依赖了动态参数(字段名动态), 下文会探讨这样做会不会有什么问题
and ${criterion.condition}
and ${criterion.condition} #{criterion.value}
and ${criterion.condition} #{criterion.value} and #{criterion.secondValue}
and ${criterion.condition}
#{listItem}
idea mybatis-generator (idea的插件)
idea是商用的IDE,我先放个图看看
与上文的不同,该插件生成的实体类只有两个,但是mapper和xml均生成了两组,有继承关系
tableNameBaseMapper 和 tableNameMapper
(代码里没有体现,实现在使用的时候有,新增的sql语句可以放到tableNameMapper里,看起来比较清爽,点开basemapper对应的xml文件就知道了)
实体类中封装了内部类,用于构造复杂的查询条件
xml文件也写的完全不一样,因为没有采用动态的方式,所以每个xml都很大。 估计设计上分离就是因为这个原因,如果也在这个文件里,可能会找不到...
`ID` = #{ID} and
`ID` in
#{item}
and
(
`NAME` like concat('%',#{item},'%')
) and
(
`NAME` like concat(#{item},'%')
) and
`CREATE_TIME` >= #{cREATETIMESt} and
`CREATE_TIME` <= #{cREATETIMEEd} and
mapper里封装的方法
默认生成的以[query|update]{EntityName}[Limit1]? 以及query|update构成的方法名称
python
django 自带的ORM框架
Flask flask_sqlalchemy
C
简单搜了下花样比较多...就不写了
mybatis框架解析原理
SqlSessionFactoryBuilder.build 入口
生成DefaultSqlSessionFactory ,调用xmlconfigbuilder进行初始化
XMLConfigBuilder (org.apache.ibatis.builder.xml)
负责解析mapper的配置文件,其中mapperParser.parse();函数会对配置的主体部分(sql语句、mapper节点下的内容)进行解析
解析完成后,将Sql节点存放到 Map sqlFragments 结构上;
进一步的解析调用buildStatementFromContext进一步解析
最终生成了MappedStatement存储在configuration对象中
调用SqlSessionFactory.opensession,默认生成DefaultSqlSession,调用其方法进行查询等操作
MappedStatement ms = configuration.getMappedStatement(statement); // 取出之前生成的mappedstatement
return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER); // 调用执行器,执行 默认将parameter封装成数组, 其他根据其类型支持collection 和 list
// 执行器最终会调用preparestatement 通过预编译完成
MappedStatement的getBoundSql方法
DynamicContext context = new DynamicContext(configuration, parameterObject); // 包装输入的参数parameterObject
rootSqlNode.apply(context); // 实际上在这个阶段完成SQL预计动态拼接的,同时会调用OGNL表达式获取相关值,根据不同类型的SQLNode不同的拼接方式,文本是直接添加,其他的部分可能调用ognl表达式获取值
// ....
SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings()); // 参数拼接的函数
#{}类型 -> 转化调用java的预编译
// parse(...) #{}形式的参数处理,
GenericTokenParser parser = new GenericTokenParser("#{", "}", handler);
/*
转化成固定的返回 ? 用于预编译
*/
String sql = parser.parse(originalSql);
return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
// Class: GenericTokenParser
// parse(String)
public String parse(String text) {
if (text == null || text.isEmpty()) {
return "";
}
// search open token
int start = text.indexOf(openToken, 0);
// .....
while (start > -1) {
if (start > 0 && src[start - 1] == '\\') {
// this open token is escaped. remove the backslash and continue. 如果存在反斜杠的转义自动掠过
// ..
} else {
// found open token. let's search close token.
if (expression == null) {
expression = new StringBuilder(); // 实际上就是处理完一些特殊符号后#{}中间的内容
} else {
expression.setLength(0);
}
builder.append(src, offset, start - offset);
offset = start + openToken.length();
int end = text.indexOf(closeToken, offset);
while (end > -1) {
if (end > offset && src[end - 1] == '\\') {
// this close token is escaped. remove the backslash and continue. 如果存在反斜杠的转义自动掠过
// .....
}
if (end == -1) {
// close token was not found.
builder.append(src, start, src.length - start);
offset = src.length;
} else {
/*
转化成固定的返回 ? 用于预编译
// SqlSourceBuilder
public String handleToken(String content) {
parameterMappings.add(buildParameterMapping(content));
return "?";
}
根据之前声明的参数类型映射prepare相应的set函数,例如setString
*/
builder.append(handler.handleToken(expression.toString()));
offset = end + closeToken.length();
}
}
start = text.indexOf(openToken, offset);
}
if (offset < src.length) {
builder.append(src, offset, src.length - offset);
}
return builder.toString();
}
// // \t\n\r\f 会被替换成空格,重构sql语句
// org.apache.ibatis.executor.statement
// class: PreparedStatementHandler : instantiateStatement(connection)
String sql = boundSql.getSql();
if (mappedStatement.getKeyGenerator() instanceof Jdbc3KeyGenerator) {
String[] keyColumnNames = mappedStatement.getKeyColumns();
if (keyColumnNames == null) {
return connection.prepareStatement(sql, PreparedStatement.RETURN_GENERATED_KEYS);
} else {
return connection.prepareStatement(sql, keyColumnNames);
}
} else if (mappedStatement.getResultSetType() != null) {
return connection.prepareStatement(sql, mappedStatement.getResultSetType().getValue(), ResultSet.CONCUR_READ_ONLY);
} else {
return connection.prepareStatement(sql); // jdbc的预编译
}
PS: 后续有时间再去了解底层的实现。
常见的安全问题
信息泄露/拒绝服务风险
提供空值或者空的对象,导致查询空值条件失效,实现了全库查询,可能造成信息泄露或者DOS风险。
idea生成的例子如下:
`ID` = #{ID} and
maven generator插件生成的代码由于没有强制的判定,似乎不会造成该风险(仅限select语句)
SQL注入风险
#{}采用了jdbc的预编译不存在风险,但是${}在构建语句的过程是需要进行表达式的计算的是动态拼接到语句中,如果直接采用这种方式存在SQL注入的风险。
在预编译中各个类型都有相应的set函数,还有一些的函数,例如setInternal, 对于输入的变量不做任何处理,如果直接拼接了变量到其中也会存在相应的安全风险
对于maven上的generator插件而言,生成的mapper.xml大致如下:
其中 order by就存在注入的风险,语句如下:
mysql 如下:
IF(1=1,1,(select+1+from+information_schema.tables))
updatexml(1,if(1=1,1,user()),1)
(CASE+WHEN+(1=1)+THEN+name+ELSE+price+END)
oracle如下:
CASE WHEN (ASCII(SUBSTRC((SELECT NVL(CAST(USER AS VARCHAR(4000)),CHR(32)) FROM DUAL),3,1))>96) THEN DBMS_PIPE.RECEIVE_MESSAGE(CHR(71)||CHR(106)||CHR(72)||CHR(73),1) ELSE 7238 END)
order by CASE WHEN 1=1 THEN 1 ELSE 0 END DESC
mssql:
https://github.com/incredibleindishell/exploit-code-by-me/blob/master/MSSQL%20Error-Based%20SQL%20Injection%20Order%20by%20clause/Error%20based%20SQL%20Injection%20in%20%E2%80%9COrder%20By%E2%80%9D%20clause%20(MSSQL).pdf
另外还有一处如下:
and ${criterion.condition}
and ${criterion.condition} #{criterion.value}
and ${criterion.condition} #{criterion.value} and #{criterion.secondValue}
and ${criterion.condition}
#{listItem}
不过生成的相关