HTTP Parameter Pollution with cookies in PHP

Published on by

Strange things happen when you parse URL arguments and Cookies in PHP. By using a single square bracket [ or a null byte its possible to rename an HTTP parameter and to set multiple unique cookies in the browser that PHP interprets as being the same. This makes it possible to perform HTTP parameter pollution with cookies. All testing was done on PHP 5.4.16 with Nginx on Chrome 27.

When URL arguments (including POST and Cookie variables) are parsed in PHP they use the parse_str function - it accepts strings such as:

x=1&y=2

parse_str can even accept associative arrays like this:

x[0]=123&x[1]=456&x[2]=456

When you only specify a single open bracket [ and don't close it, it'll get converted into an underscore. This bizarre behaviour means that the two strings below ($s1 and $s2) create identical associative arrays when parsed.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
$s1 = 'user_id=123';
$s2 = 'user[id=123';

parse_str($s1, $o1);
parse_str($s2, $o2);

// Both $o2 and $o2 now contain:

array(1) {
  ["user_id"]=>
  string(3) "123"
}

This means we can re-write any parameter names with underscores in them using a square bracket.

It turns out you can do a similar trick with null bytes (see MongoDB Null Byte Injection attacks which in hindsight wasn't the fault of MongoDB but rather PHP itself). However, in this case anything after the null byte in a parameter name gets removed - so the following two strings are also parsed into the same array:

user=123
user%00blahblah=123

Security implications

Being able to rename HTTP parameters is a concern, particularly if you have a Web Application Firewall that has been configured to patch a specific parameter against a specific vulnerability. Being able to rename your parameter may allow you to bypass this restriction (admittedly the null byte method would probably get blocked by the WAF)

Furthermore, if your application was performing any filtering on the QUERY_STRING parameter in an attempt to prevent HTTP parameter pollution (HPP) you can now turn requests like this:

account_id=123&account_id=456

Into requests like these:

account_id=123&account[id=456
account_id=123&account_id%00woot=456

The biggest problem however appears to be how this behavior allows the polluting of a browsers cookies.

HTTP Parameter Pollution with Cookies

Traditionally HTTP parameter pollution wasn't possible with cookies as most browsers only allow one instance of a cookie name to be set. Furthermore, if a cookie has the HttpOnly flag set then it is not possible to overwrite it using JavaScript. So this hasn't left any way to pollute the browser cache.

However, as browsers don't parse cookie names in the same way that the PHP parse_str function does we can use the above tricks to have two or more cookies set which the browser sees as unique cookies, but which PHP sees as just one. This enables us to "overwrite" an HttpOnly cookie using XSS and pollute the browser cookie cache.

You might recall when it comes to HTTP parameter pollution that PHP takes the last occurrence of a parameter as the actual value. So in the above examples account_id would be overwritten with "456". With cookies it works the other way round; so the first occurrence of the variable takes preference. This particular feature is essential to enable permanent/persistent cookie pollution.

An Example: Permanent session fixation using reflected XSS

Lets assume we have an application that sets the cookie "session_id" which directly relates to the application session state. Session cookies normally expire once the browser is closed. When the client issues their first HTTP request the server responds with the following which sets their initial session identifier:

Set-Cookie: session_id=gjh6vk37g6toubpg8maacog6pga3nj0q HttpOnly Secure;

If an attacker then sets a cookie called session[id using XSS which has an expiry in the future we end up with two cookies stored in the browsers cache, the more recent one last, which results in the following string being sent with every http request:

Cookie: session_id=gjh6vk37g6toubpg8maacog6pga3nj0q; session[id=EVIL;

You could set the session[id using JavaScript like so - remember, you can't just set session_id as its protected with HttpOnly, so our solution is to set session[id or session%00blah which PHP will interpret as session_id:

// Using [ to replace _
document.cookie='session[id=EVIL; expires=Tue, 18 Apr 2023 13:38:31 GMT';

// Extending with a null byte
document.cookie='session_id%00blah=EVIL; expires=Tue, 18 Apr 2023 13:38:31 GMT';

The cookie session_id is valid for the current session, however, session[id is valid for 10 years. As soon as session_id is destroyed by the browser (when it closes, the session regenerates or when the user logs out) we end up with the just the single session[id in the browser which now becomes the first cookie in the list:

Cookie: session[id=EVIL;

As long as your application is not vulnerable to session fixation; when the user visits the application again a subsequent Set-Cookie is issued for session_id and this gets appended to the Cookie list. The consequence is that the browser ends up sending requests to the server that look like this (note the reversal of session[id and session_id):

Cookie: session[id=EVIL; session_id=ovvalnr30mhe7l718dueau4rri1v4u4d;

As session[id is now the first parameter PHP will read the $_COOKIE['session_id'] value as EVIL and not the genuine session ID which was set by the server. As the application believes the session_id to be invalid the chances are the application will no longer work until the user clears their browser.

If your application is vulnerable to session fixation then setting one of the above cookies will set your session ID permanently allowing an attacker to repeatedly hijack your session until the browser cache is cleared. Even if you log out and log in again the malicious cookie is likely to still be present as PHP doesn't know its there and so can't delete it.

Other attack vectors

You don't even necessarily need XSS to exploit this. If the application is vulnerable to session fixation a malicious user could poison the cookies on a bunch of public terminals/kiosks and then wait for the victim to log in with the known session ID.

This bug doesn't just affect session IDs. Other cookies are also affected - any cookie that's used to store a client state can be abused. e.g. setting the default website language or currency permanently to Chinese. Once the attacker has set the cookie the application will not be able to easily change it.

Defending against Cookie Pollution

So what can you do to defend against this apart from removing every instance of XSS from your application? Well you can't traverse the $_COOKIE variable looking for null bytes or rouge brackets as the values by their very nature won't appear in the associative array.

The only option is to manually parse $_SERVER['HTTP_COOKIE'] and check for rogue [ or %00 values contained in parameter names - if you identify any of them unset them with setcookie.

The following code should work:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
/**
 * Identifies polluted cookies and returns an array of them 
 * or false if none present. Requires $_SERVER['HTTP_COOKIE']
 **/
function rogueCookies($httpCookie) {

  $keyValues = explode(';', $httpCookie);
  $affected  = array();

  foreach($keyValues as $n => $keyValue) {

    $kv  = trim($keyValue);
    $key = substr($kv, 0, strpos($kv, '=', 0));

    // Null byte check
    if (strpos($kv, '%00', 0) !== false) {
      $affected[] = $kv;
      continue;
    }

    // Rogue [ check
    if (substr_count($kv, '[') != substr_count($kv, ']')) {
      $affected[] = $kv;
      continue;
    }
  }

  if (sizeof($affected) > 0)
    return $affected;

  return false;
}

$rogue = rogueCookies("h=1; user[id=2; user_id=1; h%00x=2");

// Do setcookie NULL for each entry in $x here.

if (!$rogue) 
  foreach ($rogue as $n => $key) 
    setcookie($key, NULL);