PhishFlood: a poc for flooding phishing kits


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, 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”.

A SMS used to distribute a phishing campaign. Picture from

A SMS used to distribute a phishing campaign. Picture from

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.


For this proof of concept, I choose to target a particular phishing kit, which I have reported multiple times:

The target of this poc: a phishing page for the Italian bank Intesa Sanpaolo

The target of this poc: a phishing page for the Italian bank Intesa Sanpaolo

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:

    <form id="command" class="form-group" action="/core/login.php" method="post"
	    autocomplete="off"><br />
	        <h2><label for="camp1">Codice Titolare</label></h2>
	    <input class = "form-control" type="number" name="codice" id="_camp1"
	        required tabindex="1" value="" minlength="4" maxlength="10"><br />
	        <strong><label for="camp2">PIN</label></strong>
	    <input class="form-control" type="number" type="password" name="password"
	        id="_camp2" required tabindex="2" value=""><br />
	        <strong> <label for="camp3">Numero di telefono</label></strong>
	    <input class="form-control" type="number" name="cellulare"
	        id="_camp3" required tabindex="3" value=""><br />
	        <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>

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

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.

The POST made with cURL worked!

The POST made with cURL worked!

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)

	// 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")

    // 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")

    // 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

    // 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++ {

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")

    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 {
    } 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:

Output of phishflood

Output of phishflood

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:

Logs on the phishing page

Logs on the phishing page

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.


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.