前言
分析漏洞的本质是为了能让我们从中学习漏洞挖掘者的思路以及挖掘到新的漏洞,而CodeQL就是一款可以将我们对漏洞的理解快速转化为可实现的规则并挖掘漏洞的利器。根据网上的传言Log4j2的RCE漏洞就是作者通过CodeQL挖掘出的。虽然如何挖掘的我们不得而知,但我们现在站在事后的角度再去想想,可以推测一下作者如何通过CodeQL挖掘到漏洞的,并尝试基于作者的思路挖掘新漏洞。
分析过程
首先我们要构建Log4j的数据库,由于lgtm.com中构建的是新版本的Log4j数据库,所以只能手动构建数据库了。首先从github获取源码并切换到2.14.1版本。
git clone https://github.com/apache/logging-log4j2.git
git checkout be881e5
由于我们这次分析的主要是log4j-core和log4j-api中的内容,所以打开根目录的Pom.xml注释下面的内容。
<modules>
<module>log4j-api-java9</module>
<module>log4j-api</module>
<module>log4j-core-java9</module>
<module>log4j-core</module>
<!-- <module>log4j-layout-template-json</module>
<module>log4j-core-its</module>
<module>log4j-1.2-api</module>
<module>log4j-slf4j-impl</module>
<module>log4j-slf4j18-impl</module>
<module>log4j-to-slf4j</module>
<module>log4j-jcl</module>
<module>log4j-flume-ng</module>
<module>log4j-taglib</module>
<module>log4j-jmx-gui</module>
<module>log4j-samples</module>
<module>log4j-bom</module>
<module>log4j-jdbc-dbcp2</module>
<module>log4j-jpa</module>
<module>log4j-couchdb</module>
<module>log4j-mongodb3</module>
<module>log4j-mongodb4</module>
<module>log4j-cassandra</module>
<module>log4j-web</module>
<module>log4j-perf</module>
<module>log4j-iostreams</module>
<module>log4j-jul</module>
<module>log4j-jpl</module>
<module>log4j-liquibase</module>
<module>log4j-appserver</module>
<module>log4j-osgi</module>
<module>log4j-docker</module>
<module>log4j-kubernetes</module>
<module>log4j-spring-boot</module>
<module>log4j-spring-cloud-config</module> -->
</modules>
由于log4j-api-java9和log4j-core- java9需要依赖JDK9,所以要先下载JDK9并且在C:\Users\用户名.m2\toolchains.xml中加上下面的内容。
<toolchains>
<toolchain>
<type>jdk</type>
<provides>
<version>9</version>
<vendor>sun</vendor>
</provides>
<configuration>
<jdkHome>C:\Program Files\Java\jdk-9.0.4</jdkHome>
</configuration>
</toolchain>
</toolchains>
通过下面的命令完成数据库构建
CodeQL database create Log4jDB --language=java --overwrite --command="mvn clean install -Dmaven.test.skip=true"
构建好数据库后,我们要找JNDI注入的漏洞,首先要确定在这套系统中调用了InitialContext#lookup方法。在LookupInterface项目中已经集成了常见的发起JNDI请求的类,只要稍微改一下即可。
首先定义Context类型,这个类中综合了可能发起JNDI请求的类。
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")
}
}
下面寻找那里调用了Context的lookup方法。
from Call call,Callable parseExpression
where
call.getCallee() = parseExpression and
parseExpression.getDeclaringType() instanceof Context and
parseExpression.hasName("lookup")
select call
- DataSourceConnectionSource#createConnectionSource
@PluginFactory
public static DataSourceConnectionSource createConnectionSource(@PluginAttribute("jndiName") final String jndiName) {
if (Strings.isEmpty(jndiName)) {
LOGGER.error("No JNDI name provided.");
return null;
}
try {
final InitialContext context = new InitialContext();
final DataSource dataSource = (DataSource) context.lookup(jndiName);
if (dataSource == null) {
LOGGER.error("No data source found with JNDI name [" + jndiName + "].");
return null;
}
return new DataSourceConnectionSource(jndiName, dataSource);
} catch (final NamingException e) {
LOGGER.error(e.getMessage(), e);
return null;
}
}
- JndiManager#lookup
@SuppressWarnings("unchecked")
public <T> T lookup(final String name) throws NamingException {
return (T) this.context.lookup(name);
}
找到sink后我们还需要找到source,虽然Codeql定义了RemoteFlowSource支持多种source,但是我们还是要根据实际的代码业务来分析可能作为source的点。
在Log4j作为日志记录的工具,除了从HTTP请求中获取输入点外,还可以在记录日志请求或者解析配置文件中来获取source。先不看解析配置文件获取source的点了,因为这需要分析Log4j解析配置文件的流程比较复杂。所以目前我们只考虑通过日志记录作为source的情况。稍微了解Log4j的同学都知道,Log4j会通过error/fatal/info/debug/trace等方法对不同级别的日志进行记录。通过分析我们可以看到我们输入的message都调用了logIfEnabled方法并作为第四个参数输入,所以可以将这里定义为source。
下面使用全局污点追踪分析JNDI漏洞,还是套用LookupInterface项目中的代码,修改source部分即可。
/**
*@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")
}
}
predicate isLookup(Expr arg) {
exists(MethodAccess ma |
ma.getMethod().getName() = "lookup"
and
ma.getMethod().getDeclaringType() instanceof Context
and
arg = ma.getArgument(0)
)
}
predicate isLogging(Expr arg) {
exists(MethodAccess ma |
ma<