点击上方“凌天实验室”可订阅哦!
文接上回,穿越捷径: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
项目通常都采用了模块化方式开发,借助于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请求方法的请求。
4. JSP、Servlet之间的关系
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
更多的是用来处理后端业务请求上。
7. 初识JavaWeb MVC框架
传统的开发存在结构混乱易用性差耦合度高可维护性差等多种问题,为了解决这些毛病分层思想和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查找方式这里不再讲解。
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)
1. Java反射机制特性
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();
}
}
}
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:还有最后一讲哦,敬请期待~