以下是一篇不完整的文章,主要记录了在审计过程中的一些记录,在面对这类复杂的代码审计的时候,一旦被打断或者过后重新复习都会花费巨大的代价,所以这次稍微记录了一下结构。
以下笔记适用于 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 │ ├─new mail_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
结尾的,所以我们必须要从入口文件进入才能走到具体的代码部分。
首先我们要关注:
路由分配 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' ]);
跳回登录页面
相应的引入路由文件的代码如下
在引入每个路由文件之前,还会相应的先引入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我们可以在后台的很多地方找到。
mvc结构 roundcube的MVC结构,出口函数为
跟随这个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
从这里我们就可以看到模板被引入了,比较可惜的是,这里的模板名字无法控制,否则可以构造本地文件包含来攻击。
跟入到后面的_write
函数可以看到对模板的编译以及替换
而具体到相关的模板对象编译,则到涉及到
1 program /include /rcmail_output_html.php line 1217
在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['&' ] = '&' ; for ($c=160 ; $c<256 ; $c++) { $xml_rep_table[chr($c)] = "&#$c;" ; } $xml_rep_table['"' ] = '"' ; $js_rep_table['"' ] = '\\"' ; $js_rep_table["'" ] = "\\'" ; $js_rep_table["\\" ] = "\\\\" ; $js_rep_table[chr(hexdec('E2' )).chr(hexdec('80' )).chr(hexdec('A8' ))] = '
' ; $js_rep_table[chr(hexdec('E2' )).chr(hexdec('80' )).chr(hexdec('A9' ))] = '
' ; } 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%的情况了。
这里主要是集中在分类上,如果说这里分类到转义比较清晰的路径上,就没什么办法和绕过什么的相关。
比如函数Q设置enctype为html,mode为strict,输出时就会转义包括尖括号、双引号等和XSS相关的符号。我们就没办法绕过了。