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())
}
}