cover_image

Java Web安全-代码审计(二)

凌天实验室 凌天实验室 2019年01月21日 06:16

点击上方“凌天实验室”可订阅哦!


文接上回,穿越捷径:Java Web安全-代码审计(一)


4
图片
Java Web基础
图片

1. Java分层思想

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


常见的JavaWeb项目分层:

视图层(View 视图)
控制层(Controller、Action 控制层)
服务层(Service)
业务逻辑层BO(business object)  
实体层(entity 实体对象、VO(value object) 值对象 、模型层(bean)。
持久层(dao- Data Access Object 数据访问层、PO(persistant object) 持久对象)


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


图片


2. Java模块化开发

如今的较为大型的Java Web项目通常都采用了模块化方式开发,借助于MavenGradle依赖管理工具,Java可以非常轻松的完成模块化开发。除此之外使用OSGi(Open Service Gateway Initiative 可实现模块热部署)技术开发来Java动态模块化系统也是较为常见的。


采用模块化开发也会给我们做代码审计带来一定的难度,因为需要在更多的依赖库中去寻找需要我们审计的代码。

使用Maven开发的JavaWeb项目示例:

图片

图片


3. 什么是Servlet?

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

基于注解的Servlet

图片


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

基于配置实现的Servlet

图片


HttpServlet类


图片


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


4. JSP、Servlet之间的关系

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


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

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


图片


5. 什么是Filter

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

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

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

图片


6. Filter和Servlet的总结

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


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

  1. FilterServlet都需要在web.xml注解(@WebFilter@WebServlet)中配置,而且配置方式是非常的相似的。

  2. FilterServlet都可以处理来自Http请求的请求,两者都有requestresponse对象。

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

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

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


7. 初识JavaWeb MVC框架

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


截至2018年底,绝大多数的新项目都已然改为了基于Spring BootSpring 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注解:

  1. @Controller

  2. @RestController

  3. @RepositoryRestController


Spring MVC请求配置注解:

  1. @RequestMapping

  2. @GetMapping

  3. @PostMapping

  4. @PutMapping

  5. @DeleteMapping

  6. @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方法(当然还有其他途径,如:AbstractCommandControllerSimpleFormController但都已经过时了)。

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地址和对应的处理类。

图片


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


图片


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

7.3 快速找出Http请求请求URL

代码审计中我们可以选择优先从ControllerServletJSP中入手,也可以选择从漏洞点反向推出Http请求的入口地址,这里将讲解下如何快速找到这些请求入口,因为Struts2Spring 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 MVCStruts2的控制器,在后面的框架服务章节将会详细介绍。至于如何去快速定位Struts2的action请自行参考Spring MVC的Controller查找方式这里不再讲解。


5
图片
Java语言的动态性
图片


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


  1. Java反射机制

  2. MethodHandle

  3. JDK动态代理

  4. 使用JVM上的动态语言(如:GroovyJRubyJython)

  5. 表达式库(如:OGNLMVELSpELEL)

  6. JSPJSPXQuercus(Resin容器提供了PHP5支持)

  7. 字节码库(如:AsmJavassistCglibBCEL)

  8. ScriptEngineManager(脚本引擎)。

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

  10. ClassLoaderURLClassLoader

  11. 模版引擎(如:FreemarkerVelocity)

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

  13. JNIJNA(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)

1. Java反射机制特性

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()

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

Java的大部分框架都是采用了反射机制来实现的(如:Spring MVCORM框架等),所以我们不得不掌握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();
		}
	}

}


2. JDK7+ MethodHandle

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


参考:通过代码简单介绍JDK 7的MethodHandle,并与.NET的委托对比。

基于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();
		}
	}

}


未完待续


精彩继续,下回预告:

六、Java代码审计-Checklist

PS:还有最后一讲哦,敬请期待~

图片
凌天
实验室
安全实验室,是安百科技旗下针对应用安全领域进行攻防研究的专业技术团队,其核心成员来自原乌云创始团队及社区知名白帽子,团队专业性强、技术层次高且富有实战经验。实验室成立于2016年,发展至今团队成员已达35人,在应用安全领域深耕不辍,向网络安全行业顶尖水平攻防技术团队的方向夯实迈进。
继续滑动看下一个
凌天实验室
向上滑动看下一个