简介

本篇是整个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
}

总结如下:

  1. 所有的类必须声明为public

  2. 所有属性为private

  3. 提供默认构造方法

  4. 提供getter和setter

  5. 实现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
        @Override
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
    @Override
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有两种启动方式:

  1. 一是随Java进程启动而启动,经常见到的java -agentlib就是这种方式;
  2. 二是运行时载入,通过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三种通配符:

  1. directory/ 表示directory目录下所有.class文件,不包括.jar文件
  2. directory/* 表示directory目录下所有的.class及.jar文件
  3. 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设计到安全管理器的原理:

  1. 请求java api
  2. java api使用安全管理器判断许可权限
  3. 通过则顺序执行,否则抛出Exception

例如JDK源码中的FileInputStream类,如果开启沙箱,则安全管理器不是null,检查checkRead(name)。而checkRead方法则是依据访问控制策略的一个权限检查。

###如何破坏反序列化漏洞

对于java反序列对象漏洞利用来说,一般两种形式:

  1. 在classpath下寻找弱点jar包,通过gadget串联拼凑最终通过该反序列执行任意代码。 – 这种场景实际利用困难,一方面适合的gadget不容易找,另一方面业界已经披露有问题的三方件,产品一般都已升级
  2. 在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