Hacking Question Pulling Splatoon2 related data from the Nintendo Switch Online app

Dann_

Well-Known Member
OP
Newcomer
Joined
May 3, 2016
Messages
66
Trophies
0
Age
32
XP
204
Country
Afghanistan
So I've been trying to programatically aquire data from the Splatoon2 service on the Nintendo Switch Online app for a site like splatoon.ink I've succesfully authenticated as a user with my program but I can't seem to make any api calls after that. I'm just gonna share what I've learned so people can maybe use it or help me out documenting the whole service.

One thing that may be at fault is my gathered packets, They're from the app "Packet Capture" on the Play store, it uses a VPN and custom SSL Certificate and some things in the app stop working because of it ("Opening the splatoon 2 service results in an empty page :/"), gonna retry this with my phone rooted and an actually decent sniffer in a bit. Will update the thread if something changes because of it.

I've successfully sniffed 3 HTTPS POST requests the app sends to some service, one logs the user in with a previously aquired token and I've been able to replicate the request in Python3, that requests returns a access_token and an id_token, both which expire after 900 seconds. The second requests is to api.account.nintendo.com/2.0.0/users/me which gets some user related data like mii picture and email address. It requires you to set the Authorization header to "Bearer {access_token}", which I did, however it throws me back an html file with status code 405: Method not allowed (Maybe someone can try this just to see if it isn't me being sleep deprived that is the issue). After this the app is logged in and has the users info. When the Splatoon2 service is clicked another request is send. This request is magical to me, it sends a whole bunch of info to the server including device manufacturer, device name, android version etc etc and some ID's which the app doesn't retrieve at launch (hard coded maybe?), the request is encoded with gzip, all the server returns for me is a json encoded messages saying something along the lines of "1 item receive, 1 item accepted".

If someone wants the snippet of code I made then do tell, I'll upload it to pastebin or something :P
 

rctgamer3

Well-Known Member
Member
Joined
May 5, 2008
Messages
321
Trophies
1
XP
552
Country
Netherlands
Since the app is just a fancy website viewer (WebView), i dug a bit into the workings of the app by decrypting the TLS/HTTPS connection. I found at least one JSON that contains schedule data. My token only seemed to expire after a day or two, strange. The hardest part is probably getting a valid token on a computer. Otherwise just grab a token from the app and replay it in your browser.
Edit 1: Just need to steal the iksm_session token from whatever program you use to proxy the requests, be it Fiddler+HTTPS decrypting or whatever program you use. Thank http://s2terminal.hatenablog.com/entry/2017/07/23/203831 for that.


daf84fe32c9b3f5087b47c6e13030271.png
Meow.

Edit 2: stages are at /api/timeline:

Code:
  "schedule":{
      "importance":0.8,
      "schedules":{
         "regular":[
            {
               "game_mode":{
                  "key":"regular",
                  "name":"Regular Battle"
               },
               "stage_a":{
                  "name":"Musselforge Fitness",
                  "image":"/images/stage/83acec875a5bb19418d7b87d5df4ba1e38ceac66.png",
                  "id":"1"
               },
               "end_time":1500840000,
               "id":4780952683920125481,
               "stage_b":{
                  "id":"2",
                  "name":"Starfish Mainstage",
                  "image":"/images/stage/187987856bf575c4155d021cb511034931d06d24.png"
               },
               "rule":{
                  "name":"Turf War",
                  "key":"turf_war",
                  "multiline_name":"Turf\nWar"
               },
               "start_time":1500832800
            }
         ],
         "league":[
            {
               "stage_a":{
                  "id":"4",
                  "name":"Inkblot Art Academy",
                  "image":"/images/stage/5c030a505ee57c889d3e5268a4b10c1f1f37880a.png"
               },
               "game_mode":{
                  "key":"league",
                  "name":"League Battle"
               },
               "start_time":1500832800,
               "rule":{
                  "name":"Splat Zones",
                  "multiline_name":"Splat\nZones",
                  "key":"splat_zones"
               },
               "end_time":1500840000,
               "id":4780952683920125481,
               "stage_b":{
                  "image":"/images/stage/0907fc7dc325836a94d385919fe01dc13848612a.png",
                  "name":"Port Mackerel",
                  "id":"7"
               }
            }
         ],
         "gachi":[
            {
               "game_mode":{
                  "key":"gachi",
                  "name":"Ranked Battle"
               },
               "stage_a":{
                  "id":"3",
                  "name":"Sturgeon Shipyard",
                  "image":"/images/stage/bc794e337900afd763f8a88359f83df5679ddf12.png"
               },
               "stage_b":{
                  "name":"The Reef",
                  "image":"/images/stage/98baf21c0366ce6e03299e2326fe6d27a7582dce.png",
                  "id":"0"
               },
               "end_time":1500840000,
               "id":4780952683920125481,
               "start_time":1500832800,
               "rule":{
                  "name":"Tower Control",
                  "multiline_name":"Tower\nControl",
                  "key":"tower_control"
               }
            }
         ]
      }
   },
 
Last edited by rctgamer3,

Dann_

Well-Known Member
OP
Newcomer
Joined
May 3, 2016
Messages
66
Trophies
0
Age
32
XP
204
Country
Afghanistan
Thanks for finding the endpoint :D, So I figured out why my SSLStrip didn't work, it seems that Android N will still allow you to install user signed certificates, but it will only use them if the app specifically specifies if it's okay with it (which no-one does)... Grabbing my tablet rn since it's on M I believe.. Basically the only packets I've been able to sniff were api auth, crash reports, data collection and google-analytics lol
 

Dann_

Well-Known Member
OP
Newcomer
Joined
May 3, 2016
Messages
66
Trophies
0
Age
32
XP
204
Country
Afghanistan
Since the app is just a fancy website viewer (WebView), i dug a bit into the workings of the app by decrypting the TLS/HTTPS connection. I found at least one JSON that contains schedule data. My token only seemed to expire after a day or two, strange. The hardest part is probably getting a valid token on a computer. Otherwise just grab a token from the app and replay it in your browser.
Edit 1: Just need to steal the iksm_session token from whatever program you use to proxy the requests, be it Fiddler+HTTPS decrypting or whatever program you use. Thank http://s2terminal.hatenablog.com/entry/2017/07/23/203831 for that.


daf84fe32c9b3f5087b47c6e13030271.png
Meow.

Edit 2: stages are at /api/timeline:

Code:
  "schedule":{
      "importance":0.8,
      "schedules":{
         "regular":[
            {
               "game_mode":{
                  "key":"regular",
                  "name":"Regular Battle"
               },
               "stage_a":{
                  "name":"Musselforge Fitness",
                  "image":"/images/stage/83acec875a5bb19418d7b87d5df4ba1e38ceac66.png",
                  "id":"1"
               },
               "end_time":1500840000,
               "id":4780952683920125481,
               "stage_b":{
                  "id":"2",
                  "name":"Starfish Mainstage",
                  "image":"/images/stage/187987856bf575c4155d021cb511034931d06d24.png"
               },
               "rule":{
                  "name":"Turf War",
                  "key":"turf_war",
                  "multiline_name":"Turf\nWar"
               },
               "start_time":1500832800
            }
         ],
         "league":[
            {
               "stage_a":{
                  "id":"4",
                  "name":"Inkblot Art Academy",
                  "image":"/images/stage/5c030a505ee57c889d3e5268a4b10c1f1f37880a.png"
               },
               "game_mode":{
                  "key":"league",
                  "name":"League Battle"
               },
               "start_time":1500832800,
               "rule":{
                  "name":"Splat Zones",
                  "multiline_name":"Splat\nZones",
                  "key":"splat_zones"
               },
               "end_time":1500840000,
               "id":4780952683920125481,
               "stage_b":{
                  "image":"/images/stage/0907fc7dc325836a94d385919fe01dc13848612a.png",
                  "name":"Port Mackerel",
                  "id":"7"
               }
            }
         ],
         "gachi":[
            {
               "game_mode":{
                  "key":"gachi",
                  "name":"Ranked Battle"
               },
               "stage_a":{
                  "id":"3",
                  "name":"Sturgeon Shipyard",
                  "image":"/images/stage/bc794e337900afd763f8a88359f83df5679ddf12.png"
               },
               "stage_b":{
                  "name":"The Reef",
                  "image":"/images/stage/98baf21c0366ce6e03299e2326fe6d27a7582dce.png",
                  "id":"0"
               },
               "end_time":1500840000,
               "id":4780952683920125481,
               "start_time":1500832800,
               "rule":{
                  "name":"Tower Control",
                  "multiline_name":"Tower\nControl",
                  "key":"tower_control"
               }
            }
         ]
      }
   },
Thanks a ton, got the service working, right now it tweets the current maps to @splatoon2info, Gonna set up an API and quick little site since I've gotten all the data already. This is it's first successful tweet :P Failed to fetch tweet https://twitter.com/splatoon2info/status/889446480683073538
Once I've got some more free time I'm gonna see if I can get Salmon Run and Splatfests in there too.
 
Last edited by Dann_,

Dann_

Well-Known Member
OP
Newcomer
Joined
May 3, 2016
Messages
66
Trophies
0
Age
32
XP
204
Country
Afghanistan
Hello, I got the Json too but the cookie only lasts 24h. ¿How do you bypass this?

Thank you.
Currently not bypassing in but I'm quite sure it gets the cookie from a previous request to the server. I'll check it out tonight if I have time but I'd guess you just have to send your session token and client id to the api at some point and you get back the cookie :P
 

Souloibur

Well-Known Member
Newcomer
Joined
Nov 8, 2016
Messages
49
Trophies
0
Age
28
XP
216
Country
Currently not bypassing in but I'm quite sure it gets the cookie from a previous request to the server. I'll check it out tonight if I have time but I'd guess you just have to send your session token and client id to the api at some point and you get back the cookie :P

I'm trying to find that request. If you find something and want to tell me, i'll be grateful.
 

crankcube

New Member
Newbie
Joined
Jul 24, 2017
Messages
3
Trophies
0
Age
45
XP
53
Country
United Kingdom
Thanks for posting this, it gave me enough of a starting point to figure out the rest.

I followed the article rctgamer3 posted to sniff an android phone's Nitendo Switch App and discovered that 3 initial inputs are needed


  1. client_id - some hash value, I suspect its the hash of the device or the App.
  2. resource_id - a large numerical number, i suspect this is the identifier for the splatoon2 content with in the app
  3. initial_token_id - a large of data, I think this must be baked into the app
using the above which you can fish out from your proxy sniffer you can generate valid tokens and ultimately make the splatoon site give you a the iksm_session cookie.


Code:
#!/usr/bin/env python3
import logging
import requests
import json
import sys
import http.client as http_client
http_client.HTTPConnection.debuglevel = 1

logging.basicConfig()
logging.getLogger().setLevel(logging.DEBUG)
requests_log = logging.getLogger("requests.packages.urllib3")
requests_log.setLevel(logging.DEBUG)
requests_log.propagate = True

def main():

    client_id = "xxxxxxxxxxxx"
    resource_id = 123456789
    init_session_token = "eyJhbGci.....Wq2Q"

    session = requests.Session()
    response = session.post('https://accounts.nintendo.com/connect/1.0.0/api/token',
                            headers={'Accept': 'application/json'},
                            json={ "client_id": client_id,
                                  "grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer-session-token",
                                  "session_token": init_session_token})
    api_tokens = response.json()
    #print(json.dumps(response.json(),indent=4))
  
    response = session.post('https://api-lp1.znc.srv.nintendo.net/v1/Account/GetToken',
                            headers={'Accept': 'application/json', 
                                     'Authorization': "Bearer " + api_tokens["access_token"]},
                            json={"parameter": {
                                    "language": 'null',
                                    "naBirthday": 'null',
                                    "naCountry": 'null',
                                    "naIdToken": api_tokens["id_token"] }
                                    })
    tokens = response.json()["result"]
    #print(json.dumps(response.json(),indent=4))
  
    response = session.post('https://api-lp1.znc.srv.nintendo.net/v1/Game/GetWebServiceToken',
                        headers={'Accept': 'application/json',
                                 'Authorization': "Bearer "+tokens["webApiServerCredential"]["accessToken"]},
                        json={"parameter": {"id": resource_id}} )

    res_json = response.json()
    if res_json["status"] != 0:
        logging.error(json.dumps(res_json,indent=4))
        raise RuntimeError("initial auth failed")
    access_token = res_json["result"]["accessToken"]

    # get the cookie setup
    response = session.get("https://app.splatoon2.nintendo.net/?lang=ja-JP",
                        headers={'Accept': 'application/json',
                                 'X-gamewebtoken': access_token})

    # now we can rock'n'roll
    response = session.get('https://app.splatoon2.nintendo.net/api/schedules', headers={'accept': 'application/json'})
    print(json.dumps(response.json(), indent=4))
 
  • Like
Reactions: Marxally and Dann_

Dann_

Well-Known Member
OP
Newcomer
Joined
May 3, 2016
Messages
66
Trophies
0
Age
32
XP
204
Country
Afghanistan
Thanks for posting this, it gave me enough of a starting point to figure out the rest.

I followed the article rctgamer3 posted to sniff an android phone's Nitendo Switch App and discovered that 3 initial inputs are needed


  1. client_id - some hash value, I suspect its the hash of the device or the App.
  2. resource_id - a large numerical number, i suspect this is the identifier for the splatoon2 content with in the app
  3. initial_token_id - a large of data, I think this must be baked into the app
using the above which you can fish out from your proxy sniffer you can generate valid tokens and ultimately make the splatoon site give you a the iksm_session cookie.


Code:
#!/usr/bin/env python3
import logging
import requests
import json
import sys
import http.client as http_client
http_client.HTTPConnection.debuglevel = 1

logging.basicConfig()
logging.getLogger().setLevel(logging.DEBUG)
requests_log = logging.getLogger("requests.packages.urllib3")
requests_log.setLevel(logging.DEBUG)
requests_log.propagate = True

def main():

    client_id = "xxxxxxxxxxxx"
    resource_id = 123456789
    init_session_token = "eyJhbGci.....Wq2Q"

    session = requests.Session()
    response = session.post('https://accounts.nintendo.com/connect/1.0.0/api/token',
                            headers={'Accept': 'application/json'},
                            json={ "client_id": client_id,
                                  "grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer-session-token",
                                  "session_token": init_session_token})
    api_tokens = response.json()
    #print(json.dumps(response.json(),indent=4))
 
    response = session.post('https://api-lp1.znc.srv.nintendo.net/v1/Account/GetToken',
                            headers={'Accept': 'application/json',
                                     'Authorization': "Bearer " + api_tokens["access_token"]},
                            json={"parameter": {
                                    "language": 'null',
                                    "naBirthday": 'null',
                                    "naCountry": 'null',
                                    "naIdToken": api_tokens["id_token"] }
                                    })
    tokens = response.json()["result"]
    #print(json.dumps(response.json(),indent=4))
 
    response = session.post('https://api-lp1.znc.srv.nintendo.net/v1/Game/GetWebServiceToken',
                        headers={'Accept': 'application/json',
                                 'Authorization': "Bearer "+tokens["webApiServerCredential"]["accessToken"]},
                        json={"parameter": {"id": resource_id}} )

    res_json = response.json()
    if res_json["status"] != 0:
        logging.error(json.dumps(res_json,indent=4))
        raise RuntimeError("initial auth failed")
    access_token = res_json["result"]["accessToken"]

    # get the cookie setup
    response = session.get("https://app.splatoon2.nintendo.net/?lang=ja-JP",
                        headers={'Accept': 'application/json',
                                 'X-gamewebtoken': access_token})

    # now we can rock'n'roll
    response = session.get('https://app.splatoon2.nintendo.net/api/schedules', headers={'accept': 'application/json'})
    print(json.dumps(response.json(), indent=4))
Well done! I'm pretty sure the session token is made when you first log in to the app since it's also used to get information of the user account by the app.
 
Last edited by Dann_,

crankcube

New Member
Newbie
Joined
Jul 24, 2017
Messages
3
Trophies
0
Age
45
XP
53
Country
United Kingdom
Well done! I'm pretty sure the session token is made when you first log in to the app since it's also used to get information of the user account by the app.

When you login to Nintendo via the switch app it asks you which account to use, I think i t must be registering the user with the app and getting a initial session setup then.

The Nintendo account login part has some protection against MitMProxies and rejects login attempts via the proxy so I've not been able to figure out how to go from a username/password -> initial token. if I can figure that bit out, it should be possible to remove all the hardcoded values.
 
Last edited by crankcube,

rctgamer3

Well-Known Member
Member
Joined
May 5, 2008
Messages
321
Trophies
1
XP
552
Country
Netherlands
When you login to Nintendo via the switch app it asks you which account to use, I think i t must via the proxy so I've not been able to figure out how to go from a username/password -> initial token. if I can figure that bit out, it should be possible to remove all the hardcoded values.
My root cert setup also works during the init login attempt. I'll try to reinstall the app and check if i can find something out.
 

XenonNSMB

New Member
Newbie
Joined
Jul 27, 2017
Messages
2
Trophies
0
Age
33
XP
44
Country
United States
If you sniff the contents of a request to /api/nickname_and_icon you can get the URL to your Nintendo Switch profile photo. Might be useful for people who want to save their Switch picture to their computer. And unlike everything else, the URL to the icon doesn't require a token to access from a browser. For example, here's the URL to my icon: https://cdn-image-e0d67c509fb203858ebcb2fe3f88c2aa.baas.nintendo.com/1/92e977edee90df24


Here are some other URLs:
api/festivals/active - presumably lets you check if a Splatfest is going on
https://api.accounts.nintendo.com/2.0.0/users/me - Lets you get your Mii and account data
https://api-lp1.znc.srv.nintendo.net/v1/Game/ListWebServices - Used by the app to get a list of webpages to show under "Game-Specific Services". Example JSON for Splatoon 2:
Code:
            "imageUri": "https://cdn.znc.srv.nintendo.net/gameWebServices/splatoon2/images/usEn/banner.png",

            "name": "Splatoon 2",

            "uri": "https://app.splatoon2.nintendo.net/",

            "whiteList": [

                "app.splatoon2.nintendo.net"

            ]
Apparently each game has its own whitelist array of allowed sites.

switch2.png

Man, the Switch Online app is my favorite web browser.
 
Last edited by XenonNSMB,

eliboa

Well-Known Member
Member
Joined
Jan 13, 2016
Messages
157
Trophies
0
XP
1,257
Country
France
Hi,
Since the Nintendo Switch Online app had been updated, I'm having trouble accessing POST https://api-lp1.znc.srv.nintendo.net/v1/Account/Login

It looks like a new parameter "f" was added to the request body. Here is the HTTPS request sent by the app
Code:
POST https://api-lp1.znc.srv.nintendo.net/v1/Account/Login HTTP/1.1
X-ProductVersion: 1.1.0
X-Platform: Android
User-Agent: com.nintendo.znca/1.1.0 (Android/7.0)
Accept: application/json
Authorization: Bearer
Content-Type: application/json; charset=utf-8
Content-Length: 977
Host: api-lp1.znc.srv.nintendo.net
Connection: Keep-Alive
Accept-Encoding: gzip

{
   "parameter":{
      "naIdToken":"eyJraWQiOiIwMD[...]jDE9DfWUpg",
      "naCountry":null,
      "naBirthday":null,
      "language":null,
      "f":"5c5ab0a441658711115122ab753e5fc3c4291c6de8a0dc91dc6ca7a82a83412a"
   }
}
I can't figure out how this new token is generated :nayps3:
Maybe by the app itself since neither "/connect/1.0.0/api/session_token" nor "/connect/1.0.0/api/token" return it.

Any clue ?

Btw, note that the "X-ProductVersion" header parameter is now "1.1.0"
 

Site & Scene News

Popular threads in this forum

General chit-chat
Help Users
    SylverReZ @ SylverReZ: Or Genesis.