DoS and NTLM Brute force protection for SIP traffic
Problem this snippet solves: This snippet has been designed to mainly protect against NTLM's downgrade attacks present in a widely used Instant Messaging solution. This solution authenticate users on the SIP/TLS protocol. This irule block brute forced users and source IP address. How to use this snippet: This snippet should be applied on the Virtual Server that handle SIP/TLS traffic. SSL bridging is required to make this irule work properly. Moreover, Skype for Business may require that you change your cipher suite to a weak one. The internal domain should be define in " email_domain " and " user_domain " variables. The script will focus on those domains. The max attempts before blocking is defined in the " max_failures " variable. This setting should be under the max attempts allowed on the Active Directory. The blocking duration is configured in the " block_duration " variable. (in seconds) The " fail_memory " variable define the window that we increase the attempt counter. After reaching the end of this duration, the entry is removed until a new invalid attempt occurs. External links Github : github.com/e-XpertSolutions/f5 Related Articles DoS and NTLM Brute force protection for HTTP(s) flow Code : when RULE_INIT { array set NTLMFlags { unicode 0x00000001 oem 0x00000002 req_target 0x00000004 unknown1 0x00000008 sign 0x00000010 seal 0x00000020 datagram 0x00000040 lmkey 0x00000080 netware 0x00000100 ntlm 0x00000200 unknown2 0x00000400 unknown3 0x00000800 ntlm_domain 0x00001000 ntlm_server 0x00002000 ntlm_share 0x00004000 NTLM2 0x00008000 targetinfo 0x00800000 128bit 0x20000000 keyexch 0x40000000 56bit 0x80000000 } set static::email_domain "domain.org" set static::user_domain "DOMAIN" set static::log_pri "local0." set static::fail_tab "NTLMfails" set static::blacklist_tab "NTLMblackhole" set static::userfail_tab "NTLMUserfails" set static::userblacklist_tab "NTLMUserblackhole" set static::max_failures 5 set static::fail_memory 300 set static::block_duration 300 } when CLIENT_ACCEPTED { if {[table lookup -subtable $static::blacklist_tab [IP::client_addr]] == 1} { log $static::log_pri "[virtual] - BLACKHOLED IPADDR [IP::client_addr]:[TCP::client_port] (Reputation=[IP::reputation [IP::client_addr]])" reject return } } when CLIENTSSL_HANDSHAKE { SSL::collect } when CLIENTSSL_DATA { set payload [SSL::payload] if { ($payload contains "3 REGISTER") } { regexp -nocase {gssapi-data=\"([A-Za-z0-9+\/=]*)\",} $payload match gssapi garbage if { [info exists match] } { unset match unset garbage if { $gssapi != "" } { set ntlm_msg [ b64decode [string trim $gssapi]] binary scan $ntlm_msg a7ci protocol zero type if { $type eq 3} { binary scan $ntlm_msg @12ssissississississii \ lmlen lmlen2 lmoff \ ntlen ntlen2 ntoff \ dlen dlen2 doff \ ulen ulen2 uoff \ hlen hlen2 hoff \ slen slen2 soff \ flags set ntlm_domain {}; binary scan $ntlm_msg @${doff}a${dlen} ntlm_domain set ntlm_user {}; binary scan $ntlm_msg @${uoff}a${ulen} ntlm_user set ntlm_host {}; binary scan $ntlm_msg @${hoff}a${hlen} ntlm_host set unicode [expr {$flags & 0x00000001}] if {$unicode} { set ntlm_domain_convert "" foreach i [ split $ntlm_domain ""] { scan $i %c c if {$c>1} { append ntlm_domain_convert $i } elseif {$c<128} { set ntlm_domain_convert $ntlm_domain_convert } else { append ntlm_domain_convert \\u[format %04.4X $c] } } set ntlm_domain $ntlm_domain_convert set ntlm_user_convert "" foreach i [ split $ntlm_user ""] { scan $i %c c if {$c>1} { append ntlm_user_convert $i } elseif {$c<128} { set ntlm_user_convert $ntlm_user_convert } else { append ntlm_user_convert \\u[format %04.4X $c] } } set ntlm_user $ntlm_user_convert set ntlm_host_convert "" foreach i [ split $ntlm_host ""] { scan $i %c c if {$c>1} { append ntlm_host_convert $i } elseif {$c<128} { set ntlm_host_convert $ntlm_host_convert } else { append ntlm_host_convert \\u[format %04.4X $c] } } set ntlm_host $ntlm_host_convert } binary scan $ntlm_msg @${ntoff}a${ntlen} ntdata binary scan $ntlm_msg @${lmoff}a${lmlen} lmdata binary scan $ntdata H* ntdata_h binary scan $lmdata H* lmdata_h set interesting 1 if { ($ntlm_domain equals $static::user_domain or $ntlm_user ends_with $static::email_domain) } { set attack 1 if {[table lookup -subtable $static::userblacklist_tab $ntlm_user] == 1} { log $static::log_pri "[virtual] - BLACKHOLED $ntlm_domain\\$ntlm_user from $ntlm_host at [IP::client_addr]:[TCP::client_port] (Reputation=[IP::reputation [IP::client_addr]])" reject return } else { log $static::log_pri "[virtual] - Login attempt by $ntlm_domain\\$ntlm_user from $ntlm_host for SIP." } } else { set attack 0 log $static::log_pri "[virtual] - Not a valid user - Login attempt by $ntlm_domain\\$ntlm_user from $ntlm_host for SIP." } } } } } # Release the payload SSL::release SSL::collect } when SERVERSSL_HANDSHAKE { SSL::collect SSL::release 0 } when SERVERSSL_DATA { set payload [SSL::payload] if {[info exists interesting] && $interesting == 1} { set client [IP::client_addr]:[TCP::client_port] set node [IP::server_addr]:[TCP::server_port] if { $payload contains "401 Unauthorized ms-user-logon-data" and ([info exists attack] and $attack == 1) } { table set -subtable $static::fail_tab -notouch -excl [IP::client_addr] 0 indef $static::fail_memory table incr -subtable $static::fail_tab [IP::client_addr] set now [clock seconds] set now_date [split [clock format $now -format {%X %x}] " "] set later [expr {$now + $static::block_duration}] set later_date [split [clock format $later -format {%X %x}] " "] if {[info exists ntlm_user]} { table set -subtable $static::userfail_tab -notouch -excl $ntlm_user 0 indef $static::fail_memory table incr -subtable $static::userfail_tab $ntlm_user if {[table lookup -subtable $static::userfail_tab $ntlm_user] >= $static::max_failures} { log $static::log_pri "[virtual] - BLACKHOLING USER - $ntlm_user at $now_date until $later_date" table set -subtable $static::userblacklist_tab -excl $ntlm_user 1 indef $static::block_duration } } if {[table lookup -subtable $static::fail_tab [IP::client_addr]] >= $static::max_failures} { log $static::log_pri "[virtual] - BLACKHOLING IPADDR - [IP::client_addr] (Reputation=[IP::reputation [IP::client_addr]]) at $now_date until $later_date" table set -subtable $static::blacklist_tab -excl [IP::client_addr] 1 indef $static::block_duration } } } SSL::release SSL::collect } Tested this on version: 11.5348Views0likes0CommentsSIP MRF ALG Support for RFC-7315 Implicit Registration
Problem this snippet solves: The SIP MRF ALG, when used with an LSN pool, provides the ability to inspect SIP messages and modify all of the appropriate headers with a CGNAT LSN pool. However, in 3GPP IMS networks, the REGISTER message uses a different SIP ID than the ID that is used in subsequent messages, such as a SUBSCRIBE. If you try to deploy the F5 ALG in this scenario, the F5 will block the SUBSCRIBE message because the From header does not match what was stored in the registration database. This is called implicit registration, and it is the mechanism by which a user is allowed to register simultaneously more than one of his/her Public User Identities. When the initial REGISTER is sent by the UE, the F5 MRF ALG creates a database entry for the REGISTER event, and reserves and IP and port for the CGNAT translation. It keeps this listener and database entry alive until the end of the timeout in the EXPIRES header. The next message is the 200 OK for the REGISTER. This contains a P-Associated-URI that gets used by the client in subsequent requests. In subsequent messages, the client will use the value from the P-Associated-URI. When the F5 sees the To: and From: headers with the new value, it drops the message because it doesn’t match what is in the database for that connection and creates an error in the log that it is an unregistered subscriber. The iRule, to fix this problem, will watch for the SIP_RESPONSE event, which is triggered when the system receives a response from the server and the SIP headers have been parsed. This is before the “MR_INGRESS” event, which happens when the system is sending it to the Message Routing SIP MRF ALG. In the SIP_RESPONSE event, we look for a CSeq that matches “* REGISTER”. If we see this, we get the P-Associated-URI value, and create a table entry that is indexed by the P-Associated-URI value with a value equal to the ID that was in the REGISTER To and From headers. The table will have a timeout value equal to the Expires header that was in the 200 OK response for the REGISTER. When the SUBSCRIBE is sent and then this packet is received by the F5 and the SIP headers have been processed, it is known as the “SIP_REQUEST” event. When it is passed over to the MRF ALG, this is known as the “MR_INGRESS” event. In the SIP_REQUEST event, we look for a SUBSCRIBE message. We lookup the P-Associated-URI value in the From header, and see if there is a corresponding table entry. If there is, we save a copy of the SIP From, and To headers. We then replace those headers by changing the P-Associated-URI value back to the MCPTT value. Now, when MR_INGRESS fires, and it is sent to the SIP ALG, it sees that the From and To headers are the same as the database entry and it allow it. The next event, MR_EGRESS, means it has left the Message Routing ALG, and then the event that occurs is SIP_REQUEST_SEND. This means it has been processed by MRF, and is now being sent to the server. In the SIP_REQUEST_SEND event, we change the headers back to how they were originally. In addition, this iRule inserts and "ep=" header into the Contact header that contains the original client IP for a REGISTER message, and inserts a header called "P-UE-Flow-Info" that contains information about the original subscriber. This is needed by some CSCF servers. How to use this snippet: Create a virtual server with an MRF session and MRF routing profile and apply it. An example: ltm virtual vs-sip-alg-v6 { destination 2002:abcd:f5:20::.sip ip-protocol udp last-modified-time 2018-09-04:11:20:51 mask ffff:ffff:ffff:ffff:: profiles { my-siprouter-alg { } my-sipsession-alg { } udp { } } rules { SIP-Headers } source-address-translation { pool sip-alg-lsn-pool-v6 type lsn } translate-address enabled translate-port enabled vlans { clients } vlans-enabled vs-index 3 } ltm message-routing sip profile router my-siprouter-alg { app-service none defaults-from siprouter-alg traffic-group traffic-group-1 } ltm message-routing sip profile router siprouter-alg { app-service none defaults-from siprouter nonregister-subscriber-callout disabled operation-mode application-level-gateway traffic-group traffic-group-1 } ltm message-routing sip profile session my-sipsession-alg { app-service none defaults-from sipsession-alg } Code : when CLIENT_ACCEPTED { #Get info about client in variables to use later in other events like SIP_REQUEST_SEND #This is needed for inserting the P-UE-Flow-Info information set clientaddr [IP::client_addr] set clientport [UDP::client_port] set vip [IP::local_addr] set vipport [UDP::local_port] } when MR_INGRESS { } when MR_EGRESS { } when SIP_REQUEST { #Look at the SIP method. If it is a SUBSCRIBE, NOTIFY, or INVITE, we need to look this up in the table and switch "[SIP::method]" { "SUBSCRIBE" - "NOTIFY" - "INVITE" { #Mvalue is the value of the SIP ID inside the SIP::from header between the : and @ symbol #Example From: <>;tag=dc11036c the mvalue is +11395600061 set mvalue "[lindex [split [lindex [split [SIP::from] @] 0] :] 1]" #Lookup the value in the table that shows the original SIP ID in the REGISTER message #Example if it was From: <312670100004861>;tag=08bcb27c #Would be 312670100004861 #nvalue is that value we find in the table set nvalue "[table lookup $mvalue]" #Check if there is a table value. If so then grab old headers and then replace with new information if { $nvalue ne "" } { #Save the header values as X-From and X-To so we can put them back in the SIP_REQUEST_SEND event SIP::header insert X-From "[SIP::header value From]" SIP::header insert X-To "[SIP::header value To]" #Use these to pass into the procedure to get the new header value set oldfrom "[SIP::header value From]" set oldto "[SIP::header value To]" #Run proc to get the new header value that inserts new number into field set newfrom "[call replace_number_fromto_field $oldfrom $nvalue]" set newto "[call replace_number_fromto_field $oldto $nvalue]" #Replace the SIP From and To headers with the new values SIP::header replace From "$newfrom" SIP::header replace To "$newto" } } default { } } } when SIP_REQUEST_SEND { #If this is one of the events that we modified the headers, put the headers back the way they were. switch "[SIP::method]" { "SUBSCRIBE" - "NOTIFY" - "INVITE" { #Check if the X-From and X-To have anything in them. If they do, put the headers back with that information #Remove the X-From and X-To headers before sending to the server if { [SIP::header value X-From] ne "" } { SIP::header replace From "[SIP::header value X-From]" SIP::header remove X-From } if { [SIP::header value X-To] ne "" } { SIP::header replace To "[SIP::header value X-To]" SIP::header remove X-To } #The following only applies if this is an INVITE if { [SIP::method] eq "INVITE" } { #The following is the logic for inserting the P-UE-Flow-Info header #The UE address is just the client_address #The clientport is the UE original port set UE "\"\[$clientaddr\]:$clientport\"" #Find the audio port log local0. "First m= is [findstr [SIP::message] "m=audio " 8 " "] " set audioport "[findstr [SIP::message] "m=audio " 8 " "]" log local0. "Audioport is $audioport" set applicationport "[findstr [SIP::message] "m=application " 14 " "]" log local0. "Applicationport is $applicationport" set header "sig;src=$UE;msrc=\"\[[IP::local_addr]\]:$vipport\";dst=\"\[$vip\]:[UDP::local_port]\",audio;src=$UE;msrc=\"\[[IP::local_addr]\]:$vipport\";dst=\"\[$vip\]:[UDP::local_port]\"" log local0. "P-UE-Flow-Info header is = $header" SIP::header insert P-UE-Flow-Info "$header" } } default { } } } when SIP_RESPONSE { #log local0. "Message in response is [SIP::message]" #Check if response is a REGISTER. If so create a table entry for it #Table entry is indexed by the P-Associated-URI with a value of the original SIP ID in the REGISTER message switch -glob "[SIP::header value CSeq]" { "* REGISTER" { if { [SIP::header value P-Associated-URI] ne ""} { set myuri "[SIP::header value P-Associated-URI]" #Get the value between the : and the @ symbol #Example P-Associated-URI is #P-Associated-URI: <>,<> set pvalue "[lindex [split [lindex [split $myuri @] 0] :] 1]" #fvalue is the value between the : and @ symbol in the original from #example original from is sip:312670100004861@ims.mnc180.mcc310.3gppnetwork.org so fvalue is 312670100004861 set fvalue "[lindex [split [lindex [split [SIP::from] @] 0] :] 1]" #We need to look at the Contact header to determine the value of the Expires there #A procedure called find_expires returns 0 if it couldn't parse it and the number if it could # set expires "[call find_expires [SIP::header value Contact]]" #Create a table entry indexed by the P-Associated-URI as the key with a value equal to the original SIP ID table set $pvalue $fvalue $expires } else { #If there was no P-Associated-URI we can't create a table entry log local0. "No P-Associated-URI found for [SIP::from] [SIP::to] in REGISTER response" } } "default" { #Default action for an event that didn't match above } } } when SIP_RESPONSE_SEND { } proc find_expires { contact } { #Look for the word "expires=" and then get the number value after that and before the ";" character set found [scan [string range $contact [string first expires= $contact] end] {expires=%[0-9];} expires] #Return a value of 0 if the above couldn't be parsed properly #A good example is Contact: <312670100004861>;expires=3600;+sip.instance=" ";mobility="mobile";+g.3gpp.icsi-ref="urn%3Aurn-7%3A3gpp-service.ims.icsi.mcptt";video;+g.3gpp.mcptt if { $found } { return $expires } else { return 0 } } proc replace_number_fromto_field { str newnumber } { #Parses the string passed in and splits it up into a scheme, number, domain, and tag #Then puts the newnumber string into there replacing the previous number #Supposed to be used with the SIP::header value from and SIP::header value to # scan $str {%[^:]:%[^@]@%[^;];%s} scheme number domain tag if { [info exists tag] } { set newheader "$scheme:$newnumber@$domain;$tag" } else { set newheader "$scheme:$newnumber@$domain" } return $newheader } proc replace_number_contact_field { str newnumber } { #Pass in a string called str and newnumber and replace the number in the string #Assumes a string similar to the following #Contact: <312670100004861>;+sip.instance=" ";mobility="mobile";+g.3gpp.icsi-ref="urn%3Aurn-7%3A3gpp-service.ims.icsi.mcptt";video;+g.3gpp.mcptt #Split this up by first ":" the "@" symbol and remainder scan $str {%[^:]:%[^@]@%s} scheme number remainder set newheader "$scheme:$newnumber@$remainder" #Returns a header in the format above with the newnumber in place return $newheader }552Views0likes1Comment