Instances PostgreSQL accessibles d'Internet
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
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.