CVE-2017-7500 (published 2018-08-13T17:29Z)

It was found that rpm did not properly handle RPM installations when a destination path was a symbolic link to a directory, possibly changing ownership and permissions of an arbitrary directory, and RPM files being placed in an arbitrary destination. An attacker, with write access to a directory in which a subdirectory will be installed, could redirect that directory to an arbitrary location and gain root privilege.

Severity: HIGH
CVSS v2 base score: 7.2 (AV:L/AC:L/Au:N/C:C/I:C/A:C)
CVSS v3 base score: 7.8, HIGH (CVSS:3.0/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H)

Weakness information:
cwe_id: CWE-59
cwe_name: Improper Link Resolution Before File Access ('Link Following')
description: The product attempts to access a file based on the filename, but it does not properly prevent that filename from identifying a link or shortcut that resolves to an unintended resource.
extended_description: 
            
            
         
         

Fix information:
repo_url: https://github.com/rpm-software-management/rpm
commit_details: repo_url: https://github.com/rpm-software-management/rpm
author: Panu Matilainen
author_date: 2017-09-26 16:54:16+03:00
msg: Restrict following symlinks to directories by ownership (CVE-2017-7500)

Only follow directory symlinks owned by target directory owner or root.
This prevents privilege escalation from user-writable directories via
directory symlinks to privileged directories on package upgrade, while
still allowing admin to arrange disk usage with symlinks.

The rationale is that if you can create symlinks owned by user X you *are*
user X (or root), and if you also own directory Y you can do whatever with
it already, including change permissions. So when you create a symlink to
that directory, the link ownership acts as a simple stamp of authority that
you indeed want rpm to treat this symlink as it were the directory that
you own. Such a permission can only be given by you or root, which
is just the way we want it. Plus it's almost ridiculously simple as far
as rules go, compared to trying to calculate something from the
source vs destination directory permissions etc.

In the normal case, the user arranging diskspace with symlinks is indeed
root so nothing changes, the only real change here is to links created by
non-privileged users which should be few and far between in practise.
Unfortunately our test-suite runs as a regular user via fakechroot and
thus the testcase for this fails under the new rules. Adjust the testcase
to get the ownership straight and add a second case for the illegal
behavior, basically the same as the old one but with different expectations.
repository: repo_url: https://github.com/rpm-software-management/rpm
repo_name: rpm-software-management/rpm
description: The RPM package manager
repo_language: C
owner: rpm-software-management
file_changes: filename: fsm.c
change_type: ModificationType.MODIFY
diff: @@ -650,7 +650,7 @@ static int fsmUtime(const char *path, mode_t mode, time_t mtime)
     return rc;
 }
 
-static int fsmVerify(const char *path, rpmfi fi)
+static int fsmVerify(const char *path, rpmfi fi, const struct stat *fsb)
 {
     int rc;
     int saveerrno = errno;
@@ -675,11 +675,14 @@ static int fsmVerify(const char *path, rpmfi fi)
     } else if (S_ISDIR(mode)) {
         if (S_ISDIR(dsb.st_mode)) return 0;
         if (S_ISLNK(dsb.st_mode)) {
+	    uid_t luid = dsb.st_uid;
             rc = fsmStat(path, 0, &dsb);
             if (rc == RPMERR_ENOENT) rc = 0;
             if (rc) return rc;
             errno = saveerrno;
-            if (S_ISDIR(dsb.st_mode)) return 0;
+	    /* Only permit directory symlinks by target owner and root */
+            if (S_ISDIR(dsb.st_mode) && (luid == 0 || luid == fsb->st_uid))
+		    return 0;
         }
     } else if (S_ISLNK(mode)) {
         if (S_ISLNK(dsb.st_mode)) {
@@ -921,7 +924,7 @@ int rpmPackageFilesInstall(rpmts ts, rpmte te, rpmfiles files,
 	    }
 	    /* Assume file does't exist when tmp suffix is in use */
 	    if (!suffix) {
-		rc = fsmVerify(fpath, fi);
+		rc = fsmVerify(fpath, fi, &sb);
 	    } else {
 		rc = (action == FA_TOUCH) ? 0 : RPMERR_ENOENT;
 	    }

programming_language: C
filename: replacetest.spec
change_type: ModificationType.MODIFY
diff: @@ -1,5 +1,6 @@
 %{!?filetype: %global filetype file}
 %{?fixit: %global havepretrans 1}
+%{!?user: %global user root}
 
 Name:		replacetest%{?sub:-%{sub}}
 Version:	%{ver}
@@ -43,5 +44,5 @@ rm -rf $RPM_BUILD_ROOT
 %endif
 
 %files
-%defattr(-,root,root,-)
+%defattr(-,%{user},%{user},-)
 /opt/*

programming_language: Shell
filename: rpmreplace.at
change_type: ModificationType.MODIFY
diff: @@ -402,12 +402,14 @@ runroot rpmbuild --quiet -bb \
         --define "ver 1.0" \
 	--define "filetype datadir" \
 	--define "filedata README1" \
+	--define "user $(id -u -n)" \
           /data/SPECS/replacetest.spec
 
 runroot rpmbuild --quiet -bb \
         --define "ver 2.0" \
 	--define "filetype datadir" \
 	--define "filedata README2" \
+	--define "user $(id -u -n)" \
           /data/SPECS/replacetest.spec
 
 mkdir "${RPMTEST}"/opt/f00f
@@ -421,6 +423,38 @@ test -L "${tf}" && test -d "${tf}"
 [])
 AT_CLEANUP
 
+AT_SETUP([upgrade invalid locally symlinked directory])
+AT_KEYWORDS([install])
+AT_CHECK([
+RPMDB_CLEAR
+RPMDB_INIT
+tf="${RPMTEST}"/opt/foo
+rm -rf "${RPMTEST}"/opt/*
+rm -rf "${TOPDIR}"
+
+runroot rpmbuild --quiet -bb \
+        --define "ver 1.0" \
+	--define "filetype datadir" \
+	--define "filedata README1" \
+          /data/SPECS/replacetest.spec
+
+runroot rpmbuild --quiet -bb \
+        --define "ver 2.0" \
+	--define "filetype datadir" \
+	--define "filedata README2" \
+          /data/SPECS/replacetest.spec
+
+mkdir "${RPMTEST}"/opt/f00f
+ln -s f00f "${RPMTEST}"/opt/foo
+runroot rpm -U /build/RPMS/noarch/replacetest-1.0-1.noarch.rpm
+test -L "${tf}" && test -d "${tf}" && runroot rpm -U /build/RPMS/noarch/replacetest-2.0-1.noarch.rpm
+test -d "${tf}"
+],
+[0],
+[],
+[])
+AT_CLEANUP
+
 AT_SETUP([upgrade empty directory to broken link])
 AT_KEYWORDS([install])
 AT_CHECK([

programming_language: Shell
repo_url: https://github.com/rpm-software-management/rpm
commit_details: repo_url: https://github.com/rpm-software-management/rpm
author: Panu Matilainen
author_date: 2017-09-27 13:45:17+03:00
msg: Make verification match the new restricted directory symlink behavior

Only follow directory symlinks owned by target directory owner or root
during verification to match the behavior of fsmVerify() in the new
CVE-2017-7500 world order.

The code is klunkier than it should and the logic should use common code
with fsmVerify() instead of duplicating it here, but that needs more
changes than is comfortable to backport so starting with this.

Also worth noting that the previous "follow the link" logic from
commit 3ccd774255b8215733e0bdfdf5a683da9dd10923 was not quite right,
it'd fail with RPMVERIFY_LSTATFAIL on a broken symlink when it should've
ran verification on the symlink itself. This behavior is fixed here too.

Finally, once again fakechroot gets in the way and forces the related
verify testcase to be changed to be able to create a valid link. Reuse
the replacement testcase for the purpose and add another case for
verifying an invalid link.
repository: repo_url: https://github.com/rpm-software-management/rpm
repo_name: rpm-software-management/rpm
description: The RPM package manager
repo_language: C
owner: rpm-software-management
file_changes: filename: verify.c
change_type: ModificationType.MODIFY
diff: @@ -94,9 +94,18 @@ rpmVerifyAttrs rpmfilesVerify(rpmfiles fi, int ix, rpmVerifyAttrs omitMask)
     }
 
     /* If we expected a directory but got a symlink to one, follow the link */
-    if (S_ISDIR(fmode) && S_ISLNK(sb.st_mode) && stat(fn, &sb) != 0) {
-	vfy |= RPMVERIFY_LSTATFAIL;
-	goto exit;
+    if (S_ISDIR(fmode) && S_ISLNK(sb.st_mode)) {
+	struct stat dsb;
+	/* ...if it actually points to a directory  */
+	if (stat(fn, &dsb) == 0 && S_ISDIR(dsb.st_mode)) {
+	    uid_t fuid;
+	    /* ...and is by a legit user, to match fsmVerify() behavior */
+	    if (sb.st_uid == 0 ||
+			(rpmugUid(rpmfilesFUser(fi, ix), &fuid) == 0 &&
+			 sb.st_uid == fuid)) {
+		sb = dsb; /* struct assignment */
+	    }
+	}
     }
 
     /* Links have no mode, other types have no linkto */

programming_language: C
filename: rpmverify.at
change_type: ModificationType.MODIFY
diff: @@ -69,18 +69,56 @@ AT_KEYWORDS([verify])
 AT_CHECK([
 RPMDB_CLEAR
 RPMDB_INIT
+tf="${RPMTEST}"/opt/foo
+rm -rf "${RPMTEST}"/opt/*
+rm -rf "${TOPDIR}"
 
-runroot rpm -U --nodeps --noscripts --ignorearch --ignoreos \
-	/data/RPMS/hello-1.0-1.i386.rpm
-mv "${RPMTEST}"/usr/share/doc/hello-1.0 "${RPMTEST}"/usr/share/doc/hello-1.0.orig
-ln -s hello-1.0.orig "${RPMTEST}"/usr/share/doc/hello-1.0
-runroot rpm -Vva --nodeps --nouser --nogroup
-rm -rf "${RPMTEST}"/usr/share/doc/
+runroot rpmbuild --quiet -bb \
+        --define "ver 1.0" \
+        --define "filetype datadir" \
+        --define "filedata README1" \
+        --define "user $(id -u -n)" \
+          /data/SPECS/replacetest.spec
+
+runroot rpm -U /build/RPMS/noarch/replacetest-1.0-1.noarch.rpm
+mv "${RPMTEST}"/opt/foo "${RPMTEST}"/opt/was
+ln -s was "${RPMTEST}"/opt/foo
+runroot rpm -Vv replacetest
 ],
 [0],
-[.........    /usr/local/bin/hello
-.........    /usr/share/doc/hello-1.0
-.........  d /usr/share/doc/hello-1.0/FAQ
+[.........    /opt/foo
+.........    /opt/foo/README1
+.........    /opt/goo
+.........    /opt/zoo
+],
+[])
+AT_CLEANUP
+
+AT_SETUP([directory replaced with an invalid directory symlink])
+AT_KEYWORDS([verify])
+AT_CHECK([
+RPMDB_CLEAR
+RPMDB_INIT
+tf="${RPMTEST}"/opt/foo
+rm -rf "${RPMTEST}"/opt/*
+rm -rf "${TOPDIR}"
+
+runroot rpmbuild --quiet -bb \
+        --define "ver 1.0" \
+        --define "filetype datadir" \
+        --define "filedata README1" \
+          /data/SPECS/replacetest.spec
+
+runroot rpm -U /build/RPMS/noarch/replacetest-1.0-1.noarch.rpm
+mv "${RPMTEST}"/opt/foo "${RPMTEST}"/opt/was
+ln -s was "${RPMTEST}"/opt/foo
+runroot rpm -Vv --nouser --nogroup replacetest
+],
+[1],
+[....L....    /opt/foo
+.........    /opt/foo/README1
+.........    /opt/goo
+.........    /opt/zoo
 ],
 [])
 AT_CLEANUP

programming_language: Shell