点击上方“ 凌天实验室 ”可订阅哦!
文接上回,穿越捷径: Java Web安全-代码审计(一)
4
Java Web基础
为了更好的管理项目我们通常会采用分层架构的方式来开发Java Web项目,分层设计的好处在于可以非常方便的分清楚包之间的业务逻辑关系。
常见的JavaWeb项目分层:
视图层(View 视图) 控制层(Controller、Action 控制层) 服务层(Service) 业务逻辑层BO(business object) 实体层(entity 实体对象、VO(value object) 值对象 、模型层(bean)。 持久层(dao- Data Access Object 数据访问层、PO(persistant object) 持久对象)
基于Java分层架构的示例项目:
如今的较为大型的 Java Web 项目通常都采用了模块化方式开发,借助于 Maven 、 Gradle 依赖管理工具,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请求方法的请求。
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 方法调用。
5. 什么是Filter
Filter是JavaWeb中的过滤器,用于过滤URL请求。通过Filter我们可以实现URL请求资源权限验证、用户登陆检测等功能。Filter是一个接口,实现一个Filter只需要重写 init 、 doFilter 、 destroy 方法即可,其中过滤逻辑都在 doFilter 方法中实现。
Filter和Servlet一样是Java Web中最为核心的部分,使用Servlet和Filter可以实现后端接口开发和权限控制,当然使用Filter机制也可以实现MVC框架, Struts2 实现机制就是使用的Filter。
Filter的配置类似于Servlet,由 <filter> 和 <filter-mapping> 两组标签组成,如果Servlet版本大于3.0同样可以使用注解的方式配置Filter。
6. Filter和Servlet的总结
对于基于 Filter 和 Servlet 实现的简单架构项目,代码审计的重心集中于找出所有的 Filter 分析其过滤规则,找出是否有做全局的安全过滤、敏感的URL地址是否有做权限校验并尝试绕过 Filter 过滤。第二点则是找出所有的 Servlet ,分析 Servlet 的业务是否存在安全问题,如果存在安全问题是否可以利用?是否有权限访问?利用时是否被Filter过滤等问题,切勿看到 Servlet 、 JSP 中的漏洞点就妄下定论,不要忘了 Servlet 前面很有可能存在一个全局安全过滤的 Filter 。
Filter 和 Servlet 都是 Java Web 提供的API,简单的总结了下有如下共同点。
Filter 和 Servlet 都需要在 web.xml 或 注解 ( @WebFilter 、 @WebServlet )中配置,而且配置方式是非常的相似的。
Filter 和 Servlet 都可以处理来自Http请求的请求,两者都有 request 、 response 对象。
Filter 和 Servlet 基础概念不一样, Servlet 定义是容器端小程序,用于直接处理后端业务逻辑,而 Filter 的思想则是实现对Java Web请求资源的拦截过滤。
Filter 和 Servlet 虽然概念上不太一样,但都可以处理Http请求,都可以用来实现MVC控制器( Struts2 和 Spring 框架分别基于 Filter 和 Servlet 技术实现的)。
一般来说 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"/>
Struts2主要的开发模式是基于xml配置,在 struts.xml 中配置Action地址和对应的处理类。
不过Struts2( 2.1.6 版本开始)也可以使用 struts2-convention-plugin 插件来实现基于注解方式的配置。
需要注意的是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"
查找请求处理URL的方式同理,使用如下find命令查找所有class中的请求处理注解:
find ~/cms/ -type f -name "*.class" |xargs grep -E "RequestMapping|GetMapping|PostMapping|PutMapping|DeleteMapping|PatchMapping|RepositoryRestResource"
这一小节我们只是简单的介绍下 Spring MVC 和 Struts2 的控制器,在后面的框架服务章节将会详细介绍。至于如何去快速定位Struts2的action请自行参考Spring MVC的Controller查找方式这里不再讲解。
5
Java语言的动态性
Java语言动态性一直以来都比较差,并不像PHP那样灵活。在Java中的动态性往往需要使用一些曲折的方式来实现.这里简单列举了Java十余种动态性相关技术并总结部分技术实现安全问题。
Java反射机制
MethodHandle
JDK动态代理
使用JVM上的动态语言(如: Groovy 、 JRuby 、 Jython )
表达式库(如: OGNL 、 MVEL 、 SpEL 、 EL )
JSP 、 JSPX 、 Quercus (Resin容器提供了PHP5支持)
字节码库(如: Asm 、 Javassist 、 Cglib 、 BCEL )
ScriptEngineManager(脚本引擎)。
动态编译(如:JDT、JavaCompiler)
ClassLoader 、 URLClassLoader
模版引擎(如: Freemarker 、 Velocity )
序列化、反序列化(包含 Java 对象序列化 、 XML 、 JSON 等)
JNI 、 JNA (Java调用C/C++)
OSGi ( Open Service Gateway Initiative )
RMI(Java远程方法调用,基于对象序列化机制实现)
WebService
JDWP ( Java Platform Debugger Architecture Java调试协议)
JMX(Java Management Extensions)
Java反射机制可以无视类方法、变量访问权限修饰符,可以 调用任何类的任意方法、访问并修改成员变量值 。也就是说只要发现一处Java反射调用漏洞几乎就可以为所欲为了。当然前提可能需要你能 控制反射的类名、方法名和参数 。
一行代码即可实现反射调用Runtime执行本地命令:
Runtime.class.getMethod("exec", String.class).invoke(Runtime.class.getMethod("getRuntime").invoke(null), "whoami")
获取一个类的对象(如Runtime类)我们一般会采用如下几种方式:
Class.forName("java.lang.Runtime")、"".getClass().forName("java.lang.Runtime")
Runtime.class
ClassLoader.getSystemClassLoader().loadClass("java.lang.Runtime")
Java反射获取类方法有两种方式:
getMethod(xxx) , getMethods()
getDeclaredMethod(xxx) 、 getDeclaredMethods() 。
区别在于 getMethod会返回当前类和父类的所有public方法 ,而 getDeclaredMethod返回的是当前的所有方法 。
Java反射获取类成员变量有两种方式:
getField(xxx) 、 getFields()
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 也无法像反射那样调用私有方法和变量。
参考:通过代码简单介绍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();
}
}
}
精彩继续,下回预告:
PS:还有最后一讲哦,敬请期待~
凌天
实验室
安全实验室,是安百科技旗下针对应用安全领域进行攻防研究的专业技术团队,其核心成员来自原乌云创始团队及社区知名白帽子,团队专业性强、技术层次高且富有实战经验。实验室成立于2016年,发展至今团队成员已达35人,在应用安全领域深耕不辍,向网络安全行业顶尖水平攻防技术团队的方向夯实迈进。