CVE-2023-43608
A data integrity vulnerability exists in the BR_NO_CHECK_HASH_FOR functionality of Buildroot 2023.08.1 and dev commit 622698d7847. A specially crafted man-in-the-middle attack can lead to arbitrary command execution in the builder.
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
Buildroot - https://www.buildroot.org/
8.1 - CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H
CWE-494 - Download of Code Without Integrity Check
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 defined 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 availableThe 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):
.hash
file exists for the package [6]$file
’s hash matches the definition in the .hash
file [7]$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 3. This seems to be commonly used to skip integrity checks for resources that can’t be easily hashed (for example, developement resources that change often). It is also used when building specific versions of a package, for which hashes may not be available in Buildroot’s sources.
For example, the Linux Kernel Buildroot Makefile (linux/linux.mk
):
...
ifeq ($(BR2_LINUX_KERNEL)$(BR2_LINUX_KERNEL_LATEST_VERSION),y)
BR_NO_CHECK_HASH_FOR += $(LINUX_SOURCE)
endif
...
If both BR2_LINUX_KERNEL
and BR2_LINUX_KERNEL_LATEST_VERSION
where enabled, the result of $(BR2_LINUX_KERNEL)$(BR2_LINUX_KERNEL_LATEST_VERSION)
would be yy
.
So, the ifeq
checks if BR2_LINUX_KERNEL_LATEST_VERSION
is NOT selected, in which case it adds linux-<version>-br1.tar.gz
to the BR_NO_CHECK_HASH_FOR
variable.
This is, however, problematic in some setups. For example, consider this Buildroot minimal configuration:
BR2_LINUX_KERNEL=y
BR2_LINUX_KERNEL_CUSTOM_GIT=y
BR2_LINUX_KERNEL_CUSTOM_REPO_URL="https://192.168.50.50"
BR2_LINUX_KERNEL_CUSTOM_REPO_VERSION="123"
This defines a custom repository for fetching the Linux Kernel, so it may be important to only fetch the sources from that repository, for Linux specifically. This will work fine the majority of the time. However, if an attacker is able to drop connections towards 192.168.50.50, this will make the if
condition at [2] fail, and the loop at [1] will perform the download using the next $uri
. The next $uri
is going to be http://sources.buildroot.net/linux/linux-123-br1.tar.gz
, because of the default BR2_BACKUP_SITE
variable. This will lead to downloading Linux Kernel sources via plain HTTP without performing any hash checks.
Since the Linux Kernel 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.
This proof-of-concept assumes that an attacker is MITM-ing the network and dropping requests to 192.168.50.50
, 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-headers 123 Downloading
GIT_DIR=/opt/buildroot/dl/linux/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/linux/git/.git/
GIT_DIR=/opt/buildroot/dl/linux/git/.git git remote add origin 'https://192.168.50.50'
GIT_DIR=/opt/buildroot/dl/linux/git/.git git remote set-url origin 'https://192.168.50.50'
Fetching all references
GIT_DIR=/opt/buildroot/dl/linux/git/.git git fetch origin
fatal: unable to access 'https://192.168.50.50/': Failed to connect to 192.168.50.50 port 443 after 0 ms: Connection refused
Detected a corrupted git cache.
Removing it and starting afresh.
GIT_DIR=/opt/buildroot/dl/linux/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/linux/git/.git/
GIT_DIR=/opt/buildroot/dl/linux/git/.git git remote add origin 'https://192.168.50.50'
GIT_DIR=/opt/buildroot/dl/linux/git/.git git remote set-url origin 'https://192.168.50.50'
Fetching all references
GIT_DIR=/opt/buildroot/dl/linux/git/.git git fetch origin
fatal: unable to access 'https://192.168.50.50/': Failed to connect to 192.168.50.50 port 443 after 0 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/.linux-123-br1.tar.gz.OBKF3J/output' 'http://sources.buildroot.net/linux/linux-123-br1.tar.gz'
--2023-10-11 16:16:26-- http://sources.buildroot.net/linux/linux-123-br1.tar.gz
Resolving sources.buildroot.net (sources.buildroot.net)... 104.26.0.37, 172.67.72.56, 104.26.1.37
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/.linux-123-br1.tar.gz.OBKF3J/output'
/tmp/builddir/build/.linux-123-br1.tar.gz.OB [ <=> ] 160 --.-KB/s in 0s
2023-10-11 16:16:26 (24.1 MB/s) - '/tmp/builddir/build/.linux-123-br1.tar.gz.OBKF3J/output' saved [160]
busybox-1.36.1.tar.bz2: OK (sha256: b8cc24c9574d809e7279c3be349795c5d5ceb6fdf19ca709f80cde50e47de314)
kmod-31.tar.xz: OK (sha256: f5a6949043cc72c001b728d8c218609c5a15f3c33d75614b78c79418fcf00d80)
pkgconf-1.6.3.tar.xz: OK (sha256: 61f0b31b0d5ea0e862b454a80c170f57bad47879c0c42bd8de89200ff62ea210)
patchelf-0.13.tar.bz2: OK (sha256: 4c7ed4bcfc1a114d6286e4a0d3c1a90db147a4c3adda1814ee0eee0f9ee917ed)
flex-2.6.4.tar.gz: OK (sha256: e87aae032bf07c26f85ac0ed3250998c37621d95f8bd748b31f15b33c45ee995)
autoconf-2.71.tar.xz: OK (sha256: f14c83cfebcc9427f2c3cea7258bd90df972d92eb26752da4ddad81c87a0faa4)
libtool-2.4.6.tar.xz: OK (sha256: 7c87a8c2c8c0fc9cd5019e402bed4292462d00a718a7cd5f11218153bf28b26f)
automake-1.16.5.tar.xz: OK (sha256: f01d58cd6d9d77fbdca9eb4bbd5ead1988228fdb73d6f7a201f5f8d6b118b469)
gettext-tiny-0.3.2.tar.gz: OK (sha256: 29cc165e27e83d2bb3760118c2368eadab550830d962d758e51bd36eb860f383)
gettext-0.22.2.tar.xz: OK (sha256: 4c82fbfe5e53d71a97c634aa98a898b9da807b08b27410f6a4641e8bb44dc4b2)
2023-10-25 - Vendor Disclosure
2023-12-04 - Vendor Patch Release
2023-12-05 - Public Release
Discovered by Claudio Bozzato and Francesco Benvenuto of Cisco Talos.