[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"$f6yAns82ZsyClB34PTYn1pZOmf2NmKB_T2maA3nHaK4g":3},{"id":4,"question":5,"answer":6,"answerHtml":7,"slug":8,"keywords":9,"article":10,"status":35,"aiModel":40,"aiConfidence":40,"updatedAt":52,"createdAt":52,"_status":51},390,"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)](\u002Fnews\u002Fpenetration-basics-command-line-implementation-for-reading-exchange-emails-via-outlook-web-access-owa), which also provides open‑source code on GitHub.","\u003Cp>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)](\u002Fnews\u002Fpenetration-basics-command-line-implementation-for-reading-exchange-emails-via-outlook-web-access-owa), which also provides open‑source code on GitHub.\u003C\u002Fp>\u003Cp>\u003Ca href=\"\u002Fnews\u002Fpenetration-basics-command-line-implementation-for-reading-exchange-emails-via-outlook-web-access-owa\">Read the related One Day Sec article\u003C\u002Fa>\u003C\u002Fp>","how-can-i-read-exchange-emails-via-owa-from-the-command-line-for-penetration-tes-1777483763927","OWA, Exchange, command-line, Python, penetration testing, authentication, JSON, FindItem, GetConversationItems",{"id":11,"title":12,"slug":13,"description":14,"content":15,"contentHtml":30,"cover":31,"author":32,"views":33,"readingTime":34,"status":35,"publishedAt":36,"seo":37,"tags":42,"qaPairs":43,"meta":48,"updatedAt":49,"createdAt":50,"_status":51},98,"Penetration Basics - Command Line Implementation for Reading Exchange Emails via Outlook Web Access (OWA)","penetration-basics-command-line-implementation-for-reading-exchange-emails-via-outlook-web-access-owa","Learn how to implement command-line email reading via OWA for Exchange penetration testing, including login, email retrieval, and attachment download using Python.",{"root":16},{"type":17,"format":18,"indent":19,"version":20,"children":21,"direction":29},"root","",0,1,[22],{"type":23,"format":18,"indent":19,"version":20,"children":24,"direction":29},"paragraph",[25],{"mode":26,"text":27,"type":28,"style":18,"detail":19,"format":19,"version":20},"normal","\u003Chtml>\u003Chead>\u003C\u002Fhead>\u003Cbody>\u003Ch2>0x00 Preface\u003C\u002Fh2>\u003Cp>---\u003C\u002Fp>\u003Cp>Outlook Web Access, abbreviated as OWA, is the web interface for Exchange to send and receive emails, enabled by default for all mailbox users.\u003C\u002Fp>\u003Cp>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.\u003C\u002Fp>\u003Cp>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.\u003C\u002Fp>\u003Ch2>0x01 Introduction\u003C\u002Fh2>\u003Cp>---\u003C\u002Fp>\u003Cp>This article will cover the following topics:\u003C\u002Fp>\u003Cul>\u003Cli>Implementation Approach\u003C\u002Fli>\u003Cli>Implementation Details\u003C\u002Fli>\u003Cli>Issues to Note When Writing the Program\u003C\u002Fli>\u003Cli>Open-Source Code\u003C\u002Fli>\u003Cli>Usage Process\u003C\u002Fli>\u003C\u002Ful>\u003Ch2>0x02 Implementation Approach\u003C\u002Fh2>\u003Cp>---\u003C\u002Fp>\u003Cp>I haven't found any documentation introducing the OWA protocol format yet, so I can only implement it through packet capturing.\u003C\u002Fp>\u003Cp>Here I use the built-in packet capturing tool in the Chrome browser. Press F12 in the Chrome interface and select Network.\u003C\u002Fp>\u003Ch2>0x03 Implementation Details\u003C\u002Fh2>\u003Cp>---\u003C\u002Fp>\u003Ch3>1. Login Operation\u003C\u002Fh3>\u003Cp>The accessed URL is https:\u002F\u002F\u003Cdomain>\u002Fowa\u002Fauth.owa\u003C\u002Fdomain>\u003C\u002Fp>\u003Cp>A POST request needs to be sent with the data format:\u003C\u002Fp>\u003Ctable>\u003Ctbody>\u003Ctr>\u003Ctd>\u003Cp>destination=https:\u002F\u002F\u003Cdomain>\u002Fowa&amp;flags=4&amp;forcedownlevel=0&amp;username=\u003Cusername>&amp;password=\u003Cpassword>&amp;passwordText=&amp;isUtf8=1\u003C\u002Fpassword>\u003C\u002Fusername>\u003C\u002Fdomain>\u003C\u002Fp>\u003C\u002Ftd>\u003C\u002Ftr>\u003C\u002Ftbody>\u003C\u002Ftable>\u003Cp>After successful login, the Cookie includes X-OWA-CANARY, which can be used as a judgment basis.\u003C\u002Fp>\u003Cp>The actual login process sent three data packets in total, as shown in the figure below.\u003C\u002Fp>\u003Cp>\u003Cimg alt=\"Alt text\" src=\"\u002Fuploads\u002Fdocx_image_1770017282917_0_fc4c44aae9.jpeg\">\u003C\u002Fp>\u003Cp>In program implementation, using Python's requests library does not require considering this detail.\u003C\u002Fp>\u003Cp>The complete implementation code has been uploaded to GitHub, with the address as follows:\u003C\u002Fp>\u003Cp>An open-source project\u003C\u002Fp>\u003Cp>The code implements password verification\u003C\u002Fp>\u003Cp>Note that OWA only supports plaintext password login, hash cannot be used\u003C\u002Fp>\u003Ch3>2. Access resources\u003C\u002Fh3>\u003Cp>Packet capture reveals that basically every operation follows this format:\u003C\u002Fp>\u003Cul>\u003Cli>Send POST packet\u003C\u002Fli>\u003Cli>Set X-OWA-CANARY and Action in the Header\u003C\u002Fli>\u003Cli>X-OWA-CANARY can be obtained from the Cookie returned after successful login\u003C\u002Fli>\u003Cli>Cookie needs to be set\u003C\u002Fli>\u003Cli>POST packet data format is JSON\u003C\u002Fli>\u003Cli>Return result is also in JSON format\u003C\u002Fli>\u003C\u002Ful>\u003Cp>To read email content and download attachments, we need to implement the following operations programmatically:\u003C\u002Fp>\u003Ch4>(1) Read information of all emails in the folder\u003C\u002Fh4>\u003Cp>The accessed URL is https:\u002F\u002F\u003Cdomain>\u002Fowa\u002Fservice.svc?action=FindItem\u003C\u002Fdomain>\u003C\u002Fp>\u003Cp>The corresponding Action is FindItem\u003C\u002Fp>\u003Cp>POST packet data format:\u003C\u002Fp>\u003Ctable>\u003Ctbody>\u003Ctr>\u003Ctd>\u003Cp>{\"__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\":\"\u003Cfolder>\"}],\"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\"}}]}}\u003C\u002Ffolder>\u003C\u002Fp>\u003C\u002Ftd>\u003C\u002Ftr>\u003C\u002Ftbody>\u003C\u002Ftable>\u003Cp>The \u003Cfolder> needs to be replaced with a specific folder name, such as inbox or sentitems, and MaxEntriesReturned can be set to 999999\u003C\u002Ffolder>\u003C\u002Fp>\u003Cp>As shown in the figure below\u003C\u002Fp>\u003Cp>\u003Cimg alt=\"Alt text\" src=\"\u002Fuploads\u002Fdocx_image_1770017309597_1_ac5b620ac7.jpeg\">\u003C\u002Fp>\u003Cp>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\u003C\u002Fp>\u003Cp>Here, the ConversationId corresponding to each email needs to be extracted for use as a parameter to read the email content\u003C\u002Fp>\u003Cp>In terms of program implementation, we need to use the session object from requests to maintain the session state\u003C\u002Fp>\u003Cp>The specific implementation code is as follows:\u003C\u002Fp>\u003Ctable>\u003Ctbody>\u003Ctr>\u003Ctd>\u003Cp>def ListFolder(url, username, password, folder, mode):\u003Cbr>    session = requests.session()\u003Cbr>    url1 = 'https:\u002F\u002F'+ url + '\u002Fowa\u002Fauth.owa'\u003Cbr>    headers = {\u003Cbr>        \"User-Agent\": \"Mozilla\u002F5.0 (Windows NT 6.3; Win64; x64) AppleWebKit\u002F537.36 (KHTML, like Gecko) Chrome\u002F81.0.4044.129 Safari\u002F537.36\"\u003Cbr>    } \u003Cbr>    payload = 'destination=https:\u002F\u002F%s\u002Fowa&amp;flags=4&amp;forcedownlevel=0&amp;username=%s&amp;password=%s&amp;passwordText=&amp;isUtf8=1'%(url, username, password)\u003Cbr>    r = session.post(url1, headers=headers, data=payload, verify = False)\u003Cbr>    print(\"[*] Try to login\")\u003Cbr>    if 'X-OWA-CANARY' in r.cookies:\u003Cbr>        print(\"[+] Valid:%s  %s\"%(username, password))\u003Cbr>    else:\u003Cbr>        print(\"[!] Login error\")\u003Cbr>        return 0\u003Cbr>    print(\"[*] Try to ListFolder\")\u003Cbr>    url2 = 'https:\u002F\u002F'+ url + '\u002Fowa\u002Fservice.svc?action=FindItem'\u003Cbr>    headers = {\u003Cbr>        'X-OWA-CANARY': r.cookies['X-OWA-CANARY'],\u003Cbr>        'Action': 'FindItem',\u003Cbr>        \"User-Agent\": \"Mozilla\u002F5.0 (Windows NT 6.3; Win64; x64) AppleWebKit\u002F537.36 (KHTML, like Gecko) Chrome\u002F81.0.4044.129 Safari\u002F537.36\"\u003Cbr>    }\u003Cbr>    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\"}}]}}\u003Cbr>    body['Body']['ParentFolderIds'][0]['Id'] = folder\u003Cbr>    r = session.post(url2, headers=headers, json = body, verify = False)\u003Cbr>    for item in json.loads(r.text)['Body']['ResponseMessages']['Items'][0]['RootFolder']['Items']:\u003Cbr>\t\tprint('ConversationId:' + item['ConversationId']['Id'])\u003C\u002Fp>\u003C\u002Ftd>\u003C\u002Ftr>\u003C\u002Ftbody>\u003C\u002Ftable>\u003Cp>The code will parse the returned JSON format and extract the ConversationId of each email.\u003C\u002Fp>\u003Ch4>(2) Read the content of a specified email\u003C\u002Fh4>\u003Cp>The accessed URL is https:\u002F\u002F\u003Cdomain>\u002Fowa\u002Fservice.svc?action=GetConversationItems\u003C\u002Fdomain>\u003C\u002Fp>\u003Cp>The corresponding Action is GetConversationItems\u003C\u002Fp>\u003Cp>Data format of the POST packet:\u003C\u002Fp>\u003Ctable>\u003Ctbody>\u003Ctr>\u003Ctd>\u003Cp>{\"__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\u002Fgif;base64,R0lGODlhAQABAIAAAAAAAP\u002F\u002F\u002FyH5BAEAAAEALAAAAAABAAEAAAIBTAA7\",\"MaximumBodySize\":2097152,\"InlineImageUrlOnLoadTemplate\":\"InlineImageLoader.GetLoader().Load(this)\",\"InlineImageCustomDataTemplate\":\"\u003Cid>\"},\"ShapeName\":\"ItemPartUniqueBody\",\"SortOrder\":\"DateOrderDescending\",\"MaxItemsToReturn\":20}}\u003C\u002Fid>\u003C\u002Fp>\u003C\u002Ftd>\u003C\u002Ftr>\u003C\u002Ftbody>\u003C\u002Ftable>\u003Cp>Where \u003Cid> needs to be modified to the corresponding ConversationId of the email\u003C\u002Fid>\u003C\u002Fp>\u003Cp>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\u003C\u002Fp>\u003Cp>The return result of the POST request is in JSON format, including the detailed content of the email\u003C\u002Fp>\u003Cp>Here, the Id and ContentType corresponding to the email attachments need to be extracted for use as parameters in the attachment saving operation\u003C\u002Fp>\u003Cp>The specific implementation code is as follows:\u003C\u002Fp>\u003Ctable>\u003Ctbody>\u003Ctr>\u003Ctd>\u003Cp>def ViewMail(url, username, password, ConversationId):\u003Cbr>    session = requests.session()\u003Cbr>    url1 = 'https:\u002F\u002F'+ url + '\u002Fowa\u002Fauth.owa'\u003Cbr>    headers = {\u003Cbr>        \"User-Agent\": \"Mozilla\u002F5.0 (Windows NT 6.3; Win64; x64) AppleWebKit\u002F537.36 (KHTML, like Gecko) Chrome\u002F81.0.4044.129 Safari\u002F537.36\"\u003Cbr>    } \u003Cbr>    payload = 'destination=https:\u002F\u002F%s\u002Fowa&amp;flags=4&amp;forcedownlevel=0&amp;username=%s&amp;password=%s&amp;passwordText=&amp;isUtf8=1'%(url, username, password)               \u003Cbr>    r = session.post(url1, headers=headers, data=payload, verify = False)\u003Cbr>    print(\"[*] Try to login\")\u003Cbr>    if 'X-OWA-CANARY' in r.cookies:\u003Cbr>        print(\"[+] Valid:%s  %s\"%(username, password))\u003Cbr>    else:\u003Cbr>        print(\"[!] Login error\")\u003Cbr>        return 0\u003Cbr>    print(\"[*] Try to ViewMail\")\u003Cbr>    url2 = 'https:\u002F\u002F'+ url + '\u002Fowa\u002Fservice.svc?action=GetConversationItems'\u003Cbr>    headers = {\u003Cbr>        'X-OWA-CANARY': r.cookies['X-OWA-CANARY'],\u003Cbr>        'Action': 'GetConversationItems',\u003Cbr>        \"User-Agent\": \"Mozilla\u002F5.0 (Windows NT 6.3; Win64; x64) AppleWebKit\u002F537.36 (KHTML, like Gecko) Chrome\u002F81.0.4044.129 Safari\u002F537.36\"\u003Cbr>    }\u003Cbr>    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\u002Fgif;base64,R0lGODlhAQABAIAAAAAAAP\u002F\u002F\u002FyH5BAEAAAEALAAAAAABAAEAAAIBTAA7\",\"MaximumBodySize\":2097152,\"InlineImageUrlOnLoadTemplate\":\"InlineImageLoader.GetLoader().Load(this)\",\"InlineImageCustomDataTemplate\":\"{id}\"},\"ShapeName\":\"ItemPartUniqueBody\",\"SortOrder\":\"DateOrderDescending\",\"MaxItemsToReturn\":20}}\u003Cbr>    body['Body']['Conversations'][0]['ConversationId']['Id'] = ConversationId\u003Cbr>    r = session.post(url2, headers=headers, json = body, verify = False)\u003Cbr>    for item in json.loads(r.text)['Body']['ResponseMessages']['Items'][0]['Conversation']['ConversationNodes'][0]['Items']:\u003Cbr>        print('Subject:' + item['Subject'])\u003Cbr>        if 'From' in item:\u003Cbr>            print('From:' + item['From']['Mailbox']['Name'])\u003Cbr>            print('FromEmailAddress:' + item['From']['Mailbox']['EmailAddress'])\u003Cbr>        else:\u003Cbr>            print('From:' + 'Self')\u003Cbr>        for user in item['ToRecipients']:\u003Cbr>            print('ToRecipients:' + user['Name'])\u003Cbr>            print('ToRecipientsEmailAddress:' + user['EmailAddress'])\u003Cbr>        print('DisplayTo:' + item['DisplayTo'])\u003Cbr>        print('HasAttachments:' + str(item['HasAttachments']))\u003Cbr>        if item['HasAttachments'] == True:\u003Cbr>            for att in item['Attachments']:\u003Cbr>                print('  Name:' + att['Name'])\u003Cbr>                print('  ContentType:' + att['ContentType'])\u003Cbr>                print('  Id:' + att['AttachmentId']['Id'])\u003Cbr>        print('IsRead:' + str(item['IsRead']))\u003Cbr>        print('DateTimeReceived:' + item['DateTimeReceived'])\u003Cbr>        print('Body:\\r\\n' + item['UniqueBody']['Value'])\u003Cbr>        print('\\r\\n')\u003Cbr>    r.close()\u003C\u002Fp>\u003C\u002Ftd>\u003C\u002Ftr>\u003C\u002Ftbody>\u003C\u002Ftable>\u003Cp>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.\u003C\u002Fp>\u003Ch4>(3) Download and save attachments\u003C\u002Fh4>\u003Cp>The accessed URL is https:\u002F\u002F\u003Cdomain>\u002Fowa\u002Fservice.svc\u002Fs\u002FGetFileAttachment?id=\u003Cid>&amp;X-OWA-CANARY=\u003Cx-owa-canary>\u003C\u002Fx-owa-canary>\u003C\u002Fid>\u003C\u002Fdomain>\u003C\u002Fp>\u003Cp>Here, \u003Cid> needs to be replaced with the corresponding attachment Id, and \u003Cx-owa-canary> is obtained from the Cookie returned after successful login.\u003C\u002Fx-owa-canary>\u003C\u002Fid>\u003C\u002Fp>\u003Cp>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.\u003C\u002Fp>\u003Cp>When saving attachments, pay attention to the saved format, distinguishing between text files and binary files.\u003C\u002Fp>\u003Cp>If it is a text file, you can save the content of r.text.\u003C\u002Fp>\u003Cp>If it is a binary file, you can save the content of r.content.\u003C\u002Fp>\u003Cp>The specific implementation code is as follows:\u003C\u002Fp>\u003Ctable>\u003Ctbody>\u003Ctr>\u003Ctd>\u003Cp>def DownloadAttachment(url, username, password, Id, mode):\u003Cbr>    session = requests.session()\u003Cbr>    url1 = 'https:\u002F\u002F'+ url + '\u002Fowa\u002Fauth.owa'\u003Cbr>    headers = {\u003Cbr>        \"User-Agent\": \"Mozilla\u002F5.0 (Windows NT 6.3; Win64; x64) AppleWebKit\u002F537.36 (KHTML, like Gecko) Chrome\u002F81.0.4044.129 Safari\u002F537.36\"\u003Cbr>    } \u003Cbr>    payload = 'destination=https:\u002F\u002F%s\u002Fowa&amp;flags=4&amp;forcedownlevel=0&amp;username=%s&amp;password=%s&amp;passwordText=&amp;isUtf8=1'%(url, username, password)\u003Cbr>    r = session.post(url1, headers=headers, data=payload, verify = False)\u003Cbr>    print(\"[*] Try to login\")\u003Cbr>    if 'X-OWA-CANARY' in r.cookies:\u003Cbr>        print(\"[+] Valid:%s  %s\"%(username, password))\u003Cbr>    else:\u003Cbr>        print(\"[!] Login error\")\u003Cbr>        return 0\u003Cbr>    print(\"[*] Try to DownloadAttachment\")\u003Cbr>    url2 = 'https:\u002F\u002F'+ url + '\u002Fowa\u002Fservice.svc\u002Fs\u002FGetFileAttachment?id=' + Id + '&amp;X-OWA-CANARY=' + r.cookies['X-OWA-CANARY']\u003Cbr>    headers = {\u003Cbr>        \"User-Agent\": \"Mozilla\u002F5.0 (Windows NT 6.3; Win64; x64) AppleWebKit\u002F537.36 (KHTML, like Gecko) Chrome\u002F81.0.4044.129 Safari\u002F537.36\"\u003Cbr>    }\u003Cbr>    r = session.get(url2, headers=headers, verify = False)\u003Cbr>    pattern_name = re.compile(r\"\\\"(.*?)\\\"\")\u003Cbr>    name = pattern_name.findall(r.headers['Content-Disposition'])\u003Cbr>    print('[+] Attachment name: %s'%(name[0]))\u003Cbr>    if mode == 'text':\u003Cbr>        with open(name[0], 'w+', encoding='utf-8') as file_object:\u003Cbr>            file_object.write(r.text)     \u003Cbr>    elif mode == 'raw':\u003Cbr>        with open(name[0], 'wb+') as file_object:\u003Cbr>            file_object.write(r.content) \u003Cbr>    r.close()       \u003C\u002Fp>\u003C\u002Ftd>\u003C\u002Ftr>\u003C\u002Ftbody>\u003C\u002Ftable>\u003Cp>The complete implementation code has been uploaded to GitHub at the following address:\u003C\u002Fp>\u003Cp>An open-source project\u003C\u002Fp>\u003Cp>Usage example:\u003C\u002Fp>\u003Cp>(1) View emails in the Sent Items folder\u003C\u002Fp>\u003Ctable>\u003Ctbody>\u003Ctr>\u003Ctd>\u003Cp>python owaManage.py 192.168.1.1 test1 DomainUser123! ListFolder\u003C\u002Fp>\u003C\u002Ftd>\u003C\u002Ftr>\u003C\u002Ftbody>\u003C\u002Ftable>\u003Cp>Specified folder: sentitems\u003C\u002Fp>\u003Cp>Specified output result type: full\u003C\u002Fp>\u003Cp>As shown in the figure below\u003C\u002Fp>\u003Cp>\u003Cimg alt=\"Alt text\" src=\"\u002Fuploads\u002Fdocx_image_1770017337767_2_e6768f0acf.jpeg\">\u003C\u002Fp>\u003Cp>Result returns the total number of emails and information for each email. Here, the ConversationId corresponding to the email is obtained: AAQkADc4YjRlNDc1LWI0YjctNDEzZi1hNTQ5LWZkYWY0ZGZhZDM0NgAQAJdkOHS5cphDrNGlVbVpnIo=\u003C\u002Fp>\u003Cp>(2) Read email content\u003C\u002Fp>\u003Ctable>\u003Ctbody>\u003Ctr>\u003Ctd>\u003Cp>python owaManage.py 192.168.1.1 test1 DomainUser123! ViewMail\u003C\u002Fp>\u003C\u002Ftd>\u003C\u002Ftr>\u003C\u002Ftbody>\u003C\u002Ftable>\u003Cp>Specify the ConversationId corresponding to the email\u003C\u002Fp>\u003Cp>As shown in the figure below\u003C\u002Fp>\u003Cp>\u003Cimg alt=\"Alt text\" src=\"\u002Fuploads\u002Fdocx_image_1770017384040_3_2932f46532.jpeg\">\u003C\u002Fp>\u003Cp>Result returns the specific content of the email. Here, the attachment 111.txt is obtained with the type text\u002Fplain and the corresponding Id: AAMkADc4YjRlNDc1LWI0YjctNDEzZi1hNTQ5LWZkYWY0ZGZhZDM0NgBGAAAAAABEBlGH6URWQp6Nlg9RxLmyBwA1ZCfAg9a0Sq75no2JOzsqAAAAAAEKAAA1ZCfAg9a0Sq75no2JOzsqAAAAAByNAAABEgAQAO2T\u002FTJsdj9Emo9dwiMqlrM=\u003C\u002Fp>\u003Cp>(3) Download attachment\u003C\u002Fp>\u003Ctable>\u003Ctbody>\u003Ctr>\u003Ctd>\u003Cp>python owaManage.py 192.168.1.1 test1 DomainUser123! DownloadAttachment\u003C\u002Fp>\u003C\u002Ftd>\u003C\u002Ftr>\u003C\u002Ftbody>\u003C\u002Ftable>\u003Cp>Specify the Id corresponding to the attachment\u003C\u002Fp>\u003Cp>Specify the save format as text\u003C\u002Fp>\u003Cp>As shown in the figure below\u003C\u002Fp>\u003Cp>\u003Cimg alt=\"Alt text\" src=\"\u002Fuploads\u002Fdocx_image_1770017407753_4_e29544ddd4.jpeg\">\u003C\u002Fp>\u003Ch2>0x04 Summary\u003C\u002Fh2>\u003Cp>---\u003C\u002Fp>\u003Cp>This article introduces the implementation details of writing Python code to read Exchange emails through Outlook Web Access (OWA), documenting the development process.\u003C\u002Fp>\u003C\u002Fbody>\u003C\u002Fhtml>","text","ltr","\u003Chtml>\u003Chead>\u003C\u002Fhead>\u003Cbody>\u003Ch2>0x00 Preface\u003C\u002Fh2>\u003Cp>---\u003C\u002Fp>\u003Cp>Outlook Web Access, abbreviated as OWA, is the web interface for Exchange to send and receive emails, enabled by default for all mailbox users.\u003C\u002Fp>\u003Cp>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.\u003C\u002Fp>\u003Cp>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.\u003C\u002Fp>\u003Ch2>0x01 Introduction\u003C\u002Fh2>\u003Cp>---\u003C\u002Fp>\u003Cp>This article will cover the following topics:\u003C\u002Fp>\u003Cul>\u003Cli>Implementation Approach\u003C\u002Fli>\u003Cli>Implementation Details\u003C\u002Fli>\u003Cli>Issues to Note When Writing the Program\u003C\u002Fli>\u003Cli>Open-Source Code\u003C\u002Fli>\u003Cli>Usage Process\u003C\u002Fli>\u003C\u002Ful>\u003Ch2>0x02 Implementation Approach\u003C\u002Fh2>\u003Cp>---\u003C\u002Fp>\u003Cp>I haven't found any documentation introducing the OWA protocol format yet, so I can only implement it through packet capturing.\u003C\u002Fp>\u003Cp>Here I use the built-in packet capturing tool in the Chrome browser. Press F12 in the Chrome interface and select Network.\u003C\u002Fp>\u003Ch2>0x03 Implementation Details\u003C\u002Fh2>\u003Cp>---\u003C\u002Fp>\u003Ch3>1. Login Operation\u003C\u002Fh3>\u003Cp>The accessed URL is https:\u002F\u002F\u003Cdomain>\u002Fowa\u002Fauth.owa\u003C\u002Fdomain>\u003C\u002Fp>\u003Cp>A POST request needs to be sent with the data format:\u003C\u002Fp>\u003Ctable>\u003Ctbody>\u003Ctr>\u003Ctd>\u003Cp>destination=https:\u002F\u002F\u003Cdomain>\u002Fowa&amp;flags=4&amp;forcedownlevel=0&amp;username=\u003Cusername>&amp;password=\u003Cpassword>&amp;passwordText=&amp;isUtf8=1\u003C\u002Fpassword>\u003C\u002Fusername>\u003C\u002Fdomain>\u003C\u002Fp>\u003C\u002Ftd>\u003C\u002Ftr>\u003C\u002Ftbody>\u003C\u002Ftable>\u003Cp>After successful login, the Cookie includes X-OWA-CANARY, which can be used as a judgment basis.\u003C\u002Fp>\u003Cp>The actual login process sent three data packets in total, as shown in the figure below.\u003C\u002Fp>\u003Cp>\u003Cimg alt=\"Alt text\" src=\"\u002Fapi\u002Fmedia\u002Ffile\u002Fdocx_image_1770017282917_0_fc4c44aae9-1.jpeg\">\u003C\u002Fp>\u003Cp>In program implementation, using Python's requests library does not require considering this detail.\u003C\u002Fp>\u003Cp>The complete implementation code has been uploaded to GitHub, with the address as follows:\u003C\u002Fp>\u003Cp>An open-source project\u003C\u002Fp>\u003Cp>The code implements password verification\u003C\u002Fp>\u003Cp>Note that OWA only supports plaintext password login, hash cannot be used\u003C\u002Fp>\u003Ch3>2. Access resources\u003C\u002Fh3>\u003Cp>Packet capture reveals that basically every operation follows this format:\u003C\u002Fp>\u003Cul>\u003Cli>Send POST packet\u003C\u002Fli>\u003Cli>Set X-OWA-CANARY and Action in the Header\u003C\u002Fli>\u003Cli>X-OWA-CANARY can be obtained from the Cookie returned after successful login\u003C\u002Fli>\u003Cli>Cookie needs to be set\u003C\u002Fli>\u003Cli>POST packet data format is JSON\u003C\u002Fli>\u003Cli>Return result is also in JSON format\u003C\u002Fli>\u003C\u002Ful>\u003Cp>To read email content and download attachments, we need to implement the following operations programmatically:\u003C\u002Fp>\u003Ch4>(1) Read information of all emails in the folder\u003C\u002Fh4>\u003Cp>The accessed URL is https:\u002F\u002F\u003Cdomain>\u002Fowa\u002Fservice.svc?action=FindItem\u003C\u002Fdomain>\u003C\u002Fp>\u003Cp>The corresponding Action is FindItem\u003C\u002Fp>\u003Cp>POST packet data format:\u003C\u002Fp>\u003Ctable>\u003Ctbody>\u003Ctr>\u003Ctd>\u003Cp>{\"__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\":\"\u003Cfolder>\"}],\"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\"}}]}}\u003C\u002Ffolder>\u003C\u002Fp>\u003C\u002Ftd>\u003C\u002Ftr>\u003C\u002Ftbody>\u003C\u002Ftable>\u003Cp>The \u003Cfolder> needs to be replaced with a specific folder name, such as inbox or sentitems, and MaxEntriesReturned can be set to 999999\u003C\u002Ffolder>\u003C\u002Fp>\u003Cp>As shown in the figure below\u003C\u002Fp>\u003Cp>\u003Cimg alt=\"Alt text\" src=\"\u002Fapi\u002Fmedia\u002Ffile\u002Fdocx_image_1770017309597_1_ac5b620ac7-1.jpeg\">\u003C\u002Fp>\u003Cp>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\u003C\u002Fp>\u003Cp>Here, the ConversationId corresponding to each email needs to be extracted for use as a parameter to read the email content\u003C\u002Fp>\u003Cp>In terms of program implementation, we need to use the session object from requests to maintain the session state\u003C\u002Fp>\u003Cp>The specific implementation code is as follows:\u003C\u002Fp>\u003Ctable>\u003Ctbody>\u003Ctr>\u003Ctd>\u003Cp>def ListFolder(url, username, password, folder, mode):\u003Cbr>    session = requests.session()\u003Cbr>    url1 = 'https:\u002F\u002F'+ url + '\u002Fowa\u002Fauth.owa'\u003Cbr>    headers = {\u003Cbr>        \"User-Agent\": \"Mozilla\u002F5.0 (Windows NT 6.3; Win64; x64) AppleWebKit\u002F537.36 (KHTML, like Gecko) Chrome\u002F81.0.4044.129 Safari\u002F537.36\"\u003Cbr>    } \u003Cbr>    payload = 'destination=https:\u002F\u002F%s\u002Fowa&amp;flags=4&amp;forcedownlevel=0&amp;username=%s&amp;password=%s&amp;passwordText=&amp;isUtf8=1'%(url, username, password)\u003Cbr>    r = session.post(url1, headers=headers, data=payload, verify = False)\u003Cbr>    print(\"[*] Try to login\")\u003Cbr>    if 'X-OWA-CANARY' in r.cookies:\u003Cbr>        print(\"[+] Valid:%s  %s\"%(username, password))\u003Cbr>    else:\u003Cbr>        print(\"[!] Login error\")\u003Cbr>        return 0\u003Cbr>    print(\"[*] Try to ListFolder\")\u003Cbr>    url2 = 'https:\u002F\u002F'+ url + '\u002Fowa\u002Fservice.svc?action=FindItem'\u003Cbr>    headers = {\u003Cbr>        'X-OWA-CANARY': r.cookies['X-OWA-CANARY'],\u003Cbr>        'Action': 'FindItem',\u003Cbr>        \"User-Agent\": \"Mozilla\u002F5.0 (Windows NT 6.3; Win64; x64) AppleWebKit\u002F537.36 (KHTML, like Gecko) Chrome\u002F81.0.4044.129 Safari\u002F537.36\"\u003Cbr>    }\u003Cbr>    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\"}}]}}\u003Cbr>    body['Body']['ParentFolderIds'][0]['Id'] = folder\u003Cbr>    r = session.post(url2, headers=headers, json = body, verify = False)\u003Cbr>    for item in json.loads(r.text)['Body']['ResponseMessages']['Items'][0]['RootFolder']['Items']:\u003Cbr>\t\tprint('ConversationId:' + item['ConversationId']['Id'])\u003C\u002Fp>\u003C\u002Ftd>\u003C\u002Ftr>\u003C\u002Ftbody>\u003C\u002Ftable>\u003Cp>The code will parse the returned JSON format and extract the ConversationId of each email.\u003C\u002Fp>\u003Ch4>(2) Read the content of a specified email\u003C\u002Fh4>\u003Cp>The accessed URL is https:\u002F\u002F\u003Cdomain>\u002Fowa\u002Fservice.svc?action=GetConversationItems\u003C\u002Fdomain>\u003C\u002Fp>\u003Cp>The corresponding Action is GetConversationItems\u003C\u002Fp>\u003Cp>Data format of the POST packet:\u003C\u002Fp>\u003Ctable>\u003Ctbody>\u003Ctr>\u003Ctd>\u003Cp>{\"__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\u002Fgif;base64,R0lGODlhAQABAIAAAAAAAP\u002F\u002F\u002FyH5BAEAAAEALAAAAAABAAEAAAIBTAA7\",\"MaximumBodySize\":2097152,\"InlineImageUrlOnLoadTemplate\":\"InlineImageLoader.GetLoader().Load(this)\",\"InlineImageCustomDataTemplate\":\"\u003Cid>\"},\"ShapeName\":\"ItemPartUniqueBody\",\"SortOrder\":\"DateOrderDescending\",\"MaxItemsToReturn\":20}}\u003C\u002Fid>\u003C\u002Fp>\u003C\u002Ftd>\u003C\u002Ftr>\u003C\u002Ftbody>\u003C\u002Ftable>\u003Cp>Where \u003Cid> needs to be modified to the corresponding ConversationId of the email\u003C\u002Fid>\u003C\u002Fp>\u003Cp>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\u003C\u002Fp>\u003Cp>The return result of the POST request is in JSON format, including the detailed content of the email\u003C\u002Fp>\u003Cp>Here, the Id and ContentType corresponding to the email attachments need to be extracted for use as parameters in the attachment saving operation\u003C\u002Fp>\u003Cp>The specific implementation code is as follows:\u003C\u002Fp>\u003Ctable>\u003Ctbody>\u003Ctr>\u003Ctd>\u003Cp>def ViewMail(url, username, password, ConversationId):\u003Cbr>    session = requests.session()\u003Cbr>    url1 = 'https:\u002F\u002F'+ url + '\u002Fowa\u002Fauth.owa'\u003Cbr>    headers = {\u003Cbr>        \"User-Agent\": \"Mozilla\u002F5.0 (Windows NT 6.3; Win64; x64) AppleWebKit\u002F537.36 (KHTML, like Gecko) Chrome\u002F81.0.4044.129 Safari\u002F537.36\"\u003Cbr>    } \u003Cbr>    payload = 'destination=https:\u002F\u002F%s\u002Fowa&amp;flags=4&amp;forcedownlevel=0&amp;username=%s&amp;password=%s&amp;passwordText=&amp;isUtf8=1'%(url, username, password)               \u003Cbr>    r = session.post(url1, headers=headers, data=payload, verify = False)\u003Cbr>    print(\"[*] Try to login\")\u003Cbr>    if 'X-OWA-CANARY' in r.cookies:\u003Cbr>        print(\"[+] Valid:%s  %s\"%(username, password))\u003Cbr>    else:\u003Cbr>        print(\"[!] Login error\")\u003Cbr>        return 0\u003Cbr>    print(\"[*] Try to ViewMail\")\u003Cbr>    url2 = 'https:\u002F\u002F'+ url + '\u002Fowa\u002Fservice.svc?action=GetConversationItems'\u003Cbr>    headers = {\u003Cbr>        'X-OWA-CANARY': r.cookies['X-OWA-CANARY'],\u003Cbr>        'Action': 'GetConversationItems',\u003Cbr>        \"User-Agent\": \"Mozilla\u002F5.0 (Windows NT 6.3; Win64; x64) AppleWebKit\u002F537.36 (KHTML, like Gecko) Chrome\u002F81.0.4044.129 Safari\u002F537.36\"\u003Cbr>    }\u003Cbr>    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\u002Fgif;base64,R0lGODlhAQABAIAAAAAAAP\u002F\u002F\u002FyH5BAEAAAEALAAAAAABAAEAAAIBTAA7\",\"MaximumBodySize\":2097152,\"InlineImageUrlOnLoadTemplate\":\"InlineImageLoader.GetLoader().Load(this)\",\"InlineImageCustomDataTemplate\":\"{id}\"},\"ShapeName\":\"ItemPartUniqueBody\",\"SortOrder\":\"DateOrderDescending\",\"MaxItemsToReturn\":20}}\u003Cbr>    body['Body']['Conversations'][0]['ConversationId']['Id'] = ConversationId\u003Cbr>    r = session.post(url2, headers=headers, json = body, verify = False)\u003Cbr>    for item in json.loads(r.text)['Body']['ResponseMessages']['Items'][0]['Conversation']['ConversationNodes'][0]['Items']:\u003Cbr>        print('Subject:' + item['Subject'])\u003Cbr>        if 'From' in item:\u003Cbr>            print('From:' + item['From']['Mailbox']['Name'])\u003Cbr>            print('FromEmailAddress:' + item['From']['Mailbox']['EmailAddress'])\u003Cbr>        else:\u003Cbr>            print('From:' + 'Self')\u003Cbr>        for user in item['ToRecipients']:\u003Cbr>            print('ToRecipients:' + user['Name'])\u003Cbr>            print('ToRecipientsEmailAddress:' + user['EmailAddress'])\u003Cbr>        print('DisplayTo:' + item['DisplayTo'])\u003Cbr>        print('HasAttachments:' + str(item['HasAttachments']))\u003Cbr>        if item['HasAttachments'] == True:\u003Cbr>            for att in item['Attachments']:\u003Cbr>                print('  Name:' + att['Name'])\u003Cbr>                print('  ContentType:' + att['ContentType'])\u003Cbr>                print('  Id:' + att['AttachmentId']['Id'])\u003Cbr>        print('IsRead:' + str(item['IsRead']))\u003Cbr>        print('DateTimeReceived:' + item['DateTimeReceived'])\u003Cbr>        print('Body:\\r\\n' + item['UniqueBody']['Value'])\u003Cbr>        print('\\r\\n')\u003Cbr>    r.close()\u003C\u002Fp>\u003C\u002Ftd>\u003C\u002Ftr>\u003C\u002Ftbody>\u003C\u002Ftable>\u003Cp>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.\u003C\u002Fp>\u003Ch4>(3) Download and save attachments\u003C\u002Fh4>\u003Cp>The accessed URL is https:\u002F\u002F\u003Cdomain>\u002Fowa\u002Fservice.svc\u002Fs\u002FGetFileAttachment?id=\u003Cid>&amp;X-OWA-CANARY=\u003Cx-owa-canary>\u003C\u002Fx-owa-canary>\u003C\u002Fid>\u003C\u002Fdomain>\u003C\u002Fp>\u003Cp>Here, \u003Cid> needs to be replaced with the corresponding attachment Id, and \u003Cx-owa-canary> is obtained from the Cookie returned after successful login.\u003C\u002Fx-owa-canary>\u003C\u002Fid>\u003C\u002Fp>\u003Cp>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.\u003C\u002Fp>\u003Cp>When saving attachments, pay attention to the saved format, distinguishing between text files and binary files.\u003C\u002Fp>\u003Cp>If it is a text file, you can save the content of r.text.\u003C\u002Fp>\u003Cp>If it is a binary file, you can save the content of r.content.\u003C\u002Fp>\u003Cp>The specific implementation code is as follows:\u003C\u002Fp>\u003Ctable>\u003Ctbody>\u003Ctr>\u003Ctd>\u003Cp>def DownloadAttachment(url, username, password, Id, mode):\u003Cbr>    session = requests.session()\u003Cbr>    url1 = 'https:\u002F\u002F'+ url + '\u002Fowa\u002Fauth.owa'\u003Cbr>    headers = {\u003Cbr>        \"User-Agent\": \"Mozilla\u002F5.0 (Windows NT 6.3; Win64; x64) AppleWebKit\u002F537.36 (KHTML, like Gecko) Chrome\u002F81.0.4044.129 Safari\u002F537.36\"\u003Cbr>    } \u003Cbr>    payload = 'destination=https:\u002F\u002F%s\u002Fowa&amp;flags=4&amp;forcedownlevel=0&amp;username=%s&amp;password=%s&amp;passwordText=&amp;isUtf8=1'%(url, username, password)\u003Cbr>    r = session.post(url1, headers=headers, data=payload, verify = False)\u003Cbr>    print(\"[*] Try to login\")\u003Cbr>    if 'X-OWA-CANARY' in r.cookies:\u003Cbr>        print(\"[+] Valid:%s  %s\"%(username, password))\u003Cbr>    else:\u003Cbr>        print(\"[!] Login error\")\u003Cbr>        return 0\u003Cbr>    print(\"[*] Try to DownloadAttachment\")\u003Cbr>    url2 = 'https:\u002F\u002F'+ url + '\u002Fowa\u002Fservice.svc\u002Fs\u002FGetFileAttachment?id=' + Id + '&amp;X-OWA-CANARY=' + r.cookies['X-OWA-CANARY']\u003Cbr>    headers = {\u003Cbr>        \"User-Agent\": \"Mozilla\u002F5.0 (Windows NT 6.3; Win64; x64) AppleWebKit\u002F537.36 (KHTML, like Gecko) Chrome\u002F81.0.4044.129 Safari\u002F537.36\"\u003Cbr>    }\u003Cbr>    r = session.get(url2, headers=headers, verify = False)\u003Cbr>    pattern_name = re.compile(r\"\\\"(.*?)\\\"\")\u003Cbr>    name = pattern_name.findall(r.headers['Content-Disposition'])\u003Cbr>    print('[+] Attachment name: %s'%(name[0]))\u003Cbr>    if mode == 'text':\u003Cbr>        with open(name[0], 'w+', encoding='utf-8') as file_object:\u003Cbr>            file_object.write(r.text)     \u003Cbr>    elif mode == 'raw':\u003Cbr>        with open(name[0], 'wb+') as file_object:\u003Cbr>            file_object.write(r.content) \u003Cbr>    r.close()       \u003C\u002Fp>\u003C\u002Ftd>\u003C\u002Ftr>\u003C\u002Ftbody>\u003C\u002Ftable>\u003Cp>The complete implementation code has been uploaded to GitHub at the following address:\u003C\u002Fp>\u003Cp>An open-source project\u003C\u002Fp>\u003Cp>Usage example:\u003C\u002Fp>\u003Cp>(1) View emails in the Sent Items folder\u003C\u002Fp>\u003Ctable>\u003Ctbody>\u003Ctr>\u003Ctd>\u003Cp>python owaManage.py 192.168.1.1 test1 DomainUser123! ListFolder\u003C\u002Fp>\u003C\u002Ftd>\u003C\u002Ftr>\u003C\u002Ftbody>\u003C\u002Ftable>\u003Cp>Specified folder: sentitems\u003C\u002Fp>\u003Cp>Specified output result type: full\u003C\u002Fp>\u003Cp>As shown in the figure below\u003C\u002Fp>\u003Cp>\u003Cimg alt=\"Alt text\" src=\"\u002Fapi\u002Fmedia\u002Ffile\u002Fdocx_image_1770017337767_2_e6768f0acf-1.jpeg\">\u003C\u002Fp>\u003Cp>Result returns the total number of emails and information for each email. Here, the ConversationId corresponding to the email is obtained: AAQkADc4YjRlNDc1LWI0YjctNDEzZi1hNTQ5LWZkYWY0ZGZhZDM0NgAQAJdkOHS5cphDrNGlVbVpnIo=\u003C\u002Fp>\u003Cp>(2) Read email content\u003C\u002Fp>\u003Ctable>\u003Ctbody>\u003Ctr>\u003Ctd>\u003Cp>python owaManage.py 192.168.1.1 test1 DomainUser123! ViewMail\u003C\u002Fp>\u003C\u002Ftd>\u003C\u002Ftr>\u003C\u002Ftbody>\u003C\u002Ftable>\u003Cp>Specify the ConversationId corresponding to the email\u003C\u002Fp>\u003Cp>As shown in the figure below\u003C\u002Fp>\u003Cp>\u003Cimg alt=\"Alt text\" src=\"\u002Fapi\u002Fmedia\u002Ffile\u002Fdocx_image_1770017384040_3_2932f46532-1.jpeg\">\u003C\u002Fp>\u003Cp>Result returns the specific content of the email. Here, the attachment 111.txt is obtained with the type text\u002Fplain and the corresponding Id: AAMkADc4YjRlNDc1LWI0YjctNDEzZi1hNTQ5LWZkYWY0ZGZhZDM0NgBGAAAAAABEBlGH6URWQp6Nlg9RxLmyBwA1ZCfAg9a0Sq75no2JOzsqAAAAAAEKAAA1ZCfAg9a0Sq75no2JOzsqAAAAAByNAAABEgAQAO2T\u002FTJsdj9Emo9dwiMqlrM=\u003C\u002Fp>\u003Cp>(3) Download attachment\u003C\u002Fp>\u003Ctable>\u003Ctbody>\u003Ctr>\u003Ctd>\u003Cp>python owaManage.py 192.168.1.1 test1 DomainUser123! DownloadAttachment\u003C\u002Fp>\u003C\u002Ftd>\u003C\u002Ftr>\u003C\u002Ftbody>\u003C\u002Ftable>\u003Cp>Specify the Id corresponding to the attachment\u003C\u002Fp>\u003Cp>Specify the save format as text\u003C\u002Fp>\u003Cp>As shown in the figure below\u003C\u002Fp>\u003Cp>\u003Cimg alt=\"Alt text\" src=\"\u002Fapi\u002Fmedia\u002Ffile\u002Fdocx_image_1770017407753_4_e29544ddd4-1.jpeg\">\u003C\u002Fp>\u003Ch2>0x04 Summary\u003C\u002Fh2>\u003Cp>---\u003C\u002Fp>\u003Cp>This article introduces the implementation details of writing Python code to read Exchange emails through Outlook Web Access (OWA), documenting the development process.\u003C\u002Fp>\u003C\u002Fbody>\u003C\u002Fhtml>",1279,"Onedaysec",2,7,"published","2026-02-02T07:51:00.264Z",{"title":38,"description":14,"keywords":39,"ogImage":40,"canonicalUrl":40,"noIndex":41},"Read Exchange Emails via OWA Command Line - Penetration Testing Guide","OWA, Exchange, email reading, penetration testing, command line, Python, Outlook Web Access, email security",null,false,[],{"docs":44,"hasNextPage":41},[45,46,47,4],393,392,391,{"title":40,"description":40,"image":40},"2026-07-24T15:37:13.713Z","2026-07-23T16:01:29.662Z","draft","2026-07-23T16:05:50.337Z"]