0. 介绍

最近工作上做了一些渗透测试项目,在测试过程中发现有些系统使用了开源软件,通过对开源软件的代码审计,发现了该开源软件的漏洞,并利用该软件的漏洞拿下了目标。在整理完报告后,就一直在思考是否有一款工具,可以准确的告诉我:

  • 代码中有哪些 Web 接口;
  • 某个 Web 接口下面是否调用了某个函数;
  • 某个具体的函数能从哪些 Web 接口访问到;

如果能做到这些,把代码的信息提取出来,可以让我像查询 DB 一样对代码进行查询,会极大的提高我在审计这方面的效率。除此之外,支持的语言越多越好,谁也不知道下一个目标是什么语言开发的。:)

而目前市面上大部分工具,都是针对发现漏洞制作的,无一例外都使用了污点传播的方法。这就导致了一个问题,如果我单纯的想获知代码中的一些信息而不是扫描漏洞,这些工具是无法使用的,因为这些工具的目的是发现漏洞,而不是汇总代码中的信息。虽然这些工具在扫描过程中也会整理这些信息(例如CFG等),但是也不会保存起来,即便保存了,也无法方便的查询。

经过了一番调研,已经打算自己造轮子了,甚至思路都已经想的差不多了,突然想起来 ShiftLeft 提出的 CPG 以及对应的开源项目 Joern,试用了两天,感觉基本满足了需求,于是就有了这篇文章记录一下使用过程。

Joern 是一款基于 CPG 的静态分析工具,可以使用基于 OverflowDb 构建的查询语言,在 CPG 上进行搜索、查询、执行数据流分析,进而帮助安全人员分析代码,发现代码中的缺陷。

关于 Joern 的一些介绍,可以参考官方仓库:https://github.com/joernio/joern

CPG 全称 Code Property Graph,是由 AST + CFG + PDG 叠加构建而来(其实还包含 CG 等)的一张图,可以直观的表示程序的结构。

关于 CPG 的内容不再赘述了,有兴趣可以参考:

1. Joern VS CodeQL

前面说到了一个想法:“像查询 DB 一样对代码进行查询”,那么最容易直接想到的就是 CodeQL,为啥我没有直接使用 CodeQL 呢,有以下几个原因。

一方面,我个人认为 CodeQL 的语法不太友好,写起来很不顺手,而 Joern 的查询语言就很直接,可能与它是基于 Scala 开发的有关,写起来比较顺滑,不过这一点其实就有点因人而异了,没那么客观。

另一方面,也是最主要的方面,CodeQL 在执行查询之前,需要对项目进行构建或编译,很多情况下,我们拿到的代码构建起来很难,甚至无法构建,这种情况下 CodeQL 就无能为力了。

因此决定试一试 Joern。

2. 基础用法

2.1 安装

Joern 的安装非常简单,前提是需要 Java 11 及以上版本。

按照仓库中的 Quick Installation 安装即可,这里我们就不再赘述了。

2.2 基础语法

我们使用开源项目 java-sec-code 作为测试样例。

启动 joern 并导入待分析的项目:

./joern

joern> importCode("../java-sec-code/")
Using generator for language: JAVASRC: JavaSrcCpgGenerator
Creating project `java-sec-code` for code at `../java-sec-code/`
moving cpg.bin.zip to cpg.bin because it is already a database file
Creating working copy of CPG to be safe
Loading base CPG from: /Users/lightless/program/joern-cli/workspace/java-sec-code/cpg.bin.tmp
Code successfully imported. You can now query it using `cpg`.
For an overview of all imported code, type `workspace`.
Adding default overlays to base CPG
The graph has been modified. You may want to use the `save` command to persist changes to disk.  All changes will also be saved collectively on exit
res0: Cpg = Cpg (Graph [17522 nodes])

joern> cpg.metaData.l
res2: List[MetaData] = List(
  MetaData(
    id -> 1L,
    hash -> None,
    language -> "JAVASRC",
    overlays -> ArraySeq("base", "controlflow", "typerel", "callgraph", "dataflowOss"),
    root -> "/Users/lightless/program/java-sec-code",
    version -> "0.1"
  )
)

使用 importCode 导入待分析的项目。导入后,可以使用 cpg.metaData.l 查询 cpg 的基础元数据。

Joern-Cli 有一个小技巧,可以使用 Tab 键补全查询命令,例如 cpg.<TAB> 就可以列出所有可用的查询,在任何时候都可以使用 Tab 进行补全。由于 Joern 的文档还不太完善,使用 Tab 补全可以获取更多的信息。

ltoList 的简写,将查询结果转换为 List 并打印出来,这个命令会经常用到,类似的还有 toJson 等。

查询代码中所有的 method 列表:

joern> cpg.method.l
res5: List[Method] = List(
  Method(
    id -> 7L,
    astParentFullName -> "<empty>",
    astParentType -> "<empty>",
    code -> "protected SpringApplicationBuilder configure(SpringApplicationBuilder application)",
    columnNumber -> Some(value = 5),
    columnNumberEnd -> Some(value = 5),
    filename -> "/Users/lightless/program/java-sec-code/src/main/java/org/joychou/Application.java",
    fullName -> "org.joychou.Application.configure:<unresolvedSignature>(1)",
    hash -> None,
    isExternal -> false,
    lineNumber -> Some(value = 16),
    lineNumberEnd -> Some(value = 19),
    name -> "configure",
    order -> 1,
    signature -> "<unresolvedSignature>(1)"
  ),
  ...... // 省略
  Method(
    id -> 20L,
    astParentFullName -> "<empty>",
    astParentType -> "<empty>",
    code -> "public static void main(String[] args) throws Exception",
    columnNumber -> Some(value = 5),
    columnNumberEnd -> Some(value = 5),
    filename -> "/Users/lightless/program/java-sec-code/src/main/java/org/joychou/Application.java",
    fullName -> "org.joychou.Application.main:void(java.lang.String[])",
    hash -> None,
    isExternal -> false,
    lineNumber -> Some(value = 21),
    lineNumberEnd -> Some(value = 23),
    name -> "main",
    order -> 2,
    signature -> "void(java.lang.String[])"
  )
)

这个查询会列出所有 Method 节点的具体信息,如果只想展示具体的属性,可以使用 cpg.method.name.l,这个会列出所有的方法名称,并不会展示更具体的信息。如果像获取多个信息,比如方法名,行号,代码段,可以使用如下查询:

joern> cpg.method.map(n=>List(n.lineNumber, n.name, n.code)).take(3).l
res7: List[List[Object with Serializable]] = List(
  List(
    Some(value = 16),
    "configure",
    "protected SpringApplicationBuilder configure(SpringApplicationBuilder application)"
  ),
  List(Some(value = 21), "main", "public static void main(String[] args) throws Exception"),
  List(None, "<init>", "<empty>")
)

通过使用 map 函数,可以只展示选定的属性。

接下来看一个更加复杂的例子,比如我们要查询 getRequestBody 这个方法,都被哪些方法调用了,应该如何查询,最直接的想法是先找到对应的 Method,然后使用 caller 获取所有的调用者:

joern> cpg.method.name("getRequestBody").caller.l
res12: List[Method] = List(
  Method(
    id -> 6548L,
    astParentFullName -> "<empty>",
    astParentType -> "<empty>",
    code -> "public String parseXml(HttpServletRequest request) throws Exception",
    columnNumber -> Some(value = 5),
    columnNumberEnd -> Some(value = 5),
    filename -> "/Users/lightless/program/java-sec-code/src/main/java/org/joychou/controller/XStreamRce.java",
    fullName -> "org.joychou.controller.XStreamRce.parseXml:<unresolvedSignature>(1)",
    hash -> None,
    isExternal -> false,
    lineNumber -> Some(value = 23),
    lineNumberEnd -> Some(value = 29),
    name -> "parseXml",
    order -> 1,
    signature -> "<unresolvedSignature>(1)"
  ),..... // 省略其他内容
)

通过查询结果可以看到,其中一处在 parseXml 里调用了,但是具体是怎么调用的,在哪一行调用的,并没有给出明确的结果,所以我们稍微修改一下查询语句:

joern> cpg.call.name("getRequestBody").l.map(n=>(n.method.name, n.code, n.location.filename, n.location.lineNumber)).l
res11: List[(String, String, String, Option[Integer])] = List(
  (
    "parseXml",
    "getRequestBody(request)",
    "/Users/lightless/program/java-sec-code/src/main/java/org/joychou/controller/XStreamRce.java",
    Some(value = 25)
  ),...
)

这样就可以清晰看到在 XStreamRce.java 文件的第 25 行,在 parseXml 函数中,调用了 getRequestBody(request) 方法。

刚刚我们查找了某个函数在哪里被调用了,虽然只有一层,我们现在来查找一下,getRequestBody 方法中,是否调用了 convertStreamToString 方法。

joern> cpg.method.name("getRequestBody").callee.name("convert.*").l
res78: List[Method] = List(
  Method(
    id -> 12780L,
    astParentFullName -> "<empty>",
    astParentType -> "<empty>",
    code -> "public static String convertStreamToString(java.io.InputStream is)",
    columnNumber -> Some(value = 5),
    columnNumberEnd -> Some(value = 5),
    filename -> "/Users/lightless/program/java-sec-code/src/main/java/org/joychou/util/WebUtils.java",
    fullName -> "org.joychou.util.WebUtils.convertStreamToString:java.lang.String(java.io.InputStream)",
    hash -> None,
    isExternal -> false,
    lineNumber -> Some(value = 22),
    lineNumberEnd -> Some(value = 25),
    name -> "convertStreamToString",
    order -> 2,
    signature -> "java.lang.String(java.io.InputStream)"
  )
)

截止到目前为止,我们已经尝试了 joern 的常用用法,但是很不幸,现实世界远没有这么简单,我们目前只能查询单个函数内的调用,甚至无法跨多层函数。现在我们尝试一个更复杂的例子,我们看看 getRequestBody 这个方法,有哪些 Web 接口调用到了。

首先我们先尝试找出所有的 Web 接口,由于这是个 SpringBoot 项目,大部分 Web 接口都有 @RequestMapping 之类的注解,我们查询一下:

joern> cpg.method.where(_.annotation.name(".*Mapping")).map(n=>(n.name, n.annotation.code.l)).l
res112: List[(String, List[String])] = List(
  ("crlf", List("@RequestMapping(\"/safecode\")", "@ResponseBody")),
  ("index", List("@GetMapping(\"/\")")),
  ("post", List("@PostMapping(\"/post\")", "@ResponseBody")),
  ("codeInject", List("@GetMapping(\"/codeinject\")")),
  ("codeInjectHost", List("@GetMapping(\"/codeinject/host\")")),
  ("codeInjectSec", List("@GetMapping(\"/codeinject/sec\")")),
  ("vuln01", List("@GetMapping(value = \"/vuln01\")")),
  .... // 省略
)

接下来就要查询哪些接口,会调用到 getRequestBody 方法了,这里有两种思路,第一种就是从给定的方法 getRequestBody开始反向查找所有的调用方,看看调用方是否具有相关的注解。另外一种方法就是从所有带有相关注解的方法,正向向下展开查找。

第二种的方法的搜索成本会非常高,推荐使用第一种方法。我们只需要将前面的搜索语句,简单的修改一下即可:

// 方法1:反向查找所有的caller,直到有相关的注解为止
joern> cpg.method.name("getRequestBody").repeat(_.caller)(_.until(_.annotation.name(".*Mapping"))).size
res116: Int = 19

// 方法2:正向查找,非常费时,搜索成本极高
joern> cpg.method.where(_.annotation.name(".*Mapping")).repeat(_.callee)(_.until(_.name("getRequestBody"))).size
res119: Int = 19

可以看到,两种方法都查到了19个接口。这里我们使用了 repeat..until.. 语法进行重复操作,类似的有以下几种:

  • repeat..times..:重复指定次数。

    • 例如:x.repeat(_.caller)(_.times(5)) 重复调用五次 caller 查询。
  • repeat..until..:重复操作直到满足 until 中指定的条件。

    • 例如:x.repeat(_.caller)(_.until(_.name("foo"))),重复调用 caller 查询,直到找到一个方法名为 foo 的方法,找不到就返回空。
  • repeat..emit..times..:同 repeat..times..,emit 的作用是将搜索过的节点加入结果集,无论是否到达指定的 times 次数,可以简单理解为or关系。如果 emit 没有指定参数,则将所有搜索过的节点加入结果集,如果指定了参数,只将符合条件的节点加入结果集中。

    • 例如:x.repeat(_.caller)(_.emit(_.isMethod).times(5)),这个查询会将查询5次 caller 的结果加入结果集,同时将搜索路径上所有满足 isMethod (即所有Method节点)的节点也加入结果集中。
  • repeat..emit..until..:同 repeat..until..,其中 emit 的作用同上,不再赘述。

    • 例如:x.repeat(_.caller)(_.emit.until(_.name("foo"))),该查询会重复调用 caller 查询,直到找到方法名为 foo 的方法,同时会记录所有的搜索过的节点。可以简单的理解为将符合emit条件的节点或符合until条件的节点都加入结果集,但循环的终结依然是until指定的条件。

到现在为止,我们已经可以跨函数分析了,但是如果仔细看这些结果,会发现只打印出了 Web 入口,没有打印调用链路,我们并不知道代码是如何从 Web 入口运行到 getRequestBody 方法的。我们对查询语句简单的修改一下:

joern> cpg.method.name("getRequestBody").enablePathTracking.repeat(_.caller)(_.until(_.annotation.name(".*Mapping"))).path.l
或
joern> cpg.method.where(_.annotation.name(".*Mapping")).enablePathTracking.repeat(_.callee)(_.until(_.name("getRequestBody"))).path.l (不推荐)

现在返回的List中,每一项都是 Vectory,每一个 Vectory 都是代表一条路径,唯一美中不足的是,其中的 Call 和 Method 有些重复,不太直观,目前也还没找到解决方案。

enablePathTracking 的查询是我在 overflowDb 的源码里找到的,是否为正确用法暂不确定,反正确实解决问题了。:)

2.3 数据流分析

Joern 除了可以查询代码中的信息外,也可以执行一些数据流分析。使用数据流分析也非常简单,只需要定义好 source 和 sink,就可以调用 reachableBy 执行数据流分析了。

该查询的格式为:sink.reachableBy(source) 所以我们要先定义出 source 和 sink。以 SSRF 为例,我们简单的定义一下:

joern> def source = cpg.method.where(_.annotation.name(".*Mapping")).parameter
defined function source

joern> def sink = cpg.call.name("openConnection")
defined function sink

然后执行查询就可以了:

joern> sink.reachableByFlows(source).dedup.p
res146: List[String] = List(
  """__________________________________________________________________________________________________________________________________________________________________
| tracked                            | lineNumber| method           | file                                                                                        |
|=================================================================================================================================================================|
| URLConnectionSec(this, String url) | 43        | URLConnectionSec | /Users/lightless/program/java-sec-code/src/main/java/org/joychou/controller/SSRF.java       |
| SecurityUtil.isHttp(url)           | 46        | URLConnectionSec | /Users/lightless/program/java-sec-code/src/main/java/org/joychou/controller/SSRF.java       |
| isHttp(String url)                 | 30        | isHttp           | /Users/lightless/program/java-sec-code/src/main/java/org/joychou/security/SecurityUtil.java |
| url.startsWith("http://")          | 31        | isHttp           | /Users/lightless/program/java-sec-code/src/main/java/org/joychou/security/SecurityUtil.java |
| url.startsWith("https://")         | 31        | isHttp           | /Users/lightless/program/java-sec-code/src/main/java/org/joychou/security/SecurityUtil.java |
| boolean                            | 30        | isHttp           | /Users/lightless/program/java-sec-code/src/main/java/org/joychou/security/SecurityUtil.java |
| SecurityUtil.isHttp(url)           | 46        | URLConnectionSec | /Users/lightless/program/java-sec-code/src/main/java/org/joychou/controller/SSRF.java       |
| HttpUtils.URLConnection(url)       | 52        | URLConnectionSec | /Users/lightless/program/java-sec-code/src/main/java/org/joychou/controller/SSRF.java       |
| URLConnection(String url)          | 92        | URLConnection    | /Users/lightless/program/java-sec-code/src/main/java/org/joychou/util/HttpUtils.java        |
| new URL(url)                       | 94        | URLConnection    | /Users/lightless/program/java-sec-code/src/main/java/org/joychou/util/HttpUtils.java        |
| new URL(url)                       | 94        | URLConnection    | /Users/lightless/program/java-sec-code/src/main/java/org/joychou/util/HttpUtils.java        |
| u.openConnection()                 | 95        | URLConnection    | /Users/lightless/program/java-sec-code/src/main/java/org/joychou/util/HttpUtils.java        |
| u.openConnection()                 | 95        | URLConnection    | /Users/lightless/program/java-sec-code/src/main/java/org/joychou/util/HttpUtils.java        |
""",
  """______________________________________________________________________________________________________________________________________________________________
| tracked                             | lineNumber| method            | file                                                                                  |
|=============================================================================================================================================================|
| URLConnectionVuln(this, String url) | 37        | URLConnectionVuln | /Users/lightless/program/java-sec-code/src/main/java/org/joychou/controller/SSRF.java |
| HttpUtils.URLConnection(url)        | 38        | URLConnectionVuln | /Users/lightless/program/java-sec-code/src/main/java/org/joychou/controller/SSRF.java |
| URLConnection(String url)           | 92        | URLConnection     | /Users/lightless/program/java-sec-code/src/main/java/org/joychou/util/HttpUtils.java  |
| new URL(url)                        | 94        | URLConnection     | /Users/lightless/program/java-sec-code/src/main/java/org/joychou/util/HttpUtils.java  |
| new URL(url)                        | 94        | URLConnection     | /Users/lightless/program/java-sec-code/src/main/java/org/joychou/util/HttpUtils.java  |
| u.openConnection()                  | 95        | URLConnection     | /Users/lightless/program/java-sec-code/src/main/java/org/joychou/util/HttpUtils.java  |
| u.openConnection()                  | 95        | URLConnection     | /Users/lightless/program/java-sec-code/src/main/java/org/joychou/util/HttpUtils.java  |
""",
  """____________________________________________________________________________________________________________________________________________________________________________
| tracked                                           | lineNumber| method            | file                                                                                  |
|===========================================================================================================================================================================|
| httpURLConnection(this, @RequestParam String url) | 67        | httpURLConnection | /Users/lightless/program/java-sec-code/src/main/java/org/joychou/controller/SSRF.java |
| HttpUtils.HttpURLConnection(url)                  | 70        | httpURLConnection | /Users/lightless/program/java-sec-code/src/main/java/org/joychou/controller/SSRF.java |
| HttpURLConnection(String url)                     | 117       | HttpURLConnection | /Users/lightless/program/java-sec-code/src/main/java/org/joychou/util/HttpUtils.java  |
| new URL(url)                                      | 119       | HttpURLConnection | /Users/lightless/program/java-sec-code/src/main/java/org/joychou/util/HttpUtils.java  |
| new URL(url)                                      | 119       | HttpURLConnection | /Users/lightless/program/java-sec-code/src/main/java/org/joychou/util/HttpUtils.java  |
| u.openConnection()                                | 120       | HttpURLConnection | /Users/lightless/program/java-sec-code/src/main/java/org/joychou/util/HttpUtils.java  |
| u.openConnection()                                | 120       | HttpURLConnection | /Users/lightless/program/java-sec-code/src/main/java/org/joychou/util/HttpUtils.java  |
""",
  """____________________________________________________________________________________________________________________________________________________________________________________
| tracked                                               | lineNumber| method                | file                                                                                  |
|===================================================================================================================================================================================|
| httpURLConnectionVuln(this, @RequestParam String url) | 80        | httpURLConnectionVuln | /Users/lightless/program/java-sec-code/src/main/java/org/joychou/controller/SSRF.java |
| HttpUtils.HttpURLConnection(url)                      | 81        | httpURLConnectionVuln | /Users/lightless/program/java-sec-code/src/main/java/org/joychou/controller/SSRF.java |
| HttpURLConnection(String url)                         | 117       | HttpURLConnection     | /Users/lightless/program/java-sec-code/src/main/java/org/joychou/util/HttpUtils.java  |
| new URL(url)                                          | 119       | HttpURLConnection     | /Users/lightless/program/java-sec-code/src/main/java/org/joychou/util/HttpUtils.java  |
| new URL(url)                                          | 119       | HttpURLConnection     | /Users/lightless/program/java-sec-code/src/main/java/org/joychou/util/HttpUtils.java  |
| u.openConnection()                                    | 120       | HttpURLConnection     | /Users/lightless/program/java-sec-code/src/main/java/org/joychou/util/HttpUtils.java  |
| u.openConnection()                                    | 120       | HttpURLConnection     | /Users/lightless/program/java-sec-code/src/main/java/org/joychou/util/HttpUtils.java  |
"""
)

可以看到,Joern 找到了4条路径,接下来就可以人工验证一下是否存在问题了。

我们只是简单的定义了 sink 和 source,并没有处理是否有URL过滤、白名单的问题,所以可能会出现误报。而更复杂的查询,可以通过编写 sc 脚本来解决,这里暂不展开了,后面有机会可以详细展开讲讲。

3. Play In Real World

测试的开源软件尚未修复此漏洞,所以这里对目标代码打码处理了。

因为之前已经挖到了洞,漏洞主要是因为某个接口调用了 org.apache.maven.shared.invoker.DefaultInvoker.execute 方法,可以构建任意的 maven 项目,进而导致 RCE 漏洞。现在我们就来查询一下,是否有 Web 接口调用了该 execute 方法:

joern> cpg.method.fullName("org.apache.maven.shared.invoker.Invoker.execute:.*").repeat(_.caller)(_.until(_.annotation.name("(.*Mapping|OnOpen)"))).l
res4: List[Method] = List(
  Method(
    id -> 50741L,
    astParentFullName -> "<empty>",
    astParentType -> "<empty>",
    code -> "public void onOpen(@PathParam(\"id\") Long id, Session session)",
    columnNumber -> Some(value = 5),
    columnNumberEnd -> Some(value = 5),
    filename -> "xxx.java",
    fullName -> "com.xxxx.yyy.onOpen:<unresolvedSignature>(2)",
    hash -> None,
    isExternal -> false,
    lineNumber -> Some(value = 30),
    lineNumberEnd -> Some(value = 48),
    name -> "onOpen",
    order -> 2,
    signature -> "<unresolvedSignature>(2)"
  )
)

确实有一个 WebSocket 接口,调用了这个 execute 方法,但是没有具体的调用链路。因为这个漏洞在实际利用时,待编译的maven项目是通过其他接口上传的,所以污点传播分析肯定是分析不到的,所以我们简单的寻找调用流程即可:

joern> cpg.method.fullName("org.apache.maven.shared.invoker.Invoker.execute:.*").enablePathTracking.repeat(_.caller)(_.until(_.annotation.name("(.*Mapping|OnOpen)"))).path.l

结果这里就不贴了,最终是列出了调用链路,经过实际测试,也确实可以利用。

除了这种常规的查询外,Joern 也可以执行 sc 脚本进行查询,比如:./joern --script test.sc --params cpgFile=/src.path.zip,outFile=output.log 。也可以将 Joern 作为服务端跑起来,客户端通过 HTTP 与其进行通信。这些更多的功能还没有深入研究,后续有机会将随着更加深入的使用,逐步总结出来。

-1. 参考文档

  • https://github.com/joernio/workshops/blob/master/2021-RSA/RSA-LAB2-R08.pdf
  • https://github.com/ShiftLeftSecurity/overflowdb/blob/master/traversal/src/main/scala/overflowdb/traversal/Traversal.scala
  • https://docs.joern.io/home/