Creating an Apfell - Part 6
Published:
Ok, now that we’re starting to get a bunch of stuff added to our ecosystem, we want to make sure that it’s not just open to anybody. Additionally, we’ll want people to actually log in so that we can track which operators do which commands, spawn which processes, and create which payloads. I’ve seen a lot of times where we’ll get seemingly random new callbacks and have to check with everybody to track down that somebody spawned a new, redundant access. Eventually this will even be an interesting way to lock down the capabilities of certain operators so that they have to get approval from more senior members of a team before being able to do potentially OPSEC unsafe techniques. How will we do this? Initially, we’ll create a registration page and login page that submits a web form. Then, we’ll go through and mark that certain pages need authentication to be accessed.
sanic-wtf
No, not that wtf, sanic-wtf. This is a Sanic implementation of WTForms. The wtf in this case refers to What-The-Forms, an open source project trying to make form handling easier for web development. There are a few components to get this integrated into our projects. We need to:
- Create a form in HTML
- Create a form object in Python
- Create a route to handle the form interaction
- Modify our database objects to handle users
Let’s start with the simplest part - creating the python object. Create a file called “loginform.py” and add it to our “/forms” folder.
from sanic_wtf import SanicForm
from wtforms import StringField, PasswordField, SubmitField
from wtforms.validators import DataRequired, EqualTo
class LoginForm(SanicForm):
username = StringField('Username', validators=[DataRequired()])
password = PasswordField('Password', validators=[DataRequired()])
submit = SubmitField('Sign In')
class RegistrationForm(SanicForm):
username = StringField('Username', validators=[DataRequired()])
password = PasswordField('Password', validators=[DataRequired()])
password2 = PasswordField('Repeat Password', validators=[DataRequired(), EqualTo('password')])
submit = SubmitField('Register')
Ok, what’s going on here? We created two forms - one for logging in and one for registering an account. These forms must take SanicForm as a parameter. We need to specifiy what kinds of fields these are, hence the StringField
and PasswordField
declarations. As part of these declarations, we specify what the label
is for that field. Finally, we get into the magic of WTForms. For each field, we can specify validators
. These can be pretty elaborate and really come into play in the next portion we’ll cover, but in our case, we’re mandating that each field is required to be filled in (DataRequired()
), and that password2
matches password
.
Now that we have these classes, how do we use them? That’s where the 3rd portion comes into play - create a route to handle the form interaction. We’ll make the next changes within our routes.py
file.
from app.forms.loginform import LoginForm, RegistrationForm
@apfell.route("/login", methods=['GET', 'POST'])
async def login(request):
form = LoginForm(request)
errors = {}
if request.method == 'POST' and form.validate():
username = form.username.data
password = form.password.data
try:
user = await db_objects.get(Operator, username=username)
if await user.check_password(password):
login_user = User(id=user.id, name=user.username)
auth.login_user(request, login_user)
return response.redirect("/")
except:
errors['validate_errors'] = "Username or password invalid"
errors['token_errors'] = '<br>'.join(form.csrf_token.errors)
errors['username_errors'] = '<br>'.join(form.username.errors)
errors['password_errors'] = '<br>'.join(form.password.errors)
template = env.get_template('login.html')
content = template.render(links=links, form=form, errors=errors)
return response.html(content)
@apfell.route("/register", methods=['GET', 'POST'])
async def register(request):
errors = {}
form = RegistrationForm(request)
if request.method == 'POST' and form.validate():
username = form.username.data
password = await crypto.hash_SHA512(form.password.data)
# we need to create a new user
try:
user = await db_objects.create(Operator, username=username, password=password)
login_user = User(id=user.id, name=user.username)
auth.login_user(request, login_user)
return response.redirect("/")
except:
# failed to insert into database
errors['validate_errors'] = "failed to create user"
errors['token_errors'] = '<br>'.join(form.csrf_token.errors)
errors['username_errors'] = '<br>'.join(form.username.errors)
errors['password_errors'] = '<br>'.join(form.password.errors)
template = env.get_template('register.html')
content = template.render(links=links, form=form, errors=errors)
return response.html(content)
Ok, we created a route for /register
and /login
. We used the same route for both GET
and POST
requests and just check for the method via a request.method
check. If this was a simple GET
request, then there isn’t actually a form submission, so we fall past the “if” statement down to setting the template, rendering our content, and returning the response (pretty standard so far). However, what if this was a POST
request? You’ll notice that we require form.validate()
to be true as well in our “if” statement. Remember all those validator
pieces in the form objects we created? When you call form.validate()
, the data passed in via the form is checked against those requirements and returns true
or false
. We only want to continue processing the data if it meets our validation requirements, otherwise we populate some errors and pass them on to the template for the user.
For registering a new user, you’ll see that we don’t actually just save their password in plain text - instead, we hash it and store that in the database. Eventually I’ll get around to storing/using a salt with it, but for now this is good enough. You can ignore the auth.login_user
stuff for now, that’s for the next topic we’ll cover. You’ll notice one more piece of information here - csrf_token
. When we deal with forms with WTF, we need to deal with csrf tokens. Just make sure to include the following in your main __init__.py
, apfell.config['WTF_CSRF_SECRET_KEY'] = 'really secure super secret key here, and change me!'
. You should change the secret to be something different for you, but that’s going to be the default so…
Now that we’ve talked about parts 2 and 3, let’s address that first one - form in HTML. This is pretty straightforward and just requires you to add in new HTML files with a form in the body block. Here’s the registration one:
<h1>Register</h1>
<form action="" method="post">
{{ form.csrf_token }}
<span style="color: red;">{{ errors.validate_errors }}</span>
<p>
{{ form.username.label }}<br>
{{ form.username(size=32) }}<br>
{% for error in form.username.errors %}
<span style="color: red;">[{{ error }}]</span>
{% endfor %}
</p>
<p>
{{ form.password.label }}<br>
{{ form.password(size=32) }}<br>
{% for error in form.password.errors %}
<span style="color: red;">[{{ error }}]</span>
{% endfor %}
</p>
<p>
{{ form.password2.label }}<br>
{{ form.password2(size=32) }}<br>
{% for error in form.password2.errors %}
<span style="color: red;">[{{ error }}]</span>
{% endfor %}
</p>
<p>{{ form.submit() }}</p>
</form>
And here’s the login one:
<h1>Sign In</h1>
<form action="" method="post">
{{ errors.token_errors }}
<span style="color: red;">{{errors.validate_errors}}</span>
<br>
{{ form.csrf_token }}
{{form.username.label}}
{{form.username(size=32)}}
<span style="color: red;">{{ errors.username_errors }}</span>
<br>
{{form.password.label}}
{{form.password(size=32)}}
<span style="color: red;">{{ errors.password_errors }}</span>
<br>
{{form.submit()}}
</form>
<p>New User? <a href="{{ links.register }}">Click to Register!</a></p>
{% endblock %}
The format of this should definitely remind you of the Jinja2 stuff we were doing earlier. In our form object, we had variables called username
, password
, password2
, and submit
. You’ll see all of those make a return in this HTML code as well as a reference to that variable’s label
. WTForms does a nifty thing of providing errors (that match up to the validators we put into the objects) that we can iterate through with Jinja2. In this example, we iterate over these errors and display them in red text for the user if they fail. This is a pretty straight forward form, so I won’t spend a lot of time on this.
The last part was incorporating this into our database. We already had a username
and password
field in our operator model (good foresight), but we need to add in a new function to make sure we’re dealing with hashes instead of plaintext passwords.
async def check_password(self, password):
temp_pass = await crypto.hash_SHA512(password)
return self.password == temp_pass.decode("utf-8")
I added in this check_password
function which you’ll recognize from the /login
route where I do await user.check_password(password)
to see if the user supplied the correct password. This function simply hashes the password that was sent in and compares it to the hash that’s stored in the database for that user.
All of those pieces together allow you to do forms. It was kind of a lot, but none of them were too complicated on their own. Now that you’ve created a form for a user to register and sign in with, how do you actually enforce that? How do you mark certain routes as requiring user authentication?
sanic-auth
Sanic-auth allows us to do exactly this! Since we already have the other user stuff done, there isn’t a lot required to get sanic-auth up and going. In __init__.py
in your main directory, we need to add the following:
from sanic_auth import Auth
apfell = Sanic(__name__)
apfell.config.AUTH_LOGIN_ENDPOINT = 'login'
auth = Auth(apfell)
session = {}
@apfell.middleware('request')
async def add_session(request):
request['session'] = session
All we’re doing here is setting up how the authentication will work. Take note though, that AUTH_LOGIN_ENDPOINT
is describing the URL for where to go to make people authenticate. In our case, that’s simply /login
so we specify login
(which is the default). Now we want to actually use it. For example:
from app import auth
@apfell.route("/")
@auth.login_required(user_keyword='user')
async def index(request, user):
template = env.get_template('main_page.html')
content = template.render(name=user.name, links=links)
return response.html(content)
All we need to do is specify the additional decorator of @auth.login_required
. Note though that it’s auth
simply because that’s what I called the variable in my __init__.py
file. If you call it something else, it’ll be different. You also need to add the user
parameter to your function. What’s pretty cool is that if we put this decorator here, and you’re not logged in, you’ll be redirected to our AUTH_LOGIN_ENDPOINT
and upon successfully logging in, you’ll be brought back to the page you tried to visit initially. I use the user’s logged in name in the banner at the top of each page, so that’s why I pass in user.name
. This user
parameter is something special to sanic-auth
though, so it only has a name and an id parameter. How do we set this though? Well, if you look back to the /login
and /register
routes, you’ll see that we simply call auth.login_user(request, login_user)
. Where login_user
is the user object.
And that’s essentially it! Now you have some basic authentication and forms for your web application!
In the next post, I’ll tie everything together to show a process flow of how a user interacts with the system and how the flow of information actually works when an implant connects.