6

I'm receiving a multipart content response that belongs to an OAuth batch request:

// batchRequest is a HttpRequestMessage, http is an HttpClient
HttpResponseMessage response = await http.SendAsync(batchRequest);

If I read its content as full text:

string fullResponse = await response.Content.ReadAsStringAsync();

This is what it contains:

--batchresponse_e42a30ca-0f3a-4c17-8672-22abc469cd16
Content-Type: application/http
Content-Transfer-Encoding: binary

HTTP/1.1 200 OK
DataServiceVersion: 3.0;
Content-Type: application/json;odata=minimalmetadata;streaming=true;charset=utf-8

{\"odata.metadata\":\"https://graph.windows.net/XXX.onmicrosoft.com/$metadata#directoryObjects/@Element\",\"odata.type\":\"Microsoft.DirectoryServices.User\",\"objectType\":\"User\",\"objectId\":\"5f6851c3-99cc-4a89-936d-4bb44fa78a34\",\"deletionTimestamp\":null,\"accountEnabled\":true,\"signInNames\":[],\"assignedLicenses\":[],\"assignedPlans\":[],\"city\":null,\"companyName\":null,\"country\":null,\"creationType\":null,\"department\":\"NRF\",\"dirSyncEnabled\":null,\"displayName\":\"dummy1 Test\",\"facsimileTelephoneNumber\":null,\"givenName\":\"dummy1\",\"immutableId\":null,\"isCompromised\":null,\"jobTitle\":\"test\",\"lastDirSyncTime\":null,\"mail\":null,\"mailNickname\":\"dummy1test\",\"mobile\":null,\"onPremisesSecurityIdentifier\":null,\"otherMails\":[],\"passwordPolicies\":null,\"passwordProfile\":{\"password\":null,\"forceChangePasswordNextLogin\":true,\"enforceChangePasswordPolicy\":false},\"physicalDeliveryOfficeName\":null,\"postalCode\":null,\"preferredLanguage\":null,\"provisionedPlans\":[],\"provisioningErrors\":[],\"proxyAddresses\":[],\"refreshTokensValidFromDateTime\":\"2016-12-02T08:37:24Z\",\"showInAddressList\":null,\"sipProxyAddress\":null,\"state\":\"California\",\"streetAddress\":null,\"surname\":\"Test\",\"telephoneNumber\":\"666\",\"thumbnailPhoto@odata.mediaEditLink\":\"directoryObjects/5f6851c3-99cc-4a89-936d-4bb44fa78a34/Microsoft.DirectoryServices.User/thumbnailPhoto\",\"usageLocation\":null,\"userPrincipalName\":\"dummy1test@XXX.onmicrosoft.com\",\"userType\":\"Member\"}
--batchresponse_e42a30ca-0f3a-4c17-8672-22abc469cd16
Content-Type: application/http
Content-Transfer-Encoding: binary

HTTP/1.1 200 OK
DataServiceVersion: 3.0;
Content-Type: application/json;odata=minimalmetadata;streaming=true;charset=utf-8

{\"odata.metadata\":\"https://graph.windows.net/XXX.onmicrosoft.com/$metadata#directoryObjects/@Element\",\"odata.type\":\"Microsoft.DirectoryServices.User\",\"objectType\":\"User\",\"objectId\":\"dd35d761-e6ed-44e7-919f-f3b1e54eb7be\",\"deletionTimestamp\":null,\"accountEnabled\":true,\"signInNames\":[],\"assignedLicenses\":[],\"assignedPlans\":[],\"city\":null,\"companyName\":null,\"country\":null,\"creationType\":null,\"department\":null,\"dirSyncEnabled\":null,\"displayName\":\"Max Admin\",\"facsimileTelephoneNumber\":null,\"givenName\":null,\"immutableId\":null,\"isCompromised\":null,\"jobTitle\":null,\"lastDirSyncTime\":null,\"mail\":null,\"mailNickname\":\"maxadmin\",\"mobile\":null,\"onPremisesSecurityIdentifier\":null,\"otherMails\":[],\"passwordPolicies\":null,\"passwordProfile\":null,\"physicalDeliveryOfficeName\":null,\"postalCode\":null,\"preferredLanguage\":null,\"provisionedPlans\":[],\"provisioningErrors\":[],\"proxyAddresses\":[],\"refreshTokensValidFromDateTime\":\"2016-12-05T15:11:51Z\",\"showInAddressList\":null,\"sipProxyAddress\":null,\"state\":null,\"streetAddress\":null,\"surname\":null,\"telephoneNumber\":null,\"thumbnailPhoto@odata.mediaEditLink\":\"directoryObjects/dd35d761-e6ed-44e7-919f-f3b1e54eb7be/Microsoft.DirectoryServices.User/thumbnailPhoto\",\"usageLocation\":null,\"userPrincipalName\":\"maxadmin@XXX.onmicrosoft.com\",\"userType\":\"Member\"}
--batchresponse_e42a30ca-0f3a-4c17-8672-22abc469cd16--

I need to get all these contents as objects (like classics HttpResponseMessage, not simple strings), in order to get the HTTP return code, the JSON content, etc. as properties and be able to treat them.

I know how to read separatly all these contents, but I can't figure how to get them as objects, I've only succeeded in getting a string content :

var multipartContent = await response.Content.ReadAsMultipartAsync();
foreach (HttpContent currentContent in multipartContent.Contents) {
     var testString = currentContent.ReadAsStringAsync();
     // How to get this content as an exploitable object?
}

In my example, testString contains:

HTTP/1.1 200 OK
DataServiceVersion: 3.0;
Content-Type: application/json;odata=minimalmetadata;streaming=true;charset=utf-8

{\"odata.metadata\":\"https://graph.windows.net/XXX.onmicrosoft.com/$metadata#directoryObjects/@Element\",\"odata.type\":\"Microsoft.DirectoryServices.User\",\"objectType\":\"User\",\"objectId\":\"5f6851c3-99cc-4a89-936d-4bb44fa78a34\",\"deletionTimestamp\":null,\"accountEnabled\":true,\"signInNames\":[],\"assignedLicenses\":[],\"assignedPlans\":[],\"city\":null,\"companyName\":null,\"country\":null,\"creationType\":null,\"department\":\"NRF\",\"dirSyncEnabled\":null,\"displayName\":\"dummy1 Test\",\"facsimileTelephoneNumber\":null,\"givenName\":\"dummy1\",\"immutableId\":null,\"isCompromised\":null,\"jobTitle\":\"test\",\"lastDirSyncTime\":null,\"mail\":null,\"mailNickname\":\"dummy1test\",\"mobile\":null,\"onPremisesSecurityIdentifier\":null,\"otherMails\":[],\"passwordPolicies\":null,\"passwordProfile\":{\"password\":null,\"forceChangePasswordNextLogin\":true,\"enforceChangePasswordPolicy\":false},\"physicalDeliveryOfficeName\":null,\"postalCode\":null,\"preferredLanguage\":null,\"provisionedPlans\":[],\"provisioningErrors\":[],\"proxyAddresses\":[],\"refreshTokensValidFromDateTime\":\"2016-12-02T08:37:24Z\",\"showInAddressList\":null,\"sipProxyAddress\":null,\"state\":\"California\",\"streetAddress\":null,\"surname\":\"Test\",\"telephoneNumber\":\"666\",\"thumbnailPhoto@odata.mediaEditLink\":\"directoryObjects/5f6851c3-99cc-4a89-936d-4bb44fa78a34/Microsoft.DirectoryServices.User/thumbnailPhoto\",\"usageLocation\":null,\"userPrincipalName\":\"dummy1test@XXX.onmicrosoft.com\",\"userType\":\"Member\"}

I can't just imagine to parse manually this string... So if someone has a clue or can explain me the good way to read the content, it would be nice.

Thanks, Max

Max Xapi
  • 750
  • 8
  • 22

1 Answers1

7

Here is a way it can be done. The key is to add a new content type "msgtype" header to the response:

var multipartContent = await response.Content.ReadAsMultipartAsync();
var multipartRespMsgs = new List<HttpResponseMessage>();
foreach (HttpContent currentContent in multipartContent.Contents) {
    // Two cases:
    // 1. a "single" response
    if (currentContent.Headers.ContentType.MediaType.Equals("application/http", StringComparison.OrdinalIgnoreCase)) {
        if (!currentContent.Headers.ContentType.Parameters.Any(parameter => parameter.Name.Equals("msgtype", StringComparison.OrdinalIgnoreCase) && parameter.Value.Equals("response", StringComparison.OrdinalIgnoreCase))) {
            currentContent.Headers.ContentType.Parameters.Add(new NameValueHeaderValue("msgtype", "response"));
        }
        multipartRespMsgs.Add(await currentContent.ReadAsHttpResponseMessageAsync());
        // The single object in multipartRespMsgs contains a classic exploitable HttpResponseMessage (with IsSuccessStatusCode, Content.ReadAsStringAsync().Result, etc.)
    }
    // 2. a changeset response, which is an embedded multipart content
    else {
        var subMultipartContent = await currentContent.ReadAsMultipartAsync();
        foreach (HttpContent currentSubContent in subMultipartContent.Contents) {
            currentSubContent.Headers.ContentType.Parameters.Add(new NameValueHeaderValue("msgtype", "response"));
            multipartRespMsgs.Add(await currentSubContent.ReadAsHttpResponseMessageAsync());
            // Same here, the objects in multipartRespMsgs contain classic exploitable HttpResponseMessages
        }
    }
}

Thanks to darl0026

Jpsy
  • 20,077
  • 7
  • 118
  • 115
Max Xapi
  • 750
  • 8
  • 22
  • Adding the "msgtype" parameter did the trick for me and saved my day. THX! – Jpsy May 17 '19 at 06:49
  • 1
    If I see this right there is a small error in your code: case 1. is only triggered if the msgtype parameter does not exist. If Headers.ContentType already contains "application/http; msgtype=response" then your run into case 2, which is wrong. Instead you should check only for "application/http" to identify case 1, and then use a second inner if/then to decide whether you have to add the "msgtype=response" or not. – Jpsy May 17 '19 at 06:57
  • 1
    @Jpsy you're right, thanks! Because this code has just been used for a PoC (followed by... nothing), it has obviously not been tested enough – Max Xapi May 22 '19 at 09:58
  • 1
    @ MaxXapi: I took the liberty to correct your code. I added an inner if clause that adds the `msgtype=response` only if it is missing. I also changed the variable to store incoming HttpResponseMessages to a List type, so it can hold multiple responses in the 2. case. This code is tested in production. (Hope I did not introduce typos when adapting it to your variable names. :D ) – Jpsy May 24 '19 at 08:03
  • 1
    @Jpsy Glad you've corrected it! I did not work on this subject since I've posted here, it wasn't fresh in my head so I did not have enough time to get back in this subject and correct the mistakes and do some improvements. Correct/better code will be more helpful, thanks! – Max Xapi May 24 '19 at 08:18