之前写过jsp图片上传的程序,没有考虑它的安全性,很容易被文件上传攻击。我希望自己多点这方面的尝试,写出比较不容易被攻击的图片上传程序。
先简单写一个jsp图片上传程序,新建一个web工程,index.jsp为
<%-- Created by IntelliJ IDEA. User: vocus Date: 2020/4/14 Time: 17:02 To change this template use File | Settings | File Templates. --%> <%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <head> <title>上传图片测试</title> </head> <body> 选择要上传的图片: <br/> <form enctype="multipart/form-data" action="UploadImg" method="post"> <input type="file" name="uploaded"/> <br/> <br/> <input name="Upload" value="上传" type="submit"/> <% %> <div id="messageBox" style="color: red">${uploadMessage}</div> </form> </body> </html>
UploadImgServlet
//问题代码,包含文件上传漏洞,不要使用
public class UploadImgServlet extends HttpServlet { @Override protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding("utf-8"); ServletInputStream in=req.getInputStream(); byte[] bytes=in.readAllBytes(); in.close(); String str=new String(bytes); //获得MIME类型 int mimeStart=str.indexOf("Content-Type:")+"Content-Type:".length(); int mineEnd=str.indexOf("\n",mimeStart)-1; String mime=str.substring(mimeStart,mineEnd).replace(" ",""); //获得Boundary String contentType=req.getContentType(); int boundaryStart=contentType.indexOf("boundary=")+"boundary=".length(); String boundary="--"+contentType.substring(boundaryStart); //获得文件名 int filenameStart=str.indexOf("filename=")+"filename=".length(); int filenameEnd=str.indexOf("\n",filenameStart)-1; String filename=str.substring(filenameStart,filenameEnd).replace("\"",""); //文件保存路径 String savePath=req.getSession().getServletContext().getRealPath("/")+"../upload/"; //检查MIME类型 if(mime.equals("image/jpeg")){ //获得文件起始位置 int fileStart=mineEnd+1+1+1+1; int contentStart=str.substring(0,fileStart).getBytes().length; int fileEnd=str.indexOf(boundary,fileStart)-1; int contentEnd=str.substring(0,fileEnd).getBytes().length; int extraLen=str.substring(fileEnd+1,str.length()).getBytes().length; int contentLen=bytes.length-extraLen-fileStart; //写入文件 FileOutputStream out=new FileOutputStream(savePath+filename); out.write(bytes,contentStart,contentLen-1); out.close(); req.setAttribute("uploadMessage","文件上传成功!"); req.getRequestDispatcher("index.jsp").forward(req,resp); }else{ req.setAttribute("uploadMessage","上传文件类型错误,请上传jpg文件"); req.getRequestDispatcher("index.jsp").forward(req,resp); } } }
这个程序的问题在于,使用了MIME类型来判断用户上传的图片的类型,而MIME类型是可以被用户修改的。
这里简单做个测试:
然后找个简单的jsp一句话木马,保存为test.jsp
<% if(request.getParameter("f")!=null)(new java.io.FileOutputStream(application.getRealPath("/")+request.getParameter("f"))).write(request.getParameter("t").getBytes()); %>
(这段代码就是把request请求的两个参数f和t,f作为文件名,t作为f文件的内容,写入到图片上传目录)
回到文件上传的代码,之前只是判断了一下MIME类型,这个MIME类型是可以通过代理拦截修改的
通过代理拦截,test.jsp已经被上传至服务器目录
然后我们可以以例如http://192.168.149.130:8080/upload/test.jsp?f=1.txt&t=hello的方式提交请求,就将内容为hello的1.txt文件写入到目录
临时的解决办法是:在图片上传时把对MIME类型判断,变成判断文件后缀名为jpeg格式或其他图片格式
更规范的做法可以参考KindEditor的图片上传程序,以下是KindEditor的图片上传程序upload_json.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <%@ page import="java.util.*,java.io.*" %> <%@ page import="java.text.SimpleDateFormat" %> <%@ page import="org.apache.commons.fileupload.*" %> <%@ page import="org.apache.commons.fileupload.disk.*" %> <%@ page import="org.apache.commons.fileupload.servlet.*" %> <%@ page import="org.json.simple.*" %> <%@ page import="java.beans.Encoder" %> <% /** * KindEditor JSP * * 本JSP程序是演示程序,建议不要直接在实际项目中使用。 * 如果您确定直接使用本程序,使用之前请仔细确认相关安全设置。 * */ //文件保存目录路径 //String savePath = pageContext.getServletContext().getRealPath("/") + "attached/"; String savePath=request.getSession().getServletContext().getRealPath("/"); //String savePath = this.getClass().getResource("/").getPath().replaceAll("^\\/", "")+"kindeditor-4.1.11-zh-CN/kindeditor/attached"; //System.out.println(savePath); //文件保存目录URL String saveUrl =request.getSession().getServletContext().getRealPath("/");; //定义允许上传的文件扩展名 HashMap<String, String> extMap = new HashMap<String, String>(); extMap.put("image", "gif,jpg,jpeg,png,bmp"); extMap.put("flash", "swf,flv"); extMap.put("media", "swf,flv,mp3,wav,wma,wmv,mid,avi,mpg,asf,rm,rmvb"); extMap.put("file", "doc,docx,xls,xlsx,ppt,htm,html,txt,zip,rar,gz,bz2"); //最大文件大小 long maxSize = 1000000; response.setContentType("text/html; charset=UTF-8"); if(!ServletFileUpload.isMultipartContent(request)){ out.println(getError("请选择文件。")); return; } //检查目录 File uploadDir = new File(savePath); if(!uploadDir.isDirectory()){ out.println(getError("上传目录不存在。")); return; } //检查目录写权限 if(!uploadDir.canWrite()){ out.println(getError("上传目录没有写权限。")); return; } String dirName = request.getParameter("dir"); if (dirName == null) { dirName = "image"; } if(!extMap.containsKey(dirName)){ out.println(getError("目录名不正确。")); return; } //创建文件夹 savePath += dirName + "/"; saveUrl += dirName + "/"; File saveDirFile = new File(savePath); if (!saveDirFile.exists()) { saveDirFile.mkdirs(); } SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd"); String ymd = sdf.format(new Date()); savePath += ymd + "/"; saveUrl += ymd + "/"; File dirFile = new File(savePath); if (!dirFile.exists()) { dirFile.mkdirs(); } FileItemFactory factory = new DiskFileItemFactory(); ServletFileUpload upload = new ServletFileUpload(factory); upload.setHeaderEncoding("UTF-8"); List items = upload.parseRequest(request); Iterator itr = items.iterator(); while (itr.hasNext()) { FileItem item = (FileItem) itr.next(); String fileName = item.getName(); long fileSize = item.getSize(); if (!item.isFormField()) { //检查文件大小 if(item.getSize() > maxSize){ out.println(getError("上传文件大小超过限制。")); return; } //检查扩展名 String fileExt = fileName.substring(fileName.lastIndexOf(".") + 1).toLowerCase(); if(!Arrays.<String>asList(extMap.get(dirName).split(",")).contains(fileExt)){ out.println(getError("上传文件扩展名是不允许的扩展名。\n只允许" + extMap.get(dirName) + "格式。")); return; } SimpleDateFormat df = new SimpleDateFormat("yyyyMMddHHmmss"); String newFileName = df.format(new Date()) + "_" + new Random().nextInt(1000) + "." + fileExt; try{ File uploadedFile = new File(savePath, newFileName); item.write(uploadedFile); }catch(Exception e){ out.println(getError("上传文件失败。")); return; } JSONObject obj = new JSONObject(); // String saveUrlEncode="data:image/jpeg;base64,"+getBaseImg(saveUrl + newFileName); obj.put("error", 0); obj.put("url", saveUrlEncode); out.println(obj.toJSONString()); } } %> <%! private String getError(String message) { JSONObject obj = new JSONObject(); obj.put("error", 1); obj.put("message", message); return obj.toJSONString(); } %>
它对上传图片做了三个处理,一是检查文件大小,二是把上传文件名称改成上传时间,三是检查上传文件的后缀名。如果考虑到上传文件安全性,最重要的是第三点吧。