Code Analysis With Joern
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 的内容不再赘述了,有兴趣可以参考:
- Why Your Code Is A Graph <- 简单易懂,推荐阅读
- Code Property Graph Specification 1.1 <- CPG 规范,可以当作字典使用
- Finding Stranger Things in Code - STATIC ANALYSIS USING JOERN <- ShiftLeft Inc 的演讲 PPT,推荐阅读
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 补全可以获取更多的信息。
l
是 toList
的简写,将查询结果转换为 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/
您好 什么叫 重复调用五次 caller 查询。我太愚蠢了,不能理解,可以指教下么
> repeat..times..:重复指定次数。
> 例如:x.repeat(_.caller)(_.times(5)) 重复调用五次 caller 查询。
---
x.repeat(_.caller)(_.times(5)) 等价于 => x.caller.caller.caller.caller.caller
joern可以直接分析字节码吗,如果我只能拿到编译构建好的jar/war呢
可以的
博主您好,请问如果我想要提取某个开源项目(p)中、所有调用test.java文件的方法m的一级caller,考虑到java中的继承、多态和重载等特性,使用joern是否能够精确提取?谢谢回答~
时间有点久了,而且近期我也没有在继续使用 joern 了 XD。
我不太确定继承、多态等特性是否可以分析,抱歉。
好的好的,谢谢回答~
joern的java分析支持继承多态和重载
大致查询语句为 cpg.typeDecl.name("test").method.caller
感觉joern好多问题啊,我分析JNI的native层,各种问题,比如返回值不为JNI类型就没法识别,还有java的lambda匿名函数也是直接unresolved,不分配行号