Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
2fcbbbb
Add cron-based privilege escalation modules and CTF scenario
girlier Mar 28, 2026
fb1c9c3
Fix invalid difficulty value in cron_writable_script metadata
girlier Mar 30, 2026
2594cf9
Fix cron_privesc scenario: change server base type from server to des…
girlier Mar 30, 2026
bb7b33d
Change Hint to involve sudo -l
girlier Apr 10, 2026
945a814
Add random_file_path generator for configurable cron locations
girlier Apr 29, 2026
3c03dc8
Add optional cron_user parameter for horizontal privilege escalation
girlier Apr 29, 2026
f1ea5fa
Add permission control to cron_tar_wildcard module
girlier Apr 29, 2026
fa02321
Restrict crontab viewing when restrict_write_user is set
girlier Apr 29, 2026
fda8450
Implement two-stage privilege escalation scenario: Root Cron-trol
girlier Apr 29, 2026
a70879e
Make target_user random in Root Cron-trol scenario
girlier Apr 29, 2026
135d926
Format root_cron-trol scenario to match project standards
girlier Apr 29, 2026
410d259
Fix base64 decode crash: Logger outputs to stderr not stdout
girlier Apr 29, 2026
08375a3
Fix Puppet array interpolation: extract [0] from secgen_parameters
girlier Apr 29, 2026
2202f44
Fix sudo class declaration: move outside conditional block
girlier Apr 29, 2026
d0d681d
Comprehensive audit fixes: critical and high severity issues
girlier Apr 29, 2026
2fcfc38
Fix sudo puppet module dependency in cron_tar_wildcard
girlier Apr 29, 2026
5db6a09
CRITICAL FIX: Separate requires blocks for each puppet module dependency
girlier Apr 29, 2026
9eff587
Fix: Create cron_location directory before placing files
girlier Apr 29, 2026
8c07f84
Simplify scenario: single machine, no SSH, no Kali
girlier Apr 30, 2026
aa4d635
Change lowpriv to random username with fixed password
girlier Apr 30, 2026
19d020c
Update scenario: separate cron locations, KDE autologin, DHCP network
girlier Apr 30, 2026
484a623
Merge branch 'cliffe:master' into cron_priv_esc
girlier May 27, 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
Empty file.
Empty file.
181 changes: 181 additions & 0 deletions modules/generators/random/random_file_path/secgen_local/local.rb
Original file line number Diff line number Diff line change
@@ -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
19 changes: 19 additions & 0 deletions modules/generators/random/random_file_path/secgen_metadata.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?xml version="1.0"?>

<generator xmlns="http://www.github/cliffe/SecGen/generator"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.github/cliffe/SecGen/generator">
<name>Random File Path Generator</name>
<author>Rosie Fletcher</author>
<module_license>MIT</module_license>
<description>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.</description>

<type>location_generator</type>
<type>path_generator</type>
<type>local_calculation</type>
<platform>linux</platform>

<read_fact>username</read_fact>

<output_type>location</output_type>
</generator>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
include cron_tar_wildcard::config
Original file line number Diff line number Diff line change
@@ -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',
}
}
Loading