Play Framework 服务端插件,同时提供 Godzilla 与 Command 两类内存马。插件放在宿主
plugins/ 目录即可在启动时被 PluginBootstrap 自动装载,无需修改宿主代码。构建目标是
3.2.0-SNAPSHOT 的宿主 SDK。
| Play 版本 | HTTP 实现 | shellType | shellTool |
|---|---|---|---|
| 2.8.x | Akka HTTP 10.1.x | AkkaHandler |
Godzilla / Command |
| 3.0.x | Pekko HTTP 1.0.x | PekkoHandler |
Godzilla / Command |
AkkaHandler 与 PekkoHandler 共用同一份注入器和 shell 字节码;运行期按请求对象的包名
(akka.* / org.apache.pekko.*)选择目标 API,因此同一个插件 jar 同时覆盖两代 Play。插件通过
getToolMappingOverrides() 把这两个类型挂到内置工具上,不注册新的工具名,界面里的配置项与宿主
自带工具完全一致。
依赖全部来自远程仓库,不需要 includeBuild,也不需要本地发布 SDK。settings.gradle.kts
从 Maven Central 取发行版(shelltool 3.0.0 / 3.1.0 等),从 Sonatype snapshots 取
3.2.0-SNAPSHOT 的宿主 SDK:
https://central.sonatype.com/repository/maven-snapshots
本地和 CI 使用同一组仓库,避免开发机的 mavenLocal() 里残留一份不同的快照。仓库里没有
includeBuild,可以直接推到 GitHub。
./gradlew :plugin:test # 单元测试
./gradlew :plugin:jar # 插件 jar
./gradlew :integration-test:test单元测试校验 PlayFrameworkServer 的注册信息:两个 Play 类型到同一个注入器的映射、
Godzilla / Command 的内置工具覆盖、Java 8 下限、play.core.server 包名、handle /
matches 契约,以及 META-INF/services 里的 SPI 条目。
集成测试会启动两个真实靶场(range-playframework2、range-playframework3),把生成的注入器
通过 POST /vul/load 投递进目标进程,再用 Godzilla 协议客户端和 Command 请求验证连通性:
pluginRegistersServerAndTools:插件 jar 被发现,PlayFramework服务端注册成功,内置Godzilla与Command在它下面都有AkkaHandler/PekkoHandler两种类型。serverRaisesTargetToJava8AndUsesPlayPackage:getMinimumTargetJreVersion()把默认目标抬到 Java 8,getShellPackageName()让 shell 落在play.core.server下。godzillaShellStartsAndAnswers:首个请求定义 payload,后续请求返回ok。commandShellExecutes:表单参数与查询参数都能执行命令并回显。
Godzilla 客户端是仓库内的 GodzillaTestClient,直接实现 AES/md5 协议。官方的
io.github.beichendream:godzilla:4.01 是约 32 MB 的桌面端发行包,打包了 FlatLaf、GUI 和
BouncyCastle,没有可直接调用的无头客户端 API,因此没有作为测试依赖引入。
@BeforeAll 先跑 PluginBootstrap.validate(pluginsDir),确认模板、契约、generator 构造器都完整,
再执行 PluginBootstrap.load();校验失败时测试直接报出具体条目,不用等到生成期才暴露。
靶场只是注入载体:/vul/load 接收 base64 类字节并 defineClass + 实例化,模拟一个表达式执行
入口。真实环境用任意代码执行点加载 MemShellResult#getInjectorBytes() 即可。
.github/workflows/ci.yml 在 push 到 master、PR 和手动触发时运行四个作业:
unit-test:./gradlew :plugin:test。integration-test:构建插件并跑 Play 2.8 / 3.0 两套靶场的端到端用例。package:前两个作业通过后构建插件 jar,生成SHA256SUMS,上传为 GitHub Actions artifactmemshell-party-playframework-plugin。verify-artifact:下载刚上传的 artifact,校验 SHA-256 和 jar 内容,然后通过./gradlew :integration-test:test -PpluginJar=<下载的 jar>把 CI 产物重新注入两个靶场。 这一步保证产物不仅能下载,还能实际生成并连接内存马。
本地取回产物:
gh run list --workflow=ci.yml --branch=master
gh run download <run-id> -n memshell-party-playframework-plugin -D downloaded
cd downloaded && sha256sum -c SHA256SUMSintegration-test 的 preparePlugins 默认复制 :plugin:jar;传 -PpluginJar=<path> 时改为复制
指定的 jar,verify-artifact 就是用这个入口验证下载件。
mkdir -p <host>/plugins
cp plugin/build/libs/memshell-party-playframework-plugin-3.2.0-SNAPSHOT.jar <host>/plugins/宿主启动时插件目录默认是 <启动目录>/plugins,可用 -Dmemshell.plugins.dir=... 或环境变量
MEMSHELL_PLUGINS_DIR 覆盖。
插件把 shell 类型挂在内置 Godzilla / Command 上,宿主 classpath 必须有
memshell-party-shelltool-godzilla 与 memshell-party-shelltool-command(boot 与 gui-app 的默认发行版
已包含)。插件本身不再编译依赖这两个模块。shell 模板由 ByteBuddyShellGenerator 通过
AbstractServer#getInjectorTemplateClassLoader() 解析,宿主 core 需要带该修复的 3.2.0-SNAPSHOT;
getMinimumTargetJreVersion() 与 getShellPackageName() 也要求 3.2 宿主。
内存马需要读取请求实体、改写流阶段字段并定义 payload 类,目标 JVM 必须放开对应模块:
--add-opens java.base/java.lang=ALL-UNNAMED
--add-opens java.base/java.util=ALL-UNNAMED
--add-opens java.base/java.util.concurrent=ALL-UNNAMED
ShellConfig shellConfig = ShellConfig.builder()
.server("PlayFramework")
.shellTool("Godzilla") // 或 "Command"
.shellType("PekkoHandler") // Play 3.x;Play 2.x 用 "AkkaHandler"
.targetJreVersion(52)
.build();
Map<String, Object> params = new HashMap<>();
params.put("pass", "playpass");
params.put("key", "0123456789abcdef");
MemShellResult result = new MemShellGenerator().generate(
shellConfig, InjectorConfig.builder().urlPattern("/*").build(), params);
// 投递:result.getInjectorBytes() 是单类产物,不含内部类。Command 工具用 paramName 指定参数名,查询串、请求头、application/x-www-form-urlencoded
表单三种位置都可取参。
- 注入点:
akka.stream.impl.fusing.MapAsync[Unordered].f(Pekko 同名)。每次onPush都通过$outer.f()读取该字段,替换为scala.Function1动态代理即可在路由前拦截请求, 未命中的请求原样透传给原 handler。 - 定位:从当前线程与 Akka/Pekko 的
ForkJoinWorkerThread出发,按对象图 BFS 找到流阶段 对象;同时收集Materializer与ActorSystem,找不到时用Materializer$.matFromSystem兜底。 - 实体物化:Play 请求实体默认是流式的,需要
entity.toStrict(3000, materializer)。这一步 会阻塞,因此 shell 逻辑在独立线程执行,避免 Play 2.8 的小调度器在MapAsync回调里死锁。 - 响应构造:手工拼
StatusCode、ByteString、HttpEntity.Strict、Seq与HttpResponse,再包装为Scala Future返回给流阶段。 - 单类投递:注入器本身兼作
InvocationHandler与后台任务的Runnable代理,shell 也不含 内部类,所以一次请求只投递一份类字节。 - SDK 3.2 钩子:
getToolMappingOverrides()把两个 Play 类型挂到内置Godzilla/Command, 工具名、配置项和界面条目都与宿主一致。shell 模板由ByteBuddyShellGenerator通过AbstractServer#getInjectorTemplateClassLoader()解析,插件不用再把 config 类搬进插件加载器。getMinimumTargetJreVersion()声明 Java 8 下限,调用方给更低值时由生成入口自动抬高;getShellPackageName()把 shell 放进play.core.server,不再落到通用org.apache.http.web.handlers。集成测试用PluginBootstrap.validate()在生成前校验模板与契约。
- 反应式服务端的请求/响应适配 SPI。当前 shell 契约默认拿 Servlet 的 request/response;本插件
只能为 Akka/Pekko 重写整套 Godzilla/Command 协议适配(取参、物化、构造响应、异步包装)。
建议增加 server-neutral 的
ShellRequest/ShellResponse或matches/handle生命周期, 让同一份工具实现复用到 Servlet、Netty、Akka 等多种传输。 - shell 的“快速匹配 + 异步处理”契约。反应式服务端不能同步读 body,否则会阻塞调度器;
插件只能自己实现
matches(request)预判与独立线程执行。建议把这一步纳入 SPI 文档和类型。