0x01前言最近CodeQL的概念很火,普遍认为这将是下一代的代码审计神器。网上已经有很多关于CodeQL的文章,但大部分还是在分析CodeQL的安装和简单使用案例。很少有文章真正使用CodeQL进行自动化代码审计。本文主要研究基于CodeQL的全自动代码审计工具的实现思路。预计文章会分为三部分,目前是第一部分。CodeQL(全称CodeQueryLanguage),从它的英文名字就可以看出这是一种基于代码的查询语言。它的作用主要是通过书面语句查询代码中可能存在的安全隐患。学习CodeQL类似于学习一门新的编程语言。语法和SQL类似,但还是比传统的SQL难很多。目前CodeQL支持多种语言,包括java、javascript、go、python、C、Csharp等,可惜不支持“世界上最好的语言”PHP。这可能是因为PHP太灵活了,函数名是一个字符串变量。从AST语法树静态分析问题确实很难,但这并不妨碍我们学习CodeQL的兴趣。文章所有内容基本围绕java语言展开,其他语言操作基本类似。0x02环境准备网上已经有很多关于CodeQL安装的文章。本来不打算再说这个事情的,但是因为我在安装CodeQL的过程中遇到了macm1架构不兼容的问题,所以我想很多朋友也会遇到这个问题。如果有问题,这里主要使用MAC环境来说明安装过程。CodeQL的安装主要分为engine和SDK,新建目录CodeQL(~/CodeQL/),用于存放后续所有相关工具和代码。首先下载最新的引擎包,下载地址为:https://github.com/github/codeql-cli-binaries/releases下载后解压codeql文件夹放入新建的CodeQL文件夹中,并添加环境变量。vim~/.profileexportPATH=/Users/username/CodeQL/codeql:${PATH}使用source命令使环境变量生效,然后在命令行运行codeql,如图2.1所示。图2.1CodeQL引擎安装然后需要下载CodeQL对应的sdk包。下载地址为:https://github.com/Semmle/ql下载后还需要将ql文件夹复制到~/CodeQL文件夹下。在CodeQL文件夹下新建databases文件夹,用来存放后面codeql生成的数据库。一切就绪后,我们的CodeQL目录下会出现三个文件夹,如图2.2所示。图2.2CodeQL安装完成后,我们可以使用codeqldatabasecreate命令创建一个查询数据库。命令如下。codeqldatabasecreate/Users/xxx/CodeQL/databases/project_db_name--language=java--source-root=/Users/xxx/cms/project_path--overwrite在windows环境和之前的mac环境确实没问题,但是如果是m1环境,会报错,报错信息如图2.3。报错原因是codeql官方提供的工具是基于x86架构的,不能直接在arm中使用。图2.3MACM1环境下Codeql运行报错从官网找到codeql对m1的支持,如图2.4。从图中可以明显看出codeql确实支持m1架构,但是需要依赖rosetta2和xcode。但是并没有给出具体的安装和使用步骤,不得不吐槽官方一点都不人性化,说到一半。图2.4CodeQL支持M1架构。后来慢慢尝试安装xcode和rosetta2。安装xcode是直接通过应用商店。安装rosetta2使用以下命令。softwareupdate–install-rosetta安装完成后,可以使用如下命令生成数据库。与传统方法不同的是,需要在命令前加上arch-x86_64,如图2.5所示。arch-x86_64codeql数据库创建/Users/xxx/CodeQL/databases/mvn_test--language=java--command='mvncleaninstall-DskipTests'--source-root=/Users/xxx/java/projects/mvn_test--overwrite图2.5M1中使用codeql生成数据库0x03语法基础CodeQL是一门全新的语言,网上已经有很多关于CodeQL基本语法的文章。学习前可以先参考链接了解CodeQL的基本语法,重点关注类和谓词的概念。参考链接:https://longlone.top/%E5%AE%89%E5%85%A8/%E5%AE%89%E5%85%A8%E7%A0%94%E7%A9%B6/codeql/2.CodeQL%E8%AF%AD%E6%B3%95/直接学习语法是一件很无聊的事情。这里我们只是总结一下CodeQL中的一些关键概念。语法的细节会在后续的实际案例分析中得到更深刻的理解。1)与类相关的概念与类直接相关的概念有Class、Method、Field、Constructor,它们代表的含义与Java语言相同,通过它们的相互组合,可以从数据库中筛选出符合条件的类和方法。Demo1:查询类全限定名包含Person类,getQualifiedName方法代表获取到的类对应的全限定类名。importjavafromClasscwherec.getQualifiedName().indexOf("Person")>=0selectc.getQualifiedName()演示2:查询所有字段Field,满足条件是字段类型为public,字段类型继承java.lang.Throwable。(如何找到Fastjson1.2.80的漏洞利用链)。其中,getASupertype表示获取到的类对应的父类,*表示递归查找所有父类。getDeclaringType表示获取字段对应的定义类型。getAModifier表示获取字段对应的修饰符。importjavafromClassc,Fieldfwherec.getASupertype*().hasQualifiedName("java.lang","Throwable")andf.getDeclaringType()=candf.getAModifier().getName()="public"选择c.getQualifiedName(),f.getName()2)Access相关概念access表示对变量或方法的调用,主要有VarAccess和MethodAccess。Demo1:查询所有继承自java.util.list的变量和变量引用。importjavafromRefTypet,Variablev,VarAccessvawheret.getSourceDeclaration().getASourceSupertype*().hasQualifiedName("java.util","List")andv.getType()=tandva.getVariable()=vselectv,vaDemo2:查询所有InputStream类对应的readObject方法调用(遍历反序列化漏洞的基础)。importjavafromMethodAccessma,Classcwherema.getMethod().hasName("readObject")和ma.getQualifier().getType()=candc.getASupertype*().hasQualifiedName("java.io","InputStream")selectma,ma.getEnclosingCallable()3)Type相关的概念Type表示一种类型,在CodeQL中是一个非常重要的概念。Type类有两个直接派生类PrimitiveType和RefType。PrimitiveType表示Java中的基本数据类型,派生类包括boolean、byte、char、double、float、int、long、short、void、、null。RefType表示Java中的引用类型,派生类有Class、Interface、EnumType、Array。在大多数情况下,Type与Access一起使用。事实上,上面的Acess例子中,几乎所有Type相关的类都用到了。4)Flow相关概念Flow是CodeQL中最重要的概念,代表数据流,对应的概念有source和sink。source表示一个可控的用户输入点,通常是指WEB站点中URL中的参数,如request.getParameter("name")。其他如命令行参数args也属于source。CodeQL中已经存在RemoteFlowSource类,类中定义了很多常用的源点,可以满足我们一般的代码审计需求。但是如果我们要挖掘具体的jar包漏洞,比如复现log4j2远程命令执行漏洞,由于log4j2包中没有常规源点,用户需要自定义源。Sink代表一个危险的功能,通常指的是一些危险的操作,包括命令执行、代码执行、jndi注入、SQL注入、XML注入等。虽然CodeQL也预设了一些sink点,但是远远不能满足实际需要。我们需要在不同的漏洞环境中自定义汇点。我们有了source和sink之后,就可以根据CodeQL提供的查询机制,自动判断是否有可以连接source和sink的flow。一个典型的用法如下,如图3.1所示。图3.1典型的流利用方式在图3.1所示的流中,自定义类继承自TaintTracking::Configuration并覆盖了isSource和isSink方法。这是一种固定的写法,后续的ql脚本大多都包含这样的代码。isAdditionalTaintStep方法是CodeQL类TaintTracking::Configuration提供的方法,其原型为:overridepredicateisAdditionalTaintStep(DataFlow::Nodenode1,DataFlow::Nodenode2){}。其作用是将一个可控节点A强行转移到另一个节点B上,则B节点成为可控节点,如图3.2所示。图3.2isAdditionalTaintStep方法连接函数如果CodeQL不能自动连接node1和node2节点,需要通过isAdditionalTaintStep方法手动指定连接。另外,Flow中还有一个方法经常用到isSanitizer,如图3.3所示。这是官方关于log4j2漏洞的查询脚本,定义了isSanitizer方法来限制flow流中的数据来自基本数据类型PrimitiveType和BoxedType。这是一种特别常用的过滤机制,也就是说只要是常规字符类型(Bool、int等),就不再通过。图3.3isSanitizer方法0x04的过滤效果案例实践作为新手,自己写一个有效的CodeQL查询脚本是非常困难的。幸运的是,CodeQL官方为我们提供了大量的demo。参考地址:https://github.com/github/codeql/tree/main/java/ql/src/experimental/Security/CWE我们可以直接使用这些demo来完成一些漏洞发现功能。为了更清楚的了解CodeQL的使用,下面通过具体案例来展示CodeQL的作用。若一若一是一款在国内被广泛使用的后台管理系统。我从网上下载了若仪某版本的源码。1)根据若仪源码生成数据库arch-x86_64codeqldatabasecreate/Users/pang0lin/CodeQL/databases/若仪--language=java--command='mvncleaninstall-DskipTests'--source-root=/Users/pang0lin/cms/如果按若一--overwrite成功生成数据库,会返回类似成功的界面,如图4.1。图4.1基于若一创建数据库2)使用官方demo查询漏洞官网提供了很多查询ql脚本,其中有两个脚本可以直接找到若一相关的漏洞,第一个脚本是spel注入的查询脚本表达。参考地址:https://github.com/github/codeql/blob/main/java/ql/src/experimental/Security/CWE/CWE-094/SpringViewManipulation.ql)查询结果如图4.2所示。图4.2基于SpringViewManipulation的查询结果查看汇点详情可知,该漏洞为用户输入的片段直接传入模板引擎,如图4.3所示。图4.3追踪汇点后的结果。该漏洞实际上是Zoe的一个已知安全问题。详见:https://blog.csdn.net/qq_33608000/article/details/124375219#Thymeleaf_184虽然在最新版本的Zoyi中,thymeleaf版本已经升级无法使用,但是这个问题从CodeQL的角度还是可以找到的。另一个可用的CodeQL查询脚本是基于mybatis的SQL注入查询脚本,参见:https://github.com/github/codeql/blob/main/java/ql/src/experimental/Security/CWE/CWE-089/MyBatisMapperXmlSqlInjection。ql查询结果如图4.4所示。图4.4根据MyBatisMapperXmlSqlInjection的查询结果,可以看到CodeQL发现了可能的SQL注入漏洞,跟进下沉点,如图4.5所示。每一个都是类似的问题,我们就打开来看一个。由此可见,这里的参数是传递给SQL语句的,造成了SQL注入漏洞。网上的漏洞详情中也提到了这个漏洞。详见:https://juejin.cn/post/7001087308510265352从以上两个查询可以看出CodeQL给代码审计过程带来的便利。它可以很容易地帮助我们定位可能存在的漏洞。0x05结论CodeQL为我们提供了很多查询ql脚本。手动一一尝试不是很好的解决办法,而且官方的ql脚本也不是很完善,还有很大的改进空间。如何使用大量的ql脚本来完成自动化扫码,我们将在下一篇文章中进行讲解。本文作者:盛邦安全WebRAY,转载请注明来自FreeBuf.COM
