反射的进阶知识
关于 java.lang.Runtime
关于 Java 的 Runtime 类,有必要还是说一下。
为什么要用这个 Runtime 类?
我们知道 Java 当中很多的 CVE 漏洞,都与反序列化有关,反序列化也与 RCE 有关,而 Runtime 这个类正是用来命令执行的。
最主要的原因,Runtime 类中有 exec 方法,可以用来命令执行。
设置 setAccessible(true)暴力访问权限
在一般情况下,我们使用反射机制不能对类的私有 private 字段进行操作,绕过私有权限的访问。
但一些特殊场景存在例外的时候,比如我们进行序列化操作的时候,需要去访问这些受限的私有字段,这时我们可以通过调用 AccessibleObject 上的 setAccessible() 方法来允许访问。
这种方法与 getConstructor 配合使用
和 getMethod 类似,getConstructor 接收的参数是构造函数列表类型,因为构造函数也支持重载,
所以必须用参数列表类型才能唯一确定一个构造函数。
还是以弹计算器为例。
1 2 3 4 5 6 7 8 9 10 11 12 13
| package urldns;
import java.lang.reflect.Constructor;
public class FinalReflectionCalc02 { public static void main(String[] args) throws Exception{ Class c1 = Class.forName("java.lang.Runtime"); Constructor m = c1.getDeclaredConstructor(); m.setAccessible(true); c1.getMethod("exec", String.class).invoke(m.newInstance(),"C:\\WINDOWS\\System32\\calc.exe"); } }
|
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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| package urldns;
public class FunctionSort { public static void main(String[] args) throws Exception{ Test test = new Test(); } static class Test{ { System.out.println("1"); } static { System.out.println("2"); } Test(){ System.out.println("3"); } } }
|

其实你运行一下就知道了,首先调用的是 static {} ,其次是 {} ,最后是构造函数。
其中, static {} 就是在”类初始化”的时候调用的,而 {} 中的代码会放在构造函数的 super() 后面,但在当前构造函数内容的前面。
所以说, forName 中的 initialize=true 其实就是告诉 Java 虚拟机是否执行”类初始化”。
那么,假设我们有如下函数,其中函数的参数name可控:
1 2 3
| public void ref(String name) throws Exception { Class.forName(name); }
|
我们就可以编写一个恶意类,将恶意代码放置在 static {}中,从而进行恶意代码的执行:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| import java.lang.Runtime; import java.lang.Process;
public class TouchFile { static { try { Runtime rt = Runtime.getRuntime(); String[] commands = {"touch", "/tmp/success"}; Process pc = rt.exec(commands); pc.waitFor(); } catch (Exception e) { } } }
|
Java 命令执行的三种方式
调用 Runtime 类进行命令执行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| package urldns;
import java.io.ByteArrayOutputStream; import java.io.InputStream;
public class RuntimeExec { public static void main(String[] args) throws Exception { 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 5 6 7 8 9
| byte[] cache = new byte[1024]; ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
int readLen = 0; while ((readLen = inputStream.read(cache))!=-1){ byteArrayOutputStream.write(cache, 0, readLen); }
|
ProcessBuilder
1
| InputStream inputStream = new ProcessBuilder("whoami)".start().getInputStream();
|
只是换了一种命令执行的方式,将内容读取出来的语句不变。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| package urldns;
import java.io.ByteArrayOutputStream; import java.io.InputStream;
public class ProcessBuilderExec { public static void main(String[] args) throws Exception{ InputStream inputStream = new ProcessBuilder("ipconfig").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); } }
|
使用 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
| package urldns;
import java.io.ByteArrayOutputStream; import java.io.InputStream; import java.lang.reflect.Method; import java.util.Map;
public class ProcessImplExec { public static void main(String[] args) throws Exception{ String[] cmds = new String[]{"whoami"}; Class clazz = Class.forName("java.lang.ProcessImpl"); Method method = clazz.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 反射修改 static final 修饰的字段
private
这个就不用说了,很简单,getDeclaredField 即可
PrivatePerson.java
1 2 3 4 5 6 7 8 9
| package urldns;
public class PrivatePerson { private StringBuilder name = new StringBuilder("Drunkbaby");
public void printName() { System.out.println(name); } }
|
对应的反射代码 PrivateReflect.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| package urldns;
import java.lang.reflect.Method; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException;
public class PrivateReflect { public static void main(String[] args) throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException, NoSuchMethodException, InstantiationException, InvocationTargetException { Class c = Class.forName("urldns.PrivatePerson"); Object m = c.newInstance(); Method PrintMethod = c.getDeclaredMethod("printName"); PrintMethod.invoke(m); Field nameField = c.getDeclaredField("name"); nameField.setAccessible(true); nameField.set(m, new StringBuilder("Drunkbaby Too Silly")); PrintMethod.invoke(m); } }
|

static
static 单独出现的话,getDeclaredField 也是可以的
StaticPerson.java
1 2 3 4 5 6 7 8 9 10
| package urldns;
public class StaticPerson { private static StringBuilder name = new StringBuilder("Drunkbaby");
public void printInfo() { System.out.println(name);
} }
|
StaticReflect.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| package urldns;
import java.lang.reflect.Method; import java.lang.reflect.Field;
public class StaticReflect { public static void main(String[] args) throws Exception { Class c = Class.forName("urldns.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); } }
|
final
刚才使用反射成功修改了 private 修饰的变量, 那么如果是 final 修饰的变量那么还能否使用反射来进行修改呢?这时候就需要分情况了。
final 字段能否修改,有且取决于字段是直接赋值还是间接赋值(编译时赋值和运行时赋值的区别)。直接赋值是指在创建字段时就对字段进行赋值,并且值为 JAVA 的 8 种基础数据类型或者 String 类型,而且值不能是经过逻辑判断产生的,其他情况均为间接赋值。
直接赋值
定义直接赋值的 final 修饰符属性
FinalStraightPerson.java
1 2 3 4 5 6 7 8 9 10 11 12
| package urldns;
public class FinalStraightPerson {
private final String name = "Drunkbaby"; public final int age = 20-2;
public void printInfo() { System.out.println(name+" "+age);
} }
|
如果我们直接用反射来修改值,是会报错的
FinalStraightReflect.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| package urldns;
import java.lang.reflect.Method; import java.lang.reflect.Field;
public class FinalStraightReflect { public static void main(String[] args) throws Exception { Class c = Class.forName("urldns.FinalStraightPerson"); Object m = c.newInstance(); Method printMethod = c.getDeclaredMethod("printInfo"); printMethod.invoke(m);
Field nameField = c.getDeclaredField("name"); Field ageField = c.getDeclaredField("age"); nameField.setAccessible(true); ageField.setAccessible(true); nameField.set(m,"Drunkbaby as Drun1baby"); ageField.set(m,"19");
printMethod.invoke(m); } }
|

间接赋值
InDirectPerson.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| package urldns;
public class InDirectPerson { private final StringBuilder sex = new StringBuilder("male"); public final int age = (null!=null?18:18); private final String name; public InDirectPerson(){ name = "Drunkbaby"; }
public void printInfo() { System.out.println(name+" "+age+" "+sex);
} }
|
InDirectReflect.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| package urldns;
import java.lang.reflect.Method; import java.lang.reflect.Field;
public class InDirectReflect { public static void main(String[] args) throws Exception { Class c = Class.forName("urldns.InDirectPerson"); Object m = c.newInstance(); Method printMethod = c.getDeclaredMethod("printInfo"); printMethod.invoke(m);
Field nameField = c.getDeclaredField("name"); Field ageField = c.getDeclaredField("age"); Field sexField = c.getDeclaredField("sex"); nameField.setAccessible(true); ageField.setAccessible(true); sexField.setAccessible(true); nameField.set(m,"Drunkbaby Too Silly"); ageField.set(m,180); sexField.set(m,new StringBuilder("female")); printMethod.invoke(m); } }
|
成功

static + final
使用 static final 修饰符的 name 属性,并且是间接赋值,直接通过反射修改是不可以的。师傅们可以自行尝试,这里我们需要通过反射, 把 nameField 的 final 修饰符去掉,再赋值。
StaticFinalPerson.java
1 2 3 4 5 6 7 8 9 10
| package urldns;
public class StaticFinalPerson { static final StringBuilder name = new StringBuilder("Drunkbaby");
public void printInfo() { System.out.println(name);
} }
|
StaticFinalReflect.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| package urldns;
import java.lang.reflect.Method; import java.lang.reflect.Field; import java.lang.reflect.Modifier;
public class StaticFinalReflect { public static void main(String[] args) throws Exception { Class c = Class.forName("urldns.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); } }
|

参考https://drun1baby.top/2022/05/29/Java%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E5%9F%BA%E7%A1%80%E7%AF%87-03-Java%E5%8F%8D%E5%B0%84%E8%BF%9B%E9%98%B6/