前言
好久都没有发文章了,其实在这段日子里写了很多文章,也做了很多调试,每次都是思考到最后或者做到最后的时候没有成色,导致文章也没有发出来。不过webscoket这个好歹能够收个尾,就发出来看看。
这篇文章跨度很大,看我的idea背景也看得出来(x,其实在开头我提到的某个connector层的内存马就死在了这里,不能够实战化使用其实是比较蛋疼的地方。然后我看了一下veo师傅的原项目,居然已经提到了,并且其实思路上面已经差不多能对上了,但是真正实现起来我考虑了很多不需要考虑的问题,导致最终问题堆积起来就解决不了,还是基本功太薄弱了。
websocket前置
经过之前内存马的洗礼,其实不管是未知类型的探索还是对于已知类型的复现和学习,通性应该是了解为什么要探究这个类型,以及这个类型怎么使用,如何动态构造
那首先就是我们为什么需要WebSocket型内存马
由于其双端通信的特性,websocket并不局限于java这一种利用场景,现在大部分语言都支持了websocket协议的API。且websocket区别于http请求不同的点是在它的通信阶段,建立连接握手阶段还是通过http来的,因此兼容性很强,只不过后面的通信阶段采用的是纯TCP请求,默认端口也是80和443
所以其实一段websocket请求发送过来的时候,前半段的处理还是走Http的逻辑,tomcat最终还是决定将Websocket请求的处理放在filterchain中。如果有师傅使用过一些内存马查杀工具在看filters时应该会注意到一个默认的WsFilter一直存在。且由于filter在tomcat判断当前请求路由是否为404异常的前面,所以这里不论是什么请求都会走一遍WsFilter的dofilter,只不过优先级是最后的
可想而知这一个过程是tomcat在初始化的就进行了的,接下来看看一个正常业务中如何标准自建使用WebSocket处理逻辑,然后跟进这个filter进行调试
在tomcat中有两种自建websocket的方式,一种是通过注解实现(tomcat启动时会自动进行对应的注解扫描),一种是通过继承Endpoint
类实现,不过要自己写serverConfig。
通过注解写自定义ServerEndpoint之后,tomcat在创建container层的流程中会走到standardContext的startInternal方法,其中有一段关于Call ServletContainerInitializers的操作。这其实也是Servlet 3.0开始规范下的用来初始化servlet和listener的逻辑。而遍历到的WsSci全程叫做WebSocketServerInitializer,他的onstartUp方法中会对当前项目下打了@ServerEndpoint
注解的类进行一个注册和加载
这一段还会涉及到我们后面动态构造websocket,同时也可以稍微理解一下tomcat下的websocket创建和使用
所以我们可以先采用注解实现,建立连接先调试
1 | package org.test.WebSocketGood; |
然后可以利用一些工具进行websocket连接,比如postman,点击send Message就能够发送文本消息,在OnMessage方法中打上断点就能够看到调用栈了
动态添加自定义websocket探索
0x01 WsFilter判断处理
websocket初次建立链接的时候还是采用的http握手的方式,而一开始的建立握手的基础就是WSFilter的逻辑能够正常走,其中我们会遇到很多if判断和从内存取值的操作来判断我们当前的请求是否是websocket请求,以及当前是否有自定义serverEndpoint进行了注册
一开始进入WSFilter就遇到了一段if判断
1 | if (!sc.areEndpointsRegistered() || !UpgradeUtil.isWebSocketUpgradeRequest(request, response)) { |
如果说我们没有注册自定义serverendpoint的话,或运算前半段就直接满足条件进入if逻辑按正常的http请求过了
所以这里sc属性(WsServerContainer)的areEndpintsRegistered就必须返回true,也就是有serverendponint的注册
1 | boolean areEndpointsRegistered() { |
WsServerContainer
唯一给endpointsRegistered赋值为true的地方在 void addEndpoint(ServerEndpointConfig sec, boolean fromAnnotatedPojo)
方法中,可以看到其有一个封装方法只需要传入指定的ServerEndpointConfig对象就能够完成整个自定义ServerEndpoint的操作
构造ServerEndpointConfig
所以现在问题初步落到ServerEndpointConfig对象如何构造上,构造好之后直接通过反射调用addEndpoint进行参数传递进去即可,观察其结构
本身是一个接口,所以最终构造还是落实在其实现类上。这里可以看到他有一个子类Builder,按理来说就是用来构造具体的ServerEndpointConfig的,他的build方法也的确如此
1 | public ServerEndpointConfig build() { |
所以现在的思路就是通过反射获取到ServerEndpointConfig接口的子类Builder,然后反射调用其build方法获得一个DefaultServerEndpointConfig。然后调用WsServerContainer的addEndpoint方法
动态获取WsServerContainer
WsServerContainer就是喜闻乐见的全版本构造获取的关键,如何动态的获取当前运行状态下的WsServerContainer呢?
这里可以在WsServerContainer的构造方法打个断点,然后观察tomcat初始化的过程,我们最开始的探索websocket创建的时候其实已经稍微摸到边了,在WsSci调用onstartUp方法注册servlet和listener的时候,第一段就是init方法对WsServerContainer进行创建,并且将其set进了servletContext
最终setAttribute是设置进了ApplicationContext的attributes属性值。这里取值带有很明显的tomcat版本特征,且键名已经被标记上了@Deprecated,之后版本肯定要换一个构造
所以这里只需要通过standardContext获取到ApplicationContext即可,而standardContext的getServletContext()方法能够直接获取到当前Container中的ApplicationContext
具体构造如下,省略了遍历线程获取standardContext的步骤以及通过传入Evil字节码构建恶意Endpoint的步骤,避免内容冗余,最核心的一段内容如下:
1 | public void addEndPoint(Object context,Object Endpoint) throws Exception{ |
getFV和invokeMethod分别是反射获取(会遍历父类)对象以及反射调用方法。最后的poc我会放在github中,然后是javax改包之后的poc
1 | public void addEndPoint(Object context,Object Endpoint) throws Exception{ |
这里修改完之后就可以开始测试,我们可以先封装一个测试类,为了方便,我这里就直接写一个filter里面用来触发实时的注入逻辑
1 |
|
get去触发一下对应的filter路由,然后连接ws后之后发送命令即可
多功能shell实现
现在网上应该找不到能够跑的websocket样本,不过听说某些实验室肯定已经用了很久了,为了保险起见我也不会发出来所有的改造思路,效果图如下,为了能够快速的举一反三,这里我先还用的是godzilla的二开,因为熟悉一点,不用从头看逻辑。
首先明确思路,我们不论是godzilla,还是shell本身都应该去做对应的修改。
先看shell本身
Websocketmemshell改造
java这块的godzilla的shell功能实现是通过加载恶意类之后进行各种方法反射调用实现的。流程整体是先初始化,传输过来一份巨大的payload字节码,然后交给shell进行类加载,之后存入缓存session。后续再调用test功能探测shell存活,由于刚才已经缓存过payload字节码了,这里检测到session中有缓存,于是直接取出,进行字节码加载,然后调用其基础测试功能方法,通过返回值来判断是否存活。
实现逻辑如下:
1 |
|
在onOpen处我这边设置了接受的数据长度和session的缓存。因为初始化传过来的payload实在是太大了,所以这里有必要写个最大接受数量。
shell实际可以说的都在注释里面了,相较于纯http组件有Request这种比较方便的参数载体有所不同。但websocket本质还是http建立连接,然后tcp双端通信,所以我们还是可以获取到http的一些参数的,只不过后续的参数传递不是http的传递,而是纯字节流,所以类似于Request对象的这种传参键值对,我们只能够在获取之后将其存入WSsession的usersProperties中,这一点在后续的godzilla改造中也有所体现。
Godzilla改造
这里记得引入websocket-client依赖,用以通信
webSocket-Cryption新增
我二开的思路就是能够新增分装功能就新增,而不是在原有的基础上改逻辑。不过有的时候不得不改,最多用ifelse这种判断走不同情况。
类似于javaAESbase64,websocket我们也可以封装一个功能类。
Godzilla客户端的流程我们可以看shellEntity的initShellOpertion
方法,最原始的initShellOpertion中只有这些内容,也就是一个大的if-else,本质还是通过判断当前是否有加载cache来进行CryptoModel的init和payload的init
这里我们从头到尾去微调和改造,肯定是先没有缓存的,也就代表着payload此时并没有加载进shell,所以要先通过CryptoModel去传输payload,对应的逻辑在this.cryptionModel.init(this);
当然,这么讲肯定迷糊,我们具体到代码看
看一眼JavaCryptoModel的结构,拿最基础的AesBase64举例
最终的是init方法,后续除了generate方法(跟jsp马生成有关)我们不会接触到,其他都会讲一遍。这里以init入手,可以看到在加密功能初始化完成之后就开始获取payload并发送给shell端了。
所以真正的godzilla初始化payload加载是在Crypto模块。
那么我们应该怎么改呢?
- 首先就是shellEntity中要写个判断,判断当前的Crypto模块是否为websocket类型,然后进获取websocketcrypto的逻辑
- websocketCryptoModel中肯定要发送payload,所以我们还先得写一个websocketpayload
- 与shell通信也要封装一个类websocketClient,用来sendpayload
理清楚之后发现websocketCrypto模块简直寸步难行,因为上述两个关键性类我们还没写完:websocketClient以及webscoketpayload。
这里不先提Crypto模块又不好提上述这两个,所以我们接下来讲这两个,最后再回过头来看
webscoketClient新增
新增之前肯定是先类比,我们类比Http
Http的发送连接功能对外暴露出sendHttpResponse
方法,然后层层往里进,封装参数,最外层是具体要传入的数据,具体在crypto中初始化表现为this.payload,所以我们也可以写一个类似的封装功能点。总结一下几个sendHttpResponse的逻辑,两个点,封装请求头,连接超时。这两个websocket都不需要去做,所以我们可以直接写成如下形式:
sendRequestData
我们要处理一下超时卡死的问题。websocketClient存在onClose
和onError
以及onMessage
方法。三个方法分别都会自动触发于各自的情况。而websocket一段接受到信息以及关闭连接或异常的时候,虽然在正常的websocket双端通信的时候不一定就代表测试客户端一定发送过消息了,但是在godzilla中由于我们的思路是用websocket模拟这个http的过程,所以是可以认为,只要这三个方法触发,当前客户端一定就发送过消息,所以可以设置一个sendFlag。onClose
onError
onMessage
就将其制为1,发送的时候就将其置为0。这么做的目的也是为了解决双端通信延迟的问题,当我们发送的消息被接受之后,shell端肯定会走onMessage,然后传信息回来。此时由于异步,我们Godzilla客户端的onMessage
就会被触发,并且sendFlag被设置为0,所以sendRequestData方法中的while循环判断其为0了,发送成功,所以退出等待循环,返回接收到的result。(这里我设置的值是50毫秒,这肯定是不专业的,只是为了测试避免不被卡死)
1 | public byte[] sendRequestData(byte[] requestData) { |
onMessage和onClose具体如下
1 | public void onMessage(ByteBuffer bytes){ |
websocketClient其实解的差不多了,继续看websocket-payload
websocket-payload改造
还是一样的对比,看javapayload,基本的东西就不解释了,之前的godzilla功能分析也讲的差不多了。我提几个关键点
首先一点就是equals传值的时候调用handle进行类型处理写缓存,它是要根据具体的传入类型来写的。但是websocket的server端中是不存在直接获取servletRequest的手段的,但是存在javax.websocket.session
(tomcat版本不同包名不同),能够通过session间接获取到http的一些属性值
服务端原本的javapayload的euqals与handle逻辑如下
这里其实是可以获取到Request的,但是Request到底只是一个填充基本信息和打log的作用,equals最重要的是将结果字节数组还有servletContext传入payload缓存。一个是执行功能的结果,一个是插件加载。所以这里我就直接暴力改
1 | if (this.supportClass(obj, "%s.websocket.Session")) { |
这么改的理由是:我拒绝在payload中获取request为了碟醋包饺子,所以将所有的请求值都通过wsSeesion间接传入写缓存。然后在shell外部我可以这么处理session.getUserProperties().put("parameters", data);
也就是初始化之后,test或者其他功能发送过来之后,由于我客户端发的也是字节流,不在乎参数键值对,所以直接put进parameters。然后再通过equals将session传入之后,这里再通过反射取出这个字节流,赋值给retVObject即可,此时requestdata又会取得retVObject的值,传值处理就完毕了。后续toString甚至都不用处理
这个payload改好之后要稍微改一下Godzilla的Javashell内容,因为它默认的获取payload是读取原始的payload字节,这里写个判断,然后让他读取我们刚才写好的websocketpayload就行
现在完事具备,payload和client端都已经写好,只差CryptoModel了。回过头看其逻辑处理,要改两处地方
一是在由于我们初始化payloadModel是因为我们在payloadModel-JavaShell的getPayload方法中写了一段通过shellEntity来判断当前加密模块的操作,但是在此之前我们是没有调用过PayloadModel的init的,所以shellEntity没有传入,会抛出空错误。为了解决这个问题,我们要在CryptoModel初始化之前就将payloadModel初始化了
还有一个就是,由于我们没有写类似于httpResponse这种专门调回显的,所以CryptoModel也要承担一部分回显的功能,具体操作就是将sendWebsocketResponse
的结果进行接受处理
除开前面一样的加密,具体如下
1 | this.shell.getPayloadModule().init(context); |
最后还有一段JavaShell的客户端改造,因为他跟payload本地class一起结合起来才能叫作websocketPayloadModel。得追溯到shellEntity部分,如果按照上面的思路改造完毕,那么肯定最后会来到payloadModel的test方法进行payloadclass是否在目标加载完毕的检测
此时的payloadModel是对应整个语言类型,JavaShell整个功能都是和payloadClass进行镜像对照的,只不过JavaShell最终还是要发数据和方法名过去,所以最终是通过一个方法完成的,其余所用功能只是封装了一下功能名称和参数,然后统一交给evalFunc去处理
那evalFunc其实也很简单了,只是把方法名和参数传递过去而已,这里记得GzipEncode一下,然后我们写一个判断当前请求是否是websocket请求,如果是就走webscoketClient的通信流程,将返回的数据进行两层decode之后就是结果了
至此改造完毕
总结
改一个原始godzilla没什么用。我做这个事情的目的是为了理顺自己的构造思路,因为确实在网上搜不到其他的高可用实战化的websocket马,那只能自己从头开始搓了。这篇文章也是属于记录性质的,可能会稍微细节一点。但是我并没有放出所有代码,把思路讲清楚我觉得就差不多。那之后的进一步改造就可以顺着这个思路走了,想怎么二开就是自己的事情了,希望能够帮助到师傅们。