Skip to content

Commit c25640c

Browse files
authored
Merge branch 'Pennyw0rth:main' into dc-list
2 parents c87d0c2 + 69c1137 commit c25640c

19 files changed

Lines changed: 888 additions & 161 deletions

.github/PULL_REQUEST_TEMPLATE.md

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,20 @@
11
## Description
22

33
Please include a summary of the change and which issue is fixed, or what the enhancement does.
4-
Please also include relevant motivation and context.
54
List any dependencies that are required for this change.
65

76
## Type of change
8-
Please delete options that are not relevant.
97
- [ ] Bug fix (non-breaking change which fixes an issue)
108
- [ ] New feature (non-breaking change which adds functionality)
119
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
1210
- [ ] This change requires a documentation update
1311
- [ ] This requires a third party update (such as Impacket, Dploot, lsassy, etc)
1412

15-
## How Has This Been Tested?
16-
Please describe the tests that you ran to verify your changes (e2e, single commands, etc)
17-
Please also list any relevant details for your test configuration, such as your locally running machine Python version & OS, as well as the target(s) you tested against, including software versions
18-
19-
If you are using poetry, you can easily run tests via:
20-
`poetry run python tests/e2e_tests.py -t $TARGET -u $USER -p $PASSWORD`
21-
There are additional options like `--errors` to display ALL errors (some may not be failures), `--poetry` (output will include the poetry run prepended), `--line-num $START-$END $SINGLE` for only running a subset
13+
## Setup guide for the review
14+
Please provide guidance on what setup is needed to test the introduced changes, such as your locally running machine Python version & OS, as well as the target(s) you tested against, including software versions.
15+
In particular:
16+
- Bug Fix: Please provide a short description on how to trigger the bug, to make the bug reproducable for the reviewer.
17+
- Added Feature/Enhancement: Please specify what setup is needed in order to test the changes. E.g. is additional software needed? GPO changes required? Specific registry settings that need to be changed?
2218

2319
## Screenshots (if appropriate):
2420
Screenshots are always nice to have and can give a visual representation of the change.
@@ -29,8 +25,7 @@ If appropriate include before and after screenshot(s) to show which results are
2925
- [ ] I have ran Ruff against my changes (via poetry: `poetry run python -m ruff check . --preview`, use `--fix` to automatically fix what it can)
3026
- [ ] I have added or updated the tests/e2e_commands.txt file if necessary
3127
- [ ] New and existing e2e tests pass locally with my changes
32-
- [ ] My code follows the style guidelines of this project (should be covered by Ruff above)
33-
- [ ] If reliant on third party dependencies, such as Impacket, dploot, lsassy, etc, I have linked the relevant PRs in those projects
28+
- [ ] If reliant on changes of third party dependencies, such as Impacket, dploot, lsassy, etc, I have linked the relevant PRs in those projects
3429
- [ ] I have performed a self-review of my own code
3530
- [ ] I have commented my code, particularly in hard-to-understand areas
3631
- [ ] I have made corresponding changes to the documentation (PR here: https://github.com/Pennyw0rth/NetExec-Wiki)

nxc/helpers/even6_parser.py

Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
# SPDX-License-Identifier: GPL-2.0+
2+
3+
import struct
4+
import uuid
5+
6+
from datetime import datetime
7+
8+
class Substitution:
9+
def __init__(self, buf, offset):
10+
(sub_token, sub_id, sub_type) = struct.unpack_from("<BHB", buf, offset)
11+
self.length = 4
12+
13+
self._id = sub_id
14+
self._type = sub_type
15+
self._optional = sub_token == 0x0e
16+
17+
def xml(self, template=None):
18+
value = template.values[self._id]
19+
if value.type == 0x0:
20+
return None if self._optional else ""
21+
if self._type == 0x1:
22+
return value.data.decode("utf16")
23+
elif self._type == 0x4:
24+
return str(struct.unpack("<B", value.data)[0])
25+
elif self._type == 0x6:
26+
return str(struct.unpack("<H", value.data)[0])
27+
elif self._type == 0x8:
28+
return str(struct.unpack("<I", value.data)[0])
29+
elif self._type == 0xa:
30+
return str(struct.unpack("<Q", value.data)[0])
31+
elif self._type == 0x11:
32+
# see http://integriography.wordpress.com/2010/01/16/using-phython-to-parse-and-present-windows-64-bit-timestamps/
33+
return datetime.utcfromtimestamp(struct.unpack("<Q", value.data)[0] / 1e7 - 11644473600).isoformat()
34+
elif self._type == 0x13:
35+
# see http://www.gossamer-threads.com/lists/apache/bugs/386930
36+
revision, number_of_sub_ids = struct.unpack_from("<BB", value.data)
37+
iav = struct.unpack_from(">Q", value.data, 2)[0]
38+
sub_ids = [struct.unpack("<I", value.data[8 + 4 * i:12 + 4 * i])[0] for i in range(number_of_sub_ids)]
39+
return "S-{}-{}-{}".format(revision, iav, "-".join([str(sub_id) for sub_id in sub_ids]))
40+
elif self._type == 0x15 or self._type == 0x10:
41+
return value.data.hex()
42+
elif self._type == 0x21:
43+
return value.template.xml()
44+
elif self._type == 0xf:
45+
return str(uuid.UUID(bytes_le=value.data))
46+
else:
47+
print("Unknown value type", hex(value.type))
48+
49+
class Value:
50+
def __init__(self, buf, offset):
51+
token, string_type, length = struct.unpack_from("<BBH", buf, offset)
52+
self._val = buf[offset + 4:offset + 4 + length * 2].decode("utf16")
53+
54+
self.length = 4 + length * 2
55+
56+
def xml(self, template=None):
57+
return self._val
58+
59+
class Attribute:
60+
def __init__(self, buf, offset):
61+
struct.unpack_from("<B", buf, offset)
62+
self._name = Name(buf, offset + 1)
63+
64+
(next_token) = struct.unpack_from("<B", buf, offset + 1 + self._name.length)
65+
if next_token[0] == 0x05 or next_token == 0x45:
66+
self._value = Value(buf, offset + 1 + self._name.length)
67+
elif next_token[0] == 0x0e:
68+
self._value = Substitution(buf, offset + 1 + self._name.length)
69+
else:
70+
print("Unknown attribute next_token", hex(next_token[0]), hex(offset + 1 + self._name.length))
71+
72+
self.length = 1 + self._name.length + self._value.length
73+
74+
def xml(self, template=None):
75+
val = self._value.xml(template)
76+
return None if val is None else f'{self._name.val}="{val}"'
77+
78+
class Name:
79+
def __init__(self, buf, offset):
80+
hashs, length = struct.unpack_from("<HH", buf, offset)
81+
82+
self.val = buf[offset + 4:offset + 4 + length * 2].decode("utf16")
83+
self.length = 4 + (length + 1) * 2
84+
85+
class Element:
86+
def __init__(self, buf, offset):
87+
token, dependency_id, length = struct.unpack_from("<BHI", buf, offset)
88+
89+
self._name = Name(buf, offset + 7)
90+
self._dependency = dependency_id
91+
92+
ofs = offset + 7 + self._name.length
93+
if token == 0x41:
94+
struct.unpack_from("<I", buf, ofs)
95+
ofs += 4
96+
97+
self._children = []
98+
self._attributes = []
99+
100+
while True:
101+
next_token = buf[ofs]
102+
if next_token == 0x06 or next_token == 0x46:
103+
attr = Attribute(buf, ofs)
104+
self._attributes.append(attr)
105+
ofs += attr.length
106+
elif next_token == 0x02:
107+
self._empty = False
108+
ofs += 1
109+
while True:
110+
next_token = buf[ofs]
111+
if next_token == 0x01 or next_token == 0x41:
112+
element = Element(buf, ofs)
113+
elif next_token == 0x04:
114+
ofs += 1
115+
break
116+
elif next_token == 0x05:
117+
element = Value(buf, ofs)
118+
elif next_token == 0x0e or next_token == 0x0d:
119+
element = Substitution(buf, ofs)
120+
else:
121+
print("Unknown intern next_token", hex(next_token), hex(ofs))
122+
break
123+
124+
self._children.append(element)
125+
ofs += element.length
126+
127+
break
128+
elif next_token == 0x03:
129+
self._empty = True
130+
ofs += 1
131+
break
132+
else:
133+
print("Unknown element next_token", hex(next_token), hex(ofs))
134+
break
135+
136+
self.length = ofs - offset
137+
138+
def xml(self, template=None):
139+
if self._dependency != 0xFFFF and template.values[self._dependency].type == 0x00:
140+
return ""
141+
142+
attrs = filter(lambda x: x is not None, (x.xml(template) for x in self._attributes))
143+
144+
attrs = " ".join(attrs)
145+
if len(attrs) > 0:
146+
attrs = " " + attrs
147+
148+
if self._empty:
149+
return f"<{self._name.val}{attrs}/>"
150+
else:
151+
children = (x.xml(template) for x in self._children)
152+
return "<{}{}>{}</{}>".format(self._name.val, attrs, "".join(children), self._name.val)
153+
154+
class ValueSpec:
155+
def __init__(self, buf, offset, value_offset):
156+
self.length, self.type, value_eof = struct.unpack_from("<HBB", buf, offset)
157+
self.data = buf[value_offset:value_offset + self.length]
158+
159+
if self.type == 0x21:
160+
self.template = BinXML(buf, value_offset)
161+
162+
class TemplateInstance:
163+
def __init__(self, buf, offset):
164+
token, unknown0, guid, length, next_token = struct.unpack_from("<BB16sIB", buf, offset)
165+
if next_token == 0x0F:
166+
self._xml = BinXML(buf, offset + 0x16)
167+
eof, num_values = struct.unpack_from("<BI", buf, offset + 22 + self._xml.length)
168+
values_length = 0
169+
self.values = []
170+
for x in range(num_values):
171+
value = ValueSpec(buf, offset + 22 + self._xml.length + 5 + x * 4, offset + 22 + self._xml.length + 5 + num_values * 4 + values_length)
172+
self.values.append(value)
173+
values_length += value.length
174+
175+
self.length = 22 + self._xml.length + 5 + num_values * 4 + values_length
176+
else:
177+
print("Unknown template token", hex(next_token))
178+
179+
def xml(self, template=None):
180+
return self._xml.xml(self)
181+
182+
class BinXML:
183+
def __init__(self, buf, offset):
184+
header_token, major_version, minor_version, flags, next_token = struct.unpack_from("<BBBBB", buf, offset)
185+
186+
if next_token == 0x0C:
187+
self._element = TemplateInstance(buf, offset + 4)
188+
elif next_token == 0x01 or next_token == 0x41:
189+
self._element = Element(buf, offset + 4)
190+
else:
191+
print("Unknown binxml token", hex(next_token))
192+
193+
self.length = 4 + self._element.length
194+
195+
def xml(self, template=None):
196+
return self._element.xml(template)
197+
198+
class ResultSet:
199+
def __init__(self, buf):
200+
total_size, header_size, event_offset, bookmark_offset, binxml_size = struct.unpack_from("<IIIII", buf)
201+
self._xml = BinXML(buf, 0x14)
202+
203+
def xml(self):
204+
return self._xml.xml()

nxc/modules/enable_cmdshell.py

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
class NXCModule:
2+
"""
3+
Enables or disables xp_cmdshell in MSSQL Server.
4+
Module by crosscutsaw
5+
"""
6+
7+
name = "enable_cmdshell"
8+
description = "Enable or disable xp_cmdshell in MSSQL Server"
9+
supported_protocols = ["mssql"]
10+
opsec_safe = False
11+
multiple_hosts = True
12+
13+
def __init__(self):
14+
self.mssql_conn = None
15+
self.context = None
16+
self.action = None
17+
self.advanced_options_backup = None # Stores original value of 'show advanced options'
18+
19+
def options(self, context, module_options):
20+
"""
21+
ACTION enable or disable xp_cmdshell
22+
23+
Examples
24+
--------
25+
netexec mssql $TARGET -u $username -p $password -M enable_cmdshell -o ACTION=enable
26+
netexec mssql $TARGET -u $username -p $password -M enable_cmdshell -o ACTION=disable
27+
"""
28+
if "ACTION" in module_options:
29+
self.action = module_options["ACTION"].lower()
30+
else:
31+
context.log.fail("Missing required option: ACTION (enable/disable)")
32+
exit(1)
33+
34+
def on_login(self, context, connection):
35+
self.context = context
36+
self.mssql_conn = connection.conn
37+
38+
if self.action == "enable":
39+
self.toggle_xp_cmdshell(enable=True)
40+
elif self.action == "disable":
41+
self.toggle_xp_cmdshell(enable=False)
42+
else:
43+
self.context.log.fail("Invalid ACTION. Use 'enable' or 'disable'.")
44+
45+
def backup_show_advanced_options(self):
46+
"""Backs up the current state of 'show advanced options'."""
47+
query = "SELECT CAST(value AS INT) AS value FROM sys.configurations WHERE name = 'show advanced options'"
48+
res = self.mssql_conn.sql_query(query)
49+
if res:
50+
self.advanced_options_backup = int(res[0]["value"]) # Convert to integer
51+
52+
def restore_show_advanced_options(self):
53+
"""Restores the original state of 'show advanced options' if needed."""
54+
if self.advanced_options_backup is not None and self.advanced_options_backup == 0:
55+
self.mssql_conn.sql_query("EXEC sp_configure 'show advanced options', '0'; RECONFIGURE;")
56+
57+
def toggle_xp_cmdshell(self, enable: bool):
58+
"""Enables or disables xp_cmdshell while preserving 'show advanced options' state."""
59+
state = "1" if enable else "0"
60+
61+
# Backup 'show advanced options' state
62+
self.backup_show_advanced_options()
63+
64+
# Enable 'show advanced options' if it was disabled
65+
self.mssql_conn.sql_query("EXEC sp_configure 'show advanced options', '1'; RECONFIGURE;")
66+
67+
try:
68+
# Enable or disable xp_cmdshell
69+
self.mssql_conn.sql_query(f"EXEC sp_configure 'xp_cmdshell', '{state}'; RECONFIGURE;")
70+
action_text = "enabled" if enable else "disabled"
71+
self.context.log.success(f"xp_cmdshell successfully {action_text}.")
72+
except Exception as e:
73+
self.context.log.fail(f"Failed to execute command: {e}")
74+
75+
# Restore 'show advanced options' to its original state if needed
76+
self.restore_show_advanced_options()

0 commit comments

Comments
 (0)