目录

Java-Web安全之代码审计

Java Web安全之代码审计

信息安全的75%发生在Web应用而非网络层。本文内容主要以Java Web安全-代码审计为中心展开。

通俗的说Java代码审计就是通过审计Java代码来发现Java应用程序自身中存在的安全问题,由于Java本身是编译型语言,所以即便只有class文件的情况下我们依然可以对Java代码进行审计。对于未编译的Java源代码文件我们可以直接阅读其源码,而对于已编译的class或者jar文件我们就需要进行反编译了。

Java代码审计其本身并无多大难度,只要熟练掌握审计流程和常见的漏洞审计技巧就可比较轻松的完成代码审计工作了。但是Java代码审计的方式绝不仅仅是使用某款审计工具扫描一下整个Java项目代码就可以完事了,一些业务逻辑和程序架构复杂的系统代码审计就非常需要审计者掌握一定的Java基础并具有具有一定的审计经验、技巧甚至是对Java架构有较深入的理解和实践才能更加深入的发现安全问题。

本文将分为多章节来讲述Java代码审计需要掌握的前置知识以及Java代码审计的流程、技巧。

在开始Java代码审计前请自行安装好Java开发环境,建议使用MacOS、Ubuntu操作系统。

所谓“工欲善其事,必先利其器”,合理的使用一些辅助工具可以极大的提供我们的代码审计的效率和质量!

强烈推荐下列辅助工具:

1.Jetbrains IDEA(IDE)

2.Sublime text(文本编辑器)

3.JD-GUI(反编译)

4.Fernflower(反编译)

5.Bytecode-Viewer

6.Eclipse(IDE)

7.NetBeans(IDE)

https://i-blog.csdnimg.cn/blog_migrate/9baad07b9a36ab6422e16df498b7b293.png

在渗透测试的时候需要审计的代码通常是class文件或者jar包,那么我们应该如何审计呢?让我们先来学习一下什么是Java源码和字节码。

简单的说Java源码就是未经编译的.java文件,我们可以很轻松的阅读其中的代码逻辑,而字节码.class文件则是.java文件经过编译之后产生的字节码文件,因为.class文件是编译后的二进制文件所以我们是无法直接阅读的,只能通过反编译工具将二进制文件转换成java代码或者ASM代码。

示例代码Test.java:

/**
 * @author yz
 */
public class Test {

    public static void hello() {
    	System.out.println("Hello~");
    }

    public void world() {
    	System.out.println("World!");
    }

    public static void main(String[] args) {
    	hello();
    }

}

Test.java 编译执行流程:

https://i-blog.csdnimg.cn/blog_migrate/bb593e26846d38e502e97f808fb5e7d7.png

Test.java 源码、字节码

https://i-blog.csdnimg.cn/blog_migrate/ec8cef3a459eae530be8e68d4997e1bf.png

由于 class 文件的可读性较差,通常我们需要使用 Java 反编译工具来反编译代码。我们通常会使用到 JD-GUI、IDEA Fernflower 插件、Bytecode-Viewer、Fernflower、JAD、JBE、JEB 等工具来反编译 class。

其中 JD-GUI 可能是目前反编译中使用的最多的工具了,但是个人觉得 JD-GUI 的反编译能力远不如经过 IDEA(IDEA 应该是使用的改版后的 Fernflower),因为 IDEA 默认支持对 jar 和 class 的反编译,所以我个人强烈推荐使用 IDEA 来反编译 class 代码。

当然,反编译工具很多时候也不是万能的,JD-GUI 经常遇到无法反编译或反编译过程中程序直接崩溃的情况,遇到这类情况我们通常可以使用 IDEA 反编译试试,如果 IDEA 也无法反编译可以使用 JBE 来加载 class 文件读取程序的字节码,如果 JBE 仍无法读取类信息还可以使用 JDK 自带的 javap 命令来读取 class 类字节码,如果上诉所有的方法都无法反编译,那么恐怕是这个类本身就存在无法编译问题要么可能就是类文件被加密处理过。可能你会说 java 编译的 class 不是说不可以加密吗?没错,这里所说的加密其实是为了保护编译后的 class 代码不可反编译,通过实现自定义 ClassLoader 来 loadClass 加密后的类方式而已,这种加密方式曾在实战中也有遇到。

通常我们在某些特殊的场景下拿到的只是 jar 文件,那么我们应该如何反编译整个 jar 包的 class 文件呢?

2.1. Fernflower

Fernflower 可以很轻松的实现 jar 的完整反编译,执行如下命令即可: java -jar fernflower.jar jarToDecompile.jar decomp/ 其中 jarToDecompile.jar 是需要反编译的 jar 文件,decomp 是反编译后的 class 文件所存放的目录。需要注意的是 Fernflower 如遇无法反编译的情况可能会生成空的 java 文件!

2.2. JD-GUI

JD-GUI 是一个带 GUI 的反编译工具,在 JD-GUI 的菜单中点击 File–>Save All Sources 即可反编译 jar。

2.3. IDEA

IDEA 默认就支持 jar 包反编译,同时还支持 class 文件名(⇧⌘F)、类方法名称(⇧⌘O)搜索。

2.4. Bytecode-Viewer

FernFlower 提供了 GUI 版本 Bytecode-Viewer,Bytecode-Viewer 提供了直接反编译的 class、jar、zip、apk、dex 功能,直接拖拽 jar 就可以直接对整个 jar 进行反编译了。

https://i-blog.csdnimg.cn/blog_migrate/11ddaa2e711563743e43a90b4f333a09.png

2.5. Find 命令

find 命令并不能支持 Java 反编译,但是 find 命令可以非常方便的搜索经过编译后的二进制文件中的内容,所以有的时候使用 find 命令通常是最简单实用的,直接解压 jar 包然后使用 find 命令搜索: find ./ -type f -name “_.class” |xargs grep XXXX 即可搞定。

2.6 使用 Find 命令和 Fernflower 实现批量反编译 jar

当我们只有项目 war 包且源码经过打包后发布到 WEB-INF/lib 的情况下,我们不得不去找出待审计源码的具体 jar 文件并反编译。遇到这种情况我们可以巧妙的使用 find 命令来反编译所有目标的 jar 包。

这里以 jcms 的一个非常老版本为例,jcms 最终给客户部署的 war 包中源码并不是在 WEB-INF/classes 目录下,而是将整个 jcms 系统按模块打包成了多个 jar 包放在了 WEB-INF/lib 目录下。我们可以通过搜索 com.hanweb 包名称来找出所有 jar 中包含了 jcms 的文件并通过 Fernflower 来反编译。

java -jar /Users/yz/Desktop/javaweb-decomplier/javaweb-decomplier.jar -dgs=1 $(find /Users/yz/Desktop/jcms/WEB-INF/lib/ -type f -name "_.jar" |xargs grep "com.hanweb" |awk '{print $3}') /Users/yz/jcms-decomplier

依赖的 jar: javaweb-decomplier、Intellij java-decompiler。

执行上面的命令后会在 jcms-decomplier 目录下看到所有的 jar 已经被 Fernflower 反编译了。

https://i-blog.csdnimg.cn/blog_migrate/9b84399b1b1fc921c2f5d702704ec5f6.png

IntelliJ IDEA 是 Jetbrains 出品的一款非常强大的 Java IDE,IDEA 提供了强大的代码搜索、近乎完美的反编译、动态调试等功能可以最大程度的辅助我们代码审计。

不可以否认,与 IDEA 相比虽然 Eclipse 和 Netbeans 也有与之类似的功能,但是在真正的实战体验中个人更倾向于使用 IDEA,虽然曾经的我也是一个重度 Eclipse 开发者。

IDEA 的搜索快捷键是:⇧⌘F,使用 IDEA 提供的搜索功能可以非常快速的定位漏洞点信息。

https://i-blog.csdnimg.cn/blog_migrate/f0600a04b50c69008ff97b8e237f342f.png

IDEA 可以通过自定义搜索范围来精确查找我们需要审计的代码。默认搜索的是所有的位置,不过我们可以点击红色箭头指向的…按钮来细化我们的搜索范围。

https://i-blog.csdnimg.cn/blog_migrate/f5499de7d39b5204f400ef04193b373a.png

自定义搜索范围示例:

https://i-blog.csdnimg.cn/blog_migrate/f7ebea08e520c87daa3ff6d3ff9ec73a.png

自定义搜索范围后就可以在搜索时使用自定义的配置进行范围搜索了,有助于我们在挖漏洞的时候缩小代码定位范围。

https://i-blog.csdnimg.cn/blog_migrate/7058823d34c887b2be323f892bc6b788.png

搜索快捷键: ⌘O,标记搜索支持类名、方法名搜索(包括 class 或 jar 文件中的方法也支持搜索)。

https://i-blog.csdnimg.cn/blog_migrate/5e8a54efaa8ca331330054332f720a12.png

当我们审计代码的时候发现某个方法或类有漏洞时我们需要定位到漏洞的请求地址(触发点),复杂业务系统往往会让我们很难定位到漏洞的触发点。借助 IDEA 的方法调用链搜索功能就可以很轻松的找出方法的调用链和触发点。

选择类或者方法名–>右键–>Find Useages 或者使用快捷键 ⌥F7

https://i-blog.csdnimg.cn/blog_migrate/cafd109358938ebdf823792fcc10a40e.png

为了更好的管理项目我们通常会采用分层架构的方式来开发 Java Web 项目,分层设计的好处在于可以非常方便的分清楚包之间的业务逻辑关系。

常见的 JavaWeb 项目分层:

视图层(View 视图)

控制层(Controller、Action 控制层)

服务层(Service)

业务逻辑层 BO(business object)

实体层(entity 实体对象、VO(value object) 值对象 、模型层(bean)。

持久层(dao- Data Access Object 数据访问层、PO(persistant object) 持久对象)

基于 Java 分层架构的示例项目:

https://i-blog.csdnimg.cn/blog_migrate/b684fbc24a3ec11fed820981b7f70d91.png

Servlet 是在 Java Web 容器上运行的小程序,通常我们用 Servlet 来处理一些较为复杂的服务器端的业务逻辑。值得注意的是在 Servlet3.0 之后(Tomcat7+)可以使用注解方式配置 Servlet 了。

基于注解的 Servlet

https://i-blog.csdnimg.cn/blog_migrate/b9bfe5929eb28cedc1e024c4234478d6.png

Servlet3.0 之前的版本都需要在 web.xml 中配置,Servlet 是两对标签,由组成,Spring MVC 框架就是基于 Servlet 技术实现的。

基于配置实现的 Servlet

https://i-blog.csdnimg.cn/blog_migrate/bc5c739d1a7251843f11b24baf46196d.png

HttpServlet 类

https://i-blog.csdnimg.cn/blog_migrate/7267d87ae1e6ccfb5ad8f4f0e7a9da98.png

实现一个 Servlet 很简单,只需要继承 javax.servlet.http.HttpServlet 类并重写 doXXX 方法或者 service 方法就可以了,其中需要注意的是重写 HttpServlet 类的 service 方法可以获取到上述七种 Http 请求方法的请求。

JSP、JSPX 文件是可以直接被 Java 容器直接解析的动态脚本,jsp 和其他脚本语言无异,不但可以用于页面数据展示,也可以用来处理后端业务逻辑。

从本质上说 JSP 就是一个 Servlet,因为 jsp 文件最终会被编译成 class 文件,而这个 Class 文件实际上就是一个特殊的 Servlet。

JSP 文件会被编译成一个 java 类文件,如 index.jsp 在 Tomcat 中 Jasper 编译后会生成 index_jsp.java 和 index_jsp.class 两个文件。而 index_jsp.java 继承于 HttpJspBase 类,HttpJspBase 是一个实现了 HttpJspPage 接口并继承了 HttpServlet 的标准的 Servlet,jspService 方法其实是 HttpJspPage 接口方法,类似于 Servlet 中的 service 方法,这里的jspService 方法其实就是 HttpJspBase 的 service 方法调用。

https://i-blog.csdnimg.cn/blog_migrate/2a5e1d9d3c0617fcc59ab9a78c7d879b.png

Filter 是 JavaWeb 中的过滤器,用于过滤 URL 请求。通过 Filter 我们可以实现 URL 请求资源权限验证、用户登陆检测等功能。Filter 是一个接口,实现一个 Filter 只需要重写 init、doFilter、destroy 方法即可,其中过滤逻辑都在 doFilter 方法中实现。

Filter 和 Servlet 一样是 Java Web 中最为核心的部分,使用 Servlet 和 Filter 可以实现后端接口开发和权限控制,当然使用 Filter 机制也可以实现 MVC 框架,Struts2 实现机制就是使用的 Filter。

Filter 的配置类似于 Servlet,由两组标签组成,如果 Servlet 版本大于 3.0 同样可以使用注解的方式配置 Filter。

https://i-blog.csdnimg.cn/blog_migrate/c18083196a6b4b9c0dd0dfd9896415ff.png

对于基于 Filter 和 Servlet 实现的简单架构项目,代码审计的重心集中于找出所有的 Filter 分析其过滤规则,找出是否有做全局的安全过滤、敏感的 URL 地址是否有做权限校验并尝试绕过 Filter 过滤。第二点则是找出所有的 Servlet,分析 Servlet 的业务是否存在安全问题,如果存在安全问题是否可以利用?是否有权限访问?利用时是否被 Filter 过滤等问题,切勿看到 Servlet、JSP 中的漏洞点就妄下定论,不要忘了 Servlet 前面很有可能存在一个全局安全过滤的 Filter。

Filter 和 Servlet 都是 Java Web 提供的 API,简单的总结了下有如下共同点。

1.Filter 和 Servlet 都需要在 web.xml 或注解(@WebFilter、@WebServlet)中配置,而且配置方式是非常的相似的;

2.Filter 和 Servlet 都可以处理来自 Http 请求的请求,两者都有 request、response 对象;

3.Filter 和 Servlet 基础概念不一样,Servlet 定义是容器端小程序,用于直接处理后端业务逻辑,而 Filter 的思想则是实现对 Java Web 请求资源的拦截过滤;

4.Filter 和 Servlet 虽然概念上不太一样,但都可以处理 Http 请求,都可以用来实现 MVC 控制器(Struts2 和 Spring 框架分别基于 Filter 和 Servlet 技术实现的);

5.一般来说 Filter 通常配置在 MVC、Servlet 和 JSP 请求前面,常用于后端权限控制、统一的 Http 请求参数过滤(统一的 XSS、SQL 注入、Struts2 命令执行等攻击检测处理)处理,其核心主要体现在请求过滤上,而 Servlet 更多的是用来处理后端业务请求上

传统的开发存在结构混乱易用性差耦合度高可维护性差等多种问题,为了解决这些毛病分层思想和 MVC 框架就出现了。MVC 即模型(Model)、视图(View)、控制器(Controller), MVC 模式的目的就是实现 Web 系统的职能分工。

截至 2018 年底,绝大多数的新项目都已然改为了基于 Spring Boot 的 Spring MVC 实现,也就是说曾经站在 JavaWeb MVC 最巅峰的 Struts2 框架已经逐渐陨落。

7.1 Spring MVC 控制器

在 Spring 进入了 3.0 时代,使用 Java 注解的方式也逐渐的流行了起来,曾经写一个 Spring 的控制器我们通常要在 xml 中声明 Spring bean 并配置处理的 URL,而在新时代的 Spring 项目中我们通常用 Spring MVC 注解就可以轻松完成 Spring MVC 的配置了。

一个基于 Spring 注解配置的控制器:

package org.javaweb.codereview.controller;

import org.springframework.stereotype.Controller;

import org.springframework.web.bind.annotation.GetMapping;

@Controller

public class IndexController {

@RequestMapping("/index.php")

public String index() {

return “/index.html”;

}}

Spring Controller 注解:

@Controller

@RestController

@RepositoryRestController

Spring MVC 请求配置注解:

@RequestMapping

@GetMapping

@PostMapping

@PutMapping

@DeleteMapping

@PatchMapping

Spring MVC 除了上述 6 种 Http 请求处理注解以外还有 Spring Data JPA Rest 提供的特殊的@RepositoryRestResource 注解,@RepositoryRestResource 是基于 Spring Data JPA REST 库实现的,Spring Data JPA REST 提供的 API 可支持通过 JPA 查询数据并处理 Http 请求服务。

基于 XML 配置的 Spring MVC

对于一些老旧的项目可能还保留了一些基于 xml 配置的方式 Spring MVC 项目,这里只简单的介绍下如何配置不做过多的描述。基于配置方式的控制器一般是在 Controller 类中实现了 Spring 的 org.springframework.web.servlet.mvc.Controller 接口的 handleRequest 方法(当然还有其他途径,如:AbstractCommandController 和 SimpleFormController 但都已经过时了)。

TestController.java 示例代码:

package org.javaweb.codereview.controller;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.Controller;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**

- @author yz
  _/
  public class TestController implements Controller {
  @Override
  public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {
  ModelAndView mv = new ModelAndView();
  mv.setViewName("index");
  return mv;
  }
  }

XML 配置具体的 bean

<bean name="/test.do" class="org.javaweb.codereview.controller.TestController"/>

7.2 Struts2 控制器

Struts2 主要的开发模式是基于 xml 配置,在 struts.xml 中配置 Action 地址和对应的处理类。

https://i-blog.csdnimg.cn/blog_migrate/a0f180c6a8a6cabc40d4bb44a5f3e817.png

不过 Struts2(2.1.6 版本开始)也可以使用 struts2-convention-plugin 插件来实现基于注解方式的配置。

https://i-blog.csdnimg.cn/blog_migrate/90ccaca2d13b26f95f36d42d7fd3c4c9.png

需要注意的是 Struts2 的参数是可以通过 get/set 方法传入的,如上图 TestActionAnnotation 类的 username 变量是可以直接在 Http 请求中的 URL 传入的。

7.3 快速找出 Http 请求请求 URL

代码审计中我们可以选择优先从 Controller、Servlet 和 JSP 中入手,也可以选择从漏洞点反向推出 Http 请求的入口地址,这里将讲解下如何快速找到这些请求入口,因为 Struts2 和 Spring MVC 的原理比较接近,所以本节只以 Spring MVC 为例。

7.3.1 查找 Spring MVC 所有的控制器

如果有源码的情况下可以使用 find 命令或者 IDEA 的全局搜索功能即可快速搜索到所有的控制器,如果只有 class 文件的情况下可以使用 find 命令:

find ~/cms/ -type f -name "_.class" |xargs grep -E "Controller|@RestController|RepositoryRestController"

7.3.2 查找所有的请求处理 URL

查找请求处理 URL 的方式同理,使用如下 find 命令查找所有 class 中的请求处理注解:

find ~/cms/ -type f -name "_.class" |xargs grep -E "RequestMapping|GetMapping|PostMapping|PutMapping|DeleteMapping|PatchMapping|RepositoryRestResource"

7.4 Spring MVC 和 Struts2 控制器小结

这一小节我们只是简单的介绍下 Spring MVC 和 Struts2 的控制器,在后面的框架服务章节将会详细介绍。至于如何去快速定位 Struts2 的 action 请自行参考 Spring MVC 的 Controller 查找方式这里不再讲解。

Java 语言动态性一直以来都比较差,并不像 PHP 那样灵活。在 Java 中的动态性往往需要使用一些曲折的方式来实现.这里简单列举了 Java 十余种动态性相关技术并总结部分技术实现安全问题。

1.Java 反射机制

2.MethodHandle

3.JDK 动态代理

4.使用 JVM 上的动态语言(如:Groovy、JRuby、Jython)

5.表达式库(如:OGNL、MVEL、SpEL、EL)

6.JSP、JSPX、Quercus(Resin 容器提供了 PHP5 支持)

7.字节码库(如:Asm、Javassist、Cglib、BCEL)

8.ScriptEngineManager(脚本引擎)。

9.动态编译(如:JDT、JavaCompiler)

10.ClassLoader、URLClassLoader

11.模版引擎(如:Freemarker、Velocity)

12.序列化、反序列化(包含 Java 对象序列化、XML、JSON 等)

13.JNI、JNA(Java 调用 C/C++)

14.OSGi(Open Service Gateway Initiative)

15.RMI(Java 远程方法调用,基于对象序列化机制实现)

16.WebService

17.JDWP(Java Platform Debugger Architecture Java 调试协议)

18.JMX(Java Management Extensions)

Java 反射机制可以无视类方法、变量访问权限修饰符,可以调用任何类的任意方法、访问并修改成员变量值。也就是说只要发现一处 Java 反射调用漏洞几乎就可以为所欲为了。当然前提可能需要你能控制反射的类名、方法名和参数。

一行代码即可实现反射调用 Runtime 执行本地命令:

Runtime.class.getMethod("exec", String.class).invoke(Runtime.class.getMethod("getRuntime").invoke(null), "whoami")

获取一个类的对象(如 Runtime 类)我们一般会采用如下几种方式:

1.Class.forName(“java.lang.Runtime”)、”".getClass().forName(“java.lang.Runtime”)

2.Runtime.class

3.ClassLoader.getSystemClassLoader().loadClass(“java.lang.Runtime”)

Java 反射获取类方法有两种方式:

1.getMethod(xxx),getMethods()

2.getDeclaredMethod(xxx)、getDeclaredMethods()。

区别在于 getMethod 会返回当前类和父类的所有 public 方法,而 getDeclaredMethod 返回的是当前的所有方法。

Java 反射获取类成员变量有两种方式:

1.getField(xxx)、getFields()

2.getDeclaredField(xxx)、getDeclaredFields()

getField 和 getDeclaredField 区别同上,如果想要调用 private 修饰的 Field 或者 Method 只需要设置下 setAccessible 为 true 就可以了,如:xxxMethod.setAccessible(true)。

Java 的大部分框架都是采用了反射机制来实现的(如:Spring MVC、ORM 框架等),所以我们不得不掌握 Java 反射机制来提升我们的代码审计能力。

Java 反射机制实现无关键字执行命令

import java.io.InputStream;
  import java.lang.reflect.Method;
  import java.util.Scanner;
  /**
   _ @author yz
   */
  public class ReflectionTest {
  public static void exec() {
  try {
  System.out.println(Runtime.class.getMethod("exec", String.class).invoke(Runtime.class.getMethod("getRuntime").invoke(null), "curl -i localhost:8000"));
  } catch (Exception e) {
  e.printStackTrace();
  }
  }
  public static void main(String[] args) {
  try {
  String str = "whoami";
  // java.lang.Runtime
  String runtime = new String(new byte[]{106, 97, 118, 97, 46, 108, 97, 110, 103, 46, 82, 117, 110, 116, 105, 109, 101});
  // Runtime.class
  Class<?> c = Class.forName(runtime);
  // 获取 getRuntime 方法,Runtime.getRuntime()
  Method m1 = c.getMethod(new String(new byte[]{103, 101, 116, 82, 117, 110, 116, 105, 109, 101}));
  // 获取 Runtime 的 exec 方法,rt.exec(xxx)
  Method m2 = c.getMethod(new String(new byte[]{101, 120, 101, 99}), String.class);
  // Runtime.getRuntime().exec(str)
  Object obj2 = m2.invoke(m1.invoke(null), str);
  // 获取命令执行结果 Process 类的 getInputStream()方法
  Method m = obj2.getClass().getMethod(new String(new byte[]{103, 101, 116, 73, 110, 112, 117, 116, 83, 116, 114, 101, 97, 109}));
  m.setAccessible(true);
  // process.getInputStream()
  InputStream in = (InputStream) m.invoke(obj2, new Object[]{});
  // 输出 InputStream 内容到
  Scanner scanner = new Scanner(in).useDelimiter("\\A");
  System.out.println(scanner.hasNext() ? scanner.next() : "");
  } catch (Throwable t) {
  t.printStackTrace();
  }
  }
  }

JDK7 开始 Java 提供了 MethodHandle 可以非常方便的访问和调用类方法,MethodHandle 的能力和 Java 反射机制相似,但效率却远高出 Java 反射机制,但 MethodHandle 也并不是那么完美的,缺点是 MethodHandle 必须要求 JDK 版本大于等于 1.7,MethodHandle 也无法像反射那样调用私有方法和变量。

参考: 。

基于 MethodHandle 实现的调用 Runtime 执行系统命令

import java.io.InputStream;
  import java.lang.invoke.MethodHandle;
  import java.lang.invoke.MethodHandles;
  import java.lang.invoke.MethodType;
  import java.util.Scanner;
  /**
   * @author yz
   */
  public class MethodHandlesTest {
  public static void main(String[] args) {
  try {
  String               str          = "ping p2j.cn -c 1";
  Class                runtimeClass = Runtime.class;
  MethodHandles.Lookup lookup       = MethodHandles.lookup();
  // Runtime rt = Runtime.getRuntime()
  MethodHandle methodHandle = lookup.findStatic(
  runtimeClass, "getRuntime", MethodType.methodType(runtimeClass)
  );
  // 获取 Runtime 的 exec 方法
  MethodHandle execMethod = lookup.findVirtual(
  runtimeClass, "exec", MethodType.methodType(Process.class, new Class[]{
  String.class
  })
  );
  // 获取 Process 的 getInputStream 方法
  MethodHandle inputStreamMethod = lookup.findVirtual(
  Process.class, "getInputStream", MethodType.methodType(InputStream.class)
  );
  // 调用 Runtime.getRuntime().exec(xxx).getInputStream()
  InputStream in = (InputStream) inputStreamMethod.invoke(
  execMethod.invoke(methodHandle.invoke(), str)
  );
  // 输出 InputStream 内容到
  Scanner scanner = new Scanner(in).useDelimiter("\\A");
  System.out.println(scanner.hasNext() ? scanner.next() : "");
  } catch (Throwable t) {
  t.printStackTrace();
  }
  }
  }

通常我喜欢把代码审计的方向分为业务层安全问题、代码实现和服务架构安全问题,。

业务层的安全问题集中在业务逻辑和越权问题上,我们在代码审计的过程中尽可能的去理解系统的业务流程以便于发现隐藏在业务中的安全问题。

1.1 业务层中常见的安全问题 Checklist

1.用户登陆、用户注册、找回密码等功能中密码信息未采用加密算法。

2.用户登陆、用户注册、找回密码等功能中未采用验证码或验证码未做安全刷新(未刷新 Session 中验证码的值)导致的撞库、密码爆破漏洞。

3.找回密码逻辑问题(如:可直接跳过验证逻辑直接发包修改)。

4.手机、邮箱验证、找回密码等涉及到动态验证码等功能未限制验证码失败次数、验证码有效期、验证码长度过短导致的验证码爆破问题。

5.充值、付款等功能调用了第三方支付系统未正确校验接口(如:1 分钱买 IPhone X)。

6.后端采用了 ORM 框架更新操作时因处理不当导致可以更新用户表任意字段(如:用户注册、用户个人资料修改时可以直接创建管理员账号或其他越权修改操作)。

7.后端采用了 ORM 框架查询数据时因处理不当导致可以接收任何参数导致的越权查询、敏感信息查询等安全问题。

8.用户中心转账、修改个人资料、密码、退出登陆等功能未采用验证码或 Token 机制导致存在 CSRF 漏洞。

9.后端服务过于信任前端,重要的参数和业务逻辑只做了前端验证(如:文件上传功能的文件类型只在 JS 中验证、后端不从 Session 中获取用户 ID、用户名而是直接接收客户端请求的参数导致的越权问题)。

10.用户身份信息认证逻辑问题(如:后台系统自动登陆时直接读取 Cookie 中的用户名、用户权限不做验证)。

11.重要接口采用 ID 自增、ID 可预测并且云端未验证参数有效性导致的越权访问、信息泄漏问题(如:任意用户订单越权访问)。

12.条件竞争问题,某些关键业务(如:用户转账)不支持并发、分布式部署时不支持锁的操作等。

13.重要接口未限制请求频率,导致短信、邮件、电话、私信等信息轰炸。

14.敏感信息未保护,如 Cookie 中直接存储用户密码等重要信息。

15.弱加密算法、弱密钥,如勿把 Base64 当成数据加密方式、重要算法密钥采用弱口令如 123456。

16.后端无异常处理机制、未自定义 50X 错误页面,服务器异常导致敏感信息泄漏(如:数据库信息、网站绝对路径等)。

17.使用 DWR 框架开发时前后端不分漏洞(如:DWR 直接调用数据库信息把用户登陆逻辑直接放到了前端来做)。

代码审计的核心是寻找代码中程序实现的安全问题,通常我们会把代码审计的重心放在 SQL 注入、文件上传、命令执行、任意文件读写等直接威胁到服务器安全的漏洞上,因为这一类的漏洞杀伤力极大也是最为致命的。

###2.1 代码实现中常见的安全问题 Checklist

1.任意文件读写(文件上传、文件下载)、文件遍历、文件删除、文件重命名等漏洞

2.SQL 注入漏洞

3.XXE(XML 实体注入攻击)

4.表达式执行(SpEL、OGNL、MVEL2、EL 等)

5.系统命令执行漏洞(ProcessBuilder)

6.反序列化攻击(ObjectInputStream、JSON、XML 等)

7.Java 反射攻击

8.SSRF 攻击

2.1.1 Java 文件名空字节截断漏洞(%00 Null Bytes)

空字节截断漏洞漏洞在诸多编程语言中都存在,究其根本是 Java 在调用文件系统(C 实现)读写文件时导致的漏洞,并不是 Java 本身的安全问题。不过好在高版本的 JDK 在处理文件时已经把空字节文件名进行了安全检测处理。

2013 年 9 月 10 日发布的 Java SE 7 Update 40 修复了空字节截断这个历史遗留问题。此次更新在 java.io.File 类中添加了一个 isInvalid 方法,专门检测文件名中是否包含了空字节。

https://i-blog.csdnimg.cn/blog_migrate/b71b1400c059944cb2a1d20b57be75cf.png

修复的 JDK 版本所有跟文件名相关的操作都调用了 isInvalid 方法检测,防止空字节截断。

https://i-blog.csdnimg.cn/blog_migrate/6ec2f23179bd414039460c06569dff8b.png

修复前(Java SE 7 Update 25)和修复后(Java SE 7 Update 40)的对比会发现 Java SE 7 Update 25 中的 java.io.File 类中并未添加\u0000 的检测。

https://i-blog.csdnimg.cn/blog_migrate/f5f73429cce55d86c3ecf74bbaf74718.png

受空字节截断影响的 JDK 版本范围:JDK<1.7.40,单是 JDK7 于 2011 年 07 月 28 日发布至 2013 年 09 月 10 日发表 Java SE 7 Update 40 这两年多期间受影响的就有 16 个版本,值得注意的是 JDK1.6 虽然 JDK7 修复之后发布了数十个版本,但是并没有任何一个版本修复过这个问题,而 JDK8 发布时间在 JDK7 修复以后所以并不受此漏洞影响。

参考:

2.1.2 测试 Java 写文件截断测试

测试类 FileNullBytes.java:

import java.io.File;
  import java.io.FileOutputStream;
  import java.io.IOException;
  /**
   * @author yz
   */
  public class FileNullBytes {
  public static void main(String[] args) {
  try {
  String fileName = "/tmp/null-bytes.txt\u0000.jpg";
  FileOutputStream fos = new FileOutputStream(new File(fileName));
  fos.write("Test".getBytes());
  fos.flush();
  fos.close();
  } catch (IOException e) {
  e.printStackTrace();
  }
  }
  }

使用 JDK1.7.0.25 测试成功截断文件名:

https://i-blog.csdnimg.cn/blog_migrate/a756822b8d37b4f96247604e7f4bc56e.png

使用 JDK1.7.0.80 测试写文件截断时抛出 java.io.FileNotFoundException: Invalid file path 异常:

https://i-blog.csdnimg.cn/blog_migrate/9171910a49116bfbdfc6c924e7f81751.png

空字节截断利用场景

Java 空字节截断利用场景最常见的利用场景就是文件上传时后端使用了 endWith、正则使用如:.(jpg|png|gif)$验证文件名后缀且文件名最终原样保存,同理文件删除(delete)、获取文件路径(getCanonicalPath)、创建文件(createNewFile)、文件重命名(renameTo)等方法也可适用。

空字节截断修复方案

最简单直接的方式就是升级 JDK,如果担心升级 JDK 出现兼容性问题可在文件操作时检测下文件名中是否包含空字节,如 JDK 的修复方式:fileName.indexOf(‘\u0000′)即可。

2.1.2 任意文件读取漏洞

任意文件读取漏洞即因为没有验证请求的资源文件是否合法导致的,此类漏洞在 Java 中有着较高的几率出现,任意文件读取漏洞看似很简单,但是在这个问题上翻车的有不乏一些知名的中间件:Weblogic、Tomcat、Resin 又或者是主流 MVC 框架:Spring MVC、Struts2。所以在审计文件读取功能的时候要非常仔细,或许很容易就会有意想不到的收获!

任意文件读取示例代码 file-read.jsp:

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
  <%@ page import="java.io.ByteArrayOutputStream" %>
  <%@ page import="java.io.File" %>
  <%@ page import="java.io.FileInputStream" %>
  <%
  File file = new File(request.getParameter("path"));
  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);
  }
  out.write("<pre>" + new String(baos.toByteArray()) + "</pre>");
  fis.close();
  %>

访问 file-read.jsp 文件即可读取任意文件:

https://i-blog.csdnimg.cn/blog_migrate/24836cbed374674b3677ec23383b77f9.png

快速发现这类漏洞得方式其实也是非常简单的,在 IDEA 中的项目中重点搜下如下文件读取的类。

1.JDK 原始的 java.io.FileInputStream 类

2.JDK 原始的 java.io.RandomAccessFile 类

3.Apache Commons IO 提供的 org.apache.commons.io.FileUtils 类

4.JDK1.7 新增的基于 NIO 非阻塞异步读取文件的 java.nio.channels.AsynchronousFileChannel 类。

5.JDK1.7 新增的基于 NIO 读取文件的 java.nio.file.Files 类。常用方法如:Files.readAllBytes、Files.readAllLines

如果仍没有什么发现可以搜索一下 FileUtil 很有可能用户会封装文件操作的工具类。

Java WebSevice

Web Service 是一种基于 SOAP 协议实现的跨语言 Web 服务调用,在 Java 中 Web Service 有如下技术实现:Oracle JWS、Apache Axis1、2、XFire、Apache CXF、JBossWS。

Axis1.4 配置

web.xml 配置 Axis1.4

https://i-blog.csdnimg.cn/blog_migrate/81dc9078bdac3663b695f5cb5b1f382f.png

配置 server-config.wsdd 文件注册 Web Service 服务类和方法:

https://i-blog.csdnimg.cn/blog_migrate/fb51df09e451b7e845b96246ef14f275.png

FileService 类,提供了文件读写接口:

https://i-blog.csdnimg.cn/blog_migrate/e2db64be500a5cd918793a80e9c1a9d8.png

使用 IDEA 创建 Web Service 项目默认会创建管理 Web Service 的 API:/servlet/AxisServlet、/services、SOAPMonitor、/servlet/AdminServlet,*.jws 以及用监控 Web Service 的端口 5001 或 5101。

https://i-blog.csdnimg.cn/blog_migrate/f5ca036f159c9c3ea6ab746a90df3d88.png

访问 Web Service 的 FileService 服务加上?wsdl 参数可以看到 FileService 提供的服务方法和具体的参数信息。

https://i-blog.csdnimg.cn/blog_migrate/71ea1545e08cd560127594c40c180eaa.png

使用 SOAP-UI 调用 Web Service 接口示例:

https://i-blog.csdnimg.cn/blog_migrate/6b1e3300b549245962034db71bf715ce.png

需要注意的是 Web Service 也是可以设置授权认证的,如实现了 WS-Security 的 WSS4J。

https://i-blog.csdnimg.cn/blog_migrate/7777adaf9d917379c4b5ece5d9b07f62.png

使用 IDEA 根据 wsdl 生成 Web Service 客户端代码:

https://i-blog.csdnimg.cn/blog_migrate/ce74c9af594b6061ac2a6772da68be23.png

设置 wsdl 地址、包名:

https://i-blog.csdnimg.cn/blog_migrate/b2fc00b44f9235a1597a35ec04fb3760.png

新建 FileServiceTest 类测试接口调用:

package org.javaweb.codereview.axis.client;
  import java.net.URL;
  /**
- 文件 Web Service 服务测试
-
- @author yz
*/
public class FileServiceTest {
public static void main(String[] args) {
try {
FileServiceService fileService = new FileServiceServiceLocator();
URL webServiceUrl = new URL("http://localhost:8080/services/FileService");
FileServiceSoapBindingStub soapService = new FileServiceSoapBindingStub(webServiceUrl, fileService);
String content = soapService.readFile("/etc/passwd");
System.out.println(content);
} catch (Exception e) {
e.printStackTrace();
}
}
}

参考:

转自:freebuf