Java反射进阶
Java反序列化基础篇-03-Java反射进阶
0x01 前言
前文说到 Java 反射的一些基础知识,这篇讲进阶。
0x02 反射的进阶知识
1. 关于 java.lang.Runtime
- 关于 Java 的 Runtime 类,有必要还是说一下。
为什么要用这个 Runtime
类?
我们知道 Java 当中很多的 CVE 漏洞,都与反序列化有关,反序列化也与 RCE 有关,而 Runtime
这个类正是用来命令执行的。
最主要的原因,Runtime
类中有 exec
方法,可以用来命令执行。
2. 设置 setAccessible(true)暴力访问权限
在一般情况下,我们使用反射机制不能对类的私有 private
字段进行操作,绕过私有权限的访问。
但一些特殊场景存在例外的时候,比如我们进行序列化操作的时候,需要去访问这些受限的私有字段,这时我们可以通过调用 AccessibleObject
上的 setAccessible()
方法来允许访问。
- 这种方法与
getConstructor
配合使用
和 getMethod
类似,getConstructor
接收的参数是构造函数列表类型,因为构造函数也支持重载,
所以必须用参数列表类型才能唯一确定一个构造函数。
还是以弹计算器为例。
1 2 3 4 5 6 7 8 9 10 11 12 13
| package src.ReflectDemo; 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"); } }
|
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)
4. 各种代码块执行顺序
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| package src.ReflectDemo;
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 16
| 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) {
} } }
|
0x03 Java 命令执行的三种方式
还记得最开始讲的反序列化吗?反序列化当中我们需要入口类,需要链子,还需要一个命令执行的方法。
1. 调用 Runtime 类进行命令执行
先挂整个程序,再进行细细分析
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| package src.CommandExec; 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); }
|
2. ProcessBuilder
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 19 20
| package src.CommandExec; 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); } }
|
3. 使用 ProcessImpl
ProcessImpl
是更为底层的实现,Runtime
和 ProcessBuilder
执行命令实际上也是调用了 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
| package src.CommandExec; 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); } } laptop-msg46jep\vanhurts
|
0x04 Java 反射修改 static final 修饰的字段
private
这个就不用说了,很简单,getDeclaredField
即可
PrivatePerson.java
1 2 3 4 5 6 7 8
| 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
| public class PrivateReflect { public static void main(String[] args) throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException, NoSuchMethodException, InstantiationException, InvocationTargetException { Class c = Class.forName("src.ReflectDemo.ReflectFixFinal.pojo.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
| 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
| 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); } }
|
final
刚才使用反射成功修改了 private 修饰的变量, 那么如果是 final 修饰的变量那么还能否使用反射来进行修改呢?这时候就需要分情况了。
final 字段能否修改,有且取决于字段是直接赋值还是间接赋值(编译时赋值和运行时赋值的区别)。直接赋值是指在创建字段时就对字段进行赋值,并且值为 JAVA 的 8 种基础数据类型或者 String 类型,而且值不能是经过逻辑判断产生的,其他情况均为间接赋值。
直接赋值
定义直接赋值的 final 修饰符属性
FinalStraightPerson.java
1 2 3 4 5 6 7 8 9 10
| 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
| public class FinalStraightReflect { public static void main(String[] args) throws Exception { Class c = Class.forName("src.ReflectDemo.ReflectFixFinal.pojo.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
| 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
| public class InDirectReflect { public static void main(String[] args) throws Exception { Class c = Class.forName("src.ReflectDemo.ReflectFixFinal.pojo.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
| 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
| 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); } }
|
0x05 小结
说实话,感觉自己基础不太牢固吧,还是要努力,反射的东西感觉不多。
命令执行的方式给我一种非常灵活的感觉。