首先来看一段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