Project 6: Capture the flag

  • This project is due at 11:59pm on Friday, Apr 16, 2021.

Description and Deliverables

In this project, you will gain hands on experience leveraging exploits to make a program do unexpected things that were not intended by the programmer. The assignment is structured as a Capture The Flag (CTF), which is a common format in cybersecurity competitions. You will be given access to a vulnerable program, and your task is to locate six flags hidden in the program and its surrounding files. You are required to find five flags; the sixth flag is considerably more challenging, and is worth bonus points.

To receive full credit for this project, you will turn in a single file:

  • A file named project6/flags.txt that contains the flags that you will capture from the target program.

The exact format of this deliverable is described in detail below.

While this assignment will be auto-graded, the auto-grader will not give you detailed feedback on which parts of your submission are incorrect.

Getting Started with Expense Management

Happy Funtime Plastic Co, Inc. uses a piece of software developed in-house for tracking expenses made by employees. This software is run on the command line, and allows users to add expenses (with a description and an amount), view their saved expenses, and delete expenses that were entered incorrectly. The program uses a SQLite3 database to store expenses, and all of the files for this application are stored in /home/accounting.

As security auditors, you have been given access to the expense program in a secure environment for testing. To access the expense program, SSH into p6.neucrypt.org. For example:

$ ssh <github user>@p6.neucrypt.org

As with the previous project, your login requires your Github ssh private key to be installed on the computer.

$ ssh abhvious@p6.neucrypt.org
abhvious@9f0d9dcbf0cc:/$ ls -al /home/accounting/
total 84
drwxr-xr-x 1 accounting accounting  4096 Nov 23 08:37 .
drwxr-xr-x 1 root       root        4096 Nov 23 08:37 ..
-rw-r--r-- 1 accounting accounting   220 Feb 25  2020 .bash_logout
-rw-r--r-- 1 accounting accounting  3771 Feb 25  2020 .bashrc
-rw-r--r-- 1 accounting accounting   807 Feb 25  2020 .profile
-r-sr-xr-x 1 accounting accounting 28344 Nov 22 20:36 expense
-rw------- 1 accounting accounting 20480 Nov 22 20:36 expenses.sqlite
dr-x------ 1 accounting accounting  4096 Nov 23 08:37 important_docs
-r-------- 1 accounting accounting   340 Nov 22 20:36 todo_list.txt

The command line syntax of the program is as follows:

abhvious@9f0d9dcbf0cc:/$ cd /home/accounting/
abhvious@9f0d9dcbf0cc:/home/accounting$ ./expense
This program allows users to track their expenses. It has commands for adding, viewing, and deleting
expenses. Administrators may add, view, or delete expenses for any user. The program also keeps a log
of all changes to facilitate offline auditing.

Usage: /home/accounting/expense [--admin] <--command> [arguments]
  Commands:
    --help                           Displays this message
    --add <Description> <Amount>     Adds an expense for the current user
    --list                           Lists all expenses for the current user
    --del <ID>                       Deletes the current user's expense with the given ID

Additional commands are available to administrators. See the developer docs for more information.

The basic commands available to users are --add, --list, and --del. The first step of the assignment is for you to learn how to use this program. Try to list expenses for the current user.

abhvious@9f0d9dcbf0cc:/home/accounting$ ./expense --listusers
Error: you must have administrator access to run this command

abhvious@9f0d9dcbf0cc:/home/accounting$ ls -al important_docs/
ls: cannot open directory 'important_docs/': Permission denied

Notice that your user does not have permission to list user expenses, and when you list your own expenses, there are none because you are a security auditor, and not an employee. You also cannot directly access the important_docs directory. However, notice that the expense program has the setuid bit set, and thus, when you execute it, it runs with the permissions of the accounting user.

In addition to the commands noted above, there are other commands and options for the expense program available to system administrators; this additional functionality is accessed by passing the --admin option to the program. However, access to admin functionality is password protected.

The expense program is written in C. The (slightly redacted) source code for the program is available below.

Identifying Flags

Your goal is to locate the six flags hidden in the expense program and it’s surrounding files. You are required to find at least five flags; the sixth flag is a bonus. All flags are located somewhere within the the /home/accounting directory; there is no need to search other locations in the file system. All flags follow the following format:

SECRET_FLAG_<10 characters of random lowercase and digits>

For example, a secret flag might look like this:

SECRET_FLAG_aef8dab0cf

The secret flags can be anywhere within the /home/accounting directory: inside the expense program, inside databases, inside files, etc. It is 100% feasible to find all six flags; none have been hidden in such a way that it requires wizard-level exploitation skills or raw amounts of brute force to locate.

Rules

To make this assignment fun for everyone, we ask that students obey some basic rules of decorum.

  1. As you progress through this assignment, you will achieve various levels of elevated privilege. This will give you the ability to take destructive actions. We ask that you exercise restraint and not destroy any data or files. Please leave things as you found them ;)

  2. Do not denial of service the server. Examples of denial of service attacks include but are not limited to: intentionally running large numbers of processes to control CPU and memory resources; intentionally filling the disk to capacity; and flooding the server with network traffic.

  3. Do not attack your classmates. This includes attempting to crack their passwords, read or alter files in their home directory, or kill their processes.

Any student that fails to follow these rules, i.e. intentionally attempts to make mischief, will receive a zero on the assignment. That said, we understand that mistakes happen! If you accidentally make changes to project files that you believe may hinder others’ abilities to complete the assignment, let us know immediately and we will repair the situation.

If you so desire, attacking the operating system is considered within bounds. If you achieve root, getting all the flags should be trivial. If you do choose to attack the OS, all we ask is that you do not take destructive actions that (1) leave the server in an unusable state; (2) harm your classmates or their files; or (3) generally prevent other students from completing the assignment.

Exploitation Tips

Note, your user account does not have permissions to read all of the files in the /home/accounting directory. However, the program has several vulnerabilities which will allow you to learn these secret flags.

Locating all six flags will require you to examine the C source code of the expense program, carefully investigate the program’s command line behavior, and investigate the file system around the expense program. Several of the exploits necessary to find the flags are related to topics we discussed in class. You can recover the first 5 flags using inspection tools and sql injection attacks, but these attacks will require you to think creatively. To recover the 6th flag for extra credit, in addition to creativity, you may need to carefully construct a special string. Please consult the L18,L19,L20,L21 lecture videos for hints; the tools and techniques that you need to recover all 6 flags are mentioned in those videos.

The source code for the expense program is relatively simple, and well documented. However, online C language tutorials may be useful for students who have never seen code in the language before. If you have questions about the C program and its syntax or meaning of any line of code, and you cannot find answers online, feel free to post questions on the Piazza forum.

The expense program makes use of a SQLite3 database. Many online resources exist that describe SQL query syntax in general, and SQLite3-specific queries in particular. Please also consider the L20 lecture notes for an overview of SQL.

Unfortunately, the source code we provide for the expense program has been redacted. Thus, you may need to employ binary analysis tools that allow you to inspect and disassemble the contents of compiled programs. Tools like hexdump, strings, gdb and objdump may come in handy; the man pages for these programs offer help about their capabilities and syntax. A debugger like gdb may be useful for more advanced exploits. As a starting point, take a look at the authenticate and open_db functions in the code.

Common programming tools like python, gcc, g++, nasm, and make are available on the server. If there are additional tools available via apt that you would like installed, send an email or private Piazza message to the staff.

Submission details

To receive full credit on this assignment, you must turn in a single (ASCII formatted with Unix-style line breaks) text file named flags.txt that contains the flags that you have recovered.

You should create a file named flags.txt that contains your captured flags. Each flag should be on its own line. For example, your flags.txt file might look like the following:

SECRET_FLAG_aaaflag1aa
SECRET_FLAG_bbb0000bbb
SECRET_FLAG_ccc123cccc
SECRET_FLAG_ddd1234ddd
SECRET_FLAG_eee12345ee
SECRET_FLAG_fff123456f

Your file should contain at least five flags.

Submit your project6/flags.txt via a signed Github commit and submit this to gradescope.

Submitting

Please follow these directions exactly:

  1. Create a directory project6 under your git repo.
  2. Add the file flags.txt to this directory.
  3. Commit your new file in a signed commit. Feel free to resubmit the project as many times as you like. It is ok to have extra files in the repository as well, just make sure you have this one.

Note: We require you to follow these directions exactly. If your repository does not include the project6/flags.txt in a signed commit as described above, you will lose points, and possibly receive no credit.

Grading

This project is worth 100 points, but you have a possibility of earning up to 120 points:

  • 20 points each per flag (five flags)
  • 20 bonus points for the sixth flag

Points can be lost for turning in files in incorrect formats (e.g. not UNIX-line break ASCII), failing to follow specified formatting and naming conventions, or encrypting/signing your file using the wrong keys.

Tips

CTF projects are puzzles: they’re best when students are allowed to find solutions by themselves. Thus, we’re reluctant to post tips openly that would give away key aspects of the game. That said, if you feel stuck, or like you don’t know where to begin, that is okay! Please talk to the professor or the TAs privately, either on Piazza, email, or office hours, and we will be happy to help you get started. We want everyone to enjoy the CTF, so if you feel like you’re not making progress and the project is giving you grief, come talk to us!

Please do not share explicit commands or inputs that can be used to solve this project with other students.

Expense program

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
#include <pwd.h>
#include <sys/types.h>
#include "sqlite3.h"

/*************************** USAGE NOTES *****************************

./expense --help

./expense --add "DESCRIPTION" AMOUNT
  Added item ID for USER

./expense --list
  ID     DATE/TIME     USER       DESCRIPTION     AMOUNT

./expense --del 4
  Deleted item ID for USER

./expense --admin [--db db_filename] --add USER "DESCRIPTION" AMOUNT
./expense --admin [--db db_filename] --list USER
./expense --admin [--db db_filename] --del USER "DESCRIPTION" AMOUNT
./expense --admin [--db db_filename] --listusers

***********************************************************************/

#define ADMIN_PW       "" // ADMIN PASSWORD REDACTED TO PRESERVE CONFIDENTIALITY!!!
#define DB_FILENAME    "expenses.sqlite"
#define LIST_USER_SQL  "SELECT DISTINCT username FROM expenses"
#define ADD_EXP_SQL    "INSERT INTO expenses (time, username, description, amount) VALUES (%li, \"%s\", \"%s\", %s)"
#define DEL_EXP_SQL    "DELETE FROM expenses WHERE username=\"%s\" AND id=%s"
#define LIST_EXP_SQL   "SELECT * FROM expenses WHERE username=\"%s\""
#define TEST_EXP_SQL   "SELECT * FROM expenses"
#define AUDIT_LOG_SQL  "INSERT INTO audit_log (time, admin, username, command, arguments) VALUES (%li, %i, \"%s\", \"%s\", \"%s\")"
#define MAX_QUERY_LEN  2048
#define MAX_TIME_LEN   20  // max character length of an unsigned long int

// Check if the help message is being requested, or the user hasn't typed any command line args
//   argc : The total number of command line arguments
//   argv : Array of strings, each containing a command line argument
// Exits the program if the help message is displayed
void help(int argc, char ** argv) {
    int i;
    int found = 0;

    if (argc == 1) found = 1;

    for (i = 1; i < argc; ++i) if (0 == strcmp(argv[i], "--help")) {
        found = 1;
        break;
    }

    if (found) {
        puts(
            "This program allows users to track their expenses. It has commands for adding, viewing, and deleting\n" \
            "expenses. Administrators may add, view, or delete expenses for any user. The program also keeps a log\n" \
            "of all changes to facilitate offline auditing.\n"
        );
        printf("Usage: %s [--admin] <--command> [arguments]\n", argv[0]);
        puts(
            "  Commands:\n" \
            "    --help                           Displays this message\n" \
            "    --add <Description> <Amount>     Adds an expense for the current user\n" \
            "    --list                           Lists all expenses for the current user\n" \
            "    --del <ID>                       Deletes the current user's expense with the given ID\n" \
            "\n" \
            "Additional commands are available to administrators. See the developer docs for more information."
        );

        exit(0);
    }
}

// Asks the user to input the admin password and checks the result
// Returns 1 on success, 0 on failure
int authenticate() {
    char buf[256];
    printf("Enter the administrator password: ");
    gets(buf);

    // Is the password given by the user correct?
    if (0 == strcmp(buf, ADMIN_PW)) return 1;
    return 0;
}

// Prints an error message and exits the program 
//   msg : error message to print
// Exits the program
void error(char * msg) {
    printf("Error: %s\n", msg);
    exit(-1);
}

// Prints an error message from the sqlite3 database and exits the program 
//    db : the sqlite3 database
//   msg : error message to print (malloced by sqlite3)
// Exits the program
void sql_error(sqlite3 * db, char * msg) {
    sqlite3_close(db);
    printf("Error from sqlite: %s\n", msg);
    sqlite3_free(msg);
    exit(-1);
}

// Attempts to open the sqlite3 database
//   filename : the path and filename of the sqlite3 database to open
// Returns a pointer to the open database
// Exits the program if the file cannot be found, or it is not a valid sqlite3 database
sqlite3 * open_db(char * filename) {
    sqlite3 * db;
    FILE * file;
    char buf[256], * err;

    // Make sure the specified file exists
    if (-1 == access(filename, F_OK))
        error("cannot open the specified database file");

    // Try to open the database
    int result = sqlite3_open(filename, &db);
    if (result != SQLITE_OK)
        error("cannot open the specified database file");

    // Run a test query
    result = sqlite3_exec(db, TEST_EXP_SQL, 0, 0, &err);
    if (result != SQLITE_OK) {
        sqlite3_close(db);

        // Is the database corrupt or non-existant?
        file = fopen(filename, "r");
        if (!file) error("cannot open the specified database file");

        // The database is corrupt; try printing out the contents to help the administrator
        printf("%s does not appear to be a valid sqlite3 database file.\n", filename);
        puts("To aid in debugging, here are the contents of the specified file:");
        while (!feof(file)) {
            fgets(buf, 255, file);
            fputs(buf, stdout);
        }
        // Don't forget to close the file
        fclose(file);
        exit(-1);
    }
    return db;
}

// Logs --add and --del commands in the audit_log table
//       db : the sqlite3 database
//        t : the current time
//    admin : 0 if the user is not authenticated as admin, 1 otherwise
// username : the username of the user who was targeted by the command (may not be the user who ran the program)
//      cmd : the command that was executed, either "add" or "del"
//     args : the arguments that were given to the command
void audit_log(sqlite3 * db, time_t t, int admin, char * username, char * cmd, char * args) {
    char buf[MAX_QUERY_LEN];
    char * err;
    int result;

    // Do we have enough space to build the query string?
    if (strlen(AUDIT_LOG_SQL) + MAX_TIME_LEN + 1 + strlen(username) + strlen(cmd) + strlen(args) >= MAX_QUERY_LEN - 1)
        error("sorry, the audit log entry is too long");

    // Build the query
    sprintf(buf, AUDIT_LOG_SQL, t, admin, username, cmd, args);
    
    // Add the entry to the log
    result = sqlite3_exec(db, buf, 0, 0, &err);
    if (result != SQLITE_OK) sql_error(db, err);
}

// Handles the --add command, adds an expense from the database and logs it
//       db : the sqlite3 database
//        i : the index of the current command line argument to parse
//     argc : the total number of command line arguments
//     argv : array of strings, each containing a command line argument
// username : the username of the user who ran the program
//    admin : 0 if the user is not authenticated as admin, 1 otherwise
void add_expense(sqlite3 * db, int i, int argc, char ** argv, char * username, int admin) {
    char buf[MAX_QUERY_LEN];
    char * desc, * amount, * err;
    int result;
    time_t t = time(0);

    // If the user is not admin, get the description and amount of the new expense
    if (!admin) {
        if (i + 1 >= argc) error("insufficient arguments for the --add command");
        desc = argv[i];
        amount = argv[i+1];
    }
    // If the user is admin, get the username, description, and amount of the new expense
    else {
        if (i + 2 >= argc) error("insufficient arguments for the --add command");
        username = argv[i];
        desc = argv[i+1];
        amount = argv[i+2];
    }

    // Do we have enough space to build the query string?
    if (strlen(ADD_EXP_SQL) + MAX_TIME_LEN + strlen(username) + strlen(desc) + strlen(amount) >= MAX_QUERY_LEN - 1)
        error("sorry, the length of your expense is too long");

    // Build the query string
    sprintf(buf, ADD_EXP_SQL, t, username, desc, amount);
    
    // Run the query
    result = sqlite3_exec(db, buf, 0, 0, &err);
    if (result != SQLITE_OK) sql_error(db, err);

    // Log the add
    sprintf(buf, "%s %s", desc, amount);
    audit_log(db, t, admin, username, "add", buf);
}

// Handles the --list command, lists all the expenses for a given user
//       db : the sqlite3 database
//        i : the index of the current command line argument to parse
//     argc : the total number of command line arguments
//     argv : array of strings, each containing a command line argument
// username : the username of the user who ran the program
//    admin : 0 if the user is not authenticated as admin, 1 otherwise
void list_expenses(sqlite3 * db, int i, int argc, char ** argv, char * username, int admin) {
    int result;
    char buf[MAX_QUERY_LEN];
    sqlite3_stmt * stmt;
    time_t t;
    char * ts;

    // If the user is admin, get the user that should be listed
    if (admin) {
        if (i >= argc) error("insufficient arguments for the --list command");
        username = argv[i];
    }

    // Do we have enough space to construct the query string?
    if (strlen(LIST_EXP_SQL) + strlen(username) >= MAX_QUERY_LEN - 1)
        error("sorry, the length of your username is too long");

    // Build the query string
    sprintf(buf, LIST_EXP_SQL, username);

    // Run the query
    result = sqlite3_prepare_v2(db, buf, strlen(buf)+1, &stmt, NULL);
    if (result != SQLITE_OK) {
        sqlite3_close(db);
        error("unable to execute --list query");
    }

    // Iterate through the result rows and print them
    printf("%5s %24s %16s %40s %10s\n", "ID", "Date/Time", "User", "Description", "Amount");
    do {
        result = sqlite3_step(stmt);
        if (result == SQLITE_ROW) {
            // Convert the integer time to a human-readable string
            t = sqlite3_column_int(stmt, 1);
            ts = ctime(&t);
            ts[strlen(ts) - 2] = 0; // remove annoying \n

            printf("%5i %24s %16s %40s %10.2f\n",
                sqlite3_column_int(stmt, 0),
                ts,
                (char *) sqlite3_column_text(stmt, 2),
                (char *) sqlite3_column_text(stmt, 3),
                sqlite3_column_double(stmt, 4)
            );
        }
    } while (result == SQLITE_ROW);
}

// Handles the --del command, deletes an expense from the database and logs it
//       db : the sqlite3 database
//        i : the index of the current command line argument to parse
//     argc : the total number of command line arguments
//     argv : array of strings, each containing a command line argument
// username : the username of the user who ran the program
//    admin : 0 if the user is not authenticated as admin, 1 otherwise
void del_expense(sqlite3 * db, int i, int argc, char ** argv, char * username, int admin) {
    char buf[MAX_QUERY_LEN];
    char * id, * err;
    int result;
    time_t t = time(0);

    // If the user is not admin, get the id that should be deleted
    if (!admin) {
        if (i >= argc) error("insufficient arguments for the --del command");
        id = argv[i];
    }
    // If the user is admin, get the username and the id that should be deleted
    else {
        if (i + 1 >= argc) error("insufficient arguments for the --del command");
        username = argv[i];
        id = argv[i+1];
    }

    // Do we have enough space to construct the query string?
    if (strlen(DEL_EXP_SQL) + strlen(username) + strlen(id) >= MAX_QUERY_LEN - 1)
        error("sorry, the length of your expense (username + description + amount) is too long");

    // Build the query
    sprintf(buf, DEL_EXP_SQL, username, id);
    
    // Run the query
    result = sqlite3_exec(db, buf, 0, 0, &err);
    if (result != SQLITE_OK) sql_error(db, err);

    // Log the delete
    sprintf(buf, "%s", id);
    audit_log(db, t, admin, username, "del", buf);
}

// Handles the --listusers command, prints all users in the database who have expenses
//   db    : the sqlite3 database
//   admin : 0 if the user is not authenticated as admin, 1 otherwise
void list_users(sqlite3 * db, int admin) {
    int result;
    sqlite3_stmt * stmt;

    // Make sure the user is authenticated for this privileged command
    if (!admin) error("you must have administrator access to run this command");

    // Query the database
    result = sqlite3_prepare_v2(db, LIST_USER_SQL, strlen(LIST_USER_SQL)+1, &stmt, NULL);
    if (result != SQLITE_OK) {
        sqlite3_close(db);
        error("unable to execute --listusers query");
    }

    // Iterate through the resulting rows and print them
    do {
        result = sqlite3_step(stmt);
        if (result == SQLITE_ROW) puts((char *) sqlite3_column_text(stmt, 0));
    } while (result == SQLITE_ROW);
}

// Program Start Here
//   argc : The total number of command line arguments
//   argv : Array of strings, each containing a command line argument
int main(int argc, char** argv) {
    int admin = 0;
    int i = 1;
    char * db_filename = DB_FILENAME;
    struct passwd *p;
    char * username;
    sqlite3 * db;

    // Print the help message, if requested or too few args given
    help(argc, argv);

    // Get the username of the user who ran the program
    p = getpwuid(getuid());
    username = p->pw_name;

    // Upgrade to setuid privileges
    setuid(geteuid());

    // If the user is requesting admin rights, authenticate them first
    if (0 == strcmp(argv[i], "--admin")) {
        if (!authenticate()) error("incorrect password");
        i = 2;
        admin = 1;
    }

    // Is the user trying to load a non-default database file?
    if (i < argc && 0 == strcmp(argv[i], "--db")) {
        if (!admin) error("only administrators may use the --db command");
        i += 1;
        if (i == argc) error("insufficient arguments supplied for --db command");
        db_filename = argv[i];
        i += 1;
    }

    // Execute the users command, if one is given
    if (i < argc) {
        // But first, open the database
        db = open_db(db_filename);

        if (0 == strcmp(argv[i], "--add")) add_expense(db, i+1, argc, argv, username, admin);
        else if (0 == strcmp(argv[i], "--list")) list_expenses(db, i+1, argc, argv, username, admin);
        else if (0 == strcmp(argv[i], "--del")) del_expense(db, i+1, argc, argv, username, admin);
        else if (0 == strcmp(argv[i], "--listusers")) list_users(db, admin);
        else error("unknown command supplied");

        // Don't forget to close the database
        sqlite3_close(db);
    }
    else error("no command supplied");

    return 0;
}