管中窥豹——框架下的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<AssetGroup> 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作为条件的实现,依赖了动态参数(字段名动态), 下文会探讨这样做会不会有什么问题
    <where>
<foreach collection="oredCriteria" item="criteria" separator="or">
<if test="criteria.valid">
<trim prefix="(" prefixOverrides="and" suffix=")">
<foreach collection="criteria.criteria" item="criterion">
<choose>
<when test="criterion.noValue">
and ${criterion.condition}
</when>
<when test="criterion.singleValue">
and ${criterion.condition} #{criterion.value}
</when>
<when test="criterion.betweenValue">
and ${criterion.condition} #{criterion.value} and #{criterion.secondValue}
</when>
<when test="criterion.listValue">
and ${criterion.condition}
<foreach close=")" collection="criterion.value" item="listItem" open="(" separator=",">
#{listItem}
</foreach>
</when>
</choose>
</foreach>
</trim>
</if>
</foreach>
</where>
idea mybatis-generator (idea的插件)
  • idea是商用的IDE,我先放个图看看

  • 与上文的不同,该插件生成的实体类只有两个,但是mapper和xml均生成了两组,有继承关系

tableNameBaseMapper 和 tableNameMapper

(代码里没有体现,实现在使用的时候有,新增的sql语句可以放到tableNameMapper里,看起来比较清爽,点开basemapper对应的xml文件就知道了)

  • 实体类中封装了内部类,用于构造复杂的查询条件

  • xml文件也写的完全不一样,因为没有采用动态的方式,所以每个xml都很大。 估计设计上分离就是因为这个原因,如果也在这个文件里,可能会找不到...

<trim prefix="where" suffixOverrides="and | or">
<!-- 基础的字段 省略了很多 -->
<if test="ID != null">
`ID` = #{ID} and
</if>
<if test = "(_parameter instanceof xx.xxx.xxx.xxx.ApplicationFunctions$QueryBuilder) == true">
<!-- 列表类型 -->
<if test="IDList != null">
`ID` in
<foreach collection="IDList" close=")" open="(" separator="," item="item">
#{item}
</foreach> and
</if>
<!-- 模糊查询 -->
<if test ="fuzzyNAME!=null and fuzzyNAME.size()>0">
(
<foreach collection="fuzzyNAME" separator="or" item="item">
`NAME` like concat('%',#{item},'%')
</foreach>
) and
</if>
<if test ="rightFuzzyNAME!=null and rightFuzzyNAME.size()>0">
(
<foreach collection="rightFuzzyNAME" separator="or" item="item">
`NAME` like concat(#{item},'%')
</foreach>
) and
</if>
<!-- 比较 -->
<if test="cREATETIMESt !=null">
`CREATE_TIME` >= #{cREATETIMESt} and
</if>
<if test="cREATETIMEEd!=null">
`CREATE_TIME` &lt;= #{cREATETIMEEd} and
</if>
</if>
</trim>
  • 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<String, XNode> 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生成的例子如下:
 <if test="ID != null">
`ID` = #{ID} and
</if>
<!-- 基本上如上,会包含一个test语句,用于确认当前的条件是否为null,对于字符串还会判断是否为空的字符串,如果为null或者空,当前的条件控制失效
#{}方式如果是字符串会默认添加'',空值的方式可能会差不到数据, null的情况下会查询条件被忽略
-->
  • maven generator插件生成的代码由于没有强制的判定,似乎不会造成该风险(仅限select语句)

SQL注入风险

  • #{}采用了jdbc的预编译不存在风险,但是${}在构建语句的过程是需要进行表达式的计算的是动态拼接到语句中,如果直接采用这种方式存在SQL注入的风险。
  • 在预编译中各个类型都有相应的set函数,还有一些的函数,例如setInternal, 对于输入的变量不做任何处理,如果直接拼接了变量到其中也会存在相应的安全风险
  • 对于maven上的generator插件而言,生成的mapper.xml大致如下:
<select id="selectByExample" parameterType="*.*.*Example" resultMap="BaseResultMap">
<!--
WARNING - @mbg.generated
This element is automatically generated by MyBatis Generator, do not modify.
This element was generated on Mon Mar 18 14:12:57 CST 2019.
-->
select
<if test="distinct">
distinct
</if>
<include refid="Base_Column_List" />
from asset_app
<if test="_parameter != null">
<include refid="Example_Where_Clause" />
</if>
<if test="orderByClause != null">
order by ${orderByClause}
<!-- 存在了${}拼接 -->
</if>
</select>
<sql id="Update_By_Example_Where_Clause">
<!--
WARNING - @mbg.generated
This element is automatically generated by MyBatis Generator, do not modify.
This element was generated on Mon Mar 18 14:12:57 CST 2019.
-->
<where>
<foreach collection="example.oredCriteria" item="criteria" separator="or">
<if test="criteria.valid">
<trim prefix="(" prefixOverrides="and" suffix=")">
<foreach collection="criteria.criteria" item="criterion">
<choose>
<when test="criterion.noValue">
<!-- 存在了${}拼接 -->
and ${criterion.condition}
</when>
<when test="criterion.singleValue">
and ${criterion.condition} #{criterion.value}
</when>
<when test="criterion.betweenValue">
and ${criterion.condition} #{criterion.value} and #{criterion.secondValue}
</when>
<when test="criterion.listValue">
and ${criterion.condition}
<foreach close=")" collection="criterion.value" item="listItem" open="(" separator=",">
#{listItem}
</foreach>
</when>
</choose>
</foreach>
</trim>
</if>
</foreach>
</where>
</sql>

不过生成的相关example类的时候已经封装了各种方法,只要不去直接调用addCriterion去尝试对字段名(函数的第一个参数)进行动态设置,不存在安全风险,如下:


public Criteria andIdIsNull() {
addCriterion("ID is null");
return (Criteria) this;
} public Criteria andIdIsNotNull() {
addCriterion("ID is not null");
return (Criteria) this;
} public Criteria andIdEqualTo(Integer value) {
addCriterion("ID =", value, "id");
return (Criteria) this;
} public Criteria andIdNotEqualTo(Integer value) {
addCriterion("ID <>", value, "id");
return (Criteria) this;
} public Criteria andIdGreaterThan(Integer value) {
addCriterion("ID >", value, "id");
return (Criteria) this;
}
  • idea的generator插件生成的mapper中不存在注入的风险,但是也没有提供order by的封装,可能会需要人工去编写相关的语句,在此时就要关注可能存在的注入风险。

删库风险

  • 与第一条可能比较像,但是风险不太一样,单独拉了一条。
  • 我们看generator插件生成的xml文件中关于delete方法的声明(PS:idea生成的mapper中没有关于delete方法的声明)
<delete id="deleteByExample" parameterType="com.sse.security.sys.entity.VulDetailsExample">
<!--
WARNING - @mbg.generated
This element is automatically generated by MyBatis Generator, do not modify.
This element was generated on Tue Sep 25 15:41:07 CST 2018.
-->
delete from vulnerability_details
<if test="_parameter != null">
<include refid="Example_Where_Clause" />
</if>
</delete>
  • _parameter是mybatis的内置变量,代表整个输入的对象,如果对象为null,就会造成删库,但是貌似这种情况条件有一点苛刻。
  • 不过对于目前的应用系统而言,delete方式应该处于被弃用的状态,除了针对账号注销的这类场景。

OGNL引入可能带入的后门问题

  • 在mybatis的框架中动态参数实际上是采用OGNL表达式进行处理
package org.apache.ibatis.ognl
  • 通过getValue定位的相关函数如下:

  • 那么可知支持OGNL表达式有以下这些标签或者属性:

    • if/when标签的test属性

    • foreach标签的collection属性

    • #{}或者${}中间的变量部分

    • bind标签的value属性(由name和value组成的变量会注入到context中)

    • 注: 参考以下动态节点对应的相关类

    map.put("trim", new TrimHandler());
map.put("where", new WhereHandler());
map.put("set", new SetHandler());
map.put("foreach", new ForEachHandler());
map.put("if", new IfHandler());
map.put("choose", new ChooseHandler());
map.put("when", new IfHandler());
map.put("otherwise", new OtherwiseHandler());
map.put("bind", new BindHandler());
handleToken,方法
  • 尝试过程

    • 选择以下payload进行尝试
      @java.lang.Runtime@getRuntime().exec('calc')
    • 在相关位置添加OGNL表达式后测试以下几点

      1. 在加载配置时能否触发代码
      2. 在执行语句的能否触发代码
      3. 在已经启用的应用程序中动态插入能否触发代码(PS:实际测试过程均不行,但是针对不同应用场景下,可能存在热加载的问题)
    • if/when标签的test属性

      • 情况1 触发代码
      • 情况2 触发代码
      • 情况3 无法触发
      • PS: when标签放在默认语句的最后一行无法触发,但是第一行却可被触发
    • foreach标签的collection属性

      • 情况1 触发代码
      • 情况2 触发代码
      • 情况3 无法触发
      • PS: 由于返回的对象不一定是一个iterable,日志中会有相关的错误提示。影响正常请求的访问
    • bind标签的value属性

      • 情况1 触发代码
      • 情况2 触发代码
      • 情况3 无法触发
    • #{}或者${} PS: #{}无法触发 (会调用get/set方法,没有使用ognl)

      • 情况1 触发代码
      • 情况2 触发代码
      • 情况3 无法触发
    • 补充测试,在原来目录下直接添加一个mapper文件查看,是否会被加载

      • 不会自动加载

解决方案

  • 通用情况

    • 对数据进行非空、非null的判断,避免一些条件被规避
    • 框架有些地方没办法转换成相应合适的预编译,有条件还是需要去配置一个全局的过滤器
  • 针对idea的生成器
    • 需要对条件进行分析,哪些的必要条件,哪些不是。必要条件必须对空值和null值判断,可以去修正自动生成的mapper
  • 针对maven插件的生成器
    • 避免直接调用addCriterion函数,第一个参数避免由外部输入,如果有必要可以通过枚举类结合switch case控制
    • orderByClause属性设置时,注意避免外部输入。如果有必要进行动态设置。那么需要采用枚举类结合switch case控制或者对输入的数据进行过滤,仅保留字母数字下划线逗号,至于递增还是递减的控制,通过switch case 控制后拼接字符串常量。
  • 后门问题
    • 框架实现的机制,没有办法修复。

总结

  • mybatis的框架梳理的还比较乱,有机会再理理。

参考

管中窥豹——框架下的SQL注入 Java篇的更多相关文章

  1. caffe框架下目标检测——faster-rcnn实战篇操作

    原有模型 1.下载fasrer-rcnn源代码并安装 git clone --recursive https://github.com/rbgirshick/py-faster-rcnn.git 1) ...

  2. Mybatis下的sql注入

    以前只知道mybatis框架下,order by后面接的是列名是不能用#{},这样不起效果,只能用${},这样的话就可能产生sql注入.后来发现其实还有另外两种情况也是类似的: 1.order by ...

  3. 代码审计-Thinkphp3框架EXP表达式SQL注入

    最近看java框架源码也是看的有点头疼,好多还要复习熟悉 还有好多事没做...慢慢熬. 网上好像还没有特别详细的分析 我来误人子弟吧. 0x01 tp3 中的exp表达式 查询表达式的使用格式: $m ...

  4. SQL注入总结篇

    分类SQL注入的攻击方式根据应用程序处理数据库返回内容的不同,可以分为可显注入.报错注入和盲注. 可显注入攻击者可以直接在当前界面内容中获取想要获得的内容. 报错注入数据库查询返回结果并没有在页面中显 ...

  5. SQL注入漏洞篇

    一篇SQL注入漏洞汇总,更新中-- 如有缺陷 望大佬指正 SQL注入产生的原因? 当程序执行逻辑时没有对用户输入的参数做过滤处理,使参数直接与后台数据库产生逻辑交互,即SQL注入黑客就可以利用各种SQ ...

  6. Drupal V7.3.1 框架处理不当导致SQL注入

    这个漏洞本是2014年时候被人发现的,本着学习的目的,我来做个详细的分析.漏洞虽然很早了,新版的Drupal甚至已经改变了框架的组织方式.但是丝毫不影响对于漏洞的分析.这是一个经典的使用PDO,但是处 ...

  7. 防止常见XSS 过滤 SQL注入 JAVA过滤器filter

    XSS : 跨站脚本攻击(Cross Site Scripting),为不和层叠样式表(Cascading Style Sheets, CSS)的缩写混淆,故将跨站脚本攻击缩写为XSS.恶意攻击者往W ...

  8. 浅析PHP框架Laravel最新SQL注入漏洞

    PHP知名开发框架Laravel,之前在官方博客通报了一个高危SQL注入漏洞,这里简单分析下. 首先,这个漏洞属于网站coding写法不规范,官方给了提示: 但官方还是做了修补,升级最新版本V5.8. ...

  9. yii框架各种防止sql注入,xss攻击,csrf攻击

    PHP中常用到的方法有: /*  防sql注入,xss攻击  (1)*/    function actionClean($str)    {        $str=trim($str);      ...

随机推荐

  1. Java连载22-for循环

    一.循环结构 在程序当中总有一些需要反复的/重复的执行的代码,假设没有循环结构,那么这段需要重复执行的代码自然式子最需要重复编写的,代码无法得到重复使用,所以多数编程语言都是支持循环结构的,将来把需要 ...

  2. Unity的UGUI在SetParent后修改UI的localposition问题

    正常情况下,UGUI设置UI的localposition可以直接赋值 UIxxx.rectTransform.localPosition = ] / 2f, , ); 运行后在Unity的Inspec ...

  3. xcode删除一个项目

    退出xcode. 在Finder中删除项目文件夹.

  4. xcode简介及安装

    1. 简介 Xcode 是运行在操作系统Mac OS X上的集成开发工具(IDE),由苹果公司开发. Xcode是开发OS X 和 iOS 应用程序的最快捷的方式. Xcode 具有统一的用户界面设计 ...

  5. CFdiv2 165E. Compatible Numbers 子集枚举

    传送门 题意: 给出一个序列,输出每个数x对应的一个ans,要求ans在数列中,并且ans & x  = 0:数列的每个数小于(4e6) 思路: 这道题的方向比较难想.想到了就比较轻松了,可以 ...

  6. POJ 2643 Election map

    POJ 2643 Election 第一次写博客,想通过写博客记录自己的ACM历程,也想解释下英文题目,写些自己的理解.也可以让自己以后找题目更加方便点嘛.ElectionTime Limit: 10 ...

  7. Node基础-CommonJS模块化规范

    1.在本地项目中基于NPM/YARN安装第三方模块 第一步:在本地项目中创建一个"package.json"的文件 作用:把当前项目所有依赖的第三方模块信息(包含:模块名称以及版本 ...

  8. 虚IP解决AlWaysON读库服务器过保替换

    公司核心交易数据库,使用SQL 2012 AlWaysON的1主4从,有2台(8.14,8.15)从库服务器,已经使用3年多,过保替换,新买的2台服务器已经安装好,一开始方案如下: 服务器(8.14) ...

  9. 使用IDEA创建maven web项目

    1.打开idea-->configer-->setting-->build-->runner-->设置VM Options内添加-DarchetypeCatalog=in ...

  10. java 中for循环中断的办法

    /* 中断for循环的办法: 1.break ***2.return是结束方法的,不是结束循环的. 3.标签的方法. 格式: 表签名:语句 运行结果:D:\test\day0413>java T ...