首先来看一段CI内核在开启session储存在数据库选项的时候的操作.也就是
/system/core/config.php
内核配置文件设置为以下的时候
$config['sess_use_database'] = true;
/system/libraries/Session/session.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153
| public function __construct($params = array())
{ log_message('debug', "Session Class Initialized"); $this->CI =& get_instance(); 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'; } $this->CI->load->helper('string'); if ($this->sess_encrypt_cookie == TRUE) { $this->CI->load->library('encrypt'); } if ($this->sess_use_database === TRUE AND $this->sess_table_name != '') { $this->CI->load->database(); } $this->now = $this->_get_time(); if ($this->sess_expiration == 0) { $this->sess_expiration = (60*60*24*365*2); } $this->sess_cookie_name = $this->cookie_prefix.$this->sess_cookie_name; if ( ! $this->sess_read()) { $this->sess_create(); } else { $this->sess_update(); } $this->_flashdata_sweep(); $this->_flashdata_mark(); $this->_sess_gc(); log_message('debug', "Session routines successfully run"); }
|
简单的用大白话来说,这段代码就是用来做一些对Session 的初始工作,检测配置,检测cookie是否设置等,检测配置最后有这个函数_sess_gc().这是每次请求的时候都会运行到的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| 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的时候

这个请求对应的返回的时间戳,是php先生成的。
假如当前是 10:00
php代码是
1 2 3 4 5 6 7 8 9
| echo "start"; sleep(5); echo "pause"; sleep(10)
|
大家可能觉得返回的时间戳是请求接受到的时间+处理的时间。10:15
实际上应该就是请求接收到的时间。
为什么我要说这个呢?
因为CI的框架本身代码执行时间就是等于返回的时间戳的时间。所以在运行到
时候这里的time()就等于返回的时间戳。
当种子设置为时间戳之后,之后所有的基于PRNG的函数全部变的可计算了。
举个例子:
FineCMS2.0.1有一个文件解压getshell漏洞
详情可以看
/bugs/wooyun-2010-064128
这篇文章。
官方的修复方案是将目录名称随机
1
| $temp = APP_ROOT.'cache/attack/'.md5(uniqid().rand(0, 9999)).'/'
|
让攻击者不可猜测,从而即使攻击者上传了shell之后,也找不到执行的目录。
假设开启了数据库储存session选项之后。那么即使官方用了两个随机函数,一个加密函数,都变辣鸡。
我们先来看uniqid()的php的内核源码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87
| 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); 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()函数,全部都会被击溃。
千里之堤,溃于蚁穴。
攻击者发送请求

根据服务器返回的时间,转为时间戳

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

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