Naar de inhoud

Blog ·

Een fout secret dat op stilte lijkt

Drie bewakingsprotocollen met de hand geschreven in plaats van binnengehaald, en een foutbeeld dat het protocol zelf niet van een dode server onderscheidt.

Door Provibr

Een bewakingscontrole voor een nieuw protocol toevoegen ziet eruit als een afhankelijkhedenvraag. Er is een crate voor MongoDB, een crate voor BSON, een crate die het draadprotocol van SQL Server spreekt. Drie regels in een manifest, veertig regels lijmcode, klaar.

We hebben alle drie de protocollen met de hand geschreven, en de redenen zijn het opschrijven waard omdat geen ervan "niet hier bedacht" is. Twee zijn metingen, en de derde is een belofte die we klanten al gedaan hadden over wat deze agent kan.

Wat een afhankelijkheid werkelijk meebrengt

De BSON-crate trekt eenenveertig crates mee. Dat is te meten en het is gemeten, maar het aantal crates gaf niet de doorslag.

De controle leest vier velden uit één serverantwoord. Een generiek documenttype houdt het hele antwoord in het geheugen: de hostlijst, de tags, het tijdstip van de laatste schrijfactie, de clustertijd, alles wat de server wilde meesturen. Dan is "wij lezen er maar vier velden uit" een afspraak tussen ontwikkelaars in plaats van een eigenschap van de code, en afspraken worden gebroken door de volgende ronde die om een goede reden een vijfde veld nodig heeft.

Er staat dus een lezer die de elementen langsloopt, de vier gevraagde teruggeeft en de rest overslaat zonder hem ergens neer te zetten. Hij kán het antwoord niet vasthouden, dus de belofte is een vorm in plaats van een gewoonte. Hetzelfde besluit als de allowlist op het INFO-commando van Redis, en hetzelfde besluit dat een volledige databasedriver buiten de PostgreSQL-controle hield: een driver brengt een querybouwer mee, en dan bestáát er, in de binary die op het netwerk van een klant draait, code die een willekeurige vraag aan de database van die klant kan stellen.

De crate voor SQL Server is de scherpere versie. Hij bouwt voor ons releasedoel, en dat was juist wat we verwachtten dat hem zou tegenhouden. Wat hij meebrengt is een tweede TLS-stapel: een uit onderhoud geraakte lijn naast de huidige die we al uitleveren. De procesbrede cryptoprovider wordt voor de huidige lijn geïnstalleerd en doet voor de oudere niets, dus TLS onder één controle zou een andere provider gebruiken dan elke andere verbinding van deze agent; een kwetsbaarheid in die lijn is er één die wij uitleveren en niet kunnen repareren zonder op de leverancier te wachten; en het certificaatbeleid, dat in precies één module staat juist zodat het niet uit de pas kan lopen, zou een tweede implementatie krijgen tegen een andere API.

Overslaan is waar een zelfgeschreven lezer misgaat

Wie zelf een lezer voor een binair formaat schrijft, kan één fout maken die zwaarder weegt dan de rest, en die zit niet in het ontleden. Hij zit in het overslaan.

Elke waarde in BSON heeft een typebyte en een lengte die daaruit volgt. Een lezer die de types kent die hij toevallig is tegengekomen, en een onbekend type met nul bytes overslaat, faalt niet: hij leest de rest van het document gewoon vanaf de verkeerde plek verder. Alles daarna is verschoven. Je krijgt aannemelijke waarden die niet de verzonden waarden zijn.

De lengtetabel is daarom compleet, met een gesloten weigering voor alles daarbuiten. Een onbekende typebyte is een protocolfout en nooit een overslag.

Dezelfde soort fout zit een laag hoger, in hoe getallen gelezen worden. Een MongoDB-server antwoordt ok: 1 als double en een fout-code als 32-bits geheel getal, in hetzelfde document. Een lezer die alleen 32-bits kent mist het eerste stil, en dan leest elke mislukte inlog als een geslaagde. De lezer accepteert een BSON-getal in alle drie zijn breedtes en geeft één geheel getal terug.

De inlogstap die moet kunnen falen

De controle logt in met SCRAM-SHA-256 en niet met SCRAM-SHA-1, om twee redenen die dezelfde kant op wijzen.

Het oudere mechanisme voert niet het wachtwoord in SCRAM in. Het voert een hexadecimale MD5 van de gebruikersnaam, een vaste tekst en het wachtwoord in. Het ondersteunen zou betekenen dat wij code schrijven die een MD5 van inloggegevens maakt, voor een mechanisme dat geen enkele moderne server nog hoeft aan te bieden.

De stillere reden is erger. Een server die beide aanbiedt zou bij een val-terug op het zwakkere uitkomen zonder dat er iets rood wordt, en dan staat er een groene meting boven een inlog die zwakker is dan de klant denkt te hebben. Biedt de server geen SHA-256 aan voor deze gebruiker, dan weigert de controle met een eigen code en zegt dat erbij.

SCRAM met de hand schrijven is in een eerdere ronde voor de PostgreSQL-controle geweigerd, en dat bezwaar was echt: de uitwisseling heeft een serverhandtekening die de client hoort te verifiëren, en een implementatie die die stap overslaat is groen tegen een echte server en even groen tegen een vervalste. Hier is het bezwaar niet weggeredeneerd maar beantwoord. De handtekeningcontrole is een eigen functie met een eigen foutcode, zodat "de stap ontbreekt" iets anders is dan "de stap slaagde", en er is een test die een vervalste handtekening aanbiedt en eist dat de inlog dan faalt. Die test is rood bewezen door de controle weg te halen.

De crate die de PostgreSQL-controle gebruikt was hier niet bruikbaar, en dat is gemeten en niet aangenomen. Zijn SCRAM-implementatie schrijft het eerste clientbericht met een lege gebruikersnaam, omdat PostgreSQL de naam in zijn startupbericht meestuurt. MongoDB heeft zo'n bericht niet en leest de naam juist uit dat veld. De naam er achteraf in patchen zou precies de bytes veranderen waarover al getekend is, en de server zou ons bewijs afwijzen. Er is geen API om hem mee te geven.

Een fout die het protocol niet laat benoemen

Nu het deel dat geen enkele stub had kunnen vinden.

RADIUS wordt gecontroleerd met een Status-Server-verzoek, dat de server vraagt of hij leeft en verder niets vraagt. Het voor de hand liggende alternatief, een toegangsverzoek met een testaccount, is afgewezen: dat is een echte inlogpoging op het netwerk van de klant, met een echt account waar iemand mee binnen kan, dat een regel in het authenticatielog van de klant schrijft tussen zijn echte gebruikers. Bij een interval van een minuut zijn dat veertienhonderdveertig regels per dag die niets over zijn dienst zeggen, en op veel opstellingen is een fout wachtwoord een reden om een account te vergrendelen, waarmee de bewaking precies het incident veroorzaakt dat ze had moeten zien aankomen.

Toen hebben we gemeten wat een verkeerd shared secret doet, tegen een echte server.

Niets. Een server die de message authenticator van een verzoek niet kan verifiëren laat het pakket stil vallen, zonder foutantwoord. Dat staat in de specificatie, en het betekent dat het foutbeeld van een verkeerd secret niet te onderscheiden is van een firewallregel, een verkeerde poort of een dode machine.

Dat is in een adapter niet te repareren, want het is een eigenschap van het protocol. Wat je wél kunt doen is ophouden met doen alsof. De melding "geen antwoord" noemt een verkeerd shared secret als mogelijke oorzaak, in de tekst die de klant leest, zodat een beheerder geen middag in zijn firewall zoekt naar iets dat in een configuratiebestand staat. En antwoordt de server tóch, dan wordt de response authenticator geverifieerd, en een verschil daar wordt gemeld als een authenticatiefeit in plaats van een protocolfout, want in die opstelling is het dat werkelijk.

SituatieWat de klant zietWaarom
Fout secret, geen antwoordGeen antwoord, met het secret als mogelijke oorzaakDe server liet het pakket vallen; er kwam niets terug om te bekijken
Fout secret, er komt een antwoordEen fout secret, als authenticatie-uitslagDe response authenticator klopte niet
Goed secret, geen antwoordGeen antwoordDezelfde stilte, en die verdient dezelfde woorden

Die middelste rij is het enige geval waarin het verschil zichtbaar is, en het is niet het gewone geval.

Een server die niet te meten is, hardop gezegd

De laatste meting is de meting die ons het minst beviel.

SQL Server op Linux maakt bij zijn eerste start een certificaat. Dat certificaat is X.509 versie 1, een vorm zonder extensies die al lang niet meer voor TLS bedoeld is, en de certificaatparser eronder weigert hem. De versleutelde stand faalt dus met een niet-ondersteunde certificaatversie.

Het is goed om precies te zijn over waarom die stand dan nog iets verifieert. Identiteit wordt daar niet gewogen: de naam in het certificaat wordt niet gecontroleerd. De handtekening wél, want zonder die controle is er geen handshake meer, alleen een uitwisseling van bytes met wie er toevallig antwoordde. Om een handtekening te kunnen wegen moet de publieke sleutel uit het certificaat komen, en dat is de stap die weigert.

De uitkomst is dus: een SQL Server met zijn eigen gegenereerde certificaat is met deze controle niet te meten. De klant leest de werkelijke parserfout als detail, geciteerd in plaats van geparafraseerd, en de reparatie is een echt certificaat in de serverconfiguratie, wat een beheerder die zijn server bewaakbaar maakt toch al aan het doen is.

Twee dingen zijn bewust niet gedaan. De handtekeningcontrole wordt in die stand niet overgeslagen, want dat zou de belofte breken op precies de instelling die de meeste mensen kiezen en van een zichtbare weigering een onzichtbare zwakte maken. En er is nog geen eigen foutcode voor dit geval, omdat de enige manier om het te herkennen is: matchen op de menselijk leesbare fouttekst van een bibliotheek, en dat is een aanname over andermans code die stil kan verschuiven. Dat is een eigen ronde waard, met een eigen proef.

Er is ook geen stand helemaal zonder versleuteling, om een reden die eigen is aan dit protocol: het wachtwoord in het loginpakket wordt versluierd met een vaste exclusieve-of en een nibble-wissel. Dat is een codering, geen versleuteling. Wie de bytes ziet, leest het wachtwoord, dus een onversleutelde stand zou het wachtwoord van een klant in klare tekst over zijn netwerk sturen onder een naam die iets anders suggereert.

Wat de canary vond

Alles draaide tegen echte servers in plaats van stubs: veertien controles tegen MongoDB kaal en met TLS, een echte RADIUS-server en een echte SQL Server. De nuttigste uitslag is saai. Een fout wachtwoord is overal een authenticatiefout en nooit een protocolfout. Dat is het onderscheid dat een klant vertelt of hij naar zijn inloggegevens of naar zijn netwerk moet kijken, en een eerdere ronde had het in de PostgreSQL-controle fout: een fout tijdens de authenticatiestap werd vertaald naar "dit is geen PostgreSQL-server".

Wat we zouden opschrijven

  1. Tel wat een afhankelijkheid vasthoudt, niet alleen wat hij kost. Een generiek documenttype maakt van "wij lezen vier velden" een afspraak in plaats van een eigenschap.
  2. Een driver brengt een querybouwer mee. Is de belofte dat er geen willekeurige query verzonden kan worden, dan hoort de code om er één te sturen niet in de binary te bestaan.
  3. Twee TLS-stapels in één binary is een beleidsprobleem, geen omvangsprobleem. Die je niet gekozen hebt is die je niet kunt repareren.
  4. In een zelfgeschreven lezer is de overslagtabel het gevaarlijke deel. Een onbekend type moet een fout zijn; met nul bytes overslaan verschuift alles erna.
  5. Accepteer een getal in elke breedte die het protocol toestaat. Eén veld dat als double binnenkomt is genoeg om elke mislukking als succes te lezen.
  6. Een val-terug op een zwakker mechanisme hoort een weigering te zijn, geen val-terug. Groen boven een zwakkere inlog is erger dan rood.
  7. Maakt een protocol een fout ononderscheidbaar, zeg dat dan in de melding. De formulering is de enige plek waar dat onderscheid nog gemaakt kan worden.
  8. Sommige servers zijn niet te meten, en dat hoort in de documentatie en niet in een omweg. De omweg hier zou zijn geweest: stoppen met handtekeningen controleren.

Voor de duidelijkheid over wat dit is: de drie controles zijn gebouwd, tegen echte servers getoetst, en staan achter een rolloutschakelaar. Onbewezen blijft dat ze met zijn drieën over de draad naar een echte klantagent gaan, RADIUS-accounting voorbij het statusverzoek, en SQL Server in andere opstellingen dan één instantie op Linux.

Uit het platformwerk aan Provibr. Hoe dezelfde canary-aanpak twee defecten in de eerdere databasecontroles blootlegde, staat in een canary die tegen echte servers draait, en hoe de inloggegevens die deze controles gebruiken bij de agent komen, staat in een geheim dat één keer reist.

Begin met één host en één agent

Maak een account aan, installeer de agent met één commando en koppel je eerste Proxmox-host. De proefperiode loopt 14 dagen en vraagt geen betaalgegevens.