0%

CodeQL坐牢记录

CodeQL坐牢记录

CodeQL,也就是Code query language,高级源码自动化审计语言,大概就是能把代码解析成数据库创建变量之间的联系,进行污点追踪和自动化漏洞挖掘之类的操作。

为什么学是因为在不知道学什么的时候rmb神仙和我说学一下CodeQL吧。然后就踩了一万个坑(然后又突然不是特别闲了导致这个玩意一直没做完)

虽然说codeql也已经出了几年了,相较于一两年前稀碎的文档和少有的资料相比还是好了很多,但实际上遇到问题也基本搜不到什么想要的内容。所以还是在一路狂踩坑。。。

血泪教训:这个玩意一定只适用于已知源码的情况

完成之后的感想,想搞gadget寻找什么的别来这边,这个玩意的功能应该是在有一定思路之后污点追踪之类的,我就当坐了几天牢学习了一下新把戏,以及感觉更适合java以外的语言,总之就是非常坐牢

环境配置

理论上来说是非常简单的。去GitHub上下一个codeql cli的zip,然后解压一下添加PATH就行(想每次敲路径的也可以不加),简单的说就是有手就行.jpg

然后再下一套依赖库,这个随便塞哪都行,似乎接下来写的ql语句需要和这个东西在一个目录下?

也可以直接下那个starter仓库,git clone的时候要递归下载就可以顺便把依赖下下来,也就可以就着在他给的例子文件夹里面写了

依赖里面塞了对应的各个语言的解析工具,不在里面的就不被支持
比如世界上最好的垃圾语言PHP没有CodeQL支持。可能是因为太垃圾了吧

然后接下来有两个选择,一个是装一个VS Code然后使用官方插件,一个是直接就着刚才下的zip直接命令行形式启动。就命令行凑合着用吧(其实专门下了个VSCode然后装插件,发现好像也没什么多的好处)

解释型语言数据库创建

为什么要标明解释型语言呢,因为解释型语言有一个解释器就能跑,而编译型的编译要加一堆buff指定编译流程,需要在创建数据库的时候指定一下command选项进行编译
而我的C++和java全部交付与Visual Studio和IDEA,maven估计用的是IDEA自带的,我都不知道我机器上的C++编译器在哪,反正就VS一键编译运行了。。。所以暂且先试一下解释型语言。

这个理论上来说只要把CodeQL装下来就能跑起来(只要你装了解释器)。但由于我的智力条件有限,在这里也被坑了半个下午。。。

网上的教程多少有些抽象。也没有说明什么目录结构之类的,这个starter仓库,里面其实就是塞了两个例子,也没有实际的database。说到底还是要自己建一下

命令为codeql create database <path> --language=<lang> --source-root <path>
database的path填生成的db的位置,language处填需要分析的语言,source-root里塞想要被分析的文件
然后就连解释型型语言的数据库创建我都给踩了坑

autobuild.cmd超级报错

似乎从来没有人在这里踩过坑,因此这个坑给我整麻了。

A fatal error occurred: Exit status 9009 from command: [D:\codeql\tools\win64\runner.exe, cmd.exe, /C, type, NUL, &&, D:\codeql\python\tools\autobuild.cmd]

光看这里完全看不懂发生了什么,那个runner.exe肯定是不能看发生了什么的,所以看一下这个autobuild.cmd。

@echo off

rem Legacy environment variables for the autobuild infrastructure.
set LGTM_SRC=%CD%
set LGTM_WORKSPACE=%CODEQL_EXTRACTOR_PYTHON_SCRATCH_DIR%

type NUL && python "%CODEQL_EXTRACTOR_PYTHON_ROOT%\tools\index.py"
exit /b %ERRORLEVEL%

简单的来说看不懂,但是我看到最后一句调用了python,但是由于我装了python2和3,为了区别直接叫python2和python3,也就意味着这里没有python这个可执行文件,因此我直接把这里的python改成python3,然后出现了另一个究极报错

[2022-03-15 17:12:02] [build-stdout] Calling py -3 D:\codeql\python\tools\python_tracer.py -v -z all -c D:\projects\VSCodeProjects\CodeQLTest\db\working\trap_cache -p D:\projects\VSCodeProjects\CodeQLTest\db\working\venv\Lib -R D:\projects\VSCodeProjects\CodeQLTest
[2022-03-15 17:12:02] [build-stderr] Installed Pythons found by py Launcher for Windows
[2022-03-15 17:12:02] [build-stdout] Python 3 not found!
[2022-03-15 17:12:02] [build-stderr] No Installed Pythons Found!
[2022-03-15 17:12:02] [build-stderr] Requested Python version (3) not installed, use -0 for available pythons

简单的说就是找不到python。然后观察一下他调用了一个叫Py的东西,很怪,没听说过,然后百度一下,在Stack Overflow上找到了一个回答。说这个玩意在C:/system下面,是通过注册表找python的,去注册表的这个位置找python路径
\HKEY_CURRENT_USER\SOFTWARE\Python\PythonCore\3.8\InstallPath
然后我注册表里写的是python.exe,把注册表值改成python3.exe或者把python3.exe复制一份叫python.exe都行

本来说解释型语言不需要配更多环境进行编译,反而这个问题是因为解释型语言不需要指定编译命令导致找不到解释器出现的

接下来就能正常的创建db了

创建db的时候似乎是会把所有的依赖都过一遍,导致创建db的速度很慢,每次查询的时候也不知道干什么要进行一对操作,整个的效率并不是很高(并且vscode+插件会有一个java进程长期占用cpu,导致我的小风扇呼呼的响)

hello world

终于搞起来了。直接把starter仓库里queries python文件夹下面的example.ql改成select "hello world",然后codeql query run example.ql -d ..\databases\python\db\指定数据库进行查询(这里数据库内容是什么都无所谓了,反正我就只hello world)。也可以装了插件的vscode直接右键run query。然后开始学语法吧

这里有一个奇怪的地方,按照原理上来说,进行ql查询的时候,需要将ql文件写在对应的依赖库目录下。比如python的话就得写在\ql\python\ql\examples目录下,而如果就着他这个starter项目的话,直接写在他的codeql-custom-queries-python下也行。但是另开文件夹写的话就会出现import无效之类的问题。暂且不想理清楚什么情况,就着他的来吧

看rmb神仙的博客里有提到可以在config里设置ql库的位置,不过我windows好像没有这个东西,看codeql插件也没有这个选项,就拉倒了

在 $HOME/.config/codeql/config 里面设置 ql repo 的路径, 这样才能被 import, 感觉现在这种手动配置好蛋痛 =.= 感觉跟自己编译 cpp 一样链接一堆库
而且文档 u1s1, 挺乱的, 不过刚被 github 收购, 可以理解, 希望之后能更方便一点.
–search-path <path to ql repo>
注意不要写成 –search-path=<path to ql repo>, 不然识别不了… 坑了我好久

然后就是ql文件同目录中应该得有一个qlpack.yml,类似于pom.xml,就是引入依赖用的,不过我反正没怎么成功,还是就着下下来的例子直接用吧。。

name: codeql/python-examples
groups: 
  - python
  - examples
dependencies:
  codeql/python-all: "*"

简易语法入门

这个时候还是启动一下VSCode好了。。。因为插件能展示AST,从AST中边看边学比直接阅读魔幻文档要来的快一点。(理论上来说我写过php parser应该能比较快的理解ast吧。。。因为我的智力条件你也知道.jpg)操作流程应该就是先命令行开一个db,然后在VSCODE里指定那个db文件夹。然后就会多出来一个[db source archive],内容就是把db文件夹下面的src.zip解包。从这里面选个文件然后从QL插件处点生成AST即可。

然后边看ast边查文档吗。。。太艰难了
文档里面把函数叫做Predicates,然后就真的对着文档和AST和例子强行看

官方文档

写了个破烂
语法规则就是先from定义变量,where设置条件,select选出结果,然后他也支持一些常用的关键字和函数,比如这里的instanceof,但究竟支持哪些呢。无从知晓,强行看文档和例子吧

这里有一个很怪的点,他只告诉函数的描述和返回值,并没有说清楚具体表现,有一系列名为getAxxx的函数,这个函数的描述就是返回一个xxx,比如Constructor。那么是返回第一个呢,还是每调用一次就返回后面的一个?没说
在后面写java的时候,当我想判断某个类是不是有一个无参构造函数时,我就写了大概这样子的一个语句getAConstructor().getNumOfParams()=0,这时判断的到底是哪个构造函数呢?实际用起来感觉像个迭代器一样似乎是把每个构造函数都返回一遍并进行后续判断,那这个时候是用且逻辑还是或逻辑呢?也没说。。。使用起来感觉是或,但文档里似乎一个字也没提,太折磨了

当然,实际上这个功能不应该用上述语句去完成,而应该定义一个Constructor变量c,然后c.getDeclaringType().hasQualifiedName(“javax”)之类的。。。

这个语言的编写思路不是先找到一个大的范围然后再向下搜索,而是应该把小的数据先搜索到再对比他是否属于那个大的目标。比如这里应该是先找到无参的public构造函数,再获取到这个函数所属的类是否属于javax。而不是对javax的类进行遍历,再搜索每个类下是否存在无参public构造函数
这个教程文档很好的体现了这一点
analyzing-data-flow-in-java

import python
 
from Function f
where f.getStmt(0) instanceof Return
select f.getName()

功能就是找到所有第一句就是return的函数

以及不同语言下的数据结构并不完全互通,一开始的目的就是用codeql搞java,据说究极log4j就是用这个挖出来的。接下来试着搞一下java。

Java数据库建立和失败的尝试

首先要配好编译环境。编译Java挺麻烦的,这里选择用mvn搞定。直接用IDEA捆绑的maven即可,找一下路径加入PATH里面即可
\IDEA 2021.3.2\plugins\maven\lib\maven3\bin\

然后开始引入依赖快乐编译,发现编译出来的东西根本不能用。。。
可以通过查看db下的src.zip来确认当前数据库包含了多少代码,点开一看发现只携带了我自己写的几个Java文件,一万个引入的依赖都没有被打包进去(python的话import了的东西就自动引入了),然后简单搜索找到了这篇文章
使用 CodeQL 分析闭源 Java 程序

CodeQL无法分析已经预先编译好jar包
CodeQL无法分析在运行时才被编译的jsp代码

虽然我知道需要编译,但是jar包也需要重新编译多少有点折磨了。。呜呜,有源码的开源项目还能去GitHub上下源码,没源码的项目可能就得究极反编译jar了。。。然后就顺着这个文章反编译一下jar吧

把idea下的decompiler抽出来
\IDEA 2021.3.2\plugins\java-decompiler\lib
然后用java -cp java-decompiler.jar org.jetbrains.java.decompiler.main.decompiler.ConsoleDecompiler跑起来,然后超级报错

Exception in thread "main" java.lang.UnsupportedClassVersionError: org/jetbrains/java/decompiler/main/decompiler/ConsoleDecompiler has been compiled by a more recent version of the Java Runtime (class file version 55.0), this version of the Java Runtime only recognizes class file versions up to 52.0

55.0是java11,52.0是java8,等于说我超级新8u311都不给用。
幸好我还装了个openjdk的java17,这下总不会有问题了吧

反编译出来之后装模作样整一个maven项目格式的文件夹(就是套上/src/main/java/)把反编译的代码丢进去,然后搓一个pom.xml,进行编译建立数据库
codeql database create db --language=java --source-root source --command="mvn compile"
终于建起来了。。。我是垃圾

想做的操作是复现一下上次java2的找一个javax下的有Object作为成员的类能被fastjson反序列化的类。但是似乎并没有在网上找到javax的源码。。。然后问了下甫舟和我说在lib下的rt.jar中带了全部的javax。再次拉下来进行反编译,写一个pom再编译回去

然后又出现了一些包不存在、反编译时类出错反编译不回来,出现了奇怪的依赖等问题,源码编译不通过就没法用了,计划大失败

如rmb神仙所言,这个是用来审计源代码的,而不是字节码,在没有源码的情况下,不如使用一个叫做gadgetinspator的工具(没听说过,我为什么什么都不会),不要强行折磨自己进行坐牢。。。并且这个的主要作用也是污点追踪之类的操作,而不是我这种找某个符合条件的类
(说起来好像这种东西直接反射找会跟来得快。。。)
我直接知难而退,使用GitHub上的例子来进行学习

analyzing-data-flow-in-java

analyzing-data-flow-in-java
不得不说,这个教程带题目和答案还挺良心的,比那个抽象的要死的官方文档好一万倍

练习1

要求是找出以硬编码形式构造的java.net.URL类
使用局部数据追踪,就是追踪一个函数内的意思吧?
先上答案

import java
import semmle.code.java.dataflow.DataFlow
import semmle.code.java.dataflow.TaintTracking

from Constructor netURL, Call call, StringLiteral str
where netURL.getDeclaringType().hasQualifiedName("java.net", "URL") and
    call.getCallee() = netURL and
    DataFlow::localFlow(DataFlow::exprNode(str), DataFlow::exprNode(call.getArgument(0)))
select str

hasQualifiedName这个函数好像只能找完全限定名,所以说之前想找javax下的类可能得用indexof之类的操作了。
然后函数调用和函数定义是不一样的,因此需要分别定义Constructor作为定义,Call作为调用进行判断。
然后一个demo用于测试

import java.net.URL;

class Demo{
    public static void main(String[] args) throws Exception {
        String a = "http";
        String b = "www.z3ratu1.cn";
        String c = "blog.z3ratu1.cn";
        String d = a+b;
        String e = b;
        String f = b+":8080";
        URL url = new URL(c);
        URL url2 = new URL(d);
        URL url3 = new URL(e);
        URL url4 = new URL(f);
    }
}

(new URL的时候可能抛出一个exception,vscode一开始没提示我编译还没过。。。)
数据跟踪有两种形式,一种是Dataflow,一种是TaintTracking
前者就是数据流,后者是污点追踪。DataFlow::localFlow方法可以将指定的两个节点作为数据流关联起来,其是DataFlow::localFlowStep的递归版本。TaintTracking继承自Dataflow,其localTaint方法能进行污点追踪。就是DataFlow只认为var b=a这种直接等于的赋值纳入范围,而TaintTracking则会将var c =a+b也同样纳入追踪,同样也有localTaintStep,仅追踪单步

简单测试一下四个方法的查询结果(这个玩意跑的真的慢,就这么个文件查一次也要十几秒,建立数据库感觉也得个半分钟)

函数 对应字符串所在的变量
localFlow b c
localFlowStep c
localTaint a b c “:8080”
localTaintStep c

从这个结果上来说就大致能理解这两个数据追踪方法的原理了,所以基本上都是用污点追踪嗷

练习2

Write a query that finds all hard-coded strings used to create a java.net.URL, using global data flow

使用全局数据追踪模块追踪java.net.URL中使用的硬编码字符串,重写他的类来实现,我看大部分的人也都是用的这个进行污点追踪
先看答案再反向分析

import semmle.code.java.dataflow.DataFlow

class Configuration extends DataFlow::Configuration {
  Configuration() {
    this = "LiteralToURL Configuration"
  }

  override predicate isSource(DataFlow::Node source) {
    source.asExpr() instanceof StringLiteral
  }

  override predicate isSink(DataFlow::Node sink) {
    exists(Call call |
      sink.asExpr() = call.getArgument(0) and
      call.getCallee().(Constructor).getDeclaringType().hasQualifiedName("java.net", "URL")
    )
  }
}

from DataFlow::Node src, DataFlow::Node sink, Configuration config
where config.hasFlow(src, sink)
select src, "This string constructs a URL $@.", sink, "here"

这里用了个新操作,exists,简单学习一下
expressions
formulas
其实不看文档也能猜出来大概是怎么回事,竖线前面定义一个变量,竖线后面就是该变量需要满足的条件
这里就是要求目标node是java.net.URL的构造函数的第一个参数

写一个Demo

import java.net.URL;

class Demo{
    private static String getURL(){
        return "blog.z3ratu1.cn";
    }

    private static String echo(String s){
        return s;
    }

    private static String echoWithPadding(String s){
        return s+":8080";
    }

    public static void main(String[] args) throws Exception {
        String a = "www.z3ratu1.cn";
        String b = "test.z3ratu1.cn";
        String c = echo(b);
        String d = echoWithPadding(b);
        String e = getURL();
        URL url = new URL(a);
        URL url2 = new URL(c);
        URL url3 = new URL(d);
        URL url4 = new URL(e);
    }
}

具体的跟踪过程应该是封装在了Dataflow类里了,也就是把跨函数的直接相等类的操作全部获取到了,三个z3ratu1.cn全部被追踪到。换成污点追踪的类再try一下,直接把继承类换成TaintTracking即可,也把:8080给追踪上了

练习3

Write a class that represents flow sources from java.lang.System.getenv(..).

有点抽象的问题,就是把跟着对象从java.net.URL换成了java.lang.System.getenv吧?不过也没说是字符串了。考虑着把构造函数的判断改成getQualifiedName直接判定函数权限定名。看了答案之后再次不知所云。。。

import java

class GetenvSource extends MethodAccess {
  GetenvSource() {
    exists(Method m | m = this.getMethod() |
      m.hasName("getenv") and
      m.getDeclaringType() instanceof TypeSystem
    )
  }
}

exist中除了第一个竖线外剩下的竖线都表示and,但是这个MethodAccess是个什么类。。。然后TypeSystem又是什么。。。看一眼文档。居然直接把这个类封装成了一个单独的类。。。
Class TypeSystem

太抽象了,MethodAccess的描述是

A method access is an invocation of a method with a list of arguments.

这不是和Call差不多,然后这个类继承自Call类,那么Call的描述又是什么呢

Any call to a callable.
This includes method calls, constructor and super constructor invocations, and constructors invoked through class instantiation.

意思是构造函数不算MethodAccess咯?
虽然很抽象,但是这个类应该能捕捉到getenv这个函数调用,以及我的读题有一点问题

练习4

Using the answers from 2 and 3, write a query which finds all global data flows from getenv to java.net.URL.

缝合一下,就把2的instanceof那里改一下就行,然后3中的类需要改成继承Dataflow

写一个样例

import java.net.URL;
import java.lang.System;

class Demo{
    private static String getEnv(){
        return System.getenv("aaa");
    }

    private static String getEnvFullName(){
        return java.lang.System.getenv("bbb");
    }

    private static String getEnvWithPadding(){
        return System.getenv("ccc")+"ccc";
    }


    public static void main(String[] args) throws Exception {
        URL url = new URL(System.getenv("ddd"));
        URL url2 = new URL(getEnv());
        URL url3 = new URL(getEnvFullName());
        URL url4 = new URL(getEnvWithPadding());
    }
}

能够追踪到padding外的所有getenv。然后加了padding的必然是用污点追踪实现
把继承的类改成污点追踪后找到所有getenv

CodeQL and Chill

GitHub上codeql ctf第四题,java题
GitHub Security Lab CTF 4: CodeQL and Chill - The Java Edition
大意就是用户输入会触发模板注入可以打EL表达式,需要找到这条利用链。直接从GitHub上下载压缩包导入数据库即可(我直接从压缩包导入的数据库用不了,select “hello”都会报一个没头没脑的什么tuple错误,解压之后再导入似乎就能用了,然后那个导入导入了我四五分钟。。。)

然后随便进行个查询也要半分钟。。。刚好边查边写了

Step 1: Data flow and taint tracking analysis

Step 1.1: Sources

污点追踪对source进行定义。继承一个TaintTracking::Configuration类,重写isSource方法即可,写了个破烂,然后跑起来一条都查不出来。。。

    override predicate isSource(DataFlow::Node source) {
        exists(MethodAccess ma, Method m, Interface i | ma.getMethod().overridesOrInstantiates*(m) and 
        i.hasQualifiedName("javax.validation", "ConstraintValidator") and
        m.hasName("isValid") and m.getDeclaringType() = i and 
        source.asExpr() = ma
        )
    }

实际上好像是不能这么查?看了一眼答案,答案是查的函数形参,为什么呢。魔改一下变成这样。能得出和答案一致的结果,但是答案的话是通过定义了一系列类来实现的

    override predicate isSource(DataFlow::Node source) {
        exists(Method m, Method isValid | m.overridesOrInstantiates*(isValid) and 
        isValid.hasQualifiedName("javax.validation", "ConstraintValidator", "isValid") and 
        source.asParameter() = m.getParameter(0) and
        m.fromSource()
        )
    }

学习一下java继承的判断,也都是通过写新的类来实现的。。。
types-in-java

解构到类里面去,一层层的写也能一层层的debug,是一个良好的开发思路。

Hints:

  • Make sure you catch only the implementations of methods defined in the ConstraintValidator interface.
  • There is a convenient class RemoteFlowSource that tells you when a particular data flow node is obtained from remote user input.
  • Pay attention to get only results that pertain to the project source code.

第一点通过递归调用overridesOrInstantiates确认是否继承或重写对应函数
第二点不知道怎么用
第三点他的表述非常的奇怪,除了项目源码外这个数据库还需要分析哪呢?附带了jdk的代码?解决方案是加一个m.fromSource(),但实际上加了查出来的结果也没变(当做一个习惯记着吗?)

Step 1.2: Sink

sinks是ConstraintValidatorContext.buildConstraintViolationWithTemplate的第一个实参。这里有点怪?source是函数的形参,而sink是函数的实参。理论上来说都是实参的话我会好理解一点,为什么一个形参一个实参呢?

依葫芦画瓢的写一个即可,也能得到描述上所说的五个结果

    override predicate isSink(DataFlow::Node sink) {
        exists(MethodAccess ma, Method buildConstraintViolationWithTemplate| ma.getMethod().overridesOrInstantiates*(buildConstraintViolationWithTemplate) and 
        buildConstraintViolationWithTemplate.hasQualifiedName("javax.validation", "ConstraintValidatorContext", "buildConstraintViolationWithTemplate") and 
        sink.asExpr() = ma.getArgument(0)
       )
    }

Step 1.3: TaintTracking configuration

写一个TaintTracking::Configuration类把isSource和isSink缝合进去,跑一下
然后跑不出结果来。。。。然后继续看文档,然后发现他在耍我

Run your query using the command CodeQL: Run Query (either in the Command Palette or the right-click menu). It should give you … 0 results! Ok, this is disappointing! But don’t give up just now.

Step 1.4: Partial Flow to the rescue

使用了全新的技术DataFlow::PartialPathGraph,需要重新import一下,并且和DataFlow::PathGraph还是互相冲突的,只能import一个。需要临时互相替换

由于步骤1.3中未能直接从source走到sink,故引入Partial Flow从source展示从source往下走的情况,用于调试。可以在config类中重写explorationLimit方法来限制步长,样例给的步长是10(感觉太大了)步长缩到5我的笔记本也有点吃不消。。。cpu没耗多少,倒是内存吃了四五个G。。。缩到3总算能在短时间内跑下来,4就已经四五分钟跑不出结果了。。。

根据1.3中的提示,目标位于SchedulingConstraintSetValidator.java,所以限制一下node位置,抄的答案(说起来这应该是已知漏洞反向复现时写查询语句的方式,而不是反向搜索漏洞,或者说我知道了这里有一个可能的漏洞然后现在编写查询语句找他是否会和用户输入关联起来?)

from MyTaintTrackingConfig cfg, DataFlow::PartialPathNode source, DataFlow::PartialPathNode sink
where
  cfg.hasPartialFlow(source, sink, _) and
  source.getNode().getLocation().getFile().getBaseName() = "SchedulingConstraintValidator.java"
select sink, source, sink, "Partial flow from unsanitized user data"

简单的看了下步长3的情况,发现有的地方都已经进了jdk的自带类里面去了。但是未能debug出问题在哪,也许可以以步长为3的终点重新为起点继续前进,就等于从这个点继续步长为3的搜索了。但是我的笔记本性能估计只能走三步,基本上没法调试了。。。他的默认步长是10估计就很好调试了吧

hint中有提到要读一下hasPartialFlow的文档,没有看到什么有意义的东西。是我没找到文档的位置吗

Step 1.5: Identifying a missing taint step

直接告诉我codeQL没有将getter方法纳入到污点追踪的传递过程中,因此需要将getter方法连接起来

Step 1.6: Adding additional taint steps

继承一个TaintTracking::AdditionalTaintStep类并实现step方法来把断掉的连接续上
看了下这篇文章善用isAdditionalTaintStep和isSanitizer
我之前一直没有特别理解Call的getQualifier方法,虽然大概猜出来了可能是指调用该函数的对象。就比如obj.func(arg)这样子一个操作,call对象拿到的应该是这个调用的操作整体,然后getQualifier获取到obj,getMethod获取到func的定义,getCallee获取到当前函数调用。大致如此,英语水平有限对着他那个qualifier的描述看了半天看不懂

写到这的时候发现这个破烂往C盘写缓存写了5个G差点把我C盘写爆,应该是1.4的污点追踪究极递归搞的。然后关了重启缓存清空之后一次查询又要两分钟组织数据库。。。越发觉得不好用了呢?

写下一个Step类

class KeySetTaintStep extends TaintTracking::AdditionalTaintStep{
  override predicate step(DataFlow::Node f, DataFlow::Node t){
    exists(MethodAccess ma | ma.getMethod().getName() = "keySet" and 
    ma.getMethod().getDeclaringType().getSourceDeclaration().hasQualifiedName("java.util", "Map") and
    f.asExpr() = ma.getQualifier() and 
    t.asExpr() = ma
    )
  }
}

如上所说,这个step将污点从obj转移到obj.keySet()这个调用的结果,过程一开始理解了半天。。。
但实际上我在一开始就感觉他能够将污点传递到keySet调用那去。。。?
语句如下

value.keySet().stream().map(String::toLowerCase).collect(Collectors.toSet())

从三步跟进的情况来看,应该能跟进到value.keySet().stream()这里啊?还是说我没看懂他这个跟进的输出是什么

这里还踩了一个坑
一开始ma的判断条件为

ma.getMethod().hasQualifiedName("java.util", "Map", "keySet")

感觉就一步到位了,然而这样子查不出来任何一个结果。。。并且我一开始还没发现,后来测试的时候才发现的,因为对TaintTracking::AdditionalTaintStep的quick evaluation是对全部step都搜索出来。直接看是看不懂的,后来对着答案的类单独对类计算,结果发现我自己写的语句当类计算时查不出结果来。。。以及文档的这个getSourceDeclaration方法完全看不懂在说什么

但是开头的config的source和sink也是这么写的却能查出答案来。。。

又一个教训,判断条件写成类单独调试不会有坏处

Step 1.7: Adding taint steps through a constructor

不知道这么写对不对

// x -> HashSet(x)
class HashSetTaintStep extends TaintTracking::AdditionalTaintStep {
  override predicate step(DataFlow::Node f, DataFlow::Node t) {
    exists(ConstructorCall call | call.getConstructor().hasQualifiedName("java.util", "HashSet", "HashSet") and
      f.asExpr() = call.getAnArgument() and
      t.asExpr() = call
    )
  }
}

trick
在寻找函数的时候感觉都要考虑一下继承,那么就使用如下的通用模板较为合适

ma.getMethod().getName() = "stream" and
ma.getMethod().getDeclaringType().getASourceSupertype+().hasQualifiedName("java.util", "Collection")

但这个操作在上述hashmap时失效了,暂时不知道原因是什么。。。

Step 1.8: Finish line for our first issue

写了一大堆连接

// x -> x.keySet()
class KeySetTaintStep extends TaintTracking::AdditionalTaintStep{
  override predicate step(DataFlow::Node f, DataFlow::Node t){
    exists(MethodAccess ma | ma.getMethod().getName() = "keySet" and 
    ma.getMethod().getDeclaringType().getSourceDeclaration().hasQualifiedName("java.util", "Map") and
    f.asExpr() = ma.getQualifier() and 
    t.asExpr() = ma
    )
  }
}



// x -> x.getXXX()
class GetterTaintStep extends TaintTracking::AdditionalTaintStep{
  override predicate step(DataFlow::Node f, DataFlow::Node t){
    exists(MethodAccess ma | ma.getMethod().getName().indexOf("get") = 0 and
     f.asExpr() = ma.getQualifier() and
     t.asExpr() = ma
    )
  }
}


// x -> HashSet(x)
class HashSetTaintStep extends TaintTracking::AdditionalTaintStep {
  override predicate step(DataFlow::Node f, DataFlow::Node t) {
    exists(ConstructorCall call | call.getConstructedType().getSourceDeclaration().hasQualifiedName("java.util", "HashSet") and
      f.asExpr() = call.getAnArgument() and
      t.asExpr() = call
    )
  }
}

// x -> y.RetainAll(x)
class CollectionRetainAllTaintStep extends TaintTracking::AdditionalTaintStep {
  override predicate step(DataFlow::Node f, DataFlow::Node t) {
    exists(MethodAccess ma | ma.getMethod().getDeclaringType().getASourceSupertype+().hasQualifiedName("java.util", "Collection") and
      f.asExpr() = ma.getAnArgument() and
      t.asExpr() = ma.getQualifier()
    )
  }
}

实际上就是从完成了这个函数的从source到sink。。。突然感觉有点迷。主要是debug那段不会搞跳过了就没怎么理解

    @Override
    public boolean isValid(Container container, ConstraintValidatorContext context) {
        if (container == null) {
            return true;
        }
        Set<String> common = new HashSet<>(container.getSoftConstraints().keySet());
        common.retainAll(container.getHardConstraints().keySet());

        if (common.isEmpty()) {
            return true;
        }

        context.buildConstraintViolationWithTemplate(
                "Soft and hard constraints not unique. Shared constraints: " + common
        ).addConstraintViolation().disableDefaultConstraintViolation();
        return false;
    }

其实也就是连上了这里面的两句。。。

Step 2: Second Issue

刚才找出来的链是在SchedulingConstraintSetValidator.java,而在SchedulingConstraintValidator.java中也存在类似的调用,补上对应的连接类

Tip: We don’t like duplicate code. ;-)

意思就是写之前的全局类进行连接咯?

    @Override
    public boolean isValid(Map<String, String> value, ConstraintValidatorContext context) {
        Set<String> namesInLowerCase = value.keySet().stream().map(String::toLowerCase).collect(Collectors.toSet());
        HashSet<String> unknown = new HashSet<>(namesInLowerCase);
        unknown.removeAll(JobConstraints.CONSTRAINT_NAMES);
        if (unknown.isEmpty()) {
            return true;
        }
        context.buildConstraintViolationWithTemplate("Unrecognized constraints " + unknown)
                .addConstraintViolation().disableDefaultConstraintViolation();
        return false;
    }

他的函数长这样,那么就是把stream,map,collect函数连一下就行了吧
补充如下代码

// x -> x.stream()
class CollectionStreamTaintStep extends TaintTracking::AdditionalTaintStep {
  override predicate step(DataFlow::Node f, DataFlow::Node t) {
    exists( MethodAccess ma| ma.getMethod().getName() = "stream" and
    ma.getMethod().getDeclaringType().getASourceSupertype+().hasQualifiedName("java.util", "Collection") and
    f.asExpr() = ma.getQualifier() and 
    t.asExpr() = ma)
  }
}


// x -> x.map()
class MapTaintStep extends TaintTracking::AdditionalTaintStep {
  override predicate step(DataFlow::Node f, DataFlow::Node t) {
    exists(MethodAccess ma | ma.getMethod().getName() = "map" and
    ma.getMethod().getDeclaringType().getASourceSupertype+().hasQualifiedName("java.util.stream", "Stream") and
    f.asExpr() = ma.getQualifier() and
    t.asExpr() = ma )
  }
}

// x -> x.collect()
class CollectTaintStep extends TaintTracking::AdditionalTaintStep {
  override predicate step(DataFlow::Node f, DataFlow::Node t) {
    exists(MethodAccess ma |  ma.getMethod().getName() = "collect" and
    ma.getMethod().getDeclaringType().getASourceSupertype+().hasQualifiedName("java.util.stream", "Stream") and
    f.asExpr() = ma.getQualifier() and
    t.asExpr() = ma )
  }
}

看了一眼答案,答案还是用的更为优雅的定义类的写法。。。下次一定改,然后hint中的减少重复代码就是因为map和collect这两个函数都是java.util.stream.Stream这个类的,所以再把这个类抽出来写一遍

f.asExpr() = ma.getQualifier() and t.asExpr() = ma
写最后一个函数的时候忘了这两句经典操作。。。导致查询除了问题可能进入了死循环,我当时还在想怎么这就十分钟都跑不出结果了。。。

Step 3: Errors and Exceptions

思路是因为这个模板渲染渲染的是报错,因此可能会有类似try catch的操作进行污点传递,需要使用额外的代码进行传递

try {
    parse(tainted);
} catch (Exception e) {
    sink(e.getMessage())
}

这段直接学习答案的例子了,思路比较清晰

class ExceptionTaintStep extends TaintTracking::AdditionalTaintStep {
  override predicate step(DataFlow::Node n1, DataFlow::Node n2) {
    exists(Call call, TryStmt try, CatchClause catch, MethodAccess getMessageCall |
      // the call is within the `try` block, which has a corresponding `catch` clause
      call.getEnclosingStmt().getEnclosingStmt*() = try.getBlock() and
      try.getACatchClause() = catch and
      // the `catch` clause is likely to catch an exception thrown by the call
      (
        catch.getACaughtType().getASupertype*() = call.getCallee().getAThrownExceptionType() or
        catch.getACaughtType().getASupertype*() instanceof TypeRuntimeException
      ) and
      // the exception message is read by `getMessageCall` within the `catch` block
      catch.getVariable().getAnAccess() = getMessageCall.getQualifier() and
      getMessageCall.getMethod().getName().regexpMatch("get(Localized)?Message|toString") and
      // taint flows from any argument of the call to a place where the exception message is accessed
      n1.asExpr() = call.getAnArgument() and
      n2.asExpr() = getMessageCall
    )
  }
}

这里就两个地方没有很理解,一个是call.getEnclosingStmt().getEnclosingStmt*(),感觉可以直接+表示一次以上递归。没有很理解,然后就是函数名这里的正则get(Localized)?Message|toString,是java的错误处理函数命名规范吗?

那段对catch接受的错误与call抛出的错误的类型判断倒是一个很精彩的思路

Step 4: Exploit and remediation

Step 4.1: PoC

怎么就快进到write a poc了。。。我还没找到用户输入在哪呢。再回头看一下用户输入追踪环节。是Step1.1的Bonus环节

Step 1.1 Bonus

答案的类和各种函数定义的顺序还有点乱,理不清楚,看起来略微不适,并且语法都变了好多,不太看得懂了。。。

import了一个TaintTracking2并定义了一个新的Config

  class UserInputToValidatedFieldConfig extends TaintTracking2::Configuration {
    UserInputToValidatedFieldConfig() { this = "UserInputToValidatedFieldConfig" }

    override predicate isSource(DataFlow2::Node source) { source instanceof RemoteFlowSource }

    override predicate isSink(DataFlow2::Node sink) {
      sink.asExpr() = any(Field field).getAnAssignedValue()
    }
  }

source是RemoteFlowSource,这个是codeql内置的类,表示远端的资源,然后sink就是一个赋值语句。这段很好理解

接下来的整体都比较玄幻,比较难再整合成一个语句了,尽可能的整合然后eval一下看看都是写什么东西

class ConstraintAnnotation extends Annotation {
  ConstraintAnnotation() { this.getType().hasQualifiedName("javax.validation", "Constraint") }

  /** Holds if this constraint is validated by the class `validatorType`. */
  predicate isValidatedBy(RefType validatorType) {
    validatorType =
      this.getValue("validatedBy").(ArrayInit).getAnInit().(TypeLiteral).getTypeName().getType()
  }
}

这个类寻找对应名称的注解,并提供一个函数寻找该注解中validatedBy属性对应的值(但是我并看不懂后面这堆操作在干什么。。。)

然后接下来定义了一个函数,但是实际上这个函数后面使用的时候第二个和第三个参数没有用?

  /**
   * Holds if `validatedElement` is annotated with a validation constraint defined by `constraintType`,
   * which in turn is annotated with `constraintAnnotation` and validated by `validatorType`.
   */
  predicate validatedConstraint(
    Annotatable validatedElement, RefType constraintType, ConstraintAnnotation constraintAnnotation,
    RefType validatorType
  ) {
    validatedElement.getAnAnnotation().getType() = constraintType and
    constraintType.getAnAnnotation() = constraintAnnotation and
    constraintAnnotation.isValidatedBy(validatorType)
  }

实际调用的时候是这样子的validatedConstraint(validatedField, _, _, validatorType)
下划线表示any,也就是任意值,这个玩意的称呼似乎被称为don’t care var。意思是无用?那我能不能把这个函数之间缩成这样子的

predicate validatedConstraint(Annotatable validatedElement, RefType validatorType) {
  validatedElement.getAnAnnotation().getType().getAnAnnotation().(ConstraintAnnotation).isValidatedBy(validatorType)
}

验证了一下确实可以,但这会导致结果有些跳脱,更加让人看不懂。使用原函数的quick eval就能看出该函数的思路,功能是寻找一个可被注解的对象,其注解的注解继承自javax.validation.Constraint类,寻找其isValidateBy值,该对象即会被isValidateBy指定类的isValid函数进行检验

然后写一个复杂函数

predicate validatesUserControlledBeanProperty(
  Method isValidMethod, Field validatedField, RefType validatorType,
  RemoteFlowSource remoteInput
) {
  // This `isValid` method is used to validate a field, or the field's class.
  isValidMethod.overridesOrInstantiates*(any(Method t | t.hasQualifiedName("javax.validation", "ConstraintValidator", "isValid"))) and
  validatorType = isValidMethod.getDeclaringType() and
  (
    validatedConstraint(validatedField, validatorType) or
    validatedConstraint(validatedField.getDeclaringType(),validatorType)
  ) and
  // The value of the field is obtained from user input.
  any(UserInputToValidatedFieldConfig config)
      .hasFlow(remoteInput, DataFlow::exprNode(validatedField.getAnAssignedValue()))
}

先找到符合要求的isValid函数,然后获取其对应类,然后寻找被该类注解的对象,最后将用户输入和被对应类注解的对象关联起来(要我说这个部分已经超出我的能力范围了。。。)

最后定义一个类,扩展DataFlow的Node类

class BeanValidationSource extends DataFlow::Node {
  BeanValidationSource() {
    exists(Method isValidMethod |
      // This source is the first parameter of the `isValid` method
      isValidMethod.overridesOrInstantiates*(any(Method t | t.hasQualifiedName("javax.validation", "ConstraintValidator", "isValid"))) and
      this.asParameter() = isValidMethod.getParameter(0) and
      // which must be present in the source code
      isValidMethod.fromSource() and
      // and must be used to validate user-controlled data
      validatesUserControlledBeanProperty(isValidMethod, _, _, _)
    )
  }
}

简单魔改了一下,然后这里最后调用validatesUserControlledBeanProperty时还是使用了这个下划线参数,感觉,学到了一个全新的技巧。函数用来定位某个东西的时候可以多声明几个辅助变量,然后最后到调用的时候用下划线参数直接不管就行

这个函数倒是很好看懂了,就是寻找到一个有用户输入的isValid方法,然后这个类表示有用户输入的isValid方法的第一个参数

可以用这个类替代我们一开始的isSource中写的那些判断条件,把原来只是简单的找isValid的第一个参数,变成了有用户输入的isValid的第一个参数。。。。(我还以为是从输入点到触发点,这样子我还是没有特别懂输入点在哪。不过可以quick eval validatesUserControlledBeanProperty这个函数,它的validatedField应该就是用户输入处

我不管我不管搞完了,不坐牢了

个人意见,这个东西写java是真折磨,首先需要完成源代码的编译,这就已经能砍掉一堆项目了,然后还由于java的各种特性各种继承各种注解,得考虑一万个东西,或者说我感觉这个是在已经有一定的漏洞思路之后去寻找的。像这个项目后期的用户输入到sink,注解那段我完全就没能理解,简单看了下代码才知道怎么回事。对于我来说这个过程更像是一个已知漏洞存在然后去验证的过程,而不是一个从零开始挖掘的过程

以及感觉没有这种奇怪的挖掘需求。。。最近的需求都是找java gadget之类的东西,只是寻找一个存在特定方法特定属性的类之类的,而不是一个完整的过程。这把就当做了几天牢简单的尝试了一下新科技吧。。。