点击上方“ 凌天实验室 ”可订阅哦!
文接上回,穿越捷径: 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人,在应用安全领域深耕不辍,向网络安全行业顶尖水平攻防技术团队的方向夯实迈进。