CodeQL的自动化代码审计之路(上篇)

发布者:盛邦安全
发布于:2022-11-14 14:57


0x01 前言


最近关于CodeQL的概念很火,大家普遍认为这会是下一代的代码审计神器。网上关于CodeQL的文章已经有不少,但是多数文章还是在分析CodeQL的安装和简单使用用例。真正使用CodeQL来进行自动化代码审计的文章较少,本文主要研究基于CodeQL实现全自动的代码审计工具实现思路预计文章分成三部分完成,目前是第一部分内容。


CodeQL(全称Code Query Language),从其英文名称中可以看出这是一种基于代码的查询语言,其作用主要是通过编写好的语句查询代码中可能存在的安全隐患。学习CodeQL类似于学习一门全新的编程语言,语法类似于SQL,但是比传统SQL还是要难很多。目前CodeQL支持对多种语言,包括java、javascript、go、python、C、Csharp等,但是很遗憾的是不支持“世界上最好的语言”PHP。这大概是因为PHP实在是太灵活了,函数名是字符串变量这种调用方式确实很难从AST语法树中静态分析出问题,但这并不能阻碍我们学习CodeQL的兴趣。文章所有内容基本上围绕java语言展开,其他语言操作基本类似。


0x02 环境准备


网上关于CodeQL安装的文章已经很多了,本来不打算再说这个事情,但是因为本人在CodeQL安装过程中遇到不兼容mac m1架构的情况,我想还有很多小伙伴也会遇到这个问题的,这里主要以MAC的环境来说明安装过程。


CodeQL的安装主要分成引擎和SDK,新建一个目录CodeQL(~/CodeQL/)来保存后续所有的相关的工具和代码。


首先下载最新的引擎包,下载地址是:a38K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3V1k6U0L8$3c8W2M7h3I4Q4x3X3c8U0L8r3W2Q4x3X3c8T1K9h3&6S2M7X3W2W2M7#2)9J5c8Y4u0W2L8r3g2S2M7$3g2K6


下载之后解压把codeql文件夹放在刚才新建的文件夹CodeQL中,添加环境变量。

    vim ~/.profile
    export PATH=/Users/用户名/CodeQL/codeql:${PATH}


    使用source命令是环境变量生效,然后命令行中运行codeql,如图2.1所示。

    图片

    图2.1 CodeQL引擎安装


    然后需要下载CodeQL对应的sdk包,下载地址是:

    ed3K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6e0k6h3#2E0L8r3g2Q4x3V1k6I4L8l9`.`.


    下载之后也需要把ql文件夹复制到~/CodeQL文件夹中。


    在CodeQL文件夹中新建databases文件夹,用于存放后续使用codeql生成的数据库,那么一切准备好了之后我们的CodeQL目录之下就会是三个文件夹,如图2.2所示。

    图片

    图2.2 CodeQL安装


    后续我们就可以使用codeql database create命令来创建查询数据库,命令如下所示。

      codeql database create /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.3 在MAC M1环境中codeql运行错误


      从官网中找到了codeql对m1的支持情况,如图2.4所示。从图中可以明确看出codeql确实是支持m1架构的,但是需要依赖rosetta2和xcode。但是并没有给出具体的安装和使用步骤,必须吐槽官方一点也不人性化,说话说一半。

      图片

      图2.4 CodeQL支持M1架构


      后来慢慢摸索着装xcode和rosetta2,安装xcode是直接通过appstore来装的,安装rosetta2是使用下面的命令。

        softwareupdate –install-rosetta


        安装好了之后就可以使用下面的命令来生成数据库,与传统方式不同的是需要在命令前面增加arch -x86_64,如图2.5所示。

          arch -x86_64 codeql database create /Users/xxx/CodeQL/databases/mvn_test --language=java --command='mvn clean install -DskipTests' --source-root=/Users/xxx/java/projects/mvn_test --overwrite


          图片

          图2.5 在M1中使用codeql生成数据库



          0x03 语法基础


          CodeQL是一门全新的语言,基础的CodeQL语法网上已经有很多文章。大家在学习之前可以首先参考链接,了解关于CodeQL的基础语法,重点掌握关于类和谓词的概念。


          参考链接:598K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6D9L8$3&6Y4L8r3!0F1k6g2)9J5k6i4c8G2M7q4)9J5c8W2)9J5y4f1f1#2i4K6t1#2b7f1g2Q4x3U0f1^5z5g2)9J5y4f1f1#2i4K6t1#2z5o6g2Q4x3U0g2m8z5q4)9J5c8W2)9J5y4f1f1#2i4K6t1#2b7f1g2Q4x3U0f1^5z5g2)9J5y4f1f1#2i4K6t1#2z5o6g2Q4x3U0g2m8z5q4)9J5y4f1f1%4i4K6t1#2b7e0m8Q4x3U0f1&6y4q4)9J5y4f1f1%4i4K6t1#2b7e0W2Q4x3U0g2n7y4W2)9J5c8X3y4G2k6r3g2I4L8q4)9J5c8U0u0Q4x3X3g2o6L8$3c8W2f1f1I4Q4x3U0g2q4z5q4)9J5y4f1q4r3i4K6t1#2b7f1c8Q4x3U0g2q4y4W2)9J5y4f1t1K6i4K6t1#2z5e0g2Q4x3V1j5`.


          直接来学习语法是一件很枯燥的事情,我们这里只是总结一些CodeQL中重点的概念。关于语法详情在后续的实际案例分析中会有更深刻的体会。


          1)  与Class相关的概念


          与类直接相关的概念包括Class、Method、Field、Constructor,其代表的意义与java语言一致,通过其相互组合可以从数据库中筛选出符合条件的类和方法。


          Demo1: 查询类的全限定名中包含Person的类,其中方法getQualifiedName代表获取类对应的全限定类名。

            import java
            from Class cwhere c.getQualifiedName().indexOf("Person") >=0select c.getQualifiedName()


            Demo2: 查询所有字段Field,满足条件是字段类型是public,并且字段类型继承java.lang.Throwable。(Fastjson1.2.80漏洞利用链的查找方式)。


            其中getASupertype代表获取类对应的父类,*代表递归查找所有父类。


            getDeclaringType代表获取字段对应的定义类型。


            getAModifier代表获取字段对应的修饰符。

              import java
              from Class c, Field fwhere c.getASupertype*().hasQualifiedName("java.lang", "Throwable") andf.getDeclaringType() = c and f.getAModifier().getName() = "public"select c.getQualifiedName(),f.getName()


              2)  与Access相关的概念


              access代表对变量或者方法的调用,主要有VarAccess和MethodAccess。


              Demo1:查询所有继承自java.util.list的变量及变量的引用。

                import java
                from RefType t,Variable v,VarAccess vawhere t.getSourceDeclaration().getASourceSupertype*().hasQualifiedName("java.util", "List") and    v.getType() = t and    va.getVariable() = vselect v,va


                Demo2:查询所有InputStream类对应的readObject方法调用(遍历反序列化漏洞的基础)。

                  import java
                  from MethodAccess ma,Class cwhere ma.getMethod().hasName("readObject") and      ma.getQualifier().getType() = c and      c.getASupertype*().hasQualifiedName("java.io", "InputStream")select ma,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多数情况下是和Acess相互使用的,其实在上面Acess的例子中几乎都用到了Type相关的类。


                  4)与Flow相关的概念


                  Flow是CodeQL中最重要的概念,代表数据流,与此对应的概念包括source和sink。


                  source代表可控的用户输入点,通常是指WEB站点中的URL中参数,例如

                  request.getParameter("name")。其他例如命令行参数args也属于source。在CodeQL中已经存在RemoteFlowSource类,在类中已经定义了很多常见的source点,可以满足我们做一般性代码审计的需要。但是如果我们是要做特定jar包漏洞挖掘,例如复现log4j2的远程命令执行漏洞,由于log4j2包中不包含常规的source点,就需要用户自定义source。


                  sink代表危险的函数,通常是指一些危险的操作,包括命令执行、代码执行、jndi注入、SQL注入、XML注入等。CodeQL虽然也预置了部分的sink点,但是远不能满足实际的需求,需要我们在不同的漏洞环境中自定义sink点。


                  在有了source和sink之后我们可以基于CodeQL提供的查询机制,自动判断是否存在flow可以连接source和sink,一个典型的用法如下,如图3.1所示。

                  图片

                  图3.1 典型的flow利用方式


                  在图3.1所示的Flow中,自定义类继承自TaintTracking::Configuration,并且覆盖其中的isSource个isSink方法。这个是固定写法,后续的绝大部分的ql脚本都包含这样的代码。


                  其中isAdditionalTaintStep方法是CodeQL的类TaintTracking::Configuration提供的的方法,它的原型是:override predicate isAdditionalTaintStep(DataFlow::Node node1, DataFlow::Node node2) {}。它的作用是将一个可控节点A强制传递给另外一个节点B,那么节点B也就成了可控节点,如图3.2所示。

                  图片

                  图3.2 isAdditionalTaintStep方法的连接作用


                  如果CodeQL不能自动连接node1和node2节点,就需要手动通过isAdditionalTaintStep来指定连接。


                  除此之外,在Flow中还有一个方法经常用到isSanitizer,用法如图3.3所示。这是官方提供的关于log4j2漏洞的查询脚本,其中定义了isSanitizer方法来限制flow流中的数据不能是基本数据类型PrimitiveType和BoxedType类型。这是一个特别常用的过滤机制,代表只要是常规的字符类型(Bool、int这些)则不再进行传递。

                  图片

                  图3.3 isSanitizer方法的过滤作用



                  0x04 案例实践


                  作为新手来说,要自己编写有效的CodeQL查询脚本是一件很难的事情,幸运的是CodeQL官方为我们提供了大量的demo。


                  参考地址:8ebK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3V1k6U0L8$3c8W2M7h3I4Q4x3V1k6@1M7X3g2W2i4K6u0r3L8h3q4A6L8W2)9J5c8X3A6S2N6X3q4Q4x3V1k6I4L8q4)9J5c8Y4y4J5j5#2)9J5c8X3g2^5M7r3g2J5K9h3#2W2L8Y4c8S2L8q4)9J5c8W2y4W2j5%4g2J5K9i4c8&6i4K6u0r3b7#2N6q4


                  我们可以直接使用这些demo来完成部分漏洞发现功能。


                  为了更加清晰的理解关于CodeQL的使用,通过具体案例来演示CodeQL的作用。若依RuoYi是国内使用量较大的后台管理系统,从网上下载到某版本的RuoYi的源码。


                  1)基于RuoYi的源码生成数据库


                    arch -x86_64 codeql database create /Users/pang0lin/CodeQL/databases/RuoYi --language=java --command='mvn clean install -DskipTests' --source-root=/Users/pang0lin/cms/若依RuoYi --overwrite


                    成功生成数据库之后,会返回类似的success界面,如图4.1所示。

                    图片

                    图4.1 创建基于RuoYI的数据库


                    2)使用官方demo查询漏洞


                    官网提供了很多查询的ql脚本,其中能直接找到若依相关漏洞的有两个脚本,其中第一个脚本是spel表达式注入的查询脚本。


                    参考地址:881K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3V1k6U0L8$3c8W2M7h3I4Q4x3V1k6T1L8r3!0T1i4K6u0r3L8h3q4A6L8W2)9J5c8X3A6S2N6X3q4Q4x3V1k6I4L8q4)9J5c8Y4y4J5j5#2)9J5c8X3g2^5M7r3g2J5K9h3#2W2L8Y4c8S2L8q4)9J5c8W2y4W2j5%4g2J5K9i4c8&6i4K6u0r3b7#2N6q4i4K6u0r3b7#2N6q4i4K6u0V1x3o6V1@1i4K6u0r3f1%4m8J5K9h3&6Y4g2X3W2W2N6@1#2S2L8X3W2H3N6h3I4S2N6r3W2G2L8W2)9J5k6i4q4D9i4@1g2r3i4@1u0o6i4K6R3&6


                    查询结果如图4.2所示。

                    图片

                    图4.2 基于SpringViewManipulation的查询结果


                    查看sink点详情可知这个漏洞是用户输入的fragment直接传入了模版引擎中,如图4.3所示。

                    图片

                    图4.3 跟踪sink点之后的结果


                    这个漏洞其实是属于若依的一个已知的安全问题,详情见:0b2K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6T1L8r3!0Y4i4K6u0W2j5%4y4V1L8W2)9J5k6h3&6W2N6q4)9J5c8Y4q4I4i4K6g2X3x3K6x3$3x3o6R3H3x3o6m8Q4x3V1k6S2M7Y4c8A6j5$3I4W2i4K6u0r3k6r3g2@1j5h3W2D9M7#2)9J5c8U0p5J5y4o6x3%4y4e0t1I4z5g2)9J5x3#2c8Z5P5h3#2W2L8r3g2S2k6W2)9#2k6U0p5^5y4l9`.`.


                    虽然在最新版的若依中已经因为升级了thymeleaf版本导致无法利用,但是站在CodeQL的角度还是可以发现这种问题。


                    另一个可用的CodeQL的查询脚本是基于mybatis的SQL注入查询脚本,详情见:4aeK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3V1k6U0L8$3c8W2M7h3I4Q4x3V1k6T1L8r3!0T1i4K6u0r3L8h3q4A6L8W2)9J5c8X3A6S2N6X3q4Q4x3V1k6I4L8q4)9J5c8Y4y4J5j5#2)9J5c8X3g2^5M7r3g2J5K9h3#2W2L8Y4c8S2L8q4)9J5c8W2y4W2j5%4g2J5K9i4c8&6i4K6u0r3b7#2N6q4i4K6u0r3b7#2N6q4i4K6u0V1x3o6R3&6i4K6u0r3e0i4W2n7j5i4c8A6M7@1#2S2M7s2m8W2M7W2S2E0L8q4y4I4L8p5W2F1K9X3g2U0N6r3W2G2L8W2)9J5k6i4q4D9


                    查询结果如图4.4所示。

                    图片

                    图4.4 基于MyBatisMapperXmlSqlInjection的查询结果


                    可以看到CodeQL找到了若依可能存在的SQL注入漏洞,跟进sink点看一下,如图所示。每一个都是类似的问题,我们随便打开看一个就可以了。

                    图片

                    这个可以看到这里的参数传递到SQL语句中,造成了SQL注入漏洞。这个漏洞在网上也有大佬已经提到了漏洞细节信息,详情见:

                    8bcK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6B7N6h3g2B7K9h3&6Q4x3X3g2U0L8W2)9J5c8Y4m8G2M7%4c8Q4x3V1j5%4x3o6l9I4x3o6R3%4x3K6l9^5y4e0p5H3x3U0j5#2x3K6f1J5


                    从上面的两次查询中我们可以看到CodeQL在代码审计过程中带来的便利,可以方便的帮助我们定位可能存在的漏洞点。


                    0x05 结论


                    CodeQL给我们提供的查询ql脚本有很多,如果是通过手工一个一个试的话并不是一个好的解决办法,并且官方的ql脚本并不完善,还有很大的完善空间。


                    如何利用大量的ql脚本完成自动化的代码扫描,我们会在下一篇文章中进行讲解。


                    原文链接


                    声明:该文观点仅代表作者本人,转载请注明来自看雪