debops / ansible-pki

Bootstrap and manage internal PKI, Certificate Authorities and OpenSSL/GnuTLS certificates
GNU General Public License v3.0
65 stars 29 forks source link

[Security] debops.pki does not validate CSRs allowing certificate mis-issuance by compromised remote host #106

Open ypid opened 7 years ago

ypid commented 7 years ago

The vulnerability goes in the same direction as other vulns on https://www.ansible.com/security which are compromised remote hosts exploiting the Ansible controller.

Steps to reproduce:

pki_ca_domain: 'example.org'
pki_ca_organization: 'example'
pki_default_authority: 'example-issuing-ca'

pki_authorities:

  - name: 'example-root-ca'
    domain: '{{ network__public_dns_fqdn }}'
    subdomain: 'root-ca'
    subject: [ 'o=example Internal Root CA' ]
    key_size: '{{ pki_ca_root_key_size }}'

  - name: 'example-issuing-ca'
    domain: '{{ network__public_dns_fqdn }}'
    subdomain: 'issuing-ca'
    subject: [ 'o=example', 'ou=example Internal Issuing CA' ]
    issuer_name: 'example-root-ca'
    key_size: '{{ pki_ca_domain_key_size }}'

# What the Ansible controller expects:
pki_host_realms:

  - name: 'good.{{ pki_ca_domain }}'

# That is what the remote host will give us (the Ansible controller):
pki_evil_host_realms:

  - name: 'good.{{ pki_ca_domain }}'
    domains:
      - 'evil.{{ pki_ca_domain }}'

Then simply replace all:

  with_flattened:
    - '{{ pki_realms }}'
    - '{{ pki_group_realms }}'
    - '{{ pki_host_realms }}'
    - '{{ pki_default_realms }}'
    - '{{ pki_dependent_realms }}'

with:

  with_flattened:
    - '{{ pki_evil_host_realms }}'

in tasks where pki-realm is called on the remote host (which can be spoofed by the remote host to their liking).

Then run the role. The remote host gets this cert issued:

Certificate:
    Data:
        Version: 3 (0x2)
    Signature Algorithm: sha256WithRSAEncryption
        Subject: CN=good.example.org
        X509v3 extensions:
            X509v3 Subject Alternative Name:
                DNS:evil.example.org

The above is just my way of demonstrating/reproducing it.

Basically, the pki-realms script is always executed by the remote host, so the input and output of the script could be controlled by an attacker if the remote host got compromised. That is basically what I simulated here. I defined a second variable in the inventory on the controller pki_evil_host_realms representing the input by which pki-realms gets called despite the fact that the controller sends pki_host_realms. I could have done it manually on the remote host but, oh well, automation.

pki-authority must validate pki_*realms and check if the admin actually authorized the CSR generated by that particular (untrusted) remote host.

Unfortunately, DebOps does not have a patch for this and it is currently not clear when one will be available as it might end in a rewrite of pki-authority. For transparency reasons, we therefore decided to make this public to not leave users in the dark. Technically, DebOps is not yet recommended for production.

@drybjed Already wrote how this can be solved. It is probably best when he pastes that in himself.

My idea was: pki-authority could just get all pki_*realms of the current Ansible run, encoded as JSON, build Subject and SANs for each realm. Then subject and SANs (and possibly other interesting parts of the CSR) get extracted from the CSR the remote host provided, sorted and compared. Python would make that a bit easier.

Timeline: