Defect to Doctrine: Security Bug Case Studies

Defect to Doctrine: Security Bug Case Studies

I am not a security research or bug bounty hunter. Just a humble programmer who has found and fixed some security bugs over the years. I admire exploit authors who take advantage of programmers' mistakes to penetrate, pivot and profit. But this is not that talk.

As an industry, we can (and must) improve the security of the the systems we write by learning from our mistakes. Every bug tells a story. Every story has a moral, if you care to look for it.

In this talk I will describe four different vulnerabilities in programs I worked on, including FreeIPA, Dogtag PKI and Firefox. I will explain what the bug was, it's impact, how it was discovered and how I resolved it. From each case study I will develop one or two important principles for secure programming.

This presentation will be most useful for programmers, engineering managers, and security folk who want an engineer's perspective on how issues arise and how to avoid them.

7c0f9b056604fe541691e18aeb679cf7?s=128

Fraser Tweedale

April 06, 2019
Tweet

Transcript

  1. From Defect to Doctrine Security bug case studies Fraser Tweedale

    @hackuador April 4, 2019
  2. Dogtag PKI

  3. Access control - definition certServer.ca.authorities :create,modify :allow (list,read) user="anybody"; allow

    (create,modify,delete) group="Administrators"; deny (create,modify,delete) user="mallory" :Administrators may create and modify lightweight authorities
  4. Access control - evaluation order authz.evaluateOrder=deny,allow

  5. Access control - processing (1) if (order.equals("deny ,allow")) entries =

    getDenyEntries(acls , op); else entries = getAllowEntries(acls , op); for (ACLEntry entry : entries) { if (evaluate(token , entry.getExpressions ())) throw new EACLsException("permission␣denied"); }
  6. Access control - processing (2) if (order.equals("deny ,allow")) entries =

    getAllowEntries(acls , op); else entries = getDenyEntries(acls , op); boolean result = false; for (ACLEntry entry : entries) { if (evaluate(token , entry.getExpressions ())) result = true; } if (! result) throw new EACLsException("permission␣denied");
  7. CVE-2018-10801 1https://access.redhat.com/security/cve/cve-2018-1080

  8. Principle: avoid booleans; use custom types

  9. Principle: avoid booleans; use custom types Corollary: use tools that

    support custom types
  10. Enumeration types: Java, C#, C++11, TypeScript, Python** Algebraic data types:

    Rust, Haskell, Scala, Swift, F#, C++17
  11. Access control - fixed2 public enum ACLOrder { DenyAllow ,

    AllowDeny }; public enum ACLEntryType { Allow , Deny }; public enum ACLResult { Allowed , Denied }; if (order == EvaluationOrder.DenyAllow) { checkDenyEntries(token , acls , op); // throws result = checkAllowEntries(token , acls , op); } else { result = checkAllowEntries(token , acls , op); checkDenyEntries(token , acls , op); // throws } if (result != ACLResult.Allowed) throw new EACLsException("permission␣denied"); 2https://frasertweedale.github.io/blog-redhat/posts/2018-03-26-old-dogtag-new-tricks.html
  12. FreeIPA

  13. +-----------------+ | User | | +-------------+ | | | krb5

    ticket | | +-+------|------+-+ | V +----------------------------------------+ | FreeIPA | | +-----------------+ +---------------+ | | |krb5 proxy ticket| | RA agent cert | | +-+-------|---------+--+------|--------+-+ | | V V +--------+ +--------+ | 389 DS |<--------+ Dogtag | +--------+ +--------+
  14. CVE-2016-54043 3https://access.redhat.com/security/cve/cve-2016-5404

  15. Principle: never cut corners on privilege separation

  16. CVE-2017-25904 4https://access.redhat.com/security/cve/cve-2017-2590

  17. Firefox

  18. Object Identifier (OID) 2.5.4.3 commonName 2.5.29.14 subjectKeyIdentifier 1.2.840.113549.1.1.11 sha256WithRSAEncryption

  19. Object Identifier (OID) 1.3.6.1.4.1.311.21.8.8950086.10656446.2706058.12775672.480128.147.13466065.13029902

  20. python-cryptography (bug) 5,6 buf_len = 80 buf = backend._ffi.new("char[]", buf_len)

    res = backend._lib.OBJ_obj2txt(buf , buf_len , obj , 1) backend.openssl_assert(res > 0) return backend._ffi.buffer(buf , res )[:]. decode () 5https://github.com/pyca/cryptography/pull/3612 6https://bugs.python.org/issue30502
  21. python-cryptography (fixed) 5,6 buf_len = 80 buf = backend._ffi.new("char[]", buf_len)

    res = backend._lib.OBJ_obj2txt(buf , buf_len , obj , 1) if res > buf_len - 1: # account for null terminator buf_len = res + 1 buf = backend._ffi.new("char[]", buf_len) res = backend._lib.OBJ_obj2txt(buf , buf_len , obj , 1) backend.openssl_assert(res > 0) return backend._ffi.buffer(buf , res )[:]. decode () 5https://github.com/pyca/cryptography/pull/3612 6https://bugs.python.org/issue30502
  22. A buffer length of 80 should be more than enough

    to handle any OID encountered in practice. — OBJ_obj2txt(3)7 7https://www.openssl.org/docs/manmaster/man3/OBJ_nid2ln.html#return_values
  23. Object Identifier (OID) 0.8.8950086.10656446.2706058.12775672.480128.147.13466065.130299021.3.6.1.4.1.311.21 .8.8950086.10656446.2706058.12775672.480128.147.13466065.130299021.3.6.1.4.1.311.21 .8.8950086.10656446.2706058.12775672.480128.147.13466065.130299021.3.6.1.4.1.311.21 .8.8950086.10656446.2706058.12775672.480128.147.13466065.130299021.3.6.1.4.1.311.21 .8.8950086.10656446.2706058.12775672.480128.147.13466065.130299021.3.6.1.4.1.311.21 .8.8950086.10656446.2706058.12775672.480128.147.13466065.130299021.3.6.1.4.1.3

  24. CVE-2017-77928 8https://access.redhat.com/security/cve/cve-2017-7792

  25. Firefox (bug)9 char buf [300]; int i, n = 0,

    first = 1; unsigned long v = 0; for (i = 0; i < oid ->len; ++i) { v = oid ->data[i]; if (first) n += snprintf (&buf[n], sizeof(buf) - n, "%lu", v); else n += snprintf (&buf[n], sizeof(buf) - n, ".%lu", v); first = 0; } 9https://bugzilla.mozilla.org/show_bug.cgi?id=1368652
  26. Principle: don’t rely on assumptions about input

  27. Principle: don’t rely on assumptions about input Corollary: validate /

    sanitise everything
  28. Principle: use memory-safe languages

  29. An unnamed online service

  30. Authentication - library (v1) def authenticate(user , password ): ...

    # talk to the database if all_good: return True else: raise AuthenticationError ()
  31. Authentication - application user = request[’username’] password = request[’password’] try:

    authenticate(user , password) except AuthenticationError: respond_401_unauthorized () # user is logged in do_stuff ()
  32. Authentication - library (v2) def authenticate(user , password ): ...

    # talk to the database if all_good: return True else: return False
  33. Principle: avoid using exceptions

  34. Principle: avoid using exceptions Corollary: return types shall express failure

    cases
  35. Principle: avoid using exceptions Corollary: return types shall express failure

    cases Corollary: use tools that ensure failure cases are handled
  36. Error handling in Rust enum Result <T, E> { Ok(T),

    Err(E) } enum Error { AuthError (), ... } fn authenticate(user: &str , password: &str) -> Result <String , Error > { ... // talk to the database if all_good { return Ok(String :: from(user )); } else { return Err(Error :: AuthError ()); } }
  37. Wrapping up

  38. Principles avoid booleans; use custom types never cut corners on

    privilege separation don’t rely on assumptions about input use memory-safe languages avoid using exceptions
  39. Principle: learn from mistakes

  40. Thank you! Except where otherwise noted this work is licensed

    under http://creativecommons.org/licenses/by/4.0/ https://speakerdeck.com/frasertweedale @hackuador
  41. Access control - fixed public enum ACLResult { Allowed ,

    Denied }; List <ACLEntry > entries; if (order == EvaluationOrder.DenyAllow) { entries = getDenyEntries(acls , op); entries.addAll(getAllowEntries(acls , op)); } else { entries = getAllowEntries(acls , op); entries.addAll(getDenyEntries(acls , op)); } for (ACLEntry entry : entries) { Optional <ACLResult > result = entry.evaluate(token ); if (result.isPresent ()) return result.get (); } return ACLResult.Denied;
  42. Access control - fixed data RuleOrder = AllowDeny | DenyAllow

    data RuleType = Allow | Deny deriving (Eq) data Result = Allowed | Denied eval :: RuleOrder -> Token -> Op -> [Rule] -> Result eval order tok op rules = fromMaybe Denied (alaf First foldMap (evalRule tok) orderedRules) where opRules = filter (elem op . aclRulePermissions) rules allowRules = filter ((== Allow) . aclRuleType) opRules denyRules = filter ((/= Allow) . aclRuleType) opRules orderedRules = case order of DenyAllow -> denyRules <> allowRules AllowDeny -> allowRules <> denyRules