首先来看一段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");
// 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().这是每次请求的时候都会运行到的。

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的时候

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

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

假如当前是 10:00

php代码是

1
2
3
4
5
6
7
8
9
echo "start";
sleep(5);
//Insert some code.
echo "pause";
sleep(10)

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

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

为什么我要说这个呢?

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

1
srand(time());

时候这里的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);
/* 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

Comments

⬆︎TOP