From: Antonio Torres Date: Thu, 11 Jul, 2024 Subject: Backport fix for BlastRADIUS Manual backport from v3.0.x branch, commit range 3a00a6ecc188629b0441fd45ad61ca8986de156e..da643f1edc267ce95260dc36069e6f1a7a4d66f8 Resolves: RHEL-46572 Signed-off-by: Antonio Torres --- man/man1/radclient.1 | 10 ++- man/man1/radtest.1 | 11 ++- raddb/clients.conf | 47 +++++++++--- raddb/proxy.conf | 19 +++++ raddb/radiusd.conf.in | 185 ++++++++++++++++++++++++++++++++++++++++++++++++ src/include/clients.h | 6 +- src/include/conffile.h | 1 + src/include/libradius.h | 19 ++++- src/include/radius.h | 1 + src/include/radiusd.h | 6 ++ src/include/realms.h | 1 + src/lib/radius.c | 87 ++++++++++++++++++++--- src/main/client.c | 45 ++++++++++-- src/main/conffile.c | 4 +- src/main/listen.c | 141 +++++++++++++++++++++++++++++++++++- src/main/mainconfig.c | 70 ++++++++++++++++++ src/main/process.c | 65 +++++++++++++++++ src/main/radclient.c | 147 +++++++++++++++++++++++++++++++++++++- src/main/radtest.in | 6 +- src/main/realms.c | 11 +++ src/main/tls_listen.c | 5 ++ 21 files changed, 855 insertions(+), 32 deletions(-) diff --git a/man/man1/radclient.1 b/man/man1/radclient.1 index 229dcae0c7..b83bee931a 100644 --- a/man/man1/radclient.1 +++ b/man/man1/radclient.1 @@ -1,10 +1,11 @@ -.TH RADCLIENT 1 "22 March 2019" "" "FreeRADIUS Daemon" +.TH RADCLIENT 1 "21 May 2024" "" "FreeRADIUS Daemon" .SH NAME radclient - send packets to a RADIUS server, show reply .SH SYNOPSIS .B radclient .RB [ \-4 ] .RB [ \-6 ] +.RB [ \-b ] .RB [ \-c .IR count ] .RB [ \-d @@ -52,6 +53,13 @@ automatically encrypted before the packet is sent to the server. Use IPv4 (default) .IP \-6 Use IPv6 +.IP \-b +Enforce the Blast RADIUS checks. All replies to an Access-Request packet +must contain a Message-Authenticator as the first attribute. + +For compatibility with old servers, this flag is not set by default. +However, radclient still checks for the Blast RADIUS signature, and +discards packets which match the attack. .IP \-c\ \fIcount\fP Send each packet \fIcount\fP times. .IP \-d\ \fIraddb_directory\fP diff --git a/man/man1/radtest.1 b/man/man1/radtest.1 index b3184779c0..db32e7f68d 100644 --- a/man/man1/radtest.1 +++ b/man/man1/radtest.1 @@ -1,4 +1,4 @@ -.TH RADTEST 1 "5 April 2010" "" "FreeRADIUS Daemon" +.TH RADTEST 1 "21 May 2024" "" "FreeRADIUS Daemon" .SH NAME radtest - send packets to a RADIUS server, show reply .SH SYNOPSIS @@ -15,6 +15,8 @@ radtest - send packets to a RADIUS server, show reply .IR ] .RB [ \-6 .IR ] +.RB [ \-b +.IR ] .I user password radius-server nas-port-number secret .RB [ ppphint ] .RB [ nasname ] @@ -26,6 +28,13 @@ way to test a radius server. .SH OPTIONS +.IP \-b +Enforce the Blast RADIUS checks. All replies to an Access-Request packet +must contain a Message-Authenticator as the first attribute. + +For compatibility with old servers, this flag is not set by default. +However, radclient still checks for the Blast RADIUS signature, and +discards packets which match the attack. .IP "\-d \fIraddb_directory\fP" The directory that contains the RADIUS dictionary files. Defaults to \fI/etc/raddb\fP. diff --git a/raddb/clients.conf b/raddb/clients.conf index 76b300d3c5..d55414b7d2 100644 --- a/raddb/clients.conf +++ b/raddb/clients.conf @@ -100,15 +100,44 @@ client localhost { secret = testing123 # - # Old-style clients do not send a Message-Authenticator - # in an Access-Request. RFC 5080 suggests that all clients - # SHOULD include it in an Access-Request. The configuration - # item below allows the server to require it. If a client - # is required to include a Message-Authenticator and it does - # not, then the packet will be silently discarded. - # - # allowed values: yes, no - require_message_authenticator = no + # The global configuration "security.require_message_authenticator" + # flag sets the default for all clients. That default can be + # over-ridden here, by setting it to a value. If no value is set, + # then the default from the "radiusd.conf" file is used. + # + # See that file for full documentation on the flag, along + # with allowed values and meanings. + # + # This flag exists solely for legacy clients which do not send + # Message-Authenticator in all Access-Request packets. We do not + # recommend setting it to "no". + # + # The number one way to protect yourself from the BlastRADIUS + # attack is to update all RADIUS servers, and then set this + # flag to "yes". If all RADIUS servers are updated, and if + # all of them have this flag set to "yes" for all clients, + # then your network is safe. You can then upgrade the + # clients when it is convenient, instead of rushing the + # upgrades. + # + # allowed values: yes, no, auto +# require_message_authenticator = no + + # + # The global configuration "security.limit_proxy_state" + # flag sets the default for all clients. That default can be + # over-ridden here, by setting it to "no". + # + # See that file for full documentation on the flag, along + # with allowed values,and meanings. + # + # This flag exists solely for legacy clients which do not send + # Message-Authenticator in all Access-Request packets. We do not + # recommend setting it to "no". + # + # allowed values: yes, no, auto + # +# limit_proxy_state = yes # # The short name is used as an alias for the fully qualified diff --git a/raddb/proxy.conf b/raddb/proxy.conf index 91b4b37930..fa362b8a74 100644 --- a/raddb/proxy.conf +++ b/raddb/proxy.conf @@ -204,6 +204,25 @@ home_server localhost { # secret = testing123 + + # + # The global configuration "security.require_message_authenticator" + # flag sets the default for all home servers. That default can be + # over-ridden here, by setting it to a value. If no value is set, + # then the default from the "radiusd.conf" file is used. + # + # See that file for full documentation on the flag, along + # with allowed values and meanings. + # + # This flag exists solely for legacy home servers which do + # not send Message-Authenticator in all Access-Accept, + # Access-Reject, or Access-Challenge packets. We do not + # recommend setting it to "no". + # + # allowed values: yes, no, auto + # +# require_message_authenticator = no + ############################################################ # # The rest of the configuration items listed here are optional, diff --git a/raddb/radiusd.conf.in b/raddb/radiusd.conf.in index e8aee3c001..6fa9ded0f9 100644 --- a/raddb/radiusd.conf.in +++ b/raddb/radiusd.conf.in @@ -564,6 +564,191 @@ security { # status_server = yes + # + # Global configuration for requiring Message-Authenticator in + # all Access-* packets sent over UDP or TCP. This flag is + # ignored for TLS. + # + # The number one way to protect yourself from the BlastRADIUS + # attack is to update all RADIUS servers, and then set this + # flag to "yes". If all RADIUS servers are updated, and if + # all of them have this flag set to "yes" for all clients, + # then your network is safe. You can then upgrade the + # clients when it is convenient, instead of rushing the + # upgrades. + # + # This flag sets the global default for all clients and home + # servers. It can be over-ridden in an individual client or +# home_server definition by adding the same flag to that + # section with an appropriate value. + # + # All upgraded RADIUS implementations should send + # Message-Authenticator in all Access-Request, Access-Accept, + # Access-Reject, and Access-Challenge packets. Once all + # systems are upgraded, setting this flag to "yes" is the + # best protection from the attack. + # + # The possible values and meanings for + # "require_message_authenticator" are; + # + # * "no" - allow Access-* packet which do not contain + # Message-Authenticator + # + # For a client, if this flag is set to "no", then the + # "limit_proxy_state" flag, below, is also checked. + # + # For a home_server, if this flag is set to "no", then the + # Access-Accept, Access-Reject, and Access-Challenge + # packets do not need to contain Message-Authenticator. + # + # The only reason to set this flag to "no" is when the + # RADIUS client or home server has not been updated. It is + # always safer to set this flag "no" in the individual + # client or home_server definition. The global flag SHOULD + # still be set to a safe value: "yes". + # + # WARNING: Setting this flag and the "limit_proxy_state" + # flag to "no" will allow MITM attackers to create fake + # Access-Accept packets to the NAS! At least one of them + # MUST be set to "yes" for the system to have any + # protection against the attack. + # + # * "yes" - Require that all Access-* packets (client and + # home_server) contain Message-Authenticator. If a packet + # does not contain Message-Authenticator, then it is + # discarded. + # + # * "auto" - Automatically determine the value of the flag, + # based on the first packet received from that client or + # home_server. + # + # If the packet does not contain Message-Authenticator, + # then the value of the flag is automatically switched to + # "no". + # + # If the packet contains Message-Authenticator but not + # EAP-Message, then the value of the flag is automatically + # switched to "yes". The server has to check for + # EAP-Message, because the previous RFCs require that the + # packet contains Message-Authenticator when it also + # contains EAP-Message. So having a Message-Authenticator + # in those packets doesn't give the server enough + # information to determined if the client or home_server + # has been updated. + # + # If the packet contains Message-Authenticator and + # EAP-Message, then the flag is left at the "auto" value. + # + # WARNING: This switch is done for the first packet + # received from that client or home server. The change + # does NOT persist across server restarts. You MUST change + # the to "yes" manually, in order to make a permanent + # change to the configuration. + # + # WARNING: If there are multiple NASes with the same source + # IP and client definitions, BUT the NASes have different + # behavior, then this flag WILL LIKELY BREAK YOUR NETWORK. + # + # That is, when there are multiple different RADIUS clients + # behind one NATed IP address, then these security settings + # have to be set to allow the MOST INSECURE packets to be + # processed. This is a terrible idea, and will leave your + # network vulnerable to the attack. Please upgrade all + # clients immediately. + # + # The only solution to that rare configuration is to set + # this flag to "no", in which case the network will work, + # but will be vulnerable to the attack. + # + require_message_authenticator = auto + + # + # Global configuration for limiting the combination of + # Proxy-State and Message-Authenticator. This flag only + # applies to packets sent over UDP or TCP. This flag is + # ignored for TLS. + # + # This flag sets the global default for all clients. It can + # be over-ridden in an individual client definition by adding + # the same flag to that section with an appropriate value. + # + # If "require_message_authenticator" is set to "yes", this + # configuration item is ignored. + # + # If "require_message_authenticator" is set to "no", this + # configuration item is checked. + # +# The possible values and meanings for "limit_proxy_state" are; + # + # * "no" - allow any packets from the client, even packets + # which contain the BlastRADIUS attack. Please be aware + # that in this configuration the server will complain for + # EVERY packet which it receives. + # + # The only reason to set this flag to "no" is when the + # client is a proxy, AND the proxy does not send + # Message-Authenticator in Access-Request packets. Even + # then, the best approach to fix the issue is to (1) update + # the proxy to send Message-Authenticator, and if that + # can't be done, then (2) set this flag to "no", but ONLY + # for that one client. The global flag SHOULD still be set + # to a safe value: "yes". + # + # WARNING: Setting both this flag and the + # "require_message_authenticator" flag to "no" will allow + # MITM attackers to create fake Access-Accept packets to the + # NAS! At least one of them MUST be set to "yes" for the + # system to have any protection against the attack. + # + # * "yes" - Allow packets without Message-Authenticator, + # but only when they do not contain Proxy-State. + # packets which contain Proxy-State MUST also contain + # Message-Authenticator, otherwise they are discarded. + # + # This setting is safe for most NASes, GGSNs, BRAS, etc. + # Most regular RADIUS clients do not send Proxy-State + # attributes for Access-Request packets that they originate. + # However some aggregators (e.g. Wireless LAN Controllers) + # may act as a RADIUS proxy for requests from their cohort + # of managed devices, and in such cases will provide a + # Proxy-State attribute. For those systems, you _must_ look + # at the actual packets to determine what to do. It may be + # that the only way to fix the vulnerability is to upgrade + # the WLC, and set "require_message_authenticator" to "yes". + # + # * "auto" - Automatically determine the value of the flag, + # based on the first packet received from that client. + # + # If the packet contains Proxy-State but no + # Message-Authenticator, then the value of the flag is + # automatically switched to "no". + # + # For all other situations, the value of the flag is + # automatically switched to "yes". + # + # WARNING: This switch is done for the first packet + # received from that client. The change does NOT persist + # across server restarts. You MUST change the to "yes" + # manually, in order to make a permanent change to the + # configuration. + # + # WARNING: If there are multiple NASes with the same source + # IP and client definitions, BUT the NASes have different + # behavior, then this flag WILL LIKELY BREAK YOUR NETWORK. + # + # That is, when there are multiple different RADIUS clients + # behind one NATed IP address, then these security settings + # have to be set to allow the MOST INSECURE packets to be + # processed. This is a terrible idea, and will leave your + # network vulnerable to the attack. Please upgrade all + # clients immediately. + # + # The only solution to that rare configuration is to set + # this flag to "no", in which case the network will work, + # but will be vulnerable to the attack. + # + limit_proxy_state = auto + @openssl_version_check_config@ } diff --git a/src/include/clients.h b/src/include/clients.h index 560211557f..2cde3a2e37 100644 --- a/src/include/clients.h +++ b/src/include/clients.h @@ -39,7 +39,11 @@ typedef struct radclient { char const *secret; //!< Secret PSK. - bool message_authenticator; //!< Require RADIUS message authenticator in requests. + fr_bool_auto_t require_ma; //!< Require RADIUS message authenticator in requests. + + bool dynamic_require_ma; //!< for dynamic clients + + fr_bool_auto_t limit_proxy_state; //!< Limit Proxy-State in requests char const *nas_type; //!< Type of client (arbitrary). diff --git a/src/include/conffile.h b/src/include/conffile.h index 8cb045c946..ddbcae4e4f 100644 --- a/src/include/conffile.h +++ b/src/include/conffile.h @@ -140,6 +140,7 @@ typedef struct timeval _timeval_t; #define PW_TYPE_MULTI (1 << 18) //!< CONF_PAIR can have multiple copies. #define PW_TYPE_NOT_EMPTY (1 << 19) //!< CONF_PAIR is required to have a non zero length value. #define PW_TYPE_FILE_EXISTS ((1 << 20) | PW_TYPE_STRING) //!< File matching value must exist +#define PW_TYPE_IGNORE_DEFAULT (1 << 21) //!< don't set from .dflt if the CONF_PAIR is missing /* @} **/ #define FR_INTEGER_COND_CHECK(_name, _var, _cond, _new)\ diff --git a/src/include/libradius.h b/src/include/libradius.h index ce2f713de1..2efef8b1d3 100644 --- a/src/include/libradius.h +++ b/src/include/libradius.h @@ -402,6 +402,10 @@ typedef struct radius_packet { size_t partial; int proto; #endif + bool tls; //!< uses secure transport + bool message_authenticator; + bool proxy_state; + bool eap_message; } RADIUS_PACKET; typedef enum { @@ -507,6 +511,13 @@ DICT_VENDOR *dict_vendorbyvalue(int vendor); /* radius.c */ int rad_send(RADIUS_PACKET *, RADIUS_PACKET const *, char const *secret); bool rad_packet_ok(RADIUS_PACKET *packet, int flags, decode_fail_t *reason); + +/* + * 1 == require_ma + * 2 == msg_peek + * 4 == limit_proxy_state + * 8 == require_ma for Access-* replies and Protocol-Error + */ RADIUS_PACKET *rad_recv(TALLOC_CTX *ctx, int fd, int flags); ssize_t rad_recv_header(int sockfd, fr_ipaddr_t *src_ipaddr, uint16_t *src_port, int *code); void rad_recv_discard(int sockfd); @@ -694,7 +705,7 @@ extern bool fr_dns_lookups; /* do IP -> hostname lookups? */ extern bool fr_hostname_lookups; /* do hostname -> IP lookups? */ extern int fr_debug_lvl; /* 0 = no debugging information */ extern uint32_t fr_max_attributes; /* per incoming packet */ -#define FR_MAX_PACKET_CODE (52) +#define FR_MAX_PACKET_CODE (53) extern char const *fr_packet_codes[FR_MAX_PACKET_CODE]; #define is_radius_code(_x) ((_x > 0) && (_x < FR_MAX_PACKET_CODE)) extern FILE *fr_log_fp; @@ -932,6 +943,12 @@ int fr_socket_wait_for_connect(int sockfd, struct timeval *timeout); } #endif +typedef enum { + FR_BOOL_FALSE = 0, + FR_BOOL_TRUE, + FR_BOOL_AUTO, +} fr_bool_auto_t; + #include #ifdef WITH_TCP diff --git a/src/include/radius.h b/src/include/radius.h index 473528d65d..147d674eed 100644 --- a/src/include/radius.h +++ b/src/include/radius.h @@ -61,6 +61,7 @@ typedef enum { PW_CODE_COA_REQUEST = 43, //!< RFC3575/RFC5176 - CoA-Request PW_CODE_COA_ACK = 44, //!< RFC3575/RFC5176 - CoA-Ack (positive) PW_CODE_COA_NAK = 45, //!< RFC3575/RFC5176 - CoA-Nak (not willing to perform) + PW_CODE_PROTOCOL_ERROR = 52, //!< RFC7930 - Protocol layer issue PW_CODE_MAX = 255, //!< Maximum possible code } PW_CODE; diff --git a/src/include/radiusd.h b/src/include/radiusd.h index 0af3f51458..bd0aa668d0 100644 --- a/src/include/radiusd.h +++ b/src/include/radiusd.h @@ -171,6 +171,10 @@ typedef struct main_config { bool exiting; //!< are we exiting? + fr_bool_auto_t require_ma; //!< global configuration for all clients and home servers + + fr_bool_auto_t limit_proxy_state; //!< global configuration for all clients + #ifdef ENABLE_OPENSSL_VERSION_CHECK char const *allow_vulnerable_openssl; //!< The CVE number of the last security issue acknowledged. @@ -557,6 +561,8 @@ int main_config_free(void); void main_config_hup(void); void hup_logfile(void); +int fr_bool_auto_parse(CONF_PAIR *cp, fr_bool_auto_t *out, char const *str); + /* listen.c */ void listen_free(rad_listen_t **head); int listen_init(CONF_SECTION *cs, rad_listen_t **head, bool spawn_flag); diff --git a/src/include/realms.h b/src/include/realms.h index 6dae8b4f85..ea5665c56c 100644 --- a/src/include/realms.h +++ b/src/include/realms.h @@ -59,6 +59,7 @@ typedef struct home_server { //!< stats or when specifying home servers for a pool. bool dual; //!< One of a pair of homeservers on consecutive ports. + fr_bool_auto_t require_ma; //!< for all replies to Access-Request and Status-Server char const *server; //!< For internal proxying char const *parent_server; diff --git a/src/lib/radius.c b/src/lib/radius.c index 3881111f7d..556f3b961e 100644 --- a/src/lib/radius.c +++ b/src/lib/radius.c @@ -142,8 +142,9 @@ char const *fr_packet_codes[FR_MAX_PACKET_CODE] = { "47", "48", "49", - "IP-Address-Allocate", - "IP-Address-Release", //!< 50 + "IP-Address-Allocate", //!< 50 + "IP-Address-Release", + "Protocol-Error", }; @@ -1700,6 +1701,15 @@ int rad_vp2attr(RADIUS_PACKET const *packet, RADIUS_PACKET const *original, return rad_vp2vsa(packet, original, secret, pvp, start, room); } +static const bool code2ma[FR_MAX_PACKET_CODE] = { + [ PW_CODE_ACCESS_REQUEST ] = true, + [ PW_CODE_ACCESS_ACCEPT ] = true, + [ PW_CODE_ACCESS_REJECT ] = true, + [ PW_CODE_ACCESS_CHALLENGE ] = true, + [ PW_CODE_STATUS_SERVER ] = true, + [ PW_CODE_PROTOCOL_ERROR ] = true, +}; + /** Encode a packet * @@ -1712,6 +1722,7 @@ int rad_encode(RADIUS_PACKET *packet, RADIUS_PACKET const *original, uint16_t total_length; int len; VALUE_PAIR const *reply; + bool seen_ma = false; /* * A 4K packet, aligned on 64-bits. @@ -1775,6 +1786,27 @@ int rad_encode(RADIUS_PACKET *packet, RADIUS_PACKET const *original, * memcpy. */ + /* + * Always add Message-Authenticator for replies to + * Access-Request packets, and for all Access-Accept, + * Access-Reject, Access-Challenge. + * + * It must be the FIRST attribute in the packet. + */ + if (!packet->tls && + ((code2ma[packet->code]) || (original && code2ma[original->code]))) { + seen_ma = true; + + packet->offset = RADIUS_HDR_LEN; + + ptr[0] = PW_MESSAGE_AUTHENTICATOR; + ptr[1] = 18; + memset(ptr + 2, 0, 16); + + ptr += 18; + total_length += 18; + } + /* * Loop over the reply attributes for the packet. */ @@ -1832,6 +1864,13 @@ int rad_encode(RADIUS_PACKET *packet, RADIUS_PACKET const *original, * length and initial value. */ if (!reply->da->vendor && (reply->da->attr == PW_MESSAGE_AUTHENTICATOR)) { + /* + * We have already encoded the Message-Authenticator, don't do it again. + */ + if (seen_ma) { + reply = reply->next; + continue; + } if (room < 18) break; /* @@ -2323,6 +2362,8 @@ bool rad_packet_ok(RADIUS_PACKET *packet, int flags, decode_fail_t *reason) radius_packet_t *hdr; char host_ipaddr[128]; bool require_ma = false; + bool limit_proxy_state = false; + bool seen_proxy_state = false; bool seen_ma = false; uint32_t num_attributes; decode_fail_t failure = DECODE_FAIL_NONE; @@ -2371,15 +2412,26 @@ bool rad_packet_ok(RADIUS_PACKET *packet, int flags, decode_fail_t *reason) } /* - * Message-Authenticator is required in Status-Server - * packets, otherwise they can be trivially forged. + * If the caller requires Message-Authenticator, then set + * the flag. */ - if (hdr->code == PW_CODE_STATUS_SERVER) require_ma = true; /* - * It's also required if the caller asks for it. + * We also require Message-Authenticator if the packet + * code is Status-Server. + * + * If we're receiving packets from a proxy socket, then + * require Message-Authenticator for Access-* replies, + * and for Protocol-Error. */ - if (flags) require_ma = true; + require_ma = ((flags & 0x01) != 0) || (hdr->code == PW_CODE_STATUS_SERVER) || (((flags & 0x08) != 0) && code2ma[hdr->code]); + + /* + * + * We only limit Proxy-State if we're not requiring + * Message-Authenticator. + */ + limit_proxy_state = ((flags & 0x04) != 0) && !require_ma; /* * Repeat the length checks. This time, instead of @@ -2534,6 +2586,7 @@ bool rad_packet_ok(RADIUS_PACKET *packet, int flags, decode_fail_t *reason) case PW_EAP_MESSAGE: require_ma = true; eap = true; + packet->eap_message = true; break; case PW_USER_PASSWORD: @@ -2542,6 +2595,11 @@ bool rad_packet_ok(RADIUS_PACKET *packet, int flags, decode_fail_t *reason) non_eap = true; break; + case PW_PROXY_STATE: + seen_proxy_state = true; + packet->proxy_state = true; + break; + case PW_MESSAGE_AUTHENTICATOR: if (attr[1] != 2 + AUTH_VECTOR_LEN) { FR_DEBUG_STRERROR_PRINTF("Malformed RADIUS packet from host %s: Message-Authenticator has invalid length %d", @@ -2553,6 +2611,7 @@ bool rad_packet_ok(RADIUS_PACKET *packet, int flags, decode_fail_t *reason) goto finish; } seen_ma = true; + packet->message_authenticator = true; break; } @@ -2609,7 +2668,19 @@ bool rad_packet_ok(RADIUS_PACKET *packet, int flags, decode_fail_t *reason) * Message-Authenticator attributes. */ if (require_ma && !seen_ma) { - FR_DEBUG_STRERROR_PRINTF("Insecure packet from host %s: Packet does not contain required Message-Authenticator attribute", + FR_DEBUG_STRERROR_PRINTF("Insecure packet from host %s: Packet does not contain required Message-Authenticator attribute. You may need to set \"require_message_authenticator = no\" in the configuration.", + inet_ntop(packet->src_ipaddr.af, + &packet->src_ipaddr.ipaddr, + host_ipaddr, sizeof(host_ipaddr))); + failure = DECODE_FAIL_MA_MISSING; + goto finish; + } + + /* + * The client is a NAS which shouldn't send Proxy-State, but it did! + */ + if (limit_proxy_state && seen_proxy_state && !seen_ma) { + FR_DEBUG_STRERROR_PRINTF("Insecure packet from host %s: Packet does not contain required Message-Authenticator attribute, but still has one or more Proxy-State attributes", inet_ntop(packet->src_ipaddr.af, &packet->src_ipaddr.ipaddr, host_ipaddr, sizeof(host_ipaddr))); diff --git a/src/main/client.c b/src/main/client.c index abcc15d225..af5b122af5 100644 --- a/src/main/client.c +++ b/src/main/client.c @@ -279,7 +279,8 @@ bool client_add(RADCLIENT_LIST *clients, RADCLIENT *client) (old->coa_server == client->coa_server) && (old->coa_pool == client->coa_pool) && #endif - (old->message_authenticator == client->message_authenticator)) { + (old->require_ma == client->require_ma) && + (old->limit_proxy_state == client->limit_proxy_state)) { WARN("Ignoring duplicate client %s", client->longname); client_free(client); return true; @@ -443,6 +444,8 @@ static fr_ipaddr_t cl_ipaddr; static uint32_t cl_netmask; static char const *cl_srcipaddr = NULL; static char const *hs_proto = NULL; +static char const *require_message_authenticator = NULL; +static char const *limit_proxy_state = NULL; #ifdef WITH_TCP static CONF_PARSER limit_config[] = { @@ -465,7 +468,8 @@ static const CONF_PARSER client_config[] = { { "src_ipaddr", FR_CONF_POINTER(PW_TYPE_STRING, &cl_srcipaddr), NULL }, - { "require_message_authenticator", FR_CONF_OFFSET(PW_TYPE_BOOLEAN, RADCLIENT, message_authenticator), "no" }, + { "require_message_authenticator", FR_CONF_POINTER(PW_TYPE_STRING| PW_TYPE_IGNORE_DEFAULT, &require_message_authenticator), NULL }, + { "limit_proxy_state", FR_CONF_POINTER(PW_TYPE_STRING| PW_TYPE_IGNORE_DEFAULT, &limit_proxy_state), NULL }, { "secret", FR_CONF_OFFSET(PW_TYPE_STRING | PW_TYPE_SECRET, RADCLIENT, secret), NULL }, { "shortname", FR_CONF_OFFSET(PW_TYPE_STRING, RADCLIENT, shortname), NULL }, @@ -661,7 +665,7 @@ static const CONF_PARSER dynamic_config[] = { { "FreeRADIUS-Client-Src-IP-Address", FR_CONF_OFFSET(PW_TYPE_IPV4_ADDR, RADCLIENT, src_ipaddr), NULL }, { "FreeRADIUS-Client-Src-IPv6-Address", FR_CONF_OFFSET(PW_TYPE_IPV6_ADDR, RADCLIENT, src_ipaddr), NULL }, - { "FreeRADIUS-Client-Require-MA", FR_CONF_OFFSET(PW_TYPE_BOOLEAN, RADCLIENT, message_authenticator), NULL }, + { "FreeRADIUS-Client-Require-MA", FR_CONF_OFFSET(PW_TYPE_BOOLEAN, RADCLIENT, dynamic_require_ma), NULL }, { "FreeRADIUS-Client-Secret", FR_CONF_OFFSET(PW_TYPE_STRING, RADCLIENT, secret), "" }, { "FreeRADIUS-Client-Shortname", FR_CONF_OFFSET(PW_TYPE_STRING, RADCLIENT, shortname), "" }, @@ -843,8 +847,19 @@ RADCLIENT *client_afrom_cs(TALLOC_CTX *ctx, CONF_SECTION *cs, bool in_server, bo c = talloc_zero(ctx, RADCLIENT); c->cs = cs; + /* + * Set the "require message authenticator" and "limit + * proxy state" flags from the global default. If the + * configuration item exists, AND is set, it will + * over-ride the flag. + */ + c->require_ma = main_config.require_ma; + c->limit_proxy_state = main_config.limit_proxy_state; + memset(&cl_ipaddr, 0, sizeof(cl_ipaddr)); cl_netmask = 255; + require_message_authenticator = NULL; + limit_proxy_state = NULL; if (cf_section_parse(cs, c, client_config) < 0) { cf_log_err_cs(cs, "Error parsing client section"); @@ -855,6 +870,9 @@ RADCLIENT *client_afrom_cs(TALLOC_CTX *ctx, CONF_SECTION *cs, bool in_server, bo cl_srcipaddr = NULL; #endif + require_message_authenticator = NULL; + limit_proxy_state = NULL; + return NULL; } @@ -1112,6 +1130,16 @@ done_coa: } #endif + if (fr_bool_auto_parse(cf_pair_find(cs, "require_message_authenticator"), &c->require_ma, require_message_authenticator) < 0) { + goto error; + } + + if (c->require_ma != FR_BOOL_TRUE) { + if (fr_bool_auto_parse(cf_pair_find(cs, "limit_proxy_state"), &c->limit_proxy_state, limit_proxy_state) < 0) { + goto error; + } + } + return c; } @@ -1156,7 +1184,7 @@ RADCLIENT *client_afrom_query(TALLOC_CTX *ctx, char const *identifier, char cons if (shortname) c->shortname = talloc_typed_strdup(c, shortname); if (type) c->nas_type = talloc_typed_strdup(c, type); if (server) c->server = talloc_typed_strdup(c, server); - c->message_authenticator = require_ma; + c->require_ma = require_ma; return c; } @@ -1342,10 +1370,10 @@ RADCLIENT *client_afrom_request(RADCLIENT_LIST *clients, REQUEST *request) *pi = vp->vp_integer; /* - * Same nastiness as above. + * Same nastiness as above, but hard-coded for require Message-Authenticator. */ for (parse = client_config; parse->name; parse++) { - if (parse->offset == dynamic_config[i].offset) break; + if (parse->type == PW_TYPE_BOOLEAN) break; } if (!parse) break; @@ -1434,6 +1462,11 @@ validate: goto error; } + /* + * It can't be set to "auto". Too bad. + */ + c->require_ma = (fr_bool_auto_t) c->dynamic_require_ma; + if (!client_add_dynamic(clients, request->client, c)) { return NULL; } diff --git a/src/main/conffile.c b/src/main/conffile.c index 6d6441a55b..0e1ced2385 100644 --- a/src/main/conffile.c +++ b/src/main/conffile.c @@ -1417,6 +1417,7 @@ int cf_item_parse(CONF_SECTION *cs, char const *name, unsigned int type, void *d { int rcode; bool deprecated, required, attribute, secret, file_input, cant_be_empty, tmpl, multi, file_exists; + bool ignore_dflt; char **q; char const *value; CONF_PAIR *cp = NULL; @@ -1440,6 +1441,7 @@ int cf_item_parse(CONF_SECTION *cs, char const *name, unsigned int type, void *d cant_be_empty = (type & PW_TYPE_NOT_EMPTY); tmpl = (type & PW_TYPE_TMPL); multi = (type & PW_TYPE_MULTI); + ignore_dflt = (type & PW_TYPE_IGNORE_DEFAULT); if (attribute) required = true; if (required) cant_be_empty = true; /* May want to review this in the future... */ @@ -1463,7 +1465,7 @@ int cf_item_parse(CONF_SECTION *cs, char const *name, unsigned int type, void *d * section, use the default value. */ if (!cp) { - if (deprecated) return 0; /* Don't set the default value */ + if (deprecated || ignore_dflt) return 0; /* Don't set the default value */ rcode = 1; value = dflt; diff --git a/src/main/listen.c b/src/main/listen.c index ebf7f5221c..0d178138c9 100644 --- a/src/main/listen.c +++ b/src/main/listen.c @@ -456,6 +456,122 @@ int rad_status_server(REQUEST *request) return 0; } +static void blastradius_checks(RADIUS_PACKET *packet, RADCLIENT *client) +{ + if (client->require_ma == FR_BOOL_TRUE) return; + + if (client->require_ma == FR_BOOL_AUTO) { + if (!packet->message_authenticator) { + ERROR("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"); + ERROR("BlastRADIUS check: Received packet without Message-Authenticator."); + ERROR("Setting \"require_message_authenticator = false\" for client %s", client->shortname); + ERROR("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"); + ERROR("UPGRADE THE CLIENT AS YOUR NETWORK IS VULNERABLE TO THE BLASTRADIUS ATTACK."); + ERROR("Once the client is upgraded, set \"require_message_authenticator = true\" for client %s", client->shortname); + ERROR("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"); + client->require_ma = FR_BOOL_FALSE; + + /* + * And fall through to the + * limit_proxy_state checks, which might + * complain again. Oh well, maybe that + * will make people read the messages. + */ + + } else if (packet->eap_message) { + /* + * Don't set it to "true" for packets + * with EAP-Message. It's already + * required there, and we might get a + * non-EAP packet with (or without) + * Message-Authenticator + */ + return; + } else { + ERROR("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"); + ERROR("BlastRADIUS check: Received packet with Message-Authenticator."); + ERROR("Setting \"require_message_authenticator = true\" for client %s", client->shortname); + ERROR("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"); + ERROR("It looks like the client has been updated to protect from the BlastRADIUS attack."); + ERROR("Please set \"require_message_authenticator = true\" for client %s", client->shortname); + ERROR("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"); + + client->require_ma = FR_BOOL_TRUE; + return; + } + + } + + /* + * If all of the checks are turned off, then complain for every packet we receive. + */ + if (client->limit_proxy_state == FR_BOOL_FALSE) { + /* + * We have a Message-Authenticator, and it's valid. We don't need to compain. + */ + if (!fr_debug_lvl) return; /* easier than checking for each line below */ + + DEBUG("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"); + DEBUG("BlastRADIUS check: Received packet without Message-Authenticator."); + DEBUG("YOU MUST SET \"require_message_authenticator = true\", or"); + DEBUG("YOU MUST SET \"limit_proxy_state = true\" for client %s", client->shortname); + DEBUG("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"); + DEBUG("The packet does not contain Message-Authenticator, which is a security issue"); + DEBUG("UPGRADE THE CLIENT AS YOUR NETWORK IS VULNERABLE TO THE BLASTRADIUS ATTACK."); + DEBUG("Once the client is upgraded, set \"require_message_authenticator = true\" for client %s", client->shortname); + DEBUG("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"); + return; + } + + /* + * Don't complain here. rad_packet_ok() will instead + * complain about every packet with Proxy-State but which + * is missing Message-Authenticator. + */ + if (client->limit_proxy_state == FR_BOOL_TRUE) { + return; + } + + if (packet->proxy_state && !packet->message_authenticator) { + ERROR("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"); + ERROR("BlastRADIUS check: Received packet with Proxy-State, but without Message-Authenticator."); + ERROR("This is either a BlastRADIUS attack, OR"); + ERROR("the client is a proxy RADIUS server which has not been upgraded."); + ERROR("Setting \"limit_proxy_state = false\" for client %s", client->shortname); + ERROR("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"); + ERROR("UPGRADE THE CLIENT AS YOUR NETWORK IS VULNERABLE TO THE BLASTRADIUS ATTACK."); + DEBUG("Once the client is upgraded, set \"require_message_authenticator = true\" for client %s", client->shortname); + ERROR("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"); + + client->limit_proxy_state = FR_BOOL_FALSE; + + } else { + client->limit_proxy_state = FR_BOOL_TRUE; + + ERROR("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"); + if (!packet->proxy_state) { + ERROR("BlastRADIUS check: Received packet without Proxy-State."); + } else { + ERROR("BlastRADIUS check: Received packet with Proxy-State and Message-Authenticator."); + } + + ERROR("Setting \"limit_proxy_state = true\" for client %s", client->shortname); + ERROR("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"); + + if (!packet->message_authenticator) { + ERROR("The packet does not contain Message-Authenticator, which is a security issue."); + ERROR("UPGRADE THE CLIENT AS YOUR NETWORK MAY BE VULNERABLE TO THE BLASTRADIUS ATTACK."); + DEBUG("Once the client is upgraded, set \"require_message_authenticator = true\" for client %s", client->shortname); + } else { + ERROR("The packet contains Message-Authenticator."); + if (!packet->eap_message) ERROR("The client has likely been upgraded to protect from the attack."); + ERROR("Please set \"require_message_authenticator = true\" for client %s", client->shortname); + } + ERROR("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"); + } +} + + #ifdef WITH_TCP static int dual_tcp_recv(rad_listen_t *listener) { @@ -532,6 +648,21 @@ static int dual_tcp_recv(rad_listen_t *listener) switch (packet->code) { case PW_CODE_ACCESS_REQUEST: if (listener->type != RAD_LISTEN_AUTH) goto bad_packet; + + /* + * Enforce BlastRADIUS checks on TCP, too. + */ + if (!rad_packet_ok(packet, (client->require_ma == FR_BOOL_TRUE) | ((client->limit_proxy_state == FR_BOOL_TRUE) << 2), NULL)) { + FR_STATS_INC(auth, total_malformed_requests); + rad_free(&sock->packet); + return 0; + } + + /* + * Perform BlastRADIUS checks and warnings. + */ + if (packet->code == PW_CODE_ACCESS_REQUEST) blastradius_checks(packet, client); + FR_STATS_INC(auth, total_requests); fun = rad_authenticate; break; @@ -1562,7 +1693,7 @@ static int auth_socket_recv(rad_listen_t *listener) * Now that we've sanity checked everything, receive the * packet. */ - packet = rad_recv(ctx, listener->fd, client->message_authenticator); + packet = rad_recv(ctx, listener->fd, (client->require_ma == FR_BOOL_TRUE) | ((client->limit_proxy_state == FR_BOOL_TRUE) << 2)); if (!packet) { FR_STATS_INC(auth, total_malformed_requests); if (DEBUG_ENABLED) ERROR("Receive - %s", fr_strerror()); @@ -1570,6 +1701,12 @@ static int auth_socket_recv(rad_listen_t *listener) return 0; } + + /* + * Perform BlastRADIUS checks and warnings. + */ + if (packet->code == PW_CODE_ACCESS_REQUEST) blastradius_checks(packet, client); + #ifdef __APPLE__ #ifdef WITH_UDPFROMTO /* @@ -1955,7 +2092,7 @@ static int coa_socket_recv(rad_listen_t *listener) * Now that we've sanity checked everything, receive the * packet. */ - packet = rad_recv(ctx, listener->fd, client->message_authenticator); + packet = rad_recv(ctx, listener->fd, client->require_ma | (((int) client->limit_proxy_state) << 2)); if (!packet) { FR_STATS_INC(coa, total_malformed_requests); if (DEBUG_ENABLED) ERROR("Receive - %s", fr_strerror()); diff --git a/src/main/mainconfig.c b/src/main/mainconfig.c index e9dd412dee..854c7155aa 100644 --- a/src/main/mainconfig.c +++ b/src/main/mainconfig.c @@ -73,6 +73,8 @@ static char const *gid_name = NULL; static char const *chroot_dir = NULL; static bool allow_core_dumps = false; static char const *radlog_dest = NULL; +static char const *require_message_authenticator = NULL; +static char const *limit_proxy_state = NULL; /* * These are not used anywhere else.. @@ -87,6 +89,53 @@ static bool do_colourise = false; static char const *radius_dir = NULL; //!< Path to raddb directory +static const FR_NAME_NUMBER fr_bool_auto_names[] = { + { "false", FR_BOOL_FALSE }, + { "no", FR_BOOL_FALSE }, + { "0", FR_BOOL_FALSE }, + + { "true", FR_BOOL_TRUE }, + { "yes", FR_BOOL_TRUE }, + { "1", FR_BOOL_TRUE }, + + { "auto", FR_BOOL_AUTO }, + + { NULL, 0 } +}; + +/* + * Get decent values for false / true / auto + */ +int fr_bool_auto_parse(CONF_PAIR *cp, fr_bool_auto_t *out, char const *str) +{ + int value; + + /* + * Don't change anything. + */ + if (!str) return 0; + + value = fr_str2int(fr_bool_auto_names, str, -1); + if (value >= 0) { + *out = value; + return 0; + } + + /* + * This should never happen, as the defaults are in the + * source code. If there's no CONF_PAIR, and there's a + * parse error, then the source code is wrong. + */ + if (!cp) { + fprintf(stderr, "%s: Error - Invalid value in configuration", main_config.name); + return -1; + } + + cf_log_err(cf_pair_to_item(cp), "Invalid value for \"%s\"", cf_pair_attr(cp)); + return -1; +} + + /********************************************************************** * * We need to figure out where the logs go, before doing anything @@ -159,6 +208,8 @@ static const CONF_PARSER security_config[] = { { "max_attributes", FR_CONF_POINTER(PW_TYPE_INTEGER, &fr_max_attributes), STRINGIFY(0) }, { "reject_delay", FR_CONF_POINTER(PW_TYPE_TIMEVAL, &main_config.reject_delay), STRINGIFY(0) }, { "status_server", FR_CONF_POINTER(PW_TYPE_BOOLEAN, &main_config.status_server), "no"}, + { "require_message_authenticator", FR_CONF_POINTER(PW_TYPE_STRING, &require_message_authenticator), "auto"}, + { "limit_proxy_state", FR_CONF_POINTER(PW_TYPE_STRING, &limit_proxy_state), "auto"}, #ifdef ENABLE_OPENSSL_VERSION_CHECK { "allow_vulnerable_openssl", FR_CONF_POINTER(PW_TYPE_STRING, &main_config.allow_vulnerable_openssl), "no"}, #endif @@ -838,6 +889,8 @@ int main_config_init(void) if (!main_config.dictionary_dir) { main_config.dictionary_dir = DICTDIR; } + main_config.require_ma = FR_BOOL_AUTO; + main_config.limit_proxy_state = FR_BOOL_AUTO; /* * About sizeof(REQUEST) + sizeof(RADIUS_PACKET) * 2 + sizeof(VALUE_PAIR) * 400 @@ -1127,6 +1180,23 @@ do {\ main_config.init_delay.tv_sec = 0; main_config.init_delay.tv_usec = 2* (1000000 / 3); + { + CONF_PAIR *cp = NULL; + + subcs = cf_section_sub_find(cs, "security"); + if (subcs) cp = cf_pair_find(subcs, "require_message_authenticator"); + if (fr_bool_auto_parse(cp, &main_config.require_ma, require_message_authenticator) < 0) { + cf_file_free(cs); + return -1; + } + + if (subcs) cp = cf_pair_find(subcs, "limit_proxy_state"); + if (fr_bool_auto_parse(cp, &main_config.limit_proxy_state, limit_proxy_state) < 0) { + cf_file_free(cs); + return -1; + } + } + /* * Free the old configuration items, and replace them * with the new ones. diff --git a/src/main/process.c b/src/main/process.c index 78c6d8a9e5..a478026024 100644 --- a/src/main/process.c +++ b/src/main/process.c @@ -2594,6 +2594,23 @@ int request_proxy_reply(RADIUS_PACKET *packet) PTHREAD_MUTEX_UNLOCK(&proxy_mutex); + if (!request->proxy_reply) { + decode_fail_t reason; + + /* + * If the home server configuration requires a Message-Authenticator, then set the flag, + * but only if the proxied packet is Access-Request or Status-Sercer. + * + * The realms.c file already clears require_ma for TLS connections. + */ + bool require_ma = (request->home_server->require_ma == FR_BOOL_TRUE) && (request->proxy->code == PW_CODE_ACCESS_REQUEST); + + if(!rad_packet_ok(packet, require_ma, &reason)) { + DEBUG("Ignoring invalid packet - %s", fr_strerror()); + return 0; + } + } + /* * No reply, BUT the current packet fails verification: * ignore it. This does the MD5 calculations in the @@ -2619,6 +2636,54 @@ int request_proxy_reply(RADIUS_PACKET *packet) return 0; } + + /* + * BlastRADIUS checks. We're running in the main + * listener thread, so there's no conflict + * checking or setting these fields. + */ + if (!request->proxy_reply && (request->proxy->code == PW_CODE_ACCESS_REQUEST) && +#ifdef WITH_TLS + !request->home_server->tls && +#endif + !packet->eap_message) { + if (request->home_server->require_ma == FR_BOOL_AUTO) { + if (!packet->message_authenticator) { + RERROR("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"); + RERROR("BlastRADIUS check: Received response to Access-Request without Message-Authenticator."); + RERROR("Setting \"require_message_authenticator = false\" for home_server %s", request->home_server->name); + RERROR("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"); + RERROR("UPGRADE THE HOME SERVER AS YOUR NETWORK IS VULNERABLE TO THE BLASTRADIUS ATTACK."); + RERROR("Once the home_server is upgraded, set \"require_message_authenticator = true\" for home_server %s.", request->home_server->name); + RERROR("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"); + + request->home_server->require_ma = FR_BOOL_FALSE; + } else { + RERROR("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"); + RERROR("BlastRADIUS check: Received response to Access-Request with Message-Authenticator."); + RERROR("Setting \"require_message_authenticator = true\" for home_server %s", request->home_server->name); + RERROR("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"); + RERROR("It looks like the home server has been updated to protect from the BlastRADIUS attack."); + RERROR("Please set \"require_message_authenticator = true\" for home_server %s", request->home_server->name); + RERROR("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"); + + request->home_server->require_ma = FR_BOOL_TRUE; + } + + } else if (fr_debug_lvl && (request->home_server->require_ma == FR_BOOL_FALSE) && !packet->message_authenticator) { + /* + * If it's "no" AND we don't have a Message-Authenticator, then complain on every packet. + */ + RDEBUG("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"); + RDEBUG("BlastRADIUS check: Received packet without Message-Authenticator from home_server %s", request->home_server->name); + RDEBUG("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"); + RDEBUG("The packet does not contain Message-Authenticator, which is a security issue"); + RDEBUG("UPGRADE THE HOME SERVER AS YOUR NETWORK IS VULNERABLE TO THE BLASTRADIUS ATTACK."); + RERROR("Once the home_server is upgraded, set \"require_message_authenticator = true\" for home_server %s.", request->home_server->name); + RDEBUG("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"); + } + } + /* * This shouldn't happen, but threads and race * conditions. diff --git a/src/main/radclient.c b/src/main/radclient.c index 52d2872b13..dd2b342ba2 100644 --- a/src/main/radclient.c +++ b/src/main/radclient.c @@ -54,6 +54,7 @@ static fr_ipaddr_t server_ipaddr; static int resend_count = 1; static bool done = true; static bool print_filename = false; +static bool blast_radius = false; static fr_ipaddr_t client_ipaddr; static uint16_t client_port = 0; @@ -89,6 +90,7 @@ static void NEVER_RETURNS usage(void) fprintf(stderr, " One of auth, acct, status, coa, disconnect or auto.\n"); fprintf(stderr, " -4 Use IPv4 address of server\n"); fprintf(stderr, " -6 Use IPv6 address of server.\n"); + fprintf(stderr, " -b Mandate checks for Blast RADIUS (this is not set by default).\n"); fprintf(stderr, " -c Send each packet 'count' times.\n"); fprintf(stderr, " -d Set user dictionary directory (defaults to " RADDBDIR ").\n"); fprintf(stderr, " -D Set main dictionary directory (defaults to " DICTDIR ").\n"); @@ -1000,6 +1002,130 @@ static int send_one_packet(rc_request_t *request) return 0; } +/* + * Do Blast RADIUS checks. + * + * The request is an Access-Request, and does NOT contain Proxy-State. + * + * The reply is a raw packet, and is NOT yet decoded. + */ +static int blast_radius_check(rc_request_t *request, RADIUS_PACKET *reply) +{ + uint8_t *attr, *end; + VALUE_PAIR *vp; + bool have_message_authenticator = false; + + /* + * We've received a raw packet. Nothing has (as of yet) checked + * anything in it other than the length, and that it's a + * well-formed RADIUS packet. + */ + switch (reply->data[0]) { + case PW_CODE_ACCESS_ACCEPT: + case PW_CODE_ACCESS_REJECT: + case PW_CODE_ACCESS_CHALLENGE: + if (reply->data[1] != request->packet->id) { + ERROR("Invalid reply ID %d to Access-Request ID %d", reply->data[1], request->packet->id); + return -1; + } + break; + + default: + ERROR("Invalid reply code %d to Access-Request", reply->data[0]); + return -1; + } + + /* + * If the reply has a Message-Authenticator, then it MIGHT be fine. + */ + attr = reply->data + 20; + end = reply->data + reply->data_len; + + /* + * It should be the first attribute, so we warn if it isn't there. + * + * But it's not a fatal error. + */ + if (blast_radius && (attr[0] != PW_MESSAGE_AUTHENTICATOR)) { + RDEBUG("WARNING The %s reply packet does not have Message-Authenticator as the first attribute. The packet may be vulnerable to Blast RADIUS attacks.", + fr_packet_codes[reply->data[0]]); + } + + /* + * Set up for Proxy-State checks. + * + * If we see a Proxy-State in the reply which we didn't send, then it's a Blast RADIUS attack. + */ + vp = fr_pair_find_by_num(request->packet->vps, PW_PROXY_STATE, 0, TAG_ANY); + + while (attr < end) { + /* + * Blast RADIUS work-arounds require that + * Message-Authenticator is the first attribute in the + * reply. Note that we don't check for it being the + * first attribute, but simply that it exists. + * + * That check is a balance between securing the reply + * packet from attacks, and not violating the RFCs which + * say that there is no order to attributes in the + * packet. + * + * However, no matter the status of the '-b' flag we + * still can check for the signature of the attack, and + * discard packets which are suspicious. This behavior + * protects radclient from the attack, without mandating + * new behavior on the server side. + * + * Note that we don't set the '-b' flag by default. + * radclient is intended for testing / debugging, and is + * not intended to be used as part of a secure login / + * user checking system. + */ + if (attr[0] == PW_MESSAGE_AUTHENTICATOR) { + have_message_authenticator = true; + goto next; + } + + /* + * If there are Proxy-State attributes in the reply, they must + * match EXACTLY the Proxy-State attributes in the request. + * + * Note that we don't care if there are more Proxy-States + * in the request than in the reply. The Blast RADIUS + * issue requires _adding_ Proxy-State attributes, and + * cannot work when the server _deletes_ Proxy-State + * attributes. + */ + if (attr[0] == PW_PROXY_STATE) { + if (!vp || (vp->length != (size_t) (attr[1] - 2)) || (memcmp(vp->vp_octets, attr + 2, vp->length) != 0)) { + ERROR("Invalid reply to Access-Request ID %d - Discarding packet due to Blast RADIUS attack being detected.", request->packet->id); + ERROR("We received a Proxy-State in the reply which we did not send, or which is different from what we sent."); + return -1; + } + + vp = fr_pair_find_by_num(vp->next, PW_PROXY_STATE, 0, TAG_ANY); + } + + next: + attr += attr[1]; + } + + /* + * If "-b" is set, then we require Message-Authenticator in the reply. + */ + if (blast_radius && !have_message_authenticator) { + ERROR("The %s reply packet does not contain Message-Authenticator - discarding packet due to Blast RADIUS checks.", + fr_packet_codes[reply->data[0]]); + return -1; + } + + /* + * The packet doesn't look like it's a Blast RADIUS attack. The + * caller will now verify the packet signature. + */ + return 0; +} + /* * Receive one packet, maybe. */ @@ -1051,6 +1177,21 @@ static int recv_one_packet(int wait_time) } request = fr_packet2myptr(rc_request_t, packet, packet_p); + + /* + * We want radclient to be able to send any packet, including + * imperfect ones. However, we do NOT want to be vulnerable to + * the "Blast RADIUS" issue. Instead of adding command-line + * flags to enable/disable similar flags to what the server + * sends, we just do a few more smart checks to double-check + * things. + */ + if ((request->packet->code == PW_CODE_ACCESS_REQUEST) && + blast_radius_check(request, reply) < 0) { + rad_free(&reply); + return -1; + } + /* * Fails the signature validation: not a real reply. * FIXME: Silently drop it and listen for another packet. @@ -1183,7 +1324,7 @@ int main(int argc, char **argv) exit(1); } - while ((c = getopt(argc, argv, "46c:d:D:f:Fhn:p:qr:sS:t:vx" + while ((c = getopt(argc, argv, "46bc:d:D:f:Fhn:p:qr:sS:t:vx" #ifdef WITH_TCP "P:" #endif @@ -1196,6 +1337,10 @@ int main(int argc, char **argv) force_af = AF_INET6; break; + case 'b': + blast_radius = true; + break; + case 'c': if (!isdigit((int) *optarg)) usage(); diff --git a/src/main/radtest.in b/src/main/radtest.in index 38b1ba9a0f..8a6741a26c 100644 --- a/src/main/radtest.in +++ b/src/main/radtest.in @@ -19,6 +19,7 @@ usage() { echo " -x Enable debug output" >&2 echo " -4 Use IPv4 for the NAS address (default)" >&2 echo " -6 Use IPv6 for the NAS address" >&2 + echo " -6 Mandate checks for Blast RADIUS (this is not set by default)." >&2 exit 1 } @@ -55,6 +56,10 @@ do NAS_ADDR_ATTR="NAS-IPv6-Address" shift ;; + -b) + OPTIONS="$OPTIONS -b" + shift + ;; -d) OPTIONS="$OPTIONS -d $2" shift;shift @@ -120,7 +125,6 @@ fi echo "$PASSWORD = \"$2\"" echo "$NAS_ADDR_ATTR = $nas" echo "NAS-Port = $4" - echo "Message-Authenticator = 0x00" if [ "$radclient" = "$radeapclient" ] then echo "EAP-Code = Response" diff --git a/src/main/realms.c b/src/main/realms.c index eb42598116..c82d2eb1ea 100644 --- a/src/main/realms.c +++ b/src/main/realms.c @@ -366,7 +366,10 @@ static CONF_PARSER home_server_coa[] = { }; #endif +static const char *require_message_authenticator = NULL; + static CONF_PARSER home_server_config[] = { + { "require_message_authenticator", FR_CONF_POINTER(PW_TYPE_STRING| PW_TYPE_IGNORE_DEFAULT, &require_message_authenticator), NULL }, { "ipaddr", FR_CONF_OFFSET(PW_TYPE_COMBO_IP_ADDR, home_server_t, ipaddr), NULL }, { "ipv4addr", FR_CONF_OFFSET(PW_TYPE_IPV4_ADDR, home_server_t, ipaddr), NULL }, { "ipv6addr", FR_CONF_OFFSET(PW_TYPE_IPV6_ADDR, home_server_t, ipaddr), NULL }, @@ -640,6 +643,9 @@ home_server_t *home_server_afrom_cs(TALLOC_CTX *ctx, realm_config_t *rc, CONF_SE home->cs = cs; home->state = HOME_STATE_UNKNOWN; home->proto = IPPROTO_UDP; + home->require_ma = main_config.require_ma; + + require_message_authenticator = false; /* * Parse the configuration into the home server @@ -647,6 +653,10 @@ home_server_t *home_server_afrom_cs(TALLOC_CTX *ctx, realm_config_t *rc, CONF_SE */ if (cf_section_parse(cs, home, home_server_config) < 0) goto error; + if (fr_bool_auto_parse(cf_pair_find(cs, "require_message_authenticator"), &home->require_ma, require_message_authenticator) < 0) { + goto error; + } + /* * It has an IP address, it must be a remote server. */ @@ -924,6 +934,7 @@ home_server_t *home_server_afrom_cs(TALLOC_CTX *ctx, realm_config_t *rc, CONF_SE * Parse the SSL client configuration. */ if (tls) { + home->require_ma = false; home->tls = tls_client_conf_parse(tls); if (!home->tls) { goto error; diff --git a/src/main/tls_listen.c b/src/main/tls_listen.c index 0eed87b64f..4ae3c5b975 100644 --- a/src/main/tls_listen.c +++ b/src/main/tls_listen.c @@ -299,6 +299,8 @@ get_application_data: packet->vps = NULL; PTHREAD_MUTEX_UNLOCK(&sock->mutex); + packet->tls = true; + if (!rad_packet_ok(packet, 0, NULL)) { if (DEBUG_ENABLED) ERROR("Receive - %s", fr_strerror()); DEBUG("Closing TLS socket from client"); @@ -713,6 +715,8 @@ int proxy_tls_recv(rad_listen_t *listener) memcpy(packet->data, data, packet->data_len); memcpy(packet->vector, packet->data + 4, 16); + packet->tls = true; + /* * FIXME: Client MIB updates? */ @@ -765,6 +769,7 @@ int proxy_tls_send(rad_listen_t *listener, REQUEST *request) * if there's no packet, encode it here. */ if (!request->proxy->data) { + request->reply->tls = true; request->proxy_listener->encode(request->proxy_listener, request); }