0x00 Preface

---

Outlook Web Access, abbreviated as OWA, is the web interface for Exchange to send and receive emails, enabled by default for all mailbox users.

Typically, we use a browser to access OWA and read emails. However, from a penetration testing perspective, we need to achieve the same functionality via the command line.

Currently, I haven't seen suitable open-source code or reference materials, so I plan to write Python code based on my own understanding to implement the functions of reading emails and downloading attachments.

0x01 Introduction

---

This article will cover the following topics:

  • Implementation Approach
  • Implementation Details
  • Issues to Note When Writing the Program
  • Open-Source Code
  • Usage Process

0x02 Implementation Approach

---

I haven't found any documentation introducing the OWA protocol format yet, so I can only implement it through packet capturing.

Here I use the built-in packet capturing tool in the Chrome browser. Press F12 in the Chrome interface and select Network.

0x03 Implementation Details

---

1. Login Operation

The accessed URL is https:///owa/auth.owa

A POST request needs to be sent with the data format:

destination=https:///owa&flags=4&forcedownlevel=0&username=&password=&passwordText=&isUtf8=1

After successful login, the Cookie includes X-OWA-CANARY, which can be used as a judgment basis.

The actual login process sent three data packets in total, as shown in the figure below.

Alt text

In program implementation, using Python's requests library does not require considering this detail.

The complete implementation code has been uploaded to GitHub, with the address as follows:

An open-source project

The code implements password verification

Note that OWA only supports plaintext password login, hash cannot be used

2. Access resources

Packet capture reveals that basically every operation follows this format:

  • Send POST packet
  • Set X-OWA-CANARY and Action in the Header
  • X-OWA-CANARY can be obtained from the Cookie returned after successful login
  • Cookie needs to be set
  • POST packet data format is JSON
  • Return result is also in JSON format

To read email content and download attachments, we need to implement the following operations programmatically:

(1) Read information of all emails in the folder

The accessed URL is https:///owa/service.svc?action=FindItem

The corresponding Action is FindItem

POST packet data format:

{"__type":"FindItemJsonRequest:#Exchange","Header":{"__type":"JsonRequestHeaders:#Exchange","RequestServerVersion":"Exchange2013","TimeZoneContext":{"__type":"TimeZoneContext:#Exchange","TimeZoneDefinition":{"__type":"TimeZoneDefinitionType:#Exchange","Id":"SA Pacific Standard Time"}}},"Body":{"__type":"FindItemRequest:#Exchange","ItemShape":{"__type":"ItemResponseShape:#Exchange","BaseShape":"IdOnly"},"ParentFolderIds":[{"__type":"DistinguishedFolderId:#Exchange","Id":""}],"Traversal":"Shallow","Paging":{"__type":"IndexedPageView:#Exchange","BasePoint":"Beginning","Offset":0,"MaxEntriesReturned":999999},"ViewFilter":"All","ClutterFilter":"All","IsWarmUpSearch":0,"ShapeName":"MailListItem","SortOrder":[{"__type":"SortResults:#Exchange","Order":"Descending","Path":{"__type":"PropertyUri:#Exchange","FieldURI":"DateTimeReceived"}}]}}

The needs to be replaced with a specific folder name, such as inbox or sentitems, and MaxEntriesReturned can be set to 999999

As shown in the figure below

Alt text

The POST request returns results in JSON format, including brief information for each email in the folder (such as subject, sender, send time, read status, and whether it contains attachments, but not the body content), which is essentially the same as the results returned by the EWS GetFolder operation

Here, the ConversationId corresponding to each email needs to be extracted for use as a parameter to read the email content

In terms of program implementation, we need to use the session object from requests to maintain the session state

The specific implementation code is as follows:

def ListFolder(url, username, password, folder, mode):
session = requests.session()
url1 = 'https://'+ url + '/owa/auth.owa'
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.129 Safari/537.36"
}
payload = 'destination=https://%s/owa&flags=4&forcedownlevel=0&username=%s&password=%s&passwordText=&isUtf8=1'%(url, username, password)
r = session.post(url1, headers=headers, data=payload, verify = False)
print("[*] Try to login")
if 'X-OWA-CANARY' in r.cookies:
print("[+] Valid:%s %s"%(username, password))
else:
print("[!] Login error")
return 0
print("[*] Try to ListFolder")
url2 = 'https://'+ url + '/owa/service.svc?action=FindItem'
headers = {
'X-OWA-CANARY': r.cookies['X-OWA-CANARY'],
'Action': 'FindItem',
"User-Agent": "Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.129 Safari/537.36"
}
body = {"__type":"FindItemJsonRequest:#Exchange","Header":{"__type":"JsonRequestHeaders:#Exchange","RequestServerVersion":"Exchange2013","TimeZoneContext":{"__type":"TimeZoneContext:#Exchange","TimeZoneDefinition":{"__type":"TimeZoneDefinitionType:#Exchange","Id":"SA Pacific Standard Time"}}},"Body":{"__type":"FindItemRequest:#Exchange","ItemShape":{"__type":"ItemResponseShape:#Exchange","BaseShape":"IdOnly"},"ParentFolderIds":[{"__type":"DistinguishedFolderId:#Exchange","Id":""}],"Traversal":"Shallow","Paging":{"__type":"IndexedPageView:#Exchange","BasePoint":"Beginning","Offset":0,"MaxEntriesReturned":999999},"ViewFilter":"All","ClutterFilter":"All","IsWarmUpSearch":0,"ShapeName":"MailListItem","SortOrder":[{"__type":"SortResults:#Exchange","Order":"Descending","Path":{"__type":"PropertyUri:#Exchange","FieldURI":"DateTimeReceived"}}]}}
body['Body']['ParentFolderIds'][0]['Id'] = folder
r = session.post(url2, headers=headers, json = body, verify = False)
for item in json.loads(r.text)['Body']['ResponseMessages']['Items'][0]['RootFolder']['Items']:
print('ConversationId:' + item['ConversationId']['Id'])

The code will parse the returned JSON format and extract the ConversationId of each email.

(2) Read the content of a specified email

The accessed URL is https:///owa/service.svc?action=GetConversationItems

The corresponding Action is GetConversationItems

Data format of the POST packet:

{"__type":"GetConversationItemsJsonRequest:#Exchange","Header":{"__type":"JsonRequestHeaders:#Exchange","RequestServerVersion":"Exchange2013","TimeZoneContext":{"__type":"TimeZoneContext:#Exchange","TimeZoneDefinition":{"__type":"TimeZoneDefinitionType:#Exchange","Id":"SA Pacific Standard Time"}}},"Body":{"__type":"GetConversationItemsRequest:#Exchange","Conversations":[{"__type":"ConversationRequestType:#Exchange","ConversationId":{"__type":"ItemId:#Exchange","Id":""},"SyncState":""}],"ItemShape":{"__type":"ItemResponseShape:#Exchange","BaseShape":"IdOnly","FilterHtmlContent":1,"BlockExternalImagesIfSenderUntrusted":1,"AddBlankTargetToLinks":1,"ClientSupportsIrm":1,"InlineImageUrlTemplate":"data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAEALAAAAAABAAEAAAIBTAA7","MaximumBodySize":2097152,"InlineImageUrlOnLoadTemplate":"InlineImageLoader.GetLoader().Load(this)","InlineImageCustomDataTemplate":""},"ShapeName":"ItemPartUniqueBody","SortOrder":"DateOrderDescending","MaxItemsToReturn":20}}

Where needs to be modified to the corresponding ConversationId of the email

Note here: The POST packet data format captured via the browser cannot be recognized by Python for false and true; false needs to be replaced with 0, and true with 1

The return result of the POST request is in JSON format, including the detailed content of the email

Here, the Id and ContentType corresponding to the email attachments need to be extracted for use as parameters in the attachment saving operation

The specific implementation code is as follows:

def ViewMail(url, username, password, ConversationId):
session = requests.session()
url1 = 'https://'+ url + '/owa/auth.owa'
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.129 Safari/537.36"
}
payload = 'destination=https://%s/owa&flags=4&forcedownlevel=0&username=%s&password=%s&passwordText=&isUtf8=1'%(url, username, password)
r = session.post(url1, headers=headers, data=payload, verify = False)
print("[*] Try to login")
if 'X-OWA-CANARY' in r.cookies:
print("[+] Valid:%s %s"%(username, password))
else:
print("[!] Login error")
return 0
print("[*] Try to ViewMail")
url2 = 'https://'+ url + '/owa/service.svc?action=GetConversationItems'
headers = {
'X-OWA-CANARY': r.cookies['X-OWA-CANARY'],
'Action': 'GetConversationItems',
"User-Agent": "Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.129 Safari/537.36"
}
body = {"__type":"GetConversationItemsJsonRequest:#Exchange","Header":{"__type":"JsonRequestHeaders:#Exchange","RequestServerVersion":"Exchange2013","TimeZoneContext":{"__type":"TimeZoneContext:#Exchange","TimeZoneDefinition":{"__type":"TimeZoneDefinitionType:#Exchange","Id":"SA Pacific Standard Time"}}},"Body":{"__type":"GetConversationItemsRequest:#Exchange","Conversations":[{"__type":"ConversationRequestType:#Exchange","ConversationId":{"__type":"ItemId:#Exchange","Id":""},"SyncState":""}],"ItemShape":{"__type":"ItemResponseShape:#Exchange","BaseShape":"IdOnly","FilterHtmlContent":1,"BlockExternalImagesIfSenderUntrusted":1,"AddBlankTargetToLinks":1,"ClientSupportsIrm":1,"InlineImageUrlTemplate":"data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAEALAAAAAABAAEAAAIBTAA7","MaximumBodySize":2097152,"InlineImageUrlOnLoadTemplate":"InlineImageLoader.GetLoader().Load(this)","InlineImageCustomDataTemplate":"{id}"},"ShapeName":"ItemPartUniqueBody","SortOrder":"DateOrderDescending","MaxItemsToReturn":20}}
body['Body']['Conversations'][0]['ConversationId']['Id'] = ConversationId
r = session.post(url2, headers=headers, json = body, verify = False)
for item in json.loads(r.text)['Body']['ResponseMessages']['Items'][0]['Conversation']['ConversationNodes'][0]['Items']:
print('Subject:' + item['Subject'])
if 'From' in item:
print('From:' + item['From']['Mailbox']['Name'])
print('FromEmailAddress:' + item['From']['Mailbox']['EmailAddress'])
else:
print('From:' + 'Self')
for user in item['ToRecipients']:
print('ToRecipients:' + user['Name'])
print('ToRecipientsEmailAddress:' + user['EmailAddress'])
print('DisplayTo:' + item['DisplayTo'])
print('HasAttachments:' + str(item['HasAttachments']))
if item['HasAttachments'] == True:
for att in item['Attachments']:
print(' Name:' + att['Name'])
print(' ContentType:' + att['ContentType'])
print(' Id:' + att['AttachmentId']['Id'])
print('IsRead:' + str(item['IsRead']))
print('DateTimeReceived:' + item['DateTimeReceived'])
print('Body:\r\n' + item['UniqueBody']['Value'])
print('\r\n')
r.close()

The code will parse the JSON format of the returned result to extract the specific content of the email. If multiple attachments are included, it will output the Name, ContentType, and Id for each one.

(3) Download and save attachments

The accessed URL is https:///owa/service.svc/s/GetFileAttachment?id=&X-OWA-CANARY=

Here, needs to be replaced with the corresponding attachment Id, and is obtained from the Cookie returned after successful login.

A GET request is used here. The returned result's header includes the attachment's file name, and the webpage content of the returned result is the attachment's content.

When saving attachments, pay attention to the saved format, distinguishing between text files and binary files.

If it is a text file, you can save the content of r.text.

If it is a binary file, you can save the content of r.content.

The specific implementation code is as follows:

def DownloadAttachment(url, username, password, Id, mode):
session = requests.session()
url1 = 'https://'+ url + '/owa/auth.owa'
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.129 Safari/537.36"
}
payload = 'destination=https://%s/owa&flags=4&forcedownlevel=0&username=%s&password=%s&passwordText=&isUtf8=1'%(url, username, password)
r = session.post(url1, headers=headers, data=payload, verify = False)
print("[*] Try to login")
if 'X-OWA-CANARY' in r.cookies:
print("[+] Valid:%s %s"%(username, password))
else:
print("[!] Login error")
return 0
print("[*] Try to DownloadAttachment")
url2 = 'https://'+ url + '/owa/service.svc/s/GetFileAttachment?id=' + Id + '&X-OWA-CANARY=' + r.cookies['X-OWA-CANARY']
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.129 Safari/537.36"
}
r = session.get(url2, headers=headers, verify = False)
pattern_name = re.compile(r"\"(.*?)\"")
name = pattern_name.findall(r.headers['Content-Disposition'])
print('[+] Attachment name: %s'%(name[0]))
if mode == 'text':
with open(name[0], 'w+', encoding='utf-8') as file_object:
file_object.write(r.text)
elif mode == 'raw':
with open(name[0], 'wb+') as file_object:
file_object.write(r.content)
r.close()

The complete implementation code has been uploaded to GitHub at the following address:

An open-source project

Usage example:

(1) View emails in the Sent Items folder

python owaManage.py 192.168.1.1 test1 DomainUser123! ListFolder

Specified folder: sentitems

Specified output result type: full

As shown in the figure below

Alt text

Result returns the total number of emails and information for each email. Here, the ConversationId corresponding to the email is obtained: AAQkADc4YjRlNDc1LWI0YjctNDEzZi1hNTQ5LWZkYWY0ZGZhZDM0NgAQAJdkOHS5cphDrNGlVbVpnIo=

(2) Read email content

python owaManage.py 192.168.1.1 test1 DomainUser123! ViewMail

Specify the ConversationId corresponding to the email

As shown in the figure below

Alt text

Result returns the specific content of the email. Here, the attachment 111.txt is obtained with the type text/plain and the corresponding Id: AAMkADc4YjRlNDc1LWI0YjctNDEzZi1hNTQ5LWZkYWY0ZGZhZDM0NgBGAAAAAABEBlGH6URWQp6Nlg9RxLmyBwA1ZCfAg9a0Sq75no2JOzsqAAAAAAEKAAA1ZCfAg9a0Sq75no2JOzsqAAAAAByNAAABEgAQAO2T/TJsdj9Emo9dwiMqlrM=

(3) Download attachment

python owaManage.py 192.168.1.1 test1 DomainUser123! DownloadAttachment

Specify the Id corresponding to the attachment

Specify the save format as text

As shown in the figure below

Alt text

0x04 Summary

---

This article introduces the implementation details of writing Python code to read Exchange emails through Outlook Web Access (OWA), documenting the development process.