实际上这是一个老洞了,始于2021,今天拿出来,是因为遇到了,觉得有一定的学习价值,就拿出来分享了,大佬绕过~
废话不多说,首先存在漏洞的点位是在一个叫JobInvokeUtil的类
接下来先对代码进行一下解读
String invokeTarget = sysJob.getInvokeTarget();
invokeTarget 调用的SysJob类里面的getInvokeTarget()方法
紧接着,String beanName = getBeanName(invokeTarget);
将invokeTarget的值传入getBeanName()方法,该方法用来截取第一个小括号并保留小括号之前的字符串,接着利用StringUtils.substringAfterLast 方法用于获取最后一个指定分隔符之前的子字符串(这里用来获取类的路径)
String methodName = getMethodName(invokeTarget);
与上面相似,该方法用来截取第一个小括号并保留小括号之前的字符串,接着利用StringUtils.substringAfterLast 方法用于获取最后一个指定分隔符之后的子字符串(这里用来获取类的某一个方法)
List<Object[]> methodParams = getMethodParams(invokeTarget);
此处将invokeTarget 拆分成List<Object[]> 对象,就从invokeTarget中解析出方法的参数列表
if (!isValidClassName(beanName)) 用来判断传入的beanName是不是合法的,他的判断方法其实就是看他有几个小数点
这里面其实是用来判断,调用的类名是spring bean的还是lib依赖包里的,如果是spring自带的,就会利用SpringUtils.getBean(beanName)获取bean,然后利用invokeMethod()去进行实例化;
如果调用的bean是外来的,就利用Class.forName反射调用外部的类然后进行实例化操作
因为该处漏洞利用需要调用外部的类,所以这里直接走else条件分支语句
这里细说一下Object bean = Class.forName(beanName).newInstance();
Class.forName(beanName)
是Java中用于动态加载类的方法。当你传递一个类的全名(包括包名)给这个方法时,它会尝试加载该类并返回该类的 Class 对象。如果类不存在或者因为其他原因无法加载,这个方法会抛出 ClassNotFoundException。
newInstance() 是 Class 类的一个方法,它用于创建一个该类的实例。但是,它有一些限制:
1.它只能用于具有无参数构造函数的类。
2.它只能用于不是抽象的类。
3.如果类的无参数构造函数是私有的,它也会失败。
于是乎我们可以知道,想要利用这里的反射去执行恶意的类,首先这个类不能是抽象的类并且他需要具有无参构造函数的类,并且无参构造函数需要有public属性
构造函数,大家可以理解为,他的函数名字和类名字是一样的,他没有返回类型、语句;在创建类对象的时候,他会被自动调用等等,而无参构造,其实就是他是空的,函数体里面没有东西,例如下面的这个Comxxxxxls()无参构造函数
invokeMethod(bean, methodName, methodParams); 这个是用来调用反射的关键方法,它使用Java的反射API来动态地调用一个对象(bean)上的方法。这个方法接受三个参数:
1.Object bean:要调用其方法的对象。
2.String methodName:要调用的方法的名称。
3.List<Object[]> methodParams:一个列表,其中每个元素都是一个对象数组,表示要传递给方法的参数。
使用StringUtils.isNotNull(methodParams)检查methodParams是否为非空。
如果methodParams非空且大小大于0,说明有参数需要传递给方法。由于我们需要利用不安全的反射,所以我们传入的参数一定不能是空的所以直接进入到if判断里面
Method method = bean.getClass().getDeclaredMethod(methodName, getMethodParamsType(methodParams));
这里可以更写为
Class.forName(“类名”).getDeclaredMethod(“方法名”, “方法参数”);
为了更加直观,笔者在这里写了一下二者之间的对比代码
method.invoke(bean, getMethodParamsValue(methodParams));
该代码为实际执行的代码,
invoke 方法用于动态地调用 method 方法。它接受两个参数:第一个参数是调用该方法的对象(即 bean),第二个参数是一个对象数组,包含了调用该方法所需的参数值(即 getMethodParamsValue(methodParams) 的返回值)
最后两行代码加载一块,就是典型的反射调用
Class.forName(“类名”).getDeclaredMethod(“方法名”, “方法参数”).invoke(Class.forName(“类名”).newInstance(), “参数”);
对比完整代码如下
可以看到,二者几乎没有区别,那么思路正确
由于代码是扣的,所以源项目跑不起来,但是不要紧,笔者在本地魔改了一下代码,方便给大家展示节目效果
首先笔者创建了一个SysJob类,该类仅仅是为了传递invokeTarget参数
接着是第二个类,JobInvokeUtil类,该类仅仅是为了传递参数,执行反射
我们动态调试一下,验证我们的思路
大家可以很直观的看到,beanName、methodName以及methodParams,其中methodParams是一个list object类型,里面主要有两个参数,一个是方法传递的参数,一个是方法的类型
进入bean判断,很明显我们是调用的外部的类,所以进入到else分支
跟进 invokeMethod方法
首先利用getClass()方法获取字节码对象,因为bean已经被实例化了,不能直接利用 getDeclaredMethod()方法获取里面的单个方法,所以需要先getClass()获取字节码,然后再getDeclaredMethod()获取某个特定的方法;由于getDeclaredMethod()方法是java内置的方法,所以这里就省略了getMethodParamsType(methodParams)是怎么来的,getDeclaredMethod()传递两个参数,一个是方法名,一个是方法参数
invoke 方法用于动态地调用 method 方法。它接受两个参数:第一个参数是调用该方法的对象,第二个参数是调用该方法所需的参数值
算了,还是简单看一下getMethodParamsType(methodParams)和getMethodParamsValue(methodParams)吧
getMethodParamsType(methodParams),其实就是获取List<Object[]对象里的下标为1的参数
getMethodParamsValue(methodParams) 其实就是获取List<Object[]对象里的下标为0的参数
再对比下图,想必大家就明白了吧
再给大家打印一下
而原始代码里,其实也过滤了一下东西
不允许调用rmi/ldap/http/https 但是他其实是调的字符串,直接用curl就行了,远程下载的地址不填http就可以,我们看一下效果
忘记写了,这个具备危险方法的外部类,我们应该如何寻找呢?这个就得靠运气了,不安全的反射,想要调用危险方法,前提是外部的类里有危险的方法,他与反序列化的重写方法还是有区别的,所以找的话,大家可以直接打开项目包的lib,找到里面的jar包,一个一个找,笔者运气不错,找到不少
该方法直接引用了危险的方法,并且传入的参数为String类型,那么只要参数可控就可以利用了
总结如下:
想要利用这个洞,需要找到存在危险方法的、可以传参的、存在构造函数且具备public属性的外部的类,只要输入的字符串不带http/https/rmi/ldap就行
原文始发于微信公众号(我不懂安全):JAVA-不安全的反射-RCE