CVE-2013-6825 - DCMTK 3.6.1 - Root Privilege escalation

Authors:Hector Marco & Ismael Ripoll
CVE:CVE-2013-6825
BUG:Lack of checking setuid() return code
Dates:24 November 2013 - Discovered the bug
24 November 2013 - Contacted affected vendor
19 February 2014 - Fixed
25 March 2014    - Public disclosure


Description

A bug in DCMTK for versions prior to 3.6.1 has been found. The bug is caused by not checking the return value of setuid() call. The process must not continue its normal execution when this call fails (return an error) to drop privileges.

Impact

In some tools of the DCMTK software the return value of setuid() is not checked. In some cases it allows to do a privilege escalation because the process does not give up its root privileges and continue as root.

All of the tools will receive DICOM messages (images, print jobs, or queries) over the network and create corresponding DICOM files. When running with root privileges, called by a user with access to the local system, the user can force the tool to write to directories to which the normal user account has no access rights. However, no filenames can be chosen for DICOM files, so it is not possible to selectively overwrite certain files.

Furthermore, all tools can be configured by a user-defined configuration file to write a log file with a filename defined by the user. This could possibly be abused to destroy certain system files by overwriting them with a logfile.

Finally, all tools will read configuration files, and some of them will also read a certificate and public key file for TLS/SSL communication. The calling user may provoke the tool to read these files from directories where he would otherwise not have read access. However, there is no function that would display the file content, or copy a file, so the user can mostly abuse this to detect if a certain file exists or not, or try to extract information from error messages generated when a file not conforming to the expected file format is read.

Vulnerable packages

DCMTK package 3.6.1 is vulnerable, all DCMTK versions since 1993 to the current 3.6.1 (released February-2014) are affected.

The bug

The bug appears because the return value of the setuid() call is not checked. When the setuid() fails, the application continues executing (does not terminate) and the following code is executed with the original privileges. The bug appears in the following files:

File dcmnet/apps/movescu.cc line 761 and file dcmnet/apps/storescp.cc line 1074:

#ifdef HAVE_GETUID
    /* return to normal uid so that we can't do too much damage in case
     * things go very wrong.   Only does someting if the program is setuid
     * root, and run by another user.  Running as root user may be
     * potentially disasterous if this program screws up badly.
     */
    setuid(getuid());
#endif

File dcmnet/libsrc/scp.cc line 120 and file dcmwlm/libsrc/wlmactmg.cc line 249:

#if defined(HAVE_SETUID) && defined(HAVE_GETUID)
  // Return to normal uid so that we can't do too much damage in case
  // things go very wrong. Only works if the program is setuid root,
  // and run by another user. Running as root user may be
  // potentially disastrous if this program screws up badly.
  setuid( getuid() );
#endif

File dcmpstat/apps/dcmprscp.cc line 472, file dcmpstat/apps/dcmpsrcv.cc line 1278, file dcmpstat/tests/msgserv.cc line 193 and file dcmqrdb/apps/dcmqrscp.cc line 688:

#if defined(HAVE_SETUID) && defined(HAVE_GETUID)
    /* return to normal uid so that we can't do too much damage in case
     * things go very wrong.   Only relevant if the program is setuid root,
     * and run by another user.  Running as root user may be
     * potentially disasterous if this program screws up badly.
     */
    setuid(getuid());
#endif

Exploit

The strategy to exploit this kind of bugs consists in creating as many processes as the target user is allowed to make (which is given by RLIMIT_NPROC), and then the next attempt to drop privileges will fail, letting the process alive and with the original privileges.

FIX

We have created a non official patch for irssi-0.8.15:
diff --git dcmnet/apps/movescu.cc dcmnet/apps/movescu.cc
index 0e98b7b..0c1ba6f 100644
--- dcmnet/apps/movescu.cc
+++ dcmnet/apps/movescu.cc
@@ -758,7 +758,10 @@ main(int argc, char *argv[])
      * root, and run by another user.  Running as root user may be
      * potentially disasterous if this program screws up badly.
      */
-    setuid(getuid());
+    if ( setuid(getuid()) != 0 ){
+       OFLOG_FATAL(movescuLogger, "Failed to drop privileges");
+       return -1;
+    }
 #endif
 
     /* set up main association */
diff --git dcmnet/apps/storescp.cc dcmnet/apps/storescp.cc
index 2dfc512..795b388 100644
--- dcmnet/apps/storescp.cc
+++ dcmnet/apps/storescp.cc
@@ -1071,7 +1071,10 @@ int main(int argc, char *argv[])
    * root, and run by another user.  Running as root user may be
    * potentially disastrous if this program screws up badly.
    */
-  setuid(getuid());
+  if ( setuid(getuid()) != 0){
+     OFLOG_FATAL(storescpLogger, "Failed to drop privileges");
+     return -1;
+  }
 #endif
 
 #ifdef WITH_OPENSSL
diff --git dcmnet/libsrc/scp.cc dcmnet/libsrc/scp.cc
index 3d974b6..a24661c 100644
--- dcmnet/libsrc/scp.cc
+++ dcmnet/libsrc/scp.cc
@@ -117,7 +117,10 @@ OFCondition DcmSCP::listen()
   // things go very wrong. Only works if the program is setuid root,
   // and run by another user. Running as root user may be
   // potentially disastrous if this program screws up badly.
-  setuid( getuid() );
+  if( setuid(getuid()) != 0){
+     DCMNET_ERROR("Failed to drop privileges");
+     return -1;
+  }
 #endif
 
// If we get to this point, the entire initialization process has been completed
diff --git dcmpstat/apps/dcmprscp.cc dcmpstat/apps/dcmprscp.cc
index 5e82165..297f848 100644
--- dcmpstat/apps/dcmprscp.cc
+++ dcmpstat/apps/dcmprscp.cc
@@ -469,7 +469,10 @@ int main(int argc, char *argv[])
      * and run by another user.  Running as root user may be
      * potentially disasterous if this program screws up badly.
      */
-    setuid(getuid());
+    if ( setuid(getuid()) != 0){
+       OFLOG_FATAL(dcmprscpLogger, "Failed to drop privileges");
+       return -1;
+    }
 #endif
 
 #ifdef HAVE_FORK
diff --git dcmpstat/apps/dcmpsrcv.cc dcmpstat/apps/dcmpsrcv.cc
index 7d116bb..39ab1fa 100644
--- dcmpstat/apps/dcmpsrcv.cc
+++ dcmpstat/apps/dcmpsrcv.cc
@@ -1275,7 +1275,10 @@ int main(int argc, char *argv[])
        * and run by another user.  Running as root user may be
        * potentially disasterous if this program screws up badly.
        */
-      setuid(getuid());
+      if( setuid(getuid()) != 0){
+        OFLOG_FATAL(dcmpsrcvLogger, "Failed to drop privileges"); 
+        return -1;
+      }
 #endif
 
 #ifdef HAVE_FORK
diff --git dcmpstat/tests/msgserv.cc dcmpstat/tests/msgserv.cc
index 81181ec..9444bbe 100644
--- dcmpstat/tests/msgserv.cc
+++ dcmpstat/tests/msgserv.cc
@@ -190,7 +190,10 @@ int main(int argc, char *argv[])
        * and run by another user.  Running as root user may be
        * potentially disasterous if this program screws up badly.
        */
-      setuid(getuid());
+     if( setuid(getuid()) != 0){
+        OFLOG_FATAL(msgservLogger, "Failed to drop privileges");
+        return -1;
+     }
 #endif
 
     fd_set fdset;
diff --git dcmqrdb/apps/dcmqrscp.cc dcmqrdb/apps/dcmqrscp.cc
index 3a0fc0d..e20d60f 100644
--- dcmqrdb/apps/dcmqrscp.cc
+++ dcmqrdb/apps/dcmqrscp.cc
@@ -685,7 +685,10 @@ main(int argc, char *argv[])
      * and run by another user.  Running as root user may be
      * potentially disasterous if this program screws up badly.
      */
-    setuid(getuid());
+    if( setuid(getuid()) != 0){
+       OFLOG_FATAL(dcmqrscpLogger, "Failed to drop privileges");
+       return -1;
+    }
 #endif
 
 #if defined(HAVE_SETUID) && defined(HAVE_GRP_H) && defined(HAVE_PWD_H)
diff --git dcmwlm/libsrc/wlmactmg.cc dcmwlm/libsrc/wlmactmg.cc
index d84f0c0..7d46c37 100644
--- dcmwlm/libsrc/wlmactmg.cc
+++ dcmwlm/libsrc/wlmactmg.cc
@@ -246,7 +246,10 @@ OFCondition WlmActivityManager::StartProvidingService()
   // things go very wrong. Only works if the program is setuid root,
   // and run by another user. Running as root user may be
   // potentially disasterous if this program screws up badly.
-  setuid( getuid() );
+  if( setuid(getuid()) != 0 ){
+     DCMWLM_ERROR("Failed to drop privileges");
+     return -1;
+  }
 #endif
 
  // If we get to this point, the entire initialization process has been completed

[ dcmtk-3.6.1-drop-privileges-fixed.patch ]

Patching dcmtk:

$ wget http://hmarco.org/bugs/patches/dcmtk-3.6.1-drop-privileges-fixed.patch
$ cd dcntk-3.6.1
$ patch -p0 < ../dcmtk-3.6.1-drop-privileges-fixed.patch

Discussion

For all tools, the bug can be exploited if and only if

All affected tools open a listen socket for incoming DICOM network connections, which requires root privileges if the official DICOM port 104 should be used on Posix systems. After opening the listen socket, the tools will revert to the effective user ID of the calling user by calling (setuid(getuid()). However, if setuid() fails because the maximum number of processes for the calling user has been reached, the tools do not stop, but continue operation with root privileges.

Since none of the tools will fork() before calling setuid(), it is not possible normally to exploit this remotely over the network, unless the tools are started by a super-daemon such as inetd, in which case it may be possible to initiate network connections until the maximum number of processes is reached.

A user with local access to the system may, however, exploit this bug by running the maximum number of processes for his own account and then starting one of the tools.

Acknowledgements

Thanks to the DCMTK team for the constructive interaction.


Hector Marco - http://hmarco.org