CodeQL环境搭建

参考:https://help.semmle.com/codeql/codeql-cli/procedures/get-started.html

下载

下载链接:https://github.com/github/codeql-cli-binaries/releases

下载最新的codeql.zip就可以了,比如:
https://github.com/github/codeql-cli-binaries/releases/download/v2.1.0/codeql.zip

不要下载下面的Source code

搭建本地环境

新建一个codeql目录用来放cli工具,比如$HOME/codeql-home

下载CodeQL依赖的库和examples:
https://github.com/Semmle/ql
在这里插入图片描述

重新组织目录

组织完之后是这样的:

77@ubuntu:~/repos/CodeqlHome$ ls codeql-repo/
change-notes  CODE_OF_CONDUCT.md  CODEOWNERS  config  CONTRIBUTING.md  cpp  csharp  docs  java  javascript  LICENSE  misc  python  README.md
77@ubuntu:~/repos/CodeqlHome$ ls codeql-cli/
codeql  codeql.cmd  codeql.exe  cpp  csharp  go  java  javascript  legacy-upgrades  LICENSE.md  Open-Source-Notices  python  tools  xml

在这里插入图片描述

codeQL常用命令

查看codeql执行哪些语言:

codeql resolve languages

在这里插入图片描述
根据已有的源码创建codeql工程:

codeql database create CodeQL_java-sec-code --source-root=/home/77/repos/java-sec-code --language=java

codeql自动识别了这个项目是maven的,然后编译了。
指定编译的命令,使用

--command="mvn clean install --file pom.xml"

参考:https://geekmasher.dev/posts/sast/codeql-introduction

如果是查询配置文件中的安全问题,则需要使用分步骤的命令:

codeql database init --source-root=<src> --language java <db>
codeql database trace-command --working-dir=<src> <db> <java command>
codeql database index-files --language xml --include-extension .xml --working-dir=<src> <db>
codeql database index-files --language properties --include-extension .properties--working-dir=<src> <db>
codeql database finalize <db>

参考:https://github.com/github/codeql/issues/3887
在这里插入图片描述

ERROR: Could not detect a suitable build command for the source checkout. 

如果碰到这个问题,可能是
由于构造codeql数据库的时候,需要对源代码进行编译,这里应该是需要mvn等编译工具的命令在环境变量中。

然后得到了这样的一个codeql工程目录:
在这里插入图片描述

src.zip是源码的压缩包,

codeql命令行参考手册:
https://help.semmle.com/codeql/codeql-cli/commands.html

从LGTM.com中获取databases

LGTM.com已经使用CodeQL分析了上千个项目,可以从LGTM.com下载这些项目的databases。

一些CodeQL给Java写的示例

参考:https://help.semmle.com/QL/learn-ql/ql-training.html

//TODO

Windows环境搭建

参考:
https://www.angelwhu.com/paper/2019/12/30/CodeQL-introduction/#0x00-%E7%8E%AF%E5%A2%83%E6%90%AD%E5%BB%BA

先下载VS Code,然后安装CodeQL插件:

在这里插入图片描述

下载这个官方准备好的环境:
vscode-codeql-starter

git clone --recursive https://github.com/github/vscode-codeql-starter/

然后用VS Code打开:
打开之后,右键:

CodeQL: Run Query

发现不能运行成功,
在这里插入图片描述
但是VS帮忙自动下载CLI工具了

在这里插入图片描述
在这里插入图片描述
当VS帮忙下载完成之后,再点击,由于没有设置代码库(Databases),在弹出的对话框里选择Database即可。
在这里插入图片描述
选择java-sec-code之后,要给这个目录生成database:
在此之前需要将之前VS Code帮忙下载的命令行工具(https://github.com/github/codeql-cli-binaries/releases)的目录加入到PATH中,以便在命令行中直接调用。

C:\repos\codeql-win64\codeql

在这里插入图片描述
命令为:

codeql database create java-sec-code --language=java

运行完之后右边就生成了这样一个结果:
在这里插入图片描述
选一个点进去看一下,确实是根据ql查询出来的结果:
在这里插入图片描述
然后使用这个目录下的:
https://github.com/Semmle/ql/blob/master/java/ql/src/Security/CWE
有很多CWE的漏洞类型:

CWE类型参考:
https://cwe.mitre.org/top25/archive/2019/2019_cwe_top25.html

试一下不安全的反序列化(CWE 502)的效果:
在这里插入图片描述
这个代码还是比较简单:
在这里插入图片描述
具体实现要看一下UnsafeDeserializationSink
在这里插入图片描述
其实就是找
Java原生反序列化:

java.io.ObjectInputStream#readObject, readUnshared

和XMLDecoder的反序列化:

java.beans.XMLDecoder#readObject

查看一下查询结果:
在这里插入图片描述
sink点确实是找到了,source点可能需要改进一下。

CWE-22:路径穿越

参考:
https://cwe.mitre.org/data/definitions/22.html

CWE-78:OS Command Injection

参考:https://cwe.mitre.org/data/definitions/78.html

CWE-79:XSS
CWE-89:SQLi
CWE-90:LDAP Injection
CWE-113: HTTP响应拆分
CWE-129: Improper Validation of Array Index
CWE-134: Use of Externally-Controlled Format String

就不一一列举了,codeql里自带的Java的CWE并不多,主要就是SQLi,OS injection,XSS等。

学习struts-CVE-2018-11776的挖掘方法

参考:
https://securitylab.github.com/research/apache-struts-CVE-2018-11776

就是从以往的Struts2的RCE的source和sink来找可能存在类似漏洞的点。
S2-032 (CVE-2016-3081),
S2-033 (CVE-2016-3687) and
S2-037 (CVE-2016-4438).

这个三个漏洞都是OGNL表达式的RCE,
methodName

OgnlUtil#getValue()

存在漏洞的代码如下:

String methodName = proxy.getMethod();    //<--- untrusted source, but where from?
LOG.debug("Executing action method = {}", methodName);
String timerKey = "invokeAction: " + proxy.getActionName();
try {
    UtilTimerStack.push(timerKey);
    Object methodResult;
    try {
        methodResult = ognlUtil.getValue(methodName + "()", getStack().getContext(), action); //<--- RCE

这里的可控变量methodName是由proxy对象产生的,它属于com.opensymphony.xwork2.ActionProxy类型。
然后认为这个类型的getMethodgetNamespacegetActionName方法可能产生用户可控的数据。
于是写出下面这样的QL代码:

class ActionProxyGetMethod extends Method {
  ActionProxyGetMethod() {
    getDeclaringType().getASupertype*().hasQualifiedName("com.opensymphony.xwork2", "ActionProxy") and
    (
      hasName("getMethod") or
      hasName("getNamespace") or
      hasName("getActionName")
    )
  }
}

predicate isActionProxySource(DataFlow::Node source) {
   source.asExpr().(MethodAccess).getMethod() instanceof ActionProxyGetMethod
}

到这里是完成了确定了source。
然后接下来是寻找sink。

按照以往出漏洞的点,先是想到了OgnlUtil#getValue(),以及TextParseUtil::translateVariables(),但是这个不是很深入,没有跟进到更底层的方法调用,即他们共同的执行OGNL表达式的方法。
后来觉得
OgnlUtil#compileAndExecute()
OgnlUtl#compileAndExecuteMethod
可作为sink。然后写出以下的sink:

predicate isOgnlSink(DataFlow::Node sink) {
  exists(MethodAccess ma | ma.getMethod().hasName("compileAndExecute") or ma.getMethod().hasName("compileAndExecuteMethod") |
    ma.getMethod().getDeclaringType().getName().matches("OgnlUtil") and
    sink.asExpr() = ma.getArgument(0)
  )
}

即找出方法调用为compileAndExecute或者compileAndExecuteMethod的,且调用该方法的对象为OgnlUtil(虽然不是全限定名。)

然后需要定义一个DataFlow Configuration,其实就是继承DataFlow::Configuration。代码如下:

class OgnlTaintTrackingCfg extends DataFlow::Configuration {
  OgnlTaintTrackingCfg() {
    this = "mapping"
  }

  override predicate isSource(DataFlow::Node source) {
    isActionProxySource(source)
  }

  override predicate isSink(DataFlow::Node sink) {
    isOgnlSink(sink)
  }

  override predicate isAdditionalFlowStep(DataFlow::Node node1, DataFlow::Node node2) {
    TaintTracking::localTaintStep(node1, node2) or
    exists(Field f, RefType t | node1.asExpr() = f.getAnAssignedValue() and node2.asExpr() = f.getAnAccess() and
      node1.asExpr().getEnclosingCallable().getDeclaringType() = t and
      node2.asExpr().getEnclosingCallable().getDeclaringType() = t
    )
  }
}

// 最后是执行查询(好像基本都是这个套路)
from OgnlTaintTrackingCfg cfg, DataFlow::Node source, DataFlow::Node sink
where cfg.hasFlow(source, sink)
select source, sink

反序列化查询学习

比如先用这样的代码找出调用了的java.io.ObjectInputStream#readObject的地方:

import java

from MethodAccess call, Method readobject
where
  call.getMethod() = readobject and
  readobject.hasName("readObject") and
  readobject.getDeclaringType().hasQualifiedName("java.io", "ObjectInputStream")
select call

参考:
https://securitylab.github.com/research/insecure-deserialization
结果如下:
在这里插入图片描述
但是这个结果可能包含了不被用户可控的ois
比如这个:
在这里插入图片描述
这里的文件路径并不可控。
为了找出那些读取了可控数据源的方法调用,我们就需要使用DataFlow这个库。
这个库包含两个有用的东西:
1、一个类RemoteUserInput,这个类代表污染的数据的来源,比如从http请求的参数;
2、一个member predicate flowsTo,则这个变量可以告诉我们数据能否从给定的source流到给定的sink。

接下来我们需要将刚才做的查询封装成一个类:

class UnsafeDeserializationSink extends Expr {
  UnsafeDeserializationSink() {
    exists(MethodAccess call, Method readobject |
      call.getMethod() = readobject and
      readobject.hasName("readObject") and
      readobject.getDeclaringType().hasQualifiedName("java.io", "ObjectInputStream") and
      this = call.getQualifier()
    )
  }
}

代表某种条件的表达式expressions (Expr),在其构造方法中写之前的查询逻辑。
然后可以写出完整的查询:

import java
import semmle.code.java.security.DataFlow

class UnsafeDeserializationSink extends Expr {
  UnsafeDeserializationSink() {
    exists(MethodAccess call, Method readobject |
      call.getMethod() = readobject and
      readobject.hasName("readObject") and
      readobject.getDeclaringType().hasQualifiedName("java.io", "ObjectInputStream") and
      this = call.getQualifier()
    )
  }
}


from RemoteUserInput source, UnsafeDeserializationSink sink
where source.flowsTo(sink)
select source, sink

不过可能时间有点久远了,以上的API有些失效了。
加了一个继承DataFlow::Configuration的类作为cfg,
source.flowsTo(sink)
->
cfg.hasFlowPath(source, sink)

现在的套路是:

from source, sink, conf
where conf.hasFlowPath(source, sink)
select source, sink

其中这里的conf继承自TaintTracking::Configuration

关于CodeQL的source

标准CodeQL库提供了一个类叫做:RemoteUserInput,用来表示可能的非可信的数据来源。一般来说,这个类可以较好的用于污点分析,包含多个来自HttpServletRequest类的source。但是当作者找漏洞的时候,他喜欢扩展这个类,以包含更多的source,

在新的API中,这个叫做:RemoteFlowSource

反序列化的修复方法:
仅在ObjectInputStream上读取基础类型的数据(比如readInt,因为任何类型都是Object的子类,所以不安全)
参考:
https://lgtm.com/rules/1823453799/

基础教程参考:

https://www.4hou.com/posts/yJOW
在线的控制台:
https://lgtm.com/query
选择语言,选择项目,然后执行QL查询即可!

在这里插入图片描述
或者直接执行这样的代码也行:
在这里插入图片描述

污点追踪

codeql提供了几种数据流的查询:

  • local data flow
  • local taint data flow
  • global data flow
  • global taint data flow

local data flow(DataFlow模块下面)基本是用在一个方法中的,比如想要知道一个方法的入参是否可以进入到某一个方法,就可以用local data flow
local taint data flow(TaintTracking模块下面)
global data flow(继承这个类DataFlow::Configuration)是用在整个项目的,比local data flow更强大,但是也更耗时,耗内存,而且没local data flow准确。
global taint data flow(继承这个类TaintTracking::Configuration)

参考:https://www.anquanke.com/post/id/203674
详细解释参考官方文档:
https://help.semmle.com/QL/learn-ql/java/dataflow.html

global data flow有这么几个predicates :

  • isSource—defines where data may flow from
  • isSink—defines where data may flow to
  • isBarrier—optional, restricts the data flow
  • isAdditionalFlowStep—optional, adds additional flow steps

global taint data flow有这么几个predicates:

  • isSource—defines where taint may flow from
  • isSink—defines where taint may flow to
  • isSanitizer—optional, restricts the taint flow
  • isAdditionalTaintStep—optional, adds additional taint steps

exprNode就比如这样的:
在这里插入图片描述
就是表达式节点。
下面以一个例子说明source和sink:
source:
在这里插入图片描述

sink:
在这里插入图片描述
代码为:

import java
import semmle.code.java.dataflow.DataFlow

from Constructor url, Call call, Parameter p, Expr src
where
  url.getDeclaringType().hasQualifiedName("java.net", "URL") and
  call.getCallee() = url and
  //DataFlow::localFlow(DataFlow::exprNode(src), DataFlow::exprNode(call.getArgument(0)))
  DataFlow::localFlow(DataFlow::parameterNode(p),DataFlow::exprNode(call.getArgument(0)))
select p, call

找出传入new java.net.URL() 的字符串常量:
在这里插入图片描述

修改待查询的java代码之后,要删除之前的database,然后重新进行查询。
目前没有比较方便的办法。
//TODO

global data flow 找传入new java.net.URL()的参数:
在这里插入图片描述
代码:

import semmle.code.java.dataflow.DataFlow
/**
找出全局的调用new java.net.URL()的硬编码字符串
*/

// 定义一个globa data flow
class TestConfiguration extends DataFlow::Configuration{
    TestConfiguration(){
        this = "TestConfiguration"
    }

    override  predicate isSource(DataFlow::Node source){
        source.asExpr() instanceof StringLiteral
    }

    override predicate isSink(DataFlow::Node sink){
        // 存在这样一个方法调用
        exists(Call call|
          // sink是call的第一个参数
          sink.asExpr() = call.getArgument(0) and
          // call的调用者(一个构造器)
          call.getCallee().(Constructor).getDeclaringType().hasQualifiedName("java.net", "URL")
        )
    }
}

// 开始查找
from DataFlow::Node src, DataFlow::Node sink, TestConfiguration config
where config.hasFlow(src, sink)
select src, "This string constructs a URL $@.", sink

完整的“找出所有global data flow从java.lang.System.getenv(…)到java.net.URL()的构造方法的通路”:

import java
import semmle.code.java.dataflow.DataFlow


// 查找调用java.lang.System.getenv(..)方法的source
class GetenvSource extends MethodAccess {
  GetenvSource() {
    exists(Method m | m = this.getMethod() |
      m.hasName("getenv") and
      //m.getDeclaringType() instanceof TypeSystem
      m.getDeclaringType().hasQualifiedName("java.lang", "System")
    )
  }
}

/**
找出全局的调用new java.net.URL()的硬编码字符串
*/

// 定义一个globa data flow
class TestConfiguration extends DataFlow::Configuration{
    TestConfiguration(){
        this = "TestConfiguration"
    }

    override  predicate isSource(DataFlow::Node source){
        //source.asExpr() instanceof StringLiteral
        source.asExpr() instanceof GetenvSource
    }

    override predicate isSink(DataFlow::Node sink){
        // 存在这样一个方法调用
        exists(Call call|
          // sink是call的第一个参数
          sink.asExpr() = call.getArgument(0) and
          // call的调用者(一个构造器)
          call.getCallee().(Constructor).getDeclaringType().hasQualifiedName("java.net", "URL")
        )
    }
}

// 开始查找
from DataFlow::Node src, DataFlow::Node sink, TestConfiguration config
where config.hasFlow(src, sink)
select src,  sink

Demo:
在这里插入图片描述

来源:
https://help.semmle.com/QL/learn-ql/java/dataflow.html
具体的codeql中关于Java的方法调用,AST语法之类的详细示例参考:
https://help.semmle.com/wiki/display/CBJAVA
在这里插入图片描述

其他教程:

Java的注解

https://help.semmle.com/QL/learn-ql/java/annotations.html

Java的泛型(子类、父类、接口、继承)

https://help.semmle.com/QL/learn-ql/java/types-class-hierarchy.html

Java的表达式和声明

https://help.semmle.com/QL/learn-ql/java/expressions-statements.html
具体的可以参考这个文档,有详细的AST的说明。
https://help.semmle.com/QL/learn-ql/java/ast-class-reference.html
空语句;
表达式语句;
if语句;
while语句;
do语句

声明

在这里插入图片描述

表达式

其中常用到的是:
字面量表达式:
在这里插入图片描述
一元表达式:

二元表达式:
在这里插入图片描述
赋值表达式:
在这里插入图片描述
访问:
在这里插入图片描述
杂项:
在这里插入图片描述

Java的方法调用

https://help.semmle.com/QL/learn-ql/java/call-graph.html
简单来说,
Callable是可以被调用的类,比如Method and Constructor
Call是调用别人的类。

QL语法教程

参考:
https://help.semmle.com/QL/ql-handbook/index.html

Predicates
参考:
https://help.semmle.com/QL/ql-handbook/predicates.html
从返回值来分:分为带结果的,和不带结果的。
三种:
characteristic predicates(构造器?)
member predicates(类的方法?)
non-member predicates(独立的方法?)

参考链接

Logo

瓜分20万奖金 获得内推名额 丰厚实物奖励 易参与易上手

更多推荐