Microsoft 365 Security: Running Arbitrary Commands via the API

Running Arbitrary Commands on an Endpoint via the API

Microsoft Defender’s Live Response is a delightful addition to your Incident Response toolbelt, but it does have a particularly irksome deficiency. As of publication, it does not allow execution of arbitrary commands. I know. Ridiculous right? Well there is an simple fix for this…

C:\windows\system32\cmd.exe /c $args

yeah…that simple. cmd.ps1

All we need to do now is upload it to our Live Response Library like we did in my previous post…

Uploading cmd.ps1 to the Live Response Library

script = """
         C:\windows\system32\cmd.exe /c $args
         """
fileobj = StringIO(script)
fileobj.filename = "cmd.ps1"

postFileToLiveResponseLibrary(
    fileObjToUpload = fileobj,
    description = "Allows you to run arbitrary commands. Simply put the commands into the script parameters.",
    parameters = "Command you want to Run. Example: dir /s /b c:\ > cdrive.txt",
    override = True
    )

Output:

{'@odata.context': 'https://api.securitycenter.windows.com/api/$metadata#LibraryFiles/$entity',
 'fileName': 'cmd.ps1',
 'sha256': '8e1bc75666cede5d95cf422e413bf5ebd691a4cb0fbf768e5ed398c1624cb4eb',
 'description': 'Allows you to run arbitrary commands. Simply put the commands into the script parameters.',
 'creationTime': '2022-03-12T17:25:28.743863Z',
 'lastUpdatedTime': '2022-03-12T17:25:28.743863Z',
 'createdBy': 'Jonathan Glass',
 'hasParameters': False,
 'parametersDescription': 'Command you want to Run. Example: dir /s /b c:\\ > cdrive.txt'}

Executing Arbitrary Commands using cmd.ps1 via the API

Building the command sequence

This is a quick a method of compiling the json needed. Might not be perfect..


def addFileFromLibrary(FileName):
    command = {}
    command['type'] = "PutFile"
    command["params"] = []
    command["params"].append({"key":"FileName","value":FileName})
    return command

def addCmdToCommandList(cmdline):
    command = {}
    command['type'] = "RunScript"
    command["params"] = []
    command["params"].append({"key":"ScriptName","value":"cmd.ps1"})
    command["params"].append({"key":"Args","value":str(cmdline)})
    return command

def addGetfileToCommandList(filepath):
    command = {}
    command['type'] = "GetFile"
    command["params"] = []
    command["params"].append({"key":"Path","value":filepath})
    return command

# As simple example, if I wanted to look at the hosts DNS cache...
liveResponseCommands = {}
liveResponseCommands['Commands'] = []
# Add a command to run ipconfig and redirect to a file
command = addCmdToCommandList("ipconfig /displaydns > c:\dnscache.txt")
liveResponseCommands['Commands'].append(command)
# Add a command t collect the file's contents
command = addGetfileToCommandList("c:\dnscache.txt")
liveResponseCommands['Commands'].append(command)
# ALL LIVE RESPONSE REQUESTS MUST HAVE A COMMENT
liveResponseCommands['Comment'] = "Collecting DNS cache"

liveResponseCommands

Output:

{
"Commands": [
    {
        "type": "RunScript",
        "params": [
            {
                "key": "ScriptName",
                "value": "cmd.ps1"
            },
            {
                "key": "Args",
                "value": "ipconfig /displaydns > c:\\dnscache.txt"
            }
        ]
    },
    {
        "type": "GetFile",
        "params": [
            {
                "key": "Path",
                "value": "c:\\dnscache.txt"
            }
        ]
    }
],
"Comment": "Collecting DNS cache"
}

Running the Live Response Machine Action

To run this against a specific endpoint, grab the machine id and do something like this…

machineID = "796f7520617265207375706572206c616d652121"
cmdRequest = MDErequest("machines/%s/runliveresponse" % (machineID),liveResponseCommands)
cmdRequest

Output:

{'@odata.context': 'https://api.securitycenter.windows.com/api/$metadata#MachineActions/$entity',
    'id': '944850f6-8043-4ca4-a65f-b50d0a1ea6c8',
    'type': 'LiveResponse',
    'title': None,
    'requestor': 'JonathanGlass@HalfFullofSecurity.onmicrosoft.com',
    'requestorComment': 'Collecting DNS cache',
    'status': 'Pending',
    'machineId': '796f7520617265207375706572206c616d652121',
    'computerDnsName': 'desktop-l3mnbj9',
    'creationDateTimeUtc': '2022-03-13T17:06:03.5759458Z',
    'lastUpdateDateTimeUtc': '2022-03-13T17:06:03.5759458Z',
    'cancellationRequestor': None,
    'cancellationComment': None,
    'cancellationDateTimeUtc': None,
    'errorHResult': 0,
    'scope': None,
    'externalId': None,
    'requestSource': 'PublicApi',
    'relatedFileInfo': None,
    'commands': [{'index': 0,
    'startTime': None,
    'endTime': None,
    'commandStatus': 'Created',
    'errors': [],
    'command': {'type': 'RunScript',
    'params': [{'key': 'ScriptName', 'value': 'cmd.ps1'},
        {'key': 'Args', 'value': 'ipconfig /displaydns > c:\\dnscache.txt'}]}},
    {'index': 1,
    'startTime': None,
    'endTime': None,
    'commandStatus': 'Created',
    'errors': [],
    'command': {'type': 'GetFile',
    'params': [{'key': 'Path', 'value': 'c:\\dnscache.txt'}]}}],
    'troubleshootInfo': None}

Checking the status of the Live Response Machine Action

This one is pretty straightforward. Submit the ID you received from generating the machine action to the ol’ machineactions endpoint.

cmdStatus = MDErequest("machineactions/%s" % (cmdRequest['id']))
cmdStatus

Output:

{'@odata.context': 'https://api.securitycenter.windows.com/api/$metadata#MachineActions/$entity',
 'id': '944850f6-8043-4ca4-a65f-b50d0a1ea6c8',
 'type': 'LiveResponse',
 'title': None,
 'requestor': 'JonathanGlass@HalfFullofSecurity.onmicrosoft.com',
 'requestorComment': 'Collecting DNS cache',
 'status': 'Pending',
 'machineId': '796f7520617265207375706572206c616d652121',
 'computerDnsName': 'desktop-l3mnbj9',
 'creationDateTimeUtc': '2022-03-13T17:06:03.5759458Z',
 'lastUpdateDateTimeUtc': '2022-03-13T17:06:18.480311Z',
 'cancellationRequestor': None,
 'cancellationComment': None,
 'cancellationDateTimeUtc': None,
 'errorHResult': 0,
 'scope': None,
 'externalId': None,
 'requestSource': 'PublicApi',
 'relatedFileInfo': None,
 'commands': [{'index': 0,
   'startTime': None,
   'endTime': None,
   'commandStatus': 'Created',
   'errors': [],
   'command': {'type': 'RunScript',
    'params': [{'key': 'ScriptName', 'value': 'cmd.ps1'},
     {'key': 'Args', 'value': 'ipconfig /displaydns > c:\\dnscache.txt'}]}},
  {'index': 1,
   'startTime': None,
   'endTime': None,
   'commandStatus': 'Created',
   'errors': [],
   'command': {'type': 'GetFile',
    'params': [{'key': 'Path', 'value': 'c:\\dnscache.txt'}]}}],
 'troubleshootInfo': None}

The machine action status field contains the current status of the command. Possible values are: “Pending”, “InProgress”, “Succeeded”, “Failed”, “TimeOut”, and “Cancelled”.

cmdStatus = MDErequest("machineactions/%s" % (cmdRequest['id']))
cmdStatus

Output:

{'@odata.context': 'https://api.securitycenter.windows.com/api/$metadata#MachineActions/$entity',
 'id': '944850f6-8043-4ca4-a65f-b50d0a1ea6c8',
 'type': 'LiveResponse',
 'title': None,
 'requestor': 'JonathanGlass@HalfFullofSecurity.onmicrosoft.com',
 'requestorComment': 'Collecting DNS cache',
 'status': 'Succeeded',
 'machineId': '796f7520617265207375706572206c616d652121',
 'computerDnsName': 'desktop-l3mnbj9',
 'creationDateTimeUtc': '2022-03-13T17:06:03.5759458Z',
 'lastUpdateDateTimeUtc': '2022-03-13T17:08:51.982182Z',
 'cancellationRequestor': None,
 'cancellationComment': None,
 'cancellationDateTimeUtc': None,
 'errorHResult': 0,
 'scope': None,
 'externalId': None,
 'requestSource': 'PublicApi',
 'relatedFileInfo': None,
 'commands': [{'index': 0,
   'startTime': '2022-03-13T17:08:24.28Z',
   'endTime': '2022-03-13T17:08:28.003Z',
   'commandStatus': 'Completed',
   'errors': [],
   'command': {'type': 'RunScript',
    'params': [{'key': 'ScriptName', 'value': 'cmd.ps1'},
     {'key': 'Args', 'value': 'ipconfig /displaydns > c:\\dnscache.txt'}]}},
  {'index': 1,
   'startTime': '2022-03-13T17:08:44.583Z',
   'endTime': '2022-03-13T17:08:48.243Z',
   'commandStatus': 'Completed',
   'errors': [],
   'command': {'type': 'GetFile',
    'params': [{'key': 'Path', 'value': 'c:\\dnscache.txt'}]}}],
 'troubleshootInfo': None}

Retrieving the Contents of a GetFile Request

When we leverage the GetLiveResponseResultDownloadLink endpoint, we can get the results of the commands we executed. Each command has an index. Since the GetFile command is index 1, we have to specify that in the result request.

liveResponseResult = MDErequest("machineactions/%s/GetLiveResponseResultDownloadLink(index=1)" % (cmdStatus['id']))
liveResponseResult

Output:

{'@odata.context': 'https://api.securitycenter.windows.com/api/$metadata#Edm.String',
 'value': 'https://ssus1westprod7.blob.core.windows.net/67e/78/sha256/767e78ee71556652fbdfa602a93645265ea0d6de583f38e047dac26367b1a3f2.zip?sv=2015-12-11&sr=b&sig=C1TdPqqYBLHpyoRM9Il1UMCewLjW9AuINMZnXitq6F4%3D&spr=https&st=2022-03-13T17%3A05%3A20Z&se=2022-03-13T17%3A40%3A20Z&sp=r&rscd=attachment%3B%20filename%3D%22dnscache.txt.gz%22'}

The files collected by GetFile are returned in a compressed format. Below is an example of how to decompress one in memory.

Decompressing Files from GetFile Command

import gzip
from io import BytesIO
response = requests.get(liveResponseResult['value'])
compressedFile = BytesIO(response.content)
decompressedFile = gzip.GzipFile(fileobj=compressedFile)

Since we know this is a file is going to be a unicode text file, we can view the contents like this..

print(decompressedFile.read().decode('utf-16').encode('utf-8').decode('ascii'))

Output:

Windows IP Configuration

    automatedirstrprdeus3.blob.core.windows.net
    ----------------------------------------
    Record Name . . . . . : automatedirstrprdeus3.blob.core.windows.net
    Record Type . . . . . : 5
    Time To Live  . . . . : 57
    Data Length . . . . . : 4
    Section . . . . . . . : Answer
    CNAME Record  . . . . : blob.bnz10prdstr13a.store.core.windows.net


    Record Name . . . . . : blob.bnz10prdstr13a.store.core.windows.net
    Record Type . . . . . : 1
    Time To Live  . . . . : 57
    Data Length . . . . . : 4
    Section . . . . . . . : Answer
    A (Host) Record . . . : 20.60.132.4

SUCCESS!!! I will show in the next post how to just read the STDOUT of a command from the Transcript but there maybe scenarios where you need to pipe the output of a command to file instead of STDOUT. Off the top of my head… * Maybe your command takes longer that 10 minutes to run. * Maybe your output is too large for the buffer Live Response is using to pull back STDOUT.

I am sure there are other reasons to send to a file instead of just reading the STDOUT manually.

Limitations of the Machine Action API

  1. Rate limitations for this API are 10 calls per minute (additional requests are responded with HTTP 429).
  2. 25 concurrently running sessions (requests exceeding the throttling limit will receive a “429 - Too many requests” response).
  3. If the machine is not available, the session will be queued for up to 3 days.
  4. RunScript command timeouts after 10 minutes.
  5. Live response commands cannot be queued up and can only be executed one at a time.
  6. If the machine that you are trying to run this API call is in an RBAC device group that does not have an automated remediation level assigned to it, you’ll need to at least enable the minimum Remediation Level for a given Device Group.
  7. Multiple live response commands can be run on a single API call. However, when a live response command fails all the subsequent actions will not be executed.
  8. Parameters are a pain in the ass to encode across PowerShell and Batch it is probably best to stick with Powershell

References