หน้าเว็บ

วันจันทร์ที่ 10 พฤศจิกายน พ.ศ. 2557

In-Depth Analysis: IP.Board <= 3.4.7 SQL Injection

          เมื่อวันที่ 9 พ.ย. ที่ผ่านมาได้มีการปล่อย Exploit SQL Injection ของ IP.Board 3.4.7 ซึ่งเป็นเวอร์ชั่นล่าสุด ในเว็บไซต์ของ Full Disclosure และได้มีการ Public โค้ด Python ที่ใช้ในการโจมตีช่องโหว่ดังกล่าวด้วย จึงเป็นที่มาของบทความนี้ครับ
          IP.Board ที่ผมเอามาลองคือเวอร์ชั่น 3.4.6 ครับจากการอ่านโค้ดนั้นเขียนด้วย PHP OOP ไฟล์ที่มีปัญหาก็คือไฟล์ ipsconnect.php ที่ส่งค่าไปยัง ipsMember.php ครับ
/**
 *
 * Map - can modify to add additional parameters, but the IPS Community Suite will only send the defaults
 *
 */
$map = array(
 'login'  => array( 'idType', 'id', 'password', 'key', 'redirect', 'redirectHash' ),
 'logout' => array( 'id', 'key', 'redirect', 'redirectHash' ),
 'register' => array( 'key', 'username', 'displayname', 'password', 'email', 'revalidateurl' ),
 'cookies' => array( 'data' ),
 'check'  => array( 'key', 'id', 'username', 'displayname', 'email' ),
 'change' => array( 'id', 'key', 'username', 'displayname', 'email', 'password', 'redirect', 'redirectHash' ),
 'validate' => array( 'id', 'key' ),
 'delete' => array( 'id', 'key' )
 );

/**
 *
 * Process Logic - do not modify
 *
 */ 
$ipsConnect = new ipsConnect();
if ( isset( $_REQUEST['act'] ) and isset( $map[ $_REQUEST['act'] ] ) )
{
 $params = array();
 foreach ( $map[ $_REQUEST['act'] ] as $k )
 {
  $params[ $k ] = $_REQUEST[ $k ];
 }

 call_user_func_array( array( $ipsConnect, $_REQUEST['act'] ), $params );
}

exit;

          จากโค้ดมีการเรียกใช้คลาส ipsConnect() ที่อยู่ในไฟล์เดียวกันผ่านฟังก์ชั่น call_user_func_array โดยมีการส่งค่าจากตัวแปร $param ซึ่งรับค่า $_REQUEST ที่เก็บไว้ส่งเข้าไปด้วย
          เมื่อนำมาประกอบกันก็จะได้เป็น
call_user_func_array(array(ipsConnect,login),$param); // $param ค่าพารามิเตอร์ส่งเข้าไปในฟังก์ชั่น login

          เป็นการเรียกฟังก์ชั่น login ที่อยู่ในคลาส ipsConnect นั่นเองไล่ไปดูฟังก์ชั่น login กัน
 public function login( $identifier, $identifierValue, $md5Password, $key, $redirect, $redirectHash )
 {
  $member = NULL;
  $statusCode = 'MISSING_DATA';
  $secondsUntilUnlock = 0;
  $revalidateUrl = '';
 
  /* Check */
  if ( in_array( $identifier, array( 'id', 'email', 'username' ) ) )
  {
   $member = IPSMember::load( $identifierValue, 'none', $identifier );
   if ( $member['member_id'] )
   {

          มีการนำค่าที่ $param (idType) ที่รับมาเก็บอยู่ในตัวแปร $identifier จากนั้นเช็คว่าค่าที่ส่งเข้ามานั้นตรงกับค่าใดค่าหนึ่งใน array หรือไม่ด้วยฟังก์ชั่น in_array (idType=id) เมื่อเป็นจริงจึงส่งตัวแปร $identifier และ $identifierValue (id=-1,id[]=-1) เข้าไปในฟังก์ชั่น load ของคลาส IPSMember ในไฟล์ ipsMember.php ครับ
          ตามไปดูฟังก์ชั่น load ในคลาส IPSMember กันครับ
static public function load( $member_key, $extra_tables='all', $key_type='' )
...[snip]...
switch( $key_type )
{
 default:
 case 'id':
  if ( is_array( $member_key ) )
  {
   $multiple_ids = $member_key;
  }
  else
  {
   $member_value = intval( $member_key );
  }
  $member_field = 'member_id';
 break;

          จะเห็นว่าค่าพารามิเตอร์ที่ 3 ($key_type) ที่ส่งเข้ามาในฟังก์ชั่น load นั้นถูกส่งไปทีี่ switch case และตกที่ case id จากนั้นเช็คว่าค่า id ที่รับมานั้นเป็น array หรือไม่ด้วยฟังก์ชั่น is_array ซึ่งการโจมตีที่เกิดขึ้นทำให้เงื่อนไขนี้เป็นจริง และเก็บค่าดังกล่าวไว้ในตัวแปร $multiple_ids ตามไปดูตัวแปรนี้กันครับ
  else if( count($multiple_ids) AND is_array($multiple_ids) )
  {
   $_totalUsers = count($multiple_ids);
   $_gotFromCache = 0;
   $_fromCache  = array();
   
   foreach( $multiple_ids as $_memberValue )
   {
    $member = self::_fetchFromCache( $_memberValue, $_usedTables );
    
    if ( $member !== FALSE )
    {
     $_fromCache[ $member['member_id'] ] = $member;
     $_gotFromCache++;
    }
   }

          เงื่อนไขนี้จะเช็คตัวแปร $multiple_ids โดยฟังก์ชั่น count นับจำนวนใน array และเช็คว่าตัวแปรนี้เป็น array หรือไม่โดยฟังก์ชั่น is_array อีกแล้ว และเมื่อเป็นจริงก็ถูกจับใส่ foreach เพื่อวนเอาค่าในตัวแปร $multiple_ids ออกมาทีละตัวใส่ในตัวแปร $_memberValue และส่งตัวแปร $_memberValue เข้าไปในฟังก์ชั่น _fetchFromCache ครับ ปัญหาอยู่ตรงนี้ครับ ถ้าค่าใน $multiple_ids ไม่ใช่ array จะถูกเข้าฟังก์ชั่น intval ก่อนนำไปคิวรี่ในฟังก์ชั่น _fetchFromCache ($member_value = intval( $member_key );)
          ทดสอบการโจมตีด้วย Firefox + HackBar เมื่อค่าในตัวแปร $multiple_ids ไม่ใช่ array


          สังเกตว่าผมใส่ id=1icheernoom แต่เวลาคิวรี่จะเหลือแค่ 1 เพราะถูกฟังก์ชั่น intval ตัดออกไปเหลือแต่ตัวเลข แต่ถ้าค่าในตัวแปร $multiple_ids เป็น array


          Array ช่องที่ 0 (id[]) คือ  1icheernoom และเมื่อนำไปคิวรี่ยังเป็น 1icheernoom อยู่เพราะไม่ได้เข้าฟังก์ชั่น intval ไว้ ช่องโหว่จึงเกิดขึ้นตรงนี้ครับ


          เมื่อนำคิวรี่ที่ผมไฮท์ไลท์ไว้ไปคิวรี่ใน phpMyAdmin ก็จะได้เป็น


          ผลลัพธ์ของ COUNT(*) FROM members คือ 1 เพราะตอนทดสอบมีแค่ admin คนเดียว และเนื่องจากช่องโหว่นี้เป็น Error based และ IP.Board ได้มีการเก็บ SQL Error ไว้ในไฟล์ /cache/sql_error_latest.cgi (อ้างอิงจากในโค้ด python) ทำให้สามารถไปดึงค่าที่ Error นั้นออกมาได้ นับถือคนเจอบัคนี้เลย ชาบูๆ (=/\=)
          ถึงตอนนี้ Patch สำหรับแก้ไขช่องโหว่ IP.Board <= 3.4.7 SQL Injection ได้ออกมาแล้วครับและแก้ปัญหานี้โดย ถ้าค่าในตัวแปร  $multiple_ids เป็น array ก็ใช้ฟังก์ชั่น intval ครอบไว้ทุกค่าใน array โดยฟังก์ชั่น array_map ครับ
$multiple_ids = array_map( 'intval', $member_key );

          เป็นการส่งฟังก์ชั่น intval เข้าไปทำกับทุกค่าใน array ของตัวแปร $member_key ครับ
Ref : IP.Board version 3.4.7 (latest) suffers from a SQL injection vulnerability.
Ref : IP.Board 3.3.x, 3.4.x Security Update
Ref : 2600 Thailand

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

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