Penetration Basics - Command Line Implementation for Reading Exchange Emails via Outlook Web Access (OWA)

Onedaysec
7 min read
6 views
docx image 1770017282917 0 fc4c44aae9

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.

1. Login Operation — technical illustration 1

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

(1) Read information of all emails in the folder — technical illustration 2

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

(3) Download and save attachments — technical illustration 3

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

(3) Download and save attachments — technical illustration 4

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

(3) Download and save attachments — technical illustration 5

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.

Related Questions & Answers

How can I read the full content of an email and download its attachments via the OWA command-line interface?

Use the `GetConversationItems` action by sending a POST to `/owa/service.svc?action=GetConversationItems` with the `ConversationId` obtained from the listing step. The JSON response includes the email body, metadata, and attachment details (Id and ContentType). For downloading attachments, you will need to make subsequent requests (not detailed in this snippet) using the attachment IDs. This command‑line implementation is analogous to other remote execution techniques described in [Domain Penetration - Remote Execution via Scheduled Tasks in GPO](/news/domain-penetration-remote-execution-via-scheduled-tasks-in-gpo-command-line-implementation-principles-and-script-details) and [Penetration Basics - Implementation of Webshell Supporting NTLM Over HTTP Protocol](/news/penetration-basics-implementation-of-webshell-supporting-ntlm-over-http-protocol), all of which rely on crafting HTTP requests with proper session management.

How do I list all emails in an OWA folder and obtain their ConversationId?

After authentication, send a POST request to `/owa/service.svc?action=FindItem` with a JSON body that specifies the folder name (e.g., `inbox` or `sentitems`) and sets `MaxEntriesReturned` to a high value like 999999. The response JSON contains an array of email items, each including a `ConversationId` – a unique identifier needed to later fetch the full content of that email. This method is similar in concept to reading emails via the IMAP protocol, but tailored to OWA’s proprietary API as explained in [Penetration Basics - Reading Emails Using the IMAP Protocol](/news/penetration-basics-reading-emails-using-the-imap-protocol).

What authentication method does OWA use, and why is it important for penetration testing?

OWA only supports plaintext password login; NTLM hash authentication is not accepted for the web interface. This means attackers who capture a user’s password (e.g., through phishing or keylogging) can directly access OWA, whereas hash‑based attacks like pass‑the‑hash require alternate protocols such as those used in [Domain Penetration - Remote Execution via Scheduled Tasks in GPO](/news/domain-penetration-remote-execution-via-scheduled-tasks-in-gpo-command-line-implementation-principles-and-script-details). The login process sends a POST to `/owa/auth.owa` and returns the critical `X-OWA-CANARY` token in the response cookie.

How can I read Exchange emails via OWA from the command line for penetration testing?

You can achieve this by writing a Python script that mimics the OWA web interface through HTTP requests. The process involves authenticating with a plaintext password, using the returned session cookie (including X-OWA-CANARY) to send JSON payloads to specific OWA endpoints like `FindItem` and `GetConversationItems`. This approach is detailed in the article [Penetration Basics - Command Line Implementation for Reading Exchange Emails via Outlook Web Access (OWA)](/news/penetration-basics-command-line-implementation-for-reading-exchange-emails-via-outlook-web-access-owa), which also provides open‑source code on GitHub.

Continue Reading