代码审计 | 用CodeQL写QL Code





 引   言 

2019年9月,一个开源的新一代代码分析工具引起了安全界轩然大波,它就是CodeQL。它是一款静态源代码分析工具。不同于上一代,它的误报率低,很多时候简单一扫就能发现真实的漏洞。它的概念比较新颖,使用类似SQL查询的方式,把代码转换成一个数据库,并对其进行语法查询,以此大大提升了其准确性。再加上它是开源的,且很容易自定义强大的规则,已成为新一代代码扫描工具中的利器。我们今天来对CodeQL的原理进行一些了解,熟悉其使用方法,并自己上手写出任意的新规则。

此文章假设读者对Java有一定了解;如果对一阶逻辑和函数式编程有接触会很有帮助。关于CodeQL的基础用法,可以参考CodeQL官方文档,本文不再赘述。

01 原理介绍

如果用过类似SonarQube(免费版)的上一代代码扫描工具,你就会知道误报是多么常见的一件事。尤其是作为开发人员,如果CICD平台集成了某种安全扫描流水线,那么需要经常清理其误报。而在误报太多的情况下,真正的问题也容易被忽略。

我们以代码注入来举例,以下4个函数,在SonarQube看来都是有问题的,但是只有第一个才是真正的问题,因为其可以被攻击者利用。其他的代码根本没有用到,不造成任何实际的安全威胁。

代码审计 | 用CodeQL写QL Code

但是用CodeQL能准确判别什么危险代码是可以被攻击者利用的。CodeQL对代码进行了深度的理解。对它来说代码不再是需要扫描关键字或正则的字符串,而是抽象语法树(AST)。CodeQL能看懂代码的结构和语法上的含义。比如,它知道一个类有多少个函数、继承了什么父类,一个方法在所有代码中的调用,以及一段代码是否是在try catch块中等等。

比如,以下便是一段伪代码(来自维基百科)。

代码审计 | 用CodeQL写QL Code

以下是其解析后得到的AST。树的根有两个分支,也就是代码语法上被分为了两块,一块是while代码块,一块是return。Return只有一个参数,而while代码块可以往下细分为循环条件和body。可以看到条件是比较两者是否相等,而这两者便是条件下的两个分支,b和0。类似的,程序的所有部分都可以被递归解析成AST的结构。

代码审计 | 用CodeQL写QL Code

显而易见的是,有了AST,我们可以清晰地知道这段代码中有多少个比较条件,多少个if代码块等等。使用有语法信息的AST分析,要比直接对代码以字符串的方式分析方便得多。

CodeQL把代码解析成字符串的方式和编译器相同。事实上,很多安全静态源代码扫描工具中使用的技术,和编译器的技术相同。编译器或者解析器都需要把源代码从字符串,根据语法规则解析成AST,并赋予其实际含义。根据解析的AST,编译器了解其语法结构,并以此将其转化为另一种语言,比如java字节码;解析器可以直接根据AST的结构,找到对应的指令和参数,并运行其代码。

CodeQL将AST输入了数据库,可以用类似SQL的方式轻易查询。我们可以想象每个语法结构(例如try catch代码块,while代码块,对比代码块等)都是一张表,都可以去SQL搜索。比如这个查询可以找到所有不判断call返回码的代码(截图不全)。

代码审计 | 用CodeQL写QL Code

这是查询结果。

代码审计 | 用CodeQL写QL Code

有了AST的数据库,CodeQL就可以进行更深入的分析,例如污点追踪。

我们先了解一下污点追踪。虽然这个词你可能没有听过,但是这个概念对于安全研究员并不陌生。首先我们定义source是外部输入。它是脏的、可能是恶意的,是污点的传染源。然后再定义sink是不能有污点的代码,例如SQL命令拼接、exec、eval等不应该被外部操控的代码。然后我们追踪这个污点,是如何从source传染下去的,以及是否进入了sink。我们看以下例子中,para是外部数据,所以是污点。在第40行,这个污点被传入了“logService.logArch”函数中。然后para在101行被传入,107行被拼接使污点传递到了cmd变量上,最终在108行作为sink的危险函数中传入。这便是这个命令注入漏洞的整个数据传递链。

代码审计 | 用CodeQL写QL Code
代码审计 | 用CodeQL写QL Code

02 规则自定义

CodeQL有很多自带的规则,并涵盖了很多的CWE,但是想要发挥其更多潜力,还需要自己写CodeQL规则。CodeQL其实是一个声明式编程语言,类似SQL。我们不需要告诉它怎么去做,而只是告诉它我们想要查询哪些字段,然后它会自己编译并优化QL命令去数据库里查询对应的信息。这样的编程方式不同于大家已熟知的命令式编程语言,所以可能理解起来会有些难度。

我们学习的材料是CodeQL官方的CTF题目及答案“CodeQL and Chill”。这是一个Netflix的真实的CVE漏洞,程序语言为Java,漏洞类型为Java EL注入。我们的目标是写一个精准的CodeQL规则,能把这一类的漏洞都涵盖,且很少出现误报。在我们一起做这道题之前,感兴趣的朋友可以自己先挑战一下这道题。以下的链接包括了关于这道题所需的所有信息,包括题目涉及的CodeQL数据库。

https://securitylab.github.com/ctf/codeql-and-chill/ 

如果没有安装CodeQL的朋友可以在官网安装。我推荐Visual Studio Code的安装方式。请在环境准备好后再继续。

https://codeql.github.com/ 

漏洞介绍

那么我们开始。在编写这个漏洞的扫描规则之前,我们需要先了解这个漏洞。尽管题目链接中包括了所需的所有信息,为了阅读连贯性,我将在这里做一个简单的介绍。代码库是Netflix的titus管理面,漏洞号是CVE-2020-9297。本次的sink是ConstraintValidatorContext. buildConstraintViolationWithTemplate的第一个参数,如果被外部数据控制,可造成Java EL注入。

题目1.1 定义Source

我们认为所有传入ConstraintValidator.isValid的第一个参数都是外部数据,也就是source。现在,我们需要并找到所有相关的代码。正确的查询语句会返回6个结果。

这其实比看上去要困难一点。我们先找到ConstraintValidator的isValid方法本身。创建一个ql文件,例如“article.ql”。写一个QL语句,table为Method,查询所有的方法。Predicate类似一个函数,其内容是布尔判断。getIsValid的条件为方法名是“isValid”,且拥有此方法的类或interface的全名为“javax.validation.ConstraintValidator”。然后在select中返回其方法。

此时确保你的CodeQL选择了titus的CodeQL数据库(在题目链接中下载)。最后点击图中箭头指向的两个按钮都可以运行该语句。右上角会运行QL指令,而“Quick Evaluation: getIsValid”会单独跑predicate的查询。

代码审计 | 用CodeQL写QL Code

可以从结果看到一个匹配,所以我们的代码成功了。由于这个方法来自于javax而不是titus的业务代码,所以点击isValid链接也不会跳转到对应代码。

代码审计 | 用CodeQL写QL Code

在接着下一步之前,我们应该改善一下这丑陋的代码。作为简单的QL语句是很好的,但是由于我们要做的事情较为复杂,所以写一个符合面向对象编程规范的代码更优美、更易维护。

在第1行,导入Java QLL库。然后第3行定义一个类。这个类就是CodeQL中的ConstraintValidator这个interface。也就是说,如果我们把TypeConstaintValidator当做一个table,对其进行数据库查询,就会查到代码中的这个interface。类似的,getIsValidMethod就会查到代码中该interface的isValid方法;我们的代码将所有的Method(也就是方法)进行对比,并匹配属于“this”也就是当前interface的方法,且其命名为“isValid”。第16行是没有功能的代码,暂时放在那里以符合ql语法。这样改写后跟原来的功能相同,但是代码质量好了很多。点击“Quick Evaluation: getIsValidMethod”查询。

代码审计 | 用CodeQL写QL Code
代码审计 | 用CodeQL写QL Code

可以注意到代码中所有方法的body中都是布尔判断。代码中调用的方法可以在Visual Studio Code中跳转到源码。如果对这段代码没有完全理解,可以先阅读文档对语法进行熟悉,再继续以下的步骤。下面将不再对语法一一解释。

然后我们找到所有该interface方法的实现。定义一个ConstraintValidatorIsValidMethod类来继承Method类,其匹配任何覆盖了该interface的方法。注意代码中使用了any语法,虽然我们知道这个interface匹配的只有一个,但是在ql语法中还是需要使用任意匹配的类。代码完成后点击按钮进行查询。

代码审计 | 用CodeQL写QL Code

得到13个结果。我们点开看一下哪些是错误匹配的。可以发现,第7到13个结果都点不开,也就是说不在源码内,而是在某个库中,这不是我们要找的。

代码审计 | 用CodeQL写QL Code

我们定义source为isValid的第一个参数,且isValid来自于titus业务代码。

代码审计 | 用CodeQL写QL Code

然后就能找到正确的source。

代码审计 | 用CodeQL写QL Code

题目1.2 定义Sink

我们认为ConstraintValidatorContext.buildConstraintViolationWithTemplate的第一个参数是sink,不能被外部数据控制。相关的代码应该有5个。

首先我们定义其类,如法炮制1.1的写法。大家注意下图exists的写法。这个概念来自于一阶逻辑,“|”之前的符号代表了类型和变量,之后是筛选条件。意思是所有符合该类型的变量,只要有一个符合后面的条件,就返回true。

代码审计 | 用CodeQL写QL Code

但是我们的source是方法定义,而sink是方法调用,是两个不一样的概念,请注意65-67行的变化。运行后得到5条。

代码审计 | 用CodeQL写QL Code

题目1.3 设置污点追踪

设定污点追踪的配置,例如source和sink。设置成功后,会得到0个结果。这一题非常简单。

我们引入了几个新的QLL依赖库,以及更改了最下面的查询语句。注意第1行的元数据是需要的,否则污点追踪结果无法正常展现。

代码审计 | 用CodeQL写QL Code

题目1.4 Partial Flow

在配置污点追踪的时候,我们定义了source和sink,但是明明污点会传过去,CodeQL却发现不了。这是因为它默认的传递污点逻辑和我们需要的不同。那么它是怎么传递的,该如何修复呢?Partial flow可以用来找到当前CodeQL搜索的路径,用来调试我们的污点追踪。

如果你看官方答案,你会看到这个模板。这是2020年的代码,有些已经废弃,有些直接用不了了,例如“import DataFlow::PartialPathGraph”。

代码审计 | 用CodeQL写QL Code

这也是我们学习CodeQL的时候经常会遇到的问题,就是教程太老,已经不完全跑得起来了。不过解决起来也很简单,我们直接搜索“CodeQL partial flow”,就能找到最新的用法。我们这里将使用污点追踪新的用法。现在可以运行这个命令,并确认运行成功,还是0结果。

https://github.blog/changelog/2023-08-14-new-dataflow-api-for-writing-custom-codeql-queries/ 

代码审计 | 用CodeQL写QL Code

然后进行Partial flow。注意我们使用FlowExplorationFwd来分析正向数据路径,如果有需要也可以用FlowExplorationRev分析反向数据路径。由于我们事先知道漏洞代码在哪里,所以可以有针对性地分析对应函数中的污点。漏洞代码的source是container,对应src列,函数式isValid,对应c列。

代码审计 | 用CodeQL写QL Code

题目1.5 找到缺失的污点传播步骤

点击返回结果中对应的函数,查看污点追踪的路径。我们看到一个有点奇怪的情况,明明common已经被认为是污点了,为什么没有把污点传递到拼接后的字符串上呢?是因为字符串拼接一个HashSet不传播污点吗?

代码审计 | 用CodeQL写QL Code

这里的问题比较隐蔽。如果好奇的话我建议大家先亲自试一下添加几个污点传播步骤,尝试一下。问题其实并不在上面图中的这一行。如果我们仔细查看n列,会发现其中详细描写了当前AST的类型,而且它的类型和预想的不一样。污点传播的不是这个HashSet,而是HashSet当中的element,HashSet[],也就是其中的每一项。然而仅仅因为HashSet的element带有污点,不会让HashSet拼接的字符串也被污点传递。但是如果HashSet本身(不是其中的某个element)有污点,其拼接的字符串是会传播污点的。这就是为什么CodeQL无法找到传播路径。

我们现在需要想办法把污点传播给整个HashSet。那么谁需要把污点传给HashSet呢?通过报告和源码可以看到是container中(类型为Map的)属性的keySet。能看到keySet也不是污点,因为污点在keySet中的类型为Set[],而不是Set本身。所以,我们先让keySet成为污点,再让HashSet也成为污点,就可以把污点传给拼接的字符串,也就是我们的sink。

现在我们把技术原理了解了,但还面临着一个哲学问题,那就是为什么CodeQL不去自动传播这条污点路径呢?从题目中的解释可以看到,CodeQL是期望尽量少地设置污点传播路径的,但是如果写规则的时候需要,可以自己加。因为CodeQL不同于其他工具的一大特点,就是它的误报率较低。而把传播路径增加很可能使误报率上升,是不理想的。

一个哲学问题解决了,还有另一个哲学问题,就是能否在不提升误报率的情况下,找到更多的漏洞?CodeQL认为,如果HashSet的element是污点,那么其HashSet本身不一定是污点。如果认为是污点,会导致误报。但是我们知道,虽然HashSet不一定是污点,HashSet用于拼接字符串时却一定会传播污点。那么,我们是否可以写这么一条污点传播路径,当HashSet[]是污点,element的类型是String,且对应的HashSet被用作字符串拼接的第二个参数的时候,传播污点给被拼接的字符串。经过一番尝试可以看出,也许这就是CodeQL乃至污点追踪本身的限制,无法做过于复杂的判断。有了这样的思考,在使用或编写CodeQL规则的时候,能对其寻找漏洞的能力有更深刻的理解。

题目1.6 添加污点传播步骤

自定义一个类继承“TaintTracking::AdditionalTaintStep”,这样在污点追踪的时候,这个污点传播步骤就会生效。在这个step中,n1是污点,n2是需要传染的数据。传染条件是,当n1是”Map.keySet()”的qualifier的时候,把整个方法调用本身都看做污点。

代码审计 | 用CodeQL写QL Code

再次运行Partial flow,这次可以看到step成功了,20行的Set本身被认为是污点了,但是HashSet依然不是污点。

代码审计 | 用CodeQL写QL Code

题目1.7 添加通过Constructor的污点传播

源码中container本身是污点,container中属性的keySet(类型为Set[])也是污点。在HashSet用Set[]的污点传播的时候,默认会传给HashSet[]。那么为了我们能找到这个漏洞,写一条规则,让HashSet本身也被传递污点。

代码审计 | 用CodeQL写QL Code

查看partial flow结果,成功把污点传递到sink。

代码审计 | 用CodeQL写QL Code

题目1.8 找到漏洞,提交报告

不知道是不是2020年的CodeQL和现在有什么不一样,我们只添加两个step就已经找到了漏洞。这一步自动完成了。直接运行整个查询命令,得到了我们想要的结果。

代码审计 | 用CodeQL写QL Code

题目2 找到另一个漏洞

另一个漏洞在SchedulingConstraintValidator,我们直接如法炮制,不需要其他新奇的技术。首先找到partial flow中哪里传播失败了。可以看到缺少stream、map、collect等传播。这一步跟之前几乎一样,只是添加step。这里就不多赘述了,给大家当做一个小挑战。

代码审计 | 用CodeQL写QL Code

成功找到第二个漏洞。

代码审计 | 用CodeQL写QL Code

题目3 Exception污点传播

由于当前的漏洞与异常有关,所以应该考虑通过异常,从try传播到catch的污点。可能192-193行有的朋友看起来很别扭,其实很简单,就是找到有对应关系的try、catch和call。CodeQL中不存在变量赋值,所以以这样的方式编写。熟悉函数式编程的朋友应该对此不陌生。

代码审计 | 用CodeQL写QL Code

题目HHHKB 从用户输入到isValid设置

官方的题目到这里就结束了。第4题需要写一个PoC脚本,由于跟CodeQL不强相关,本文不会讨论。现在由我来出一道题。

Netflix的titus管理面代码使用了JAX-RS的web框架接受用户数据。以上题目中我们定义了特定条件下isValid的第一个入参为source,但是并没有把真正的source,比如说web框架下的请求体算进来。我们可以试一下把请求体作为source,把isValid的第一个参数作为sink会找到什么。(提示:如果遇到困难,可以参考官方题解的Bonus部分)

以下为一个成功的扫描结果。污点追踪的传播路径找到了ContainerHealthProvider的attributes字段,并认为其会传入isValid的第一个参数。可以看到attributes有一个标注“@CollectionInvariants”,它使用了CollectionValidator。也就是说,attributes会进入isValid的第一个参数。

代码审计 | 用CodeQL写QL Code
代码审计 | 用CodeQL写QL Code





 总   结 

本篇文章简单介绍了CodeQL的特点,以及AST、污点追踪等原理。我们编写了一个CodeQL规则,精准地找出了一个CVE漏洞。在过程中,我们学会了用类似SQL的方式去查询代码、定义source和sink、设置污点追踪逻辑、调试污点传播步骤和找到用户输入到目标方法的数据流。

我个人认为学习CodeQL或类似的工具是非常有用的。我们可以尝试自己不断地写出新的CodeQL规则,去自动化找到新的漏洞。平时也可以用CodeQL去搜索代码,而代替简单的关键词字符串搜索或正则搜索。当然,想用好CodeQL需要对其进行更深入的了解;这不仅体现在读写CodeQL,也要思考其设计理念和理论上的瓶颈。

代码审计 | 用CodeQL写QL Code

原文始发于微信公众号(中尔安全实验室):代码审计 | 用CodeQL写QL Code

版权声明:admin 发表于 2024年5月11日 下午5:31。
转载请注明:代码审计 | 用CodeQL写QL Code | CTF导航

相关文章