Forum Discussion

Tim_Haynie_3150's avatar
Oct 11, 2018

iRule to decrypt and rewrite RADIUS User-Password AVP

In the RADIUS protocol, the user's cleartext password is transmitted inside Attribute-Value Pair (AVP) 2, padded with null characters as necessary, and then encrypted by the shared secret by XOR'ing it across the authenticator somehow or other. The technical details of how this works is a bit above my level of understanding as I'm not a cryptography expert.

 

We have an infrastructure where our PAN VPN Gateway prompts a user for their username and password. In our environment, after the password, the user appends a fixed-length HOTP token from a Yubikey. The backend FreeRADIUS server has been configured to decrypt the password received, extract the fixed-length token, and perform backend checks to our LDAP and token servers. FYI, the password is encoded as PAP prior to RADIUS encryption in our setup, which is why this works; CHAP would prevent this from working.

 

We've been having trouble with the stability of our FreeRADIUS server and we would like to leverage our much more stable Aruba ClearPass infrastructure which is load balanced globally with our GTMs and LTMs and highly stable. This also moves control of the RADIUS piece away from the systems team and onto the network team (me, specifically). Unfortunately, ClearPass doesn't have a direct mechanism to break the password from the token, and PAN doesn't have a way to transmit the token separately. This is where we would like to leverage an iRule.

 

Basically, the way I envision this working is as such:

 

  • Decrypt the password+OTP that is received from PAN using the authenticator value and shared secret
  • Rewrite AVP 2 as just the password, encrypted by the shared secret (make sure to adjust the length of the AVP)
  • Insert AVP 17 (which is not defined by the IEFT) with the token (ClearPass can be configured to look for this by modifying its RADIUS dictionary).
  • Rewrite the length value at layer 7 if necessary - not sure if this would happen automatically by the F5; probably not.
  • Ship the modified RADIUS packet to ClearPass

I know how to accomplish all of this on the ClearPass side, but my dev skills are weak, I'm not very familiary with Tcl, and I don't have a solid understanding of how to encrypt/decrypt the password correctly.

 

I've search high and low but the only solutions for decrypting the password seem to be written in languages that are even more difficult to understand like C.

 

I obviously understand it is too much to expect someone to write the entire solution for me, but any advice on where to start would be very helpful. I think the trickiest part for me would be the encrypt/decrypt step.

 

  • Here is my final, working product. Any feedback, suggestions, or critiques are welcome.

     

    when CLIENT_ACCEPTED {
    set KEY "this-obviously-isn't-my-real-key:)"
    
    
    set AUTHENTICATOR "[string range [UDP::payload 20] 4 20]"
    set KEY_AUTH_MD5 [md5 "$KEY$AUTHENTICATOR"]
    
    the below commands collapse the above 2 set commands commented out above
    set KEY_AUTH_MD5 [md5 "$KEY[string range [UDP::payload 20] 4 20]"]
    
    
    
    
    
    
    
     START OF PACKET INTEGRITY VERIFICATION 
    based off of https://devcentral.f5.com/codeshare/radius-server-using-apm-to-authenticate-users-1078 lines 39-64 and 78-86
    
    EVALUATE REQUEST MESSAGE-AUTHENTICATOR
     RFC 2869 : A RADIUS Server receiving an Access-Request with a Message-Authenticator Attribute present MUST calculate the correct value
     of the Message-Authenticator and silently discard the packet if it does not match the value sent.
    
     Store only PAYLOAD in variable if Length field is valid (less than 4096 and less than payload length). prevent variable allocation if payload not valid.
     Octets outside the range of the Length field MUST be treated as padding and ignored on reception.
    if {[binary scan [UDP::payload] cH2Sa16 QCODE IDENTIFIER QLEN Q_AUTHENTICATOR] != 4 || [set QLEN [expr {$QLEN & 0xFFFF}]] > [UDP::payload length] || $QLEN > 4096} {
        UDP::drop
        log local0. "Badly formatted RADIUS packet received"
        return
    } else {
        set PAYLOAD [UDP::payload $QLEN]
    }
    
    we need to find AVP 80 within the UDP payload and set it to 32 bytes of 0s, but we are also checking that all other AVPs are formatted correctly
    set ORIGINAL_MESSAGE_AUTHENTICATOR [RADIUS::avp 80 string]
    
    for {set record_offset 20} {$record_offset < $QLEN } {incr record_offset $QAVP_LEN} {
     If an Attribute is received in an Access-Accept, Access-Reject or Access-Challenge packet with an invalid length, 
     the packet MUST either be treated as an Access-Reject or else silently discarded.
    if {([binary scan $PAYLOAD @${record_offset}cc QAVP_TYPE QAVP_LEN] != 2) || ([set QAVP_LEN [expr {$QAVP_LEN & 0xFF}]] < 3) || ($record_offset+$QAVP_LEN > $QLEN) } {
        UDP::drop
        log local0. "Badly formatted AVP received"
        return
    }
    switch -- [set QAVP_TYPE [expr { $QAVP_TYPE & 0xFF}]] {
            80 {
                binary scan [string replace $PAYLOAD $record_offset [expr {$record_offset + 18}] [binary format ccH32 80 18 [string repeat 0 32]]] a* UNSIGNED_REQUEST
            }
        }
    }
    
    
    EVALUATE REQUEST MESSAGE-AUTHENTICATOR
     RFC 2869 : A RADIUS Server receiving an Access-Request with a Message-Authenticator Attribute present MUST calculate the correct value
     of the Message-Authenticator and silently discard the packet if it does not match the value sent.
    if { "$ORIGINAL_MESSAGE_AUTHENTICATOR" != "" && ![CRYPTO::verify -alg hmac-md5 -key $KEY -signature $ORIGINAL_MESSAGE_AUTHENTICATOR $UNSIGNED_REQUEST]} {
        UDP::drop
        log local0. "Message-Authenticator (AVP 80) verification failed"
        return
    }
    else {
        we've verified the Message-Authenticator, no need to have the RADIUS server verify again
        RADIUS::avp delete 80
    }
     END OF PACKET INTEGRITY VERIFICATION 
    
    
    
    
     For a minimum 8 character password and 44 character Yubikey OTP, we expect a minimum of 64 cypher bytes. 
    set CYPHER_PW_OTP [RADIUS::avp 2 string]
    if {[string length $CYPHER_PW_OTP]<64} {
        log local0. "password for [RADIUS::avp 1 string] too short BEFORE decryption"
        return
    }
    
    
    
    
    
     START OF PASSWORD DECRYPTION 
    based on suggestion from Stanislas Piron on https://devcentral.f5.com/questions/irule-to-decrypt-and-rewrite-radius-user-password-avp-62084answer159896
    binary scan $KEY_AUTH_MD5 WW bx_64bits_1 bx_64bits_2
    binary scan $CYPHER_PW_OTP W* USER_PASSWORD_W_LIST
    set PASSWORD_LIST [list]
    foreach {px_64bits_1 px_64bits_2} $USER_PASSWORD_W_LIST {
        lappend PASSWORD_LIST [expr { $px_64bits_1 ^ $bx_64bits_1 }] [expr { $px_64bits_2 ^ $bx_64bits_2 }]
        binary scan [md5 $KEY[binary format WW $px_64bits_1 $px_64bits_2]] WW bx_64bits_1 bx_64bits_2
    }
    binary scan [binary format W* $PASSWORD_LIST] A* PASSWORD_OTP
     END OF PASSWORD DECRYPTION 
    
    
    
    
    
    
     START OF OTP/PASSWORD SEPARATION 
     For a minimum 8 character password and 44 character Yubikey OTP, we expect a minimum of 52 cleartext bytes
    if {[string length $PASSWORD_OTP] > 51} {
        set OTP "[string range "$PASSWORD_OTP" [expr {[string length $PASSWORD_OTP]-44}] end]"
        set PW "[string range "$PASSWORD_OTP" 0 [expr {[string length $PASSWORD_OTP]-45}]]"
        RADIUS::avp insert 17 "$OTP" string
        RADIUS::avp insert 17 "[string range "$PASSWORD_OTP" [expr {[string length $PASSWORD_OTP]-44}] end]"
    } else {
        log local0. "password for [RADIUS::avp 1 string] too short AFTER decryption"        
        return
    }
     END OF OTP/PASSWORD SEPARATION 
    
    
    
    
    
     START OF PASSWORD NULL PADDING 
    this function has been collapsed into the next section
    
    set PW_LENGTH [expr {[string length "$PW"]}]
    set PW_MOD [expr {$PW_LENGTH%16}]
    set PW_NULL_BYTES_TO_PAD [expr {(16-$PW_MOD)%16}]
    set PW_PADDED "$PW[string repeat \x0 $PW_NULL_BYTES_TO_PAD]"
    
    the below command collapses the above 4 set commands
    set PW_PADDED "$PW[string repeat \x0 [expr {(16-[expr {[expr {[string length "$PW"]}]%16}])%16}]]"
     END OF PASSWORD NULL PADDING 
    
    
    
    
    
     START OF PASSWORD ENCRYPTION 
    binary scan $KEY_AUTH_MD5 WW bx_64bits_1 bx_64bits_2
    binary scan $PW_PADDED W* USER_PASSWORD_W_LIST
    binary scan "$PW[string repeat \x0 [expr {(16-[expr {[expr {[string length "$PW"]}]%16}])%16}]]" W* USER_PASSWORD_W_LIST
    binary scan [binary format a[expr {[string length $PW] + 16 - [string length $PW]%16}] $PW ] W* USER_PASSWORD_W_LIST
    set PASSWORD_LIST [list]
    foreach {px_64bits_1 px_64bits_2} $USER_PASSWORD_W_LIST {
        lappend PASSWORD_LIST [expr { $px_64bits_1 ^ $bx_64bits_1 }] [expr { $px_64bits_2 ^ $bx_64bits_2 }]
        binary scan [md5 $KEY[binary format WW $px_64bits_1 $px_64bits_2]] WW bx_64bits_1 bx_64bits_2
        binary scan [md5 $KEY[binary format W2 [lrange $PASSWORD_LIST end-1 end]]] WW bx_64bits_1 bx_64bits_2
    }
    binary scan [binary format W* $PASSWORD_LIST] A* CYPHER_PW
     END OF PASSWORD ENCRYPTION 
    
    
    
    
     Modify the RADIUS payload based on the above work
    RADIUS::avp replace 2 "$CYPHER_PW" string
    }
    

     

  • The format of a RADIUS packet is as follows: Byte 1: RADIUS code (always 1 for an Access-Request) Byte 2: Packet identifier Byte 3-4: Length of the entire RADIUS portion, beginning to end (including code and packet identifier) Byte 5-20: Authenticator Remaining bytes: AVPs

     

    AVP format: Byte 1: AVP type (number) Byte 2: Length of this AVP (including bytes 1 and 2) Remaining bytes: Value

     

  • Clearpass has a built-in radius proxy... can't you use that?

     

    I've never had stability issues radius servers... you usually deploy them in pairs, and to be safe, dedicate 2 boxes or VM's to them. They need very little care & feeding

     

    • Tim_Haynie_3150's avatar
      Tim_Haynie_3150
      Icon for Cirrus rankCirrus

      But that would defeat the purpose of this; we want to remove FreeRADIUS from the picture and have ClearPass talk to LDAP and the token server directly.

       

    • devnullNZ's avatar
      devnullNZ
      Icon for Nimbostratus rankNimbostratus

      You might have more luck auth'ing the vpn clients directly via ldap, rather than trying to write a radius server in TCL. Palo Alto devices can make LDAP queries directly...

       

    • Tim_Haynie_3150's avatar
      Tim_Haynie_3150
      Icon for Cirrus rankCirrus

      But we still need a mechanism which differentiates the password from the token, and PAN doesn't have such a mechanism.

       

  • Clearpass has a built-in radius proxy... can't you use that?

     

    I've never had stability issues radius servers... you usually deploy them in pairs, and to be safe, dedicate 2 boxes or VM's to them. They need very little care & feeding

     

    • Tim_Haynie_3150's avatar
      Tim_Haynie_3150
      Icon for Cirrus rankCirrus

      But that would defeat the purpose of this; we want to remove FreeRADIUS from the picture and have ClearPass talk to LDAP and the token server directly.

       

    • devnullNZ_11602's avatar
      devnullNZ_11602
      Icon for Altostratus rankAltostratus

      You might have more luck auth'ing the vpn clients directly via ldap, rather than trying to write a radius server in TCL. Palo Alto devices can make LDAP queries directly...

       

    • Tim_Haynie_3150's avatar
      Tim_Haynie_3150
      Icon for Cirrus rankCirrus

      But we still need a mechanism which differentiates the password from the token, and PAN doesn't have such a mechanism.

       

  • Here is my final, working product. Any feedback, suggestions, or critiques are welcome.

     

    when CLIENT_ACCEPTED {
    set KEY "this-obviously-isn't-my-real-key:)"
    
    
    set AUTHENTICATOR "[string range [UDP::payload 20] 4 20]"
    set KEY_AUTH_MD5 [md5 "$KEY$AUTHENTICATOR"]
    
    the below commands collapse the above 2 set commands commented out above
    set KEY_AUTH_MD5 [md5 "$KEY[string range [UDP::payload 20] 4 20]"]
    
    
    
    
    
    
    
     START OF PACKET INTEGRITY VERIFICATION 
    based off of https://devcentral.f5.com/codeshare/radius-server-using-apm-to-authenticate-users-1078 lines 39-64 and 78-86
    
    EVALUATE REQUEST MESSAGE-AUTHENTICATOR
     RFC 2869 : A RADIUS Server receiving an Access-Request with a Message-Authenticator Attribute present MUST calculate the correct value
     of the Message-Authenticator and silently discard the packet if it does not match the value sent.
    
     Store only PAYLOAD in variable if Length field is valid (less than 4096 and less than payload length). prevent variable allocation if payload not valid.
     Octets outside the range of the Length field MUST be treated as padding and ignored on reception.
    if {[binary scan [UDP::payload] cH2Sa16 QCODE IDENTIFIER QLEN Q_AUTHENTICATOR] != 4 || [set QLEN [expr {$QLEN & 0xFFFF}]] > [UDP::payload length] || $QLEN > 4096} {
        UDP::drop
        log local0. "Badly formatted RADIUS packet received"
        return
    } else {
        set PAYLOAD [UDP::payload $QLEN]
    }
    
    we need to find AVP 80 within the UDP payload and set it to 32 bytes of 0s, but we are also checking that all other AVPs are formatted correctly
    set ORIGINAL_MESSAGE_AUTHENTICATOR [RADIUS::avp 80 string]
    
    for {set record_offset 20} {$record_offset < $QLEN } {incr record_offset $QAVP_LEN} {
     If an Attribute is received in an Access-Accept, Access-Reject or Access-Challenge packet with an invalid length, 
     the packet MUST either be treated as an Access-Reject or else silently discarded.
    if {([binary scan $PAYLOAD @${record_offset}cc QAVP_TYPE QAVP_LEN] != 2) || ([set QAVP_LEN [expr {$QAVP_LEN & 0xFF}]] < 3) || ($record_offset+$QAVP_LEN > $QLEN) } {
        UDP::drop
        log local0. "Badly formatted AVP received"
        return
    }
    switch -- [set QAVP_TYPE [expr { $QAVP_TYPE & 0xFF}]] {
            80 {
                binary scan [string replace $PAYLOAD $record_offset [expr {$record_offset + 18}] [binary format ccH32 80 18 [string repeat 0 32]]] a* UNSIGNED_REQUEST
            }
        }
    }
    
    
    EVALUATE REQUEST MESSAGE-AUTHENTICATOR
     RFC 2869 : A RADIUS Server receiving an Access-Request with a Message-Authenticator Attribute present MUST calculate the correct value
     of the Message-Authenticator and silently discard the packet if it does not match the value sent.
    if { "$ORIGINAL_MESSAGE_AUTHENTICATOR" != "" && ![CRYPTO::verify -alg hmac-md5 -key $KEY -signature $ORIGINAL_MESSAGE_AUTHENTICATOR $UNSIGNED_REQUEST]} {
        UDP::drop
        log local0. "Message-Authenticator (AVP 80) verification failed"
        return
    }
    else {
        we've verified the Message-Authenticator, no need to have the RADIUS server verify again
        RADIUS::avp delete 80
    }
     END OF PACKET INTEGRITY VERIFICATION 
    
    
    
    
     For a minimum 8 character password and 44 character Yubikey OTP, we expect a minimum of 64 cypher bytes. 
    set CYPHER_PW_OTP [RADIUS::avp 2 string]
    if {[string length $CYPHER_PW_OTP]<64} {
        log local0. "password for [RADIUS::avp 1 string] too short BEFORE decryption"
        return
    }
    
    
    
    
    
     START OF PASSWORD DECRYPTION 
    based on suggestion from Stanislas Piron on https://devcentral.f5.com/questions/irule-to-decrypt-and-rewrite-radius-user-password-avp-62084answer159896
    binary scan $KEY_AUTH_MD5 WW bx_64bits_1 bx_64bits_2
    binary scan $CYPHER_PW_OTP W* USER_PASSWORD_W_LIST
    set PASSWORD_LIST [list]
    foreach {px_64bits_1 px_64bits_2} $USER_PASSWORD_W_LIST {
        lappend PASSWORD_LIST [expr { $px_64bits_1 ^ $bx_64bits_1 }] [expr { $px_64bits_2 ^ $bx_64bits_2 }]
        binary scan [md5 $KEY[binary format WW $px_64bits_1 $px_64bits_2]] WW bx_64bits_1 bx_64bits_2
    }
    binary scan [binary format W* $PASSWORD_LIST] A* PASSWORD_OTP
     END OF PASSWORD DECRYPTION 
    
    
    
    
    
    
     START OF OTP/PASSWORD SEPARATION 
     For a minimum 8 character password and 44 character Yubikey OTP, we expect a minimum of 52 cleartext bytes
    if {[string length $PASSWORD_OTP] > 51} {
        set OTP "[string range "$PASSWORD_OTP" [expr {[string length $PASSWORD_OTP]-44}] end]"
        set PW "[string range "$PASSWORD_OTP" 0 [expr {[string length $PASSWORD_OTP]-45}]]"
        RADIUS::avp insert 17 "$OTP" string
        RADIUS::avp insert 17 "[string range "$PASSWORD_OTP" [expr {[string length $PASSWORD_OTP]-44}] end]"
    } else {
        log local0. "password for [RADIUS::avp 1 string] too short AFTER decryption"        
        return
    }
     END OF OTP/PASSWORD SEPARATION 
    
    
    
    
    
     START OF PASSWORD NULL PADDING 
    this function has been collapsed into the next section
    
    set PW_LENGTH [expr {[string length "$PW"]}]
    set PW_MOD [expr {$PW_LENGTH%16}]
    set PW_NULL_BYTES_TO_PAD [expr {(16-$PW_MOD)%16}]
    set PW_PADDED "$PW[string repeat \x0 $PW_NULL_BYTES_TO_PAD]"
    
    the below command collapses the above 4 set commands
    set PW_PADDED "$PW[string repeat \x0 [expr {(16-[expr {[expr {[string length "$PW"]}]%16}])%16}]]"
     END OF PASSWORD NULL PADDING 
    
    
    
    
    
     START OF PASSWORD ENCRYPTION 
    binary scan $KEY_AUTH_MD5 WW bx_64bits_1 bx_64bits_2
    binary scan $PW_PADDED W* USER_PASSWORD_W_LIST
    binary scan "$PW[string repeat \x0 [expr {(16-[expr {[expr {[string length "$PW"]}]%16}])%16}]]" W* USER_PASSWORD_W_LIST
    binary scan [binary format a[expr {[string length $PW] + 16 - [string length $PW]%16}] $PW ] W* USER_PASSWORD_W_LIST
    set PASSWORD_LIST [list]
    foreach {px_64bits_1 px_64bits_2} $USER_PASSWORD_W_LIST {
        lappend PASSWORD_LIST [expr { $px_64bits_1 ^ $bx_64bits_1 }] [expr { $px_64bits_2 ^ $bx_64bits_2 }]
        binary scan [md5 $KEY[binary format WW $px_64bits_1 $px_64bits_2]] WW bx_64bits_1 bx_64bits_2
        binary scan [md5 $KEY[binary format W2 [lrange $PASSWORD_LIST end-1 end]]] WW bx_64bits_1 bx_64bits_2
    }
    binary scan [binary format W* $PASSWORD_LIST] A* CYPHER_PW
     END OF PASSWORD ENCRYPTION 
    
    
    
    
     Modify the RADIUS payload based on the above work
    RADIUS::avp replace 2 "$CYPHER_PW" string
    }
    

     

    • Stanislas_Piro2's avatar
      Stanislas_Piro2
      Icon for Cumulonimbus rankCumulonimbus

      Here is an improvement of password decryption to prevent useless encoding / decoding...

       

         START OF PASSWORD DECRYPTION 
        binary scan [md5 $RADCLIENT(KEY)$Q_AUTHENTICATOR] WW bx_64bits_1 bx_64bits_2
        binary scan $USER_PASSWORD W* USER_PASSWORD_W_LIST
        set PASSWORD_LIST [list]
        foreach {px_64bits_1 px_64bits_2} $USER_PASSWORD_W_LIST {
          lappend PASSWORD_LIST [expr { $px_64bits_1 ^ $bx_64bits_1 }] [expr { $px_64bits_2 ^ $bx_64bits_2 }]
          binary scan [md5 $RADCLIENT(KEY)[binary format WW $px_64bits_1 $px_64bits_2]] WW bx_64bits_1 bx_64bits_2
        }
        binary scan [binary format W* $PASSWORD_LIST] A* PASSWORD
        log local0. "Password is $PASSWORD"
         END OF PASSWORD DECRYPTION 
      

       

    • Tim_Haynie_3150's avatar
      Tim_Haynie_3150
      Icon for Cirrus rankCirrus

      Can you elaborate why this is an improvement? I am a newbie and barely getting my feet wet, so I'm not really sure what I'm looking at.

       

    • Stanislas_Piro2's avatar
      Stanislas_Piro2
      Icon for Cumulonimbus rankCumulonimbus

      In the first code:

       

      Create the md5 sum and convert to hex
      binary scan [md5 "$KEY$AUTHENTICATOR"] H* bx_hex
      convert the password to hex
      binary scan $CYPHER_PW_OTP H* px_full_hex
      set Password_Padded ""
       loop on the hex string by 32 char blocks
      for {set x 0} {$x<[string length $px_full_hex]} {set x [expr {$x+32}]} {
          set px_hex [string range $px_full_hex $x [expr {$x+31}]]
           string is still split to 16 char blocks because XOR only support up to 64 bits integer
           0X followed by hex string convert hex to integer, the only format supported by XOR operation
          append Password_Padded [binary format W [expr 0x[string range $px_hex 0 15] ^ 0x[string range $bx_hex 0 15]]]
          append Password_Padded [binary format W [expr 0x[string range $px_hex 16 31] ^ 0x[string range $bx_hex 16 31]]]
          binary scan [md5 "$KEY[binary format H* $px_hex]"] H* bx_hex
      }
      binary scan $Password_Padded A* PASSWORD_OTP
      log local0. "the decrypted password_OTP is $PASSWORD_OTP"

      now with the new code:

       

        START OF PASSWORD DECRYPTION 
        Create the md5 sum and convert to 2 64 bits integers
        binary scan [md5 $RADCLIENT(KEY)$Q_AUTHENTICATOR] WW bx_64bits_1 bx_64bits_2
        convert password to a list of 64 bits integers
        binary scan $USER_PASSWORD W* USER_PASSWORD_W_LIST
        set PASSWORD_LIST [list]
        foreach {px_64bits_1 px_64bits_2} $USER_PASSWORD_W_LIST {
         All operations are from 64 bits integers to 64 bits integer list.
      lappend PASSWORD_LIST [expr { $px_64bits_1 ^ $bx_64bits_1 }] [expr { $px_64bits_2 ^ $bx_64bits_2 }]
      binary scan [md5 $RADCLIENT(KEY)[binary format WW $px_64bits_1 $px_64bits_2]] WW bx_64bits_1 bx_64bits_2
        }
         Convert the Integer list to binary
        binary scan [binary format W* $PASSWORD_LIST] A* PASSWORD
        log local0. "Password is $PASSWORD"
         END OF PASSWORD DECRYPTION