Un serveur PostgreSQL peut être accessible d’Internet, au sens d’avoir le service en écoute sur une adresse IP publique et un port TCP ouvert à toute connexion. A titre indicatif, shodan.io, un service qui sonde ce genre de choses, trouve plus de 650000 instances dans ce cas actuellement. Avec la popularisation du modèle DBaaS (“Database As A Service”), les serveurs PostgreSQL peuvent être légitimement accessibles d’Internet, mais ça peut être aussi le résultat involontaire d’une mauvaise configuration.

Car cette configuration réseau ouverte s’oppose à une autre plus traditionnelle et plus sécurisée lorsque les serveurs de bases de données sont au minimum protégés par un pare-feu, voire n’ont même pas d’interface réseau reliée à Internet, ou bien n’écoutent pas dessus.

La conséquence d’avoir des instances ouvertes est que des tentatives d’intrusion sur le port 5432 sont susceptibles de se produire à tout moment, tout comme il y a des tentatives de piratage en tout genre sur d’autres services comme ssh, le mail ou des applications web populaires comme Wordpress, Drupal ou phpMyAdmin.

Si vous avez un serveur accessible publiquement, il est possible de mettre son IP dans le champ de recherche de shodan.io, histoire de voir ce qu’il sait de vous.

Que vous ayez déjà des instances PostgreSQL ouvertes à l’Internet, que vous envisagiez d’en avoir, ou au contraire que vous vouliez vous assurer que vos instances ne sont pas accessibles, voici deux ou trois réflexions à ce sujet.

Ne pas ouvrir involontairement son instance à l’Internet!

Quand on demande “comment activer l’accès à PostgreSQL à partir d’une autre machine?”, la réponse typique est d’ajouter des règles dans pg_hba.conf et de mettre dans postgresql.conf:

listen_addresses = *

(en remplacement du listen_addresses = localhost initial)

Effectivement ça fonctionne, en faisant écouter toutes les interfaces réseau de la machine, pas seulement celle où les connexions PostgreSQL sont attendues. Dans le cas, assez typique, où ces connexions sont initiées exclusivement d’un réseau local privé, on pourrait plutôt préciser les adresses des interfaces concernées. Si par exemple le serveur a une IP privée 192.168.1.12, on pourrait mettre:

listen_addresses = localhost, 192.168.1.12

Pourquoi ces adresses plutôt que * ? On peut se poser plus généralement la question: pourquoi PostgreSQL n’a pas listen_addresses = * par défaut, de façon à ce qu’un poste distant puisse se connecter directement, sans obliger un admin à modifier d’abord la configuration?

MongoDB faisait ça, et l’ampleur des attaques réussies contre cette base illustre assez bien pourquoi ce n’est pas une bonne idée. En 2015 shodan estimait qu’au moins 30000 instances MongoDB étaient librement accessibles d’Internet, probablement dans leur configuration par défaut, laissant l’accès à 595 TB de données. Fin 2016, une campagne d’attaque dite “Mongo Lock” commençait à affecter une bonne partie de ces victimes potentielles. Le piratage consistait à effacer ou chiffrer les données et exiger une rançon en bitcoins pour les récupérer. Cet épisode a été une vraie débâcle pour la réputation de MongoDB.

Indépendamment de la question du mot de passe, dont l’absence par défaut est aussi un facteur important dans ces attaques, l’ampleur aurait été biensûr moindre si le service écoutait par défaut uniquement sur l’interface réseau locale, puisque c’est suffisant quand un site et sa base sont la même machine.

MongoDB a changé depuis cette configuration par défaut, mais des années après on voit toujours ce qui semble être des exploitations de ce problème, par exemple en janvier 2019, cette fuite de données: MongoDB : 202 millions de CV privés exposés sur internet à cause d’une base de données non protégée.

C’est qu’il y a toujours dans la nature des installations jamais mises à jour dont les gérants, quand il y en a, n’ont aucune idée qu’il y a un danger pour leurs données et qu’il faudrait changer une configuration alors même que “ça marche”…

Quand on ouvre volontairement son instance

Evidemment, il faut protéger les comptes utilisateur par des mots de passe solides, mais ça ne suffit pas.

Un pré-requis indispensable est de se tenir au courant des mises à jour de sécurité et d’être prêt à les appliquer en urgence si nécessaire. Par exemple en 2013, la faille de sécurité CVE-2013-1899 permettait de prendre la main à distance sur n’importe quelle instance PostgreSQL, indépendamment des mots de passe et des règles du pg_hba.conf, tant qu’on avait un moyen de la joindre par le réseau (d’où encore une fois l’intérêt de ne pas s’exposer inutilement en mettant listen_addresses = * quand ce n’est pas indispensable).

Cette faille de sécurité est scrutée par des sondes à qui on a rien demandé, puisque si je regarde les logs récents de mon instance PostgreSQL ouverte sur Internet, je vois des entrées du style (modulo le masquage de la source):

2019-01-31 05:51:44 CET FATAL:  no pg_hba.conf entry for host "185.x.x.x", 
user "postgres", database "template0", SSL on
2019-01-31 05:51:44 CET FATAL:  no pg_hba.conf entry for host "185.x.x.x", 
user "postgres", database "template0", SSL off
2019-01-31 05:51:44 CET FATAL:  unsupported frontend protocol 65363.19778: serve
r supports 1.0 to 3.0
2019-01-31 05:51:44 CET FATAL:  no pg_hba.conf entry for host "185.x.x.x", 
user "postgres", database "-h", SSL on
2019-01-31 05:51:44 CET FATAL:  no pg_hba.conf entry for host "185.x.x.x", 
user "postgres", database "-h", SSL off

Le nom de base “-h” n’est pas choisi au hasard, la faille ci-dessus étant décrite par:

Argument injection vulnerability in PostgreSQL 9.2.x before 9.2.4, 9.1.x before 9.1.9, and 9.0.x before 9.0.13 allows remote attackers to cause a denial of service (file corruption), and allows remote authenticated users to modify configuration settings and execute arbitrary code, via a connection request using a database name that begins with a “-“ (hyphen)

Ce genre de tentative peut venir d’un service comme shodan ou d’un bot malveillant, voire d’un attaquant qui vous vise spécifiquement, difficile à savoir.

L’attaque à la cryptomonnaie

Il y a des exemples d’attaques réussies sur postgres, notamment visant à faire miner de la cryptomonnaie Monero.

Pour autant qu’on puisse en juger de l’extérieur, ces attaques n’exploitent pas une faille spécifique de postgres, mais parviennent à se connecter en super-utilisateur postgres. On peut imaginer que ça arrive à cause d’un mot de passe trop faible, d’un pg_hba.conf trop laxiste, ou via le piratage d’un autre service (typiquement un site web) qui se connecte à PostgreSQL en super-utilisateur.

Par exemple dans cette question sur dba.stackexchange: Mysterious postgres process pegging CPU at 100%; no running queries un utilisateur demande pourquoi postgres fait tourner une commande ./Ac2p20853 consommant tout le CPU disponible. L’explication de loin la plus plausible est un piratage dans lequel ce binaire a été téléchargé et lancé via une fonction postgresql ayant les droits super-utilisateur.

Cette autre question sur stackoverflow.com (CPU 100% usage caused by unknown postgres query) est assez similaire, mais en plus elle montre des requêtes servant de coquille au programme parasite:

pg_stat_activity:

pid   datname   username  query
19882 postgres  postgres  select Fun013301 ('./x3606027128 &')
19901 postgres  postgres  select Fun013301 ('./ps3597605779 &')

top:

PID   USER      PR  NI    VIRT    RES    SHR S %CPU %MEM   TIME+   COMMAND
19885 postgres  20   0  192684   3916   1420 S 98.3  0.1   5689:04 x3606027128

Ce comportement ressemble trait pour trait à l’attaque que Imperva a détecté via leurs instances “pot de miel”, et disséquée dans leur article A Deep Dive into Database Attacks [Part III]: Why Scarlett Johansson’s Picture Got My Postgres Database to Start Mining Monero.

En résumé, une fois qu’une connexion SQL sur un compte super-utilisateur est obtenue (par un moyen non précisé), le code attaquant créé une fonction SQL permettant d’exécuter n’importe quel programme sur disque. Ensuite il créé sur le disque via lo_export() un programme qui a pour objet d’aller récupérer sur Internet le vrai programme qui mine. Le programme en question est sur un site d’images public, en l’occurrence caché ici dans un fichier photo représentant Scarlett Johansson, d’où la référence improbable à l’actrice dans le titre de l’article.

Moralité: il faut limiter les comptes super-utilisateur à un usage d’administration, et éviter de leur attribuer le droit aux connexions distantes, via pg_hba.conf.

Interdire les connexions distantes non chiffrées

Avoir ssl=on dans la configuration serveur signifie que le chiffrage est possible quand le client le demande, mais pas qu’il est obligatoire. Le chiffrage évite qu’une tierce partie ayant accès au réseau puisse lire tout ce qui passe entre le client et le serveur.

Si on veut l’obliger du côté serveur, on peut y arriver via les règles du fichier pg_hba.conf (les règles sont interprétées dans l’ordre et le test s’arrête dès qu’une correspondance est trouvée, comme dans une cascade de IF…ELSEIF…ELSIF…ELSIF…END IF):

# autorise les connexions locales "Unix domain sockets"
# sans mot de passe pour le même utilisateur OS

local      all  all                 peer

# permet l'économie du chiffrage, mais pas du mot de passe
# pour les connexions TCP locales

host       all  all  127.0.0.1/32   md5   # plutôt scram avec postgresql 10 et plus
host       all  all  ::1/128        md5

# rejette les connexions distantes non chiffrées

hostnossl  all  all  0.0.0.0/0     reject
hostnossl  all  all  ::/0          reject

# ajouter les autres règles à partir d'ici
...
...

Par défaut la bibliothèque cliente la plus souvent utilisée, libpq, lorsqu’elle est compilée avec le support SSL, essaie d’abord une connexion chiffrée, puis le cas échéant une connexion non chiffrée. Ce comportement correspond à sslmode=prefer dans les paramètres de connexion (voir le détail dans la section Support de SSL de la doc). C’est pour ça que dans les logs, une tentative de connexion infructueuse comme ci-dessus apparaît en double, une première fois avec SSL=on et la seconde avec SSL=off.

Depuis la version 9.5, il est possible de savoir parmi les connexions établies quelles sont celles qui sont chiffrées ou pas avec la vue système pg_stat_ssl

SELECT datname,usename, ssl, client_addr 
  FROM pg_stat_ssl
  JOIN pg_stat_activity
    ON pg_stat_ssl.pid = pg_stat_activity.pid;

A défaut d’interdire les connexions non chiffrées, cette requête permet de vérifier s’il y en a et d’où elles viennent.