Forum Discussion
iRule to decrypt and rewrite RADIUS User-Password AVP
- Oct 23, 2018
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 }
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_Piro2Oct 23, 2018Cumulonimbus
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_3150Oct 23, 2018Cirrus
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_Piro2Oct 23, 2018Cumulonimbus
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
- Tim_Haynie_3150Oct 28, 2018Cirrus
Thanks for this suggestion! It worked great and even prevented warnings from being thrown into the log every time I clicked update.
I have the final working iRule updated above with all functions complete.
- Stanislas_Piro2Oct 28, 2018Cumulonimbus
I suggest to use this to add padding bytes to the password
binary scan [binary format a[expr {[string length $PW] + 16 - [string length $PW]%16}] $PW ] W* USER_PASSWORD_W_LIST
I tried this code successfully:
% set PW "MySuperVeryLongPassword" MySuperVeryLongPassword % binary scan "$PW[string repeat \x0 [expr {(16-[expr {[expr {[string length "$PW"]}]%16}])%16}]]" W* USER_PASSWORD_W_LIST 1 % echo $USER_PASSWORD_W_LIST 5582584976964416086 7310038514369718096 7022083200808805376 0 % binary scan [binary format a[expr {[string length $PW] + 16 - [string length $PW]%16}] $PW ] W* USER_PASSWORD_W_LIST 1 % echo $USER_PASSWORD_W_LIST 5582584976964416086 7310038514369718096 7022083200808805376 0
- Stanislas_Piro2Oct 28, 2018Cumulonimbus
you can also replace :
set OTP "[string range "$PASSWORD_OTP" [expr {[string length $PASSWORD_OTP]-44}] [string length $PASSWORD_OTP]]" set PW "[string range "$PASSWORD_OTP" 0 [expr {[string length $PASSWORD_OTP]-45}]]"
with
set OTP [string range $PASSWORD_OTP [string length $PASSWORD_OTP]-44 end] set PW [string range $PASSWORD_OTP 0 [string length $PASSWORD_OTP]-45]
- Tim_Haynie_3150Oct 29, 2018Cirrus
It seems I still need the "expr" term for the "set OTP" and "set PW" commands, otherwise it evaluates the subtraction as a string, as shown in the log:
Oct 28 22:45:14 ams-it-ltm02 err tmm1[17926]: 01220001:3: TCL error: /Common/AA-TEST-OTP-IRULE - bad index "53-44": must be integer or end?-integer? while executing "string range $PASSWORD_OTP [string length $PASSWORD_OTP]-44 end"
But I've updated accordingly with both suggestions, thanks once again.
- Stanislas_Piro2Oct 30, 2018Cumulonimbus
You can try to change this whole code
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
with
RADIUS::avp replace 80 [binary format a16 ""] 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 [UDP::payload]]} { 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
This can be useful to add MESSAGE-AUTHENTICATOR on server side... add the following code at the end of the irule (this require you remove the line $1)
if { "$ORIGINAL_MESSAGE_AUTHENTICATOR" != "" } { RADIUS::avp replace 80 [CRYPTO::sign -alg hmac-md5 -key $KEY [UDP::payload]] string }
MESSAGE-AUTHENTICATOR is not set to check the request from client but to sign every request for integrity check.
How can you say there is no device which can change request from F5 to server to add, remove or replace non encrypted AVP?
- Tim_Haynie_3150Oct 30, 2018Cirrus
The VS and servers are on the same VLAN so I know with 100% certainty there are no devices between which will modify the payload.
Recent Discussions
Related Content
* Getting Started on DevCentral
* Community Guidelines
* Community Terms of Use / EULA
* Community Ranking Explained
* Community Resources
* Contact the DevCentral Team
* Update MFA on account.f5.com