Skip to content

Commit 27313a0

Browse files
authored
Merge pull request Pennyw0rth#366 from termanix/NFS-Protocol
New Protocol NFS
2 parents aef2675 + 90cc0e5 commit 27313a0

9 files changed

Lines changed: 528 additions & 3 deletions

File tree

nxc/connection.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,9 @@ def print_host_info(self):
206206
def create_conn_obj(self):
207207
return
208208

209+
def disconnect(self):
210+
return
211+
209212
def check_if_admin(self):
210213
return
211214

@@ -234,6 +237,7 @@ def proto_flow(self):
234237
else:
235238
self.logger.debug("Calling command arguments")
236239
self.call_cmd_args()
240+
self.disconnect()
237241

238242
def call_cmd_args(self):
239243
"""Calls all the methods specified by the command line arguments

nxc/protocols/nfs.py

Lines changed: 379 additions & 0 deletions
Large diffs are not rendered by default.

nxc/protocols/nfs/__init__.py

Whitespace-only changes.

nxc/protocols/nfs/database.py

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
from pathlib import Path
2+
from sqlalchemy.orm import sessionmaker, scoped_session
3+
from sqlalchemy import MetaData, Table
4+
from sqlalchemy.exc import (
5+
IllegalStateChangeError,
6+
NoInspectionAvailable,
7+
NoSuchTableError,
8+
)
9+
from nxc.logger import nxc_logger
10+
import sys
11+
12+
13+
class database:
14+
def __init__(self, db_engine):
15+
self.CredentialsTable = None
16+
self.HostsTable = None
17+
self.LoggedinRelationsTable = None
18+
self.SharesTable = None
19+
20+
self.db_engine = db_engine
21+
self.db_path = self.db_engine.url.database
22+
self.protocol = Path(self.db_path).stem.upper()
23+
self.metadata = MetaData()
24+
self.reflect_tables()
25+
26+
session_factory = sessionmaker(bind=self.db_engine, expire_on_commit=True)
27+
Session = scoped_session(session_factory)
28+
self.sess = Session()
29+
30+
@staticmethod
31+
def db_schema(db_conn):
32+
db_conn.execute(
33+
"""CREATE TABLE "credentials" (
34+
"id" integer PRIMARY KEY,
35+
"username" text,
36+
"password" text
37+
)"""
38+
)
39+
40+
db_conn.execute(
41+
"""CREATE TABLE "hosts" (
42+
"id" integer PRIMARY KEY,
43+
"ip" text,
44+
"hostname" text,
45+
"port" integer
46+
)"""
47+
)
48+
db_conn.execute(
49+
"""CREATE TABLE "loggedin_relations" (
50+
"id" integer PRIMARY KEY,
51+
"cred_id" integer,
52+
"host_id" integer,
53+
FOREIGN KEY(cred_id) REFERENCES credentials(id),
54+
FOREIGN KEY(host_id) REFERENCES hosts(id)
55+
)"""
56+
)
57+
db_conn.execute(
58+
"""CREATE TABLE "shares" (
59+
"id" integer PRIMARY KEY,
60+
"lir_id" integer,
61+
"data" text,
62+
FOREIGN KEY(lir_id) REFERENCES loggedin_relations(id)
63+
)"""
64+
)
65+
66+
def reflect_tables(self):
67+
with self.db_engine.connect():
68+
try:
69+
self.CredentialsTable = Table("credentials", self.metadata, autoload_with=self.db_engine)
70+
self.HostsTable = Table("hosts", self.metadata, autoload_with=self.db_engine)
71+
self.LoggedinRelationsTable = Table("loggedin_relations", self.metadata, autoload_with=self.db_engine)
72+
self.SharesTable = Table("shares", self.metadata, autoload_with=self.db_engine)
73+
except (NoInspectionAvailable, NoSuchTableError):
74+
print(
75+
f"""
76+
[-] Error reflecting tables for the {self.protocol} protocol - this means there is a DB schema mismatch
77+
[-] This is probably because a newer version of nxc is being run on an old DB schema
78+
[-] Optionally save the old DB data (`cp {self.db_path} ~/nxc_{self.protocol.lower()}.bak`)
79+
[-] Then remove the {self.protocol} DB (`rm -f {self.db_path}`) and run nxc to initialize the new DB"""
80+
)
81+
sys.exit()
82+
83+
def shutdown_db(self):
84+
try:
85+
self.sess.close()
86+
# due to the async nature of nxc, sometimes session state is a bit messy and this will throw:
87+
# Method 'close()' can't be called here; method '_connection_for_bind()' is already in progress and
88+
# this would cause an unexpected state change to <SessionTransactionState.CLOSED: 5>
89+
except IllegalStateChangeError as e:
90+
nxc_logger.debug(f"Error while closing session db object: {e}")
91+
92+
def clear_database(self):
93+
for table in self.metadata.sorted_tables:
94+
self.sess.execute(table.delete())

nxc/protocols/nfs/db_navigator.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
from nxc.nxcdb import DatabaseNavigator, print_help
2+
3+
4+
class navigator(DatabaseNavigator):
5+
def do_clear_database(self, line):
6+
if input("This will destroy all data in the current database, are you SURE you want to run this? (y/n): ") == "y":
7+
self.db.clear_database()
8+
9+
def help_clear_database(self):
10+
help_string = """
11+
clear_database
12+
THIS COMPLETELY DESTROYS ALL DATA IN THE CURRENTLY CONNECTED DATABASE
13+
YOU CANNOT UNDO THIS COMMAND
14+
"""
15+
print_help(help_string)

nxc/protocols/nfs/proto_args.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
def proto_args(parser, parents):
2+
nfs_parser = parser.add_parser("nfs", help="NFS", parents=parents)
3+
nfs_parser.add_argument("--port", type=int, default=111, help="NFS portmapper port (default: %(default)s)")
4+
nfs_parser.add_argument("--nfs-timeout", type=int, default=30, help="NFS connection timeout (default: %(default)ss)")
5+
6+
dgroup = nfs_parser.add_argument_group("NFS Mapping/Enumeration", "Options for Mapping/Enumerating NFS")
7+
dgroup.add_argument("--shares", action="store_true", help="List NFS shares")
8+
dgroup.add_argument("--enum-shares", nargs="?", type=int, const=3, help="Authenticate and enumerate exposed shares recursively (default depth: %(const)s)")
9+
dgroup.add_argument("--get-file", nargs=2, metavar="FILE", help="Download remote NFS file. Example: --get-file remote_file local_file")
10+
dgroup.add_argument("--put-file", nargs=2, metavar="FILE", help="Upload remote NFS file with chmod 777 permissions to the specified folder. Example: --put-file local_file remote_file")
11+
12+
return parser

poetry.lock

Lines changed: 18 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ paramiko = "^3.3.1"
5454
poetry-dynamic-versioning = "^1.2.0"
5555
pyasn1-modules = "^0.3.0"
5656
pylnk3 = "^0.4.2"
57+
pynfsclient = { git = "https://github.com/Pennyw0rth/NfsClient" }
5758
pypsrp = "^0.8.1"
5859
pypykatz = "^0.6.8"
5960
pywerview = "^0.3.3" # pywerview 5 requires libkrb5-dev installed which is not default on kali (as of 9/23)

tests/e2e_commands.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,3 +248,8 @@ netexec ftp TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD --get test_file.txt
248248
netexec ftp TARGET_HOST -u TEST_USER_FILE -p TEST_PASSWORD_FILE --no-bruteforce
249249
netexec ftp TARGET_HOST -u TEST_USER_FILE -p TEST_PASSWORD_FILE --no-bruteforce --continue-on-success
250250
netexec ftp TARGET_HOST -u TEST_USER_FILE -p TEST_PASSWORD_FILE
251+
##### NFS
252+
netexec nfs TARGETHOST -u "" -p "" --shares
253+
netexec nfs TARGETHOST -u "" -p "" --enum-shares
254+
netexec nfs TARGETHOST -u "" -p "" --get-file /NFStest/test/test.txt ../test.txt
255+
netexec nfs TARGETHOST -u "" -p "" --put-file ../test.txt /NFStest/test

0 commit comments

Comments
 (0)