【实战】某友文件上传漏洞分析

admin 2024年5月29日14:56:18评论16 views字数 6403阅读21分20秒阅读模式

某友,不(敢)多说。本文对前段时间热传的文件上传漏洞的成因做简单的分析(要是没看过,当我没说)。

代码分析

常规操作 把源码导入idea 将对应lib下的jar文件添加为库

【实战】某友文件上传漏洞分析

简单过一下web.xml,发现只有一个filter,仅对编码做了相对应的处理。‍‍‍‍‍

【实战】某友文件上传漏洞分析

servlet部分也着实有些单调了,简单目测仅有log4j(引用配置文件),springboot(引用配置文件)和pushlet(某种基于 HTTP 协议的轻量级服务器推送技术)。‍
那就简单了,简单粗暴直接进spring-mvc.xml去看。
先找一下有没有关于拦截部分的代码,ctrl+f搜索‍‍‍interceptors。
可惜(T_T)!还真有~

【实战】某友文件上传漏洞分析

那就老规矩,简单过亿下主要代码吧(T_T)
public class LoginInterceptor extends HandlerInterceptorAdapter {    private static final Logger logger = LoggerFactory.getLogger(LoginInterceptor.class);    private static final String[] IGNORE_URI = new String[]{"/initcfg", "/login", "/index", "/automsg", "/loginxietong", "/loginportal", "/signportal", "/loginportaldetail", "/loginportalmobile"};    public LoginInterceptor() {    }    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {        // 获取当前会话的 HttpSession 对象        HttpSession session = request.getSession();        // 获取名为 "user_logined" 的属性值,用于判断用户是否已登录        Object logined = session.getAttribute("user_logined");        // 获取当前请求的 URL 地址        String url = request.getRequestURL().toString();        // 如果当前请求的 URL 不以 "mp/" 或 "mp" 结尾,即不是请求登录页或首页,则执行下面的逻辑        if (!url.endsWith("mp/") && !url.endsWith("mp")) {                // 如果当前请求的 URL 是以 "index.html" 结尾的,则执行以下逻辑                if (url.endsWith("index.html")) {                        // 如果用户未登录,则重定向到登录页,并返回 false                        if (logined == null) {                                response.sendRedirect(request.getContextPath() + "/login.html");                                return false;                        } else {                                // 如果用户已登录,则返回 true,允许请求继续执行                                return true;                        }                } else if (!(handler instanceof HandlerMethod)) {                        // 如果处理器不是 HandlerMethod 类型的实例(即不是 Controller 方法),则直接返回 true,允许请求继续执行                        return true;                } else {                        // 否则,即为普通请求,则执行以下逻辑                        HandlerMethod handler2 = (HandlerMethod)handler;                        // 获取处理器方法上的 ResponseBody 注解                        ResponseBody json = (ResponseBody)handler2.getMethodAnnotation(ResponseBody.class);                        String msg = "";                        boolean flag = false;                        // 记录请求的 URL 地址                        logger.info(">>>: " + url);                        // 遍历忽略的 URI 列表,判断当前请求的 URL 是否包含在忽略列表中                        for (String s : IGNORE_URI) {                                if (url.contains(s)) {                                        flag = true;                                        break;                                }                        }                        // 如果当前请求的 URL 不在忽略列表中,则执行以下逻辑                        if (!flag) {                                // 进行自动登录,并返回相关提示信息                                msg = this.autoLogin(request);                                if (logined == null) {                                        // 如果用户未登录,则返回错误信息或进行重定向                                        msg = StringUtils.isNotEmpty(msg) ? msg : "<a href='/mp/logout' class='tips_login'>您未登录或长时间未操作,请先登录<a/>";                                        if (null == json) {                                                if (url.indexOf("indexforportal") >= 0) {                                                        response.sendRedirect("/mp?indexforportallogin=portallogin");                                                } else {                                                        response.sendRedirect("/mp");                                                }                                        } else {                                                MPUtils.sendResponseMsg(response, false, msg);                                        }                                } else {                                        // 如果用户已登录,则设置 flag 为 true                                        flag = true;                                }                        }                        // 根据 flag 的值决定是否允许请求继续执行                        return flag;                }        } else if (logined == null) {                // 如果当前请求的 URL 以 "mp/" 或 "mp" 结尾,并且用户未登录,则重定向到登录页,并返回 false                response.sendRedirect(request.getContextPath() + "/login.html");                return false;        } else {                // 如果当前请求的 URL 以 "mp/" 或 "mp" 结尾,并且用户已登录,则返回 true,允许请求继续执行                return true;        }     }

到这里就简单明了了,只需要让URL包含IGNORE_URI中的任意URL即可绕过权限认证。就是这么简单(bushi)!‍

再让我们回到spring-mvc.xml

【实战】某友文件上传漏洞分析

OK,获得Controller控制的位置,com.yonyou.uap.mp,那就直接跳过去,收获了若干Controller控制器!Easy(zhende)!

【实战】某友文件上传漏洞分析

既然是分析文件上传,别的咱就不看了,直接进UploadFileServlet!

【实战】某友文件上传漏洞分析

// 将该类声明为Spring MVC控制器@Controller  // 指定该控制器处理请求的基础URL路径为"/uploadControl"@RequestMapping({"/uploadControl"})  // 定义名为UploadFileServlet的Servlet类,继承自HttpServlet类public class UploadFileServlet extends HttpServlet {      // 序列化版本UID    private static final long serialVersionUID = 1L;      // 创建Logger对象用于记录日志    private static Logger logger = LoggerFactory.getLogger(UploadFileServlet.class);      // 自动注入IMSGService对象    @Autowired      // 用于处理消息的服务接口    private IMSGService msgService;      public UploadFileServlet() {    }    // 声明处理请求的方法    @RequestMapping(          // 指定处理请求的URL路径为"/uploadControl/uploadFile"        value = {"uploadFile"},          // 限定请求方法为POST        method = {RequestMethod.POST}      )    protected void doPost(HttpServletRequest request, HttpServletResponse response, String pluginCode, String pk_message) throws ServletException, IOException {        // 设置响应字符编码为UTF-8        response.setCharacterEncoding("UTF-8");          // 设置响应内容类型为HTML,字符编码为UTF-8        response.setContentType("text/html;charset=utf-8");          // 获取响应输出流        PrintWriter writer = response.getWriter();          // 设置请求字符编码为UTF-8        request.setCharacterEncoding("utf-8");          // 设置消息类型为TASK        String msgtype = "TASK";          // 初始化文件名为空字符串        String file = "";          // 初始化返回信息为空字符串        String returnInfo = "";          // 声明BufferedReader对象        BufferedReader bf = null;          // 检查请求是否是multipart类型的表单数据        boolean isMultipart = ServletFileUpload.isMultipartContent(request);          // 如果是multipart类型的表单数据        if (isMultipart) {              // 创建DiskFileItemFactory对象            DiskFileItemFactory factory = new DiskFileItemFactory();              // 创建ServletFileUpload对象            ServletFileUpload upload = new ServletFileUpload(factory);              // 设置请求头编码为UTF-8            upload.setHeaderEncoding("UTF-8");              try {                // 解析请求,获取表单项迭代器                Iterator items = upload.parseRequest(request).iterator();                  // 遍历表单项                while(items.hasNext()) {                      // 获取表单项                    FileItem item = (FileItem)items.next();                      // 如果不是普通表单项(即文件项)                    if (!item.isFormField()) {                          // 计算文件大小(MB)                        long filesize_m = item.getSize() / 1024L / 1024L;                          // 如果文件大小超过32MB                        if (filesize_m > 32L) {                              // 返回文件大小超限的错误信息                            writer.println("{"forbidden":true,"msg":"文件大小不能超过32M!"}");                              // 结束方法执行                            return;                          }                        // 获取文件名                        String fileName = item.getName();                          // 截取文件名                        fileName = fileName.substring(fileName.lastIndexOf("\") + 1);                          // 获取应用程序的上下文路径                        String contextPath = MPUtils.getContextRootPath(request.getSession().getServletContext());                          // 设置文件上传目录                        String staticUploadFileDir = contextPath + "uploadFileDir";                          // 如果上传目录不存在                        if (!(new File(staticUploadFileDir)).isDirectory()) {                              // 创建上传目录                            (new File(staticUploadFileDir)).mkdir();                          }                        // 构建文件路径                        String path = staticUploadFileDir + File.separator + fileName;                          // 创建文件对象                        File uploaderFile = new File(path);                          // 写入文件                        item.write(uploaderFile);                          // 调用消息服务接口上传文件                        this.msgService.uploadFile(pluginCode, msgtype, pk_message, fileName, uploaderFile);                          // 删除上传的临时文件                        uploaderFile.delete();                      }                }                // 返回文件上传成功的信息                writer.println("{"forbidden":false}");              } catch (Exception var25) {  // 捕获异常                // 打印异常堆栈信息                var25.printStackTrace();                  // 返回异常信息                writer.println("{"forbidden":true,"msg":"" + var25.getMessage() + ""}");              } finally {                // 关闭输出流                writer.close();              }        }    }}

平平无奇,毫无新意,敢敢单单!‍‍‍‍‍‍‍

【实战】某友文件上传漏洞分析

看到这里就有人问了,明明有uploaderFile.delete(); 怎么没有删除呢。

【实战】某友文件上传漏洞分析

既然没有执行uploaderFile.delete(); 还产生了报错,那问题就一定是出在了this.msgService.uploadFile(pluginCode, msgtype, pk_message, fileName, uploaderFile);

废话不多说!上代码‍

public String uploadFile(String pluginCode, String msgtype, String pk_message, String fileName, File file) {    // 定义一个 SourceSystemVO 类型的变量 systemvo,并初始化为 null    SourceSystemVO systemvo = null;    // 定义一个 String 类型的变量 ret,并初始化为空字符串    String ret = "";    // 检查 isUserDB 的值是否为 false    if (!this.isUserDB) {        // 如果 isUserDB 为 false,从 XML 中获取 systemvo 对象        systemvo = this.sourceSystemServiceForXML.getSystemVOByCodeFromXML(pluginCode);    } else {        // 如果 isUserDB 为 true,从数据库中获取 systemvo 对象        systemvo = this.sourceSystemService.getSystemVOByCode(pluginCode);    }    // 根据 systemvo 获取对应的插件对象    IMessagePlugin plugin = this.getPluginByCode(systemvo);    // 检查插件对象是否不为 null    if (null != plugin) {        // 调用插件的 uploadAttachment 方法上传附件,并将返回值赋给 ret        ret = plugin.uploadAttachment(systemvo, msgtype, pk_message, fileName, file);    }    // 返回结果    return ret;}

    好家伙,破案了,原来pluginCode参数还是个必要参数,输入的数据包没设置pluginCode参数,导致无法生成systemvo,无奈跳出了之前try进入到catch中 ,文件传上去却删不掉,可惜可惜。

【实战】某友文件上传漏洞分析

【实战】某友文件上传漏洞分析
【实战】某友文件上传漏洞分析
复现过程

那就直接进入受害者环节吧(哪有什么受害者,终究是自我安慰罢了T_T)!

【实战】某友文件上传漏洞分析

【实战】某友文件上传漏洞分析

复现结束!‍‍‍

【实战】某友文件上传漏洞分析

原文始发于微信公众号(悦海数安):【实战】某友文件上传漏洞分析

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2024年5月29日14:56:18
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   【实战】某友文件上传漏洞分析http://cn-sec.com/archives/2791015.html

发表评论

匿名网友 填写信息