Simovits

Min kamp mot Nmap, och att alla skurkar börjar någonstans

I föregående blogg (skamlös reklam: https://simovits.com/de-inledande-grymheterna-efterforskningar-infor-sarbarhetsskanning/) diskuterades efterforskningar inför sårbarhetsskanningar. Framåt slutet av den bloggen nämndes Nmap och dess förmåga att ”ladda ner” TLS certifikat. I denna blogg avhandlas ett alternativ till detta sätt, som framförallt uppfyller två (möjligen tre) fördelar som alltid har hägrat. Dessa är:

* Modernt dataformat (Nmap producerar som ”bäst” XML som är redigt omständlig, speciellt i fallet med plugins)

* Portabilitet och frånvaron att behöva köra kommandon mot det underliggande operativsystemet

* Om möjligt, ökad hastighet jämfört mot Nmap, som just när det gäller somliga skript-funktioner kan vara tämligen långsam pga Lua.

Kombinationen av dessa tre önskemål innebär egentligen enbart alternativet att implementera detta själv i valt programmeringsspråk (med tillhörande inbyggda moduler för exekveringsmiljön).

I linje med tidigare blogg så använder vi JavaScript och Node.js. Det senare har den inbyggda modulen ”tls” som bland annat kan upprätta TLS anslutningar och kontrollera samt justera olika aspekter som rör dessa. Med det så behöver vi inte köra några kommandon mot underliggande OS. Punkt 1 uppfyller vi sedan genom att enbart arbete med JSON och att slänga bort onödig information så snabbt vi tar emot denna. Till sist försöker vi utnyttja det asynkroniserade beteendet i JavaScript och Node.js så gott som möjligt. Med det kan vi ha hoppet att slå Nmap, eftersom ”Nmap Scripting Engine (NSE)” företrädesvis använder sig av icke-optimerad Lua-kod.

Nedan följer koden som implementerar detta:

"use strict";
// Beroenden. Inget förutom grund Node.js saker och indata-fil
const tls = require('tls');
const util = require('util');ket som ger certifikatet!!
           let rawCert = tlsSocket.getPeerCertificate()
           // Nedan är till för att se att certifikatet är meningsfullt
           // och inte innehåller skräp som är ganska vanligt tyvärr
           let cnNameError = 0; // 0 = inget fel
           // Wildcard certifikat är farliga, och ger oss inte den infon vi vill ha just nu
           // Obs: det går att spara ner det också om man finner det 
const file = require("./test13_lägre_hastighet_6_fast_lite_snabbare.json"); // <-- Indata som exempel

// Våran huvudfunktion. Tar en samling mål.
// Notera att vi i princip skulle kunna kolla på någon annan port än 443
// eftersom TLS i princip kan vara förekommande på godtycklig port 
async function performOwncertScan(targets, port = 443) { 
  // Variabler för framtiden
  let certificate;
  let client;
  let options;

  // Eftersom tls paketet fortfarande främst använder sig av 
  // callbacks så är det bättre att slå in det i löftesstrukturen för att få
  // mer lätt överskådlig kod och även bättre prestanda vid flertalet anrop
  // Omslagsfunktionen tar ett mål och en port
  const getRawCert = async (target, port) => {
      // Ger tillbaka vårat löfte
      return new Promise ((resolve, reject) => {
        console.log("Examinig target: " + target);
        // Alternativ till socket och dess connect metod
        options = {
            host: target,
            port: port,
            rejectUnauthorized: false
        };
        // För resultatet
        let respondObject = {
         target  : target
        };
        // Här gör vi anslutningen med tls paketet
        // Detta är egentligen bara en socket med lite extra
        // saker.
        let tlsSocket = tls.connect(options, function () {
           // En metod för en aktiv tls socket som ger certifikatet!!
           let rawCert = tlsSocket.getPeerCertificate()
           // Nedan är till för att se att certifikatet är meningsfullt
           // och inte innehåller skräp som är ganska vanligt tyvärr
           let cnNameError = 0; // 0 = inget fel
           // Wildcard certifikat är farliga, och ger oss inte den infon               vi vill ha just nu
           // Obs: det går att spara ner det också om man finner det intressant
           // Vidare måste vi slänga allt som inte är ett riktigt domännamn (som enbart "pelle" typ)
           if (rawCert.subject.CN.includes("*") || !rawCert.subject.CN.includes(".")  ) {
             cnNameError = 1; // indikation på att något är galet med certifikatet
           }
           else {
             respondObject["CN"] = rawCert.subject.CN ; // Innehåller commonname, vilket ska vara namnet för målet 
           }
           // Nedan är samma resonemang som ovan.
           // Notera att altname fortfarande inte används av alla så därför måste man ta höjd för att något
           // kan gå galet här, att fältet inte finns helt enkelt också.
           let altNameError = 0; // som innan, fel = 1
           try {
             // Galna namn
             if (rawCert.subjectaltname.includes("*") || !rawCert.subjectaltname.includes(".")  ) {
               altNameError = 1; // fel pga galet namn
             }
             else {
               // Välj rätt fält och trimma bort oönskat 
               respondObject["subjectaltname"] = rawCert.subjectaltname.replace(/DNS:/g, "").split(", ") ; 
             }
           // Här kan vi hamna om det inte finns något altname  
           } catch (error) {
             altNameError = 1;
           }

           // Vad som görs när anslutningen är klar
           tlsSocket.end(() => {
               console.log("Client closed successfully");
             });

           // Om vi varken hittar något meningsfullt cm eller altname så är 
           // nog hela certifikatet dåligt och vi bortser från det
           if ( cnNameError == 1 && altNameError == 1 ) {
              reject({
               target : target,
               error : "Bad certificate..."
             })
           }
          // Vi förmedlar vidare det objektet som innehåller
          // namnen som vi har hittat för målet
          resolve(respondObject);
          })

          // Den här är viktig. Om det finns tjänster som lyssnar
          // på 443 men som inte är TLS relaterade så kan de få hela
          // socket:en att hänga sig. Så vi måste döda den
          // efter någon given tidpunk. Här testar vi på 20 sek, vilket borde
          // vara OK.
          tlsSocket.setTimeout(20000,function(){
            tlsSocket.end("");
            tlsSocket.destroy();
            // Eget felmeddelande
            reject({
              target : target,
              error : "Slow_res"
            })
          });
          // Även kontroll om socket får något uppenbart fel, exp en tjänst
          // som klarar av att säga ifrån mot TLS anslutningar som den inte förväntar sig
          tlsSocket.on('error', (error) => {
            reject(error)
          });
        })

   };
  // Nu kan vi dra fördel av att ha slagit in tls med dess återkallning i ett löfte.
  // Genom att använda list metoden map och kombinera den med en relativt ny löftesmetod "allSettled"
  // Går det att få en lista tillbaka med löften. Denna sammanlagda listan kan man då vänta på
  // och den kommer enbart ge svar när alla löften är klara, oavsett deras status!  
  const allCert = Promise.allSettled(targets.map(entry => getRawCert (entry, port) ) );
  return allCert;
};

// Main funktionen
(async () => {
  // Bearbetning av indata från exempel-fil, såsom resultat från masscan
  let targetsWithPorts = file.filter(itm => itm.ports[0].port == 443).map(itm => itm.ip);
   // Anrop av funktion, och filtrering av resultat för
   // framgångsrika och misslyckade
   // Vi undersöker de som var för långsamma seperat
   let result = await performOwncertScan(targetsWithPorts); // Notera att vi kan vänta på denna, vilket då ger hela listan! 
   let resultFulfilled = result.filter(entry => entry.status == "fulfilled");
   let resultRejected = result.filter(entry => entry.status == "rejected")
   let resultSlow = resultRejected.filter(entry => entry.reason.error == "Slow_res");

   // Utskrift
   console.log("-------------");
   console.log(JSON.stringify(resultFulfilled,null, 2));
   console.log("-------------");
   console.log(JSON.stringify(resultRejected,null, 2));
   console.log("-------------");
   console.log(JSON.stringify(resultSlow,null, 2));
   console.log("-------------");
   console.log("Number of targets scanned: " + targetsWithPorts.length);
})();

En simpel testkörning (där dessvärre all rolig information är borttagen sådant att ingen kan påstå att det finns känsliga uppgifter i bloggen) påvisar att koden innan är väsentligt snabbare än motsvarande Nmap körning. Exempelvis finner vi:

Node

Number of targets scanned: 133

real 0m20.160s

user 0m0.252s

sys 0m0.084s
Nmap

Nmap done: 133 IP addresses (133 hosts up) scanned in 639.92 seconds

real 10m39.940s

user 0m18.029s

sys 0m4.419s

Observera att Node körningen mycket begränsas av den tiden som sätts som timeout för sockets som inte svarar på TLS-anslutningar.

Det går självfallet att argumentera att Nmap kan bli snabbare än här genom att justera dess parametrar. Vid någon gräns går dock detta över till att bli en ökad komplexitet. Och då har ingen hänsyn tagits till dess resultat fil, som minst sagt är avskräckande oavsett format (även XML) men speciellt för detta plugin. Om önskan är att få resultatet på ett modernt och enkelt format samt att kunna flytta programmet utan behovet av att installera och köra saker mot det underliggande operativsystemet så är detta ett mycket bättre alternativ. Dessutom får vi större insikt och information om processen samt varför somliga anslutningar misslyckas. Som när på större Nmap optimeringar får vi även ett snabbare program.

Avslutningsvis har vi sett här att det ibland kan löna sig att implementera redan existerande verktyg/funktionalitet själv för att kunna få det exakt som man vill ha det. Kräsmagad kanske somliga kallar det.