Talos Vulnerability Report

TALOS-2023-1844

Buildroot package hash checking data integrity vulnerabilities

December 5, 2023
CVE Number

CVE-2023-45841,CVE-2023-45842,CVE-2023-45838,CVE-2023-45839,CVE-2023-45840

SUMMARY

Multiple data integrity vulnerabilities exist in the package hash checking functionality of Buildroot 2023.08.1 and Buildroot dev commit 622698d7847. A specially crafted man-in-the-middle attack can lead to arbitrary command execution in the builder.

CONFIRMED VULNERABLE VERSIONS

The versions below were either tested or verified to be vulnerable by Talos or confirmed to be vulnerable by the vendor.

Buildroot 2023.08.1
Buildroot dev commit 622698d7847

PRODUCT URLS

Buildroot - https://www.buildroot.org/

CVSSv3 SCORE

8.1 - CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H

CWE

CWE-494 - Download of Code Without Integrity Check

DETAILS

Buildroot is a tool that automates builds of Linux environments for embedded systems. It supports cross-compiling for multiple target platforms and allows for building a cross-compilation toolchain, Linux kernel image, boot loader, root file system and various utilities.

When building a package, Buildroot executes the corresponding Makefile. Source code is typically downloaded from the internet for most packages, while some are included within Buildroot. Upon downloading the source, Buildroot verifies the integrity of the package using a hash file, extracts the sources, applies any necessary patches and then proceeds with the actual building process.

To describe the logic in detail, let’s use the strace package as a simple example:

In package/strace/strace.mk:

STRACE_VERSION = 6.5
STRACE_SOURCE = strace-$(STRACE_VERSION).tar.xz
STRACE_SITE = https://github.com/strace/strace/releases/download/v$(STRACE_VERSION)
...
$(eval $(autotools-package))

In package/strace/strace.hash:

# Locally calculated after checking signature with RSA key 0xA8041FA839E16E36
# https://strace.io/files/6.5/strace-6.5.tar.xz.asc
sha256  dfb051702389e1979a151892b5901afc9e93bbc1c70d84c906ade3224ca91980  strace-6.5.tar.xz
sha256  d92f973d08c8466993efff1e500453add0c038c20b4d2cbce3297938a296aea9  COPYING
sha256  7c379436436a562834aa7d2f5dcae1f80a25230fa74201046ca1fba4367d39aa  LGPL-2.1-or-later

STRACE_SITE defines the external site to fetch the sources from, and STRACE_SOURCE defines the actual package name to retrieve. The autotools-package is then evaluated to interpret the various <PACKAGE_NAME>_<VARIABLE> definitions in the package .mk and generate all the Makefiles rules needed to build the package.

In the .hash file, we can see sha256 hashes for any file that is being downloaded externally so their integrity can be verified after download.

When the strace package is selected in the config, calling make strace-source will download the package sources. The download function is defined in package/pkg-download.mk, which relays the request to the support/download/dl-wrapper shell script.

In the case of strace, dl-wrapper is called like this:

support/download/dl-wrapper
    -c 6.4
    -d /opt/buildroot/dl/strace
    -D /opt/buildroot/dl
    -f strace-6.4.tar.xz
    -H package/strace//strace.hash
    -n strace-6.4
    -N strace
    -o /opt/buildroot/dl/strace/strace-6.4.tar.xz
    -u https+https://github.com/strace/strace/releases/download/v6.4
    -u http|urlencode+http://sources.buildroot.net/strace
    -u http|urlencode+http://sources.buildroot.net

Interesting parameters to note:

  • -H defines the .hash file for integrity checks
  • -u can be specified multiple times, to provide a fallback in case the primary URL is not available

The http://sources.buildroot.net URLs have been passed as fallback because they correspond to the default value of BR2_BACKUP_SITE. This behavior is enabled by default. However, it is possible to set BR2_PRIMARY_SITE_ONLY to disable it and only allow downloads from the primary resource.

Inside dl-wrapper:

    ...
    download_and_check=0
    rc=1
[1] for uri in "${uris[@]}"; do
        backend_urlencode="${uri%%+*}"
        backend="${backend_urlencode%|*}"
        case "${backend}" in
            git|svn|cvs|bzr|file|scp|hg|sftp) ;;
            *) backend="wget" ;;
        esac
        uri=${uri#*+}

        ...
[2]     if ! "${OLDPWD}/support/download/${backend}" \
                $([ -n "${urlencode}" ] && printf %s '-e') \
                -c "${cset}" \
                -d "${dl_dir}" \
                -n "${raw_base_name}" \
                -N "${base_name}" \
                -f "${filename}" \
                -u "${uri}" \
                -o "${tmpf}" \
                ${quiet} ${large_file} ${recurse} -- "${@}"
        then
            ...
            continue
        fi

        ...
        # Check if the downloaded file is sane, and matches the stored hashes
        # for that file
[3]     if support/download/check-hash ${quiet} "${hfile}" "${tmpf}" "${output##*/}"; then
            rc=0
        else
            if [ ${?} -ne 3 ]; then
                rm -rf "${tmpd}"
                continue
            fi

            # the hash file exists and there was no hash to check the file
            # against
            rc=1
        fi
[4]     download_and_check=1
        break
    done

    # We tried every URI possible, none seems to work or to check against the
    # available hash. *ABORT MISSION*
[5] if [ "${download_and_check}" -eq 0 ]; then
        rm -rf "${tmpd}"
        exit 1
    fi

For each URL (specified via -u) [1], the appropriate backend is used. In the case of strace the backend is simply wget, so wget is going to be called via the support/download/wget wrapper [2].

After the file is downloaded, the hash is checked (file is specified via -H) by calling check-hash [3]. If check-hash has a 0 exit status, rc is set to 0, download_and_check [4, 5] is set to 1 to indicate success and the loop ends.

Let’s see how check-hash is implemented.

    ...
    # Does the hash-file exist?
[6] if [ ! -f "${h_file}" ]; then
        printf "WARNING: no hash file for %s\n" "${base}" >&2
        exit 0
    fi

    # Check one hash for a file
    # $1: algo hash
    # $2: known hash
    # $3: file (full path)
    check_one_hash() {
        ... # exits with error code if the hash doesn't match
    }

    # Do we know one or more hashes for that file?
    nb_checks=0
    while read t h f; do
        case "${t}" in
            ''|'#'*)
                # Skip comments and empty lines
                continue
                ;;
            *)
                if [ "${f}" = "${base}" ]; then
[7]                 check_one_hash "${t}" "${h}" "${file}"
                    : $((nb_checks++))
                fi
                ;;
        esac
    done <"${h_file}"

[8] if [ ${nb_checks} -eq 0 ]; then
[9]     case " ${BR_NO_CHECK_HASH_FOR} " in
        *" ${base} "*)
            # File explicitly has no hash
            exit 0
            ;;
        esac
        printf "ERROR: No hash found for %s\n" "${base}" >&2
        exit 3
    fi

For each hash line in the .hash file, check_one_hash is called [7]. If the hash doesn’t match check_one_hash will exit with an error code. Otherwise nb_checks is incremented to indicate one successful check. If there’s no entry in the .hash file for the specified input file to check, the check at [8] will return an error, unless BR_NO_CHECK_HASH_FOR [9] contains this specific file, meaning that the file is excluded from hash checks.

In total, there are 3 ways for check-hash to return 0 (success):

  1. no .hash file exists for the package [6]
  2. the $file’s hash matches the definition in the .hash file [7]
  3. the $file is not present in the .hash file, and BR_NO_CHECK_HASH_FOR contains the base name for the package (explicitly skipping checks) [9]

Option 2 is what we expect to reach most of the time.

In this advisory, we focus on Option 1. Indeed, we identified 5 packages that miss a .hash file:

  • two of them (aufs and aufs-util) also specify a _SITE with an http:// schema, meaning that a man-in-the-middle attacker could supply any source package to Buildroot.
  • the remaining three packages (riscv64-elf-toolchain, versal-firmware and mxsldr) specify an https:// schema. However, because the default BR2_BACKUP_SITE is set to http://sources.buildroot.net (note the http:// schema), a man-in-the-middle attacker could supply any source package in this case too. To force Buildroot to use the fallback URL, it’s enough to drop HTTPS requests to the primary site (which is easy to do when we assume a MITM position). This will make the if condition at [2] fail, and the loop at [1] will perform the download using the next $uri.

Also note, while a warning is printed for the check at [6], this can be very easily overlooked given the verbosity of the output, especially if we consider it may run in a CI (Continuous Integration) pipeline.

Because packages can ship patch files or Makefiles, by supplying a compromised source package an attacker would be able to execute arbitrary commands in the builder. As a direct consequence, an attacker could then also tamper with any file generated for Buildroot’s targets and hosts.

For example, it’s enough to provide a Makefile with the following command:

_ := $(shell id >> /injected)

Or insert such a line in a .patch file, which would allow modification of any package within Buildroot during the build process.

Below all the affected packages are listed separately.

CVE-2023-45838 - aufs

aufs fetches its sources from an http URL and does not include a .hash file for checking package integrity. An attacker could use a MITM attack to provide compromised packages, which would allow execution of arbitrary commands in the builder.

CVE-2023-45839 - aufs-util

aufs-util fetches its sources from an http URL and does not include a .hash file for checking package integrity. An attacker could use a MITM attack to provide compromised packages, which would allow execution of arbitrary commands in the builder.

CVE-2023-45840 - riscv64-elf-toolchain

riscv64-elf-toolchain fetches its sources from an https URL, however it does not include a .hash file for checking package integrity. Since Buildroot’s network requests can be downgraded to http (thanks to the default BR2_BACKUP_SITE being an http URL), an attacker could use a MITM attack to provide compromised packages, which would in turn allow execution of arbitrary commands in the builder.

CVE-2023-45841 - versal-firmware

versal-firmware fetches its sources from an https URL, however it does not include a .hash file for checking package integrity. Since Buildroot’s network requests can be downgraded to http (thanks to the default BR2_BACKUP_SITE being an http URL), an attacker could use a MITM attack to provide compromised packages, which would in turn allow execution of arbitrary commands in the builder.

CVE-2023-45842 - mxsldr

mxsldr fetches its sources from an https URL, however it does not include a .hash file for checking package integrity. Since Buildroot’s network requests can be downgraded to http (thanks to the default BR2_BACKUP_SITE being an http URL), an attacker could use a MITM attack to provide compromised packages, which would in turn allow execution of arbitrary commands in the builder.

Exploit Proof of Concept

This proof-of-concept assumes that an attacker is MITM-ing the network and dropping requests to git.denx.de on port 443, while at the same time serving any .tar.gz file requested via port 80 with a malicious version:

$ make source
/usr/bin/make -j1  O=/tmp/builddir HOSTCC="/usr/bin/gcc" HOSTCXX="/usr/bin/g++" syncconfig
mkdir -p /tmp/builddir/build/buildroot-config/lxdialog
PKG_CONFIG_PATH="" /usr/bin/make CC="/usr/bin/gcc" HOSTCC="/usr/bin/gcc" \
    obj=/tmp/builddir/build/buildroot-config -C support/kconfig -f Makefile.br conf
/usr/bin/gcc -I/usr/include/ncursesw -DCURSES_LOC="<curses.h>"  -DNCURSES_WIDECHAR=1 -DLOCALE  -I/tmp/builddir/build/buildroot-config -DCONFIG_=\"\"  -MM *.c > /tmp/builddir/build/buildroot-config/.depend 2>/dev/null || :
/usr/bin/gcc -I/usr/include/ncursesw -DCURSES_LOC="<curses.h>"  -DNCURSES_WIDECHAR=1 -DLOCALE  -I/tmp/builddir/build/buildroot-config -DCONFIG_=\"\"   -c conf.c -o /tmp/builddir/build/buildroot-config/conf.o
/usr/bin/gcc -I/usr/include/ncursesw -DCURSES_LOC="<curses.h>"  -DNCURSES_WIDECHAR=1 -DLOCALE  -I/tmp/builddir/build/buildroot-config -DCONFIG_=\"\"  -I. -c /tmp/builddir/build/buildroot-config/zconf.tab.c -o /tmp/builddir/build/buildroot-config/zconf.tab.o
/usr/bin/gcc -I/usr/include/ncursesw -DCURSES_LOC="<curses.h>"  -DNCURSES_WIDECHAR=1 -DLOCALE  -I/tmp/builddir/build/buildroot-config -DCONFIG_=\"\"   /tmp/builddir/build/buildroot-config/conf.o /tmp/builddir/build/buildroot-config/zconf.tab.o  -o /tmp/builddir/build/buildroot-config/conf
rm /tmp/builddir/build/buildroot-config/zconf.tab.c
  GEN     /tmp/builddir/Makefile
gcc-12.3.0.tar.xz: OK (sha512: 8fb799dfa2e5de5284edf8f821e3d40c2781e4c570f5adfdb1ca0671fcae3fb7f794ea783e80f01ec7bfbf912ca508e478bd749b2755c2c14e4055648146c204)
gcc-12.3.0.tar.xz: OK (sha512: 8fb799dfa2e5de5284edf8f821e3d40c2781e4c570f5adfdb1ca0671fcae3fb7f794ea783e80f01ec7bfbf912ca508e478bd749b2755c2c14e4055648146c204)
glibc-2.38-27-g750a45a783906a19591fb8ff6b7841470f1f5701.tar.gz: OK (sha256: fd991e43997ff6e4994264c3cbc23fa87fa28b1b3c446eda8fc2d1d3834a2cfb)
bison-3.8.2.tar.xz: OK (sha256: 9bba0214ccf7f1079c5d59210045227bcf619519840ebfa80cd3849cff5a5bf2)
m4-1.4.19.tar.xz: OK (sha256: 63aede5c6d33b6d9b13511cd0be2cac046f2e70fd0a07aa9573a04a82783af96)
gawk-5.2.2.tar.xz: OK (sha256: 3c1fce1446b4cbee1cd273bd7ec64bc87d89f61537471cd3e05e33a965a250e9)
gcc-12.3.0.tar.xz: OK (sha512: 8fb799dfa2e5de5284edf8f821e3d40c2781e4c570f5adfdb1ca0671fcae3fb7f794ea783e80f01ec7bfbf912ca508e478bd749b2755c2c14e4055648146c204)
binutils-2.40.tar.xz: OK (sha512: a37e042523bc46494d99d5637c3f3d8f9956d9477b748b3b1f6d7dfbb8d968ed52c932e88a4e946c6f77b8f48f1e1b360ca54c3d298f17193f3b4963472f6925)
gmp-6.3.0.tar.xz: OK (sha256: a3c2b80201b89e68616f4ad30bc66aee4927c3ce50e33929ca819d5c43538898)
mpc-1.2.1.tar.gz: OK (sha256: 17503d2c395dfcf106b622dc142683c1199431d095367c6aacba6eec30340459)
mpfr-4.1.1.tar.xz: OK (sha256: ffd195bd567dbaffc3b98b23fd00aad0537680c9896171e44fe3ff79e28ac33d)
linux-6.5.6.tar.xz: OK (sha256: 78e36d4214547051c24df2140f4ce09428d6c515ad9a71b38b28e8094a95d2f6)
busybox-1.36.1.tar.bz2: OK (sha256: b8cc24c9574d809e7279c3be349795c5d5ceb6fdf19ca709f80cde50e47de314)
>>> host-mxsldr 2793a657ab7a22487d21c1b020957806f8ae8383 Downloading
GIT_DIR=/opt/buildroot/dl/mxsldr/git/.git git init .
hint: Using 'master' as the name for the initial branch. This default branch name
hint: is subject to change. To configure the initial branch name to use in all
hint: of your new repositories, which will suppress this warning, call:
hint:
hint:   git config --global init.defaultBranch <name>
hint:
hint: Names commonly chosen instead of 'master' are 'main', 'trunk' and
hint: 'development'. The just-created branch can be renamed via this command:
hint:
hint:   git branch -m <name>
Initialized empty Git repository in /opt/buildroot/dl/mxsldr/git/.git/
GIT_DIR=/opt/buildroot/dl/mxsldr/git/.git git remote add origin 'https://git.denx.de/mxsldr.git'
GIT_DIR=/opt/buildroot/dl/mxsldr/git/.git git remote set-url origin 'https://git.denx.de/mxsldr.git'
Fetching all references
GIT_DIR=/opt/buildroot/dl/mxsldr/git/.git git fetch origin
fatal: unable to access 'https://git.denx.de/mxsldr.git/': Failed to connect to git.denx.de port 443 after 5 ms: Connection refused
Detected a corrupted git cache.
Removing it and starting afresh.
GIT_DIR=/opt/buildroot/dl/mxsldr/git/.git git init .
hint: Using 'master' as the name for the initial branch. This default branch name
hint: is subject to change. To configure the initial branch name to use in all
hint: of your new repositories, which will suppress this warning, call:
hint:
hint:   git config --global init.defaultBranch <name>
hint:
hint: Names commonly chosen instead of 'master' are 'main', 'trunk' and
hint: 'development'. The just-created branch can be renamed via this command:
hint:
hint:   git branch -m <name>
Initialized empty Git repository in /opt/buildroot/dl/mxsldr/git/.git/
GIT_DIR=/opt/buildroot/dl/mxsldr/git/.git git remote add origin 'https://git.denx.de/mxsldr.git'
GIT_DIR=/opt/buildroot/dl/mxsldr/git/.git git remote set-url origin 'https://git.denx.de/mxsldr.git'
Fetching all references
GIT_DIR=/opt/buildroot/dl/mxsldr/git/.git git fetch origin
fatal: unable to access 'https://git.denx.de/mxsldr.git/': Failed to connect to git.denx.de port 443 after 2 ms: Connection refused
Detected a corrupted git cache.
This is the second time in a row; bailing out
wget --passive-ftp -nd -t 3 -O '/tmp/builddir/build/.mxsldr-2793a657ab7a22487d21c1b020957806f8ae8383-br1.tar.gz.HW6f6m/output' 'http://sources.buildroot.net/mxsldr/mxsldr-2793a657ab7a22487d21c1b020957806f8ae8383-br1.tar.gz'
--2023-10-11 13:48:21--  http://sources.buildroot.net/mxsldr/mxsldr-2793a657ab7a22487d21c1b020957806f8ae8383-br1.tar.gz
Resolving sources.buildroot.net (sources.buildroot.net)... 104.26.0.37, 104.26.1.37, 172.67.72.56
Connecting to sources.buildroot.net (sources.buildroot.net)|104.26.0.37|:80... connected.
HTTP request sent, awaiting response... 200 OK
Length: unspecified [application/x-xz]
Saving to: '/tmp/builddir/build/.mxsldr-2793a657ab7a22487d21c1b020957806f8ae8383-br1.tar.gz.HW6f6m/output'

/tmp/builddir/build/.mxsldr-2793a657ab7a2248     [ <=>                                                                                        ]     160  --.-KB/s    in 0s

2023-10-11 13:48:21 (27.1 MB/s) - '/tmp/builddir/build/.mxsldr-2793a657ab7a22487d21c1b020957806f8ae8383-br1.tar.gz.HW6f6m/output' saved [160]

WARNING: no hash file for mxsldr-2793a657ab7a22487d21c1b020957806f8ae8383-br1.tar.gz
libusb-1.0.26.tar.bz2: OK (sha256: 12ce7a61fc9854d1d2a1ffe095f7b5fac19ddba095c259e6067a46500381b5a5)
pkgconf-1.6.3.tar.xz: OK (sha256: 61f0b31b0d5ea0e862b454a80c170f57bad47879c0c42bd8de89200ff62ea210)
patchelf-0.13.tar.bz2: OK (sha256: 4c7ed4bcfc1a114d6286e4a0d3c1a90db147a4c3adda1814ee0eee0f9ee917ed)
TIMELINE

2023-10-25 - Vendor Disclosure
2023-12-04 - Vendor Patch Release
2023-12-05 - Public Release

Credit

Discovered by Claudio Bozzato and Francesco Benvenuto of Cisco Talos.