CodeQL从入门到入狱(一)

Posted by Azeril on October 7, 2023

时隔n天,重蹈覆辙

讲解了一些常用语法
污点分析
漏报误报的解决办法

前言

以前学的间隔太长了,而且也没复习,干脆重头开始
这一节叫————————拿回我失去的东西

QL语法

import java 
from int i 
where i=1
select i

image-20231006174200164

但是如果这样就会报错(不太理解语法还)

image-20231006174248928

然后就是看AST语法树这个之前搞得时候就一直报错(先略过还没能力解决)

image-20231006175108654

我们经常会用到的ql类库:

我们经常会用到的ql类库大体如下:

名称 解释
Method 方法类,Method method表示获取当前项目中所有的方法
MethodAccess 方法调用类,MethodAccess call表示获取当前项目当中的所有方法调用
Parameter 参数类,Parameter表示获取当前项目当中所有的参数

结合ql的语法,尝试获取Micro-service-seclab项目当中定义的所有方法:

import java
from Method method
select method

可以发现会输出重复的方法名字的

image-20231006175556864

获取指定名字为getStudent的方法名称
import java
from Method method
where method.hasName("getStudent")
select method.getname(),method.getdelaringType()
method.getName()获取的是当前方法的名称
method.getDeclaringType()获取的是当前方法所属class的名称

image-20231006180701129

获取所有项目中被调用的方法名字

image-20231006193716025

谓词

CodeQL提供一种机制可以让你把很长的查询语句封装成函数。

这个函数,就叫谓词。

import java
 
predicate isStudent(Method method) {
exists(|method.hasName("getStudent"))
}
 #这里的|不能省略
from Method method
where isStudent(method)
select method.getName(), method.getDeclaringType()
语句解释
predicate 表示当前方法没有返回值
exists子查询,它根据内部的子查询返回true or false,来决定筛选出哪些数据。

重点来了::设置Source和Sink

source 是指漏洞污染链条的输入点。比如?url=传参点
sink 是指漏洞污染链条的执行点。比如 exec
sanitizer又叫净化函数,如果存在一个方法阻断了整个传递链,那么这个方法就叫sanitizer。(这不就是java链子上的羁绊)
我们通过这种来设置source(污染的输入点也就是起点)
override perdicate isSource(DataFlow::Node src){}

举个当前的例子:那么source就是username这个传参点

@RequestMapping(value = "/one")
public List<Student> one(@RequestParam(value = "username") String username) {
    return indexLogic.getStudent(username);
}

本例中我们设置source的代码为:

override predicate is Source(DataFlow::Node src){src instanceof remoteFlowSource}

这是SDK自带的规则,里面包含了大多常用的Source入口。我们使用的SpringBoot也包含在其中。

(这里我们可以简单的理解为,remoteFlowSource是集成了一些框架传参的方式那种)

设置Sink(这里是污染链子的末尾执行点)
override predicate isSink(DataFlow::Node sink){}

在本案例中,我们的sink应该为query方法(Method )的调用(MethodAccess)

(这里是query是因为,漏洞点是出在sql注入的地方,通过query进行查询(参数就是恶意代码))

override predicate isSink(DataFlow::Node sink) {
exists(Method method, MethodAccess call |       #这个call就是随便指定的名字写出methodaccess也可以
  method.hasName("query")
  and
  call.getMethod() = method and   #为什么需要这个被调用的方法不能直接指定只能借助Method来搞
  sink.asExpr() = call.getArgument(0)  #获得参数
)
}

注:以上代码使用了exists子查询语法,格式为exists(Obj obj| somthing), 上面查询的意思为:查找一个query()方法的调用点,并把它的第一个参数设置为sink。 在靶场系统(micro-service-seclab)中,sink就是:jdbcTemplate.query(sql, ROW_MAPPER);

因为我们测试的注入漏洞,当source变量流入这个方法的时候,才会发生注入漏洞!

image-20231006201745199

看一下这个例子就懂了

import java
predicate isStudent(Method method,MethodAccess access){
    exists(|method.hasName("query")|access.getMethod()=method)
}
from Method method,MethodAccess access
where isStudent(method,access)
select access,method.getDeclaringType()

image-20231006204243518

我们随便点进去一个(类是调用方法的调用类)

image-20231006204336335

设置Flow数据流

设置好Source和Sink,就相当于搞定了首尾,但是首尾是否能够连通才能决定是否存在漏洞!

一个受污染的变量,能够毫无阻拦的流转到危险函数,就表示存在漏洞!

这个连通工作就是CodeQL引擎本身来完成的。我们通过使用config.hasFlowPath(source,sink)方法来判断是否连通。

比如如下代码:

from VulConfig config,DataFlow::PathNode source, DataFlow::PathNode sink
where config.hasFlowPath(source,sink)
select source.getNode(),source,sink,"source"

我们传递给config.hasFlowPath(source,sink)我们定义好的source和sink,系统就会自动帮我们判断是否存在漏洞了。

初步成果

在CodeQL中,我们使用官方提供的TraintTracking::Configuration方法定义source和sink,至于中间是否是通的,这个后面使用CodeQL提供的config.hasFlowPath(source,sink)来帮我们处理。

class VulConfig extends TaintTracking::Configuration {
  VulConfig() { this = "SqlInjectionConfig" }  //这里外面就是一个固定的模板

  override predicate isSource(DataFlow::Node src) { src instanceof RemoteFlowSource }  //这里是我们的污染头

  override predicate isSink(DataFlow::Node sink) {
    exists(Method method, MethodAccess call |
      method.hasName("query")
      and
      call.getMethod() = method and
      sink.asExpr() = call.getArgument(0)   //这里是我们写的污染尾部
    ) 
  }
}
extends代表集成父类TaintTracking::Configuration
这个类是官方提供用来做数据流分析的通用类提供很多数据流分析相关的方法比如isSource(定义source),isSink(定义sink)
src instanceof RemoteFlowSource 表示src必须是RemoteFlowSource类型在RemoteFlowSource里官方提供很非常全的source定义我们本地用到的Springboot的Source就已经涵盖了

image-20231006230250060

完整的payload

/**
 * @id java/examples/vuldemo
 * @name Sql-Injection
 * @description Sql-Injection
 * @kind path-problem
 * @problem.severity warning
 */

import java
import semmle.code.java.dataflow.FlowSources
import semmle.code.java.security.QueryInjection
import DataFlow::PathGraph


class VulConfig extends TaintTracking::Configuration {
  VulConfig() { this = "SqlInjectionConfig" }

  override predicate isSource(DataFlow::Node src) { src instanceof RemoteFlowSource }

  override predicate isSink(DataFlow::Node sink) {
    exists(Method method, MethodAccess call |
      method.hasName("query")
      and
      call.getMethod() = method and
      sink.asExpr() = call.getArgument(0)
    )
  }
}


from VulConfig config, DataFlow::PathNode source, DataFlow::PathNode sink  //纯纯模块
where config.hasFlowPath(source, sink)  //这是那条链子 从 头  到 尾部
select source.getNode(), source, sink, "source" //污染源  污染尾

注:上面的注释和其它语言是不一样的,不能够删除,它是程序的一部分,因为我们在申城测试报告的时候,上面注释当中的name,description等信息会写入到审计报告中。

但是会有不足也就是误报

上面的代码实现的,我觉得就是一个传参点到执行点的链子能走通(在本文就是 source—》sink)没有过滤手段

长整型肯定造成不了query注入漏洞

image-20231006231540358

我们需要采取手段消除这种误报

这个手段就是isSanitizer

image-20231006231829259

isSanitizer是CodeQL的类TaintTracking::Configuration提供的净化方法它的函数原型是

override predicate isSanitizer(DataFlow::Node node) {}

在CodeQL自带的默认规则里对当前节点是否为基础类型做了判断

override predicate isSanitizer(DataFlow::Node node) {
node.getType() instanceof PrimitiveType or
node.getType() instanceof BoxedType or
node.getType() instanceof NumberType
}

表示如果当前节点是上面提到的基础类型那么此污染链将被净化阻断漏洞将不存在

由于CodeQL检测SQL注入里的isSanitizer方法,只对基础类型做了判断,并没有对这种符合类型做判断,才引起了这次误报问题。

直接把符合类型加入到isSanitizer方法,即可消除这种误报

override predicate isSanitizer(DataFlow::Node node) {
    node.getType() instanceof PrimitiveType or
    node.getType() instanceof BoxedType or
    node.getType() instanceof NumberType or
    exists(ParameterizedType pt| node.getType() = pt and pt.getTypeArgument(0) instanceof NumberType )
        //节点的参数为数值
  }
如果当前node节点的类型为基础类型,数字类型和泛型数字类型(比如List)时,就切断数据流,认为数据流断掉了,不会继续往下检测。

直接在代码中加上发现确实被筛选掉了

解决漏报

这一点我个人感觉挺鸡助的,首先试想一下你怎么知道是漏报的无疑是自己跟过调用流程已经发现了漏洞。
但如果一个崭新的项目,漏不漏报你咋知道呢?

这里文章中举了一个例子:

public List<Student> getStudentWithOptional(Optional<String> username) {
        String sqlWithOptional = "select * from students where username like '%" + username.get() + "%'";
        //String sql = "select * from students where username like ?";
        return jdbcTemplate.query(sqlWithOptional, ROW_MAPPER);
    }

那我们用codel语法跟一下整体的调用流程(xiande,熟悉语法)

首先就是谁调用了getStudentWithOptional方法

import java 
predicate getStudent(Method method,MethodAccess access){
    exists(|method.hasName("getStudentWithOptional")|access.getMethod()=method)
}
from Method method,MethodAccess access
where getStudent(method,access)

select method.getDeclaringType(),method,access

image-20231007092520005

发现就两个类,再看一眼(确认是IndexDb中的getStudentWithOptional方法)

image-20231007093823921

呃呃呃最后转了半天流程是 IndexController–>indexLogic#getStudentWithOptional–>indexDb#getStudentWithOptional

image-20231007094137865

只不过这里用的username.get,而不是直接的username然后就捕捉不到

image-20231007094831223

我们要做的就是创建他们的联系

通过使用isAdditionalTaintStep方法,断了就强制给它接上

image-20231007095651154

isAdditionalTaintStep方法是CodeQL的类TaintTracking::Configuration提供的的方法它的原型是

override predicate isAdditionalTaintStep(DataFlow::Node node1, DataFlow::Node node2) {}

它的作用是将一个可控节点
A强制传递给另外一个节点B那么节点B也就成了可控节点
这里是从 username.get断的从哪接呢,难道是从 路由-->get

完整的payload

/**
 * @id java/examples/vuldemo
 * @name Sql-Injection
 * @description Sql-Injection
 * @kind path-problem
 * @problem.severity warning
 */

import java
import semmle.code.java.dataflow.FlowSources
import semmle.code.java.security.QueryInjection
import DataFlow::PathGraph

predicate isTaintedString(Expr expSrc, Expr expDest) {
    exists(Method method, MethodAccess call, MethodAccess call1 | expSrc = call1.getArgument(0) and expDest=call and call.getMethod() = method and method.hasName("get") and method.getDeclaringType().toString() = "Optional<String>" and call1.getArgument(0).getType().toString() = "Optional<String>"  )
}

class VulConfig extends TaintTracking::Configuration {
  VulConfig() { this = "SqlInjectionConfig" }

  override predicate isSource(DataFlow::Node src) { src instanceof RemoteFlowSource }

  override predicate isSanitizer(DataFlow::Node node) {
    node.getType() instanceof PrimitiveType or
    node.getType() instanceof BoxedType or
    node.getType() instanceof NumberType or
    exists(ParameterizedType pt| node.getType() = pt and pt.getTypeArgument(0) instanceof NumberType )
  }

  override predicate isSink(DataFlow::Node sink) {
    exists(Method method, MethodAccess call |
      method.hasName("query")
      and
      call.getMethod() = method and
      sink.asExpr() = call.getArgument(0)
    )
  }
override predicate isAdditionalTaintStep(DataFlow::Node node1, DataFlow::Node node2) {
    isTaintedString(node1.asExpr(), node2.asExpr())
  }
}


from VulConfig config, DataFlow::PathNode source, DataFlow::PathNode sink
where config.hasFlowPath(source, sink)
select source.getNode(), source, sink, "source"
其实就是多加了这个函数

predicate isTaintedString(Expr expSrc, Expr expDest) {
    exists(Method method, MethodAccess call, MethodAccess call1 
           | expSrc = call1.getArgument(0)  //当前expSrc是被调用函数的参数
           and expDest=call         //expDest是被调用函数
           and call.getMethod() = method
           and method.hasName("get")   //被调用的是get函数
           and method.getDeclaringType().toString() = "Optional<String>" //指定了调用get函数的类是这个
           and call1.getArgument(0).getType().toString() = "Optional<String>"  )
        //这个指向的更多的是(找一个被调用的参数是Optional<String>类型)的函数
}
总结一下其实就是
getStudentWithOptional(Optional<String> username)  指定参数
username.get()        指定get指定username调用get的类型
public List<Student> getStudentWithOptional(Optional<String> username) {
        String sqlWithOptional = "select * from students where username like '%" + username.get() + "%'";
        //String sql = "select * from students where username like ?";
        return jdbcTemplate.query(sqlWithOptional, ROW_MAPPER);
    }

最后调用上面定义的那个函数即可,

override predicate isAdditionalTaintStep(DataFlow::Node node1, DataFlow::Node node2) {
    isTaintedString(node1.asExpr(), node2.asExpr())
  }
}
其实还是有点懵二个节点是怎么连起来的。
目前就是感觉是规定了source然后sink更换成了username.get然后一直搞条件查找 Optional<String>的这个

最后的结果确实是测出来了

image-20231007114118117


Creative Commons License
本作品采用CC BY-NC-ND 4.0进行许可。转载,请注明原作者 Azeril 及本文源链接。