Introduction
If you use Twitter to stay up to date with the latest security news, you may have noticed a community of researchers reporting phishing websites and scam pages everyday (if you want to follow them, phishunt.io have a good list of profiles in their community section).
Unfortunately, reporting these websites is not always very effective. In many cases the phishing pages are removed only after 24-48 hours of being reported, and at that point they may have already stolen credentials from a lot of victims. In order to maximize their effectiveness in few hours, these campaigns are distributed via SMS or email, urging the potential victim to perform a “quick action”.
In order to make it more difficult for threat actors, I decided to work on a proof of concept of a tool that aims to pollute the data of phishing victims with random information, so that actors will have to either validate the data to discover which are authentic, or discard the database.
Background
For this proof of concept, I choose to target a particular phishing kit, which I have reported multiple times:
This kit is particularly suitable for this experiment, because it exposes the logs of the victims in a text file that is often left unprotected online.
This poses an additional security risk for the victims, because their credentials are not only in the hands of the actor who deployed the kit, but are also potentially accessible to other actors that can crawl the web for phishing kits alredy deployed by others.
However, for the sake of this experiment, having the logs exposed makes it easier to verify if the code works as expected.
The phishing page
In this case, we don’t have access to the PHP code of the kit, because I couldn’t find the zip in these domains. However I would be interested in analyzing it, so if you have it, please let me know!
Even if we don’t have access to the PHP, we have everything we need in the HTML of the phishing page. Here is the form for entering the credentials:
<div>
<form id="command" class="form-group" action="/core/login.php" method="post"
autocomplete="off"><br />
<p><h2><label for="camp1">Codice Titolare</label></h2></p>
<input class = "form-control" type="number" name="codice" id="_camp1"
required tabindex="1" value="" minlength="4" maxlength="10"><br />
<p><strong><label for="camp2">PIN</label></strong></p>
<input class="form-control" type="number" type="password"
name="password" id="_camp2" required tabindex="2" value=""><br />
<p><strong> <label for="camp3">Numero di telefono</label></strong></p>
<input class="form-control" type="number" name="cellulare"
id="_camp3" required tabindex="3" value=""><br />
<p><strong><label for="camp4">Se sei cliente Fideuram seleziona
la casella in basso</label></strong>
<input class="form-check-label" type="checkbox" name="fideuram"
id="_camp4" value="Si" tabindex="5"></p><br /><br />
<button style="background-color: green;font-size : 20px;" type="submit"
class="btn btn-primary btn-lg btn-block">ENTRA</button></form>
</div>
The action
attribute of the form specifies where the POST request is sent, in this case to /core/login.php
. Using this address and the attributes name
and type
of the input fields, we can easily make a post with cURL:
$ curl -d "codice=123&password=123&cellulare=12345678" \
-X POST https://www.riscontrotitolare.com/core/login.php
Please note that this kit is also logging the IP address of these requests, so be sure to run the line above behind a proxy or a VPN.
After being sure that the POST request made it with cURL worked, we can start writing the code.
PhishFlood: writing the code
The idea of phishflood
is to have a program that:
- automatically detects the required attributes of the form and of the input fields
- makes POST requests with random data which are non easily distinguishable from authentic data
- uses various proxies to make requests (to hide our IP)
- wait a random time between two requests, to not create an obvious time frame of when the program was executed
- makes use of the goroutines to improve efficiency
For the first point, I decided to use goquery to detect the form
, get the content of the action
attribute, and the name
and type
attributes of the other input parameters.
For brevity reasons, I excluded from the code below all the lines for handling possible errors.
func getPostData(phishingUrl string, parsedProxies []string)
(string, []string, []string) {
postAction := ""
var inputNames []string
var inputTypes []string
var myClient *http.Client
// make post request using proxy
if len(parsedProxies) != 0 {
proxyURL, err := url.Parse(parsedProxies[0])
// be sure to handle the err..
myClient = &http.Client{Timeout: 15 * time.Second, Transport:
&http.Transport{Proxy: http.ProxyURL(proxyURL)}}
} else { myClient = &http.Client{Timeout: 15 * time.Second }
req, err := http.NewRequest("GET", phishingUrl, nil)
resp, err := myClient.Do(req)
defer resp.Body.Close()
if resp.StatusCode != 200 {
fmt.Printf("status code error: %d %s \n", resp.StatusCode, resp.Status)
os.Exit(1)
}
// Load the HTML document and find the form with goquery
doc, err := goquery.NewDocumentFromReader(resp.Body)
doc.Find("form").Each(func(i int, form *goquery.Selection) {
action, actionOk := form.Attr("action")
if actionOk {
form.Find("input").Each(func(i int, input *goquery.Selection) {
nameattr, nameOk := input.Attr("name")
typeattr, typeOk := input.Attr("type")
// find input with name and attributes
if actionOk && nameOk && typeOk {
inputNames = append(inputNames, nameattr)
inputTypes = append(inputTypes, typeattr)
u, err := url.Parse(phishingUrl)
// create full url for path where to submit the form
u.Path = path.Join(u.Path, action)
postAction = u.String()
}
})
}})
return postAction, inputNames, inputTypes
}
The beginning of the main
function of our code takes the URL of the phishing page in input, calls the function getPostData
(mentioned above) and prints the results:
func main() {
// check we have one input provided
if len(os.Args) != 2 {
fmt.Fprintf(os.Stderr,
"Please specify one URL: ./phishflood *URL* \n")
os.Exit(1)
}
// take a url from input
phishingUrl := os.Args[1]
// validate url provided
if _, err := url.ParseRequestURI(phishingUrl); err != nil {
fmt.Fprintf(os.Stderr, "It was not possible to parse the URL\n")
os.Exit(1)
}
// navigate to it and print findings
postAction, inputNames, inputTypes := getPostData(phishingUrl)
fmt.Printf("[!] Found a form with action: %s \n" +
"[!] Input fields names found: %v" +
"\n[!] Input fields types found: %v\n\n", postAction,
inputNames, inputTypes)
The rest of the main
uses 10 goroutines to make the requests concurrently (well, almost concurrently because we have a random delay), and a channel ( ch
) to communicate when a goroutine finished.
// set random seed
rand.Seed(time.Now().UnixNano())
// create channel used for goroutines
ch := make(chan string)
// specify the number of routines to use
routines := 10
// start goroutines
for i := 0; i < routines; i++ {
// create wait for a random number of seconds between 2 and 10
w := int(rand.Intn(10000-2000) + 2000)
time.Sleep(time.Duration(w) * time.Millisecond)
// send requests with fake data
go flood(i, postAction, inputNames, inputTypes, ch)
}
// when POST request is completed, print the status code from the channel
for i := 0; i < routines; i++ {
fmt.Println(<-ch)
}
}
A small delay between 10 and 2 seconds is introduced in the for loop. Ideally, this delay should be higher, to not make it obvious that these POST requests were automated.
The flood
function needs a list of proxy addresses (px
in the code below), which are used to make the requests without showing our IP address in the kit.
The fake data which are going to be submitted are contained in vals
and populated in a not sophisticated way: since all the input types are number
for this kit, it is sufficient to create random numbers between a sufficiently long interval.
In Go, it is possible to create random number between an interval in the following way:
randomnumber := rand.Intn( max - min ) + min
The input field with the name cellulare
needed particular attention: “cellulare” stands for “mobile phone” in italian, so the interval for the random generation is a bit more complicated.
After the POST request, the status code is sent to the channel, and the goroutine terminate its execution.
func flood(i int, postAction string, inputNames []string,
inputTypes []string, ch chan<- string) {
// make post request using proxy
proxyURL, _ := url.Parse(px[i%len(px)])
if err != nil {
fmt.Fprintf(os.Stderr, "Error parsing the proxy address\n")
os.Exit(1)
}
myClient := &http.Client{Timeout: 15 * time.Second, Transport:
&http.Transport{Proxy: http.ProxyURL(proxyURL)}}
// generate fake data
vals := url.Values{}
for i, valName := range inputNames {
// "cellulare" is "mobile phone" in italian, so we
// have a particular interval to make it realistic
if valName == "cellulare" {
val := rand.Intn(3499999999-3200000000) + 3200000000
vals.Set(valName, fmt.Sprintf("%d", val))
// these are generic numbers
} else if inputTypes[i] == "number" {
val := rand.Intn(99999999-10000000)+10000000
vals.Set(valName, fmt.Sprintf("%d",val))
}
}
// make the POST request
resp, err := http.PostForm(postAction, vals)
// print error
if err != nil {
fmt.Println(err)
} else {
// send to the channel the status code of the POST
ch <- fmt.Sprintf("Request #%d with these parameters {codice: %s,",
"cellulare: %s, password: %s} returned the following status code:",
"%d %s.", i+1, vals.Get("codice"), vals.Get("cellulare"),
vals.Get("password"), resp.StatusCode, http.StatusText(resp.StatusCode))
}
}
Results and possible improvements
If we run the code above specifying a URL, we should see something like this:
Depending on the status of the proxies, we may have some timeout errors. However, when I checked the logs of the kit I was able to find our fake data:
The main limitation of this poc is that is only compatible with these kinds of phishing kits. Two possible improvements are:
- Fake data generation for different types of input fields. Many phishing kits are targeting email credentials, or credit cards number, the library faker could help in the generation of these data.
- Handling multiple forms. Some phishing kits ask the user to fill different forms, and sometimes the second form is accessible only if the first one is submitted. A possible approach to overcome this would be to continue submitting form with fake data as long as there is not a redirection or no more forms are found.
Conclusion
In this post we saw how to create a proof of concept to pollute with fake data the credentials stolen with a phishing kit. There is a lot of space for improvements, but, after checking the logs of the kit, I consider the proof of concept successful as it is.
If you want to play around with phishflood
you can use this GitHub repo, I have organized the code and added some improvements. Feel free to let me know what features could be added.