TL;DR: I’ll shine a light on Gophish and how to modify it to change behavior or introduce/remove functionality. At the end of this post, you’ll know how to host custom 404 pages in Gophish and how to abuse HTTP basic auth instead of login forms embedded on the landing page to obtain juicy creds.
A few days ago I tweeted one of my modifications to Gophish:
After low click rates in my last phishing campaign due to staff being extremely well trained for this kind of attack, I modded gophish to show an HTTP Basic auth request instead of a phishing site. Once data is entered, users are redirected to a legit site: pic.twitter.com/LncsgT8OSE— Michael Eder (@michael_eder_) May 14, 2021
I decided that it is worth to go a little bit into detail on how to modify Gophish.
Gophish is an open-source phishing framework. It has several nice properties, for example, Gophish is platform independent and comes in a single, ready to use binary. We use Gophish internally a lot because of its flexibility - we’ve done countless deployments to perform phishing attacks against organizations of any size. The powerful API paired with the self-contained binary and ease of configuration really stand out compared to many other products in this area.
I believe that many people are reluctant to tinker with code on their own; as somebody who also sometimes requires a push or needs to see how easy things may go, I decided to write a blog post where I showcase some modifications to Gophish that may come in handy in some situations. If you’re a seasoned phisherman and modifying the stuff you use is your daily business, there probably won’t be anything of interest in this post. If you’re an occasional user and dislike some behavior or are missing out features, this post may help you getting started. When you decide to improve Gophish and think your changes benefit the goals of the Gophish project, I highly encourage you to submit a PR or at least open an issue to discuss if integrating your changes makes sense. The Gophish community is really awesome, so there’s a chance that your changes may also be introduced in the upstream project, helping others, too.
This post will not cover how Gophish works in detail, so if you’re unfamiliar with the tool, I recommend to take a look at the documentation.
Why even modding Gophish?
For this post, I’ll showcase two modifications:
- Return a custom 404 page. Gophish’s 404 page just prints 404 and there are no customization opportunities. I’m pretty sure a seasoned analyst visiting the page will spot the intent of the URL instantly. A page that blends in better, e.g. a 404 page of a well-known web server like nginx or something that relates to the domain can avoid raising suspicions.
- Phish users with HTTP auth instead of a phishing page. Mature organizations have trained their staff to death, so low success rates coupled with a high number of suspicious email reports hitting the SOC are something to definitely expect when sending out your average O365 login page phish. As an attempt to overcome this, I will modify Gophish to request an HTTP auth on the landing page instead of displaying the phishing page. This will bring an authentication popup, and because there is an operating system / browser window asking for credentials, I assume there’s a higher chance for people to fall for this.
The modifications I’m going to show are quick and dirty. First, of course I do not want to bloat this post more than necessary. Second, these modifications are local and on-demand. It is beneficial to keep them short and easy to understand, because when you’re stressed and in the field, a failing database migration because you messed up somewhere is a unpleasant experience. Keeping your modifications small and contained also allows you to easily adapt to newer versions - a large set of patches may be more difficult to apply to a newer version in the future. That said, don’t expect nice GUI integrations. What you’ll get is a custom Gophish with bonus functionality tailored to a specific use case. For example, the second modification I’m going to show will completely break the landing page functionality, so this is something to keep in mind.
You can skip this section if you have worked with Go and Gophish before.
Gophish is written in Go, which is one of my favorite programming languages. One thing that is particularly nice regarding Go is that it is a really boring language. Its syntax is easy to follow and understand. You also won’t see much syntactic sugar or magic expressions that are unfamiliar to you, which makes it easy to dive in and write your own code.
I usually work on Linux, either Debian or Fedora, but basically it doesn’t matter. Go is platform independent, so you can follow my steps on Windows or macOS, too. All you’re going to need is a working Go setup (at least v1.10), git, and an editor of your choice - I recommend Visual Studio Code, but in the end it doesn’t matter.
First of all, grab a copy of the Gophish code:
From within the cloned directory, you can run
go build (if this is your first time, you may want to use
go build -v to see more details on what is going on) to compile Gophish.
When you’re running the build for the first time, it will take a few minutes to fetch all dependencies and build them, but in general, Go is a language that compiles really fast, so compiling the changes later on will barely take seconds.
You can simply run the resulting gophish binary, but you may want to point
listen_url in the
phish_server section in
config.json to a port greater than 1024, unless you’re working as root.
config.json I’ll use during this post looks like this:
When you first run gophish, it will generate a new random password for the first administrative user:
Navigating to https://localhost:3333 in your browser will bring up the Gophish admin interface.
Log in with
admin and the password Gophish prints to the logs.
After changing the password, everything is ready!
First modification: Return a custom 404 page
When you access Gophish’s phishing listener, the page looks quite unpleasant:
It would be nice to have something more appealing, at least for a 404 page, here.
First of all, it is required to find out where the 404 page is rendered.
The Gophish code is well structured, nevertheless, it is not unusual that it takes some time to figure out where certain functionality is defined.
In this case, the relevant code resides in controllers/phish.go.
You can find several calls to
http.NotFound, which is a convenience function built into Go’s standard HTTP library.
One of the nice things about Go is that it is entirely written in Go. This means, you can just click the function name in the documentation and it will bring you to Go’s builtin implementation of NotFound:
Surprisingly, this is really straight forward.
The request passed in is not used any further, whereas the
ResponseWriter is passed into the
Error function, together with the string
404 page not found and the constant
The library lists the defined constants right in the beginning of the documentation (source is here), and you can see that this is just the value
Therefore, it is also possible to call the
Error function with the value
404 directly, but using the name of the HTTP status is obviously a more ergonomic choice here.
Long story short, no magic to take care of here.
This is how the
Error function looks like:
Really easy, set some HTTP headers, set the status code and print the error message to the response body.
The issue that arises though, is that this functionality is defined in the inner workings of Go, there is no functionality to pass a custom error message -
NotFound does not take an argument that allows to pass a custom message.
Since the source code is available, and luckily it is concise, we can just copy it over to the phish controller:
customNotFoundin the file, recompiling Gophish (just another
go build) and running the new binary will produce the following 404 message when a site is not found: That was easy! Unfortunately, the sophisticated HTML placed for the sake of demonstrating the next issue is not rendered. There’s custom text now, but it doesn’t look unsuspicious yet. The issue here is that the
Errorfunction sets the HTTP Response’s
text/plain, instructing the browser to not render the response:
customNotFounddefined earlier. When being stressed and tired, changing references is one thing often overlooked. I recommend you to somehow track what you intend to change. You can use your text editor to find all relevant occurences and replace them automatically with the desired function. I also recommend you to step through all occurrences, otherwise you may also replace stuff you did not intend to change; This is not the place to save time by working sloppily. The result looks like this: That’s what I call a good looking, unsuspicious error message rendered from HTML! Jokes aside, this is everything it takes to get more control over 404 error pages in Gophish. You could now inline your HTML, I went for a more fancy approach and used Go’s templating engine and a nginx error page to render the final page from a file. As this is barely related to changing the Gophish behavior and just bringing in personal preferences, I’ll just leave you with the code additionally containing my 10 LOC templating logic plus the nginx template. It is using Go’s templating instead of Gophish’s internal templating functionality (which is just some abstraction to Go’s templating, really not a big deal). As I said, keeping changes simple and future-compatible is one of my goals when modifying stuff. The less you change, the better.
Second modification: Using HTTP auth for phishing
Gophish’s workflow for phishing is to send mails to users, which then hopefully click the link and submit their credentials on the landing page. We maintain a huge set of ready-to-use templates with different functionality and stories internally, but all of them are regular web pages that ultimately require the user to submit their data in the embedded form. Recently, I came across a target that invested tremendous efforts on training their employees. 75% of the targets reported my phishing attempts, regardless of the story, and only 3% submitted their data, all of them on a story that unintentionally matched a legit internal bonus program, so even these small numbers were pure luck. Just to be clear: Particularly one of my stories was top notch and I was absolutely sure at least 20%, in the worst case, will fall for it. No one did.
This brings me to the second Gophish modification I came up with.
Instead of hosting a landing page and trying to convince the user that this is the right place to put their credentials, I’ll ask them to authenticate via HTTP Basic Authentication (see Wikipedia, MDN).
HTTP has an extensible authentication mechanism built into the standard.
The simplest one is Basic Authentication, which works by supplying the Base64 encoded credentials in the
Authorization HTTP header.
Of course, users do not set the HTTP header on their own.
If a page that requires authentication is visited, the server checks for the existence of the header (and, of course, performs authentication and authorization).
If it is not there, the server can respond with a
401 Unauthorized HTTP response and a
WWW-Authenticate header, specifying the expected authentication type (in this case
Basic) and the name of the realm the user is authenticating to.
The string can be freely chosen and is often displayed to the user to indicate where he is currently authenticating to.
For example, a good value may be
Company XXX Internet Proxy Authentication.
When the browser receives this request, an authentication window will pop up. The interesting thing here is, that this is a browser or, in the best case, operating system window, depending on the victims browser. To the user, it doesn’t feel like entering credentials on a website, but giving the OS / Browser credentials to access the site in the first place. After the user enters credentials, the browser will take care about the technical details like setting the appropriate headers, to make everything work in the end. If you’re interested in technical details, the links spread across the text are an excellent starting point.
Making Gophish capable of HTTP auth
So, after the longish quick intro to the use case, let’s find out how to implement this in Gophish. Ideally, the implementation is interoperable with the whole Gophish workflow, so everything should work from sending mails to receiving the data and finally using Gophish’s builtin analysis tools for evaluating success of the campaign. To achieve this, the best idea is probably to modify the landing page listener such that it does not render a phishing page, but issues an HTTP basic authentication request.
controllers/phish.go, more concisely, renderPhishResponse, is the main part of interest here:
As you can see, the function handles two different cases: First, a
POST request indicates that a user submitted data.
If a redirect URL is specified, the user is sent there.
If the request is not a
POST request or there is no redirect URL specified, the page is rendered as is.
Before changing anything, there are a few things to note:
- Credentials are not handled here. In fact, handling of the submitted data is a bit difficult to find and understand, if you’re interested, you may give it a shot before continue reading.
- It is optional to specify a redirect URL in Gophish. In this case, it is probably a good idea to overthink this and make it mandatory, so the existing battle-tested redirection logic of Gophish will send a user to a legit site after entering credentials.
- Gophish assumes that
GETrequests are users landing on the page while a
POSTrequest means that a user submitted data. There are no further abstractions or ways to deviate from this concept, so unless building these abstractions, the modifications for HTTP auth based phishing will replace the current logic. This means that a Gophish server with the patches will not be able to carry out regular campaigns. Implementing a GUI switch and coexistence of both types will take additional, non-trivial changes. I accept this limitation.
In this case, I’ll make coarse steps compared to the first modification.
I’ll present the modifications and walk through them.
Getting there basically works the same as depicted above, and it barely took me an hour in the middle of the night, so don’t be afraid when it comes to modifying stuff without knowing where it will take you.
This is the new
In line 3, you can see that a call to the
BasicAuth method of the request.
The BasicAuth function is again already defined by Go’s
net/http library and basically returns the provided username, password, and if these values have been provided.
Username and password are not relevant here as they’re parsed somewhere else in the code (again, try to stick to the existing workflow).
Here, only the existence of authentication data is checked.
If authentication data is present, redirect the user to the redirect URL specified in the landing page, otherwise, add an HTTP
WWW-Authenticate header and set the HTTP status to
Note that this snippet contains the
customNotFound I introduced in the first example.
When working on a vanilla code base,
http.NotFound is surely a better choice here.
Also note that the realm is set to the HTML code of the landing page.
This allows to set the realm directly in the landing page editor in the web interface, but I have to get rid of some HTML tags.
This is what
stp.Sanitize() is for.
I decided the quickest and easiest way to remove all HTML tags is to use bluemonday, a library for sanitizing HTML.
The page object does not expose raw text and this seemed the most straight forward way to remove the HTML tags without doing more substantial changes to Gophish.
So, now there’s no differentiation between
POST - what matters is if there is an HTTP Basic Auth sent along, and if not, it is requested.
By changing this function, most of the work is already done.
What’s missing though is the most important part - retrieving the credentials.
The lengthy function PhishHandler takes care of this:
In case of a
GET request, the “Link was clicked” event is fired, in case of a
HandleFormSubmit will retrieve the data and insert it into the database.
I’m not going to take a closer look at
HandleFormSubmit, in essence, and this may be surprising, Gophish is not aware of the concept of usernames and passwords, and actually it doesn’t need to.
Gophish has a type called
EventDetails (can be found in models/campaign.go) that stores User Agent and Payload, the latter one containing all parameters sent - including username and password.
This behavior is by the way also the reason, why you sometimes see more data in the campaign results, for example CSRF tokens or random crap modern web frameworks put there.
Gophish simply stores and displays everything that was sent.
Username and password usually happen to be in that data.
In order to record the data sent via HTTP auth, there are two things to do: Extract the data from the
Authorization header and present it to Gophish in a way that it believes this was a request body (again, this is the easiest, not the best way).
This time, I’m interested in username and password, so I keep them in appropriately named variables.
In case the Basic auth was not
ok, meaning there was not Basic auth at all, the click is recorded.
If there was a Basic auth header, the payload for the
EventDetails struct is manually constructed and the modified object is submitted to
Admittedly, from a coding point of view, creating the data structure in a one-liner is the most challenging part in the whole blog post if you’re not familiar with Go.
There’s only a small change to
models/page.go missing in order to enforce the existence of a redirect URL, but this is nothing that is required for the above code to work.
In the beginning of the file, I added a new error type:
Later on, in parseHTML, a check is introduced to make sure the redirect URL is not empty:
I’m just reusing the images from my tweet here, they show how it looks like when a user clicks the link:
This is what the operator sees in the Gophish logs:
Closing thoughts and tips
First of all, you can find all modifications here. When I’m stuck while modding, I usually insert some print statements or start the debugger to verify my assumptions. Assumptions are important. Take a piece of paper and draw the control and data flows. Verify your assumptions by going through the code step by step. You’ll notice that at some point the things that happen deviate from what you expect, often because your assumptions are too vague or simply wrong. When browsers are involved, I usually do page reloads only from inspector with cache disabled. I’ve lost countless hours before realizing that the browser cache still serves a broken variant and my code already worked two hours before. Taking breaks and going for a walk or a nap also help freeing the mind when you’re stuck.
I hope this blog post encourages people to be less reluctant to touch other people’s code. Especially for projects written in Go, it is often quite easy to understand what’s going on and especially small changes are really easy to implement. I’ve had many cases where tools were missing small features or didn’t work in the concrete scenario, most of them were easy to overcome with some custom patches. Apart from that, I generally recommend all Red Teamers to know what your daily tradecraft does behind the scenes, some things may surprise you and uncover your operation right after you started…
Did you know that #evilginx sets the X-Evilginx header to the value of the domain that is phishing your users?— Michael Eder (@michael_eder_) November 4, 2019
Defenders: Check your logs
Attackers: mind your opsec
I also recommend you to look at other branches than main/master and to go through Pull requests. Often you find interesting stuff you can reuse, for example there’s a quite old beef-integration branch in the Gophish repository and there are Pull requests like Implement functionality to support custom RId - Ability to specify both the character set and length. Just because stuff isn’t there, for example alternative means of delivering messages, e.g. via WebHook, doesn’t mean it has to be this way forever.
I’d love to hear your feedback, so if this post somehow helped you or you have any questions or other feedback, just get in touch.