福建二哥
个人技术博客分享

用友系统反序列化漏洞绕过分析

分类: 漏洞资讯 时间:2025-10-09 09:56:15 浏览:126次 评论:0
摘要:通过分析用友系统2023年反序列化漏洞和2025年文件上传漏洞的补丁,发现历史修复存在严重缺陷。2023年补丁仅校验第一个对象,2025年FilteredObjectInputStream存在白名单绕过漏洞:当首个类通过校验后设置rootChecked标志,后续反序列化完全不受限制。利用POJO Jackson链构造攻击载荷,成功实现命令执行。该案例揭示了补丁质量不足导致的连锁安全风险。

漏洞挖掘与历史漏洞分析

背景概述

在复现历史漏洞的过程中,我发现了一个新的安全问题,这个发现距离现在已有近半年时间。通过这次经历,我深刻体会到在复现漏洞时仔细阅读代码的重要性。

历史漏洞回顾

2023年反序列化漏洞

漏洞公告链接:
https://security.yonyou.com/#/noticeInfo?id=400

2025年文件上传漏洞

补丁信息链接:
https://security.yonyou.com/#/patchInfo?identifier=fac37cb5188a4c93bcf5abd0de1336e4

2023年漏洞补丁分析(历史遗留漏洞根源)

首先分析2023年的反序列化漏洞补丁。令人意外的是,这个补丁的效果并不理想,几乎等同于没有修复。

关键代码分析

protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    doAction(request, response);
}

public void doAction(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    ObjectInputStream in = null;
    try {
        in = new ObjectInputStream(request.getInputStream()) {
            @Override
            protected Class<?> resolveClass(ObjectStreamClass desc)
                    throws IOException, ClassNotFoundException {
                Map<String, String> map = new HashMap<String, String>();
                map.put(String.class.getName(), String.class.getName());
                map.put(HashMap.class.getName(), HashMap.class.getName());
                if (!map.containsValue(desc.getName())) {
                    throw new IllegalArgumentException("�������Ͳ�ƥ��");
                }
                Class<?> superImpl = super.resolveClass(desc);
                return superImpl;
            }
        };

        HashMap<String, String> headInfo  = (HashMap<String, String>)in.readObject();
        String dsName = headInfo.get("dsName");
        InvocationInfoProxy.getInstance().setUserDataSource(dsName);
        String oper = headInfo.get("operType");
        if ("upload".equals(oper)) {
            doUploadFile(headInfo,in,response);
        } else if ("download".equals(oper)) {
            doDownLoadFile(headInfo, response);
        }
    } catch (Exception e) {
        e.printStackTrace();
    }finally{
        if(in != null){
            in.close();
        }
    }
}

漏洞点分析:

  • 代码仅对传入的第一个对象进行校验
  • 校验通过后调用 .readObject() 方法
  • 后续版本继承了相同的校验逻辑,仍然只校验序列化传入的第一个类
  • 这是造成该漏洞的根本原因

个人反思:
在复现该漏洞时,我并未详细分析补丁内容。由于当时刚接触代码审计,技术水平有限(现在仍需提升),未能深入理解问题本质。不过,注意到修复后仍存在反序列化点,我开始思考是否存在绕过可能性,这为后续发现埋下了伏笔。

2025年漏洞补丁分析(漏洞初见端倪)

文件读取漏洞代码分析

// 完整代码见原文,此处展示关键部分
public void doAction(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    ObjectInputStream in = null;
    try {
        in = new FilteredObjectInputStream(request.getInputStream(), new Class[]{HashMap.class});
        HashMap<String, Object> headInfo = (HashMap)in.readObject();
        String dsName = (String)headInfo.get("dsName");
        InvocationInfoProxy.getInstance().setUserDataSource(dsName);
        String oper = (String)headInfo.get("operType");
        if ("upload".equals(oper)) {
            this.doUploadFile(headInfo, in, response);
        } else if ("download".equals(oper)) {
            this.doDownLoadFile(headInfo, response);
        } else if ("downloadlocal".equals(oper)) {
            this.doDownLoadFileLocal(headInfo, response);
        }
    } catch (Exception var10) {
        // 异常处理
    } finally {
        // 资源清理
    }
}

攻击向量:

  • 文件读取需要经过 readObject() 后才能触发
  • 在 HashMap 中构造 operType 参数
  • 进入 doDownLoadFileLocal 方法执行文件操作

修复代码分析

private void doDownLoadFileLocal(HashMap<String, Object> headInfo, HttpServletResponse response) {
    String path = (String)headInfo.get("path");
    OutputStream out = null;
    InputStream in = null;

    try {
        String ctxPath = RuntimeEnv.getInstance().getCanonicalNCHome();
        String realPath = ctxPath + File.separator + "mpEA" + File.separator;
        File file = new File(path);
        if (!file.getCanonicalPath().startsWith((new File(realPath)).getCanonicalPath())) {
            throw new IllegalArgumentException("Prohibit access to this folder");
        }

        // 文件操作代码...
    } catch (Exception var19) {
        // 异常处理
    } finally {
        // 资源清理
    }
}

修复措施:

  • 限制下载路径必须在 ${NCHome}/mpEA/ 目录下
  • 防止目录穿越攻击

绕过思路:
虽然目录穿越看似难以绕过,但注意到文件读取操作前存在 .readObject() 调用,且用友历史补丁质量普遍不高,这启发了反序列化绕过的尝试。

WAF 分析与绕过

FilteredObjectInputStream 分析

public class FilteredObjectInputStream extends ObjectInputStream {

    private boolean rootChecked = false;
    private boolean fullMatch = false;
    private Class<?>[] whiteList; 
    private static Set<String> blackList;

    // 初始化黑名单
    static{
        File blacks = new File(RuntimeEnv.getInstance().getNCHome(),
                "/ierp/security/unserializeBlacklist.conf");
        // 黑名单加载逻辑...
    }

    @Override
    protected Class<?> resolveClass(ObjectStreamClass arg0) throws IOException,
            ClassNotFoundException {
        Class<?> clazz = super.resolveClass(arg0);

        if(clazz == null) return clazz;
        // 非全匹配模式下的校验逻辑
        if(!fullMatch){
            // 黑名单校验
            if(blackList!=null&&blackList.size()>0&&blackList.contains(clazz.getName())){
                throw new IllegalArgumentException("####### class::" + clazz);
            }
            // 关键漏洞:rootChecked 为 true 时跳过后续校验
            if(rootChecked){
                return clazz;
            }
        }
        // 白名单校验逻辑
        if(whiteList!=null){
            for(Class white:whiteList){
                if(white.isAssignableFrom(clazz)){
                    rootChecked =true;  // 设置标志位
                    return clazz;       // 直接返回,结束校验
                }
            }
            throw new IllegalArgumentException("####### class::" + clazz);
        }
        return clazz;
    }
}

绕过原理

  1. 黑名单限制:黑名单在任何时候都会校验,但列表较短,主要针对 CC 链,容易绕过
  2. 白名单漏洞
    • 白名单仅校验第一个类
    • white.isAssignableFrom(clazz) 匹配成功时:
      • 设置 rootChecked = true
      • 直接返回,结束校验逻辑
    • 后续类的反序列化不再受限制

攻击链构造:

  • 使用 POJO Jackson 或其他可用链
  • 最后一个类使用 HashMap 通过白名单校验
  • 中间插入恶意反序列化载荷

漏洞验证

使用基于 POJO 改造的攻击链,确保最后一个类为 HashMap,成功实现命令执行,弹出计算器。

总结

通过分析用友系统的历史漏洞和补丁,发现了一个由于校验逻辑不完整导致的反序列化绕过漏洞。这个案例再次证明:

  1. 补丁质量至关重要:不完整的修复可能引入新的安全问题
  2. 代码审计需要系统性思维:需要全面理解整个校验流程
  3. 历史漏洞分析的价值:为发现新漏洞提供重要线索

个人感想: 虽然成功发现了漏洞,但对于未能及时公开利用感到遗憾。这次经历让我更加认识到漏洞挖掘的挑战和机遇。

评论留言请发表您的神机妙论……

昵称

邮箱

地址

私密评论
评论列表(共有0条评论)