前言
WebShell是网站入侵的常用后门,利用WebShell可以在Web服务器上进行执行系统命令、窃取数据、植入病毒、勒索核心数据、SEO挂马等恶意操作,危害极大。 目前业内在WebShell领域的检测主要侧重于PHP,有非常多的PHP WebShell攻防文章,而JSP的检测以及攻防文章相比都较少。但实际上Java应用在生产环境中同样占据了重要的地位,不容忽视。
阿里云非常重视WebShell对抗,总共举办了五次大型赏金活动。在数千名白帽子,共计数十万次全方位、无死角对抗测试之下,云安全中心WebShell检测的稳定性以及检出能力都经受住了考验,创造了业内收集范围最广、单个绕过样本赏金最高的纪录。
在检测引擎不断迭代升级过程中,我们也积累了一批先进的姿势与宝贵的经验,希望通过文章的形式,从源码层面系统地与大家分享各种绕过方式,共同探究JSP的检测与绕过之道。
历史文章:
《JSP WebShell攻防(一)之字符编码绕过》https://ti.aliyun.com/#/log?id=29
《JSP WebShell攻防(二)之标签绕过》https://ti.aliyun.com/#/log?id=31
代理模式简介
代理模式是指为其他对象提供一种代理,以控制对这个对象的访问,Proxy代理对象在访问对象和目标对象之间起到中介作用。
- 代理模式不同分类:按照代理类生成时机不同分为静态代理和动态代理,静态代理类在编译期生成,动态代理类则是在Java运行时动态生成;而动态代理又可以细分为JDK动态代理和CGLib动态代理两种,后面会有详细介绍。
- 代理模式不同角色:
抽象角色(Abstract Subject):通过接口或抽象类声明真实角色和代理对象实现的业务方法。
真实角色(Real Subject):实现了抽象角色中的具体业务,是代理对象所代表的真实对象,是最终要引用的对象。
代理角色(Proxy Subject):提供了与真实角色相同的接口,其内部含有对真实角色的引用,它可以访问、控制或扩展真实角色的功能。
- 代理模式示意图:
静态代理
- 示例介绍
静态代理在使用时,需要定义接口或者父类,被代理对象(即目标对象)与代理对象一起实现接口或者时继承相同的父类。如下列所示:
(1)接口类定义:定义一个服务接口,其包含一个print的业务接口需要在其子类中进行实现。
(2)接口类实现:定义一个具体的类,实现IService接口定义的方法。
(3)静态代理类实现:在代理类中通过内部包含目标对象的引用,实现对目标对象的控制或者扩展其功能。
- 优缺点分析
优点:在不修改目标对象功能的前提下,能通过代理对象对目标功能进行扩展。
缺点:代理对象需要与目标对象实现一样的接口,这样就会有很多代理类,一旦接口增加或者删减方法,目标对象与代理对象都要进行维护,成本较高。例如示例中,接口类仅定义了一个方法,如果定义多个方法,则在代理类中均需进行实现。
动态代理
静态代理中一个代理只能代理一种类型,而且是在编译器编译前就已经确定被代理的对象;而动态代理是在运行时,通过反射机制实现动态代理,并且能够代理各种类型的对象,即静态代理是一对一的关系,而动态代理则是一对多的关系。
动态代理的实现原理是,在运行时期构建需要被代理类的代理对象,这个代理对象可以针对不同的被代理类组装成不同的内容,因此可以对各种对象进行代理,这种方式是通过Java的反射机制实现的。具体实现的方案有以下两种:
(1)JDK动态代理:内置在JDK中,不需要引入第三方依赖库,使用简单,但功能相对较弱。
(2)CGLib动态代理:使用第三方CGLib库,总体性能比JDK动态代理好且功能强大,但有依赖,
JDK动态代理
下面首先来看下JDK代理类的使用,在JDK中生成代理对象所需要用的核心API如下所示:
(1)代理类所在的包:java.lang.reflect.Proxy
(2)代理类用到的核心方法:newProxyInstance()
- 示例介绍
在该示例中,所有IService的方法调用都会进入到代理类的invoke方法中,在这里可以对原有方法进行过滤或者扩展,以达到动态接管目标对象的目的。
- 原理总结
通过示例不难看出,代理类需要实现InvocationHandler接口,并重写invoke方法,而被代理类需要实现接口,它不支持继承。JDK动态代理类不需要事先定义好, 而是在运行期间动态生成,它不需要实现和被代理类一样的接口, 所以可以绑定多个被代理类。主要实现原理为反射,通过反射在运行期间动态生成代理类, 并且通过反射调用被代理类的实际业务方法。
- 优缺点分析
优点:使用简单,维护成本低;Java原生支持,不需要任何依赖;解决了静态代理存在一对一维护困难的问题。
缺点:使用反射,性能比较差;只支持接口实现,不支持继承,不能满足所有业务场景。
CGLib动态代理
CGLib是一个强大的、高性能的代码生成库。它可以在运行期扩展Java类和接口,其被广泛应用于AOP框架中(Spring等), 用以提供方法拦截。CGLib比JDK动态代理更强的地方在于它不仅可以接管Java接口, 还可以接管普通类的方法,应用场景为保护和增强目标对象。
- 示例介绍
注意:该示例的使用,需要首先导入第三方的依赖库CGLib,才能调用相关的方法进行代理。
- 原理总结
CGLib动态代理使用的是FastClass机制,生成字节码的底层实现是使用ASM字节码框架,它需要创建3份字节码,所以在第一次使用时会比较耗性能,但是后续使用较JDK动态代理方式更高效,适合单例bean场景。这种方法也存在一定的局限性,由于它是采用动态创建子类的方法,所以对于final方法无法进行代理。
- 优缺点分析
优点:能将代理对象与真实被调用的目标对象分离;一定程度上降低了系统的耦合程度,易于扩展;代理可以起到保护目标对象和增强目标对象的作用。
缺点:会造成系统设计中类的数目增加;在客户端和目标对象之间增加了一个代理对象,请求处理速度变慢;增加了系统的复杂度。
动态代理绕过
前面介绍了不少关于代理相关的基础知识,可以方便大家更好的理解接下来将要介绍的绕过姿势,这里我们主要以JDK动态代理绕过进行举例说明,首先来看下这种绕过方式的示例代码,如下所示:
示例代码
利用方式:样本访问的输入参数为?method1=exec&method2=execute&cmd=open+/System/Applications/Calculator.app
绕过特点分析
- 对于静态检测引擎而言:从代码中不难看出,这类样本最大的特点在于,完全没有危险函数的直接调用,所有敏感函数的调用和执行都可以通过参数传入来进行伪装,且这类样本本身的调用栈是极其隐蔽的,没有直接调用就没有检测的特征,所以对于使用代理类进行绕过的样本,静态检测引擎几乎没有还手的机会。
- 对于动态沙箱而言:如何能执行到危险函数的位置是关键也是难点,对于这类样本而言,执行路径是靠参数传入的,也就是动态沙箱执行的路径是外部控制的,不同的输入执行的路径完全不同,例如这里的参数”method2=excecute”,即需要调用Statement类的execute方法,才能触发整个调用链,如果传入的参数不符合预期,也就无法触发,也就无法做到检测,这也是沙箱在这类样本面前完全失效的原因,本质上就是沙箱的检测痛点“分支绕过”。
- 对于模拟分析引擎而言:这类绕过的最大挑战在于,其调用栈非常的长,涉及到的类和方法非常的多且隐蔽,很多还是间接调用,那么要精准检测就需要对这些相关的类和方法进行完整的模拟,这个模拟实现的工作量是非常大的且开发周期较长,可行性不高。而且很关键的一点,通过代理类进行调用的样本,本身利用了Java语言的动态代理特性,代理类都是在内存中动态临时生成的,并不会落盘,所以也没法做到事前分析和研判,这给检测工作带了非常大的挑战。
绕过细节分析
完整调用栈
通过上述实际调试的调用栈,不难看出其是如何被触发的,以及一步步如何走到反射调用invoke方法,进而完成任意命令执行的。
动态创建代理类
在上述的整个调用栈中,有一点非常值得关注就是$Proxy0这个类是怎么来的,它的作用是什么,又是如何被关联触发利用的,接下来我们一探究竟。
(1)$Proxy0类的由来
通过调试分析不难发现,代理类$Proxy0它是在我们调用newProxyInstance()方法时,动态创建的。
(2)$Proxy0类都包含哪些内容
- 三个默认方法:动态代理类中,首先会添加hashCode()、equals()、toString()三个默认方法。
- 被代理的所有方法:接下来会通过反射枚举被代理对象的所有方法,然后逐个添加到被代理的方法中,这样就避免了每个被代理类的都需要实现写代理类的麻烦,动态的实现一对多代理关系。
- 生成静态初始化模块:在该模块中完成对所有方法的赋值和初始化工作
(3)如何让JVM生成$Proxy0类
在默认的情况下,jvm是不会落盘临时动态创建的代理类,下面通过源码分析,打开这个配置开关,方便我们后续分析。
在ProxyGenerator.generateProxyClass()这方法中,可以清楚的看到,如果要生成class文件,是需要事先进行设置的,继续看saveGeneratedFiles这个变量的赋值逻辑。
在这里就可以清晰的看到,jvm在初始化时,会读取这个配置信息,所以在jvm启动添加如下参数:-Dsun.misc.ProxyGenerator.saveGeneratedFiles=true,即可生成完整的class文件。
(4)Dump代理类$Proxy0
// // Source code recreated from a .class file by IntelliJ IDEA // (powered by FernFlower decompiler) // package com.sun.proxy; 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 Comparable { private static Method m1; private static Method m3; private static Method m2; private static Method m0; public $Proxy0(InvocationHandler var1) throws { super(var1); } public final boolean equals(Object var1) throws { 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 int compareTo(Object var1) throws { try { return (Integer)super.h.invoke(this, m3, new Object[]{var1}); } catch (RuntimeException | Error var3) { throw var3; } catch (Throwable var4) { throw new UndeclaredThrowableException(var4); } } public final String toString() throws { try { return (String)super.h.invoke(this, m2, (Object[])null); } catch (RuntimeException | Error var2) { throw var2; } catch (Throwable var3) { throw new UndeclaredThrowableException(var3); } } public final int hashCode() throws { try { return (Integer)super.h.invoke(this, m0, (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("java.lang.Comparable").getMethod("compareTo", Class.forName("java.lang.Object")); m2 = Class.forName("java.lang.Object").getMethod("toString"); m0 = Class.forName("java.lang.Object").getMethod("hashCode"); } catch (NoSuchMethodException var2) { throw new NoSuchMethodError(var2.getMessage()); } catch (ClassNotFoundException var3) { throw new NoClassDefFoundError(var3.getMessage()); } } }
被代理的方法如何调用
这里的compareTo方法就是Proxy代理类动态生成的代理方法,它的调用都是通过InvocationHandler对象进行间接调用。
如何触发任意命令执行
触发利用的关键调用栈:
java.util.TreeSet.add // 触发点,可以触发被代理对象的compareTo方法调用,从而触发整个调用链 --> java.utilTreeMap.put --> java.util.TreeMap.compare --> com.sun.proxy.$Proxy0.compareTo *** 动态代理类 *** --> java.beans.EventHandler.invoke
核心绕过点总结
- 利用触发:在该示例中主要通过TreeSet类的add方法,触发被代理对象的CompareTo方法调用,进而触发EventHandler的invoke方法代理,在被代理的CompareTo方法中,通过精心构建Statement类的初始化参数,让compareTo方法被Runtime类的exec方法接管,从而实现任意命令执行,整个利用过程非常的隐蔽和巧妙。
- 检测难点:从Proxy.newProxyInstance方法调用原型中不难看出,这里的interfaces参数即是被代理的对象,在Java语言生态中这样的接口定义是非常多的,在模拟引擎中如果要完整实现几乎是不可能的,而且如示例中TreeSet类的add方法,想要事先分析并搜集所有的触发点,进行预判和拦截也几乎是不可能。
攻防对抗
对于模拟分析引擎而言,前文提到代理类样本检测的难点在于,
(1)其一如果跟这类样本进行硬刚,则面临着需要模拟实现的类非常众多的问题,导致开发周期很长,甚至总会有漏网之鱼,导致方案的失效;
(2)其二要收集全所有通过代理类进行利用的触发点,从而事先研判分析,也几乎是不可能的,这里涉及到的类和方法更多。
基于以上的分析和认识,同时结合模拟污点分析引擎的特点,一种可行的方案是通过追踪代理对象的方法,来解决这类样本的检测问题。那么为什么这个方案可行呢,首先看下newProxyInstance函数的第三个参数,即InvocationHandler接口对象,所有需要代理的对象都需要通过这个Handler进行封装,然后被代理对象的任意方法调用,都会被转化成为Handler中invoke方法的调用,而具体调用方法的内容,则是在Handler被初始化时决定的,对于正常非后门类web应用,其代理方法的功能是具有确定性的,仅后门类程序会在代理的方法中加入不确定性的参数,从而达到执行任意指令的目的。
有了上述对Java语言特性的分析,我们就可以大胆的利用模拟分析引擎其污点追踪的能力,只要看住InvocationHandler参数传入的对象是否包含污点,即是否具备执行任意命令的能力,便可准确的判断,代码是否包含webshell后门,达到精准检测的目的。
写在最后
云安全中心WebShell检测系统在不断对抗的过程中,逐步发展出了静态规则+动态沙箱引擎+模拟污点引擎+机器学习等多种综合手段,并不会受单一维度的绕过影响。
目前云安全中心已经对外开放恶意文件的检测能力。欢迎各位业内大咖和小伙伴们前来测试体验,反馈并提出宝贵意见,测试地址:https://ti.aliyun.com/
同时云安全中心也支持恶意文件检测的单独售卖,支持API化、SDK化,方便各种环境下接入使用。使用文档如下:https://help.aliyun.com/document_detail/446449.html
Java博大精深,深入挖掘还可以发现更多有趣的特性。本文仅为抛砖引玉,如果有不严谨的地方欢迎指正。
关于我们
阿里云安全-能力建设团队研究方向涵盖WEB安全、二进制安全、企业入侵检测与响应、安全数据分析、威胁情报等,以安全技术为本,结合云计算时代的数据与算力优势,建设全球领先的企业安全产品,为阿里集团以及公有云百万用户的基础安全保驾护航。
原文始发于阿里云安全:JSP WebShell攻防(三)之动态代理类绕过