哥斯拉流量加解密浅析(jsp篇)
瓜皮雪 安全工具 142662浏览 · 2021-11-26 15:45

0x00 简介

此篇文章针对jsp版本的哥斯拉webshell进行分析, 主要分析对应JAVA_AES_BASE64载荷, JAVA_AES_RAW载荷分析类似。另外此篇文章分析采用的哥斯拉版本为V3.03, 但是该版本和V4.01版本不论是客户端还是服务端处理均无太大不同, 甚至哥斯拉V3.03和V4.01的服务端jsp代码都一模一样, 请放心食用。

0x01 哥斯拉源码解析

哥斯拉webshell文件生成分析

哥斯拉webshell生成功能在菜单栏的管理 -> 生成

点击生成选项后, 会弹出webshell配置选项让我们进行选择, 其中包含了密码、密钥、有效载荷和加密器,这四大选项分别代表的含义如下:

密码: 哥斯拉客户端发起请求时的参数名, 例如若密码为pass, 则哥斯拉客户端请求包格式为: pass=请求密文; 若密码设置为test, 则哥斯拉客户端请求包格式为: test=请求密文。

密钥:哥斯拉加密数据所用到的密钥, 请注意该密钥并不是直接用来进行加密处理, 在进行加密处理前该密钥还需经过一道步骤, 即 md5(密钥).substring(0,16) ,处理后的结果就是用来加密的真正的密钥。

有效载荷: 有效载荷表示webshell的类型, 如果选择JavaDynamicPayload, 则表示webshell是jsp或者jspx类型; 如果选择PhpDynamicPayload, 则表示webshell是php类型。

加密器:加密器表示选择的有效载荷的加密方式, 也就是选择要生成的webshell在发送和接受请求时采用那种方式加密数据包。如果有效载荷选择JavaDynamic, 加密器选择了JAVA_AES_BASE64, 则表示访问生成的jsp或者jspx文件时, 数据使用AES + BASE64 的方式进行加密传输。

选择好webshell的配置选项后, 点击生成, 然后指定生成的文件后缀, 就可以在选定的目录下生成我们所需要的webshell文件, 这里我密码密钥使用默认的, 有效载荷选择 JavaDynamicPayload, 加密器选择 JAVA_AES_BASE64, 生成一个1.jsp文件。将该文件部署在个人虚拟机搭建的tomcat服务器上, 用来模拟成功上传哥斯拉木马。

webshell已经生成并成功部署后, 我们来分析一下哥斯拉木马生成的代码逻辑。

哥斯拉webshell生成的配置界面为GenerateShellLoder.java, 我们查看该文件。在该文件中我们可以发现生成webshell的按钮点击事件是通过 automaticBindClick.bindJButtonClick方法来调用反射机制来动态绑定点击事件的, 即绑定的点击事件为 generateButtonClick 函数, 我们查看该函数。

该函数首先判断生成webshell的密码、密钥、有效载荷以及加密器是否都已填写, 如果都填写之后则通过Application.getCryption 函数获取到一个Cryption加密器的实例, 然后调用该实体的generate方法生成webshell文件的字节数组。因Cryption是接口类型, 具体generate功能根据选择的加载器不同实现方式不同, 因为我们选择的是JAVA_AES_BASE64加载器, 因此我们查看JavaAesBase64这个加载器类来查看具体generate功能实现。

查看JavaAesBase64类的generate方法实现如下:

该generate方法调用Generate.GenerateShellLoder方法, 传入password, md5后在截取前16字符的secretKey(也就是之前说的密钥在真正使用前所需要经过的处理), 以及一个boolean类型参数, 该参数用来指定是读取JAVA_AES_BASE64模板, 还是JAVA_AES_RAW模板, 若为false 则表示读取JAVA_AES_BASE64模板。我们进入该Generate.GenerateShellLoder方法查看。

该方法首先会从 shells/java/template/ 资源目录下读取两个模板文件的内容, 根据传入 isBin 参数的boolean 值来决定读取的是 rawGlobalCode.bin 以及 rawCode.bin 还是读取base64GlobalCode.bin 以及 base64Code.bin 文件。因为JavaAesBase64类型调用Generate.GenerateShellLoder方法传入的isBin参数为false, 因此我们以传入false来分析这个Generate.GenerateShellLoder方法, 传入true的情况类似。shells/java/template/ 资源目录中的文件如下:

在读取完 base64GlobalCode.bin 以及 base64Code.bin 文件内容后, 首先用传入 Generate.GenerateShellLoder 方法时的password 和处理过的 secretKey 来替换从base64GlobalCode.bin 模板读取出来的 {pass} 以及 {secretKey} 字符, base64GlobalCode.bin 模板内容如下:

字符替换完成后, 由用户选择生成的文件后缀是jsp结尾还是jspx结尾。根据用户选择的后缀来读取 shells/java/template/ 资源目录下的 shell.jsp, 或者jspx文件。这里我们选择jsp后缀, 读取的是 shells/java/template/ 资源目录下的shell.jsp 文件。之后程序用处理过后的 base64GlobalCode.bin内容以及读取到的base64Code.bin 内容来分别替换shell.jsp文件中的{globalCode} 以及 {code}字符, 替换成功后就会在用户指定的目录下生成真正的哥斯拉jsp版本的webshell文件。(如果用户选择了哥斯拉的上帝模式, 会对读取到的shell.jsp采用不同的处理方法, 但为了不复杂化, 这里我们采用普通模式的处理方式。)哥斯拉上帝模式切换界面和 shells/java/template/ 资源目录下的shell.jsp文件的内容如下:

由此我们可以看出, 哥斯拉webshell文件基本上是写死的, 只是根据每次生成webshell时传入的 pass 和 secretKey 不同, 来替换模板文件中的 {pass} 以及 {secretKey} 字符, 也就是说每次哥斯拉生成的webshell文件除了 pass 和 secretKey可能不一样之外, 剩下的内容全都是一模一样的, 其中包括了流量加解密算法。

哥斯拉测试连接请求过程分析

分析完哥斯拉webshell生成流程后, 我们来分析一下哥斯拉的请求过程。首先我们将之前生成的1.jsp文件放置到本地虚拟机搭建的tomcat服务器上:

哥斯拉客户端请求webshell有多种类型, 有测试webshell连接情况、发送命令执行请求等, 但是请求包加解密方式是一致的, 这里我们首先分析添加webshell到客户端时, 测试webshell连接情况的请求加解密, 添加webshell到客户端的界面如下:

当我们在添加webshell界面配置好要连接的webshell信息后, 点击测试连接, 如果可以成功连接配置的webshell, 则提示 success:

为了对连接webshell流量进行, 我们在webshell配置界面设置http代理到burpsuite:

点击测试连接后, 会发送三个请求包到服务端的1.jsp文件。所发送的请求包与响应包都是经过加密的密文:

接下来我们对这测试webshell连接情况的代码进行分析。配置webshell的界面文件为ShellSetting.java 文件:

查看该java文件代码, 我们会发现哥斯拉作者依旧使用 automaticBindClick.bindJButtonClick 方法来绑定点击事件, 测试连接按钮所对应的鼠标点击事件调用的方法为testButtonClick 方法:

该方法首先调用 updateTempShellEntity() 方法来对shellContext对象进行重新赋值, 这里主要是将webshell配置界面修改的内容同步到shellContext对象上, (shellContext对象是shellEntity类型, 用来记录webshell配置信息)

ShellEntity类型定义如下:

更新好shellContext对象后, 调用该对象的initShellOperation方法:

该方法首先初始化一个Http对象赋值给ShellEntity对象的http成员变量(每个ShellEntity对象都有自己的http成员变量, 用于各自ShellEntity的Http请求), 然后初始化payloadModel 和 cryptionModel 对象, payloadModel对应于 Payload接口类型, cryptionModel 对应于上文提及的 Cryption接口类型。这里主要对Payload 接口类型进行讲解, 首先查看一下 Payload 接口类型定义:


从该 Payload接口类型定义的方法我们可以看出 Payload接口类型负责定义webshell管理工具的功能动作, 而 Cryption接口类型负责定义webshell管理工具的加解密行为。上文中Cryption接口的实现类型是JavaAesBase64类型, 这里的Payload接口的实现类型为JavaShell类型, 如下( 因Cryption以及Payload具体的实现类型要讲解的话还需要涉及到哥斯拉初始化扫描payloads 以及cryptions 目录, 然后添加加密器、载荷与类的对应关系到HashMap等内容, 这篇文章主要涉及哥斯拉流量加解密, 为避免复杂化文章内容, 因此这里直接给出此处Payload接口的实现类型为JavaShell类型)

初始化好payloadModel 以及 cryptionModel 对象后, 首先调用cryptionModel 的init 和 check方法, 也就是调用 JavaAesBase64类的 init 和 check方法。首先查看init方法:

该方法首先获取到传入该方法的ShellEntity对象的Http成员变量, 然后下面有一段md5操作, 这一操作用于哥斯拉响应包流量的解密, 这一内容会在下文分析哥斯拉响应包解密时讲解。获取到http成员变量后, 因为该http成员变量有ShellEntity对象的相关信息, 包括连接url、代理、password、secretKey等内容, 即保存有对应的webshell配置信息, 因此可以直接对相关的webshell发起请求。但是发起请求还需要请求内容, 这里的请求内容是通过获取ShellEntity对象的payloadModel, 然后调用payloadModel的getPayload() 方法来拿到。这里的payloadModel也就是JavaShell对象, 我们进入该对象的getPayload() 方法:

在该方法中首先读取 shells/java/assets/payload.class 类文件的字节码, 然后将内容传递到dynamicUpdateClassName 方法, 我们进入该方法:

该方法实际上就是根据不同情况往 dynamicClassNameHashMap 中添加内容, 但是对传入的classContent 不会造成太多影响, 我们可以理解为该方法返回的内容就是从 shells/java/assets/payload.class 类文件中读取到的字节码。即JavaShell对象调用的getPayload()方法就是获取 shells/java/assets/payload.class 类文件字节码。我们回到JavaAesBase64类的init方法, 获取到 payload.class类的字节码内容后, 该init方法将获取到的字节码内容通过http请求发送到服务端。那么问题来了, 哥斯拉请求是加密的, 这里并没有调用加密算法对获取到的字节码进行加密, 那么加密流程是在哪里, 我们进入发送http请求的sendHttpResponse 方法:

发现是方法重载, 还会调用重载的sendHttpResponse方法, 继续进入重载的 sendHttpResponse方法:

又是方法重载, 继续进入重载的sendHttpResponse 方法:

我们可以看到代码行:

byte[] requestData2 = this.shellContext.getCryptionModel().encode(requestData);

这里通过getCryptionModel()方法 获取到 ShellEntity 的Cryption对象, 不同的ShellEntity会采用不同的加密算法, 也就是不同的Cryption对象, 然后调用该对象的encode方法对传输的流量进行加密, 所有哥斯拉请求流量的加密都是在这里进行处理的。对于请求响应的后续处理, 我们在下文的请求响应包加解密时再进行分析, 到这里我们就完成了 this.cryptionModel.init(this) 代码行的分析:

我们接下来分析 this.cryptionModel.check 方法, 该方法很简单, 这里实际上就是调用JavaAesBase64的 check 方法:

这里会返回 JavaAesBase64的state成员变量的值, 而该值在之前调用 JavaAesBase64的init方法时会涉及到, 如果init方法调用成功, 也就是上文所讲述的请求发起成功后, 会将该state值设置为true:

this.cryptionModel.check方法返回true之后, 会开始调用 this.payloadModel.init() 方法, 也就是调用JavaShell的init方法, 我们进入该方法:

该方法会根据传入的ShellEntity实例赋值shell成员变量, 获取shell对象的Http对象来赋值http成员变量, 然后获取shell对象的编码方式(UTF-8、GBK等)赋值encoding成员变量。执行完成 this.payloadModel.init() 方法后, 我们开始查看this.payloadModel.test() 方法, 也就是调用JavaShell的 test 方法:

该方法调用evalFunc() 方法, 进入该方法:

这里传入的className是null, 因此不进行if语句中的处理操作, if语句判断过后, 向parameter参数添加 methodName 和 funcName的键值对配对, 之后将parameter参数进行formatEx() 格式化处理后获取到byte数组, 然后进行gzip压缩, 压缩完成后将数据通过sendHttpResponse方法向服务器发送请求。请求完成后从服务器获取到返回结果, 然后将结果进行gzip解压缩, 解压缩后的内容为byte数组, 作为evalFunc的返回值。而test()方法就是对evalFunc方法返回的byte数组进行字符串转化, 如果转化后生成的字符串内容是ok, 则返回true。这里的evalFunc发送的请求是第二次发送的请求, 服务器返回的内容解密后就是ok:

好, 到了这里我们回到之前的testButtonClick方法:

上文我们讲述的大部分内容都是该testButtonClick方法的 this.shellContext.initShellOperation() 语句, 接下来要执行的语句就是 this.shellContext.getPayloadModel().test() 语句, 这语句在 this.shellContext.initShellOperation() 中已经执行过一次了, 也是发送一段test请求给服务器, 服务器会返回ok字符串, 这就是在burpsuite中看到的第三次请求。所以我们在burpsuite会发现第二次请求和第三次请求是一模一样的:

到了这里我们就分析完testButtonClick()方法, 也就是在哥斯拉webshell配置界面点击测试连接时的处理流程, 哥斯拉的其他请求过程类似, 大家可以自行研究, 接下来我们来对哥斯拉请求包响应包加密进行分析, 在上文我们已经知道了哥斯拉请求数据的加密统一在sendHttpResponse方法中进行处理, 因此下文的分析结果在哥斯拉其他请求中可以直接使用, 一键解密, 不单单局限于测试连接的请求加解密。

哥斯拉请求包加密过程分析

接下来我们来分析哥斯拉请求包加密流程, 这里我们主要针对JAVA_AES_BASE64载荷的加密算法, 从上文我们可以找到加密统一在sendHttpResponse中调用Cryption接口encode方法进行数据加密, 在这里就是调用JavaAesBase64的encode方法, 我们进入该方法:

我们可以看到加密过程如下:

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

所以我们要对请求包加密的内容进行解密只要按照加密流程逆着来就可以了:

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

我们利用该解密思路解密第一个请求包, 发现可以成功解密, 但因为该请求包原始内容是class文件的字节码, 因此转化为String类型输出后会存在乱码。

接着用同样的解密思路解密第二个请求包, 输出的内容是一段乱码, 发现事情并不简单:

从上文我们可以知道所有发送的数据请求包都是统一进行加密的, 唯一不同的就是两次请求的数据来源, 第一次请求发送的是class文件的字节码, 是直接读取class文件的内容, 而第二次与第三次发送的请求是哥斯拉客户端自己构造的数据, 猜测哥斯拉客户端可能构造了自己的数据请求格式, 我们回到哥斯拉发送数据请求的位置, 即evalFunc方法位置处:

我们可以看到传入的数据是经过gzip压缩的, 那么如果我们对解密后的数据进行gzip解压缩可以获取到真正的数据内容吗, 发现可以成功获得数据内容:

但是依旧有乱码信息, 说明解密的还不够全面, 秉持着研究的精神, 我们继续对发送的请求内容进行查看。请求的内容通过 parameter.formatEx() 方法获得, 我们进入该方法:

方法中调用serialize() 方法, 进入serialize 方法:

parameter对象通过add方法添加的键值对都存储到parameter对象的hashMap成员变量中。serialize方法实际上就是把hashMap中的键值对转化为 byte[] 返回, 这里返回的byte[] 就是哥斯拉请求所发送的原始数据。该方法对hashMap的处理方式如下:

取出hashMap中的第一个键值对
    ->  获取键值的byte[]数组, 写入到 outputStream输出流
    ->  写入 2 的 byte值 到outputStream输出流
    ->  定义一个byte[4]数组, 大小固定为4位, 用该数组来表示键值对中值所代表的的字节数组长度, 将该byte[4]数组写入到 outputStream输出流
    ->  获取键值对中值的字节数组, 写入到 outputStream输出流

取出hashMap中的第二个键值对
    ....
重复操作, 直到hashMap中的键值对都写入后, 将outputStream输出流转化为byte[] 返回

因此我们通过解密获取到请求包内容后(以byte[]方式表示), 要得到有效内容, 也就是仅保留键值对的内容, 将多余内容删除掉, 因此我们可以定义一个格式化函数, 用来格式化解密后的请求包内容, 该格式化函数处理数据流程如下:

一个字节一个字节读取解密后的请求包 byte[]
    直到读出2, 在2出现之前的内容存储到一个名为key的byte[]中, 用来存储key
    将读到的2丢掉
    在2之后读取四个字节, 通过读到的四个字节来计算获得键值对的值字节长度
    读取计算得到的字节长度, 将内容存储到一个名为value的byte[]中, 用来存储value
    这就算是完成了第一个键值对的读取, 之后重复该流程, 读取其他在数据包中的键值对

将获取到的键值对以 键=值 的形式输出, 我们来运行一下结果(gzip解压缩操作已经在该格式化函数中进行处理)

成功解析数据, 通过该解密流程进行工具书写:

用burpsuite抓取命令执行的流量, 进行解析, 发现可成功解析:

另外该解密思路也可以解密jspx文件请求包, jspx的命令执行请求包略有不同:

哥斯拉响应包加密过程分析

接着我们来分析哥斯拉响应包加密过程。哥斯拉响应包数据加密在服务端脚本文件上进行处理, 也就是我们之前生成jsp脚本。该脚本文件分两部分代码, 上半部分用来定义加载器类和加解密算法, 下半部分则是具体的程序处理代码。

查看程序处理代码, 我们可以看到服务端对数据加密的流程也是先进行aes加密, 然后进行base64编码, 但是在数据发送之前还要经过一个步骤, 首先计算 md5( pass + xc)的值, 然后取计算值的前16位放置到加密数据的前面, 然后取计算值的后16位放置到加密数据的后面, 以此来组成真正发送的响应包数据。也就是说要对响应包进行处理的话, 首先去掉响应包数据的前16位以及后16位, 然后对剩下的数据进行 base64解码 + aes解密, 之后gzip解压缩即可。我们可以编写程序进行尝试:

解密jspx脚本响应包加密数据

我们会发现成功解密数据。除了看服务器端如何处理之外, 我们也可以从哥斯拉客户端中查看客户端对服务端返回请求的处理方式来获取解密算法。我们以上文讲述的发送第二次请求包所调用的test方法为例:

该方法调用evalFunc方法, 进入该方法:

该方法中调用了sendHttpResponse 方法发送请求, 同时根据getResult方法来获取响应包内容, 我们进入getResult方法:

可以发现该方法直接获取result成员变量的byte[]值, 但这显然不是我们想要的, 我们需要处理该result成员变量的过程, 因此我们重新进入sendHttpResponse方法查看具体的处理过程:

重载函数, 继续进入:

重载函数, 继续进入:

我们可以看到这里 sendHttpResponse 方法调用了 sendHttpConn 方法, 我们进入该方法:

该方法返回了一个new HttpResponse(), 而之前我们获取服务端响应的getResult方法就是HttpResponse对象的方法, 我们进入该方法:

该方法调用了handleHeader方法, 该方法主要用于Cookie的处理, 如果此次请求服务端webshell没有Cookie的话, 会将服务端返回的cookie保存起来, 下次请求服务器webshell会带上该cookie, 所以我们可以看到在第一次请求之后, 第二次第三次请求, burpsuite上面抓到的包都带有cookie:

接着我们查看ReadAllData方法:

我们可以看到在这个方法中调用了decode方法进行解密, 并将解密的结果赋值给result成员变量, 也就是说上文通过getResult获取到的result变量是在这里进行赋值的。另外这里调用decode方法也给了我们一个信息, 也就是说不单单是数据加密, 数据解密在哥斯拉客户端也是在进行统一处理的。这里调用的decode的方法, 对应于调用JavaAesBase64类的decode方法, 我们进入该方法:

我们可以看到在该方法中就写明了对服务端数据的处理过程, 和我们之前得出的结果一样, 也是先base64解码, 然后aes解密获得。但是在进行base64解码之前还需要通过findStr方法对获取到的服务端数据进行预处理, 我们进入该方法:

我们会发现该方法用到了findStrLeft 和 findStrRight, 找到这两个成员变量赋值的地方:

我们会发现这两个成员变量的值分别对应于 md5 (pass + key) 后的前16位以及后16位的内容。然后我们继续看findStr方法中调用的subMiddeStr方法:

这个subMiddleStr方法正好就是截取findStrLeft以及findStrRight字符串中间的部分, 这就和我们之前得出的去掉响应包返回内容的前16位以及后16位正好匹配上了。由此可以得出对服务端响应包的内容进行解密流程为:

计算出 md5 (pass + key), 对响应包截取该md5后的前16位以及后16位
    对截取后的响应包内容进行 base64解码 +  aes解密  + gzip解压缩

可能有人会疑惑gzip解压缩是在那里看出来的, 我们回到最开始evalFunc方法, 我们可以看到在这里调用了gzip解压缩方法对解密后的响应包数据进行解压缩:

到了这里顺便拓展一下每次连接时的第一个请求包所发送的class类文件的字节码的作用。我们可以对该class类字节码文件进行反编译, 反编译出的java文件包含了大量的webshell服务端管理所对应的处理方法。




我们回到生成的webshell进行查看, 我们可以看到在服务端webshell对接收到的class类字节码进行加载类, 加载到session中, 之后用户请求webshell, 服务端都从用户请求所对应的session中取出该类, 初始化对象, 然后调用该类中的方法进行相关方法的调用。所以在第一次请求后服务端会返回cookie, 而之后哥斯拉客户端发送请求会带上该cookie, 该cookie就是用来给服务端进行识别并加载session中的类。

另外查看反编译出class文件, 我们可以看到对客户端发起请求的数据进行处理的方法都写在里面, 例如对解密后的客户端数据进行格式化的 formatParameter 方法:

0x02 后记

  • 关于jsp服务端和客户端的其他连接请求处理, 命令执行, 文件管理等;

  • 关于哥斯拉asp、php的webshell处理相关内容等;

  • 关于冰蝎等其他类型webshell管理工具等;

    后续会写对应分析文章。

5 条评论
某人
表情
可输入 255