寶塔面板phpMyAdmin未授權訪問漏洞是個低級錯誤嗎?

週日晚,某群裡突然發佈了一則消息,寶塔面板的phpmyadmin存在未授權訪問漏洞的緊急漏洞預警,並給出了一大批存在漏洞的URL:

寶塔面板phpMyAdmin未授權訪問漏洞是個低級錯誤嗎?

隨便點開其中一個,赫然就是一個大大的phpmyadmin後臺管理頁面,無需任何認證與登錄。當然,隨後各種神圖神事也都刷爆了社交網絡,作為一個冷靜安全研究者,我對此當然是一笑置之,但是這個漏洞的原因我還是頗感興趣的,所以本文我們就來考證一下整件事情的緣由。

我們的問題究竟是什麼?

首先,我先給出一個結論:這件事情絕對不是簡簡單單地有一個pma目錄忘記刪除了,或者寶塔面板疏忽大意進行了錯誤地配置,更不是像某些人陰謀論中說到的官方刻意留的後門。

我為什麼這麼說?首先,根據官方的說法,這個漏洞隻影響如下版本:

  • Linux正式版7.4.2
  • Linux測試版7.5.13
  • Windows正式版6.8

這個版本就是最新版(漏洞修復版)的前一個版本。也就是說,這個確定的小版本之前的版本面板是不受影響的。我們試想一下,如果是“後門”或者官方忘記刪除的目錄,為什麼隻影響這一個版本呢?況且寶塔面板發展了這麼久,積累了400萬用戶,體系安全性也相對比較成熟,如果存在這麼低劣的錯誤或“後門”,也應該早就被發現了。

經過實際查看互聯網上的案例和詢問使用了寶塔面板的朋友,我發現在7.4.2以前的版本中沒有pma這個目錄,並且phpmyadmin默認情況下認證方法是需要輸入賬號密碼的。所以,寶塔出現這個漏洞,一定是做過了下面這兩件事:

  • 新增了一個pma目錄,內容phpmyadmin
  • phpmyadmin的配置文件被修改了認證方式

那麼,我們的問題就變成了,官方為什麼要做這兩處修改,目的究竟是什麼

為了研究這個問題,我們需要先安裝一個寶塔7.4.2版本。但是,寶塔的安裝是一個傻瓜化的一鍵化腳本:

<code>yum install -y wget && wget -O install.sh http://download.bt.cn/install/install_6.0.sh && sh install.sh
/<code>

並沒有給到用戶一個可以選擇版本號的選項,官方的Git也許久沒更新了,我們如何才能安裝到一個合適的版本(7.4.2)呢?

安裝一個合適的版本

這當然難不倒我。首先,我安裝了最新版的寶塔面板,用的就是上述一鍵化腳本。

安裝的過程自然沒什麼問題,安裝完成後,系統顯示的版本號是最新版7.4.3,因為在爆出這個漏洞以後,官方迅速進行了修復升級。不過沒關係,我們仍然可以找到離線升級包:

  • http://download.bt.cn/install/update/LinuxPanel-7.4.0.zip
  • http://download.bt.cn/install/update/LinuxPanel-7.4.2.zip
  • http://download.bt.cn/install/update/LinuxPanel-7.4.3.zip

分別是7.4.0/7.4.2/7.4.3的版本,我們分別下載並解壓,並嘗試將自己的服務器版本恢復成漏洞版本7.4.2。

在恢復代碼之前,我們先將服務器斷網,或者將寶塔設置成離線模式:

寶塔面板phpMyAdmin未授權訪問漏洞是個低級錯誤嗎?

這麼做的目的是防止寶塔進行自動版本更新,避免好不容易恢復的代碼又自動升級了。

寶塔系統代碼默認安裝完是在/www/server/panel,接著我們直接將將壓縮包內的panel目錄上傳到這裡來,覆蓋掉已有的文件。重啟下寶塔,即可發現系統版本號已經恢復成7.4.2了:

寶塔面板phpMyAdmin未授權訪問漏洞是個低級錯誤嗎?

還沒完,我們使用beyond compare打開7.4.2和7.4.3的壓縮包代碼,先看看官方是怎麼修復的漏洞:

寶塔面板phpMyAdmin未授權訪問漏洞是個低級錯誤嗎?

比較粗暴,直接判斷目錄/www/server/phpmyadmin/pma是否存在,如果存在就直接刪掉。所以,我們雖然恢復了系統版本代碼,但刪掉的pma已經不在了,我們還需要恢復一下這個目錄。

方法也很簡單,/www/server/phpmyadmin下本身存在一個phpmyadmin目錄,我們直接複製一下這個目錄即可:

寶塔面板phpMyAdmin未授權訪問漏洞是個低級錯誤嗎?

漏洞究竟是怎麼回事

有了環境,我們仍需看看代碼。

首先,由於7.4.2是引入漏洞的版本,我們看看官方對7.4.2的更新日誌:

寶塔面板phpMyAdmin未授權訪問漏洞是個低級錯誤嗎?

用beyond compare打開7.4.0和7.4.2的壓縮包代碼,看看具體增加了哪些代碼:

寶塔面板phpMyAdmin未授權訪問漏洞是個低級錯誤嗎?

可見,在7.4.2版本中增加了兩個視圖,分別對應著phpmyadmin和adminer。視圖中用到了panelPHP#start方法,這個方法其實也是新加的:

<code>    def start(self,puri,document_root,last_path = ''):
        '''
            @name 開始處理PHP請求
            @author hwliang<2020-07-11>
            @param puri string(URI地址)
            @return socket or Response
        '''
        ...

        #如果是PHP文件
        if puri[-4:] == '.php':
            if  request.path.find('/phpmyadmin/') != -1:
                ...
                if request.method == 'POST':
                    #登錄phpmyadmin
                    if puri in ['index.php','/index.php']:
                        content = public.url_encode(request.form.to_dict())
                        if not isinstance(content,bytes):
                            content = content.encode()
                        self.re_io = StringIO(content)
                        username = request.form.get('pma_username')
                        if username:
                            password = request.form.get('pma_password')
                            if not self.write_pma_passwd(username,password):
                                return Resp('未安裝phpmyadmin')

                if puri in ['logout.php','/logout.php']:
                    self.write_pma_passwd(None,None)
            else:
                ...
           
        #如果是靜態文件
        return send_file(filename)
/<code>

代碼太長,我們不展開分析,只我寫出來的部分。在請求的路徑是/phpmyadmin/index.php且存在pma_username、pma_password時,則執行self.write_pma_passwd(username,password)。

跟進self.write_pma_passwd:

<code>    def write_pma_passwd(self,username,password):
        '''
            @name 寫入mysql帳號密碼到配置文件
            @author hwliang<2020-07-13>
            @param username string(用戶名)
            @param password string(密碼)
            @return bool
        '''

        self.check_phpmyadmin_phpversion()
        pconfig = 'cookie'
        if username:
            pconfig = 'config'
        pma_path = '/www/server/phpmyadmin/'
        pma_config_file = os.path.join(pma_path,'pma/config.inc.php')
        conf = public.readFile(pma_config_file)
        if not conf: return False
        rep = r"/\* Authentication type \*/(.|\n)+/\* Server parameters \*/"
        rstr = '''/* Authentication type */
$cfg['Servers'][$i]['auth_type'] = '{}';
$cfg['Servers'][$i]['host'] = 'localhost'; 
$cfg['Servers'][$i]['port'] = '{}';
$cfg['Servers'][$i]['user'] = '{}'; 
$cfg['Servers'][$i]['password'] = '{}'; 
/* Server parameters */'''.format(pconfig,self.get_mysql_port(),username,password)
        conf = re.sub(rep,rstr,conf)
        public.writeFile(pma_config_file,conf)
        return True
/<code>

這個代碼也很好理解了,如果傳入了username和password的情況下,寶塔會改寫phpmyadmin的配置文件config.inc.php,將認證方式改成config,並寫死賬號密碼。

這就是為什麼7.4.2版本中pma可以直接訪問的原因。

補個課:

phpmyadmin支持數種認證方法,默認情況下是Cookie認證,此時需要輸入賬號密碼;用戶也可以將認證方式修改成Config認證,此時phpmyadmin會使用配置文件中的賬號密碼來連接mysql數據庫,即不用再輸入賬號密碼。

官方做這些動作的原因

其實各位看官看到這裡肯定腦子裡還是一團漿糊,這些代碼究竟意味著什麼呢?為什麼官方要將認證模式改成config模式?

是很多漏洞分析文章的通病,這些文章在出現漏洞後跟一遍漏洞代碼,找到漏洞發生點和利用方法就結束了,並沒有深入研究開發為什麼會這麼寫,那麼下次你還是挖不出漏洞。

所以,這裡思考一下,我們現在起碼還有下列疑問:

  • 在7.4.2版本以前,用戶是如何使用phpmyadmin的?
  • 寶塔為什麼要在7.4.2版本增加phpmyadmin有關的視圖?
  • 寶塔為什麼要將phpmyadmin認證模式改成config?
  • 我們如何復現這個漏洞?

第一個問題,我們其實可以簡單找到答案。在正常安裝寶塔最新版7.4.3時,我們點擊寶塔後臺的phpmyadmin鏈接,會訪問到這樣一個路徑:

寶塔面板phpMyAdmin未授權訪問漏洞是個低級錯誤嗎?

7.4.3版本為了修復這個漏洞,回滾了部分代碼,所以這種方式其實就是7.4.2以前版本的phpmyadmin的訪問方式:通過888端口下的一個以phpmyadmin_開頭的文件夾直接訪問phpmyadmin。

這種老的訪問方法中,888端口是一個單獨的Nginx或Apache服務器,整個東西是安全的,訪問也需要輸入賬號密碼。

但是這種訪問方法有些麻煩,需要額外開放888端口,而且每次登陸都要重新輸入密碼。所以,官方開發人員提出了一種新的做法,在寶塔後端的python層面轉發用戶對phpmyadmin的請求給php-fpm。這樣有三個好處:

  • 直接在python層面做用戶認證,和寶塔的用戶認證進行統一,不需要多次輸入mysql密碼
  • 也不需要再對外開放888端口了
  • 使用phpmyadmin也不再依賴於Nginx/Apache等服務器中間件了

這就是為什麼寶塔要在7.4.2增加phpmyadmin有關的視圖的原因,這個視圖就是一個phpmyadmin的代理,做的事情就是轉發用戶的請求給php-fpm。

用戶在第一次使用這種方式登錄時,系統會自動發送包含了Mysql賬號密碼的數據包,寶塔後端會捕捉到此時的賬號密碼,填入phpmyadmin的配置文件,並將認證方式改成config。對於用戶來說,感受到的體驗就是,不再需要輸入任何Mysql密碼即可使用phpmyadmin了。

這的確給用戶的使用帶來了更好的體驗。

漏洞復現

此時我們應該還有個疑問:既然官方目的是“直接在python層面做用戶認證,和寶塔的用戶認證進行統一”,那麼仍然是有認證的呀?為什麼會出現未授權訪問漏洞呢?

我們可以來複現一下這個漏洞。首先,我們以系統管理員的身份登錄寶塔後臺,來到數據庫頁面,點擊“phpMyAdmin”按鈕,會彈出如下模態框:

寶塔面板phpMyAdmin未授權訪問漏洞是個低級錯誤嗎?

這個裡面有兩種訪問模式,“通過Nginx/Apache/OIs訪問”是老版本的訪問方式,“通過面板安全訪問”就是7.4.2新增加的代理模式。

我們點擊“通過面板安全訪問”,並抓包,會抓到這樣一個數據包:

寶塔面板phpMyAdmin未授權訪問漏洞是個低級錯誤嗎?

寶塔前端將我們的Mysql賬號密碼填好了直接發給phpmyadmin。又因為我們前面分析過的那段代碼,後臺將賬號密碼直接寫入了phpmyadmin配置文件,來做到免認證的邏輯。

如果一個未認證的用戶,直接訪問http://ip:8888/phpmyadmin/index.php呢?會被直接重定向到登錄頁面:

寶塔面板phpMyAdmin未授權訪問漏洞是個低級錯誤嗎?

如果僅僅是這樣,這個過程是不存在漏洞的。但是,官方開發人員犯了一個錯誤,他將pma應用放在了/www/server/phpmyadmin目錄下,而這個目錄原本是老的phpmyadmin訪問方式所使用的Web根目錄。

這意味著,我通過老的888端口+pma目錄,可以訪問到新的phpmyadmin,而新的phpmyadmin又被官方修改了配置文件,最終導致了未授權訪問漏洞:

寶塔面板phpMyAdmin未授權訪問漏洞是個低級錯誤嗎?

所以,如何解決這個問題呢?也很簡單,只需要將pma移到其他目錄去即可。

總結

我們來做個總結。

首先,寶塔面板絕對不是傻瓜,這個漏洞不是簡簡單單的放了一個未授權的pma在外面忘記刪。這其實會打很多人臉,因為大部分人認為這只是個簡單的phpmyadmin未授權訪問漏洞,並對寶塔進行了一頓diss,沒有想到這後面其實是一個複雜的邏輯錯誤。

其次,用戶體驗和安全絕對是不衝突的,我十分不喜歡為了保障安全而閹割用戶體驗的做法。所以希望寶塔官方不會因為這次的漏洞事件而徹底將代碼回滾(據說7.4.3的更新只是臨時解決方案),該改進的地方還是要改進。

我有數年不再使用Linux面板了,這次也算重新體驗了一下2020年的Linux面板,個人感覺寶塔看外在其實是一個比較注重安全的系統,比如自動生成的用戶密碼、用戶名和密碼的策略、默認的Php安全配置、自動的版本更新等等,相比於很多國內其他的商業系統,絕對屬於有過之而無不及了。但是看代碼其實需要改進的地方還有很多,這個以後有機會再細說吧。


分享到:


相關文章: