Rebuilding a Faction: Part 2

Where we build a service, but first we have to build a library.. and before all that we have to solve authentication.

I mentioned in the last article that in rebuilding Faction I decided to basically scrap all the existing services. This article covers some of the design decisions that went into rethinking what Faction should do and how that translated into services.

‌Part of the goal in redesigning Faction is to do a better job of implementing a microservices design, at least to the extent that I understand how microservices should work. I may be totally wrong here, in which case I'll have material for a future series of articles called "Rebuilding a Faction for reals this time".

‌The appeal of microservices to me is the idea of having these small, independent services that really only handle one thing. Independence is important here as it allows each service to function on its own, with minimal dependencies on other services. Dependencies on other services equals complexity. As long as you have a solid design for the API, you're free to tinker with the underlying service and nothing that uses it will know. I also think that while microservices may complicate the architecture of an application compared to a monolithic design, in a lot of ways they're easier to figure out. If you want to figure out how some functionality works, you just have to look at the service that handles it (which should hopefully be easy to figure out).

‌What does Faction do

‌The goal of Faction has always been to provide a platform that developers can use to build their own implants. Essentially, Faction provides the server and you provide the client.

Side note, I'm thinking of calling Faction a platform going forward (instead of a framework) as I think that provides a better description of what the project is about. We'll give it a shot for this article.

‌As a C2 platform (that sounds good), Faction provides all of the server-side functionality you need to operate a C2. The goal is to provide a lot of great functionality you can leverage without imposing design decisions on the agent (old Faction didn't quite nail this). In rethinking Faction the first thing I had to do was think about what a C2 server must be able to do. I came up with the following:

  • Authentication

  • Authorization

  • Agent management (keeping track of what agents are connected)

  • Agent tasking (both sending and receiving results)

  • File management (uploaded files, generated artifacts, etc)

‌I originally had cryptography on this list, but after giving it some thought I think that crosses the line on imposing design decisions on the agent. We'll talk more about agent design in the next article though.

‌In terms of services, Hasura is kind of the API in front of our database and will handle both agent management and tasking. This leaves authentication, authorization, and file management to tackle and presents the first design decision:

‌The trials and tribulations of Authn and Authz

‌Because I'm leveraging Hasura for a substantial part of Faction, the auth story is largely driven by what options it supports. Interestingly enough, Hasura doesn't provide a means of authentication itself and instead expects to integrate into an existing auth flow. Hasura supports role-based access and ultimately it's looking for a header that tells it what role to associate the request with so it can determine permissions. We can provide that information via a JWT or webhook.

‌I've written the auth service for Faction three times since I started the redesign, first to focus on a webhook approach, then JWTs, then back to webhooks. Both have some very compelling features, but let's start with JWTs

‌JWTs are "JSON Web Tokens" and are designed as a way to securely store state inside an API token. The basic idea is that you define your claims (for example "Hasura-Role: Admin"), encrypt it, base64 it, and that's your API key. Services then use the JWT secret to decrypt the JWT and retrieve the contents. It's a really clever approach, especially for microservices cause now your services don't have to rely on another service for auth. Each service has what it needs to verify the token, so they can be self-sufficient. This becomes an even stronger story when you use RSA keys instead of a password for your JWT secret, which allows you to create token readers who have access to the public key, and token creators (a login service for example) that have access to the private key.

‌I really like the JWT approach, but the problem I couldn't adequately solve is token revocation. The best approach I found was the use of refresh tokens and revocation lists, which seems to be the standard way to address this issue. Basically, when a user authenticates to the login service, they receive a JWT and a refresh token. The JWT is valid for a short period (like 15 mins) and eventually as you use it with other services, you'll get a reply that your token has expired. Your application then uses that refresh token to request a new JWT from the login service. The login service checks its revocation list to see if the refresh token has been revoked, if it has you don't get a new JWT, if it hasn't you're issued a new JWT.

‌There are a couple of problems I have with this approach that make me feel that it's not the right fit for a C2 platform. The first is that even though it's a short window, you still have a period of time where you can't revoke an API key. The second is this adds an extra burden on client applications to handle refreshing JWTs. One of the goals of Faction is to provide as easy a developer experience as possible and this just doesn’t really fit with that goal.

‌Instead, I've decided to take a webhook approach. In this approach when a service receives a request it forwards the API key to the auth service and the auth service replies with information associated with that key (username, role, stuff like that). While this adds a dependency between services and the auth service, it provides the benefit of strict control over API keys (as soon as a key is revoked, it's no longer valid) and provides a better developer experience.

Code for the Auth service is available here: https://github.com/FactionC2/Auth

Bootstrapping Services

‌The next problem we have to solve is how do services authenticate to each other? Here are some options I considered:‌

  1. We could try and design something where internal services just trust each other, but that seems like a security issue waiting to happen.

  2. We could leverage a shared secret that functions as a service key, but that complicates our authentication process cause now we have to check if it's an API key or service key... also the idea of sending an unencrypted secret around makes me feel sick.

  3. We could manually create API keys for each service through the Faction CLI, something like faction new service --name foo. I like this approach because it's deliberate and arguably provides the best security, but it complicates the deployment/install process by adding a manual step on top of deploying the service.

  4. We can use JWTs!

‌Right now, services bootstrap themselves by sending a JWT to a specific endpoint on the Auth service. The auth service verifies the JWT and uses the information provided in it to create a new API key. The service being bootstrapped then uses that API key to authenticate to other Faction services.

‌This approach solves a lot of the issues above: there's no secret "service only" API authentication mechanism, no plain text secrets are being sent around, and there's no user interaction required. The Faction installation has a shared JWT secret key that if provided to the service (through the services k8s config), the service can bootstrap into Faction.

‌Building a Faction Service

‌If you chart out service dependencies in Faction, the first thing we need is a way to authenticate. Check. While I really, really want to get to work on porting Marauder over to new Faction, playing with Marauder means I need a need a way to build the Marauder executable, which means I need a place to store the executable. This means we need file storage first.‌

In an attempt to make developing new features and services as easy as possible, most of my development time on the File service has actually been spent writing a library to support easily building services. FactionPy is focused on providing an easy way to interact with other core services like Auth, Hasura, and eventually Files (once its built). It handles things like exchanging a JWT for an API key, executing queries against Hasura, and provides other helper functions for common tasks such as authorization.‌

Using the Faction Python Library

To interact with Faction, the first thing you do is create a client by calling FactionClient and passing in the name of your service.

client = FactionClient("file-service")

As part of the client creation process, FactionClient handles getting the JWT secret value, identifying the auth service, creating a JWT, and exchanging the JWT for an API key as described above.

Below is an example from the File service showing how to use the client to communicate with the GraphQL API (Hasura). This function takes a series of arguments, uses them to create a GraphQL request, and then uses the Faction client we created to execute the GraphQL query. Hasura provides access to GraphiQL, a GraphQL IDE, which makes writing new queries incredibly easy.

def create_file_entry(description: str, path: str, file_type: str, url: str,
source_file_path: str = None, user_id: str = None,
agent_id: str = None, metadata: str = None):
new_file_query = f"""
mutation create_file(
$agent_id: uuid = "{agent_id}"
$description: String = "{description}"
$file_path: String = "{path}"
$metadata: jsonb = "{metadata}"
$source_file_path: String = "{source_file_path}"
$type: String = "{file_type}"
$url: String = "{url}"
$user_id: uuid = ""{user_id})
{{
insert_files_one(
object: {{
agent_id: $agent_id
description: $description
metadata: $metadata
type: $type
url: $url
source_file_path: $source_file_path
user_id: $user_id
file_path: $file_path
}}
)
}}
"""
result = None
log(f"executing query: {new_file_query}")
try:
result = client.execute(gql(new_file_query))
except Exception as e:
log("error executing query: {e}", "error")
return result

FactionPy also provides some helper functions to make developing services and apps easier. The code below demonstrates two of these functions:

  1. The function is protected using the authorized_groups decorator. This function takes a list of groups and when a request comes in, it sends the API key over to the Auth service and confirms that the key is valid and that the user is in the specified groups. It makes adding authz to functions very easy (though it does only work with Flask now).

  2. The function also uses the log function for easy, standardized, and very pretty logging.

Note that other functions such as jsonify and secure_filename are provided by Flask

@authorized_groups(["standard_write"])
def post(self):
log("upload endpoint called", "debug")
if 'file' not in request.files:
log("no file detected in request", "warning")
return jsonify(
success="false",
message="No file detected in request"
), 401
file = request.files['file']
if file.filename == '':
log("no filename detected in request", "warning")
return jsonify(
success="false",
message="No filename detected in request"
), 401
if file:
filename = secure_filename(file.filename)
file.save(os.path.join(app.config['UPLOAD_FOLDER'], filename))
log(f"file uploaded: {filename}", "info")
return jsonify(
success="true",
message="File successfully uploaded"
)

Next time on "Rebuilding a Faction"

I still need to finish up this File service and once that's done I'll be able to start porting Marauder over which means in the next article we'll talk about what it looks like to build an agent for Faction. Very exciting!

Jared