CodeQL系列之域敏感和兜底规则冲突的解决过程

CodeQL系列之域敏感和兜底规则冲突的解决过程

效果: 检测SQL注入漏洞准确率达到90%以上,几乎无误报漏报。

0x01 问题

起初isAdditionalTaintStep的污染转播链规则是将所有情况都标记为污染。目的是为了避免漏报的情况发生。

image-20220708151742789

例如:此种情况下CodeQL默认的TaintStep不会扫出链路,加了兜底规则后则会扫出该链路。

image-20220708152117102

但是因为兜底规则将所有的情况都标记为了污染,那么也会出现误报的情况。

例如:模拟XML的SQL注入漏洞情况,在XML文件里,直接是获取对象的属性然后执行sql语句,而在触发SQL语句的入口点却传入的是对象。如下例子vul11testExec,souce传递进来后赋值给dsUser的cmd属性,然后dsUser的user属性赋值为常量,最后将整个对象dsUser传递给执行SQL语句的函数,在xml里的sql语句可能是select cmd from table或者是select user from table。那么当sql语句里是cmd属性时,那么是存在漏洞的,如果是user属性时,则不存在漏洞。但是因为兜底规则的影响,将dsUser整个对象标记为了污染,因此无论xml里的sql语句是cmd属性还是user属性,都会认为该链路存在SQL注入漏洞。这样就造成了误报。

image-20220708152338938

0x02 解决过程

0x02-1 方案1(解决了部分误报)

链路中出现getXXX、setXXX时,不走兜底规则,走CodeQL默认的链路,这样就不会因为当出现setXXX赋值的语句时,将整个对象标记为污染,从而解决了部分误报漏报的情况。

第一次优化

isAdditionalTaintStep只有兜底规则

image-20220711204819242

则会出现如下的误报,因为在dstest.setCmd(cmd);中,node1是cmd,node2是dstest,将对象dstest标记为污染了。所以最终能够扫出下面的误报链路。

image-20220711204722358

解决方案:CodeQL默认是会处理域敏感问题,那么当链路中出现了setXXX、getXXX方法时,不让走我们定义的兜底规则,走CodeQL默认的域敏感处理规则,那么就会将dstest的cmd属性标记为污染,不会将整个dstest对象标记污染,因此解决该误报链路问题。

isAdditionalTaintStep方法里,通过if判断条件,如果链路中存在setXXX、getXXX方法,那么执行none语句,即不走兜底规则,走CodeQL默认的域敏感处理规则。可以看到结果里没有了getUsername这条链路了。的确解决了误报问题。

image-20220711205217191

第二次优化

但是发现该if判断条件较简单粗暴,是直接粗暴的将所有setXXX、getXXX方法都走默认的CodeQL规则,但是没有将其和node1、node2连接起来。

忘记当时的场景是什么状况了,反正要优化下,将node1和node2连接起来。

image-20220711213340269

第三次优化

后面又发现一处bug,通过下面的if判断条件,居然扫出了一条误报链路

误报情况如下,dstest对象仍然被标记为污染

8b6ddf1bcdd2f81fc461c33e9ad8aac041479e20

解决方案:isAdditionalTaintStep里Method m起初放在最外层的exists中,会出现dstest.setUsername(“zhangsan”);的误报链路,将Method m移到if判断语句里,解决了该误报问题。

5922b5fd611140cd7d82c551b2074650961e989f

优化后如下

d5447e44cdf6e812a9cc32fe20578eb453c89df9

解决了误报问题。

第四次优化

下图红框走了兜底规则,将dsUser对象标记为污染。

image-20220711213556190

解决方案:实例化对象的时候,不走兜底规则,走codeql默认的TaintStep,则添加一条规则,判断call是不是实例化。即下图红框里的代码。

优化后如下,成功解决了上面的误报问题。

image-20220711213711929

最终规则

优化后代码如下:

// 链路里的所有可能污染的地方
  override predicate isAdditionalTaintStep(DataFlow::Node node1, DataFlow::Node node2) {
    exists(Call call, Callable callable | 
      // storeStep(src, c, sink) and
      call.getCallee() = callable and
      if exists(Method m | 
          // 如果是object.setXXXXXX(YYYYYY)时,则走codeql默认的TaintStep,这样只会污染object的XXXXXX属性,不会将整个对象污染
          (
            node1.asExpr() = call.getAnArgument() and
            node2.asExpr() = call.getQualifier() and
            m = call.getCallee() and
            (
              m instanceof SetterMethod or
              m instanceof GetterMethod
            )
          )
          // 如果是DSUser dsUser = new DSUser(command)时,则走codeql默认的TaintStep
          or
          (
            node1.asExpr() = call.getAnArgument() and
            node2.asExpr() = call and
            call instanceof ConstructorCall
          ) 
        )
      then
          none()
      else
          // none()
        // 兜底函数,避免漏报
        (
          (
            node1.asExpr() = call.getAnArgument() or 
            node1.asExpr() = call.getQualifier() 
          ) and 
          (
            node2.asExpr() = call or
            node2.asExpr() = call.getQualifier()
          )
        ) 

    )
  }

0x02-2 方案2(最终解决思路可行,方式错误)

在XML格式的SQL注入里,例如selectByExample函数的orderByClause注入,sink点是example对象,而xml文件里直接是调用了orderByClause属性,并没有通过getXXX方法获取orderByClause值,因此前面的通过过滤getXXX方法走CodeQL默认的规则并不适用。分析CodeQL展现结果的链路,发现当对象的属性被污染时,展现的是dsUser [cmd]:String类似的格式,即dsUser对象的cmd属性被污染。因此想到的解决方案是获取最后sink点的污染属性名字,再和xml文件里的注入点名字做对比。从而解决误报问题。

跟踪CodeQL默认的污染传播链路

为了读取Sink点的污染属性,起初想到的是寻找方法能够读取最后sink点被污染的属性(最终证实该方法并不可行,因为当sink是对象的链路打印出来后,其实整个对象就已经被标记为污染了,那么就不可能打印出被污染的属性,但是当时没有想到这一点)。于是一步步的分析跟踪CodeQL默认的污染传播链路。

TaintTracking::Configuration#isAdditionalFlowStep方法入口开始分析

发现isAdditionalFlowStep方法里调用了isAdditionalTaintStep(我们自写的污染转播规则)和defaultAdditionalTaintStep(CodeQL默认的污点传播规则)

image-20220708155345397

跟入到defaultAdditionalTaintStep(CodeQL默认的污点传播规则)里

image-20220708155633318

后面一顿分析

image-20220708161047400

分析PathNode节点

PathNode入口开始分析

发现了PathNodeImpl类,该类继承于PathNode

image-20220708161255114

看了下所有的方法,发现了toString方法,猜测可能是通过该方法打印每条链路里的每一节点信息。于是继续深入跟踪测试。

更改了原先的toString方法里的内容,添加了一下---字符串做测试,发现的确影响到了链路里的每一个节点的内容。

image-20220708161417531

跟入this.ppAp()方法里,发现如果节点PathNode是PathNodeSink时,则返回空,如果节点PathNode是PathNodeMid时,返回getAp()方法的结果。

image-20220708163903532

那么PathNodeSink和PathNodeMid的区别是什么?在toString方法里,添加打印this.getAQlClass()方法

image-20220708164109822

从结果中可以分析出来,PathNodeSink是链路中最后的一个节点,PathNodeMid是链路中的每个过程的中间节点

image-20220708164242863

接下来分析PathNodeMid的getAp()方法返回了什么内容,通过PathNodeMid的定义知道,getAp()返回了AccessPath对象。

image-20220708164409004

继续跟入分析AccessPath类,通过介绍猜测是链路中的每条路径里的信息

image-20220708164759222

继续分析发现不少类继承了AccessPath类,通过一个个测试发现AccessPathCons类的toString()方法会影响到我们结果的内容

image-20220708164653390

image-20220708165911028

继续跟入到AccessPathCons#toStringImpl()方法里,红框里的内容就是展示结果里打印的内容。

image-20220708165957056

有意思的地方是head.toSting(),该方法打印了对象被污染了的属性。于是分析下head的类TypedContent

image-20220708170119931

TypedContent

image-20220708170140420

执行TypedContent#toString()方法,发现打印的结果均是对象的属性。

image-20220708170229912

那么就能理解了为什么AccessPathCons#toStringImpl()里的head.toSting()能够将对象里被污染的属性打印出来了。

将打印污染属性的中括号里加上*测试效果

image-20220708170640172

image-20220708170718953

效果如下:的确将source传递给dstest对象的cmd属性污染了,在链路中打印出了[*cmd*]

image-20220708170611336

此时想的是以为已经能够打印对象被污染的属性,那么在PathNodeSink节点时也通过getAp()方法将对象被污染的属性打印出来。如下例子,将dsUser对象的cmd属性打印出来。

image-20220708171701421

但是发现PathNodeSink没有getAp()方法,那么可能就得需要改动大量源码,给PathNodeSink增加类似PathNodeMid的getAp()方法。但是因为感觉改动太大,就先暂时放一边了。后面重新回顾了下,发现该方案不可行。因为当出现该链路时,dsUser整个对象就已经被标记为污染了,不再存在dsUser对象没被污染,dsUser的cmd属性被污染的情况。

证明如下例子:

之所以vul11testExec扫不出来,而vul12testExec能够扫出来,问题出在sink点一个是对象,一个是对象的属性,当sink是对象时,因为污染的是对象的属性,所以对象dsUser并不认为被污染,因此没扫出来
当重构isAdditionalTaintStep时,因为默认的TaintStep没扫出来,然后经过我们的规则,在DSUser dsUser = new DSUser(cmd);时将dsUser设置为污染,因此到test.testExecDSUser(dsUser)时,sink点dsUser也是污染的。所以能够扫出来,但也会因此造成误报。

image-20220708173950316

0x02-3 方案三(获取存在漏洞链路)

解决方案:将对象的属性设置为Sink点

allowImplicitRead-匹配对象被污染的属性

没有重构allowImplicitRead方法,打印出来的链路是走了我们的兜底规则,直接将example对象标记为污染。

image-20220712154022109

image-20220712154102349

重构allowImplicitRead方法,将Content类对象c的值设置为sink对象污染属性的值,下面例子就是匹配example对象的orderByClause属性。可以看到结果里多出了一条链路,并且打印出污染属性orderByClause。

override predicate allowImplicitRead(DataFlow::Node node, DataFlow::Content c) {
    (this.isSink(node) or this.isAdditionalTaintStep(node, _)) and
    (
        defaultImplicitTaintRead(node, c) or
        c.toString() = "orderByClause"
    )
}

image-20220712154301015

SQL注入实践效果,成功的打印出对象属性被污染的链路。

image-20220712161450560

image-20220712161547815

但是最后测试发现,误报链路还是存在,说明allowImplicitRead只能增加真实存在漏洞的链路,但是并不能过滤掉误报链路。

0x03 方案四(sink点分类讨论,彻底解决了误报)

发现新的问题:大量的误报还来源于将sql函数的所有参数标记为sink点的原因,然后结合了兜底规则造成了大量的误报。

解决方案:针对sink点分类讨论,共三种情况(Object.getAttrName(),String,Obejct)

Object.getAttrName() 对象调用get获取属性值

漏洞代码如下:

test.testExampleTwoString(request.getSortType(), request.getCmd());

如果sink点是object.getAttrName()会造成大量的误报,那么思路就是将getAttrName中的AttrName取出来,然后和xml里的注入点参数名一一做对比,只有相同时才说明是个漏洞,这样可以减少大量的误报。代码arg.toString().substring(3, arg.toString().length()-5)取出Object.getAttrName()的AttrName值,然后全部转换成小写,和xml里取出来的注入点参数名作比较。

image-20220720200323555

String 字符串

漏洞代码如下:

test.testExampleTwoString(sortType, cmd);

如果sink点是字符串类型,那么就直接和xml里的注入点参数名一一做对比即可。

image-20220720200745625

Object 对象

漏洞代码如下:

test.testExampleTwoString(dsUser);

如果sink点是对象,检测sink点打印是否有 [被污染的属性] ,如果有则提取出被污染的属性,然后和xml的注入点参数名做比较,如果一样则是真实链路,如果不一样则是误报。如果没有,就是对象本身,那么获取对象的所有属性名,然后比较对象的属性里面有没有注入点参数名。

image-20220721173541870

sink点没有 [被污染的属性] ,提取出对象的所有属性名,然后比较对象的属性里面有没有注入点参数名。下图认为是真实存在漏洞的链路

image-20220721173657521

sink点有 [被污染的属性] ,提取出被污染的属性,然后和xml的注入点参数名做比较。下图认为是真实存在漏洞的链路

image-20220721173714418

sink点有 [被污染的属性] ,提取出被污染的属性,然后和xml的注入点参数名做比较。因为不一致,所以下图认为是误报链路。

image-20220721173741205

最终判断代码如下(添加到isMyBatisXMLSQL方法里):经过测试,的确大幅度的提升了漏洞的准确率。

// 如果sink点是object.getAttrName()会造成大量的误报,如下例子。那么通过正则将attrName取出来,然后和xml里的参数名一一做对比,这样可以减少大量的误报
// test.testExampleTwoString(request.getSortType(), request.getCmd());
if arg.toString().regexpMatch("get[A-Z].*")
then
  funcArgName = arg.toString().substring(3, arg.toString().length()-5).toLowerCase() and
  funcArgName.toLowerCase() = argumentName.toLowerCase()
  // funcArgName.toLowerCase() = "platformName".toLowerCase()
else
  funcArgName = "" and
  // 如果sink点是字符串
  if arg.getType() instanceof TypeString
  then
    arg.toString() = argumentName
  // 最后一种情况,sink点是对象
  else
    // 检测sink点是否有     [被污染的属性]        
    if sink.toString().regexpMatch(".*\\[.*\\]")
    then
      // 如果sink点有     [被污染的属性]        ,则提取出被污染的属性,然后和xml的注入点参数名做比较,如果一样则是真实链路,如果不一样则是误报
      objectAttrName = sink.toString().regexpFind("[^\\[\\]]*", _, _) and
      objectAttrName = argumentName
    else
      // 如果sink点没有     [被污染的属性]        ,就是对象本身,那么获取对象的所有属性名,然后比较对象的属性里面有没有注入点参数名。
      objectAttrName = arg.getType().(RefType).getAField().toString() and
      objectAttrName = argumentName

image-20220720201238618

0x03 总结

问题的根源

是在sink点的定义上,之前是将sink点标记为每一个参数,然后又通过兜底规则将整个对象也标记为污染,所以造成了大量的误报。

解决方案

是将兜底规则直接删除,然后增加allowImplicitRead方法(可以打印出真实存在漏洞的链路),再对sink点分类讨论过滤误报。最终几乎0误报检测出SQL注入漏洞。

技术难点突破

其实很多的误报都是来源于sink点为对象时导致的,其实通过PartialPathNode可以打印出sink点为对象时被污染的属性。那么为什么PartialPathNode可以打印出对象被污染的属性,而PathNode却不能打印出来(之前在这里研究了好久),通过定义可以看出PartialPathNodetoString()打印出了ppAp(),而ppAP在之前分析过,就是AccessPath,也可以理解为路径吧。所以在最后的链路中,通过PartialPathNodetoString()就能够取出被污染的属性,而如果使用PathNode就不行了。

image-20220721175019359

image-20220721175031901

0x04 靶场测试代码

package org.joychou.controller;

import org.apache.commons.lang.StringUtils;
import org.joychou.dao.*;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.*;
import java.io.IOException;
import freemarker.cache.StringTemplateLoader;
import freemarker.template.Configuration;
import freemarker.template.Template;
import freemarker.template.TemplateException;
import java.io.StringWriter;

import java.io.*;
import java.util.HashMap;
import java.util.Map;


/**
 * Java code execute
 *
 * @author JoyChou @ 2018-05-24
 */
@RestController
@RequestMapping("/domainSensitive")
public class DomainSensitive {

    private static Map<String, String> sortFieldMap = new HashMap<>();
    private static String SORT_STR = "ISNULL(%s),%s %s";

    public String testExecDSTestCmdGet(DSTest dsTest) throws IOException{
        Runtime run = Runtime.getRuntime();
        Process p = run.exec(dsTest.getCmd());     // vul1, vul2
        return "1";
    }

    public String testExecDSTestCmdDot(DSTest dsTest) throws IOException{
        Runtime run = Runtime.getRuntime();
        Process p = run.exec(dsTest.cmd);          // vul1, vul2
        return "1";
    }

    public String testExecDSTestUserNameGet(DSTest dsTest) throws IOException{
        Runtime run = Runtime.getRuntime();
        Process p = run.exec(dsTest.getUsername());    // 安全
        return "1";
    }

    public String testExecDSTestUserNameDot(DSTest dsTest) throws IOException{
        Runtime run = Runtime.getRuntime();
        Process p = run.exec(dsTest.username);         // 安全
        return "1";
    }





    public String testExecDSUserCmdGet(DSUser dsUser) throws IOException{
        Runtime run = Runtime.getRuntime();
        Process p = run.exec(dsUser.getCmd());
        return "1";
    }

    public String testExecDSUserCmdDot(DSUser dsUser) throws IOException{
        Runtime run = Runtime.getRuntime();
        Process p = run.exec(dsUser.cmd);
        return "1";
    }

    public String testExecDSUserUserGet(DSUser dsUser) throws IOException{
        Runtime run = Runtime.getRuntime();
        Process p = run.exec(dsUser.getUser());
        return "1";
    }

    public String testExecDSUserUserDot(DSUser dsUser) throws IOException{
        Runtime run = Runtime.getRuntime();
        Process p = run.exec(dsUser.user);
        return "1";
    }

    public String testExecDSUserPassGet(DSUser dsUser) throws IOException{
        Runtime run = Runtime.getRuntime();
        Process p = run.exec(dsUser.getPass());
        return "1";
    }

    public String testExecDSUserPassDot(DSUser dsUser) throws IOException{
        Runtime run = Runtime.getRuntime();
        Process p = run.exec(dsUser.pass);
        return "1";
    }

    public String testExecDSUser(DSUser dsUser) throws IOException{
        Runtime run = Runtime.getRuntime();
        Process p = run.exec(String.valueOf(dsUser));      // vul5, vul6, vul7
        return "1";
    }


    // [+] CodeQL默认的TaintStep可以扫出来
    @GetMapping("/vul1testExecDSTestCmdGet")
    public String vul1testExecDSTestCmdGet(String cmd) throws IOException {
        DSTest dstest = new DSTest();
        dstest.setCmd(cmd);
        dstest.setUsername("zhangsan");
        return testExecDSTestCmdGet(dstest);
    }

    // [+] CodeQL默认的TaintStep可以扫出来
    @GetMapping("/vul2testExecDSTestCmdDot")
    public String vul2testExecDSTestCmdDot(String cmd) throws IOException {
        DSTest dstest = new DSTest();
        dstest.cmd = cmd;
        dstest.username = "zhangsan";
        return testExecDSTestCmdDot(dstest);
    }



    @GetMapping("/sec3testExecDSTestUserNameGet")
    public String sec3testExecDSTestUserNameGet(String cmd) throws IOException {
        DSTest dstest = new DSTest();
        dstest.setCmd(cmd);
        dstest.setUsername("zhangsan");
        return testExecDSTestUserNameGet(dstest);
    }

    @GetMapping("/sec4testExecDSTestUserNameGet")
    public String sec4testExecDSTestUserNameGet(String cmd) throws IOException {
        DSTest dstest = new DSTest();
        dstest.cmd = cmd;
        dstest.username = "zhangsan";
        return testExecDSTestUserNameDot(dstest);
    }


    // [+] CodeQL默认的TaintStep可以扫出来
    @GetMapping("/vul5testExecDSUserCmdGet")
    public String vul5testExecDSUserCmdGet(String cmd) throws IOException {
        DSTest dstest = new DSTest();
        dstest.cmd = cmd;
        dstest.username = "zhangsan";
        String command = dstest.getCmd() + dstest.getUsername();
        DSUser dsUser = new DSUser(command);        // 新对象被污染
        return testExecDSUserCmdGet(dsUser);
    }


    @GetMapping("/sec6testExecDSUserUserGet")
    public String sec6testExecDSUserUserGet(String cmd) throws IOException {
        DSTest dstest = new DSTest();
        dstest.cmd = cmd;
        dstest.username = "zhangsan";
        String command = dstest.getCmd() + dstest.getUsername();
        DSUser dsUser = new DSUser(command);        // 新对象被污染
        return testExecDSUserUserGet(dsUser);
    }

    // [+] CodeQL默认的TaintStep可以扫出来
    @GetMapping("/vul7")
    public String vul7(String cmd) throws IOException {
        DSTest dstest = new DSTest();
        dstest.cmd = cmd;
        dstest.username = "zhangsan";
        String command = dstest.getCmd() + dstest.getUsername();
        DSUser dsUser = new DSUser("111");
        dsUser.pass = command;                      // 新对象的属性被污染
        return testExecDSUserPassGet(dsUser);
    }

    @GetMapping("/sec8testExecDSUserUserGet")
    public String sec8testExecDSUserUserGet(String cmd) throws IOException {
        DSTest dstest = new DSTest();
        dstest.cmd = cmd;
        dstest.username = "zhangsan";
        String command = dstest.getCmd() + dstest.getUsername();
        DSUser dsUser = new DSUser("111");
        dsUser.pass = command;                      // 新对象的属性被污染
        return testExecDSUserUserGet(dsUser);
    }


    // 下面的例子分析构造函数是否会处理域敏感问题,结果显示是会处理的
    // codeql默认的TaintStep不会扫出下面的链路,
    // codeql默认的TaintStep在DSUser dsUser = new DSUser(cmd)表达式中会将dsUser的cmd属性污染,但是因为后面重构的isAdditionalTaintStep,导致将整个对象标记为污染了。所以产生了如下的误报。
    // 因此通过m instanceof GetterMethod的方法将所有调用了getXXXX的链路都走codeql默认的TaintStep,从而解决了如下的误报
    @GetMapping("/sec9testExecDSUserUserGet")
    public String sec9testExecDSUserUserGet(String cmd) throws IOException {
        DSUser dsUser = new DSUser(cmd);        // 新对象被污染
        dsUser.setUser("aaa");
        return testExecDSUserUserGet(dsUser);
    }

    // [+] CodeQL默认的TaintStep可以扫出来
    @GetMapping("/vul10testExecDSUserCmdGet")
    public String vul10testExecDSUserCmdGet(String cmd) throws IOException {
        DSUser dsUser = new DSUser(cmd);        // 新对象被污染
        dsUser.setUser("aaa");
        return testExecDSUserCmdGet(dsUser);
    }




    // 场景
    // 模拟xml的sql注入-orderByCause的注入点,vul11testExec和vul12testExec作比较

    // 问题:
    // 之所以vul11testExec扫不出来,而vul12testExec能够扫出来,问题出在sink点一个是对象,一个是对象的属性,当sink是对象时,因为污染的是对象的属性,所以对象dsUser并不认为被污染,因此没扫出来
    // 当重构isAdditionalTaintStep时,因为默认的TaintStep没扫出来,然后经过我们的规则,在DSUser dsUser = new DSUser(cmd);时将dsUser设置为污染,
    // 因此到test.testExecDSUser(dsUser)时,sink点dsUser也是污染的。所以能够扫出来,但也会因此造成误报。

    // 解决方案
    // 获取sink的被污染的属性


    // [-] CodeQL默认的TaintStep扫不出来
    @GetMapping("/vul11testExec")
    public String vul11testExec(String cmd) throws IOException {
        DSUser dsUser = new DSUser(cmd);        // 新对象被污染
        dsUser.setUser("aaa");
        TestExec test = new TestExec();
        return test.testExecDSUser(dsUser);             // select cmd form
    }

    // [+] CodeQL默认的TaintStep扫出来
    @GetMapping("/vul12testExec")
    public String vul12testExec(String cmd) throws IOException {
        DSUser dsUser = new DSUser(cmd);        // 新对象被污染
        dsUser.setUser("aaa");
        TestExec test = new TestExec();
        return test.testExecDSUserCmd(dsUser.getCmd());
    }

    // [-] CodeQL默认的TaintStep扫不出来
    // 模拟DA186DE56A57D9697A1791B37D6E0E20漏洞的误报
    // 经过了一层函数exampleAssemble
    @GetMapping("/vul13testExec")
    public String vul13testExec(String cmd) throws IOException {
        DSTest dsTest = exampleAssemble(cmd);
        TestExec test = new TestExec();
        return test.testExecDSTest(dsTest);
    }

    public DSTest exampleAssemble(String cmd){
        DSTest dsTest = new DSTest();
        dsTest.cmd = cmd;
        return dsTest;
    }

    // CodeQL默认扫不出来
    // 不走过滤构造函数-即走兜底的规则 应该要扫出来
    // 走过滤构造函数 应该要扫不出来      主要解决该情况
    @GetMapping("/vul14testExec")
    public void vul14testExec(String templateContent) throws IOException, TemplateException {
        Configuration cfg = new Configuration();

        StringTemplateLoader stringLoader = new StringTemplateLoader();
        DSUser dsUser = new DSUser(templateContent);
        stringLoader.putTemplate("tpl", String.valueOf(dsUser));
        cfg.setTemplateLoader(stringLoader);
        Template template = cfg.getTemplate("tpl");

        StringWriter stringWriter = new StringWriter();
        //在模版上执行插值操作,并输出到制定的输出流中
        template.process(null, stringWriter);
        String resultStr = stringWriter.toString();
        System.out.println(resultStr);
    }

    // CodeQL默认扫不出来
    // 走了兜底函数,在 stringLoader.putTemplate("tpl", dsUser.getCmd()); 这里经过我们的兜底函数,将stringLoader污染了,所以能够扫出来
    @GetMapping("/vul15testExec")
    public void vul15testExec(String templateContent) throws IOException, TemplateException {
        Configuration cfg = new Configuration();

        StringTemplateLoader stringLoader = new StringTemplateLoader();
        DSUser dsUser = new DSUser(templateContent);
        stringLoader.putTemplate("tpl", dsUser.getCmd());
        cfg.setTemplateLoader(stringLoader);
        Template template = cfg.getTemplate("tpl");

        StringWriter stringWriter = new StringWriter();
        //在模版上执行插值操作,并输出到制定的输出流中
        template.process(null, stringWriter);
        String resultStr = stringWriter.toString();
        System.out.println(resultStr);
    }


    // CodeQL默认扫出来
    @GetMapping("/vul16testExec")
    public String vul16testExec(QueryRequest request) throws IOException {
        QueryExample example = buildExample(request);
        TestExec test = new TestExec();
        return test.testExampleSQL(example);
    }

    private QueryExample buildExample(QueryRequest request) {
        QueryExample example = new QueryExample();
        example.setUser(request.getUser());
        example.setOrderByClause(request.getSortType());
        return example;
    }

    // CodeQL默认扫不出来
    // 检测是否是 HashMap导致没扫出来
    // 因为HashMap的get方法返回值不会被标记为污染,但因为走了兜底规则,将sortFieldMap.get(request.getUser())标记为了污点,然后成功扫出真实存在漏洞的链路
    @GetMapping("/vul17testExec")
    public String vul17testExec(QueryRequest request) throws IOException {
        QueryExample example = buildExample2(request);
        TestExec test = new TestExec();
        return test.testExampleSQL(example);
    }

    private QueryExample buildExample2(QueryRequest request) {
        QueryExample example = new QueryExample();
        example.setOrderByClause("created_at desc");
        example.setUser(request.getUser());
        String field = sortFieldMap.get(request.getUser());
        example.setOrderByClause(field);
        return example;
    }

    // CodeQL默认扫出来
    // 检测是否是 String.format()导致没扫出来
    // 说明String.format(taint) 会将结果标记为污染
    @GetMapping("/vul18testExec")
    public String vul18testExec(QueryRequest request) throws IOException {
        QueryExample example = buildExample3(request);
        TestExec test = new TestExec();
        return test.testExampleSQL(example);
    }

    private QueryExample buildExample3(QueryRequest request) {
        QueryExample example = new QueryExample();
        example.setOrderByClause("created_at desc");
        example.setUser(request.getUser());
        String field = sortFieldMap.get(request.getUser());
        example.setOrderByClause(String.format(SORT_STR, field, field, request.getSortType()));
        return example;
    }

    @GetMapping("/vul19testExec")
    public String vul19testExec(QueryRequest request) throws IOException {
        QueryExample example = buildExample4(request);
        TestExec test = new TestExec();
        return test.testExampleSQL(example);
    }

    private QueryExample buildExample4(QueryRequest request) {
        QueryExample example = new QueryExample();
        example.setOrderByClause("created_at desc");
        example.setUser(request.getUser());
        String field = sortFieldMap.get(request.getUser());
        if (StringUtils.isNotBlank(field) && StringUtils.isNotBlank(request.getSortType())) {
            example.setOrderByClause(String.format(SORT_STR, field, field, request.getSortType()));
        }
        return example;
    }

    // [-] CodeQL默认的TaintStep扫不出来
    // 漏报问题
    @GetMapping("/vul100")
    public void vul100(String templateContent) throws IOException, TemplateException {
        Configuration cfg = new Configuration();

        StringTemplateLoader stringLoader = new StringTemplateLoader();
        stringLoader.putTemplate("tpl", templateContent);
        cfg.setTemplateLoader(stringLoader);


        Template template = cfg.getTemplate("tpl");

        StringWriter stringWriter = new StringWriter();
        //在模版上执行插值操作,并输出到制定的输出流中
        template.process(null, stringWriter);
        String resultStr = stringWriter.toString();
        System.out.println(resultStr);
    }

}

package org.joychou.dao;

import java.io.Serializable;

public class DSTest implements Serializable {

    public String username;
    public String cmd;

    public String getUsername() {
        return username;
    }
    public void setUsername(String username) {
        this.username = username;
    }

    public String getCmd() {
        return cmd;
    }
    public void setCmd(String cmd) {
        this.cmd = cmd;
    }

}
package org.joychou.dao;

import java.io.Serializable;

public class DSUser implements Serializable {

    public String user;
    public String pass;
    public String cmd;


    public DSUser(String cmd) {
        this.cmd = cmd;
    }

    public String getUser() {
        return user;
    }
    public void setUser(String user) {
        this.user = user;
    }

    public String getPass() {
        return pass;
    }
    public void setPass(String pass) {
        this.pass = pass;
    }

    public String getCmd() {
        return cmd;
    }

}
package org.joychou.dao;

public class QueryExample {
    public String user;
    public String orderByClause;

    public String getUser() {
        return user;
    }
    public void setUser(String user) {
        this.user = user;
    }

    public String getOrderByClause() {
        return orderByClause;
    }
    public void setOrderByClause(String orderByClause) {
        this.orderByClause = orderByClause;
    }

    public static class Criteria {

        protected Criteria() {
            super();
        }
    }

    public Criteria createCriteria() {
        return createCriteriaInternal();
    }

    public Criteria createCriteriaInternal(){
        return new Criteria();
    }

}
package org.joychou.dao;

public class QueryRequest {
    public String user;
    public String pass;
    public String cmd;
    public String sortType;

    public String getUser() {
        return user;
    }
    public void setUser(String user) {
        this.user = user;
    }

    public String getPass() {
        return pass;
    }
    public void setPass(String pass) {
        this.pass = pass;
    }

    public String getCmd() {
        return cmd;
    }
    public void setCmd(String cmd) {
        this.cmd = cmd;
    }

    public String getSortType() {
        return sortType;
    }
    public void setSortType(String sortType) {
        this.sortType = sortType;
    }
}
package org.joychou.dao;

import java.io.IOException;

public class TestExec {
    public String testExecDSUser(DSUser dsUser) throws IOException {
        return dsUser.getCmd();
    }

    public String testExecDSUserCmd(String cmd) throws IOException {
        return cmd;
    }

    public String testExecDSTest(DSTest dsTest) throws IOException {
        return dsTest.getCmd();
    }

    public String testExampleSQL(QueryExample example) throws IOException {
        return "1";
    }
}

   转载规则


《CodeQL系列之域敏感和兜底规则冲突的解决过程》 ske 采用 知识共享署名 4.0 国际许可协议 进行许可。