Vogelspotten

Tijdens het onderzoek voor mijn scriptie moest een deel besteden aan het bestuderen van het Mysql authenticate protocol. Aangezien Open Canary dit protocol ook implementeert besloot ik die implementatie nader te onderzoeken. Ik merkte echter een verschil in de manier waarop foutmeldingen werden teruggegeven door een echte Mysql server en de implementatie van Open Canary.

Initiële Bevinding

Als je probeert in te loggen op een Mysql server met de verkeerde gegevens, krijg je een foutmelding terug met een zogeheten SQL state. Deze states staan beschreven in de documentatie van Mysql zelf. De sectie in dit pakketje moet starten met een hekje (#) en wordt gevolgd door exact vijf karakters die de SQL state beschrijven. Echter zet de implementatie van Open Canary hier twee hashtags neer in plaats van een, waardoor de boel verschuift.

Standaard Mysql foutmelding:

1 ➜ mysql -h 127.0.0.1 -P3306
2ERROR 1045 (28000): Access denied for user 'niels'@'127.0.0.1' (using password: NO)

Open Canary foutmelding (het hekje past de syntax highlighting aan):

1 ➜ mysql -h 127.0.0.1 -P3306
2ERROR 1045 (#2800): Access denied for user 'niels'@'127.0.0.1' (using password: NO)

De Code

De code van deze neppe Mysql server is vrij simpel, hier het stuk wat de foutcode afhandelt:

1def error_pkt(self, seq_id, err_code, sql_state, msg):
2    data = b"\xff" + struct.pack("<H", err_code) + b"\x23#" + sql_state + msg
3    return self.build_packet(0x02, data)

Het probleem zit in de \x23#, aangezien de code voor een hekje 23 is. Nu staat er dus een feitelijk ## terwijl het maar een hekje moet zijn. Dan is er nog een foutje met de SQL state die gegeven wordt aan het inloggen met foutieve gegevens. Deze staat in de code als 2800 terwijl dat 28000 moet zijn. Dit is ook de reden dat er niet zoveel fout gaat met deze foutmelding omdat de hele gehele SQL state door deze twee foutjes alsnog zes karakters lang blijft.

Het Foutje Gebruiken Als Aanval

Een honeypot kan voor meerdere dingen gebruikt worden, in weze is het een apparaat dat doet alsof hij een interessante server is. Dit maakt zo'n honeypot een aantrekkelijk doelwit voor hackers. Over het algemeen is het doel van een honeypot dus ook om aangevallen te worden. Dit heeft namelijk een aantal voordelen:

  1. Je weet dat er hackers aanwezig zijn in je netwerk waardoor je er meteen op kan acteren.
  2. Je kan uitvinden wat voor exploits ze gebruiken of naar wat voor data ze op zoek zijn door de informatie uit je honeypot te analyseren.
  3. Aanvallers verdoen hun tijd met het hacken van je honeypot in plaats van het hacken van je daadwerkelijke infrastructuur.

Simpelweg gezegd: als je honeypot wordt aangevallen is het in staat om je daar van op de hoogte te stellen. Zo kan je de hackers aanpakken terwijl de aanval nog bezig is. Het is daarom ook belangrijk dat het niet duidelijk is dat jouw honeypot een honeypot is. Vergelijk het met een lokauto van de politie: het zou het doel erg teniet doen al een dief van buiten kon zien dat hij met een lokauto te maken heeft. Die laat hij dan uiteraard staan.

Het foutje in deze implementatie zorgt er dus voor dat we kunnen zien dat we niet met een echte Mysql server te maken hebben. We moeten hiervoor wel inloggen wat er weer voor zorgt dat de beheerders van de honeypot op de hoogte worden gebracht.

Is er dan een manier om een foutmelding terug te krijgen zonder dat we het alarm activeren? Het blijkt van wel! Het protocol van Mysql stelt dat pakketjes genummerd dienen te worden. De server start met de conversatie en stuurt een pakketje met alle mogelijkheden. Dit is pakketje nul:

Afbeelding dat pakketje nul laat zien

De client stuurt dan een antwoord naar de server met z'n eigen mogelijkheden en de gebruikersnaam waar deze mee wil inloggen. Dit pakketje moet nummer een hebben:

Afbeelding dat het antwoord van de client laat zien

Wanneer een client een pakketje stuurt dat niet de juiste nummering hanteert zal Mysql antwoorden met een foutmelding: Got packets out of order, deze heeft dan een SQL state van 08S01. Maar door de dubbele haakjes in de foutmelding kunnen we twee dingen opmerken:

Afbeelding van de dubbele haakjes voor de SQL state

  1. De dubbele haakjes zelf.
  2. De foutmelding start met het laatste getal van de SQL state

De code hierachter is als volgt:

 1elif seq_id != 1:
 2    # error on wrong seq_id, even if payload hasn't arrived yet
 3    self.transport.write(self.unordered_pkt(0x01))
 4    self.transport.loseConnection()
 5    return
 6elif payload is not None:
 7    # seq_id == 1 and payload has arrived
 8    username, password = self.parse_auth(payload)
 9    if username:
10        logdata = {'USERNAME': username, 'PASSWORD': password}
11        self.factory.canaryservice.log(logdata, transport=self.transport)
12        self.transport.write(self.access_denied(0x02, username, password))
13        self.transport.loseConnection()

We zien een if statement, deze kijkt of het pakket dat binnengekomen is het juiste nummer heeft. Zo niet dan geeft de server een foutmelding terug aan de client. Alleen is de logica die een log stuurt naar de beheerders pas na deze check. Op lijn drie wordt er dus een foutmelding gegenereerd en de connectie wordt opgeheven. We kunnen dus een pakket met het verkeerde pakketnummer sturen om zo een Got packets out of order te forceren. Dan kunnen we kijken of we de dubbele hashtag zien, als dat zo is dan weten we dat we met een Open Canary te maken hebben.

Mitigatie

Ik heb een patch gemaakt met 2 kleine aanpassingen aan de module:

 1--- a/opencanary/modules/mysql.py
 2+++ b/opencanary/modules/mysql.py
 3@@ -16,7 +16,7 @@ class MySQL(Protocol, TimeoutMixin):
 4     HEADER_LEN              = 4
 5     ERR_CODE_ACCESS_DENIED  = 1045
 6     ERR_CODE_PKT_ORDER      = 1156
 7-    SQL_STATE_ACCESS_DENIED = b"2800"
 8+    SQL_STATE_ACCESS_DENIED = b"28000"
 9     SQL_STATE_PKT_ORDER     = b"08S01"
10 
11     # https://dev.mysql.com/doc/internals/en/connection-phase-packets.html#packet-Protocol::Handshake
12@@ -106,7 +106,7 @@ class MySQL(Protocol, TimeoutMixin):
13                               MySQL.SQL_STATE_PKT_ORDER, msg)
14 
15     def error_pkt(self, seq_id, err_code, sql_state, msg):
16-        data = b"\xff" + struct.pack("<H", err_code) + b"\x23#" + sql_state + msg
17+        data = b"\xff" + struct.pack("<H", err_code) + b"#" + sql_state + msg
18         return self.build_packet(0x02, data)
19 
20     def connectionMade(self):
21

Er is een extra nul toegevoegd aan de SQL_STATE_ACCESS_DENIED en de \x32 is uit de byte string gehaald in de foutmelding logica.

Ik heb contact gezocht met de mensen van Thinkt (die het Open Canary project beheren) en de patch is binnen twee dagen uitgerold

  • De commit staat hier
  • De security advisory staat hier

Conclusie

Er is een manier gevonden om een Open Canary te identificeren de de Mysql module draait. Deze detectie kan worden uitgevoerd zonder dat dat een alarm activeert en omzeilt daarmee het doel van een honeypot.

Het team van Thinkst heeft snel gereageerd op mijn mail en het probleem is binnen twee dagen verholpen. Als je Open Canary gebruikt is het advies om zo snel mogelijk te upgraden naar versie 0.6.1! Mocht dat niet mogelijk zijn op dit moment dan is het belangrijk om de Mysql module uit te schakelen.

Translations: