~apport-hackers/apport/trunk

« back to all changes in this revision

Viewing changes to test/test_signal_crashes.py

  • Committer: Martin Pitt
  • Date: 2015-05-13 09:12:29 UTC
  • Revision ID: martin.pitt@canonical.com-20150513091229-d1f3cjq0o69h07es
SECURITY UPDATE: Fix local root privilege escalation through suid root exe core files

When /proc/sys/fs/suid_dumpable is enabled, crashing a program that is suid
root or not readable for the user would create root-owned core files in the
current directory of that program. Creating specially crafted core files in
/etc/logrotate.d or similar could then lead to arbitrary code execution with
root privileges.

Fix apport's drop_privileges() to actually drop privileges to the crashed
program's real ID. Before it was dropping to the owner of /proc/pid/stat, which
is root for suid root or unreadable executables.
This requires special-casing when writing .crash reports: We can't chmod the
written file to become readable as it needs to be owned by root and we already
dropped privileges; so create the reports with 0640 permissions right from the
start.

Don't write a core file for the kinds of executables above. Their
/proc/pid/stat is owned by root (or the user suid'ed to), only write core files
for processes whose real ID matches that ownership. (Note that comparing
against effective ID does not work as processes can drop their privileges.)
This is in accordance with the intention of core(5) and proc(5) whose intention
is to only allow suid_dumpable to pipes (i. e. apport) but not to core files in cwd.

Adjust signal_crashes.test_crash_setuid_{keep,drop} accordingly.  Add tests for
running a suid root and an unreadable executable in a non-user-writable cwd.
These reproduce the original exploit.

Thanks to Sander Bos for discovering this issue!

CVE-2015-1324
LP: #1452239

Show diffs side-by-side

added added

removed removed

Lines of Context:
289
289
        self.assertGreater(count, 1, 'gets at least 2 repeated crashes')
290
290
        self.assertLess(count, 7, 'stops flooding after less than 7 repeated crashes')
291
291
 
 
292
    @unittest.skipIf(os.access('/run', os.W_OK), 'this test needs to be run as user')
292
293
    def test_nonwritable_cwd(self):
293
294
        '''core dump works for non-writable cwd'''
294
295
 
295
 
        os.chdir('/')
 
296
        os.chdir('/run')
 
297
        resource.setrlimit(resource.RLIMIT_CORE, (-1, -1))
296
298
        self.do_crash()
297
299
        pr = apport.Report()
298
300
        self.assertTrue(os.path.exists(self.test_report))
 
301
        self.assertFalse(os.path.exists('/run/core'))
299
302
        with open(self.test_report, 'rb') as f:
300
303
            pr.load(f)
301
304
        assert set(required_fields).issubset(set(pr.keys()))
302
305
 
 
306
    @unittest.skipIf(os.access('/run', os.W_OK), 'this test needs to be run as user')
 
307
    def test_nonwritable_cwd_nonreadable_exe(self):
 
308
        '''no core file for non-readable exe in non-writable cwd'''
 
309
 
 
310
        # CVE-2015-1324: if a user cannot read an executable, it behaves much
 
311
        # like a suid root binary in terms of writing a core dump
 
312
 
 
313
        # create a non-readable executable in a path we can modify which apport
 
314
        # regards as likely packaged
 
315
        (fd, myexe) = tempfile.mkstemp(dir='/var/tmp')
 
316
        self.addCleanup(os.unlink, myexe)
 
317
        with open(test_executable, 'rb') as f:
 
318
            os.write(fd, f.read())
 
319
        os.close(fd)
 
320
        os.chmod(myexe, 0o111)
 
321
 
 
322
        os.chdir('/run')
 
323
        resource.setrlimit(resource.RLIMIT_CORE, (-1, -1))
 
324
 
 
325
        if suid_dumpable:
 
326
            self.do_crash(True, command=myexe, expect_corefile=False)
 
327
 
 
328
            # check crash report
 
329
            reports = apport.fileutils.get_new_system_reports()
 
330
            self.assertEqual(len(reports), 1)
 
331
            report = reports[0]
 
332
            st = os.stat(report)
 
333
            # FIXME: we would like to clean up this, but don't have privileges for that
 
334
            # os.unlink(report)
 
335
            self.assertEqual(stat.S_IMODE(st.st_mode), 0o640, 'report has correct permissions')
 
336
            # this must be owned by root as it is an unreadable binary
 
337
            self.assertEqual(st.st_uid, 0, 'report has correct owner')
 
338
 
 
339
            # no user reports
 
340
            self.assertEqual(apport.fileutils.get_all_reports(), [])
 
341
        else:
 
342
            # no cores/dump if suid_dumpable == 0
 
343
            self.do_crash(False, command=myexe, expect_corefile=False)
 
344
            self.assertEqual(apport.fileutils.get_all_reports(), [])
 
345
 
303
346
    def test_core_dump_packaged(self):
304
347
        '''packaged executables create core dumps on proper ulimits'''
305
348
 
529
572
 
530
573
    @unittest.skipIf(os.geteuid() != 0, 'this test needs to be run as root')
531
574
    def test_crash_setuid_keep(self):
532
 
        '''report generation and core dump for setuid program which stays root'''
 
575
        '''report generation for setuid program which stays root'''
533
576
 
534
577
        # create suid root executable in a path we can modify which apport
535
578
        # regards as likely packaged
544
587
        resource.setrlimit(resource.RLIMIT_CORE, (-1, -1))
545
588
 
546
589
        if suid_dumpable:
547
 
            # expect the core file to be owned by root
548
 
            self.do_crash(command=myexe, expect_corefile=True, uid=8,
549
 
                          expect_corefile_owner=0)
 
590
            # if a user can crash a suid root binary, it should not create core files
 
591
            self.do_crash(command=myexe, uid=8)
550
592
 
551
593
            # check crash report
552
594
            reports = apport.fileutils.get_all_reports()
565
607
    @unittest.skipUnless(os.path.exists('/bin/ping'), 'this test needs /bin/ping')
566
608
    @unittest.skipIf(os.geteuid() != 0, 'this test needs to be run as root')
567
609
    def test_crash_setuid_drop(self):
568
 
        '''report generation and core dump for setuid program which drops root'''
 
610
        '''report generation for setuid program which drops root'''
569
611
 
570
612
        # run ping as user "mail"
571
613
        resource.setrlimit(resource.RLIMIT_CORE, (-1, -1))
572
614
 
573
615
        if suid_dumpable:
574
 
            # expect the core file to be owned by root
575
 
            self.do_crash(command='/bin/ping', args=['127.0.0.1'],
576
 
                          expect_corefile=True, uid=8,
577
 
                          expect_corefile_owner=0)
 
616
            # if a user can crash a suid root binary, it should not create core files
 
617
            self.do_crash(command='/bin/ping', args=['127.0.0.1'], uid=8)
578
618
 
579
619
            # check crash report
580
620
            reports = apport.fileutils.get_all_reports()
591
631
                          uid=8)
592
632
            self.assertEqual(apport.fileutils.get_all_reports(), [])
593
633
 
 
634
    @unittest.skipIf(os.geteuid() != 0, 'this test needs to be run as root')
 
635
    def test_crash_setuid_unpackaged(self):
 
636
        '''report generation for unpackaged setuid program'''
 
637
 
 
638
        # create suid root executable in a path we can modify which apport
 
639
        # regards as not packaged
 
640
        (fd, myexe) = tempfile.mkstemp(dir='/tmp')
 
641
        self.addCleanup(os.unlink, myexe)
 
642
        with open(test_executable, 'rb') as f:
 
643
            os.write(fd, f.read())
 
644
        os.close(fd)
 
645
        os.chmod(myexe, 0o4755)
 
646
 
 
647
        # run test program as user "mail"
 
648
        resource.setrlimit(resource.RLIMIT_CORE, (-1, -1))
 
649
 
 
650
        if suid_dumpable:
 
651
            # if a user can crash a suid root binary, it should not create core files
 
652
            self.do_crash(command=myexe, expect_corefile=False, uid=8)
 
653
        else:
 
654
            # no cores/dump if suid_dumpable == 0
 
655
            self.do_crash(False, command=myexe, expect_corefile=False, uid=8)
 
656
 
 
657
        # there should not be a crash report
 
658
        self.assertEqual(apport.fileutils.get_all_reports(), [])
 
659
 
 
660
    @unittest.skipIf(os.geteuid() != 0, 'this test needs to be run as root')
 
661
    def test_crash_setuid_nonwritable_cwd(self):
 
662
        '''report generation and core dump for setuid program, non-writable cwd'''
 
663
 
 
664
        # create suid root executable in a path we can modify which apport
 
665
        # regards as likely packaged
 
666
        (fd, myexe) = tempfile.mkstemp(dir='/var/tmp')
 
667
        self.addCleanup(os.unlink, myexe)
 
668
        with open(test_executable, 'rb') as f:
 
669
            os.write(fd, f.read())
 
670
        os.close(fd)
 
671
        os.chmod(myexe, 0o4755)
 
672
 
 
673
        # run test program as user "mail" in /run (which should only be
 
674
        # writable to root)
 
675
        os.chdir('/run')
 
676
        resource.setrlimit(resource.RLIMIT_CORE, (-1, -1))
 
677
 
 
678
        if suid_dumpable:
 
679
            # we expect a report, but no core file
 
680
            self.do_crash(command=myexe, expect_corefile=False, uid=8)
 
681
 
 
682
            # check crash report
 
683
            reports = apport.fileutils.get_all_reports()
 
684
            self.assertEqual(len(reports), 1)
 
685
            report = reports[0]
 
686
            st = os.stat(report)
 
687
            os.unlink(report)
 
688
            self.assertEqual(stat.S_IMODE(st.st_mode), 0o640, 'report has correct permissions')
 
689
            # this must be owned by root as it is a setuid binary
 
690
            self.assertEqual(st.st_uid, 0, 'report has correct owner')
 
691
        else:
 
692
            # no core/report if suid_dumpable == 0
 
693
            self.do_crash(False, command=myexe, expect_corefile=False, uid=8)
 
694
            self.assertEqual(apport.fileutils.get_all_reports(), [])
 
695
 
594
696
    @unittest.skipUnless(os.path.exists('/usr/bin/lxc-usernsexec'), 'this test needs lxc')
595
697
    @unittest.skipUnless(os.path.exists('/bin/busybox'), 'this test needs busybox')
596
698
    @unittest.skipIf(os.access('/etc/shadow', os.R_OK), 'this test needs to be run as user')
746
848
                os.unlink('core')
747
849
        else:
748
850
            if os.path.exists('core'):
749
 
                os.unlink('core')
 
851
                try:
 
852
                    os.unlink('core')
 
853
                except OSError as e:
 
854
                    sys.stderr.write(
 
855
                        'WARNING: cannot clean up core file %s/core: %s\n' %
 
856
                        (os.getcwd(), str(e)))
 
857
 
750
858
                self.fail('leaves unexpected core file behind')
751
859
 
752
860
    def get_temp_all_reports(self):