管中窥豹 您所在的位置:网站首页 动态加载类的框架了解哪些 管中窥豹

管中窥豹

2024-05-30 09:54| 来源: 网络整理| 查看: 265

管中窥豹——框架下的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大致如下: select distinct from asset_app order by ${orderByClause}

其中 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 Error-Based SQL Injection Order by clause/Error based SQL Injection in “Order By” clause (MSSQL).pdf

另外还有一处如下:

and ${criterion.condition} and ${criterion.condition} #{criterion.value} and ${criterion.condition} #{criterion.value} and #{criterion.secondValue} and ${criterion.condition} #{listItem}

不过生成的相关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 from vulnerability_details _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表达式后测试以下几点

在加载配置时能否触发代码 在执行语句的能否触发代码 在已经启用的应用程序中动态插入能否触发代码(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的框架梳理的还比较乱,有机会再理理。 参考 Mybatis解析动态sql原理分析.


【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有