首頁 >專題 >寶塔面板 >寶塔面板phpMyAdmin未授權存取安全漏洞是低階錯誤嗎?

寶塔面板phpMyAdmin未授權存取安全漏洞是低階錯誤嗎?

coldplay.xixi
coldplay.xixi轉載
2020-08-24 14:21:484766瀏覽

週日晚,某群裡突然發布了一則訊息,寶塔面板的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版本。但是,寶塔的安裝是一個傻瓜化的一鍵化腳本:

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

並沒有給到用戶一個可以選擇版本號的選項,官方的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方法,这个方法其实也是新加的:

    def start(self,puri,document_root,last_path = ''):
        '''
            @name 开始处理PHP请求
            @author hwliang<2020-07-11>
            @param puri string(URI地址)
            @return socket or Response
        &#39;&#39;&#39;
        ...
        #如果是PHP文件
        if puri[-4:] == &#39;.php&#39;:
            if  request.path.find(&#39;/phpmyadmin/&#39;) != -1:
                ...
                if request.method == &#39;POST&#39;:
                    #登录phpmyadmin
                    if puri in [&#39;index.php&#39;,&#39;/index.php&#39;]:
                        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(&#39;pma_username&#39;)
                        if username:
                            password = request.form.get(&#39;pma_password&#39;)
                            if not self.write_pma_passwd(username,password):
                                return Resp(&#39;未安装phpmyadmin&#39;)
                if puri in [&#39;logout.php&#39;,&#39;/logout.php&#39;]:
                    self.write_pma_passwd(None,None)
            else:
                ...
      #如果是静态文件
        return send_file(filename)

           

代码太长,我们不展开分析,只我写出来的部分。在请求的路径是/phpmyadmin/index.php且存在pma_usernamepma_password时,则执行self.write_pma_passwd(username,password)

跟进self.write_pma_passwd:

    def write_pma_passwd(self,username,password):
        &#39;&#39;&#39;
            @name 写入mysql帐号密码到配置文件
            @author hwliang<2020-07-13>
            @param username string(用户名)
            @param password string(密码)
            @return bool
        &#39;&#39;&#39;
        self.check_phpmyadmin_phpversion()
        pconfig = &#39;cookie&#39;
        if username:
            pconfig = &#39;config&#39;
        pma_path = &#39;/www/server/phpmyadmin/&#39;
        pma_config_file = os.path.join(pma_path,&#39;pma/config.inc.php&#39;)
        conf = public.readFile(pma_config_file)
        if not conf: return False
        rep = r"/\* Authentication type \*/(.|\n)+/\* Server parameters \*/"
        rstr = &#39;&#39;&#39;/* Authentication type */
$cfg[&#39;Servers&#39;][$i][&#39;auth_type&#39;] = &#39;{}&#39;;
$cfg[&#39;Servers&#39;][$i][&#39;host&#39;] = &#39;localhost&#39;; 
$cfg[&#39;Servers&#39;][$i][&#39;port&#39;] = &#39;{}&#39;;
$cfg[&#39;Servers&#39;][$i][&#39;user&#39;] = &#39;{}&#39;; 
$cfg[&#39;Servers&#39;][$i][&#39;password&#39;] = &#39;{}&#39;; 
/* Server parameters */&#39;&#39;&#39;.format(pconfig,self.get_mysql_port(),username,password)
        conf = re.sub(rep,rstr,conf)
        public.writeFile(pma_config_file,conf)
        return True

这个代码也很好理解了,如果传入了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安全配置、自動的版本更新等等,相較於許多國內其他的商業系統,絕對屬於有過之而無不及了。但看程式碼其實需要改進的地方還有很多,以後有機會再細說吧。

本文來自公眾編號:https://mp.weixin.qq.com/s/3ZjwFo5gWlJACSkeYWQLXA

以上是寶塔面板phpMyAdmin未授權存取安全漏洞是低階錯誤嗎?的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文轉載於:weixin。如有侵權,請聯絡admin@php.cn刪除