java内存马回显初探
在学习java内存马回显时,总是在复现别人的利用链,就想着我们自己该怎么去找构造链呢。在网上找资料的时候发现了https://github.com/c0ny1/java-object-searcher/,真的非常感谢大神们提供的寻找思路。为了检验之前学习的反射与算法基础,本篇文章详细讲解一下自己写搜索程序的过程。
环境说明
- Tomcat/8.5.83
- java version “1.8.0_152”
- idea 2023.1
前言
我们再写内存马时,比如写linsener内存马时,需要先获取当前应用的StandardContext,回显的时候需要获取当前应用当前进程的response。又因为:
- Request对象可以获取到StandardContext,Request.getContext()。
- Request对象也可以获得response,例如tomcat的Request具体实现类org.apache.catalina.connector.Request里面包含protected Response response;
所以我们如果能获取到当前访问的Request,我们就可以实现写内存马和进行回显。
准备知识
存储Request的位置
通常情况下,java-web作为多线程应用,Request对象通常存在在线程里面,我们可以通过Thread.currentThread() 来获取到当前的线程对象。
例如我们写一个简单的jsp,println的位置下断点,我们通过idea自带的评估表达式(Evaluate),执行Thread.currentThread()看一下。
1 2 3 4
| <% String s = "sss"; System.out.println(s); %>
|
我们发现获取到的确实是当前线程的对象,他的类型为org.apache.tomcat.util.threads.TaskThread。TaskThread继承了Thread。
反射获取TaskThread里面所有变量
我们知道反射可以获取对象的字段、字段值、字段类型。反射学习详见:https://f19t.github.io/2023/02/19/Java%E5%8F%8D%E5%B0%84/
所以这里我们可以写一个servlet程序获取到TaskThread的所有字段以及字段属性。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| import org.apache.tomcat.util.threads.TaskThread;
import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.lang.reflect.Field; import java.lang.reflect.Modifier;
@WebServlet("/simple") public class simple extends HttpServlet { @Override public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException { TaskThread t = (TaskThread) Thread.currentThread(); Field fields[] = TaskThread.class.getDeclaredFields(); for (int i = 0; i < fields.length; i++) { fields[i].setAccessible(true); try { System.out.println("字段名="+fields[i].getName()); System.out.println("字段值="+fields[i].get(t)); System.out.println("字段类型="+fields[i].getType()); int mod = fields[i].getModifiers(); System.out.println("声明类型="+ Modifier.toString(mod)); System.out.println("----------------------------"); } catch (IllegalAccessException e) { throw new RuntimeException(e); } } } }
|
1 2 3 4 5 6 7 8 9 10
| 输出: 字段名=log 字段值=org.apache.juli.logging.DirectJDKLog@397d3266 字段类型=interface org.apache.juli.logging.Log 声明类型=private static final ---------------------------- 字段名=creationTime 字段值=1677745450986 字段类型=long 声明类型=private final
|
为什么只输出了两个字段呢,那是因为TaskThread类里面确实只有这两个字段,但是我们调试的时候发现TaskThread里面有很多字段呀,这是因为TaskThread继承一些父类,我们要想获得TaskThread的所有字段,还得去遍历他们的父类,为了方便查看字段在哪个类里面,我们也打印一下类名。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| import org.apache.tomcat.util.threads.TaskThread;
import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.lang.reflect.Field; import java.lang.reflect.Modifier;
@WebServlet("/simple") public class simple extends HttpServlet { @Override public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException { TaskThread t = (TaskThread) Thread.currentThread(); try { Class clazz = Class.forName("org.apache.tomcat.util.threads.TaskThread"); for (; clazz != Object.class; clazz = clazz.getSuperclass()) { Field fields[] = clazz.getDeclaredFields(); for (int i = 0; i < fields.length; i++) { fields[i].setAccessible(true); System.out.println("类名=" + clazz); System.out.println("字段名="+fields[i].getName()); System.out.println("字段类型="+fields[i].getType()); int mod = fields[i].getModifiers(); System.out.println("声明类型="+ Modifier.toString(mod)); System.out.println("字段值="+fields[i].get(t)); System.out.println("----------------------------"); } } } catch (ClassNotFoundException e) { throw new RuntimeException(e); } catch (IllegalAccessException e) { throw new RuntimeException(e); } } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| 结果: 类名=class org.apache.tomcat.util.threads.TaskThread 字段名=log 字段类型=interface org.apache.juli.logging.Log 声明类型=private static final 字段值=org.apache.juli.logging.DirectJDKLog@21d6ac54 ---------------------------- 类名=class org.apache.tomcat.util.threads.TaskThread 字段名=creationTime 字段类型=long 声明类型=private final 字段值=1677750206025 ---------------------------- 类名=class java.lang.Thread 字段名=name 字段类型=class java.lang.String 声明类型=private volatile 字段值=http-nio-8080-exec-5 ---------------------------- 类名=class java.lang.Thread 字段名=priority 字段类型=int 声明类型=private 字段值=5 ---------------------------- ........ ........后面字段省略
|
到这里我们获取了所有TaskThread的字段,字段的种类有很多我们需筛选,显然java提供的八种基础类型不是我们需要的。我们需要的类型是引用数据类型,也就是可能存在对象或者集合(集合里面可能存在其他类)。
这里我们可以创建一个黑名单,在黑名单里面的我们不进行输出。到这里我们的Object的处理逻辑就清晰了,先判断是否是黑名单,如果是普通对象那么就进行取值,如果是集合类型或者是数组类型的我们将数组内的所有值都取出来。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| public static boolean isblack(String s) { Boolean aBoolean = false; List<String> black = new ArrayList<String>(Arrays.asList("java.lang.Byte", "java.lang.Short", "java.lang.Integer", "java.lang.Long", "java.lang.Float", "java.lang.Boolean", "java.lang.String", "java.lang.Class", "java.lang.Character", "java.io.File", "byte", "short", "int", "long", "double", "float", "boolean" ));
for (int i = 0; i < black.size(); i++) { if (s == black.get(i)) { aBoolean = true; return aBoolean; } } return aBoolean; }
|
1 2 3 4 5 6 7 8 9
| public static boolean is_list(String s) { boolean b = false; if ("List".equals(s) || "ArrayList".equals(s)){ b = true; return b; } return b; }
|
1 2 3 4 5 6 7 8 9
| public static boolean is_map(String s) { boolean b = false; if ("Map".equals(s) || "HashMap".equals(s)){ b = true; return b; } return b; }
|
这里我们使用广度优先搜索算法去遍历所有的变量。这里我们定义一个类似于c语言结构体的类,用来存储状态。
1 2 3 4 5 6 7 8 9 10
| public class location { public location(Object obj, String clazz, String path) { this.obj = obj; this.path = path; this.clazz = clazz; } public Object obj; public String path; public String clazz; }
|
既然是广度优先搜索,我们还需要一个队列,表示有哪些需要进行搜索,还需要一个SET,set会记录哪些被搜索过了,防止被重复搜索。
1 2
| static Queue<location> queue = new LinkedList<location>(); static Set<Object> set = new HashSet<Object>();
|
我们再创建一个白名单
1 2 3 4 5 6 7 8 9 10
| public static boolean is_target(Field f,Object o) throws IllegalAccessException, ClassNotFoundException { boolean b = false; if ( f.get(o).getClass().isAssignableFrom(Class.forName("org.apache.catalina.connector.RequestFacade")) && f.get(o).getClass().getName() !="java.lang.Object" ){ return true; } return b; }
|
接下来我们设计广度优先搜索的入口:
1 2 3 4 5 6 7 8 9 10
| public static void search() throws ClassNotFoundException { location laco = new location(Thread.currentThread(), "org.apache.tomcat.util.threads.TaskThread","TaskThread"); queue.offer(laco); set.add(Thread.currentThread()); int i=0; while (queue.size()>0) { i++; basic_search.basic_search(queue.poll()); } }
|
这里看一下写的basic_search()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90
| public static void basic_search(location lcat) throws ClassNotFoundException {
Object obj = lcat.obj; String path = lcat.path; Class clazz = Class.forName(lcat.clazz);
try {
for (; clazz != Object.class; clazz = clazz.getSuperclass()) {
Field fields[] = clazz.getDeclaredFields();
for (int i = 0; i < fields.length; i++) { fields[i].setAccessible(true); if (fields[i].get(obj) == null){continue;}
if (!Black.isblack(fields[i].getType().getName()) && fields[i].get(obj) != null && set.add(fields[i].get(obj))) { if (judge_map.is_map(fields[i].getType().getSimpleName())) { Map map = (Map) fields[i].get(obj); if (map.size() > 0) { for (int map_num = 0; map_num < map.size(); map_num++) { Object map_obj = map.get(map_num); if (map_obj != null && set.add(map_obj) && !Black.isblack(map_obj.getClass().getName())) { location l = new location(map_obj, map_obj.getClass().getName(), path + "-->" + fields[i].getName() + "[" + map_num + "]"+map_obj.getClass().getName());
queue.offer(l); }
} } continue; } else if (judge_list.is_list(fields[i].getType().getSimpleName())) { List list = (List) fields[i].get(obj); if (list.size() > 0) { for (int list_num = 0; list_num < list.size(); list_num++) { Object list_obj = list.get(list_num); if (list_obj != null && set.add(list_obj) && !Black.isblack(list_obj.getClass().getName())) { location l = new location(list_obj, list_obj.getClass().getName(), path + "-->" + fields[i].getName() + "[" + list_num + "]"+list_obj.getClass().getName());
queue.offer(l); }
} } continue; } else if (fields[i].getType().isArray()) {
try { Object[] arrobj = (Object[])fields[i].get(obj); if (arrobj.length > 0) { for (int obj_num = 0; obj_num < arrobj.length; obj_num++) { Object arr_obj = arrobj[obj_num]; if (arr_obj != null && set.add(arr_obj) && !Black.isblack(arr_obj.getClass().getName())) { location l = new location(arr_obj, arr_obj.getClass().getName(), path + "-->" + fields[i].getName() + "[" + obj_num + "]"+arr_obj.getClass().getName());
queue.offer(l); }
} } }catch (Throwable e){
}
continue; } if (target.is_target(fields[i],obj)) {
System.out.println(path+"--->"+fields[i].getName()+"("+fields[i].get(obj).getClass().getName()+")");
} location l = new location(fields[i].get(obj), fields[i].get(obj).getClass().getName(), path+"--->"+fields[i].getName()+"("+fields[i].get(obj).getClass().getName()+")"); search.queue.offer(l);
} } } } catch (IllegalAccessException ex) { throw new RuntimeException(ex); } }
|
全部完整代码上传至https://github.com/f19t/java-Object-simple-search/
因为我们为了让回显和写内存马更方便,我们这里只搜了RequestFacade,我们简单写一个simple去搜索一下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| @WebServlet("/simple") public class simple extends HttpServlet { @Override public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException { try { search.search();
} catch (ClassNotFoundException e) { throw new RuntimeException(e); } } }
|
得到的输出是:
1 2 3 4 5 6 7 8
| TaskThread--->group(java.lang.ThreadGroup)-->threads[14]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)--> processors[0]org.apache.coyote.RequestInfo---> req(org.apache.coyote.Request)--> notes[1]org.apache.catalina.connector.Request---> applicationRequest(org.apache.catalina.connector.RequestFacade)
|
这里就表明我们已经成功搜索并拿到了当前线程的RequestFacade。有了RequestFacade我们就可以拿到StandardContext,下面操作在 if (target.is_target(fields[i],obj)) {} 里面执行
1 2 3 4 5 6 7
| RequestFacade reqfd = (RequestFacade) fields[i].get(obj); Field f = reqfd.getClass().getDeclaredField("request"); f.setAccessible(true); Request req = (Request) f.get(reqfd); StandardContext context = (StandardContext) req.getContext(); ServletRequestListener listener = new ServletRequestListener() {}; context.addApplicationEventListener(listener);
|
也可以拿到当前线程的response
1 2 3 4 5 6 7 8 9
| RequestFacade reqfd = (RequestFacade) fields[i].get(obj); Field f = reqfd.getClass().getDeclaredField("request"); f.setAccessible(true); Request req = (Request) f.get(reqfd); Field ff = req.getClass().getDeclaredField("response"); ff.setAccessible(true); Response resp = (Response) ff.get(req); PrintWriter out = resp.getWriter(); out.println("wwwwww");
|
我们可以很方便的把上面的代码组装成一个类,这样我们反序列化的时候可以使用加载字节码的方式执行我们的回显代码或者添加内存马。
完整代码
1
| https://github.com/f19t/java-Object-simple-search/blob/main/search-object/src/main/java/com/f19t/searchobject/searchobject/search_utill.java
|
反序列化简单利用
我们把要执行的代码放到构建的AbstractTranslet类里面。
1 2 3
| Class f = Class.forName("org.hang.web_test.location"); Method method = f.getMethod("search"); method.invoke(null);
|
生成序列化数据,这里用的cc6反序列化执行代码。