该博客为参考学习笔记博客,仅为本人记录的笔记,所以欢迎大家去Drunkbaby
师傅的博客中进行学习!Drunkbaby’s Blog
参考链接:Java反序列化基础篇-03-Java反射进阶
一、反射的进阶知识
1.1 关于Java的java.lang.Runtime
类
前面我们对这个类进行了一些简单的介绍:
在Java编程中,Runtime
类提供了一种与Java应用程序的运行环境进行交互的方式。Runtime
类是一个单例类,它封装了应用程序运行时的环境,通过它,开发者可以访问JVM的某些底层特性和功能。以下是 Runtime
类的主要作用和功能:
执行系统命令
可以使用 exec
方法来执行操作系统命令,这在需要与系统进程交互时非常有用。
1 2 3 4 5
| try { Runtime.getRuntime().exec("notepad"); } catch (IOException e) { e.printStackTrace(); }
|
内存管理
关闭JVM
添加JVM关闭钩子
对于一些RCE来说,这个类就是用来执行命令的;所以在反序列化中大多数的利用都与该类有关,因为可以利用exec
方法来执行系统命令;
1.2 设置setAccessible(true)暴力访问权限
在一般情况下,我们使用反射机制不能对类的私有 private
字段进行操作,所以需要通过设置访问权限setAccessible(true)
来绕过访问限制从而能够执行一些函数或者修改一些私有变量的值。
在此之前我们进行过一个弹计算器的实践,在那里我们就有利用过这个方法,从而能够调用私有的构造函数实现实例化对象;如下:
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
| package com.mango.reflection;
import java.io.IOException; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method;
public class reflectionRuntime { public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException, IOException, InvocationTargetException { Class c = Class.forName("java.lang.Runtime"); Constructor con = c.getDeclaredConstructor(); con.setAccessible(true); Object o = con.newInstance(); Method mRuntime = c.getDeclaredMethod("getRuntime"); Method mExec = c.getDeclaredMethod("exec", String.class); Object re = mRuntime.invoke(o); mExec.invoke(re, "calc"); } }
|
1.3 forName的两个重载方法
对于Class.forName()
方法,有两个重载方法;
1 2
| forName(String className) forName(String name, boolean initialize, ClassLoader loader)
|
- 第一个参数表示类名
- 第二个参数表示是否初始化
- 第三个参数表示类加载器,即告诉Java虚拟机如何加载这个类,Java默认的ClassLoader就是根据类名来加载类, 这个类名是类完整路路径,如
java.lang.Runtime
因此,forName(className)
等价于forName(className, true, currentLoader)
1.4 各种代码块执行顺序
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
| package com.mango.reflectionDemo;
public class FunctionSort { public static void main(String[] args) { Test test = new Test(); } }
class Test{ { System.out.println("1"); } static { System.out.println("2"); } Test(){ System.out.println("3"); } }
|
运行结果:
根据运行的结果可以知道,首先调用的是static{}
,其次是{}
,最后才调用构造函数Test()
。
其中,static()
就是在“类初始化”的时候调用的,而{}
中的代码回放在构造函数的super()
后面,但在当前构造函数
内容的前面。
所以说,forName
中的initialize=true
其实就是告诉Java虚拟机是否执行“类初始化”(是否执行static()
中的内容)。
那么,假设我们有如下函数,其中函数的参数name可控:
1 2 3
| public void ref(String name) throws Exception { Class.forName(name); }
|
由于默认的initialize
参数默认为true
,我们即可编写一个恶意类,将恶意代码放置在static()
中,使类默认初始化进而执行static()
中的参数;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| package com.mango.maliciousClass;
import java.io.IOException;
public class returnCalc { static { try { Runtime.getRuntime().exec("calc"); } catch (IOException e) { throw new RuntimeException(e); } } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| package com.mango.reflectionDemo;
public class staticReflection { public static void main(String[] args) throws ClassNotFoundException { Class.forName("com.mango.maliciousClass.returnCalc"); } }
|
二、Java命令执行的三种方式
反序列化当中需要入口类,需要链子,还需要一个命令执行的方法。
2.1 调用 Runtime 类进行命令执行
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
| package com.mango.maliciousClass;
import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream;
public class runtimeCE { public static void main(String[] args) throws IOException { InputStream inputStream = Runtime.getRuntime().exec("whoami").getInputStream(); byte[] cache = new byte[1024]; ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); int readLen = 0; while ((readLen= inputStream.read(cache))!=-1) byteArrayOutputStream.write(cache, 0, readLen); System.out.println(byteArrayOutputStream); } }
|
大致思路:
- 先调用 getRuntime() 返回一个 Runtime 对象,然后调用 Runtime 对象的 exec 的方法。
- 调用 Runtime 对象的 exec 的方法会返回 Process 对象,调用 Process 对象的 getInputStream() 方法。
- 调用 Process 对象的 getInputStream() 方法,此时,子进程已经执行了 whoami 命令作为子进程的输出,将这一段输出作为输入流传入 inputStream
- OK,我们的第一行就是用来执行命令的,但是我们执行命令需要得到命令的结果,所以需要将结果存储到字节数组当中
1 2 3 4
| int readLen = 0; while ((readLen = inputStream.read(cache))!=-1) byteArrayOutputStream.write(cache, 0, readLen); System.out.println(byteArrayOutputStream);
|
上述代码的作用是将命令执行的结果输出到标准输出中;
2.2 调用 ProcessBuilder 类进行命令执行
通过另一种方式执行命令再通过相同的方式将命令输出内容输出到标准输出;
ProcessBuilder processBuilder = new ProcessBuilder("命令", "参数1", "参数2");
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
| package com.mango.maliciousClass;
import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream;
public class processBuilderCE { public static void main(String[] args) throws IOException { InputStream inputStream = new ProcessBuilder("calc").start().getInputStream(); byte[] cache = new byte[1024]; int readLen = 0; ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); while((readLen=inputStream.read(cache))!=-1) byteArrayOutputStream.write(cache, 0, readLen); System.out.println(byteArrayOutputStream); } }
|
2.3 利用反射调用 ProcessImpl 类进行命令执行
ProcessImpl
类是更为底层的实现,Runtime
和ProcessBuilder
执行命令实际上也是调用ProcessImpl
这个类来实现的。对于ProcessImpl
类我们不能直接调用,但是可以通过反射来间接调用ProcessImpl
来实现执行命令的目的。
- 不能直接调用
ProcessImpl
是因为该类的构造方法是私有的,我们不能直接调用构造方法实例化对象,所以需要通过反射去进行命令执行
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
| package com.mango.maliciousClass;
import javax.activation.MimetypesFileTypeMap; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.Map;
public class processImplCE { public static void main(String[] args) throws ClassNotFoundException, InvocationTargetException, IllegalAccessException, IOException, NoSuchMethodException { Class<?> processImplClass = Class.forName("java.lang.ProcessImpl"); String[] cmds = new String[]{"whoami"}; Method method = processImplClass.getDeclaredMethod("start", String[].class, Map.class, String.class, ProcessBuilder.Redirect[].class, boolean.class); method.setAccessible(true); Process e = (Process) method.invoke(null, cmds, null, ".", null, true); InputStream inputStream = e.getInputStream(); byte[] cache = new byte[1024]; int readLen = 0; ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); while((readLen=inputStream.read(cache))!=-1) byteArrayOutputStream.write(cache, 0, readLen); System.out.println(byteArrayOutputStream); } }
|
三、Java反射各种修饰符字段
3.1 private
这个修饰符就不必多说了,直接使用getDeclaredField
获取相应变量,再通设置访问权限setAccessible
去修改即可;
这里就不在做代码演示了,之前写过比较多类似的代码了。
3.2 static
单单一个static修饰符静态变量,跟private是一致的。这里把Drunkbaby
师傅的代码粘贴过来
1 2 3 4 5 6 7 8
| public class StaticPerson { private static StringBuilder name = new StringBuilder("Drunkbaby"); public void printInfo() { System.out.println(name); } }
|
1 2 3 4 5 6 7 8 9 10 11 12
| public class StaticReflect { public static void main(String[] args) throws Exception { Class c = Class.forName("src.ReflectDemo.ReflectFixFinal.pojo.StaticPerson"); Object m = c.newInstance(); Method nameMethod = c.getDeclaredMethod("printInfo"); nameMethod.invoke(m); Field nameField = c.getDeclaredField("name"); nameField.setAccessible(true); nameField.set(m,new StringBuilder("Drunkbaby static Silly")); nameMethod.invoke(m); } }
|
3.3 final
- final变量直接复制
经过本人的一些尝试后发现确实无法修改及时绕过报错的问题,但是仍然无法做到对变量的修改;
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
| package com.mango.entity;
import java.lang.reflect.Field; import java.lang.reflect.Modifier;
public class Student { private String name = "mango"; private static String gender = "male"; private final int age = 18;
public Student(String name) { this.name = name; }
@Override public String toString() { return "Student{" + "name='" + name + '\'' + ", gender='" + gender + '\'' + ", age=" + age + '}'; } public static void main(String[] args) { try { Student student = new Student("John"); System.out.println("Before: " + student);
Class<?> clazz = Student.class;
Field ageField = clazz.getDeclaredField("age");
ageField.setAccessible(true);
Field modifiersField = Field.class.getDeclaredField("modifiers"); modifiersField.setAccessible(true);
modifiersField.setInt(ageField, ageField.getModifiers() & ~Modifier.FINAL);
ageField.setInt(student, 20);
System.out.println("After: " + student); } catch (Exception e) { e.printStackTrace(); } } }
|
- final变量间接赋值
若是final变量通过简介赋值,则只需要通过与private
修饰符修饰的变量一样的方法即可修改变量的值
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
| package com.mango.entity;
import java.lang.reflect.Field; import java.lang.reflect.Modifier;
public class Student { private String name = "mango"; private static String gender = "male"; private final int age;
public Student(String name, int age) { this.name = name; this.age = 18; }
@Override public String toString() { return "Student{" + "name='" + name + '\'' + ", gender='" + gender + '\'' + ", age=" + age + '}'; } public static void main(String[] args) { try { Student student = new Student("John", 18); System.out.println("Before: " + student);
Class<?> clazz = Student.class;
Field ageField = clazz.getDeclaredField("age");
ageField.setAccessible(true);
ageField.setInt(student, 20);
System.out.println("After: " + student); } catch (Exception e) { e.printStackTrace(); } } }
|
3.4 static + final
static + final终究也还是个final,若是该变量经过直接赋值的方式,那么变量的值便无法修改啦,通过反射机制也无法修改。
若是简介赋值则可以通过反射机制取消掉final修饰符再修改赋值即可
1 2 3 4 5 6 7 8
| public class StaticFinalPerson { static final StringBuilder name = new StringBuilder("Drunkbaby"); public void printInfo() { System.out.println(name); } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| public class StaticFinalReflect { public static void main(String[] args) throws Exception { Class c = Class.forName("src.ReflectDemo.ReflectFixFinal.pojo.StaticFinalPerson"); Object m = c.newInstance(); Method printMethod = c.getDeclaredMethod("printInfo"); printMethod.invoke(m); Field nameField = c.getDeclaredField("name"); nameField.setAccessible(true); Field nameModifyField = nameField.getClass().getDeclaredField("modifiers"); nameModifyField.setAccessible(true); nameModifyField.setInt(nameField, nameField.getModifiers() & ~Modifier.FINAL); nameField.set(m,new StringBuilder("Drunkbaby Too Silly")); nameModifyField.setInt(nameField, nameField.getModifiers() & ~Modifier.FINAL); printMethod.invoke(m); } }
|