หน้าเว็บ

วันพุธที่ 7 มกราคม พ.ศ. 2558

In-Depth Analysis: Slider Revolution (lead to SoakSoak malware)

          

          เกริ่นก่อนว่าไม่ใช่ช่องโหว่ใหม่อะไรนะครับ ถถถ 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

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

แสดงความคิดเห็น