简介
本篇是整个java安全学习系列的基础篇,这个系列篇章我会把我的整个java安全学习过程进行一个总结。至于为什么想写这个系列文章,是因为当时听了小伙伴的分享中提到“21小时可以入门任何课程”,看了他整理的学习导图,深受启发。所以,我打算把过去的和新学习的东西,完整的、系统的归纳总结出来,做到温故而知新。
学习流程
这个是我整理的java的学习路线图,每个人可能有不同的理解,大家可以自己动手进行自己的学习规划。并且学习过程是动态的,可能在学习总结过程中,我会修增某些模块。本篇我将对第一部分-基础,进行讲解。
java基础
这边讨论的基础,不是java的基础语法,这部分自己可以快速入门学习。我要讲的部分,是java的一些特性,或者比较重要的语法,在看代码中或者分析payload经常用到的。这部分是我在实践过程中觉得难点和重点,每个人的理解方法不同,所以并不适用所有人的学习,不过理解以下概念方法对于java安全学习是有帮助的,这点可以肯定。
基本语法
泛类型
泛型,即“参数化类型”。一提到参数,最熟悉的就是定义方法时有形参,然后调用此方法时传递实参。那么参数化类型怎么理解呢?
顾名思义,就是将类型由原来的具体的类型参数化,类似于方法中的变量参数,此时类型也定义成参数形式(可以称之为类型形参),然后在使用/调用时传入具体的类型(类型实参)。
泛型的本质是为了参数化类型(在不创建新的类型的情况下,通过泛型指定的不同类型来控制形参具体限制的类型)。也就是说在泛型使用过程中,操作的数据类型被指定为一个参数,这种参数类型可以用在类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法。
最典型的泛型类应用就是各种容器类,如:List、Set、Map。自己定义的泛型类形式如下:
1 | //此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型 |
2 | //在实例化泛型类时,必须指定T的具体类型 |
3 | public class Generic<T>{ |
4 | //key这个成员变量的类型为T,T的类型由外部指定 |
5 | private T key; |
6 | |
7 | public Generic(T key) { //泛型构造方法形参key的类型也为T,T的类型由外部指定 |
8 | this.key = key; |
9 | } |
10 | |
11 | public T getKey(){ //泛型方法getKey的返回值类型为T,T的类型由外部指定 |
12 | return key; |
13 | } |
14 | } |
更多泛型基本知识内容可参考:https://www.cnblogs.com/coprince/p/8603492.html
对象类型、基本类型
Java中的对象分两种类型:基本类型和非基本类型(对象类型)。
基本类型就是那些最常用的类型,例如:boolean/char/byte/short/int/long/float/double,这些类型有个特点,就是变量直接存储值。
除了基本类型之外的都是非基本类型了。非基本类型有个显著特点就是初始化的时候一般需要使用new来创建一个对象,所以非基本类型也叫非基本类型。例如:String name=new String(Tom);。非基本类型跟基本类型的本质区别,在于非基本类型变量存储的不是值,而是引用。
命令执行的方法
java命令执行,主要有两种方法Runtime.getRuntime().exec(cmd) 和ProcessBuilder(cmd).start,实例如下:
1 | package com.manba.demo; |
2 | |
3 | import java.io.BufferedReader; |
4 | import java.io.IOException; |
5 | import java.io.InputStreamReader; |
6 | |
7 | public class CmdTest { |
8 | public static void rexec() throws IOException { |
9 | String cmds = "/bin/sh -c pwd"; // 也可以数组形式 |
10 | Process process = Runtime.getRuntime().exec(cmds); |
11 | BufferedReader Reader = new BufferedReader(new InputStreamReader(process.getInputStream())); |
12 | String line; |
13 | while ((line = Reader.readLine()) != null) System.out.println(line); |
14 | } |
15 | |
16 | public static void pexexc() throws IOException { |
17 | String[] cmds = {"/bin/sh", "-c", "ls"}; // 只能数组形式 |
18 | Process pb = new ProcessBuilder(cmds).start(); |
19 | BufferedReader Reader = new BufferedReader(new InputStreamReader(pb.getInputStream())); |
20 | String line; |
21 | while ((line = Reader.readLine()) != null) System.out.println(line); |
22 | } |
23 | |
24 | public static void main(String[] args) throws IOException { |
25 | rexec(); |
26 | pexexc(); |
27 | } |
28 | |
29 | } |
这两个方法的主要区别在于Runtime.getRuntime.exec是静态方法,而ProcessBuilder().start不是静态方法,这在strust2中构造payload,是很有用的。
Java Bean和Factory概念
JavaBeans:Java中一种特殊的类,可以将多个对象封装到一个对象(bean)中。特点是可序列化,提供无参构造器,提供getter方法和setter方法访问对象的属性。名称中的“Bean”是用于Java的可重用软件组件的惯用叫法。
1 | package com.manba.demo; |
2 | public class SimpleBean{ |
3 | private String name; |
4 | private int age; |
5 | public void setName(String name){ |
6 | this.name = name; |
7 | } |
8 | public void setAge(int age){ |
9 | this.age = age; |
10 | } |
11 | public String getName(){ |
12 | return this.name; |
13 | } |
14 | public int getAge(){ |
15 | return this.age; |
16 | } |
17 | } |
总结如下:
所有的类必须声明为public
所有属性为private
提供默认构造方法
提供getter和setter
实现serializable接口
Java Factory定义:定义一个创建对象的接口,但让实现这个接口的类来决定实例化哪个类,工厂方法让类的实例化推迟到了子类中进行,它属于创建类型。
1 | 通俗理解与做法: |
2 | 定义一个抽象类或者接口来当规范工厂,它是一个只声明方法叫什么名字不实现方法的内容的一个规范类; |
3 | 定义具体工厂实现或者继承规范工厂,然后重写规范工厂中定义的方法,在该方法中生产属于自己工厂的对象; |
4 | 使用的时候,new工厂的时候是具体工厂给规范工厂进行赋值。即=号左边是规范工厂类型,右边是具体工厂类型,想获哪个具体工厂生产的对象就使用哪个具体工厂类型,最后利用对象调用方法来获取具体工厂生产的; |
5 | |
6 | 注意点: |
7 | 要有一个规范工厂,该工厂只负责声明方法叫什么名字,不实现方法的内容; |
8 | 每一个具体工厂都要继承或者实现规范工厂,重写它的方法,在方法中生产自己工厂的对象; |
9 | 使用的时候一定要具体工厂给规范工厂进行赋值; |
代码案例:
1 | //StandardFactory----规范工厂 |
2 | //SpecificFactory----具体工厂 |
3 | package com.manba.demo; |
4 | |
5 | public class Product { |
6 | interface StandardFactory { |
7 | public Product createProduct(); //声明了方法叫这个名字 |
8 | } |
9 | |
10 | static class SpecificFactory implements StandardFactory { |
11 | |
12 | public Product createProduct() { //具体工厂实现规范工厂并重写它的方法生产属于工厂的对象 |
13 | return new Product(); //这是属于该具体工厂生产的对象 |
14 | } |
15 | } |
16 | |
17 | public static class Client { |
18 | public static void main(String[] args) { |
19 | StandardFactory factory = new SpecificFactory(); |
20 | Product prodect = factory.createProduct(); |
21 | } |
22 | } |
23 | } |
Java Maven
Maven 翻译为”专家”、”内行”,是 Apache 下的一个纯 Java 开发的开源项目。基于项目对象模型(缩写:POM)概念,Maven利用一个中央信息片断能管理一个项目的构建、报告和文档等步骤。Maven 是一个项目管理工具,可以对 Java 项目进行构建、依赖管理。
POM( Project Object Model,项目对象模型 ) 是 Maven 工程的基本工作单元,是一个XML文件,包含了项目的基本信息,用于描述项目如何构建,声明项目依赖,等等。执行任务或目标时,Maven 会在当前目录中查找 POM。它读取 POM,获取所需的配置信息,然后执行目标。
POM 中可以指定以下配置:
- 项目依赖
- 插件
- 执行目标
- 项目构建 profile
- 项目版本
- 项目开发者列表
- 相关邮件列表信息
Maven 参数
-D 传入属性参数
-P 使用pom中指定的配置
-e 显示maven运行出错的信息
-o 离线执行命令,即不去远程仓库更新包
-X 显示maven允许的debug信息
-U 强制去远程参考更新snapshot包
其他参数可以通过mvn help 获取
1、mvn clean
说明: 清理项目生产的临时文件,一般是模块下的target目录
2、mvn package
说明: 项目打包工具,会在模块下的target目录生成jar或war等文件,如下运行结果
3、mvn test
说明: 测试命令,或执行src/test/java/下junit的测试用例
4、mvn install
说明: 模块安装命令 将打包的的jar/war文件复制到你的本地仓库中,供其他模块使用。 -Dmaven.test.skip=true 跳过测试(同时会跳过test compile)
5、mvn deploy
说明: 发布命令 将打包的文件发布到远程参考,提供其他人员进行下载依赖 ,一般是发布到公司的私服
mvn 快速构建java项目命令
1 | mvn archetype:generate -DgroupId=com.companyname.bank -DartifactId=consumerBanking -DarchetypeArtifactId=maven-archetype-quickstart -DinteractiveMode=false |
mvn 快速构建web项目
1 | mvn archetype:generate -DgroupId=com.companyname.automobile -DartifactId=trucks -DarchetypeArtifactId=maven-archetype-webapp -DinteractiveMode=false |
Maven内容很多,这边给大家介绍下概念,以及最基本用法,详细知识点大家可以移步到https://www.runoob.com/maven/maven-tutorial.html学习。
IDEA调试远程调试
配置tomcat调试模式
dockerfile配置样例,tomcat以调试模式打开
1 | FROM vulhub/tomcat:8.5 |
2 | |
3 | MAINTAINER phithon <root@leavesongs.com> |
4 | |
5 | USER root |
6 | RUN set -ex \ |
7 | && rm -rf /usr/local/tomcat/webapps/* \ |
8 | && chmod a+x /usr/local/tomcat/bin/*.sh |
9 | COPY S2-001.war /usr/local/tomcat/webapps/ROOT.war |
10 | ENV JPDA_ADDRESS 5005 |
11 | ENV JPDA_TRANSPORT dt_socket |
12 | CMD ["catalina.sh", "jpda", "run"] |
13 | EXPOSE 8080 |
14 | EXPOSE 5005 |
docker-compose.yml配置样例
1 | version: '2' |
2 | services: |
3 | struts2: |
4 | build: . |
5 | ports: |
6 | - "8080:8080" |
7 | - "5005:5005" |
然后docker-compose up -d就启动tomcat的调试模式
配置IDEA,连接远程服务器
点击Edit Configurations
配置Remote
点击debug,连接成功显示如下所示内容
JVM类加载器
类加载器简介
类加载器(class loader)用来加载 Java 类到 Java 虚拟机中。一般来说,Java 虚拟机使用 Java 类的方式如下:Java 源程序(.java 文件)在经过 Java 编译器编译之后就被转换成 Java 字节代码(.class 文件)。类加载器负责读取 Java 字节代码,并转换成 java.lang.Class 类的一个实例。每个这样的实例用来表示一个 Java 类。
基本上所有的类加载器都是 java.lang.ClassLoader 类的一个实例。java.lang.ClassLoader 类的基本职责就是根据一个指定的类的名称,找到或者生成其对应的字节代码,然后从这些字节代码中定义出一个 Java 类,即 java.lang.Class 类的一个实例。
Java 中的类加载器大致可以分成两类,一类是系统提供的,另外一类则是由 Java 应用开发人员编写的,开发人员可以通过继承 java.lang.ClassLoader 类的方式实现自定义类加载器,以满足一些特殊的需求。
系统提供的类加载器主要有下面三个:
- 引导类加载器(Bootstrap ClassLoader):负责将 $JAVA_HOME/lib 或者 -Xbootclasspath 参数指定路径下面的文件(按照文件名识别,如 rt.jar) 加载到虚拟机内存中。它用来加载 Java 的核心库,是用原生代码实现的,并不继承自 java.lang.ClassLoader,引导类加载器无法直接被 java 代码引用。
- 扩展类加载器(Extension ClassLoader):负责加载 $JAVA_HOME/lib/ext 目录中的文件,或者 java.ext.dirs 系统变量所指定的路径的类库,它用来加载 Java 的扩展库。
- 应用程序类加载器(Application ClassLoader):一般是系统的默认加载器,它根据 Java 应用的类路径(CLASSPATH)来加载 Java 类。一般 Java 应用的类都是由它来完成加载的,可以通过 ClassLoader.getSystemClassLoader() 来获取它。
类加载过程 — 双亲委派模型
(1) 类加载器结构
除了引导类加载器之外,所有的类加载器都有一个父类加载器。应用程序类加载器的父类加载器是扩展类加载器,扩展类加载器的父类加载器是引导类加载器。一般来说,开发人员自定义的类加载器的父类加载器是应用程序类加载器。
(2)双亲委派模型
类加载器在尝试去查找某个类的字节代码并定义它时,会先代理给其父类加载器,由父类加载器先去尝试加载这个类,如果父类加载器没有,继续寻找父类加载器,依次类推,如果到引导类加载器都没找到才从自身查找。这个类加载过程就是双亲委派模型。
首先要明白,Java 虚拟机判定两个 Java 类是否相同,不仅要看类的全名是否相同,还要看加载此类的类加载器是否一样(可以通过 class.getClassLoader() 获得)。只有两个类来源于同一个Class文件,并且被同一个类加载器加载,这两个类才相等。不同类加载器加载的类之间是不兼容的。
双亲委派模型就是为了保证 Java 核心库的类型安全的。所有 Java 应用都至少需要引用 java.lang.Object 类,也就是说在运行的时候,java.lang.Object 这个类需要被加载到 Java 虚拟机中。如果这个加载过程由 Java 应用自己的类加载器来完成的话,很可能就存在多个版本的 java.lang.Object 类,而这些类之间是不兼容的。通过双亲委派模型,对于 Java 核心库的类加载工作由引导类加载器来统一完成,保证了 Java 应用所使用的都是同一个版本的 Java 核心库的类,是互相兼容的。
类加载器在成功加载某个类之后,会把得到的 java.lang.Class 类的实例缓存起来。下次再请求加载该类的时候,类加载器会直接使用缓存的类的实例,而不会尝试再次加载。
Java字节码技术
ASM
对于需要手动操纵字节码的需求,可以使用ASM,它可以直接生产 .class字节码文件,也可以在类被加载入JVM之前动态修改类行为。ASM的应用场景有AOP(Cglib就是基于ASM)、热部署、修改其他jar包中的类等。
先看ASM对字节码操作的过程图
JavaAssist
ASM虽然可以达到修改字节码的效果,但是代码实现上更偏底层,是一个个虚拟机指令的组合,不好理解、记忆,和Java语言的编程习惯有较大差距。
利用Javassist实现字节码增强时,可以无须关注字节码刻板的结构,其优点就在于编程简单。直接使用java编码的形式,而不需要了解虚拟机指令,就能动态改变类的结构或者动态生成类。
Instrumentation
上面JavaAssist有什么缺点?
上面ASM和JavaAssist的Demo,都有一个共同点:两者例子中的目标类都没有被提前加载到JVM中,如果只能在类加载前对类中字节码进行修改,那将失去其存在意义,毕竟大部分运行的Java系统,都是在运行状态的线上系统。
Java Instrumentation指的是可以用独立于应用程序之外的代理(agent)程序来监测和协助运行在JVM上的应用程序。这种监测和协助包括但不限于获取JVM运行时状态,替换和修改类定义等。简单一句话概括下:Java Instrumentation可以在JVM启动后,动态修改已加载或者未加载的类,包括类的属性、方法。要使用instrument的类修改功能,我们需要实现它提供的ClassFileTransformer接口,定义一个类文件转换器。
先看下其关键方法
1 | public interface Instrumentation { |
2 | //添加一个类文件转换器 |
3 | void addTransformer(ClassFileTransformer transformer); |
4 | //重新加载一个类,加载时触发ClassFileTransformer接口 |
5 | void retransformClasses(Class<?>... classes) throws UnmodifiableClassException; |
6 | } |
我们需要实现ClassFileTransformer接口,并在自定义的transform方法中,利用ASM或者JavaAssist等字节码操作框架对类的字节码进行修改,修改后返回字节码的byte[]数组
自定义实现如下ClassFileTransformer,过滤掉类名不是AopDemoServiceWithoutInterface的类,同时使用JavaAssist对AopDemoServiceWithoutInterface进行增强
1 | public class MyClassTransformer implements ClassFileTransformer { |
2 | |
3 | public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException { |
4 | if (!className.equals("aop/demo/service/AopDemoServiceWithoutInterface")) { |
5 | return null; |
6 | } |
7 | try { |
8 | System.out.println("MyClassTransformer,当前类名:" + className); |
9 | ClassPool classPool = ClassPool.getDefault(); |
10 | CtClass ctClass = classPool.get("aop.demo.service.AopDemoServiceWithoutInterface"); |
11 | CtMethod ctMethod = ctClass.getDeclaredMethod("sayHelloFinal"); |
12 | ctMethod.insertBefore("{ System.out.println(\"start\");}"); |
13 | ctMethod.insertAfter("{ System.out.println(\"end\"); }"); |
14 | return ctClass.toBytecode(); |
15 | } catch (Exception e) { |
16 | e.printStackTrace(); |
17 | } |
18 | return null; |
19 | } |
20 | } |
JavaAgent
光有Instrumentation接口还不够,如何将其注入到一个正在运行JVM的进程中去呢?我们还需要自定义一个Agent,借助Agent的能力将Instrumentation注入到运行的JVM中
Agent是JVMTI的一种实现,Agent有两种启动方式:
- 一是随Java进程启动而启动,经常见到的java -agentlib就是这种方式;
- 二是运行时载入,通过attach API,将模块(jar包)动态地Attach到指定进程id的Java进程内。
SecurityManager沙箱分析
简介
安全管理器(SecurityManger)是为了保护JVM在运行有漏洞或恶意的代码不会破坏外部资源,这是api级别的,可自定义的安全策略管理器。
安全管理器(SecurityManger)在java中的作用就是检查操作是否有权限执行,是java沙箱的基础组件。通过Java命令行启动的java应用程序,默认不启用沙箱。要启动沙箱,需要:
1 | java -Djava.security.manager <other args> |
也可以指定策略文件:
1 | java -Djava.security.policy=<URL> |
如果要求启动时只遵循一个策略文件,启动需要双等号,如下:
1 | java -Djava.security.policy==<URL> |
还可以在代码中使用硬编码System.setSecurityManager()来启动安全管理器
安全策略文件
策略文件制定了具体的代码权限。可以使用jdk自带的policytool工具查看或编辑。
java.policy有三个条目,每一条在java.policy文件中为一条grant记录,每一个grant记录含有一个codeBase(指定代码)及其permission(许可):
1 | grant codeBase source { |
2 | permission permission_class_name ation; |
3 | } |
每一条grant记录遵循下面格式:
- 以保留字“grant”开头,表示一条新的记录开始。
- “permission”也是保留字,标记一个新的许可开始。
- 每一个grant记录授予一个指定的代码(CodeBase)一套许可(Permissons)。
- source指定目标类的位置
- ation用于指定目标类拥有的权限
source三种通配符:
- directory/ 表示directory目录下所有.class文件,不包括.jar文件
- directory/* 表示directory目录下所有的.class及.jar文件
- directory/- 表示dierctory目录下的所有.class及.jar文件,包括子目录
权限
权限定义的格式包含三部分:权限类型、权限名和允许的操作。例:
1 | // 权限类型 |
2 | permission java.security.AllPermission |
3 | |
4 | // 权限类型+权限名 |
5 | permission java.loang.RuntimePermission "stopThread"; |
6 | |
7 | // 权限类型+权限名+允许的操作 |
8 | permission java.io.FilePermission "/tmp/test" "read" |
所有权限列表
类型 | 权限名 | 操作 | 例子 | ||
---|---|---|---|---|---|
文件权限 | java.io.FilePermission | 文件名(平台依赖) | 读、写、删除、执行 | 允许所有文件的读写删除执行:permission java.io.FilePermission “<< ALL FILES>>”, “read,write,delete,execcute”; | |
套接字权限 | java.net.SocketPermission | 主机名:端口 | 接收、监听、连接、解析 | 允许实现所有套接字操作:permission java.net.SocketPermission “:1-“,”accept,listen,connect,resolve”; | |
属性权限 | java.util.PropertyPermission | 需要访问的jvm属性名 | 读、写 | 读标准java属性:permission java.util.PropertyPermission “java.”,”read”; | |
运行时权限 | java.lang.RuntimePermission | 多种权限名 | 无 | 允许代码初始化打印任务:permission java.lang.RuntimePermission “queuePrintJob” | |
AWT权限 | java.awt.AWTPermission | 6种权限名 | 无 | 允许代码充分使用test类:permission java.awt.AWTPermission “createTest”;permission java.awt.AWTPermission “readDisplayPixels”; | |
网络权限 | java.net.NetPermission | 3种权限名 | 无 | 允许安装流处理器:permission java.net.NetPermission “specifyStreamHandler”; | |
安全权限 | java.security.SecurityPermission | 多种权限名 | 无 | ||
序列化权限 | java.io.SerializeablePermission | 2种权限名 | 无 | ||
反射权限 | java.lang.reflect.ReflectPermission | suppressAccessChecks(允许利用反射检查任意类的私有变量) | |||
完全权限 | java.security.AllPermission | 无(拥有执行任何操作的权限) |
SecurityManager的原理与影响
一般API设计到安全管理器的原理:
- 请求java api
- java api使用安全管理器判断许可权限
- 通过则顺序执行,否则抛出Exception
例如JDK源码中的FileInputStream类,如果开启沙箱,则安全管理器不是null,检查checkRead(name)。而checkRead方法则是依据访问控制策略的一个权限检查。
###如何破坏反序列化漏洞
对于java反序列对象漏洞利用来说,一般两种形式:
- 在classpath下寻找弱点jar包,通过gadget串联拼凑最终通过该反序列执行任意代码。 – 这种场景实际利用困难,一方面适合的gadget不容易找,另一方面业界已经披露有问题的三方件,产品一般都已升级
- 在classpath下寻找弱点jar包,结合JDNI注入,通过远程加载恶意类执行任意代码 – 这种手法是目前更有效的一种方法
可以通过安全策略限制文件执行权限,导致rce失败。
如何绕过SecurityManager
如果policy中设置存在如下规则:
1 | permission java.lang.RuntimePermission "createClassLoader"; |
则存在绕过可能性。
原理:当我们拥有建立一个自己的ClassLoader的权限,我们完全可以在这个ClassLoader中建立自己的一个class,并赋予一个新的SecurityManager策略,这个策略也可以是null,及关闭整个java安全管理器。核心在ClassLoader存在一个方法叫defineClass,defineClass允许接受一个参数ProtectionDomain,我们能够自建一个ProtectionDomain将自己配置好的权限设置进去,define出来的class则拥有新的权限。
参考:
https://www.cnblogs.com/coprince/p/8603492.html
https://segmentfault.com/a/1190000020248225?utm_source=tag-newest
https://www.runoob.com/maven/maven-tutorial.html
https://blog.csdn.net/belvine/article/details/89552524