From a6524586a9addc094885aef0277cb9a00db9d7bb Mon Sep 17 00:00:00 2001 From: "ellen.freeman" Date: Tue, 18 Nov 2025 17:09:03 +0530 Subject: [PATCH 1/5] added module to find deleted objects --- john.ccache | Bin 0 -> 1369 bytes nxc/modules/svc_ldap.ccache | Bin 0 -> 1370 bytes nxc/modules/tombfind.py | 193 ++++++++++++++++++++++++++++++++++++ 3 files changed, 193 insertions(+) create mode 100644 john.ccache create mode 100644 nxc/modules/svc_ldap.ccache create mode 100644 nxc/modules/tombfind.py diff --git a/john.ccache b/john.ccache new file mode 100644 index 0000000000000000000000000000000000000000..8e5f8ae4f5d75f29553f388c648c1f60086d88fc GIT binary patch literal 1369 zcmZQ&Vc=n4WZ?J@1Plu*D?e=1Zsl=mWIS8mKuX5mWl<;OpL4x1*Oq_vPi{12_(n3P})FJ8tNPjRg0Tg z%z&mCfm8}D1QJY(npjj=nphN+)@{4W{fG1W4l{x9mGK;(=B=0iCHlf%B6xG^r!I{> zueV>Bu(srS)yFo?^Tsc?&9&xl)M5G4zGLZ$-6sr|awIC}bw(Io@MehKUHId$Q0b+W zOhM@nAKqT|<80x%B?U#b2N>$UK2PWV_QSOOn?rWK*||FL71wwxxX$d=tBm?(9`d?7 zu=~uStkAlxKSILzEq2I!Fwj)r_+~HPcaKDYQ&+dYQM|x=CMb0F8$~nIne9OrGk+a) zox;@S%O9iDzG~*gxvpiKrbHA}Fi%)@w%`43YMt)oI-_Y$36Hln-TeRU^Ue6M4SC`X zOSRb|ujV|qP|aP(uy=mK+R2QS*N(Pt6+Wrd=lB0>C$o6{%&kT#Nue_mqumV8Y+0_s zaqMWY+OyS*#Y(lct*eiexHNPBD-C$*pSDHh!K}iJJzI_+o0Mr2tnYE?yRqc{jgOqu zb=aqc2fy@Y)G2?E7Vt`MQp@wUmic0z>rYQ@o^mDKCMl-krqi2Vt^N$1;woz)mI*&% zJU1J5PmMfmsl)j6s>y5juNM~_y1AcIT=jgCLWEA^lYM+gC(r#DwliGJrL6UFqtSoq zLz~`(9&ygDRp;8%C=zJ-LgL8^o#@JjmN#o&&wbb$u+aVj+nk7?7q?eAKV$cmh|AM_ z#eS!vz3ADV?nCnu9%ub8b+|dVf7_i zD)N!+lB09hZ@zbbpU~Wt9?6ot`*HmIyZ^<_u+imv*Yth|zhTiSInKV$?~j`P=~!8o zn^haleR()9LfzEaQ*nWl)`OSgZx=tx)68Rhs%^P>0{6Rbm-hXLXl>g5JL6er&!u|} zD`eM*c30T#Un`l*zL#^7Mt}Q`l_%r07yKwqPha;T?rh?N_$7KVO2$m0hmJ{C?mohQ z$31^aJl~$OKktG*ADtd~yRSHQrJvZB=0!2qi%z`KFVdSGRnYH~bDHT_dyUHXCyV5s z*GcV9$k|}J_IkJGM2`S<#;*dK&gwkgxifVC@qqaD<+>Knx=smQc{=GdyKzAcW7Ii@ zM=w6k5t@5(q2%JvCAQt_XBV-anU%k!SnLI#wc@Q5z7A*QBFC2Iluu_>{^dR2z3jrB zl;V%merU2=$De!1S%3QbG3$phtg-I<409&V5#p0{e6(IV##!&#>ZVPd%PL=YR5%NX zd0H(zwEIb{q2t|eM{;k4%DvaD&P~+Ri}#JlsbPs^n-OZgYH{)AE9v=D-f_HXQLbF2 v%E#_fH!D3-c|qXWC2x}F{WLL~k+5}v)4IdCY!fX1wJ%$7iJ^oKR1^UK{P$d? literal 0 HcmV?d00001 diff --git a/nxc/modules/svc_ldap.ccache b/nxc/modules/svc_ldap.ccache new file mode 100644 index 0000000000000000000000000000000000000000..dc91809094784f2c2dea73043155018714169fa2 GIT binary patch literal 1370 zcmZQ&Vc=n4WZ?J@1Pl6c}23vMf zQb~FVnz#^51QgoTh8PX2(`Uf5~jg!cfiLHU)S2CHh z>wYr?JOpZk1D4*zCYDZvCYII(%uI}|3wflGT)#-(Ko%s-xKPYML>lS{WCe?xSbTwc zJwb|u76J*TMNKT$EKMxt`zEdDbzqLJuFE_je(n5<-;cyBFMO!~JafW!2tHnPB_lUnxOsBJg>AL-T)Y!jC`uW3{fH1aw`8m0!Td$K zuP@!GR4=rzKOqnuB9kj+xnSk2PiOW@-~1$?o;=m;-SsuU+%$7T9Nbt-=Wm@fVfT!F zB{%EDwoAb?-#n|euh^7W`QlZ8;Prs~Bj5TecgD2UD+pR0uE;FE&cJBP*78hM@>qd; zVLOXR&n{;XZ;g%v8^pV~Kb{Zpxm?2E^1b%y63gFTUdG#n|NQklH$GAAV&|JzADq90 zpPJ;8#Py(&{qVEDYg?^Vs`C!XJM-E#`bm9Tb;Ynrp1)wuZLh8=kB$8OVdU^;p$fgvDMt1 z9{QC6KmW~ID0Svd1K0jyCw|oxH@Zxw%)XKQdv0F1?yE`PzZ!LYydLW6TlfEoljijj zKi)Gb50-bDicDFf<&^bnTZy*for9}ZPgdL7&db2M!MT6lt5eVQvi>|4bgNr)h~wOP zi{sziPW)%NVc^FZ)-Z!5V0nT5e$Ghx&u8tbOJUN;tgn-vu}8(Q^|$CQQ31DIHp|lXD%wtd zBXT-ncmHFr!+N!nN%i-BzG16#o)NY|`0Tn#_q=3Pj$Kf0Ew8IGtu=qIW4P8dl69H! z*VrM2;RGt z-RFbKyW5|-TvIO@U09pQc)RM1kz3EcLovl`uzi%#8Sowa|Vg)?3^zZ1*v_!Vp6%Se(tIC=8n7F O7yPA_&Mw{qDwhBa Date: Tue, 18 Nov 2025 17:09:36 +0530 Subject: [PATCH 2/5] added module to find deleted objects --- nxc/modules/svc_ldap.ccache | Bin 1370 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 nxc/modules/svc_ldap.ccache diff --git a/nxc/modules/svc_ldap.ccache b/nxc/modules/svc_ldap.ccache deleted file mode 100644 index dc91809094784f2c2dea73043155018714169fa2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1370 zcmZQ&Vc=n4WZ?J@1Pl6c}23vMf zQb~FVnz#^51QgoTh8PX2(`Uf5~jg!cfiLHU)S2CHh z>wYr?JOpZk1D4*zCYDZvCYII(%uI}|3wflGT)#-(Ko%s-xKPYML>lS{WCe?xSbTwc zJwb|u76J*TMNKT$EKMxt`zEdDbzqLJuFE_je(n5<-;cyBFMO!~JafW!2tHnPB_lUnxOsBJg>AL-T)Y!jC`uW3{fH1aw`8m0!Td$K zuP@!GR4=rzKOqnuB9kj+xnSk2PiOW@-~1$?o;=m;-SsuU+%$7T9Nbt-=Wm@fVfT!F zB{%EDwoAb?-#n|euh^7W`QlZ8;Prs~Bj5TecgD2UD+pR0uE;FE&cJBP*78hM@>qd; zVLOXR&n{;XZ;g%v8^pV~Kb{Zpxm?2E^1b%y63gFTUdG#n|NQklH$GAAV&|JzADq90 zpPJ;8#Py(&{qVEDYg?^Vs`C!XJM-E#`bm9Tb;Ynrp1)wuZLh8=kB$8OVdU^;p$fgvDMt1 z9{QC6KmW~ID0Svd1K0jyCw|oxH@Zxw%)XKQdv0F1?yE`PzZ!LYydLW6TlfEoljijj zKi)Gb50-bDicDFf<&^bnTZy*for9}ZPgdL7&db2M!MT6lt5eVQvi>|4bgNr)h~wOP zi{sziPW)%NVc^FZ)-Z!5V0nT5e$Ghx&u8tbOJUN;tgn-vu}8(Q^|$CQQ31DIHp|lXD%wtd zBXT-ncmHFr!+N!nN%i-BzG16#o)NY|`0Tn#_q=3Pj$Kf0Ew8IGtu=qIW4P8dl69H! z*VrM2;RGt z-RFbKyW5|-TvIO@U09pQc)RM1kz3EcLovl`uzi%#8Sowa|Vg)?3^zZ1*v_!Vp6%Se(tIC=8n7F O7yPA_&Mw{qDwhBa Date: Tue, 18 Nov 2025 17:32:31 +0530 Subject: [PATCH 3/5] Fix line-length for Ruff to comply with allowed max --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index f03162a3ad..ebdbc675ce 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -95,7 +95,7 @@ exclude = [ ".nox", ".pants.d", ".pytype", ".ruff_cache", ".svn", ".tox", ".venv", "__pypackages__", "_build", "buck-out", "build", "dist", "node_modules", "venv" ] -line-length = 65000 +line-length = 320 preview = true [tool.ruff.lint] From b72726252b5907261a490148bb25aea38625e006 Mon Sep 17 00:00:00 2001 From: "ellen.freeman" Date: Tue, 18 Nov 2025 17:47:32 +0530 Subject: [PATCH 4/5] Added Comments to tombfind.py --- nxc/modules/tombfind.py | 83 ++++++++++++++++++++++------------------- 1 file changed, 45 insertions(+), 38 deletions(-) diff --git a/nxc/modules/tombfind.py b/nxc/modules/tombfind.py index 505bfd8abe..c26e7b92ee 100644 --- a/nxc/modules/tombfind.py +++ b/nxc/modules/tombfind.py @@ -9,7 +9,6 @@ class NXCModule: category = CATEGORY.ENUMERATION def options(self, context, module_options): - """ ACTION Action to perform: find or restore (default: find) PAGE_SIZE Number of results per page for find operation (default: 10) @@ -19,6 +18,7 @@ def options(self, context, module_options): self.page_size = int(module_options.get("PAGE_SIZE", 10)) self.output_file = module_options.get("OUTPUT", None) + # Checks if an action is supplied def on_login(self, context, connection): try: if self.action == "find": @@ -26,15 +26,16 @@ def on_login(self, context, connection): else: context.log.error(f"Unknown action: {self.action}. Use 'find'") except Exception as e: - context.log.error(f"Module execution error: {str(e)}") + context.log.error(f"Module execution error: {e!s}") context.log.debug(f"Exception details: {e}", exc_info=True) + # Handles LDAP Authentication def _get_ldap_connection(self, context, connection): - if hasattr(connection, 'ldapConnection') and connection.ldapConnection: + if hasattr(connection, "ldapConnection") and connection.ldapConnection: context.log.debug("Using existing ldapConnection") return connection.ldapConnection - if hasattr(connection, 'conn') and connection.conn: + if hasattr(connection, "conn") and connection.conn: context.log.debug("Using existing conn") return connection.conn @@ -44,30 +45,30 @@ def _get_ldap_connection(self, context, connection): ldap_conn = ldap.LDAPConnection(ldap_url) try: - if hasattr(connection, 'kerberos') and connection.kerberos: - if connection.password and connection.password != '': + if hasattr(connection, "kerberos") and connection.kerberos: + if connection.password and connection.password != "": ldap_conn.kerberosLogin( user=connection.username, password=connection.password, domain=connection.domain, - lmhash='', - nthash='' + lmhash="", + nthash="" ) else: ldap_conn.kerberosLogin( user=connection.username, - password='', + password="", domain=connection.domain, - lmhash='', - nthash='' + lmhash="", + nthash="" ) - elif hasattr(connection, 'nthash') and connection.nthash: + elif hasattr(connection, "nthash") and connection.nthash: context.log.debug("Authenticating with NTLM hash") ldap_conn.login( user=connection.username, - password='', + password="", domain=connection.domain, - lmhash='aad3b435b51404eeaad3b435b51404ee', + lmhash="aad3b435b51404eeaad3b435b51404ee", nthash=connection.nthash ) else: @@ -88,33 +89,38 @@ def _get_ldap_connection(self, context, connection): def find_deleted_objects(self, context, connection): try: - domain_parts = connection.domain.split('.') + # Gets the domain from the connection Object + domain_parts = connection.domain.split(".") + # Builds the Search Base search_base = f'CN=Deleted Objects,DC={",DC=".join(domain_parts)}' + # Defines the search filter search_filter = ( "(&(|(objectClass=User)" "(objectCategory=Computer))" "(isDeleted=TRUE))") + # Defines the attributes to be fetched attributes = [ - 'cn', - 'sAMAccountName', - 'objectClass', - 'lastKnownParent' + "cn", + "sAMAccountName", + "objectClass", + "lastKnownParent" ] - context.log.info("Searching for deleted objects...") + context.log.info("[*] Searching for deleted objects...") show_deleted_control = ldapasn1.Control() - show_deleted_control['controlType'] = ldapasn1.LDAPOID( - '1.2.840.113556.1.4.417' + show_deleted_control["controlType"] = ldapasn1.LDAPOID( + "1.2.840.113556.1.4.417" ) - show_deleted_control['criticality'] = True + show_deleted_control["criticality"] = True ldap_conn = self._get_ldap_connection(context, connection) entry_list = [] - cookie = b'' + cookie = b"" while True: + # Defines the number of Pages returned per query [default: 10] paging_control = ldapasn1.SimplePagedResultsControl( criticality=False, size=self.page_size, @@ -122,15 +128,16 @@ def find_deleted_objects(self, context, connection): ) try: + # Initiates the search resp = ldap_conn.search( searchBase=search_base, searchFilter=search_filter, - scope=ldapasn1.Scope('wholeSubtree'), + scope=ldapasn1.Scope("wholeSubtree"), attributes=attributes, searchControls=[show_deleted_control, paging_control] ) except Exception as e: - context.log.error(f"Search error: {str(e)}") + context.log.error(f"Search error: {e!s}") break for item in resp: @@ -155,12 +162,12 @@ def find_deleted_objects(self, context, connection): if not attrs: continue - - cn = attrs.get('cn', [''])[0] - guid = cn.split('\n')[1].split(':')[1] - ou = attrs.get('lastKnownParent', [''])[0] - sam = attrs.get('sAMAccountName', [''])[0] - obj_class = attrs.get('objectClass', [''])[-1] + # Filters attributes for each object returned from the query + cn = attrs.get("cn", [""])[0] + guid = cn.split("\n")[1].split(":")[1] + ou = attrs.get("lastKnownParent", [""])[0] + sam = attrs.get("sAMAccountName", [""])[0] + obj_class = attrs.get("objectClass", [""])[-1] result_str = f"[{obj_class}] {sam} | GUID: {guid} | OU: {ou}" context.log.highlight(result_str) @@ -169,24 +176,24 @@ def find_deleted_objects(self, context, connection): results.append(f"{sam},{guid},{ou},{obj_class}") if self.output_file: try: - with open(self.output_file, 'w') as f: + with open(self.output_file, "w") as f: f.write("\n".join(str(result) for result in results) + "\n") context.log.success( f"Results saved to {self.output_file}" ) except Exception as e: - context.log.error(f"Failed to write to file: {str(e)}") + context.log.error(f"Failed to write to file: {e!s}") except Exception as e: - context.log.error(f"Error in find operation: {str(e)}") + context.log.error(f"Error in find operation: {e!s}") context.log.debug(f"Exception details: {e}", exc_info=True) def _parse_entry_attributes(self, entry): attrs = {} try: - for attr in entry['attributes']: - attr_name = str(attr['type']) - attr_values = [str(val) for val in attr['vals']] + for attr in entry["attributes"]: + attr_name = str(attr["type"]) + attr_values = [str(val) for val in attr["vals"]] attrs[attr_name] = attr_values except Exception: pass From d5977ff7262b0b2fb24e20b2642afd9caeaa7a17 Mon Sep 17 00:00:00 2001 From: "ellen.freeman" Date: Tue, 18 Nov 2025 17:49:29 +0530 Subject: [PATCH 5/5] Removed testing ccache file --- john.ccache | Bin 1369 -> 0 bytes nxc/helpers/powershell.py | 2 +- nxc/protocols/winrm/database.py | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) delete mode 100644 john.ccache diff --git a/john.ccache b/john.ccache deleted file mode 100644 index 8e5f8ae4f5d75f29553f388c648c1f60086d88fc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1369 zcmZQ&Vc=n4WZ?J@1Plu*D?e=1Zsl=mWIS8mKuX5mWl<;OpL4x1*Oq_vPi{12_(n3P})FJ8tNPjRg0Tg z%z&mCfm8}D1QJY(npjj=nphN+)@{4W{fG1W4l{x9mGK;(=B=0iCHlf%B6xG^r!I{> zueV>Bu(srS)yFo?^Tsc?&9&xl)M5G4zGLZ$-6sr|awIC}bw(Io@MehKUHId$Q0b+W zOhM@nAKqT|<80x%B?U#b2N>$UK2PWV_QSOOn?rWK*||FL71wwxxX$d=tBm?(9`d?7 zu=~uStkAlxKSILzEq2I!Fwj)r_+~HPcaKDYQ&+dYQM|x=CMb0F8$~nIne9OrGk+a) zox;@S%O9iDzG~*gxvpiKrbHA}Fi%)@w%`43YMt)oI-_Y$36Hln-TeRU^Ue6M4SC`X zOSRb|ujV|qP|aP(uy=mK+R2QS*N(Pt6+Wrd=lB0>C$o6{%&kT#Nue_mqumV8Y+0_s zaqMWY+OyS*#Y(lct*eiexHNPBD-C$*pSDHh!K}iJJzI_+o0Mr2tnYE?yRqc{jgOqu zb=aqc2fy@Y)G2?E7Vt`MQp@wUmic0z>rYQ@o^mDKCMl-krqi2Vt^N$1;woz)mI*&% zJU1J5PmMfmsl)j6s>y5juNM~_y1AcIT=jgCLWEA^lYM+gC(r#DwliGJrL6UFqtSoq zLz~`(9&ygDRp;8%C=zJ-LgL8^o#@JjmN#o&&wbb$u+aVj+nk7?7q?eAKV$cmh|AM_ z#eS!vz3ADV?nCnu9%ub8b+|dVf7_i zD)N!+lB09hZ@zbbpU~Wt9?6ot`*HmIyZ^<_u+imv*Yth|zhTiSInKV$?~j`P=~!8o zn^haleR()9LfzEaQ*nWl)`OSgZx=tx)68Rhs%^P>0{6Rbm-hXLXl>g5JL6er&!u|} zD`eM*c30T#Un`l*zL#^7Mt}Q`l_%r07yKwqPha;T?rh?N_$7KVO2$m0hmJ{C?mohQ z$31^aJl~$OKktG*ADtd~yRSHQrJvZB=0!2qi%z`KFVdSGRnYH~bDHT_dyUHXCyV5s z*GcV9$k|}J_IkJGM2`S<#;*dK&gwkgxifVC@qqaD<+>Knx=smQc{=GdyKzAcW7Ii@ zM=w6k5t@5(q2%JvCAQt_XBV-anU%k!SnLI#wc@Q5z7A*QBFC2Iluu_>{^dR2z3jrB zl;V%merU2=$De!1S%3QbG3$phtg-I<409&V5#p0{e6(IV##!&#>ZVPd%PL=YR5%NX zd0H(zwEIb{q2t|eM{;k4%DvaD&P~+Ri}#JlsbPs^n-OZgYH{)AE9v=D-f_HXQLbF2 v%E#_fH!D3-c|qXWC2x}F{WLL~k+5}v)4IdCY!fX1wJ%$7iJ^oKR1^UK{P$d? diff --git a/nxc/helpers/powershell.py b/nxc/helpers/powershell.py index 7f3bb73144..84ae0a03c8 100644 --- a/nxc/helpers/powershell.py +++ b/nxc/helpers/powershell.py @@ -137,7 +137,7 @@ def create_ps_command(ps_command, force_ps32=False, obfs=False, custom_amsi=None amsi_bypass = "" # for readability purposes, we do not do a one-liner - if force_ps32: # noqa: SIM108 + if force_ps32: # https://stackoverflow.com/a/60155248 command = amsi_bypass + f"$functions = {{function Command-ToExecute{{{amsi_bypass + ps_command}}}}}; if ($Env:PROCESSOR_ARCHITECTURE -eq 'AMD64'){{$job = Start-Job -InitializationScript $functions -ScriptBlock {{Command-ToExecute}} -RunAs32; $job | Wait-Job | Receive-Job }} else {{IEX '$functions'; Command-ToExecute}}" else: diff --git a/nxc/protocols/winrm/database.py b/nxc/protocols/winrm/database.py index 7a54d3b829..ebd6393038 100644 --- a/nxc/protocols/winrm/database.py +++ b/nxc/protocols/winrm/database.py @@ -198,7 +198,7 @@ def add_admin_user(self, credtype, domain, username, password, host, user_id=Non add_links = [] creds_q = select(self.UsersTable) - if user_id: # noqa: SIM108 + if user_id: creds_q = creds_q.filter(self.UsersTable.c.id == user_id) else: creds_q = creds_q.filter(