Bug 41005 - zoneName=../../../../../etc/shadow,cn=dns,dc=foo
zoneName=../../../../../etc/shadow,cn=dns,dc=foo
Status: CLOSED FIXED
Product: UCS
Classification: Unclassified
Component: DNS
UCS 4.4
Other Linux
: P5 normal (vote)
: UCS 4.4-0-errata
Assigned To: Florian Best
Arvid Requate
https://linux.m2osw.com/bind-parser-s...
:
Depends on:
Blocks:
  Show dependency treegraph
 
Reported: 2016-04-06 07:21 CEST by Florian Best
Modified: 2021-06-23 07:29 CEST (History)
3 users (show)

See Also:
What kind of report is it?: Security Issue
What type of bug is this?: ---
Who will be affected by this bug?: ---
How will those affected feel about the bug?: ---
User Pain:
Enterprise Customer affected?:
School Customer affected?:
ISV affected?:
Waiting Support:
Flags outvoted (downgraded) after PO Review:
Ticket number:
Bug group (optional): Security
Max CVSS v3 score: 8.7 (CVSS:3.0/AV:N/AC:L/PR:H/UI:N/S:C/C:N/I:H/A:H)
best: Patch_Available+


Attachments
patch (3.81 KB, patch)
2016-07-08 11:40 CEST, Florian Best
Details | Diff
sanitizing.patch (4.50 KB, patch)
2017-08-14 18:11 CEST, Janek Walkenhorst
Details | Diff
rejection.patch (2.15 KB, patch)
2017-08-14 18:11 CEST, Janek Walkenhorst
Details | Diff
qa.diff (2.10 KB, patch)
2019-04-29 19:51 CEST, Arvid Requate
Details | Diff

Note You need to log in before you can comment on or make changes to this bug.
Description Florian Best univentionstaff 2016-04-06 07:21:24 CEST
I just created a DNS forward zone in LDAP which triggered the listener to overwrite /etc/shadow.

dn: zoneName=../../../../../etc/shadow,cn=dns,dc=foo
objectClass: top
objectClass: dNSZone
objectClass: univentionObject
univentionObjectType: dns/forward_zone
dNSTTL: 10800
relativeDomainName: @
zoneName: ../../../../../etc/shadow
aRecord: 192.168.0.113
nSRecord: xen3.school.local.
nSRecord: slave114.school.local.
sOARecord: xen3.school.local. root.school.local. 157 28800 7200 604800 10800
Comment 1 Florian Best univentionstaff 2016-04-06 07:23:12 CEST
If I remove the zone from LDAP the /etc/shadow file will be removed by the listener, too.
Comment 2 Florian Best univentionstaff 2016-06-27 19:06:20 CEST
See also Bug #36775 - maybe this can be prevented here, too.
Comment 3 Janek Walkenhorst univentionstaff 2016-07-07 19:41:51 CEST
Advisory: univention-bind.yaml
Comment 4 Florian Best univentionstaff 2016-07-08 11:40:12 CEST
Created attachment 7788 [details]
patch

The fix looks good. I would have used urlencoding instead of this injective-implementation.
There are some race conditions in the code when changing owner or permissions of the file. Attached a patch which resolves them, too.
Comment 5 Florian Best univentionstaff 2016-07-08 11:41:43 CEST
(In reply to Florian Best from comment #4)
> Created attachment 7788 [details]
> patch
The patch contains a little typo: bind.py:79: undefined name 'filename'
Comment 6 Stefan Gohmann univentionstaff 2016-07-12 07:15:54 CEST
(In reply to Janek Walkenhorst from comment #3)
> Advisory: univention-bind.yaml

Why do you run a 'univention-directory-listener-ctrl resync bind' in postinst? Is it necessary?

The sanitize_filename code looks complicated. Wouldn't it be better simply to check the realpath of the filename and to ignore everything what is not in the base folder?

For example:

>>> import os
>>> BASE='/var/cache/bind'
>>> path_a = 'foo.bar'
>>> path_b = '../../foo2.bar2'
>>> full_path_a = os.path.join(BASE, path_a)
>>> full_path_b = os.path.join(BASE, path_b)
>>> os.path.realpath(full_path_a)
'/var/cache/bind/foo.bar'
>>> os.path.realpath(full_path_b)
'/var/foo2.bar2'
>>>

Otherwise we may use a standard function like Florian mentioned?
Comment 7 Janek Walkenhorst univentionstaff 2016-08-02 18:46:46 CEST
Reverted changes in UCS 4.1-2.
Comment 8 Florian Best univentionstaff 2016-10-21 12:29:00 CEST
If you use filename = urlencode(zone_dn) instead of only the zone name which may exists twice this would also fix Bug #26683.
Comment 9 Arvid Requate univentionstaff 2017-04-20 19:30:20 CEST
For Bug 33846 I simply used this:

zoneFile = "%s.zone" % (zone,)
zoneFile = os.path.normpath('/' + zoneFile).lstrip('/')
zoneFile = os.path.join(PROXY_CACHE_DIR, zoneFile)
Comment 10 Janek Walkenhorst univentionstaff 2017-04-24 12:53:11 CEST
I think there are 3 distinct properties we should strive to achieve:

a) Readability:
"vier-eins-qa.test" should map to "vier-eins-qa.test.zone" instead of e.g. "766965722d65696e732d71612e74657374.zone" to not unnecessarily confuse the human operator.

a1) Compatibility:
The current file names for (ideally all) zone files should stay the same.
This, I think, is not completely achievable (after all, fixing the bug requires changing the name of at least some files)

a2) Usability:
The files names should not contain shell-funny-characters, e.g. "!" or "$" or " " and the like.

b) Injectivity:
<https://en.wikipedia.org/wiki/Injective_function>
The mapping should be injective, i.e. all distinct zone names should be mapped to distinct file names; Overwriting other zone files would be bad.

c) Totality:
<https://en.wikipedia.org/wiki/Partial_function>
The mapping from zone name to file name should be total, i.e. all zone names in the LDAP directory should lead to configuration files in the BIND file system directory.


All that of course in addition to the properties required to actually fix the problem:

P) file name must not contain "/"
That byte is invalid in file names (and makes it a path).

Q) file name must not contain "\x00"
That byte is invalid in file name.

R) file name must not be "."
That name is reserved.

S) file name must not be ".."
That name is reserved.

T) file name should not start with "."
Hidden files confuse humans.

U) file name must only use bytes safe for the BIND .proxy file
This is a difficult one as I could not find any documentation specifying the syntax of the BIND configuration files, thus we need to be conservative.

U1) file name must not contain '"'
That byte delimits the file name in the BIND configuration files.

X) file name must not be empty
At least one byte is required for a valid file name.


(In reply to Stefan Gohmann from comment #6)
> The sanitize_filename code looks complicated.
I hope that impression is not because I made the code longer (44 lines) because in an effort to make the code understandable.
E.g. one could remove 9 lines of asserts, but they are there to document the context/requirements for which the different sets of bytes where chosen. (reduction to 35 lines)
One could remove 10 lines of comments. (reduction to 26 lines)
All in all, one could reformat the code to reduce it to 19 lines, but I think this would just reduce maintainability

(In reply to Janek Walkenhorst from comment #3)
> Advisory: univention-bind.yaml
Has: a a1? a2 b c P Q R S T U? U1 X
Missing: ∅

(In reply to Florian Best from comment #4)
> The fix looks good. I would have used urlencoding instead of this
> injective-implementation.
Has: a a1? a2? b c P Q U? U1 X
Missing: R? S? T?

Using percent encoding seems like a possible solution; I originally discarded similar solutions like base64 and hex due to them missing property a, I did not consider percent encoding.
I do not quite like having three bytes per escaped byte, after all a file name can only be 255 bytes long; But this is probably only cosmetic.

(In reply to Stefan Gohmann from comment #6)
> The sanitize_filename code looks complicated. Wouldn't it be better simply
> to check the realpath of the filename and to ignore everything what is not
> in the base folder?
Has: P Q R? S? a a1 b X
Missing: T U U1 a2 c

(In reply to Arvid Requate from comment #9)
> For Bug 33846 I simply used this:
>
> zoneFile = "%s.zone" % (zone,)
> zoneFile = os.path.normpath('/' + zoneFile).lstrip('/')
> zoneFile = os.path.join(PROXY_CACHE_DIR, zoneFile)
Has: a P R S X
Missing: a1 a2 b c? Q T U U1

(In reply to Stefan Gohmann from comment #6)
> Why do you run a 'univention-directory-listener-ctrl resync bind' in
> postinst? Is it necessary?
Yes, due to property a1 not being completely fulfill-able some file names will change and thus need to be rewritten.
One could of course look for code to migrate only the files that need it.
Comment 11 Janek Walkenhorst univentionstaff 2017-08-14 18:11:14 CEST
Created attachment 9117 [details]
sanitizing.patch

A patch to solve the problem by sanitizing (using urlencoding)
Comment 12 Janek Walkenhorst univentionstaff 2017-08-14 18:11:56 CEST
Created attachment 9118 [details]
rejection.patch

A patch to solve the problem by rejecting "bad" inputs.
Comment 13 Philipp Hahn univentionstaff 2018-10-17 16:33:29 CEST
The good news:
  # udm dns/forward_zone create --set nameserver="$(hostname -f)" --set contact="root@$(dnsdomainname)" --set zone=../../../../proc/sysrq-trigger
  E: Invalid Syntax: Zone name: Labels must be between 1 and 63 characters long!

The bad news: Using the low-level LDAP tools directly it is still possible
  # eval "$(ucr shell hostname domainname ldap/base)"
  # ldapadd -Y EXTERNAL -H ldapi:/// <<__LDIF__
dn: zoneName=../../../../var/lib/univention-ldap/ldap/data.mdb,cn=dns,${ldap_base} 
nSRecord: ${hostname}.${domainname}.
objectClass: dNSZone
objectClass: top
dNSTTL: 10800
relativeDomainName: @
zoneName: ../../../../var/lib/univention-ldap/ldap/data.mdb
sOARecord: ${hostname}.${domainname}. root.${domainname}. 9 28800 7200 604800 10800
__LDIF__

But you already need LDAP admin credentials, so you can do arbitrary things in LDAP already.
This is still an security issue if you delegate DNS administration to a user via UMC.

Looking at the code we could do something like this:
  try:
    _validated_zonename = univention.admin.syntax.dnsZone.parse(zonename)
  except univention.admin.uexceptions.valueError:
    return  # refuse invalid DNS zone names


PS: The UDL modules postrun() logic breaks if you create a zone in TLD '.proxy':
  298 »···»···»···»···if not f.endswith('.proxy'):
Comment 14 Stefan Gohmann univentionstaff 2019-01-03 07:19:11 CET
This issue has been filled against UCS 4.1. The maintenance with bug and security fixes for UCS 4.1 has ended on 5st of April 2018.

Customers still on UCS 4.1 are encouraged to update to UCS 4.3. Please contact
your partner or Univention for any questions.

If this issue still occurs in newer UCS versions, please use "Clone this bug" or simply reopen the issue. In this case please provide detailed information on how this issue is affecting you.
Comment 15 Florian Best univentionstaff 2019-04-26 12:36:31 CEST
(In reply to Philipp Hahn from comment #13)
> Looking at the code we could do something like this:
>   try:
>     _validated_zonename = univention.admin.syntax.dnsZone.parse(zonename)
>   except univention.admin.uexceptions.valueError:
>     return  # refuse invalid DNS zone names
It's not restrictive enough and it prevents overwriting the syntax via UCR:
>>> univention.admin.syntax.dnsZone.parse('foo"\x00.zone')
'foo"\x00.zone'
Also importing UDM might not be good.

> PS: The UDL modules postrun() logic breaks if you create a zone in TLD '.proxy':
>   298 »···»···»···»···if not f.endswith('.proxy'):
I am forbidding zone names which end with .proxy and .zone now.

Following fixes have been done:
* added base dir limitation to the specific directories. zones are ignored if they are outside.
* validate the zone name, so that invalid names cannot crash named/bind: disallowed:
** null-byte
** "
** {0,127,256}.in-add.arpa
** .
** .. anywhere in the name
** zones ending with .proxy or .zone
** control characters
* fix urlencoding of the password and ldap hostdn when inserting into the ldap-uri (e.g. password containing "," or password file ending with \n breaks)
* fix zone name like "foo.zone.bar"

univention-bind (13.0.1-3)
dec0e02a4ed2 | Bug #41005: Merge branch 'fbest/41005-zone-name-path-injection' into 4.4-0
42e446896dcc | Bug #41005: fix path injection vulnerable in bind listener
acfaaf28cab0 | Bug #41005: prevent {0,127,255}.in-addr.arpa zones crashing bind/named
5b31c6bb2fd0 | Bug #41005: validate zone name
53e285eeb91e | Bug #41005: add basedir restriction
0c9e38e45bfc | Bug #41005: fix urlencoding of password and password files ending with \n
61592edb1282 | Bug #41005: fix zone names containing ".zone"
Comment 16 Florian Best univentionstaff 2019-04-26 12:41:19 CEST
Ohh, and I commmited a test case (67_udm-dns/110_dns_forward_zone_name_validation.py) which does various things which break bind. Therefor I did not enable it yet, will do it when the erratum is released.
Comment 17 Arvid Requate univentionstaff 2019-04-29 19:51:02 CEST
Created attachment 9996 [details]
qa.diff

Ok, looks good, two small suggestions attached. Also, I guess the character '"' could be allowed if validate_zonename would zonemame.replace('"','\\"'). At least named-checkconf seemed to be ok with zone "\"". But I don't insist on that.
Comment 18 Florian Best univentionstaff 2019-04-29 20:09:37 CEST
Thanks, patch plus additional quoting of " added.
Comment 19 Arvid Requate univentionstaff 2019-04-29 21:07:44 CEST
Verified:
* Code review
* Functional test
* Advisory
Comment 20 Florian Best univentionstaff 2019-04-29 21:12:54 CEST
I also removed two useless lines: /var/cache/univention-bind-proxy/ only contains files which end with ".zone".
Comment 21 Arvid Requate univentionstaff 2019-04-29 21:34:34 CEST
Ok.
Comment 22 Arvid Requate univentionstaff 2019-05-02 13:22:12 CEST
<http://errata.software-univention.de/ucs/4.4/68.html>