เมื่อวันที่ 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