Java 安全研究初探

尝试卷入 Java 安全

前言

Java 安全,通常指代的是 Java Web 安全。在刚开始学习的一段时间里,面对众多的框架和名词,比如 Spring、Weblogic、EJB、AKB,等等,总感觉狗啃泰山无从下嘴。于是就有了这篇文章,旨在记录学习过程中遇到的所有问题,希望也能对像我一样的初学者有所帮助。

Java EE

搞过 Java Web 开发的多少会知道 JavaEE。从字面上来看,它的意思是 Java Platform, Enterprise Edition,即 Java 平台企业版。这个名称有够抽象,实际上 Java EE 包含了一系列标准,这些标准主要用来为企业级应用提供分布式计算、Web 服务等的统一定义和接口。另外,这些标准在设计之初就考虑了安全性、拓展性、并发等能力。

Java EE 的前身为 J2EE、后来捐献给 Eclipse 基金会,改名为 Jakarta EE。在不引起混淆的情况下,后文统一称为 Java EE。

Java EE 是在 Java SE(Standard Edition) 的基础上建立的,并增加了许多额外的特性和技术来方便构建高可用的企业级应用。Java 中的标准一般定义在 JSR 中,即 Java Specification Requests。例如,Java EE 所涉及的标准就在 JSRs by Platform - Java EE,其中包含了一些已经撤回的。

从这些提案(标准)中,我们大概可以知道 Java EE 所涵盖的一些方向,比如:

  • jsr-9: XML Parsing Specification, 定义了 Java 中对 XML 进行处理的接口 (JAXP);
  • jsr-19: Enterprise JavaBeans 2.0,即 EJB,提供了易管理的企业级组件定义和接口;
  • jsr-52: A Standard Tag Library for JavaServer PagesTM,定义了 JSP 中标签库的标准;
  • jsr-53: JavaTM Servlet 2.3 and JavaServer PagesTM 1.2 Specifications,定义了 Servlet 和 JSP 的标准;
  • jsr-367: JavaTM API for JSON Binding (JSON-B),定义了 Java 对象到 JSON 之间转换(序列化/反序列化) 的标准;

除此之外还有许多其他的定义,如 EJB,JSF (JavaServer Faces),CDI (Contexts and Dependency Injection),JavaMail 等,这里其实可以不用太过关注,在需要深入理解的时候再阅读其文档即可。

参考资料:

Servlet

Servlet 可以说是 Java Web 开发中基础的基础。可能和我差不多的中年人都会知道早期 Web 开发大多是基于 CGI 的,即写一个二进制程序或者 Perl 脚本去处理 Web 服务器的 HTTP 请求,请求的内容会封装到环境变量里。Sun 公司在 1996 年发布 Servlet 技术就是为了和 CGI 进行竞争,Servlet 这个名字是 Server Applet 的缩写,即服务端小程序。

从上面的介绍也可以看到,Servlet 本身也是一个 Java EE 的标准,比如 Java Servlet 3.1 就是其中一个版本。一般开发者可能很少需要直接阅读标准文档,且文档中也说明其目标主要是 Web 服务器的开发者。不过作为安全研究人员,我们可以从中学习到很多 Servlet 的基础架构和设计思想。

从实现上来说,Servlet 是一个实现了特定接口的 Web 组件,由 Servlet 容器去加载并运行。容器 本身并不一定是 Web 服务器,但容器需要至少支持 HTTP 请求,并将请求的内容封装成 Servlet 接口的参数;因此容器通常与 Web 服务器集成或者作为其拓展而存在。目前常见的 Servlet 容器如 Tomcat、GlassFish、JBoss 等,同样也具备 Web 服务器的功能。

根据文档所述,一个基础的 Servlet 接口如下所示:

package javax.servlet;
import java.io.IOException;

public interface Servlet {
    void init(ServletConfig var1) throws ServletException;
    ServletConfig getServletConfig();
    void service(ServletRequest var1, ServletResponse var2) throws ServletException, IOException;
    String getServletInfo();
    void destroy();
}

其中重要的接口是 service 方法,用于处理客户端的请求,并填充所需的返回结果。initdestory 是生命周期方法,在 Servlet 被加载和销毁时只执行一次。 getServletConfig 返回的对象是类似 name/value 格式的配置信息,由用户配置文件中进行传入。

为了方便开发者进行快速的原型开发,在 JDK 中已经实现了常用的 Servlet 功能,即 GenericServlet,而对于 HTTP 请求,又进一步实现了 HttpServlet 抽象类。这样,如果用户需要编写一个 Servlet 处理如 GET/POST 请求,只需要重写对应的方法即可。

使用 IDEA 自动创建的示例 Servlet 如下:

import java.io.*;
import javax.servlet.http.*;
import javax.servlet.annotation.*;

@WebServlet(name = "helloServlet", value = "/hello")
public class HelloServlet extends HttpServlet {
    private String message;

    public void init() {
        message = "Hello World!";
    }

    public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
        response.setContentType("text/html");

        // Hello
        PrintWriter out = response.getWriter();
        out.println("<html><body>");
        out.println("<h1>" + message + "</h1>");
        out.println("</body></html>");
    }

    public void destroy() {
    }
}

值得注意的是,正常情况下定义好 HelloServlet 类之后,容器并不知道这个类的存在,也不知道应该将什么 HTTP 路径映射到这个 Servlet,传统上需要用户自己修改 web.xml 配置文件(这也是 Servlet 标准的一部分),添加 <servlet><servlet-mapping> 标签来指定这些信息。在 Servlet 3.0 之后,就可以使用注解的方式配置 Servlet 了,如上面的 WebServlet 注解。

Servlet 中另一个常用的特性是 Filter,用于为所有 Servlet 添加全局性的鉴权和过滤,一个简单的示例如下:

import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import java.io.IOException;
import java.io.PrintWriter;

@WebFilter(filterName = "HelloFilter", urlPatterns = {"/*"})
public class HelloFilter implements Filter {

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {

    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        String passwd = servletRequest.getParameter("passwd");
        if (passwd != null && passwd.equals("123456")) {
            filterChain.doFilter(servletRequest, servletResponse);
        } else {
            try (PrintWriter out = servletResponse.getWriter()) {
                out.println("passwd error!");
            }
        }
    }

    @Override
    public void destroy() {

    }
}

其中同样使用了注解来添加,如果使用配置文件的方式,可以在 web.xml 中加上对应的 <filter><filter-mapping> 标签。这里有个比较值得注意的点事 urlPatterns 的匹配方式,一开始我以为是正则表达式,但其实在 Servlet 的标准中有明确的定义,比如优先进行精确匹配,否则使用最长的路径匹配,然后是后缀匹配等,规则定义在标准的 12 章 (Mapping Requests to Servlets) 中,可以配合容器的源码去进行分析。

另外 Servlet 标准中还支持注册 Listener 作为监听器,实现上下文、会话、请求的监听能力,主要用于线上人数的统计和会话的监控和处理。实现方法与 Servlet/Filter 大同小异,这里就不再举例了。

Servlet 的标准经过了多个版本的迭代,一些关键的版本和特性如下:

  • Java Servlet 2.0 (1997): JDK 1.1
  • Java Servlet 2.1 (1998): 第一个官方版本的标准,增加 RequestDispatcherServletContext
  • Java Servlet 2.2 (1999): 正式成为 J2EE 的一部分,引入 .war 独立 web 应用格式;
  • Java Servlet 2.3 (2001): 增加 Filter API,即上文介绍的过滤器;
  • Java Servlet 2.5 (2005): 要求 Java SE 5,开始支持注解;
  • Java Servlet 3.0 (2009): 改善注解处理,引入了 @WebServlet@WebFilter 等注解支持部分参数化;支持文件上传;
  • Java Servlet 3.1 (2013): 支持非阻塞 I/O,Websocket;
  • Java Servlet 4.0 (2017): 支持 HTTP/2;
  • Jakarta Servlet 4.0.3 (2019): 捐赠给 Eclipse 开源基金,重命名 Java 商标;
  • Jakarta Servlet 5.0 (2020): 包名从 javax.servlet 转为 jakarta.servlet
  • Jakarta Servlet 6.0 (2022): 增加了一些没人关心的特性;

完整的列表可以参考 Wikipedia 中的 Servlet API history 一节。

JSP

虽然我们可以用 Servlet 来生成网页中的动态内容,但这个过程相对繁琐。从上面的示例代码中可以看出,所有文本和标签都是硬编码,即使只做出微小的修改,也需要对源码进行重新编译和部署。JSP 解决了 Servlet 的这些问题,它是 Servlet 很好的补充,可以专门用作为用户呈现视图(View),而 Servlet 作为控制器(Controller)专门负责处理用户请求并转发或重定向到某个页面。

JSP 的全称是 Java Server Pages,包含一系列技术的组合,既表示动态网页的框架,也表示一种文件类型。JSP 的标准为 JSR 245,其中包含两个主要文档,分别是 JSP 的标准文档和 JSP EL 表达式的标准。

JSP 的基本语法可以总结如下:

  • <% code %> scriptlet 可以用来包裹和执行 Java 代码,也可以用 <jsp:scriptlet> 标签来进行包含;
  • <%! declaration; [ declaration; ]+ ... %> 用于变量声明,同 <jsp:declaration>
  • <%= expr %> 用来包括和执行表达式,表达式的结果作为 HTML 的内容,同 <jsp:expression>
  • <%-- comment --%> 为 JSP 注释,注释中的内容会被 JSP 引擎忽略;
  • <%@ directive attribute="value" %> 指令,影响对应 Servlet 的类结构,后面细说;
  • <jsp:action_name attribute="value" /> 使用 XML 控制 Servlet 引擎的的行为,称为 action;

完整的语法可以参考本节末尾的 JSP Tutorial 或者相关教程。

JSP 作为一个脚本引擎,其本质上也是通过 Servlet 实现的,即 JSP 会被容器编译为对应的 Servlet 并加载。例如,下述简单的 index.jsp 文件:

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

在被 Tomcat 容器加载后会生成如下的 Java 文件:

package org.apache.jsp;

import javax.servlet.*;
import javax.servlet.http.*;
import javax.servlet.jsp.*;

public final class index_jsp extends org.apache.jasper.runtime.HttpJspBase
    implements org.apache.jasper.runtime.JspSourceDependent,
                 org.apache.jasper.runtime.JspSourceImports {

  private static final javax.servlet.jsp.JspFactory _jspxFactory =
          javax.servlet.jsp.JspFactory.getDefaultFactory();

  private static java.util.Map<java.lang.String,java.lang.Long> _jspx_dependants;

  private static final java.util.Set<java.lang.String> _jspx_imports_packages;

  private static final java.util.Set<java.lang.String> _jspx_imports_classes;

  static {
    _jspx_imports_packages = new java.util.HashSet<>();
    _jspx_imports_packages.add("javax.servlet");
    _jspx_imports_packages.add("javax.servlet.http");
    _jspx_imports_packages.add("javax.servlet.jsp");
    _jspx_imports_classes = null;
  }

  private volatile javax.el.ExpressionFactory _el_expressionfactory;
  private volatile org.apache.tomcat.InstanceManager _jsp_instancemanager;

  public java.util.Map<java.lang.String,java.lang.Long> getDependants() {
    return _jspx_dependants;
  }

  public java.util.Set<java.lang.String> getPackageImports() {
    return _jspx_imports_packages;
  }

  public java.util.Set<java.lang.String> getClassImports() {
    return _jspx_imports_classes;
  }

  public javax.el.ExpressionFactory _jsp_getExpressionFactory() {
    if (_el_expressionfactory == null) {
      synchronized (this) {
        if (_el_expressionfactory == null) {
          _el_expressionfactory = _jspxFactory.getJspApplicationContext(getServletConfig().getServletContext()).getExpressionFactory();
        }
      }
    }
    return _el_expressionfactory;
  }

  public org.apache.tomcat.InstanceManager _jsp_getInstanceManager() {
    if (_jsp_instancemanager == null) {
      synchronized (this) {
        if (_jsp_instancemanager == null) {
          _jsp_instancemanager = org.apache.jasper.runtime.InstanceManagerFactory.getInstanceManager(getServletConfig());
        }
      }
    }
    return _jsp_instancemanager;
  }

  public void _jspInit() {
  }

  public void _jspDestroy() {
  }

  public void _jspService(final javax.servlet.http.HttpServletRequest request, final javax.servlet.http.HttpServletResponse response)
      throws java.io.IOException, javax.servlet.ServletException {

    if (!javax.servlet.DispatcherType.ERROR.equals(request.getDispatcherType())) {
      final java.lang.String _jspx_method = request.getMethod();
      if ("OPTIONS".equals(_jspx_method)) {
        response.setHeader("Allow","GET, HEAD, POST, OPTIONS");
        return;
      }
      if (!"GET".equals(_jspx_method) && !"POST".equals(_jspx_method) && !"HEAD".equals(_jspx_method)) {
        response.setHeader("Allow","GET, HEAD, POST, OPTIONS");
        response.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED, "JSPs only permit GET, POST or HEAD. Jasper also permits OPTIONS");
        return;
      }
    }

    final javax.servlet.jsp.PageContext pageContext;
    javax.servlet.http.HttpSession session = null;
    final javax.servlet.ServletContext application;
    final javax.servlet.ServletConfig config;
    javax.servlet.jsp.JspWriter out = null;
    final java.lang.Object page = this;
    javax.servlet.jsp.JspWriter _jspx_out = null;
    javax.servlet.jsp.PageContext _jspx_page_context = null;


    try {
      response.setContentType("text/html;charset=UTF-8");
      pageContext = _jspxFactory.getPageContext(this, request, response,
      			null, true, 8192, true);
      _jspx_page_context = pageContext;
      application = pageContext.getServletContext();
      config = pageContext.getServletConfig();
      session = pageContext.getSession();
      out = pageContext.getOut();
      _jspx_out = out;

      out.write("\n");
      out.write("<h1> config = ");
      out.print(config);
      out.write(" </h1>");
    } catch (java.lang.Throwable t) {
      if (!(t instanceof javax.servlet.jsp.SkipPageException)){
        out = _jspx_out;
        if (out != null && out.getBufferSize() != 0)
          try {
            if (response.isCommitted()) {
              out.flush();
            } else {
              out.clearBuffer();
            }
          } catch (java.io.IOException e) {}
        if (_jspx_page_context != null) _jspx_page_context.handlePageException(t);
        else throw new ServletException(t);
      }
    } finally {
      _jspxFactory.releasePageContext(_jspx_page_context);
    }
  }
}

代码有点长,其中有几个重点:

  • 生成的类名与 jsp 文件名相同,不合法的字符会转换为 _,比如 index.jsp 会生成 index_jsp1.jsp 会生成 _1_jsp
  • 生成的 Java 类继承自抽象类 HttpJspBase,这个类继承自 HttpServlet
  • _jspInit_jspDestory 对应 Servlet 中的生命周期函数;_jspService 中处理客户端的请求,JSP 中的 Java 代码也会转移到这里;

该类在第一次访问的时候才会生成、编译、加载,因此对于没访问过的页面是没有对应文件的。另外,在 _jspService 方法中为了方便 JSP 的代码编写,定义了几个常用的对象,比如:

  1. request,对应客户端请求的 HttpServletRequest 对象;
  2. response,对应返回的 HttpServletResponse 对象;
  3. session,对应 HttpSession,存储当前请求会话信息;
  4. application,对应 ServletContext,用于全局共享数据;
  5. config,对应 ServletConfig 即 Servlet 的配置对象;
  6. page,为 Object 类型,指向当前 Servlet 对象;
  7. out,即控制页面输出的 JspWriter
  8. pageContext,当前页面共享数据对象;

这几个对象通常称为 JSP 的 “八大对象”,或者 “九大对象” (算上 exception 对象)。

前面介绍 JSP 语法的时候提到过 directive 的语法,即 <%@ directive attribute="value" %>。在 JSP 中有三种指令类型,分别是:

  1. <%@ page ... %> 用于定义页面相关的属性,比如脚本语言类型、错误页面和缓存等;
  2. <%@ include ... %> 用语言在脚本翻译阶段包含其他文件;
  3. <%@ taglib ... %> 引入标签库的定义;

其中标签(tag)是指的是在 JSP 中一个可重用的动态组件,类似于 HTML 或者 XML 的标签,只不过多用于生成动态内容。taglib 就是一系列自定义标签的集合。JSP 支持让用户自定义标签,同时也提供了标准的标签库来方便开发,这个标准库就是 JSTL。JSTL 中常见的标签库有下面这些:

关于这些标签库提供的完整功能可以参考官方文档 JavaServer Pages Standard Tag Library 1.1 Tag Reference。一个使用核心标签库的示例 JSP 如下所示:

<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<html>
<head><title>JSTL c:out Example</title></head>
<body>
<h2> <c:out value="1+1=${1+1}"/> </h2>
</body>
</html>

c:out 的输出类似于 <%= 的输出,只不过前者是针对表达式,关于表达式在后文中会另外进行介绍。

注意在 Tomcat 或者 Jetty 这种轻量级的容器内使用 JSTL 需要安装对应的依赖,可以参考 JSTL - Stackoverflow 中的相关介绍。

虽然在前后端分离的开发浪潮中 JSP 似乎已经日渐式微,但在网站快速原型开发中 JSP 还占有一席之地,而且许多历史项目也是基于 JSP 进行开发,因此对其了解还是有必要的。

参考资料:

Java 表达式语言

上节中我们说到 JSP 的标准中包含了两份文档,一份是 JSP 语言本身的标准,另一份则是表达式语言的标准。Java 表达式语言即 Expression Language,简称为 EL,最初就是为了在 JSTL 中使用便捷的表达式解析而出现的。虽然 JSP 中可以写 Java 代码,但这一方面对不懂 Java 语言的人不太友好,另一方面很多时候我们仅需要在 JSP 中执行比较、循环、加减乘除等简单操作,而引入完整的 Java 引擎显得有点 overkill 了。

在最初的版本中,Java EL 被称为 SPEL(Simplest Possible Expression Language) 或者 JUEL(Java Unified Expression Language),注意不要和 Spring 的 SpEL(Spring Expression Language) 搞混。EL 的语法被设计得和 javascript 类似,比如:

  • 无法强制类型转换,转换过程是隐式的;
  • 单引号和双引号等价;
  • object.propertyobject['property'] 实现的效果基本相同;
  • 抽象属性的类型,object.property 根据属性的类型不同,实际上可能会调用 object.get("property")object.getProperty("property") 或者 object.getProperty() 等;

EL 的标准也经过了多个版本的迭代,在 EL 2.1 版本时,为了综合考虑 JSF 等技术的需要,对 EL 的功能进行了拓展,比如增加了延迟表达式(Deferred expressions),使得表达式不用在 JSP 翻译的过程就被执行;另外提供了 get/set 去获取/修改对象属性的能力,以及方法表达式支持调用 Java 对象的方法。

EL 3.0 中正式将 EL 的标准从 JSP/JSF 的标准中独立出来,编号为 JSR-341。这个版本中在 EL 中增加了类似 Java8 Stream 和 lambda 表达式的功能,几个简单的示例如下:

public class ELTestServlet extends HttpServlet {
    @Override
    public void doGet(HttpServletRequest req, HttpServletResponse res)
            throws ServletException, IOException {
        
        List list = new ArrayList<>();
        list.add(Double.valueOf(1.0));
        list.add(Double.valueOf(2.0));
        list.add(Double.valueOf(3.0));

        ELProcessor elp = new ELProcessor();
        elp.defineBean("data", list);    

        Object message = (Double)elp.eval(
            "n = data.stream().count(); s = data.stream().sum(); sq = data.stream().map(i -> i*i).sum(); Math.sqrt(sq/n - Math.pow(s/n, 2))");
        res.getWriter().println(message);

        message = (Double)elp.eval(
            "n = 0; s = 0; sq = 0; data.stream().forEach(d -> (n = n + 1; s = s + d; sq = sq + d*d)); Math.sqrt(sq/n - Math.pow(s/n, 2))");
        res.getWriter().println(message);

        message = (Double)elp.eval(
            "n = 0; s = 0; sq = data.stream().reduce(0, (a, i) -> (n = n + 1; s = s + i; a + i*i)); Math.sqrt(sq/n - Math.pow(s/n, 2))"); 
        res.getWriter().println(message);
    }
}

目前最新的 EL 标准是 2020 年 4.0 版本,这个版本中将包名从 javax.el 移到了 jakarta.el。除了 Java EE 中定义的 EL 表达式标准和实现,在许多三方库中也实现了类似的表达式语言功能,包括但不限于:

  • OGNL: Object-Graph Navigation Language,一个开源的表达式语言实现,在许多其他项目中被使用,比如 Struts2、Spring Web Flow、MyBatis、FreeMarker 等;
  • MVEL: MVFLEX Expression Language,另一个开源的表达式语言实现,一些知名的 Java 项目使用其作为表达式引擎,比如 JBoss Drools、Hibernate ORM、JIRA 等;
  • SpEL: Spring 框架中使用的表达式语言,除了 Spring 宇宙中的组件,还有其他 Java 项目也是用了该引擎,比如 Thymeleaf、Apache Camel、Apache Sling 等;
  • JEXL: 即 Apache Commons JEXL(Java EXpression Language),参考 Java EL 和 Apache Velocity 实现的一个表达式语言,语法接近于 JavaScript 和 Shell 脚本;

这类表达式语言通常作为模版语言在 Web 页面中使用,如果其中的表达式能够被恶意输入所控制,就可能出现任意代码执行的危害。历史上出现过许多由于 EL 表达式注入导致的远程代码执行,因此这算是 Java 中高危的攻击面之一。

JDBC

在传统 MVC 架构中,将内容呈现的过程分成模型(Model),视图(View),控制器(Controller)三个部分,现在我们已经介绍了控制器部分的 Servlet,视图层的 JSP,为了使得一个网页能够真正有价值,还缺少其中关键的模型部分,即数据来源。

JDBC (Java Database Connectivity) 就是 Java 访问数据库的一个标准。历史上在 JDK 1.1 中 JDBC 就已经是其中的一部分,也就是说 JDBC 本身也是 Java SE 的标准,但也包含在 Java EE 中。其包名可通过 java.sql 或者 javax.sql 进行访问。

JDBC 标准现在由 JCP 维护,主要经过以下几个版本的迭代:

不同版本的 JDBC 主要引进了一些新的特性,比如 JDBC 4.0 支持自动加载数据库驱动,4.1 支持 try-with-resource,大对象(LOB)更新等;主要的框架并没有太大变化。

JDBC API 的常见用法如下:

 import java.sql.*;

 public class TestJdbc {
   public static void main(String[] args) throws Exception {
     // Class.forName("com.mysql.jdbc.Driver");

     // 1. Establishing the connection with the database
     Connection conn = DriverManager.getConnection(
       "jdbc:mysql://localhost:3306/mydb?useSSL=false", "root", "rxhtemp");

     // 2. Creating a statement object to execute a query
     Statement stmt = conn.createStatement();

     // 3. Executing the query and getting the ResultSet
     ResultSet rs = stmt.executeQuery("SELECT * FROM user");

     // 4. Processing the ResultSet
     while (rs.next()) {
       int id = rs.getInt("id");
       String name = rs.getString("name");
       System.out.println("ID: " + id + ", Name: " + name);
     }

     // 5. Closing the database resources
     rs.close();
     stmt.close();
     conn.close();
   }
 }

Class.forName 显式导入驱动的方式在新版本已经不需要了,不过这个方式有利于让开发者理解 JDBC 使用的具体数据库驱动,一些例子如下:

  • PostgreSQL : org.postgresql.Driver
  • Oracle : oracle.jdbc.driver.OracleDriver
  • Microsoft SQL Server : com.microsoft.sqlserver.jdbc.SQLServerDriver
  • SQLite : org.sqlite.JDBC
  • H2 : org.h2.Driver
  • Apache Derby : org.apache.derby.jdbc.ClientDriver
  • MariaDB : org.mariadb.jdbc.Driver
  • IBM DB2 : com.ibm.db2.jcc.DB2Driver
  • Informix : com.informix.jdbc.IfxDriver

如果想实现自己的驱动,需要实现 java.sql.Driver 接口,其中一些关键方法有 connectacceptsURL 等。connect 方法中会接受到传入的 URL 以及相关属性(如账号、密码)。实际上 DriverManager 查询驱动的方式也很简单,即通过循环遍历已注册的驱动 registeredDrivers,然后逐个调用其 driver.connect 方法,如果发现不为空就返回。registeredDrivers 是个列表结构,因此先添加的驱动会优先执行。

另外如果驱动中实现的有问题,且攻击者可以污染传入的 URL 或者 Property,也有可能间接造成代码执行,关于这类攻击可以参考 New Exploit Technique In Java Deserialization Attack

JMX

JMX 全称为 Java Management Extensions,即 Java 管理拓展,主要用于管理和监控 Java 程序。常见的监控资源有 CPU 占用率、线程数、JVM 内存等。下面是一些相关标准的定义:

我们常用的 JConsole、VisualVM,以及现在流行的 Spring Boot 框架中的 Spring Boot Actuator 内部都使用了 JMX 拓展,因此对其进行了解有助于我们更好地认识这些应用和框架。

JMX 主要包括几个关键组件,下面分别进行介绍。

MBean,全称是 Managed Bean,类似于 Java Bean,主要用来进行消息传递。与 Java Bean 的区别是 JMX 中对这些 Bean 进行了额外的定义,兼容 JMX 标准的 MBean 才可以被加载。在 java.lang.management 中定义了许多 JDK 提供的 MBean,包括 Memory、Thread 相关的 MBean,用于传递 JVM 应用的内存和线程相关信息。我们平时使用 jconsole 去分析 Java 应用时所展现的数据大多来源于这些 MBean。

MBeanServer,主要用于 MBean 的管理,同名接口定义在 javax.management 中,包含 MBean 创建、注册以及删除等操作相关的接口。一般用户不会直接实现该接口,而是通过 MBeanServerFactory 工厂类去获取 MBeanServer 实例。通常一个 Java 虚拟机中只会有一个 MBeanServer,只有注册到 MBeanServer 中的 MBean 才能被进行管理,即能够被连接到 MBeanServer 的客户端远程访问其属性和相关方法。

Connector/Adaptor,这是直接面对客户端的组件,负责具体协议的连接或者转换。在 JMX 标准中提到,RMI Connector 是在 JMX 标准实现中唯一强制实现的协议。RMI (Remove Method Invocation) 是 Java 中的远程调用接口,其具体的传输协议可以不同,常见有以下两种:

  1. JRMP (Java Remote Method Protocol),这是 RMI 的默认传输协议,专为 Java 而开发,因此性能较好;
  2. IIOP (Internet Inter-ORB Protocol),CORBA 标准的一部分,依赖于 CORBA 类库,因此可以实现多语言传输 RMI 远程对象,更适用于跨平台开发;

除了 RMI Connector,JMX 中还定义了 Generic Connector 即通用连接器的标准。基于上文 JSR 160 提到的 JMX Remote API 来实现可配置和可插拔的连接器,包括传输协议和对象封装等接口。JMXMP,即 JMX Remote API over Message Service Protocol,就是一种通用连接器实现。客户端和服务器之间通过消息进行通信,消息可以是纯文本或二进制格式。JMXMP 支持基于 SSL/TLS 对连接进行加密,同时还有连接池、心跳等功能。

Adaptor 称为适配器,和连接器类似,主要将客户端对服务器中 MBean 的操作适配为其他协议,比如 SNMP 或者 HTTP 等。例如,Jolokia 就是一个常用的开源 JMX-HTTP 适配器,可以使远程的 JMX 客户端通过 HTTP/HTTPS 协议访问 JMX MBean。

假设我们是某个服务端应用,需要对外暴露一些监控信息,这种情况下就可以自定义一个 MBean 实现。JMX 要求 MBean 首先需要是一个接口,且接口名字以 MBean 结尾,例如:

public interface DemoMBean {
    String getName();
    String getPassword();
    void say(String what);
}

一个具体的 MBean 需要实现上述接口:

class Demo implements DemoMBean {
    @Override
    public String getName() { return "evilpan"; }
    @Override
    public String getPassword() { return "jmxdemo"; }
    @Override
    public void say(String what) {
        System.out.println(getName() + " say: " + what);
    }
}

为了能够被客户端访问,需要将该 MBean 的实例注册到 MBeanServer:

MBeanServer server = ManagementFactory.getPlatformMBeanServer();
ObjectName userName = new ObjectName("com.evilpan:type=Foo,name=bar");
server.registerMBean(new Demo(), userName);

其中 userName 是在 JMX MBean 中显示的名称,上述示例在 jconsole 的 MBeans 中查看有以下结构:

 com.evilpan
 └─ Foo
    └─ bar
       ├─ Attributes
       │  ├─ Password
       │  └─ Name
       └─ Operations
          └─ say

点击 Attributes 中的 NamePassword 会调用对应的 getNamegetPassword 方法来获取信息,而点击 say 可以调用对应 MBean 实例的方法,并指定相关参数。

这里是使用 jconsole attach 本地应用的方式连接 MBeanServer 的,实际应用中可以通过 RMI 协议去让客户端进行远程管理,关键代码如下:

MBeanServer server = ManagementFactory.getPlatformMBeanServer();
// Register MBean ...
LocateRegistry.createRegistry(7890);
JMXServiceURL url = new JMXServiceURL("service:jmx:rmi:///jndi/rmi://localhost:7890/jmxrmi");
JMXConnectorServer jcs = JMXConnectorServerFactory.newJMXConnectorServer(url, null, server);
jcs.start();

当然,实际上 Java 本身也提供了内置的 JMX 连接器功能,可以通过命令行启动指定属性去开启,比如:

java -Dcom.sun.management.jmxremote.port=7890 \
  -Dcom.sun.management.jmxremote.authenticate=false \
  -Dcom.sun.management.jmxremote.ssl=false \
  -cp target/classes com.example.demo.BeanTest

和上面自行添加 RMI 监听效果是一样的。 其中 RMI registry 的端口为 7890,JMXServiceURL 路径的结尾可以随意指定,但如果需要用 jconsole来进行连接,则必须使用 /jmxrmi

实际应用中更多使用 -Dcom.sun.management.config.file 指定 JMX 相关的配置

应用启动后也可以通过 nmap 来查看对应端口信息:

$ nmap localhost -p 7890,56097 -sVC
PORT     STATE SERVICE  VERSION
7890/tcp open  java-rmi Java RMI
| rmi-dumpregistry:
|   jmxrmi
|     javax.management.remote.rmi.RMIServerImpl_Stub
|     @127.0.0.1:56097
|     extends
|       java.rmi.server.RemoteStub
|       extends
|_        java.rmi.server.RemoteObject
56097/tcp open  java-rmi Java RMI

上面监听的 56097 端口是实际的 JMX-RMI Connector 监听端口,被注册为 RMI Nameing Registry 中的 jmxrmi 名称。

从上面的介绍中可以看出,JMX 在 Java 应用管理和监控中的作用很大,但同时也暴露了很大的风险,如果服务端的 JMX 管理端口能被攻击者访问,就可能泄露敏感数据,或者被调用任意的 MBean 方法从而导致代码执行等危害。另外由于 RMI 是 JMX 默认的连接器,因此历史上也出现过许多针对 RMI 的攻击和反序列化漏洞,这里就先不展开了,其中相关的利用技巧和深入介绍可以参考下面的文章:

后记

写到这里,可以说对 Java 安全有了一个比较基本的了解。Java 生态历经了几十年的发展,直到今天依旧在企业级服务端应用中占有半壁江山。从文中可以看到 Java 一直致力于提供高可用、可拓展的标准,有一些标准由于设计复杂接受度不高(如 EJB),但也有很多标准从初稿到现在依然是主流(如 JNDI、JMX)。文中所介绍的几个功能应该算是 Java 初学者最先接触的技术,虽然只是冰山一角,但通过对这些技术的学习可以让我们管窥 Java EE 的全貌,也为后续针对性的研究打好基础。


版权声明: 自由转载-非商用-非衍生-保持署名 (CC 4.0 BY-SA)
原文地址: https://evilpan.com/2023/04/01/java-ee/
微信订阅: 有价值炮灰
TO BE CONTINUED.