0x00 功能
编译
闭源项目创建数据库,可以使用该工具:https://github.com/ice-doom/codeql_compile
历史查询
在VSCode左侧可以的QUERY HISTORY可以点击切换历史查询内容,也可以右键比对查询结果等功能

查看AST
在VSCode左侧选中要查看的java文件之后,点击View AST即可查看,并且鼠标点击到java文件中的类、方法等,AST VIEWER中会自动帮助我们定位到该项

快速查询
在我们编写的一些谓词上方有个快速查询按钮,点击之后可以快速查询当前谓词的结果。

0x01 语法
列出个人经常用到的语法和一些注意事项
获取具体QL类型
不确定使用什么方式获取目标时,除了通过查看AST,还可以通过词getAQlClass()获取调用它实体的具体QL类型。
from Expr e, Callable c
where e.getEnclosingCallable() = c
select e, e.getAQlClass()
尽可能缩小范围
如下定义,如果项目代码量很大,则非常耗时
override predicate isSink(DataFlow::Node sink) {
sink.asExpr().getParent() instanceof ReturnStmt
}
可以设置return语句在哪个函数中调用来缩小范围,乃至其Type的全限定名
override predicate isSink(DataFlow::Node sink) {
sink.asExpr().getParent() instanceof ReturnStmt
and sink.asExpr().getEnclosingCallable().hasName("xxxxx")
}
个人使用的几个规则
// 以某个方法的参数作为source (添加了几种过滤方式,第一个参数、该方法当前类的全限定名为xxxx)
override predicate isSource(DataFlow::Node source) {
exists(Parameter p |
p.getCallable().hasName("readValue") and
source.asParameter() = p and
source.asParameter().getPosition() = 0
and p.getCallable().getDeclaringType().hasQualifiedName("com.service.impl", "xxxxx")
)
}
// 以某个实例的所有参数作为source(`X1 x1 = new X1(a,b)`,这里a、b作为source),过滤:调用该实例的方法名称为`Caller`,实例类型名称为`X1`
override predicate isSource(DataFlow::Node source) {
exists(ClassInstanceExpr ma |
source.asExpr() = ma.getAnArgument()
and ma.getTypeName().toString() = "X1"
and ma.getCaller().hasName("Caller")
)
}
调用端点路径
比如我们想知道方法A到方法G之间调用端点路径,则可以使用edges谓词,编写如下所示,如果也想找覆写的某个方法(如:接口实现类中的方法)可以将calls替换为polyCalls
import java
class StartMethod extends Method {
StartMethod() { getName() = "main" }
}
class TargetMethod extends Method {
TargetMethod() { getName() = "vulMain" }
}
query predicate edges(Method a, Method b) { a.calls(b) }
from TargetMethod end, StartMethod entryPoint
where edges+(entryPoint, end)
select end, entryPoint, end, "Found a path from start to target."
得到的结果如图所示

对某接口实现
主要是通过codeql自带谓词overridesOrInstantiates判断该函数是否进行了重写。
如下,就能获取实现JSONStreamAware接口,重写的方法
class JsonInterface extends Interface{
JsonInterface(){
this.hasQualifiedName("com.alibaba.fastjson", "JSONStreamAware")
}
Method getJsonMethod(){
result.getDeclaringType() = this
}
}
class CMethod extends Method{
CMethod(){
this.overridesOrInstantiates*(any(JsonInterface i).getJsonMethod())
}
}
from CMethod m select m, m.getDeclaringType()


查询Select
如果编写查询不规范可能会经常碰到类似如下错误
<font color=#FF0000>Showing raw results instead of interpreted ones due to an error. Interpreting query results failed: [xxxxx] Exception caught at top level: Could not process query metadata. Error was: Expected result pattern(s) are not present for problem query: Expected exactly one pattern. [INVALID_RESULT_PATTERNS]</font>
这种情况的注意事项如下:
在不使用path查询时,元数据为@kind problem,并且也别导入path相关内容,如:import DataFlow::PathGraph,否则查询时会一直产生失败日志,而且当string中使用了$@占位符时会一直失败使其当作正常字符串展示在结果中。
这种查询由两列组成select element, string
使用path查询时,元数据为@kind path-problem,查询模板为select element, source, sink, string
当element指定为source节点时最先显示的是source

当element指定为sink节点时最先显示的是sink

0x02 AdditionalTaintStep
在为一些项目编写规则查询时,经常碰到数据流中断的情况,下面列出经常碰到中断的情况和解决方案。
setter和getter
场景1:在做GitHub CTF案例时这块有体会,CodeQL为减少误报很多地方都需要我们根据相应场景自己连接数据流,比如getter。
这种情况需要将调用方法的对象(通过getQualifier谓词获取限定符)和调用方法的返回值连接起来。如下操作就是从get%方法访问到它的限定符作为附加步骤重新连接起来。
class GetSetTaintStep extends TaintTracking::AdditionalTaintStep{
override predicate step(DataFlow::Node src, DataFlow::Node sink){
exists(MethodAccess ma |
(ma.getMethod() instanceof GetterMethod or ma.getMethod() instanceof SetterMethod or ma.getMethod().getName().matches("get%") or ma.getMethod().getName().matches("set%"))
and
src.asExpr() = ma.getQualifier()
and sink.asExpr() = ma
)
}
}
mapper
场景2:使用mybatis通常将接口命名为xxxxMapper或者xxxxDao这种形式,在xml配置文件中通过namespace指定其全限定名,当数据流需要经过数据库查询到这里会断开,那么需要手动将其连接起来。
如下我们使用普通查询从接收请求到return语句结束

最后会在此处中断

对应xml配置

那么需要添加AdditionalTaintStep将中断进行拼接。这里将污染源查询的id和某个方法连接(该方法的对象类型名称是xxxxDao),当然有的可能名称是xxxxMapper,根据情况而定
class MapperTaintStep extends TaintTracking::AdditionalTaintStep{
override predicate step(DataFlow::Node src, DataFlow::Node sink){
exists(MethodAccess ma |
(ma.getQualifier().getType().getName().matches("%Dao") or ma.getQualifier().getType().getName().matches("%Mapper"))
// and (src.asExpr() = ma.getAnArgument() or src.asExpr() = ma.getAnArgument().getAChildExpr())
and src.asExpr() = ma.getAnArgument()
and sink.asExpr() = ma
)
}
}
最后查询结果:

这一块在CWE-089的查询文件中,也有类似AdditionalTaintStep一步,可以阅读参考。
污染源作为参数传入
场景3:如下图所示,instance作为污染源,workNode也被污染,将其传入t.setSceneKey为t对象的sceneKey属性赋值,那么这里t对象理应也是被污染的。但当我们将instance作为source,return t作为sink是获取不到路径的,需要加上额外步骤。

代码如下,将调用方法的所有参数作为source(图中setSceneKey方法的workNode.getSceneKey()参数),将调用方法的对象作为sink(图中的t对象)
class SrcTaintStep extends TaintTracking::AdditionalTaintStep{
override predicate step(DataFlow::Node src, DataFlow::Node sink){
exists(MethodAccess ma |
(ma.getMethod() instanceof SetterMethod or ma.getMethod().getName().matches("set%"))
and
src.asExpr() = ma.getAnArgument()
and sink.asExpr() = ma.getQualifier()
)
}
}
可以猜猜上图中总共需要添加几个额外步骤(3个,第一:刚刚讲的;第二:instance的getter;第三:workNodeMapper)
实例化
场景4:如下图,将req传入UploadFile中创建UploadFile对象,再将其传入systemService.uploadFile方法中,这种情况,uploadFile对象应该是受污染的,但是默认情况下,我们像让数据流进入systemService.uploadFile中是不行的,因为在new UploadFile就已经断开了。那么就需要将其连接起来

代码如下,如果已经知道当前查询大概断的位置,可以缩小范围,这里将所有的都会连接起来
class InstanceTaintStep extends TaintTracking::AdditionalTaintStep{
override predicate step(DataFlow::Node src, DataFlow::Node sink){
exists(ClassInstanceExpr cie |
// cie.getTypeName().toString() = "UploadFile"
src.asExpr() = cie.getAnArgument()
and sink.asExpr() = cie)
}
}
0x03 Partial flow
对于数据流中断时候如何去解决确定中断位置在哪,官方提供了Partial flow方式,也就是查询到中断前的部分流,对于某些场景是有帮助的。如果想了解的话可以阅读官方描述Debugging data-flow queries using partial flow¶
使用:
先导入PartialPathGraph,这里需要注意不能和PathGraph共存,也就是使用PartialPathGraph则不能导入import DataFlow::PathGraph。
import DataFlow::PartialPathGraph
在TaintTracking::Configuration配置中添加一个谓词,表示探索深度
override int explorationLimit() { result = 5 }
查询如下,注:hasPartialFlow是和PartialPathGraph匹配,hasFlowPath和PathGraph匹配,导入的时候一定要注意,否则会导致查不出来内容。
from MyTaintTrackingConfiguration conf, DataFlow::PartialPathNode source, DataFlow::PartialPathNode sink
where conf.hasPartialFlow(source, sink, _)
select sink, source, sink, "Partial flow from unsanitized user data"
当整个调用链非常长的时候又不知道具体断掉的位置,然后使用Partial flow会导致查询结果内容非常多,更不好排查了。官方提供了2种解决方式

大概的意思也就是,比如:a-b-c-d-e-f-g,不知道哪个位置中断了,那么就先查a-b-c-d,将sink从g修改为a。或者说是将source的大范围修改为确定的单个source来减少输出方便排查。还有就是可以使用sanitizer来清洗掉其他不想查看到的分支。
0x04 官方规则-path-injection
path-injection
用于检测文件相关,可以是文件上传、文件读取。主要判断逻辑是对与传入文件操作时文件名是否可控
打开CEW-022,官方对于此漏洞的简要说明:java-path-injection

TaintedPathConfig污点跟踪分析的配置如下

source
使用了RemoteFlowSource,其中定义了用户输入可控的常见源。
sink
sink定义中使用了陌生的谓词和类,先看看PathCreation
override predicate isSink(DataFlow::Node sink) {
exists(Expr e | e = sink.asExpr() | e = any(PathCreation p).getAnInput() and not guarded(e))
}
跟进 PathCreation.qll 包,获取用于创建路径的输入,定义了常见用法。使用方式通过调用getAnInput()谓词获取方法内的所有参数,也就是将sink定义为传入的文件名。

再跟进 TaintedPathCommon.qll 查看guarded谓词

了解下ConditionBlock,可以使用下面查询内容
from ConditionBlock cb select cb, cb.getCondition(),cb.getCondition().getAChildExpr()
cb获取的是整个块,如:方法开始{}整个内容、if (tree.getId() != null)、if (tree.getId() == null)
cb.getCondition()表示获取此基本块最后一个节点条件,如:comboTree.getId() != null、salary.equalsIgnoreCase("null")
cb.getCondition().getAChildExpr()表示获取子表达式,如:tree.getId()、null、salary、"null"
public void demo() {
if (tree.getId() != null) {
cq.eq("id", tree.getId());
}
if (tree.getId() == null) {
cq.isNull("Depart");
}
cq.add();
......
data.setFooter("salary:"+(salary.equalsIgnoreCase("null")?"0.0":salary)+",age,email:合计");
}
回到guarded谓词中,
exists(PathCreation p | e = p.getAnInput())再次强调变量调用为文件名。
cb.getCondition().getAChildExpr*() = c将块的子表达式和表达式c匹配
c = e.getVariable().getAnAccess()文件名的所有调用和表达式c匹配
cb.controls(e.getBasicBlock(), true)注释意为:如果传入的e.getBasicBlock()是由该条件控制的基本块,即条件为true的基本块,则保持成立。
比如通过controls查询,结果如下图,只有当dirName的if判断语句为true才能将dirName传入File中。

将传入controls谓词中的true修改为false,则能匹配到如下图所示。进行判断的是!后面内容,所以可以得到该项

not inWeakCheck(c)最后一个过滤条件
inWeakCheck谓词中定义调用方法的方法名等于startsWith等,传入表达式等于调用方法的对象。
EqualityTest表示使用==或者!=的表达式,getAnOperand()谓词获取左边和右边的操作表达式,判断其中一个为null。
private predicate inWeakCheck(Expr e) {
// None of these are sufficient to guarantee that a string is safe.
exists(MethodAccess m, Method def | m.getQualifier() = e and m.getMethod() = def |
def.getName() = "startsWith" or
def.getName() = "endsWith" or
def.getName() = "isEmpty" or
def.getName() = "equals"
)
or
// Checking against `null` has no bearing on path traversal.
exists(EqualityTest b | b.getAnOperand() = e | b.getAnOperand() instanceof NullLiteral)
}
总结:
经过比对sink是否使用guarded谓词的结果如下,左边是没有使用guarded谓词

如下图,没有将文件名传入startsWith等方法,并且没有使用==或者!=对null进行判断,只有当文件名的判断条件为true才能将其传入File中,那这种情况则不能当作sink。其实官方使用guarded谓词加入判断的这种情况有点不太理解,暂时没有想到哪些场景这种情况是适用的。可能我个人使用的话会将该项注释掉。

isSanitizer
如果数据类型是基本类型或者是其包装类则清洗掉
override predicate isSanitizer(DataFlow::Node node) {
exists(Type t | t = node.getType() | t instanceof BoxedType or t instanceof PrimitiveType)
}
isSanitizerGuard
这里也是起到清洗作用,当调用方法为contains并且其参数值为..,对表达式e的判断为false则条件成立。
class ContainsDotDotSanitizer extends DataFlow::BarrierGuard {
ContainsDotDotSanitizer() {
this.(MethodAccess).getMethod().hasName("contains") and
this.(MethodAccess).getAnArgument().(StringLiteral).getValue() = ".."
}
override predicate checks(Expr e, boolean branch) {
e = this.(MethodAccess).getQualifier() and branch = false
}
}
override predicate isSanitizerGuard(DataFlow::BarrierGuard guard) {
guard instanceof ContainsDotDotSanitizer
}
上面的内容以案例来看是容易理解些的,如下图,只有当sourceFilename.contains("..")的判断语句为false才能进入File中,那么这种情况则将其清洗掉。
也就是代码中如果对文件名内容进行..检测则清洗掉,不展示该数据。

以上就是path-injection内容的讲解,不考虑guarded谓词情况,其实挺容易理解的,将常见用户输入可控的位置作为source,将常见文件操作方法的参数即文件名作为sink,清洗掉那些类型是基本类型等、如果对文件名进行..检测则也清洗掉。
应用到真实场景
当我们查询,可以看到这里查询到了一个上传的工具类中,source是multipartRequest.getFileMap()方法。如果稍微往前根据可以看到这里multipartRequest对象应该就是controller中传入进来的request对象,那么这里需要重新找到具体是哪个controller调用到这里



重新将config编写如下,这里只将source查到RequestMapping,如果要考虑全可以有GetMapping等。但只修改为如下是还不能查到内容的。
class TaintedPathConfig extends TaintTracking::Configuration {
TaintedPathConfig() { this = "TaintedPathConfig" }
override predicate isSource(DataFlow::Node source) {
exists( Method m, Parameter p|
m.getAnAnnotation().getType().hasQualifiedName("org.springframework.web.bind.annotation", "RequestMapping")
and m.hasAnnotation()
and m.getAParameter() = p
and source.asParameter()=p
and p.getType().hasName("HttpServletRequest")
)
}
override predicate isSink(DataFlow::Node sink) {
exists( Method m, Parameter p| m.hasName("uploadFile") and
m.getDeclaringType().hasQualifiedName("org.xxxx.core.common.dao.impl", "xxxxx")
and m.getAParameter() = p
and sink.asParameter()=p
and p.getType().hasName("UploadFile")
)
}
}
原因就是UploadFile实例时这里中断了

将其连接起来后即可
class InstanceTaintStep extends TaintTracking::AdditionalTaintStep{
override predicate step(DataFlow::Node src, DataFlow::Node sink){
exists(ClassInstanceExpr ma |
sink.asExpr() = ma
and src.asExpr() = ma.getAnArgument())
}
}
跳跳糖