เกริ่นก่อนว่าไม่ใช่ช่องโหว่ใหม่อะไรนะครับ ถถถ LoL ... หลังจากที่ได้อ่านข่าว Russian malware targets WordPress users, over 100,000 sites infected สรุปคร่าวๆได้ว่า มีเว็บไซต์ที่ได้รับผลกระทบจากมัลแวร์ชื่อ SoakSoak เป็นจำนวนมากถึงกวา่ 100k เว็บซึ่งมัลแวร์ดังกล่าวได้ใช้ประโยชน์จาก Slider Revolution ที่เป็น Third party ที่ติดมากับ Plugin/ Theme ของ Wordpress โดยแพคเก็จดังกล่าวมีช่องโหว่ Local File Inclusion อยู่ ก็เลยเอามาวิเคราะห์ช่องโหว่นี้เพ่ืออธิบายครับ
ขั้นตอนแรกจะเห็นว่าการโจมตีจะเกิดจากการ GET Request ไปที่ /wp-admin/admin-ajax.php?action=revslider_show_image&img=../wp-config.php จะเห็นว่าพารามิเตอร์ img ถูกเรียกไปที่ wp-config.php ซึ่งเป็นไฟล์ที่ใช้เก็บค่า Config ต่างๆ ใน Database บน Wordpress ครับ
โดยที่พารามิเตอร์ img นั้นอยู่ในคลาส UniteImageViewRev ฟังก์ชั่น showImageFromGet ที่ไฟล์ /revslider/inc_php/framework/image_view.class.php ครับ
public function showImageFromGet(){
   
 $imageFilename = UniteFunctionsRev::getGetVar("img");
 $maxWidth = UniteFunctionsRev::getGetVar("w",-1);
 $maxHeight = UniteFunctionsRev::getGetVar("h",-1);
 $type = UniteFunctionsRev::getGetVar("t","");
   
 //set effect
 $effect = UniteFunctionsRev::getGetVar("e");
 $effectArgument1 = UniteFunctionsRev::getGetVar("ea1");
   
 if(!empty($effect))
  $this->setEffect($effect,$effectArgument1);
   
 $this->showImage($imageFilename,$maxWidth,$maxHeight,$type);
}
มีการเก็บค่าที่รับมาจากพารามิเตอร์ img ใส่ไว้ในตัวแปร $imageFilename จากนั้นก็ถูกส่งเข้าไปในฟังก์ชั่น showImage ครับ
private function showImage($filename,$maxWidth=-1,$maxHeight=-1,$type=""){
   
 if(empty($filename))
  $this->throwError("image filename not found");
   
  //validate input
  if($type == self::TYPE_EXACT || $type == self::TYPE_EXACT_TOP){
   if($maxHeight == -1)
    $this->throwError("image with exact type must have height!");
   if($maxWidth == -1)
    $this->throwError("image with exact type must have width!");
  }
   
  $filepath = $this->pathImages.$filename;     
   
   
  if(!is_file($filepath)) $this->outputEmptyImageCode();
   
  //if gd library doesn't exists - output normal image without resizing.
  if(function_exists("gd_info") == false)
   $this->throwError("php must support GD Library");
  //check conditions for output original image
  if(empty($this->effect)){
   if((is_numeric($maxWidth) == false || is_numeric($maxHeight) == false)) outputImage($filepath);
   
   if($maxWidth == -1 && $maxHeight == -1) 
    $this->outputImage($filepath);
  }
   
  if($maxWidth == -1) $maxWidth = 1000000;
  if($maxHeight == -1) $maxHeight = 100000;
   
  //init variables
  $this->filename = $filename;
  $this->maxWidth = $maxWidth;
  $this->maxHeight = $maxHeight;
  $this->type = $type;
   
  $filepathNew = $this->getThumbFilepath();
   
  if(is_file($filepathNew)){
   $this->outputImage($filepathNew);
   exit();  
  }
   
  try{
   if($type == self::TYPE_EXACT || $type == self::TYPE_EXACT_TOP){
    $isSaved = $this->cropImageSaveNew($filepath,$filepathNew);
   }
   else 
    $isSaved = $this->resizeImageSaveNew($filepath,$filepathNew);
   
   if($isSaved == false){
    $this->outputImage($filepath);
    exit();
   }
    
  }catch(Exception $e){
   $this->outputImage($filepath);
  }
      
  if(is_file($filepathNew)) 
   $this->outputImage($filepathNew);
  else 
   $this->outputImage($filepath);
  
  exit();
}
ค่าในตัวแปร $filename ถูกต่อ String กับค่าในตัวแปร $pathImages โดยค่าที่ถูกรวมกันแล้วนั้นจะไปอยู่ในตัวแปร $filepath และตัวแปร $filepath ก็ถูกส่งต่อไปให้กับฟังก์ชั่น outputImage ครับ
private function outputImage($filepath){
      
 $info = UniteFunctionsRev::getPathInfo($filepath);
 $ext = $info["extension"];    
 $filetime = filemtime($filepath);
   
 $ext = strtolower($ext);
 if($ext == "jpg")
  $ext = "jpeg";
   
 $numExpires = 31536000; //one year
 $strExpires = @date('D, d M Y H:i:s',time()+$numExpires);
 $strModified = @date('D, d M Y H:i:s',$filetime);
   
 $contents = file_get_contents($filepath);
 $filesize = strlen($contents);
 header("Last-Modified: $strModified GMT");
 header("Expires: $strExpires GMT");
 header("Cache-Control: public");
 header("Content-Type: image/$ext");
 header("Content-Length: $filesize");
   
 echo $contents;
 exit();
}
จะเห็นว่าตัวแปร $filepath ที่ถูกส่งเข้าฟังก์ชั่น outputImage มานั้นถูกส่งเข้าไปในฟังก์ชั่น file_get_contests (อ่านไฟล์ออกมาเป็น string) ต่อและถูกเก็บค่าที่อ่านได้นั้น มาใส่ไว้ในตัวแปร $content จากนั้นก็ echo เพื่อแสดงข้อความที่ get content มาโดยไม่มีการกรอง Input ใดๆเลยครับ
ตอนนี้แพทช์ที่แก้ไขช่องโหว่ดังกล่าวได้ออกมาแล้ว โดยเพิ่ม Whitelist extension หรือนามสกุลไฟล์ที่อนุญาตเข้าไปนั้นเองครับ
private function outputImage($filepath){
      
 $info = UniteFunctionsRev::getPathInfo($filepath);
 $ext = $info["extension"];    
 $filetime = filemtime($filepath);
   
 $ext = strtolower($ext);
 $good_extensions = array('jpg', 'png', 'gif', 'jpeg', 'tiff', 'bmp');
 
 if(empty($ext) || !in_array($ext, $good_extensions)){
  header("HTTP/1.1 403 Unauthorized" );
  die('Unauthorized');
 }
 
 if($ext == "jpg")
  $ext = "jpeg";
   
 $numExpires = 31536000; //one year
 $strExpires = @date('D, d M Y H:i:s',time()+$numExpires);
 $strModified = @date('D, d M Y H:i:s',$filetime);
   
 $contents = file_get_contents($filepath);
 $filesize = strlen($contents);
 header("Last-Modified: $strModified GMT");
 header("Expires: $strExpires GMT");
 header("Cache-Control: public");
 header("Content-Type: image/$ext");
 header("Content-Length: $filesize");
   
 echo $contents;
 exit();
}
โดยในเงื่อนไข if หมายความว่า ถ้านามสกุลไฟล์ที่ใส่มาในพารามิเตอร์ img ไม่ได้อยู่ใน Array คือไม่ได้เป็นรูปภาพ ให้ตอบกลับไปว่า Unauthorized ครับ สำหรับใครที่ใช้ปลั๊กอินหรือธีมที่มี Slider Revolution ควรอัพเดทให้เป็นเวอร์ชั่นล่าสุดเพื่อป้องกันการถูกโจมตีด้วยนะครับ
Ref: Russian malware targets WordPress users, over 100,000 sites infected
Ref: RevSlider Vulnerability Leads To Massive WordPress SoakSoak Compromise
Ref: 2600 Thailand

ไม่มีความคิดเห็น:
แสดงความคิดเห็น