WiP: Script container and script generator tracer code - need to build the shebang into the module and finish implementing setuid script function

This commit is contained in:
ts
2018-08-02 20:39:47 +01:00
parent b730dc1cf6
commit b645916da9
23 changed files with 686 additions and 134 deletions

View File

@@ -0,0 +1,73 @@
# Install function for setgid binaries
# -- Modules calling this function must provide a Makefile and any .c files within it's <module_name>/files directory
define secgen_functions::install_setgid_script (
$challenge_name, # Challenge name, used for the wrapper-directory
$script_name, # Script filename
$script_data, # Script data
$source_module_name, # Name of the module that calls this function
$group, # Name of group
$account, # User account
$flag, # ctf flag string
$flag_name, # ctf flag name
$storage_dir = '', # Optional: Storage directory (takes precedent if supplied, e.g. nfs / smb share dir)
$strings_to_leak = [''], # Optional: strings to leak (could contain instructions or a message)
) {
if $account {
$username = $account['username']
::accounts::user { $username:
shell => '/bin/bash',
password => pw_hash($account['password'], 'SHA-512', 'mysalt'),
managehome => true,
home_mode => '0755',
}
$storage_directory = "/home/$username"
} elsif $storage_dir {
$storage_directory = $storage_dir
} else {
err('install: either account or storage_dir is required')
fail
}
$compile_directory = "$storage_directory/tmp"
$challenge_directory = "$storage_directory/$challenge_name"
$modules_source = "puppet:///modules/$source_module_name"
group { $group:
ensure => present,
}
# Create challenge directory
::secgen_functions::create_directory { "create_$challenge_directory":
path => $challenge_directory,
notify => File["$challenge_directory/$script_name"],
}
# Move the compiled binary into the challenge directory
file { "$challenge_directory/$script_name":
ensure => present,
owner => 'root',
group => $group,
mode => '4775',
content => $script_data,
require => Group[$group],
}
# Drop the flag file on the box and set permissions
::secgen_functions::leak_files { "$username-file-leak":
storage_directory => "$challenge_directory",
leaked_filenames => [$flag_name],
strings_to_leak => [$flag],
owner => 'root',
group => $group,
mode => '0440',
leaked_from => "accounts_$username",
require => Group[$group],
}
}

View File

@@ -0,0 +1,86 @@
# Install function for setuid_root binaries
# -- Modules calling this function must provide a Makefile and any .c files within it's <module_name>/files directory
define secgen_functions::install_setuid_root_binary (
$challenge_name, # Challenge name, used for the wrapper-directory
$source_module_name, # Name of the module that calls this function
$account, # User account (leak here if $storage_directory is not supplied)
$flag, # ctf flag string
$flag_name, # ctf flag name
$storage_dir = '', # Optional: Storage directory (takes precedent if supplied, e.g. nfs / smb share dir)
$strings_to_leak = [''], # Optional: strings to leak (could contain instructions or a message)
) {
if $account {
$username = $account['username']
::accounts::user { $username:
shell => '/bin/bash',
password => pw_hash($account['password'], 'SHA-512', 'mysalt'),
managehome => true,
home_mode => '0755',
}
$storage_directory = "/home/$username"
} elsif $storage_dir {
$storage_directory = $storage_dir
} else {
err('install: either account or storage_dir is required')
fail
}
$compile_directory = "$storage_directory/tmp"
$challenge_directory = "$storage_directory/$challenge_name"
$modules_source = "puppet:///modules/$source_module_name"
# Create challenge directory
::secgen_functions::create_directory { "create_$challenge_directory":
path => $challenge_directory,
notify => File["create_$compile_directory"],
}
# Move contents of the module's files directory into compile directory
file { "create_$compile_directory":
path => $compile_directory,
ensure => directory,
recurse => true,
source => $modules_source,
}
# Build the binary with gcc
exec { "gcc_$challenge_name-$compile_directory":
cwd => $compile_directory,
command => "/usr/bin/make",
require => File["create_$compile_directory"]
}
# Move the compiled binary into the challenge directory
file { "$challenge_directory/$challenge_name":
ensure => present,
owner => 'root',
group => 'root',
mode => '4755',
source => "$compile_directory/$challenge_name",
require => Exec["gcc_$challenge_name-$compile_directory"],
}
# Drop the flag file on the box and set permissions
::secgen_functions::leak_files { "$username-file-leak":
storage_directory => "$challenge_directory",
leaked_filenames => [$flag_name],
strings_to_leak => [$flag],
owner => 'root',
mode => '0400',
leaked_from => "accounts_$username",
require => Exec["gcc_$challenge_name-$compile_directory"],
notify => Exec["remove_$compile_directory"],
}
# Remove compile directory
exec { "remove_$compile_directory":
command => "/bin/rm -rf $compile_directory",
require => [File["$challenge_directory/$challenge_name"]]
}
}

View File

@@ -0,0 +1,19 @@
#!/usr/bin/ruby
require_relative '../../../../../../../lib/objects/local_string_generator.rb'
class RubyExampleScriptGenerator < StringGenerator
def initialize
super
self.module_name = 'Ruby Example Script Generator'
end
def generate
self.outputs << "#!/usr/local/bin/suid /usr/bin/ruby --
puts File.read('flag')"
end
end
RubyExampleScriptGenerator.new.run

View File

@@ -0,0 +1,18 @@
<?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>Ruby Example Challenge Generator</name>
<author>Thomas Shaw</author>
<module_license>MIT</module_license>
<description>TODO</description>
<type>ruby_script_challenge</type>
<platform>linux</platform>
<platform>windows</platform>
<output_type>script</output_type>
</generator>

View File

@@ -0,0 +1 @@
require binary_script_container::install

View File

@@ -0,0 +1,4 @@
#!/usr/local/bin/suid /bin/bash -o privileged --
set -eu
echo uid=$(id -run) euid=$(id -un)
echo gid=$(id -rgn) egid=$(id -gn)

View File

@@ -0,0 +1,206 @@
/* Generic setuid/setgid wrapper for scripts.
*
* Copyright (c) 2016 Likai Liu <liulk@likai.org>
*
* Usage of the works is permitted provided that this instrument is
* retained with the works, so that any entity that uses the works is
* notified of this instrument.
* DISCLAIMER: THE WORKS ARE WITHOUT WARRANTY.
*/
/*
* Usage: use the following shebang line in the script.
* #!/usr/local/bin/suid /path/to/interpreter [options] --
*
* Mac OS X ignores the setuid of the first interpreter, so we reexec
* ourselves to get it back. Linux will pass /path/to/interpreter and
* all the options to us as one argument, but we will split them by
* whitespaces only (no quotes).
*
* Interpreter options must be given before the "--" marker which is
* mandatory. The implicit last argument is the script itself, which
* must also have the executable bit set as well as setuid or setgid.
*
* For shell scripts, it is necessary to pass -p or -o privileged to
* /bin/bash in the shebang line, or it will reset the effective uid
* and gid to the real ones. Other options are strongly recommended
* to be set explicitly in the script, e.g. "set -eu".
*
* A setuid binary has getuid() set to the invoking user, and
* geteuid() set to the owner of the binary. This wrapper will keep
* the same getuid() and getgid() but further modify geteuid() and
* geteguid() to the owner of the script, according to the setuid and
* setgid bits of the script itself. In order for this to work, the
* wrapper binary itself must be setuid root or the script owner.
*
* Here are the safety measures we take:
*
* - All LD_* and DYLD_* environment variables are cleared.
* - The script name is replaced with /dev/fd/NN.
*
* The script is responsible for ensuring sane IFS and PATH. Note
* that on bash, IFS changes the way variables are expanded when it
* appears on a command line, but hard-coded text remains unaffected.
*
* Further reading:
* http://www.dwheeler.com/secure-programs/Secure-Programs-HOWTO/avoid-setuid.html
* http://profesores.elo.utfsm.cl/~agv/elo330/programs/shell/NotUseSetuidScripts.html
* http://burrows.svbtle.com/bash-privileged-mode-quirk
*/
#include <ctype.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sysexits.h>
#include <unistd.h>
static const char usage[] =
"Usage: use the following shebang line in the script.\n"
" #!/path/to/suid /path/to/interpreter [options] --\n";
extern char **environ;
static void sanitize_environ(char **environ) {
int in, out;
for (in = 0, out = 0; environ[in] != NULL; ++in) {
if (strncmp(environ[in], "LD_", 3) == 0 ||
strncmp(environ[in], "DYLD_", 5) == 0)
continue; /* skip */
environ[out++] = environ[in]; /* keep */
}
environ[out] = NULL; /* terminate list */
}
/* always returns the true next powers of 2; e.g. next_pow2(8) returns 16 */
static int next_pow2(int n) {
int i = 0;
while (n)
++i, n >>= 1;
return 1 << i;
}
typedef struct arg_s {
int c /* count */, n /* capacity */;
char **v /* malloc */;
} arg_t;
static char *nextspace(const char *s) {
while (*s && !isspace(*s))
++s;
return (char *) s;
}
static char *skipspace(const char *s) {
while (isspace(*s))
++s;
return (char *) s;
}
static arg_t split_argv(int argc, char **old_argv) {
/* argc does not count the trailing NULL pointer in argv */
size_t argn = next_pow2(argc + 1);
char **argv = (char **) malloc(sizeof(char *) * argn);
int in = 0, out = 0;
argv[out++] = strdup(old_argv[in++]);
if (in >= argc)
goto done;
/* split old_argv[1] */
char *stop, *start;
for (start = old_argv[in++]; *start; start = stop) {
start = skipspace(start), stop = nextspace(start);
if (start == stop) break; /* trailing space */
argv[out++] = strndup(start, stop - start);
if (argn <= out)
argn <<= 1, argv = (char **) realloc(argv, sizeof(char *) * argn);
/* if realloc() fails, just segmentation fault on NULL access */
}
/* copy the remaining arguments */
while (in < argc) {
argv[out++] = strdup(old_argv[in++]);
if (argn <= out)
argn <<= 1, argv = (char **) realloc(argv, sizeof(char *) * argn);
/* if realloc() fails, just segmentation fault on NULL access */
}
done:
argv[out] = NULL;
struct arg_s r = { out, argn, argv };
return r;
}
static int find_dashdash(int argc, char **argv, int i) {
while (i < argc) {
if (strcmp("--", argv[i]) == 0)
return i;
++i;
}
return -1;
}
int main(int argc, char **argv) {
sanitize_environ(environ);
const char *self = argv[0];
struct stat statself;
if (stat(self, &statself) < 0)
return perror(self), EX_IOERR;
if (statself.st_mode & S_ISUID) {
if (geteuid() != statself.st_uid) { /* OS ignored our setuid bit, */
execv(argv[0], argv); /* rerun self to get setuid back. */
return perror(argv[0]), EX_OSERR;
}
}
arg_t arg = split_argv(argc, argv);
int dashdash = find_dashdash(arg.c, arg.v, 2);
if (dashdash < 0 || dashdash + 1 >= arg.c)
return fputs(usage, stderr), EX_USAGE;
const char *interp = arg.v[1], *script = arg.v[dashdash + 1];
/* access() checks permission using real uid and gid (set to the
* invoking user) as opposed to the effective uid and gid (set to
* the binary).
*/
if (access(interp, X_OK) < 0)
return perror(interp), EX_NOPERM;
if (access(script, X_OK | R_OK) < 0)
return perror(script), EX_NOPERM;
int fd = open(script, O_RDONLY);
if (fd < 0)
return perror(script), EX_NOPERM;
char buf[16];
snprintf(buf, sizeof(buf), "/dev/fd/%d", fd);
struct stat statbuf;
if (fstat(fd, &statbuf) < 0)
return perror(script), EX_IOERR;
#define SETEXID(xid, S_ISXID) \
if (sete##xid(statbuf.st_mode & S_ISXID ? \
statbuf.st_##xid : get##xid()) < 0) \
return perror("sete" #xid), EX_NOPERM;
/* set effective gid first, or we might not be able to do that after
* setting effective uid away from root.
*/
SETEXID(gid, S_ISGID);
SETEXID(uid, S_ISUID);
#undef SETEXID
arg.v[dashdash + 1] = buf; /* override script to fd */
execv(arg.v[1], arg.v + 1); /* normally should not return. */
return perror(arg.v[1]), EX_OSERR;
}

View File

@@ -0,0 +1,56 @@
class binary_script_container::install {
# Create temp install directory
file { '/root/tmp':
ensure => directory,
}
# Move wrapper.c onto box
file { "/root/tmp/suid.c":
ensure => file,
source => 'puppet:///modules/binary_script_container/wrapper.c',
}
# Make and install
exec { "wrapper make install":
command => 'make suid; install -m a+rx,u+ws -s ./suid /usr/local/bin/suid',
cwd => '/root/tmp',
path => '/bin:/sbin:/usr/bin:/usr/sbin',
}
# Create group for test TODO: remove me
group { 'test':
ensure => present
}
file { '/home/tmp':
ensure => directory,
}
# Move test file onto box TODO: remove me
file { "/home/tmp/test.sh":
ensure => file,
source => 'puppet:///modules/binary_script_container/test.sh',
group => 'test',
mode => '2775',
require => [Group['test'],File['/home/tmp']],
}
# Test: add a flag file with a group TODO: remove me
::secgen_functions::leak_files { "flag-file-leak":
storage_directory => "/home/tmp/",
leaked_filenames => ['flag'],
strings_to_leak => ['flag{wayy!!!}'],
owner => 'root',
group => 'test',
mode => '0440',
leaked_from => "binary_script_container_flag",
require => [Group['test'], File["/home/tmp/test.sh"]],
}
# Remove temp install directory
}

View File

@@ -0,0 +1,19 @@
<?xml version="1.0"?>
<utility xmlns="http://www.github/cliffe/SecGen/utility"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.github/cliffe/SecGen/utility">
<name>Binary script container</name>
<author>Likai Liu</author>
<author>Thomas Shaw</author>
<module_license>MIT</module_license>
<description>Binary container module for script based challenges.</description>
<type>system</type>
<type>script_challenge_container</type>
<platform>linux</platform>
<reference>https://lifecs.likai.org/p/suid.html</reference>
</utility>

View File

@@ -0,0 +1,43 @@
class ruby_challenge_example::install {
$secgen_params = secgen_functions::get_parameters($::base64_inputs_file)
$group = $secgen_params['group']
$script_data = $secgen_params['script_data']
if $secgen_params['account'][0] and $secgen_params['account'][0] != '' {
$account = parsejson($secgen_params['account'][0])
} else {
$account = undef
}
if $secgen_params['storage_directory'] and $secgen_params['storage_directory'][0] {
$storage_dir = $secgen_params['storage_directory'][0]
} else {
$storage_dir = undef
}
if $group {
::secgen_functions::install_setgid_script { 'ruby_challenge_example':
source_module_name => $module_name,
challenge_name => $secgen_params['challenge_name'][0],
script_name => 'test.rb',
script_data => $script_data[0],
group => $group[0],
account => $account,
flag => $secgen_params['flag'][0],
flag_name => 'flag',
storage_dir => $storage_dir,
strings_to_leak => $secgen_params['strings_to_leak'],
}
# } else {
# ::secgen_functions::install_setuid_root_binary { 'ruby_challenge_example':
# source_module_name => $module_name,
# challenge_name => $secgen_params['challenge_name'][0],
# account => $account,
# flag => $secgen_params['flag'][0],
# flag_name => 'flag',
# storage_dir => $storage_dir,
# strings_to_leak => $secgen_params['strings_to_leak'],
# }
}
}

View File

@@ -0,0 +1 @@
include ruby_challenge_example::install

View File

@@ -0,0 +1,64 @@
<?xml version="1.0"?>
<vulnerability xmlns="http://www.github/cliffe/SecGen/vulnerability"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.github/cliffe/SecGen/vulnerability">
<name>Ruby Challenge Example</name>
<author>Thomas Shaw</author>
<module_license>MIT</module_license>
<description>Ruby challenge example</description>
<type>ctf_challenge</type>
<type>script_challenge</type>
<privilege>none</privilege>
<access>local</access>
<platform>linux</platform>
<!-- binary dropped in account's home directory by default. -->
<read_fact>challenge_name</read_fact>
<read_fact>script_data</read_fact>
<read_fact>group</read_fact>
<read_fact>account</read_fact>
<read_fact>flag</read_fact>
<!-- storage_directory: Blank by default. If supplied, store the files here. e.g. NFS or SMB storage location -->
<read_fact>storage_directory</read_fact>
<default_input into="challenge_name">
<value>ruby_script_example</value>
</default_input>
<default_input into="script_data">
<generator module_path=".*ruby_example"/>
</default_input>
<default_input into="group">
<value>test1</value>
</default_input>
<default_input into="account">
<generator type="account">
<input into="username">
<value>challenges</value>
</input>
<input into="password">
<value>password</value>
</input>
</generator>
</default_input>
<default_input into="flag">
<generator type="flag_generator"/>
</default_input>
<default_input into="challenge_name">
<value>ruby_script_example</value>
</default_input>
<requires>
<module_path>utilities/unix/system/accounts</module_path>
</requires>
<requires>
<module_path>utilities/unix/system/binary_script_container</module_path>
</requires>
<requires>
<module_path>utilities/unix/languages/ruby</module_path>
</requires>
</vulnerability>

View File

@@ -0,0 +1,29 @@
import java.io.PrintStream;
import java.util.Scanner;
public class Crackme
{
public Crackme()
{
}
public static void main(String args[])
{
System.out.println("Please enter the password:");
Scanner scanner = new Scanner(System.in);
String s = scanner.next();
if(s.equals("<%= @password %>"))
{
System.out.println("Correct");
System.out.print("Your key is: ");
<% @flag.split('').each do |char|-%>
System.out.print("<%= char %>");
<% end %>
System.out.print("\n");
}
else
{
System.out.println("Wrong");
}
}
}

View File

@@ -1 +0,0 @@
require ruby_script_container::init

View File

@@ -1,42 +0,0 @@
define ruby_script_container::account($username, $group, $password, $strings_to_leak, $leaked_filenames) {
group { $group:
ensure => present,
}
::accounts::user { $username:
shell => '/bin/bash',
password => pw_hash($password, 'SHA-512', 'mysalt'),
managehome => true,
home_mode => '0755',
groups => [$group],
require => Group[$group],
}
# strings_to_leak[0]: flag in /home/<username>/flag.txt
::secgen_functions::leak_files { "$username-file-leak":
storage_directory => "/home/$username/",
leaked_filenames => $leaked_filenames,
strings_to_leak => [$strings_to_leak[0]],
owner => $username,
group => $group,
mode => '2440',
leaked_from => "accounts_$username",
require => Group[$group],
}
file { "/home/$username/test.rb":
owner => $username,
group => $group,
mode => '2777',
ensure => file,
content => template('ruby_script_container/template.rb.erb'),
require => Group[$group],
}
# exec { "$username-compileandsetup1":
# cwd => "/home/$username/",
# command => "sudo chown $username:shadow prompt && sudo chmod 2755 prompt",
# path => [ '/bin/', '/sbin/' , '/usr/bin/', '/usr/sbin/' ],
# }
}

View File

@@ -1,25 +0,0 @@
class ruby_script_container::init {
$secgen_parameters = secgen_functions::get_parameters($::base64_inputs_file)
$group = $secgen_parameters['group'][0]
::accounts::user { 'temp':
shell => '/bin/bash',
password => pw_hash('temp', 'SHA-512', 'mysalt'),
managehome => true,
home_mode => '0755',
groups => [$group],
}
$accounts = $secgen_parameters['accounts']
$accounts.each |$raw_account| {
$account = parsejson($raw_account)
$username = $account['username']
ruby_script_container::account { "script_container_$username":
username => $username,
password => $account['password'],
group => $group,
strings_to_leak => $account['strings_to_leak'],
leaked_filenames => $account['leaked_filenames']
}
}
}

View File

@@ -1,49 +0,0 @@
<?xml version="1.0"?>
<vulnerability xmlns="http://www.github/cliffe/SecGen/vulnerability"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.github/cliffe/SecGen/vulnerability">
<name>Binary script container</name>
<author>Thomas Shaw</author>
<module_license>MIT</module_license>
<description>Binary container module for script based challenges.</description>
<type>system</type>
<type>script_challenge_container</type>
<privilege>none</privilege>
<access>local</access>
<platform>linux</platform>
<read_fact>accounts</read_fact>
<default_input into="accounts">
<generator type="account">
<input into="username">
<value>test</value>
</input>
<input into="password">
</input>
<input into="leaked_filenames">
<value>flag.txt</value>
</input>
<input into="strings_to_leak">
<generator type="flag_generator"/> <!-- flag in /home/<username>/flag.txt -->
</input>
</generator>
</default_input>
<requires>
<module_path>utilities/unix/system/accounts</module_path>
</requires>
<requires>
<module_path>utilities/.*ruby.*</module_path>
</requires>
<!-- Need a way on to the box. -->
<!--<requires>-->
<!--<privilege>user_rwx</privilege>-->
<!--</requires>-->
</vulnerability>

View File

@@ -1,3 +0,0 @@
puts "hallo worlden!"
File.read('/home/test/flag.txt')

View File

@@ -0,0 +1,18 @@
<?xml version="1.0"?>
<scenario xmlns="http://www.github/cliffe/SecGen/scenario"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.github/cliffe/SecGen/scenario">
<!-- TODO: Should container be a utility? -->
<system>
<system_name>challenge_server</system_name>
<base platform="linux" type="server"/>
<utility module_path=".*binary_script_container"/>
<network type="private_network" range="dhcp" />
</system>
</scenario>

View File

@@ -10,25 +10,25 @@
<base platform="linux" type="server"/>
<!-- 1) Default uses an account and drops the binary in the users home directory -->
<vulnerability type="pwnable_binary">
<input into="group">
<value>task1</value>
</input>
</vulnerability>
<!-- 2) Using a custom storage directory -->
<!--<vulnerability type="pwnable_binary">-->
<!--<input into="group">-->
<!--<value>task2</value>-->
<!--</input>-->
<!--<input into="account">-->
<!--<value/>-->
<!--</input>-->
<!--<input into="storage_directory">-->
<!--<value>/test/hidden/challenges</value>-->
<!--<value>task1</value>-->
<!--</input>-->
<!--</vulnerability>-->
<!-- 2) Using a custom storage directory -->
<vulnerability type="pwnable_binary">
<input into="group">
<value>task2</value>
</input>
<input into="account">
<value/>
</input>
<input into="storage_directory">
<value>/test/hidden/challenges</value>
</input>
</vulnerability>
<network type="private_network" range="dhcp"/>
</system>

View File

@@ -0,0 +1,35 @@
<?xml version="1.0"?>
<scenario xmlns="http://www.github/cliffe/SecGen/scenario"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.github/cliffe/SecGen/scenario">
<!-- an example system with a setgid binary. -->
<system>
<system_name>group_challenges</system_name>
<base platform="linux" type="server"/>
<!-- 1) Default uses an account and drops the binary in the users home directory -->
<vulnerability type="script_challenge">
<input into="group">
<value>task1</value>
</input>
</vulnerability>
<!-- 2) Using a custom storage directory -->
<!--<vulnerability type="pwnable_binary">-->
<!--<input into="group">-->
<!--<value>task2</value>-->
<!--</input>-->
<!--<input into="account">-->
<!--<value/>-->
<!--</input>-->
<!--<input into="storage_directory">-->
<!--<value>/test/hidden/challenges</value>-->
<!--</input>-->
<!--</vulnerability>-->
<network type="private_network" range="dhcp"/>
</system>
</scenario>