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