Roundcube mail代码审计笔记
以下是一篇不完整的文章,主要记录了在审计过程中的一些记录,在面对这类复杂的代码审计的时候,一旦被打断或者过后重新复习都会花费巨大的代价,所以这次稍微记录了一下结构。以下笔记适用于 2020-05-29 17:57:00 Author: lorexxar.cn(查看原文) 阅读量:406 收藏


以下是一篇不完整的文章,主要记录了在审计过程中的一些记录,在面对这类复杂的代码审计的时候,一旦被打断或者过后重新复习都会花费巨大的代价,所以这次稍微记录了一下结构。

以下笔记适用于 Roundcube mail 1.4.4

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

├─bin

├─config

├─installer

├─logs

├─plugins

│ ├─acl

│ ├─additional_message_headers

│ ├─archive

│ ├─attachment_reminder

│ ├─autologon

│ ├─database_attachments

│ ├─debug_logger

│ ├─emoticons

│ ├─enigma

│ ├─example_addressbook

│ ├─filesystem_attachments

│ ├─help

│ ├─hide_blockquote

│ ├─http_authentication

│ ├─identicon

│ ├─identity_select

│ ├─jqueryui

│ ├─krb_authentication

│ ├─managesieve

│ ├─markasjunk

│ ├─newmail_notifier

│ ├─new_user_dialog

│ ├─new_user_identity

│ ├─password

│ ├─redundant_attachments

│ ├─show_additional_headers

│ ├─squirrelmail_usercopy

│ ├─subscriptions_option

│ ├─userinfo

│ ├─vcard_attachments

│ ├─virtuser_file

│ ├─virtuser_query

│ └─zipdownload

├─program

│ ├─include

│ ├─js

│ ├─lib

│ │ └─Roundcube

│ │ ├─cache

│ │ ├─db

│ │ ├─session

│ │ └─spellchecker

│ ├─localization

│ ├─resources

│ └─steps

│ ├─addressbook

│ ├─mail

│ ├─settings

│ └─utils

├─public_html

├─skins

├─SQL 数据库备份

├─temp

└─vendor 外部引入的包

├─bin

├─composer

├─endroid

├─kolab

├─masterminds

├─pear

│ ├─auth_sasl

│ ├─console_commandline

│ ├─console_getopt

│ ├─crypt_gpg

│ ├─mail_mime

│ ├─net_idna2

│ ├─net_ldap2

│ ├─net_sieve

│ ├─net_smtp

│ ├─net_socket

│ ├─pear-core-minimal

│ └─pear_exception

└─roundcube

在审计roundcube mail的代码过程中,我们可以把目标的重心放在program目录下,其中

include、lib、steps这三个目录分别包含了整个系统最核心的相关代码。

1

2

3

4

├─program

│ ├─include

│ ├─lib

│ └─steps

换言之,也就是说,除了steps以外的代码只包含类以及函数定义,并没有实际的调用代码,所以我们的目标关注点主要集中在入口点steps。

在弄明白roundcube的结构时,首先我们把目标放在路由入口处。

值得注意的是steps中的代码都是.inc结尾的,所以我们必须要从入口文件进入才能走到具体的代码部分。

首先我们要关注:

1

index.php

路由分配

in index.php line 100

1

2

$RCMAIL->set_task($startup['task']);

$RCMAIL->action = $startup['action'];

这里通过task和action做路由表的分配。

1

?_task=utils&_action=text2html

直接指向

1

/program/steps/utils/text2html.inc

当然,这一切都建立在有权限的情况下,如果没有登陆,则会在

index.php line 217-251

1

2

3

4

5

6

7

8

9

10

11

12

13

$plugin = $RCMAIL->plugins->exec_hook('unauthenticated', array(

'task' => 'login',

'error' => $session_error,

'http_code' => empty($session_error) && !empty($error_message) ? 401 : 200

));

$RCMAIL->set_task($plugin['task']);

if ($plugin['http_code'] == 401) {

header('HTTP/1.0 401 Unauthorized');

}

$OUTPUT->send($plugin['task']);

跳回登录页面

相应的引入路由文件的代码如下

image.png-91.5kB

在引入每个路由文件之前,还会相应的先引入func.php。

在index.php中,除了基本的路由分配以外,还有一个重要的特性。

csrf check

in index.php line 254

1

2

$RCMAIL->request_security_check();

跟到 program/include/rcmail.php line 961

1

2

3

4

5

6

7

8

public function request_security_check($mode = rcube_utils::INPUT_POST)

{

if (!$this->check_request($mode)) {

$error = array('code' => 403, 'message' => "Request security check failed");

self::raise_error($error, false, true);

}

}

然后跟入 program/lib/roundcube/rcube.php line 955

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

public function check_request($mode = rcube_utils::INPUT_POST)

{

if ($token = $this->get_secure_url_token()) {

foreach (explode('/', preg_replace('/[?#&].*$/', '', $_SERVER['REQUEST_URI'])) as $tok) {

if ($tok == $token) {

return true;

}

}

$this->request_status = self::REQUEST_ERROR_URL;

return false;

}

$sess_tok = $this->get_request_token();

if (rcube_utils::request_header('X-Roundcube-Request') === $sess_tok) {

return true;

}

if (($mode == rcube_utils::INPUT_POST && empty($_POST))

|| ($mode == rcube_utils::INPUT_GET && empty($_GET))

) {

return true;

}

$token = rcube_utils::get_input_value('_token', $mode);

$sess_id = $_COOKIE[ini_get('session.name')];

if (empty($sess_id) || $token !== $sess_tok) {

$this->request_status = self::REQUEST_ERROR_TOKEN;

return false;

}

return true;

}

可以比较清晰的看到,check request只默认检查POST的token。

你必须保证session id有效,并且token与session中存取的相同

1

2

3

4

if (empty($sess_id) || $token !== $sess_tok) {

$this->request_status = self::REQUEST_ERROR_TOKEN;

return false;

}

除此之外,ajax还支持把token写在header里

1

2

3

4

if (rcube_utils::request_header('X-Roundcube-Request') === $sess_tok) {

return true;

}

这个csrf check对安全性的提升是比较巨大的,可以完全防护csrf类漏洞,而且在一定程度上也保护了2次漏洞的发生(如1-click to xxx)

当然他对实际的漏洞没有防护帮助,这个token我们可以在后台的很多地方找到。

roundcube的MVC结构,出口函数为

1

$OUTPUT->send()

跟随这个send函数,我们可以找到引入模板文件的位置

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

program/include/rcmail_output_html.php line 602

public function send($templ = null, $exit = true)

{

if ($templ != 'iframe') {

if ($exit != 'recur' && $this->app->plugins->is_processing('render_page')) {

rcube::raise_error(array('code' => 505, 'type' => 'php',

'file' => __FILE__, 'line' => __LINE__,

'message' => 'Recursion alert: ignoring output->send()'), true, false);

return;

}

$this->parse($templ, false);

}

else {

$this->framed = true;

$this->write();

}

ob_flush();

flush();

if ($exit) {

exit;

}

}

在602行parse主要完成引入模板的工作,跟入

1

program/include/rcmail_output_html.php line 695

image.png-127.2kB

从这里我们就可以看到模板被引入了,比较可惜的是,这里的模板名字无法控制,否则可以构造本地文件包含来攻击。

跟入到后面的_write函数可以看到对模板的编译以及替换
image.png-82.3kB

而具体到相关的模板对象编译,则到涉及到

1

program/include/rcmail_output_html.php line 1217

image.png-42.2kB

program/include/rcmail_output_html.php line 1472,涉及到外部object的变量会通过exechook取值,并暂时赋值为临时变量

1

2

3

4

5

6

7

$hook = $this->app->plugins->exec_hook("template_object_$object", $attrib + array('content' => $content));

if (strlen($hook['content']) && !empty($external)) {

$object_id = uniqid('TEMPLOBJECT:', true);

$this->objects[$object_id] = $hook['content'];

$hook['content'] = $object_id;

}

_write中 postrender 函数

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

protected function postrender($output)

{

foreach ($this->objects as $key => $val) {

$output = str_replace($key, $val, $output, $count);

if ($count) {

$this->objects[$key] = null;

}

}

$output = preg_replace_callback('/<form\s+([^>]+)>/Ui', array($this, 'alter_form_tag'), $output);

return $output;

}

相应的类变量被重新刷新回去

过滤函数

Roundcube在过滤函数上得思路比较清奇,主要集中在输出过滤上,在输入点或者过程储存上大多不会对数据做过多得处理。

数据的出口主要集中在

1

2

3

\program\include\rcmail_output_html.php

show_message 等函数

主要的过滤函数为

1

2

3

- rcube::Q

- html::

- new html_inputfield

等这类函数,其中主要的过滤函数出口类似,我们这里主要看其中1个

1

2

3

4

public static function Q($str, $mode = 'strict', $newlines = true)

{

return rcube_utils::rep_specialchars_output($str, 'html', $mode, $newlines);

}

然后跟入program/lib/roundcube/rcube_utils.php line 165

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

public static function rep_specialchars_output($str, $enctype = '', $mode = '', $newlines = true)

{

static $html_encode_arr = false;

static $js_rep_table = false;

static $xml_rep_table = false;

if (!is_string($str)) {

$str = strval($str);

}

if ($enctype == 'html') {

if (!$html_encode_arr) {

$html_encode_arr = get_html_translation_table(HTML_SPECIALCHARS);

unset($html_encode_arr['?']);

}

$encode_arr = $html_encode_arr;

if ($mode == 'remove') {

$str = strip_tags($str);

}

else if ($mode != 'strict') {

$ltpos = strpos($str, '<');

if ($ltpos !== false && strpos($str, '>', $ltpos) !== false) {

unset($encode_arr['"']);

unset($encode_arr['<']);

unset($encode_arr['>']);

unset($encode_arr['&']);

}

}

$out = strtr($str, $encode_arr);

return $newlines ? nl2br($out) : $out;

}

if ($js_rep_table === false) {

$js_rep_table = $xml_rep_table = array();

$xml_rep_table['&'] = '&amp;';

for ($c=160; $c<256; $c++) {

$xml_rep_table[chr($c)] = "&#$c;";

}

$xml_rep_table['"'] = '&quot;';

$js_rep_table['"'] = '\\"';

$js_rep_table["'"] = "\\'";

$js_rep_table["\\"] = "\\\\";

$js_rep_table[chr(hexdec('E2')).chr(hexdec('80')).chr(hexdec('A8'))] = '&#8232;';

$js_rep_table[chr(hexdec('E2')).chr(hexdec('80')).chr(hexdec('A9'))] = '&#8233;';

}

if ($enctype == 'js') {

return preg_replace(array("/\r?\n/", "/\r/", '/<\\//'), array('\n', '\n', '<\\/'), strtr($str, $js_rep_table));

}

if ($enctype == 'text') {

return str_replace("\r\n", "\n", $mode == 'remove' ? strip_tags($str) : $str);

}

if ($enctype == 'url') {

return rawurlencode($str);

}

if ($enctype == 'xml') {

return strtr($str, $xml_rep_table);

}

return $str;

}

仔细观察不难发现,其实过滤的方向主要在单双引号的转义,尖括号的转义上。当然,这样的转义已经足够应对90%的情况了。


文章来源: https://lorexxar.cn/2020/05/29/roundcube-mail-1-4-4/
如有侵权请联系:admin#unsafe.sh