Skip to content

Commit ee14874

Browse files
committed
Merge upstream changes
2 parents 28bbf9d + b77744f commit ee14874

11 files changed

Lines changed: 203 additions & 49 deletions

File tree

.github/ISSUE_TEMPLATE/config.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
blank_issues_enabled: false
2+
contact_links:
3+
- name: NetExec Wiki
4+
url: https://www.netexec.wiki/
5+
about: Check the wiki for usage guides and documentation before opening an issue.
6+
- name: NetExec Discord
7+
url: https://discord.com/invite/pjwUTQzg8R
8+
about: Join the Discord for general questions and community support.

.github/PULL_REQUEST_TEMPLATE.md

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
## Description
2-
32
Please include a summary of the change and which issue is fixed, or what the enhancement does.
43
List any dependencies that are required for this change.
54

5+
If you have used AI in any form, please state the tool you used (e.g. Claude Code, Cursor, Amp) along with the extent that the work was AI-assisted. See the project's AI policy for more details: https://github.com/Pennyw0rth/NetExec/blob/main/AI_POLICY.md
6+
67
## Type of change
78
Insert an "x" inside the brackets for relevant items (do not delete options)
89

@@ -12,24 +13,28 @@ Insert an "x" inside the brackets for relevant items (do not delete options)
1213
- [ ] Deprecation of feature or functionality
1314
- [ ] This change requires a documentation update
1415
- [ ] This requires a third party update (such as Impacket, Dploot, lsassy, etc)
16+
- [ ] This PR was created with the assistance of AI (list what type of assistance, tool(s)/model(s) in the description)
1517

1618
## Setup guide for the review
1719
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.
1820
In particular:
1921
- Bug Fix: Please provide a short description on how to trigger the bug, to make the bug reproducable for the reviewer.
20-
- 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?
22+
- Added Feature/Enhancement: Please specify what setup is needed in order to test the changes, such as:
23+
- Is additional software needed?
24+
- GPO changes required?
25+
- Specific registry settings that need to be changed?
2126

2227
## Screenshots (if appropriate):
2328
Screenshots are always nice to have and can give a visual representation of the change.
24-
If appropriate include before and after screenshot(s) to show which results are to be expected.
29+
If appropriate, include before and after screenshot(s) to show which results are to be expected.
2530

2631
## Checklist:
2732
Insert an "x" inside the brackets for completed and relevant items (do not delete options)
2833

29-
- [ ] I have ran Ruff against my changes (via poetry: `poetry run python -m ruff check . --preview`, use `--fix` to automatically fix what it can)
34+
- [ ] I have ran Ruff against my changes (poetry: `poetry run ruff check .`, use `--fix` to automatically fix what it can)
3035
- [ ] I have added or updated the `tests/e2e_commands.txt` file if necessary (new modules or features are _required_ to be added to the e2e tests)
31-
- [ ] New and existing e2e tests pass locally with my changes
3236
- [ ] If reliant on changes of third party dependencies, such as Impacket, dploot, lsassy, etc, I have linked the relevant PRs in those projects
33-
- [ ] I have performed a self-review of my own code
37+
- [ ] I have linked relevant sources that describes the added technique (blog posts, documentation, etc)
38+
- [ ] I have performed a self-review of my own code (_not_ an AI review)
3439
- [ ] I have commented my code, particularly in hard-to-understand areas
3540
- [ ] I have made corresponding changes to the documentation (PR here: https://github.com/Pennyw0rth/NetExec-Wiki)
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
name: PR Template Check
2+
3+
on:
4+
pull_request:
5+
types: [opened, edited]
6+
7+
permissions:
8+
pull-requests: write
9+
10+
jobs:
11+
check-template:
12+
runs-on: ubuntu-latest
13+
permissions:
14+
pull-requests: write
15+
steps:
16+
- name: Check PR description for template sections
17+
uses: actions/github-script@v7
18+
with:
19+
script: |
20+
const body = context.payload.pull_request.body || '';
21+
const requiredSections = [
22+
'## Description',
23+
'## Type of change',
24+
'## Setup guide for the review',
25+
'## Checklist'
26+
];
27+
28+
const missingSections = requiredSections.filter(
29+
section => !body.includes(section)
30+
);
31+
32+
if (missingSections.length === 0) return;
33+
34+
// Check if we already left a comment to avoid spamming
35+
const comments = await github.rest.issues.listComments({
36+
owner: context.repo.owner,
37+
repo: context.repo.repo,
38+
issue_number: context.payload.pull_request.number
39+
});
40+
41+
const botComment = comments.data.find(
42+
c => c.user.type === 'Bot' && c.body.includes('<!-- pr-template-check -->')
43+
);
44+
45+
if (botComment) return;
46+
47+
const missing = missingSections.map(s => `- ${s}`).join('\n');
48+
49+
await github.rest.issues.createComment({
50+
owner: context.repo.owner,
51+
repo: context.repo.repo,
52+
issue_number: context.payload.pull_request.number,
53+
body: `<!-- pr-template-check -->\nIt looks like the PR template may not have been filled out. The following sections appear to be missing:\n\n${missing}\n\nPlease edit your PR description to include them. The template helps reviewers understand and test your changes. Thanks!`
54+
});

AI_POLICY.md

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
# AI Usage Policy
2+
3+
This policy was adapted from the [ghostty project](https://github.com/ghostty-org/ghostty/).
4+
The original version can be found [Ghostty's PR #10412](https://github.com/ghostty-org/ghostty/pull/10412).
5+
6+
The NetExec project has strict rules for AI usage:
7+
8+
- **All AI usage in any form must be disclosed.** You must state
9+
the tool(s) and model(s) you used (e.g. Claude Code, Cursor, Opus 4.6,
10+
Codex 5.2, etc) along with the extent that the work was AI-assisted.
11+
12+
- **Pull requests created in any way by AI can only be for accepted issues.**
13+
Drive-by pull requests that do not reference an accepted issue may be
14+
rejected and closed. If AI isn't disclosed but a maintainer suspects its use,
15+
the PR may be rejected and closed. If you want to share code for a
16+
non-accepted issue, open a discussion or attach it to the existing issue.
17+
18+
- **Pull requests created by AI must have been fully verified with
19+
human use.** AI must not create hypothetically correct code that
20+
hasn't been tested. Importantly, you must not allow AI to write
21+
code for platforms or environments you don't have access to manually
22+
test on.
23+
24+
- **Issues and discussions can use AI assistance but must have a full
25+
human-in-the-loop.** This means that any content generated with AI
26+
must have been reviewed _and edited_ by a human before submission.
27+
AI is very good at being overly verbose and including noise that
28+
distracts from the main point. Humans must do their research and
29+
trim this down.
30+
31+
- **No AI-generated media is allowed (art, images, videos, audio, etc.).**
32+
Text and code are the only acceptable AI-generated content, per the
33+
other rules in this policy.
34+
35+
- **Bad AI drivers will be banned** You've been warned. We love to help junior
36+
developers learn and grow, but if you're interested in that then don't use
37+
AI, and we'll help you.
38+
39+
- **Official maintainers have the final say** We always strive to be helpful,
40+
but there are limits. If you submit agregiously terrible AI generated code
41+
with no review, we may ban you without word. We do not want to waste our
42+
time reviewing slop if the contributor can't be bothered to review the work
43+
themselves.
44+
45+
These rules apply only to outside contributions to NetExec. Maintainers
46+
and trusted contributors are exempt from these rules and may use AI tools at
47+
their discretion; they've proven themselves trustworthy to apply good judgment.
48+
49+
## There are Humans Here
50+
51+
Please remember that NetExec is maintained by humans.
52+
53+
Every discussion, issue, and pull request is read and reviewed by
54+
humans (and sometimes machines, too). It is a boundary point at which
55+
people interact with each other and the work done. It is rude and
56+
disrespectful to approach this boundary with low-effort, unqualified
57+
work, since it puts the burden of validation on the maintainers.
58+
59+
In a perfect world, AI would produce high-quality, accurate work
60+
every time, but today that is simply not true. This is compounded by the fact
61+
that accessibility to AI is high, allowing low skilled individuals to think
62+
that they are contributing useful code. Even many skilled programmers do not
63+
understand how to use it effectively. This has opened up a waterfall of low
64+
quality contributions across the Open Source community, wasting resources.
65+
66+
## AI is Welcome Here, Within Reason
67+
68+
NetExec maintainers acknowledge AI as a productive tool to some workflows, and
69+
are open to leveraging this technology to improve NetExec; however, there are
70+
many low quality AI tools whose use results in pure slop being generated. The
71+
security communinity is not immune from AI psychosis, over-hype, or FOMO.
72+
As with any new technology, it is important to understand how it works and how
73+
to best use it, not blindly apply it to every use case with the hope that it
74+
will fix all your issues.
75+
76+
We include this section to be transparent about the project's usage about
77+
AI for people who may disagree with it.

nxc/modules/get-userPassword.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,6 @@ def on_login(self, context, connection):
4141
resp_parsed = parse_result_attributes(resp)
4242
context.log.success("Found following users: ")
4343
for user in resp_parsed:
44-
context.log.highlight(f"User: {user['sAMAccountName']} unixUserPassword: {user['userPassword']}")
44+
context.log.highlight(f"User: {user['sAMAccountName']} userPassword: {user['userPassword']}")
4545
else:
46-
context.log.fail("No unixUserPassword Found")
46+
context.log.fail("No userPassword Found")

nxc/modules/snipped.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import ntpath
22
import os
33
from os.path import join, getsize, exists
4+
from pathlib import PurePosixPath
45
from nxc.helpers.misc import CATEGORY
56
from nxc.paths import NXC_PATH
67

@@ -84,9 +85,12 @@ def on_admin_login(self, context, connection):
8485
continue
8586

8687
remote_file_path = ntpath.join(screenshot_path, remote_file_name)
87-
sanitized_path = screenshot_path.replace("\\", "_").replace("/", "_")
88-
local_file_name = f"{folder_name}_{sanitized_path}_{remote_file_name}"
89-
local_file_path = join(user_output_dir, local_file_name)
88+
# replace \\ with underscores and ignore absolute path or path traversal attempts
89+
clean_screenshot_path = "_".join(p for p in PurePosixPath(screenshot_path.replace("\\", "/")).parts if p not in ("..", ".", "/"))
90+
clean_file = "_".join(p for p in PurePosixPath(remote_file_name.replace("\\", "/")).parts if p not in ("..", ".", "/"))
91+
92+
local_file_path = join(user_output_dir, f"{clean_screenshot_path}_{clean_file}")
93+
context.log.debug(f"{local_file_path=}")
9094

9195
try:
9296
with open(local_file_path, "wb") as local_file:
@@ -107,7 +111,7 @@ def on_admin_login(self, context, connection):
107111
context.log.debug(f"Failed to download '{remote_file_path}' for user {folder_name}: {e}")
108112

109113
if total_files_downloaded > 0 and host_output_path:
110-
context.log.success(f"{total_files_downloaded} file(s) downloaded from host {connection.host} to {host_output_path}.")
114+
context.log.success(f"{total_files_downloaded} file(s) downloaded from host {connection.host} to {host_output_path}/")
111115

112116
def find_screenshots_folders(self, user_folder_name):
113117
"""

nxc/modules/spider_plus.py

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import json
22
import errno
3-
from os.path import abspath, join, split, exists, splitext, getsize, sep
3+
from os.path import abspath, join, exists, splitext, getsize
44
from os import makedirs, remove, stat
5+
from pathlib import Path, PurePosixPath
56
import time
67
from nxc.helpers.misc import CATEGORY
78
from nxc.paths import NXC_PATH
@@ -167,19 +168,15 @@ def read_chunk(self, remote_file, chunk_size=CHUNK_SIZE):
167168
def get_file_save_path(self, remote_file):
168169
r"""Processes the remote file path to extract the filename and the folder path where the file should be saved locally.
169170
170-
It converts forward slashes (/) and backslashes (\) in the remote file path to the appropriate path separator for the local file system.
171-
The folder path and filename are then obtained separately.
171+
Creates a PurePosixPath and replaces UNC parts, then cleans it of any path traversal (see issue #1120)
172172
"""
173-
# Remove the backslash before the remote host part and replace slashes with the appropriate path separator
174-
remote_file_path = str(remote_file)[2:].replace("/", sep).replace("\\", sep)
175-
176-
# Split the path to obtain the folder path and the filename
177-
folder, filename = split(remote_file_path)
178-
179-
# Join the output folder with the folder path to get the final local folder path
180-
folder = join(self.output_folder, folder)
181-
182-
return folder, filename
173+
self.logger.debug(f"Remote file: {remote_file}")
174+
raw_path = PurePosixPath(remote_file._RemoteFile__share, remote_file._RemoteFile__fileName.replace("\\", "/"))
175+
self.logger.debug(f"Raw path: {remote_file}")
176+
clean_parts = [p for p in raw_path.parts if p not in ("..", ".")]
177+
resolved = Path(self.output_folder).joinpath(self.host, *clean_parts)
178+
self.logger.debug(f"Resolved path: {resolved}")
179+
return str(resolved.parent), resolved.name
183180

184181
def spider_shares(self):
185182
"""Enumerates all available shares for the SMB connection, spiders through the readable shares, and saves the metadata of the shares to a JSON file"""

nxc/protocols/nfs.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -366,7 +366,7 @@ def get_file(self):
366366
curr_fh = mount_fh
367367
for sub_path in remote_file_path.lstrip("/").split("/"):
368368
# Update the UID for the next object and get the handle
369-
self.update_auth(mount_fh)
369+
self.update_auth(curr_fh)
370370
res = self.nfs3.lookup(curr_fh, sub_path, auth=self.auth)
371371

372372
# Check for a bad path
@@ -448,7 +448,7 @@ def put_file(self):
448448
curr_fh = mount_fh
449449
# If target dir is "" or "/" without filter we would get one item with [""]
450450
for sub_path in list(filter(None, remote_dir_path.lstrip("/").split("/"))):
451-
self.update_auth(mount_fh)
451+
self.update_auth(curr_fh)
452452
res = self.nfs3.lookup(curr_fh, sub_path, auth=self.auth)
453453

454454
# If the path does not exist, create it

nxc/protocols/rdp.py

Lines changed: 23 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,13 @@
1919
from aardwolf.commons.target import RDPTarget
2020
from aardwolf.keyboard.layoutmanager import KeyboardLayoutManager
2121
from aardwolf.protocol.x224.constants import SUPP_PROTOCOLS
22+
from aardwolf.network.x224 import X224Network
23+
from aardwolf.network.tpkt import TPKTPacketizer
2224
from asyauth.common.credentials.ntlm import NTLMCredential
2325
from asyauth.common.credentials.kerberos import KerberosCredential
2426
from asyauth.common.constants import asyauthSecret
2527
from asysocks.unicomm.common.target import UniTarget, UniProto
28+
from asysocks.unicomm.client import UniClient
2629

2730

2831
class rdp(connection):
@@ -33,12 +36,10 @@ def __init__(self, args, db, host):
3336
self.iosettings.video_out_format = VIDEO_FORMAT.RAW
3437
self.iosettings.clipboard_use_pyperclip = False
3538
self.protoflags_nla = [
36-
SUPP_PROTOCOLS.SSL | SUPP_PROTOCOLS.RDP,
3739
SUPP_PROTOCOLS.SSL,
3840
SUPP_PROTOCOLS.RDP,
3941
]
4042
self.protoflags = [
41-
SUPP_PROTOCOLS.SSL | SUPP_PROTOCOLS.RDP,
4243
SUPP_PROTOCOLS.SSL,
4344
SUPP_PROTOCOLS.RDP,
4445
SUPP_PROTOCOLS.SSL | SUPP_PROTOCOLS.HYBRID,
@@ -113,7 +114,7 @@ def create_conn_obj(self):
113114
self.target = RDPTarget(ip=self.host, domain="FAKE", port=self.port, timeout=self.args.rdp_timeout)
114115
self.auth = NTLMCredential(secret="pass", username="user", domain="FAKE", stype=asyauthSecret.PASS)
115116

116-
self.check_nla()
117+
asyncio.run(self.check_nla())
117118

118119
for proto in reversed(self.protoflags):
119120
try:
@@ -165,22 +166,26 @@ def create_conn_obj(self):
165166

166167
return True
167168

168-
def check_nla(self):
169+
async def check_nla(self):
169170
self.logger.debug(f"Checking NLA for {self.host}")
170-
for proto in self.protoflags_nla:
171-
try:
172-
self.iosettings.supported_protocols = proto
173-
self.conn = RDPConnection(
174-
iosettings=self.iosettings,
175-
target=self.target,
176-
credentials=self.auth,
177-
)
178-
asyncio.run(self.connect_rdp())
179-
if proto.value == SUPP_PROTOCOLS.RDP or proto.value == SUPP_PROTOCOLS.SSL or proto.value == SUPP_PROTOCOLS.SSL | SUPP_PROTOCOLS.RDP:
180-
self.nla = False
181-
return
182-
except Exception:
183-
pass
171+
try:
172+
self.iosettings.supported_protocols = SUPP_PROTOCOLS.SSL
173+
self.conn = RDPConnection(
174+
iosettings=self.iosettings,
175+
target=self.target,
176+
credentials=None,
177+
)
178+
packetizer = TPKTPacketizer()
179+
client = UniClient(self.target, packetizer)
180+
self.conn._connection = await asyncio.wait_for(client.connect(), timeout=self.args.rdp_timeout)
181+
self.conn._x224net = X224Network(self.conn._connection)
182+
_, err = await asyncio.wait_for(self.conn._x224net.client_negotiate(0, SUPP_PROTOCOLS.SSL), timeout=self.args.rdp_timeout)
183+
# If no error SSL supported if SSL_NOT_ALLOWED_BY_SERVER error, plain RDP supported
184+
if err is None or "SSL_NOT_ALLOWED_BY_SERVER" in str(err):
185+
self.nla = False
186+
return
187+
except Exception:
188+
pass
184189

185190
async def connect_rdp(self):
186191
_, err = await asyncio.wait_for(self.conn.connect(), timeout=self.args.rdp_timeout)

0 commit comments

Comments
 (0)