冰蝎v2.0.1-动态二进制加密网站管理客户端源码分析

冰蝎v2.0.1-动态二进制加密网站管理客户端源码分析

最近做到一道笔试题是关于冰蝎的,问题是

上传webshell使用冰蝎进行连接时报错,可能的原因有哪些

由于之前挖洞的时间比较少,所以我对webshell管理工具的使用还停留在菜刀和Cknife。所以先在谷歌上看了一些冰蝎的文章。虽然作者在先知平台发过了几篇文章介绍了冰蝎的使用,网上其他的文章则是关于冰蝎流量检测的文章。但是作者并没有在GitHub上公开源码,因为是用java写的,所以我在下载了之后扔到jd-gui里面看源码,还好作者也没有混淆。那么我就趁此机会阅读一下冰蝎的源码,学习作者的思路。

PS:由于我目前大三,能力方面有所欠缺。对冰蝎源码的阅读分析可能与原作者有所出入,本文仅为我对工具源码学习的思路,不足之处还望各位师傅批评指正。

下载了冰蝎解压之后,拖入jd-gui中先分析一下目录结构。

因为冰蝎是对流量进行AES加密的,所以冰蝎无法使用普通的一句话木马,需要用到server文件中对应的冰蝎的webshell。net中是工具的主要的代码,test为测试代码,其余则是一些工具用到的jar包。这里我们就只分析net中的代码,从冰蝎的代码目录结构来看,我们主要分析core和utils。

我花了近一周的时间阅读了冰蝎的源码,下面是我整理的一张冰蝎源码结构的粗略归纳分析。

core中是冰蝎的主要逻辑代码,这里我就挨着进行分析,先看Crypt和Decrypt的代码。

Crypt类

这里我发现一个奇怪的问题,这里有两个类Crypt和Decryp,起初我认为分别是加密类和解密类。但是看Crypt类中的方法,既包括了加密和解密的方法,而且在后面shellservice类中调用的时候只调用了Crypt类的方法,所以这里我很懵逼这个Decrypt类到底是干嘛的?

在Crypt类中分别有对应的不同server端语言的加解密方法。

继续看各自的加解密方法,这里以C#的为例。这里我看了网上的一些java的AES加密实现,代码都一个样,感觉这段代码直接CV就好了。

接着看php和asp的加密方法,根据类型不同加密,因为作者也说过,因为客户端集成了不同语言shell的管理,所以就复用了部分代码,下面php执行异或操作的操作就是直接调用asp的代码实现的。

由于冰蝎在通信过程中使用AES加密,Java和.Net默认支持AES加密,php环境中需要开启openssl扩展,v2.0更新以后,PHP环境加密方式根据服务器端支持情况动态选择,不再依赖openssl,使得冰蝎有了更大的发挥空间。也就是上面代码中不支持AES的情况就直接调用asp的代码。同样解密的方法也是相同的,就不赘述了。

因为Decrypt类的代码没有被调用过,所以我们就跳过Decrypt的代码。

Params类

因为作者在利用动态二进制加密实现新型一句话木马之Java篇中提到过,而且这个思路非常值得我们借鉴。

如果攻击者发送的请求不是文本格式的源代码,而是编译之后的字节码(比如java环境下直接向服务器端发送class二进制文件),字节码是一堆二进制数据流,不存在参数;攻击者把二进制字节码进行加密,防御者看到的就是一堆加了密的二进制数据流;攻击者多次执行同样的操作,采用不同的密钥加密,即使是同样的payload,防御者看到的请求数据也不一样,这样防御者便无法通过流量分析来提取规则。

所以要让服务端有动态地将字节流解析成Class,但Java并没有提供直接解析class字节数组的接口。不过classloader内部实现了一个protected的defineClass方法,可以将byte[]直接转换为Class。

所以作者很聪明地定义了一个类t继承classloader,调用时返回父类的defineClass方法,就解决了protected的限制,返回解析后的Class。当然这里用反射机制来实现也是可以的。这样就可以直接动态解析并执行编译好的class字节流了,但这里就必须提到java的asm框架。

ASM 是一个 Java 字节码操控框架。它能被用来动态生成类或者增强既有类的功能。ASM 可以直接产生二进制 class 文件,也可以在类被加载入 Java 虚拟机之前动态改变类行为。

需要执行的命令是编码在class文件中的,因为class是已编译好的文件。但不能每次执行命令时都编译payload,那么就需要payload可以接收参数。为了参数被waf拦截,自然也是二进制流,所以作者使用ASM框架来动态修改class文件中的属性值。从上面的代码就可以看出,只需要向getParamedClass方法传递payload类名、参数列表即可获得经过参数化的payload class。

我们接着看php的代码实现。

同理.net,asp的实现都是类似的,只是细节上有所不同,而.net的实现是使用作者先编译好的dll文件,具体可以参考作者的原文。

这里顺便提一下,因为在jd-gui中只能查看java代码,在payload中的代码就无法查看,所以直接导出整个项目的源码,即可在文件夹中看到了。

PluginUtils类

因为目前冰蝎还不支持插件,作者在这里留了一个插件类,我猜测以后的版本应该是会有支持的。

ShellEntity类

shell实体类,就是获取shell的具体参数信息,这个没什么好分析的。

ShellManager类

先看构造函数,检查数据库文件是否存在,数据库连接驱动等。看完该类的所有方法,发现都是操作数据库代码,大致如下也没什么好分析的。

本来我觉得这样的sql赋值语句太过冗余了,问了朋友之后才知道没有用框架,jdbc就只能这样写,太难了。

ShellService类

也就是连接shell之后的一些操作,也就对应的如下的界面,也算是较为核心的功能代码。

我们先看这个类的构造函数,当然也是对一些属性的赋值。当判断类型为php的时候还会多添加一个header头,然后合并两个header。

这里调用了Utils.getKeyAndCookie方法,我们跟进该方面,由于该方法的代码比较长,但又是比较重要的一个方法,这里我就直接贴代码了。

public static Map<String, String> getKeyAndCookie(String getUrl, String password, Map<String, String> requestHeaders)
throws Exception
{
disableSslVerification();
Map<String, String> result = new HashMap();
StringBuffer sb = new StringBuffer();
InputStreamReader isr = null;
BufferedReader br = null;
URL url;
URL url;
if (getUrl.indexOf("?") > 0) {
  url = new URL(getUrl + "&" + password + "=" + new Random().nextInt(1000));
} else {
  url = new URL(getUrl + "?" + password + "=" + new Random().nextInt(1000));
}
HttpURLConnection.setFollowRedirects(false);
HttpURLConnection urlConnection;
String headerValue;
HttpURLConnection urlConnection;
if (url.getProtocol().equals("https"))
{
  if (Main.currentProxy != null)
  {
	HttpURLConnection urlConnection = (HttpsURLConnection)url.openConnection(Main.currentProxy);
	if ((Main.proxyUserName != null) && (!Main.proxyUserName.equals("")))
	{
	  String headerkey = "Proxy-Authorization";
	  String headerValue = "Basic " + 
		Base64.encode(new StringBuilder(String.valueOf(Main.proxyUserName)).append(":").append(Main.proxyPassword).toString().getBytes());
	  urlConnection.setRequestProperty(headerkey, headerValue);
	}
  }
  else
  {
	urlConnection = (HttpsURLConnection)url.openConnection();
  }
}
else if (Main.currentProxy != null)
{
  HttpURLConnection urlConnection = (HttpURLConnection)url.openConnection(Main.currentProxy);
  if ((Main.proxyUserName != null) && (!Main.proxyUserName.equals("")))
  {
	String headerkey = "Proxy-Authorization";
	headerValue = "Basic " + 
	  Base64.encode(new StringBuilder(String.valueOf(Main.proxyUserName)).append(":").append(Main.proxyPassword).toString().getBytes());
	urlConnection.setRequestProperty(headerkey, headerValue);
  }
}
else
{
  urlConnection = (HttpURLConnection)url.openConnection();
}
for (String headerName : requestHeaders.keySet()) {
  urlConnection.setRequestProperty(headerName, (String)requestHeaders.get(headerName));
}
if ((urlConnection.getResponseCode() == 302) || 
  (urlConnection.getResponseCode() == 301))
{
  String urlwithSession = ((String)((List)urlConnection.getHeaderFields().get("Location")).get(0)).toString();
  if (!urlwithSession.startsWith("http"))
  {
	urlwithSession = 
	  url.getProtocol() + "://" + url.getHost() + ":" + (url.getPort() == -1 ? url.getDefaultPort() : url.getPort()) + urlwithSession;
	urlwithSession = urlwithSession.replaceAll(password + "=[0-9]*", "");
  }
  result.put("urlWithSession", urlwithSession);
}
boolean error = false;
String errorMsg = "";
if (urlConnection.getResponseCode() == 500)
{
  isr = new InputStreamReader(urlConnection.getErrorStream());
  error = true;
  errorMsg = "密钥获取失败,密码错误?";
}
else if (urlConnection.getResponseCode() == 404)
{
  isr = new InputStreamReader(urlConnection.getErrorStream());
  error = true;
  errorMsg = "页面返回404错误";
}

从函数名来看getKeyAndCookie,可以知道是获取密钥和cookie值。先设置了禁止跟随跳转,因为后面会对Location进行判断。代码中if (getUrl.indexOf("?") > 0)先判断url中是否有?,因为?后面都是跟参数的,没有则直接将password和一个0-1000之间的伪随机数拼接进去。接着判断当前当前协议为https或http时是否设置代理,并添加对应的header后连接。未设置代理则直接连接。

发起请求后,如果返回的状态码为301或者302则从header头中取出Location的值,也就是urlwithSession。为了对代码有更深刻的理解,我们直接抓包分析流程,这里我用的Charles来分析流量,先在冰蝎上设置代理。

然后打开Charles再连接我们的shell,观察流量同时发送了四个请求。

我们看到发起了两次http://127.0.0.1/shell.php?pass=*的请求,第一次请求就是判端server端返回的状态码,如果是200,再判断是否能拿到密钥。所以第二次请求的密钥才是后面所使用的AES加密的密钥,第一次则是测试。

所以冰蝎在正式连接之前会发送一个GET请求,服务端如果正常会响应一个16位的字符串。冰蝎每一次连接请求都会向服务端发送一次GET请求获取16位的密钥,这16位的字符串就是密钥,在客户端和服务端的通信过程中使用密钥进行加密以达到免杀的目的。

逐步分析代码,发现在getKeyAndCookie方法中实际是调用getRawKey方法,我们跟进该方法的代码。

对比这两个的方法发现代码大致相同,同样这两个方法都很长,我觉得这里作者的代码过于冗余,对于相同功能的代码可以再封装到另一个方法去调用即可。这就是上面为什么会向shell.php请求两次密钥的原因,第一次是测试请求,由getKeyAndCookie方法发起,当无误后再次调用getRawKey获取真正的密钥。

从上面的这两个方法的代码差不多可以回答我们文章开头的那个问题了,我们借用作者源码来回答。

  1. 500,密钥获取失败,密码错误

  2. 404,shell文件不存在

  3. 页面存在,但是无法获取密钥

  4. 请求返回异常,这个要根据Exception信息来判断

我们再回到shellservice代码中,对照着工具中的各项功能来看代码实现。首先是基本信息。

一开始我本来是相对Utils类方法的代码进行单独的分析。但是shellservice中大量调用该类的方法,所以这里我就直接跟踪分析调用的方法,后面就不再单独分析了。跟进分析Utils.getData方法的代码。

接着跟进requestAndParse方法。

可以看到它发起请求实际是调用它下面的那个sendPostRequestBinary方法来实现的,只是多了解析数据的功能。

在该方法中设置了Content-Type为application/octet-stream,但是我们在抓包分析的过程中发现php的都是application/x-www-form-urlencoded,这是因为在前面分析shellservice的构造方法时,作者判断类型为php的时候进行了替换,所以php和另外三者的Content-Type不同。接着看命令执行的代码。

同样观察文件管理的代码,也就是显示,上传,下载,删除之类的方法,其实都是一样的,而且文件的上传和下载也是加密传输的,通过调用Utils.requestAndParse方法(该方法完成加密编码),传递参数,最后解密再base64解密拿到结果。

同样虚拟终端的功能就是传递一个路径进去可以是cmd也可以是powershell然后执行回显,和上面的过程没有本质的区别。然后是代码执行,因为在前面的代码中分析过java都是通过传输payload的class字节码,然后让服务端解析的,同样这里也是调用Utils.getClassFromSourceCode将代码转换为class。

还有socks代理,虚拟终端功能其实就已经部分实现了内网穿透的能力,在Shell环境里做的所有事情都是在内网环境中的。不过为了方便使用其他工具,客户端还提供了基于一句话木马的Socks代理功能。我觉得这里作者的介绍挺不错的,因为我在本地试这个功能没啥用,直接放上他的动图。

对比了代码之后发现其实实现的原理是一样的。大部分的功能的代码差不多也就分析到这里。

总结:

分析完了冰蝎的核心代码之后,再回过头来看冰蝎的原理以及流程就清晰多了,这里还是用原作者的流程图。

阅读冰蝎的源码我大概花了一周的时间,当然我个人能力上还有很多欠缺,虽然我对java不算很熟,但是为了学习作者的一些思路,还是硬着头皮啃硬骨头。对工具源码的分析以及参考网上一些前辈们对冰蝎流量的分析检测思路,真的学到了很多。对于作者能利用加密流量绕waf的思路真的很棒,避免了传统意义上的对代码的各种变形,从而一劳永逸。而且作者在jsp马时能想到利用传输class字节码给server动态执行,这样的思路可算得上是精巧绝伦!当然从开发者的角度来说,我觉得作者的部分代码有些冗余,还可以再优化一些。总的来说,冰蝎算是一款非常优秀的安全工具了。

最后,希望自己以后也能开发出这样优秀的工具,为安全圈做出一些自己的贡献。

zgao

如果有什么技术上的问题,可以加我的qq 1761321396 一起交流。

目前为止有一条评论

Briansoift 发布于1:59 上午 - 3月 31, 2020

Sustain the good job and producing in the crowd!