Skip to content

Commit

Permalink
dcsync fix, remote registry secrets feature
Browse files Browse the repository at this point in the history
  • Loading branch information
SkelSec committed Sep 5, 2024
1 parent 4232e42 commit 6008022
Show file tree
Hide file tree
Showing 7 changed files with 396 additions and 30 deletions.
21 changes: 21 additions & 0 deletions .github/workflows/builder_windows.yml
Original file line number Diff line number Diff line change
Expand Up @@ -81,3 +81,24 @@ jobs:
files: |
builder\pyinstaller\*.exe
builder\pyinstaller\*.7z
- uses: sarisia/actions-status-discord@v1
if: always()
with:
webhook: ${{ secrets.DISCORD_WEBHOOK }}
status: ${{ job.status }}
content: |
${{ github.event_name == 'push' && format('Hey @here! A new commit was pushed to {0}!', github.repository) || '' }}
${{ github.event_name == 'pull_request' && format('Hey @here! A new pull request has been opened on {0}!', github.repository) || '' }}
${{ github.event_name == 'release' && format('Hey @here! A new release was created for project {0}!', github.event.repository.name) || '' }}
title: |
${{ github.event_name == 'push' && 'Push Notification' || '' }}
${{ github.event_name == 'pull_request' && 'Pull Request Notification' || '' }}
${{ github.event_name == 'release' && 'Release Notification' || '' }}
color: |
${{ github.event_name == 'push' && '0x00ff00' || '' }}
${{ github.event_name == 'pull_request' && '0xff0000' || '' }}
${{ github.event_name == 'release' && '0x0000ff' || '' }}
url: "${{ github.server_url }}/${{ github.repository }}"
username: GitHub Actions
avatar_url: "https://avatars.githubusercontent.com/u/19204702"
13 changes: 10 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
![Supported Python versions](https://img.shields.io/badge/python-3.7+-blue.svg) [![Twitter](https://img.shields.io/twitter/follow/skelsec?label=skelsec&style=social)](https://twitter.com/intent/follow?screen_name=skelsec)
![Supported Python versions](https://img.shields.io/badge/python-3.8+-blue.svg) [![Twitter](https://img.shields.io/twitter/follow/skelsec?label=skelsec&style=social)](https://twitter.com/intent/follow?screen_name=skelsec)

## :triangular_flag_on_post: Sponsors

If you like this project, consider sponsoring it on GitHub! [Sponsors](https://github.com/sponsors/skelsec/)
If you like this project, consider purchasing licenses of [OctoPwn](https://octopwn.com/), our full pentesting suite that runs in your browser!
For notifications on new builds/releases and other info, hop on to our [Discord](https://discord.gg/PM8utcNxMS)


# aiosmb
Fully asynchronous SMB library written in pure python. Python 3.7+ ONLY
Fully asynchronous SMB library written in pure python. Python 3.8+ ONLY


## :triangular_flag_on_post: Runs in the browser

This project, alongside with many other pentester tools runs in the browser with the power of OctoPwn!
Check out the community version at [OctoPwn - Live](https://live.octopwn.com/)

# Features
Too many to list here, please check the examples.
Expand Down
2 changes: 1 addition & 1 deletion aiosmb/_version.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@

__version__ = "0.4.10"
__version__ = "0.4.11"
__banner__ = \
"""
# aiosmb %s
Expand Down
26 changes: 20 additions & 6 deletions aiosmb/commons/interfaces/machine.py
Original file line number Diff line number Diff line change
Expand Up @@ -397,20 +397,33 @@ async def dcsync(self, target_domain:str = None, target_users:List[str] = []) ->
target_users = [target_users]

async with samrrpc_from_smb(self.connection, auth_level=self.force_rpc_auth) as samrpc:
if target_domain is None:
logger.debug('No domain defined, fetching it from SAMR')
if target_domain is None or target_domain == '':
if self.print_cb is not None:
await self.print_cb('No domain defined, fetching it from SAMR')
else:
logger.debug('No domain defined, fetching it from SAMR')

logger.debug('Fetching domains...')
available_domains = []
async for domain, err in samrpc.list_domains():
if err is not None:
raise err
if domain == 'Builtin':
continue
if target_domain is None: #using th first available
target_domain = domain
#using th first available #if target_domain is None:
available_domains.append(domain)
if self.print_cb is not None:
await self.print_cb('Domain available: %s' % domain)
else:
logger.debug('Domain available: %s' % domain)

target_domain = available_domains[0]
if self.print_cb is not None:
await self.print_cb('Selecting first available: %s' % available_domains[0])
else:
logger.debug('Selecting first available: %s' % available_domains[0])

async with drsuapirpc_from_smb(self.connection, auth_level=self.force_rpc_auth) as drsuapi:
async with drsuapirpc_from_smb(self.connection, domain=target_domain, auth_level=self.force_rpc_auth) as drsuapi:
#async with drsuapi:
logger.debug('Using domain: %s' % target_domain)
if len(target_users) > 0:
Expand All @@ -431,7 +444,8 @@ async def dcsync(self, target_domain:str = None, target_users:List[str] = []) ->
yield None, err
return
logger.debug('username: %s' % username)
secrets, err = await drsuapi.get_user_secrets(username)
#secrets, err = await drsuapi.get_user_secrets(username)
secrets, err = await drsuapi.get_user_secrets(user_sid)
if err is not None:
yield None, err
return
Expand Down
84 changes: 65 additions & 19 deletions aiosmb/dcerpc/v5/interfaces/drsuapimgr.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import logging
from aiosmb import logger
import traceback
import enum

from aiosmb.dcerpc.v5.common.connection.authentication import DCERPCAuth
from aiosmb.dcerpc.v5.common.connection.target import DCERPCTarget
Expand All @@ -22,8 +23,8 @@
from contextlib import asynccontextmanager

@asynccontextmanager
async def drsuapirpc_from_smb(connection, auth_level=None, open=True, perform_dummy=False):
instance, err = await DRSUAPIRPC.from_smbconnection(connection, auth_level=auth_level, open=open, perform_dummy=perform_dummy)
async def drsuapirpc_from_smb(connection, domain=None, auth_level=None, open=True, perform_dummy=False):
instance, err = await DRSUAPIRPC.from_smbconnection(connection, domain=domain, auth_level=auth_level, open=open, perform_dummy=perform_dummy)
if err:
# Handle or raise the error as appropriate
raise err
Expand All @@ -33,6 +34,17 @@ async def drsuapirpc_from_smb(connection, auth_level=None, open=True, perform_du
await instance.close()


class DS_NAME_ERROR(enum.Enum):
NO_ERROR = 0
RESOLVING = 1
NOT_FOUND = 2
NOT_UNIQUE = 3
NO_MAPPING = 4
DOMAIN_ONLY = 5
NO_SYNTACTICAL_MAPPING = 6
TRUST_REFERRAL = 7


class DRSUAPIRPC:
def __init__(self, domainname = None):
self.domainname = domainname
Expand Down Expand Up @@ -177,7 +189,8 @@ async def open(self):

# If dwExtCaps is not included in the answer, let's just add it so we can unpack DRS_EXTENSIONS_INT right.
ppextServer = b''.join(resp['ppextServer']['rgb']) + b'\x00' * (
len(drsuapi.DRS_EXTENSIONS_INT()) - resp['ppextServer']['cb'])
len(drsuapi.DRS_EXTENSIONS_INT()) - resp['ppextServer']['cb']
)
drsExtensionsInt.fromString(ppextServer)

if drsExtensionsInt['dwReplEpoch'] != 0:
Expand All @@ -198,6 +211,7 @@ async def open(self):
resp, err = await drsuapi.hDRSDomainControllerInfo(self.dce, self.handle, self.domainname, 2)
if err is not None:
raise err

if logger.level == logging.DEBUG:
logger.debug('DRSDomainControllerInfo() answer %s' % resp.dump())

Expand All @@ -211,7 +225,8 @@ async def open(self):
except Exception as e:
return None, e

async def get_user_secrets(self, username):
async def get_user_secrets(self, username, formatOffered = None): #drsuapi.DS_NT4_ACCOUNT_NAME_SANS_DOMAIN
# formatoffered https://learn.microsoft.com/en-us/windows/win32/api/ntdsapi/ne-ntdsapi-ds_name_format
try:
ra = {
'userPrincipalName': '1.2.840.113556.1.4.656',
Expand All @@ -225,26 +240,57 @@ async def get_user_secrets(self, username):
'pwdLastSet': '1.2.840.113556.1.4.96',
'userAccountControl':'1.2.840.113556.1.4.8'
}
formatOffered = drsuapi.DS_NT4_ACCOUNT_NAME_SANS_DOMAIN
crackedName, err = await self.DRSCrackNames(
formatOffered,
drsuapi.DS_NAME_FORMAT.DS_UNIQUE_ID_NAME,
name=username
)
if err is not None:
raise err

username = str(username)

###### TODO: CHECKS HERE
if formatOffered is None:
if username.upper().find('CN=') != -1:
formatOffered_list = [drsuapi.DS_NAME_FORMAT.DS_FQDN_1779_NAME]
elif username.upper().find('S-1-5-') != -1:
formatOffered_list = [drsuapi.DS_NAME_FORMAT.DS_SID_OR_SID_HISTORY_NAME]
elif username.find('@') != -1:
formatOffered_list = [drsuapi.DS_NAME_FORMAT.DS_USER_PRINCIPAL_NAME, drsuapi.DS_NAME_FORMAT.DS_SERVICE_PRINCIPAL_NAME]
else:
formatOffered_list = [drsuapi.DS_NT4_ACCOUNT_NAME_SANS_DOMAIN, drsuapi.DS_NAME_FORMAT.DS_UNKNOWN_NAME]
else:
formatOffered_list = [formatOffered]

#guid = GUID.from_string(crackedName['pmsgOut']['V1']['pResult']['rItems'][0]['pName'][:-1][1:-1])
guid = crackedName['pmsgOut']['V1']['pResult']['rItems'][0]['pName'][:-1][1:-1]
if len(guid) == 0:
raise Exception('User "%s" cannot be found in the domain!' % username)
rstatus = 'UNKNOWN'
guid = None
for formatOffered in formatOffered_list:
crackedName, err = await self.DRSCrackNames(
formatOffered,
drsuapi.DS_NAME_FORMAT.DS_UNIQUE_ID_NAME,
name=username
)
if err is not None:
raise err


###### TODO: CHECKS HERE
# https://learn.microsoft.com/en-us/windows/win32/api/ntdsapi/ne-ntdsapi-ds_name_error
rstatus = crackedName['pmsgOut']['V1']['pResult']['rItems'][0]['status']
if rstatus != 0:
try:
s = DS_NAME_ERROR(rstatus)
rstatus = '%s - %s' % (rstatus, s.name)
except:
rstatus = '%s - %s' % (rstatus, 'UNKNOWN')
#guid = GUID.from_string(crackedName['pmsgOut']['V1']['pResult']['rItems'][0]['pName'][:-1][1:-1])
guid = crackedName['pmsgOut']['V1']['pResult']['rItems'][0]['pName'][:-1][1:-1]
if len(guid) == 0:
continue
break
else:
if rstatus == 0:
raise Exception('User "%s" lookup returned empty GUID' % username)
raise Exception('User "%s" lookup failed. Reason: %s' % (username, rstatus))

userRecord, err = await self.DRSGetNCChanges(guid, ra)
if err is not None:
return None, err


replyVersion = 'V%d' % userRecord['pdwOutVersion']
if userRecord['pmsgOut'][replyVersion]['cNumObjects'] == 0:
raise Exception('DRSGetNCChanges didn\'t return any object!')
Expand Down Expand Up @@ -409,11 +455,11 @@ async def get_user_secrets(self, username):
except Exception as e:
return None, e

async def DRSCrackNames(self, formatOffered=drsuapi.DS_NAME_FORMAT.DS_DISPLAY_NAME, formatDesired=drsuapi.DS_NAME_FORMAT.DS_FQDN_1779_NAME, name=''):
async def DRSCrackNames(self, formatOffered=drsuapi.DS_NAME_FORMAT.DS_UNKNOWN_NAME, formatDesired=drsuapi.DS_NAME_FORMAT.DS_FQDN_1779_NAME, name=''):
try:
logger.debug('Calling DRSCrackNames for %s' % name)
resp, err = await drsuapi.hDRSCrackNames(self.dce, self.handle, 0, formatOffered, formatDesired, (name,))
return resp, None
return resp, err
except Exception as e:
return None, e

Expand Down
Loading

0 comments on commit 6008022

Please sign in to comment.