diff --git a/modules/generators/random/random_file_path/manifests/.no_puppet b/modules/generators/random/random_file_path/manifests/.no_puppet new file mode 100644 index 000000000..e69de29bb diff --git a/modules/generators/random/random_file_path/random_file_path.pp b/modules/generators/random/random_file_path/random_file_path.pp new file mode 100644 index 000000000..e69de29bb diff --git a/modules/generators/random/random_file_path/secgen_local/local.rb b/modules/generators/random/random_file_path/secgen_local/local.rb new file mode 100644 index 000000000..d8989abe0 --- /dev/null +++ b/modules/generators/random/random_file_path/secgen_local/local.rb @@ -0,0 +1,181 @@ +#!/usr/bin/ruby +require_relative '../../../../../lib/objects/local_string_generator.rb' +require 'logger' +require 'securerandom' + +class FilePathGenerator < StringGenerator + attr_accessor :selected_location + attr_accessor :username + + BASE_SYSTEM_LOCATIONS = [ + '/usr/bin', + '/etc/cron.d', + '/opt', + '/usr/local/bin', + '/usr/sbin', + '/var/tmp', + '/usr/share', + '/var/opt', + '/usr/lib', + '/srv', + '/var/spool', + '/usr/src', + '/var/cache' + ].freeze + + def initialize + super + self.module_name = 'Random File Path Generator' + @logger = Logger.new($stderr) + @logger.level = Logger::INFO + @logger.formatter = proc do |severity, datetime, _progname, msg| + "[#{datetime.strftime('%Y-%m-%d %H:%M:%S')}] #{severity}: #{msg}\n" + end + @username = nil + end + + def get_options_array + super + [ + ['--validate-writable', GetoptLong::OPTIONAL_ARGUMENT], + ['--username', GetoptLong::REQUIRED_ARGUMENT] + ] + end + + def process_options(opt, arg) + super + case opt + when '--validate-writable' + @validate_writable = true + when '--username' + @username = arg + end + end + + def predefined_locations + locations = BASE_SYSTEM_LOCATIONS.dup + if @username && !@username.empty? + locations << "/home/#{@username}/.config" + locations << "/home/#{@username}/.config/local" + locations << "/home/#{@username}/.local/share" + @logger.info("Including user-specific paths for username: #{@username}") + else + @logger.info("No username provided, using only system locations") + end + locations << "/root/.config" + locations << "/root/.local/share" + locations + end + + def validate_path(path) + unless path.is_a?(String) && !path.empty? + @logger.error("Invalid path: path must be a non-empty string") + return false + end + + normalized_path = File.expand_path(path) + + unless normalized_path == path + @logger.warn("Path '#{path}' normalized to '#{normalized_path}'") + end + + unless predefined_locations.include?(normalized_path) + @logger.error("Path '#{normalized_path}' is not in the predefined list of allowed locations") + return false + end + + @logger.info("Path validation successful for: #{normalized_path}") + true + end + + def check_write_permissions(path) + return false unless validate_path(path) + + begin + if File.directory?(path) + if File.writable?(path) + @logger.info("Write permission confirmed for: #{path}") + return true + else + @logger.warn("No write permission for existing directory: #{path}") + end + else + parent_dir = File.dirname(path) + if File.directory?(parent_dir) && File.writable?(parent_dir) + @logger.info("Parent directory writable, can create: #{path}") + return true + else + @logger.warn("Cannot create path, parent not writable: #{parent_dir}") + end + end + rescue Errno::EACCES => e + @logger.error("Permission denied accessing: #{path} - #{e.message}") + rescue Errno::ENOENT => e + @logger.error("Path does not exist: #{path} - #{e.message}") + rescue StandardError => e + @logger.error("Unexpected error checking permissions for #{path}: #{e.message}") + end + + false + end + + def filter_available_locations + locations = predefined_locations + available = locations.select do |location| + begin + if File.directory?(location) + @logger.info("Location available: #{location}") + true + else + @logger.warn("Location not found (will be created): #{location}") + true + end + rescue StandardError => e + @logger.error("Error checking location #{location}: #{e.message}") + false + end + end + + if available.empty? + @logger.warn("No locations available, using fallback: /tmp") + available = ['/tmp'] + end + + available + end + + def select_random_location + available_locations = filter_available_locations + + if @validate_writable + writable_locations = available_locations.select { |loc| check_write_permissions(loc) } + if writable_locations.empty? + @logger.warn("No writable locations found, selecting from all available") + writable_locations = available_locations + end + available_locations = writable_locations + end + + selected = available_locations.sample(random: Random.new) + @logger.info("=" * 60) + @logger.info("SELECTED PATH: #{selected}") + @logger.info("Username context: #{@username}") + @logger.info("Selection made from #{available_locations.length} available path(s)") + @logger.info("Available paths were: #{available_locations.join(', ')}") + @logger.info("=" * 60) + @logger.info("AUDIT: Random path selected at #{Time.now.utc.strftime('%Y-%m-%dT%H:%M:%SZ')}") + @logger.info("AUDIT: Path: #{selected}") + @logger.info("AUDIT: Username: #{@username}") + @logger.info("AUDIT: Module: #{module_name}") + @logger.info("=" * 60) + + selected + end + + def generate + location = select_random_location + self.outputs << location + self.selected_location = location + end +end + +FilePathGenerator.new.run \ No newline at end of file diff --git a/modules/generators/random/random_file_path/secgen_metadata.xml b/modules/generators/random/random_file_path/secgen_metadata.xml new file mode 100644 index 000000000..db67a3c0d --- /dev/null +++ b/modules/generators/random/random_file_path/secgen_metadata.xml @@ -0,0 +1,19 @@ + + + + Random File Path Generator + Rosie Fletcher + MIT + Randomly selects a file path from a predefined list of system locations designed for misconfiguration privilege escalation vulnerabilities, specifically cron. If a username is provided, user-specific paths are also included. + + location_generator + path_generator + local_calculation + linux + + username + + location + \ No newline at end of file diff --git a/modules/vulnerabilities/unix/access_control_misconfigurations/cron_tar_wildcard/cron_tar_wildcard.pp b/modules/vulnerabilities/unix/access_control_misconfigurations/cron_tar_wildcard/cron_tar_wildcard.pp new file mode 100644 index 000000000..ae0b03023 --- /dev/null +++ b/modules/vulnerabilities/unix/access_control_misconfigurations/cron_tar_wildcard/cron_tar_wildcard.pp @@ -0,0 +1 @@ +include cron_tar_wildcard::config diff --git a/modules/vulnerabilities/unix/access_control_misconfigurations/cron_tar_wildcard/manifests/config.pp b/modules/vulnerabilities/unix/access_control_misconfigurations/cron_tar_wildcard/manifests/config.pp new file mode 100644 index 000000000..5e75cc797 --- /dev/null +++ b/modules/vulnerabilities/unix/access_control_misconfigurations/cron_tar_wildcard/manifests/config.pp @@ -0,0 +1,179 @@ +class cron_tar_wildcard::config { + $secgen_parameters = secgen_functions::get_parameters($::base64_inputs_file) + $leaked_filenames = $secgen_parameters['leaked_filenames'] + $strings_to_leak = $secgen_parameters['strings_to_leak'] + + # Extract first element from arrays (SecGen generators output arrays) + $show_crontab_hint_raw = $secgen_parameters['show_crontab_hint'] + $show_crontab_hint = $show_crontab_hint_raw ? { + undef => 'false', + default => $show_crontab_hint_raw[0], + } + + $cron_location_raw = $secgen_parameters['cron_location'] + $cron_location = $cron_location_raw ? { + undef => '/opt', + default => $cron_location_raw[0], + } + + $cron_user_raw = $secgen_parameters['cron_user'] + $cron_user = $cron_user_raw ? { + undef => 'root', + default => $cron_user_raw[0], + } + + $restrict_write_user_raw = $secgen_parameters['restrict_write_user'] + $restrict_write_user_input = $restrict_write_user_raw ? { + undef => '', + default => $restrict_write_user_raw[0], + } + + $backup_dir = "${cron_location}/backup" + $archive_dest = '/var/spool/backups' + + $leak_storage_dir = $cron_user ? { + 'root' => '/root', + default => "/home/${cron_user}", + } + + # Ensure cron_location directory exists (may be nested like /home/user/.config/local) + file { $cron_location: + ensure => directory, + owner => 'root', + group => 'root', + mode => '0755', + } + + # Declare sudo class at top level (sudo::conf requires $sudo::config_dir) + class { 'sudo': + config_file_replace => false, + } + + package { 'cron': + ensure => installed, + } + + service { 'cron': + ensure => running, + enable => true, + require => Package['cron'], + } + + if $restrict_write_user_input and $restrict_write_user_input != '' { + $restrict_write_user = $restrict_write_user_input + $backup_dir_owner = $restrict_write_user + $backup_dir_group = $restrict_write_user + $backup_dir_mode = '0755' + $archive_owner = $restrict_write_user + $archive_group = $restrict_write_user + $archive_mode = '0755' + + exec { "validate_restricted_user": + command => "/usr/bin/id ${restrict_write_user}", + unless => "/usr/bin/id ${restrict_write_user}", + path => ['/usr/bin'], + before => File[$backup_dir], + } + + notice("Permission hierarchy: Write access restricted to user '${restrict_write_user}'") + notice("Backup directory mode: 0755 (user-writable only)") + notice("Archive directory mode: 0755 (user-writable only)") + + file { $archive_dest: + ensure => directory, + owner => $archive_owner, + group => $archive_group, + mode => $archive_mode, + } + + file { $backup_dir: + ensure => directory, + owner => $backup_dir_owner, + group => $backup_dir_group, + mode => $backup_dir_mode, + require => [Exec['validate_restricted_user'], File[$cron_location]], + } + } else { + $backup_dir_owner = 'root' + $backup_dir_group = 'root' + $backup_dir_mode = '0777' + $archive_owner = 'root' + $archive_group = 'root' + $archive_mode = '0777' + + notice("Permission hierarchy: Write access global (mode 0777)") + notice("Backup directory mode: 0777 (world-writable)") + notice("Archive directory mode: 0777 (world-writable)") + + file { $archive_dest: + ensure => directory, + owner => $archive_owner, + group => $archive_group, + mode => $archive_mode, + } + + file { $backup_dir: + ensure => directory, + owner => $backup_dir_owner, + group => $backup_dir_group, + mode => $backup_dir_mode, + require => File[$cron_location], + } + } + + if $show_crontab_hint =~ /^(true|1|yes)$/ { + sudo::conf { 'users_sudo_list': + ensure => present, + content => 'ALL ALL=(root) NOPASSWD: /usr/bin/sudo -l', + } + + if $restrict_write_user_input and $restrict_write_user_input != '' { + sudo::conf { 'restricted_crontab_list': + ensure => present, + content => "${restrict_write_user} ALL=(root) NOPASSWD: /usr/bin/crontab -l", + } + + notice("Crontab hint restricted: Only user '${restrict_write_user}' can view crontab") + } else { + sudo::conf { 'users_crontab_list': + ensure => present, + content => 'ALL ALL=(root) NOPASSWD: /usr/bin/crontab -l', + } + + notice("Crontab hint global: All users can view crontab") + } + + file { "${cron_location}/backup.txt": + ensure => file, + owner => 'root', + group => 'root', + mode => '0644', + content => "# cron backup hint\n# Try: sudo -l to see what you can run\n# Then: sudo crontab -l to view root's cron jobs\n(cd ${backup_dir} && tar -zcf ${archive_dest}/backup.tar.gz *)\n", + } + } else { + file { "${cron_location}/backup.txt": + ensure => file, + owner => 'root', + group => 'root', + mode => '0644', + content => "# cron backup hint\n(cd ${backup_dir} && tar -zcf ${archive_dest}/backup.tar.gz *)\n", + } + } + + cron { 'backup_wildcard': + command => "cd ${backup_dir} && tar -zcf ${archive_dest}/backup.tar.gz *", + user => $cron_user, + hour => '*', + minute => '*', + require => [Service['cron'], File[$backup_dir], File[$archive_dest]], + } + + ::secgen_functions::leak_files { 'cron_tar_wildcard-file-leak': + storage_directory => $leak_storage_dir, + leaked_filenames => $leaked_filenames, + strings_to_leak => $strings_to_leak, + owner => $cron_user, + mode => '0600', + leaked_from => 'cron_tar_wildcard', + } +} \ No newline at end of file diff --git a/modules/vulnerabilities/unix/access_control_misconfigurations/cron_tar_wildcard/secgen_metadata.xml b/modules/vulnerabilities/unix/access_control_misconfigurations/cron_tar_wildcard/secgen_metadata.xml new file mode 100644 index 000000000..6120ef8ff --- /dev/null +++ b/modules/vulnerabilities/unix/access_control_misconfigurations/cron_tar_wildcard/secgen_metadata.xml @@ -0,0 +1,66 @@ + + + Cron Tar Wildcard Privilege Escalation + Rosie Fletcher + MIT + A cron job runs tar with a wildcard (*) inside a user-writable directory. This can be + exploited using tar checkpoint flag injection by creating specially-named files (--checkpoint=1 and + --checkpoint-action=exec=sh exploit.sh) to execute arbitrary commands as the cron user. + + By default, the backup directory is world-writable (mode 0777) allowing any user to exploit. + Optionally, write access can be restricted to a specific user via the restrict_write_user parameter, + enabling targeted privilege escalation scenarios where only that user can exploit the vulnerability. + When restrict_write_user is set, crontab viewing via sudo is also restricted to that user only. + + access_control_misconfiguration + cron_privilege_escalation + root_rwx + local + linux + medium + + strings_to_leak + leaked_filenames + show_crontab_hint + cron_location + cron_user + restrict_write_user + + + + + + + + + true + + + + + + root + + + + + + + utilities/unix/puppet_module/cron_new + + + utilities/unix/puppet_module/sudo + + + + access control + Elevated privileges + Vulnerabilities and attacks on access control misconfigurations + + + Access controls and operating systems + Linux security model + + \ No newline at end of file diff --git a/modules/vulnerabilities/unix/access_control_misconfigurations/cron_tar_wildcard/secgen_test/cron_tar_wildcard.rb b/modules/vulnerabilities/unix/access_control_misconfigurations/cron_tar_wildcard/secgen_test/cron_tar_wildcard.rb new file mode 100644 index 000000000..5413df512 --- /dev/null +++ b/modules/vulnerabilities/unix/access_control_misconfigurations/cron_tar_wildcard/secgen_test/cron_tar_wildcard.rb @@ -0,0 +1,18 @@ +require_relative '../../../../../lib/post_provision_test' + +class CronTarWildcardTest < PostProvisionTest + def initialize + self.module_name = 'cron_tar_wildcard' + self.module_path = get_module_path(__FILE__) + super + end + + def test_module + super + test_local_command('backup directory exists and is world-writable?', 'sudo ls -la /opt/backup', 'backup') + test_local_command('hint file is readable?', 'sudo cat /opt/backup.txt', 'tar') + test_local_command('cron job exists?', 'sudo crontab -l', 'backup_wildcard') + end +end + +CronTarWildcardTest.new.run diff --git a/modules/vulnerabilities/unix/access_control_misconfigurations/cron_writable_script/cron_writable_script.pp b/modules/vulnerabilities/unix/access_control_misconfigurations/cron_writable_script/cron_writable_script.pp new file mode 100644 index 000000000..6584bdd24 --- /dev/null +++ b/modules/vulnerabilities/unix/access_control_misconfigurations/cron_writable_script/cron_writable_script.pp @@ -0,0 +1 @@ +include cron_writable_script::config diff --git a/modules/vulnerabilities/unix/access_control_misconfigurations/cron_writable_script/manifests/config.pp b/modules/vulnerabilities/unix/access_control_misconfigurations/cron_writable_script/manifests/config.pp new file mode 100644 index 000000000..716e4abab --- /dev/null +++ b/modules/vulnerabilities/unix/access_control_misconfigurations/cron_writable_script/manifests/config.pp @@ -0,0 +1,80 @@ +class cron_writable_script::config { + $secgen_parameters = secgen_functions::get_parameters($::base64_inputs_file) + $leaked_filenames = $secgen_parameters['leaked_filenames'] + $strings_to_leak = $secgen_parameters['strings_to_leak'] + + # Extract first element from arrays (SecGen generators output arrays) + $cron_location_raw = $secgen_parameters['cron_location'] + $cron_location = $cron_location_raw ? { + undef => '/opt', + default => $cron_location_raw[0], + } + + $cron_message_raw = $secgen_parameters['cron_message'] + $cron_message = $cron_message_raw ? { + undef => 'hello from cron', + default => $cron_message_raw[0], + } + + $cron_user_raw = $secgen_parameters['cron_user'] + $cron_user = $cron_user_raw ? { + undef => 'root', + default => $cron_user_raw[0], + } + + # Validate cron_user exists (if not root) + if $cron_user != 'root' { + exec { "validate_cron_user_${cron_user}": + command => "/usr/bin/id ${cron_user}", + unless => "/usr/bin/id ${cron_user}", + path => ['/usr/bin'], + } + } + + $cron_script_path = "${cron_location}/cron.sh" + $leak_storage_dir = $cron_user ? { + 'root' => '/root', + default => "/home/${cron_user}", + } + + # Ensure cron_location directory exists (may be nested like /home/user/.config/local) + file { $cron_location: + ensure => directory, + owner => 'root', + group => 'root', + mode => '0755', + } + + # Define requirements for the script file + if $cron_user == 'root' { + $cron_script_requires = [File[$cron_location]] + } else { + $cron_script_requires = [Exec["validate_cron_user_${cron_user}"], File[$cron_location]] + } + + file { $cron_script_path: + ensure => file, + owner => 'root', + group => 'root', + mode => '0777', + content => "#!/bin/bash -p\necho '${cron_message}' >> /tmp/cron.log\n", + require => $cron_script_requires, + } + + cron { 'writable_cron_script': + command => $cron_script_path, + user => $cron_user, + hour => '*', + minute => '*', + require => File[$cron_script_path], + } + + ::secgen_functions::leak_files { 'cron_writable_script-file-leak': + storage_directory => $leak_storage_dir, + leaked_filenames => $leaked_filenames, + strings_to_leak => $strings_to_leak, + owner => $cron_user, + mode => '0600', + leaked_from => 'cron_writable_script', + } +} \ No newline at end of file diff --git a/modules/vulnerabilities/unix/access_control_misconfigurations/cron_writable_script/secgen_metadata.xml b/modules/vulnerabilities/unix/access_control_misconfigurations/cron_writable_script/secgen_metadata.xml new file mode 100644 index 000000000..027d0006b --- /dev/null +++ b/modules/vulnerabilities/unix/access_control_misconfigurations/cron_writable_script/secgen_metadata.xml @@ -0,0 +1,54 @@ + + + Cron Writable Script Privilege Escalation + Rosie Fletcher + MIT + A cron job executes a world-writable script. By default runs as root for vertical privilege escalation. + Optionally configurable to run as a non-privileged user, enabling horizontal privilege escalation scenarios + where a lower-privileged user can escalate to another user account. + + access_control_misconfiguration + cron_privilege_escalation + root_rwx + local + linux + low + + strings_to_leak + leaked_filenames + cron_location + cron_message + cron_user + + + + + + + + + + + + + + + root + + + + utilities/unix/puppet_module/cron_new + + + + access control + Elevated privileges + Vulnerabilities and attacks on access control misconfigurations + + + Access controls and operating systems + Linux security model + + \ No newline at end of file diff --git a/modules/vulnerabilities/unix/access_control_misconfigurations/cron_writable_script/secgen_test/cron_writable_script.rb b/modules/vulnerabilities/unix/access_control_misconfigurations/cron_writable_script/secgen_test/cron_writable_script.rb new file mode 100644 index 000000000..d2b8dda58 --- /dev/null +++ b/modules/vulnerabilities/unix/access_control_misconfigurations/cron_writable_script/secgen_test/cron_writable_script.rb @@ -0,0 +1,17 @@ +require_relative '../../../../../lib/post_provision_test' + +class CronWritableScriptTest < PostProvisionTest + def initialize + self.module_name = 'cron_writable_script' + self.module_path = get_module_path(__FILE__) + super + end + + def test_module + super + test_local_command('cron.sh is world-writable?', 'sudo ls -la /opt/cron.sh', 'rwxrwxrwx') + test_local_command('cron job exists?', 'sudo crontab -l', 'writable_cron_script') + end +end + +CronWritableScriptTest.new.run diff --git a/scenarios/ctf/root_cron-trol.xml b/scenarios/ctf/root_cron-trol.xml new file mode 100644 index 000000000..3007bdac0 --- /dev/null +++ b/scenarios/ctf/root_cron-trol.xml @@ -0,0 +1,172 @@ + + + + + Root Cron-trol + Rosie Fletcher + A two-stage privilege escalation challenge. Login to the desktop as a low-privileged user (password: tiaspbiqe2r), + then perform horizontal privilege escalation by exploiting a writable cron script owned by another user. + Finally, achieve vertical privilege escalation to root through a tar wildcard injection vulnerability in a + root-owned cron job. Master the art of cron-based privilege escalation! Login password: tiaspbiqe2r + + ctf + attack-ctf + pwn-ctf + intermediate + + + + Elevated privileges + Vulnerabilities and attacks on access control misconfigurations + Horizontal privilege escalation + + + + + Vertical privilege escalation + + + + + Access controls and operating systems + Linux security model + + + + + kill chains + + + cyber kill chain + + + + + server + + + + + + + + tiaspbiqe2r + + + + + + + + + + + + + + + + lowpriv_user + + + lowpriv_pass + + + false + + + + + target_user + + + target_pass + + + false + + + + + + + + lowpriv_user + + + true + + + + + + + + + + + + + + + + target_user + + + + + + + target_user + + + + + + + + horizontal_flag + + + target_user + + + horizontal_cron_location + + + + + + + root_flag + + + root + + + target_user + + + vertical_cron_location + + + true + + + + + + + + + + + + spoiler_admin_pass + + + + + \ No newline at end of file