ffuf logo with running mascot

Those who are keen on notifications, whether on twitter or github, will not have missed the release of version 2.0.0 of ffuf.

In my previous article Advanced Tips with ffuf, I started by introducing ffuf: What is it for? What makes it different from the others? The rest of the article focused on advanced uses of the tool. To summarize, ffuf is a file and folder scanner for web applications but also a fuzzer.

This time, the article will focus on the following new features of version 2.0:

Note 1: To practice while reading this article, you can deploy the machine in the ffuf TryHackMe room, use the ffufme docker image or use the online version of ffufme.

Note 2: This article is also available in french 🇫🇷.

Configuration

We will start with this point as we will need it for the following ones.

In my previous article, I detailed The Configuration File usage but revealed that it was not in a standard location.
This is now done, as ffuf will now look for the configuration file in a location that respects the XDG Base Directory standard: $XDG_CONFIG_HOME/ffuf/ffufrc.

A first run of ffuf 2.0 creates the configuration folder.

$ tree ~/.config/ffuf/
/home/noraj/.config/ffuf/
├── history
└── scraper

To migrate a configuration file from ffuf 1.5, simply move it.

$ mv ~/.ffufrc ~/.config/ffuf/ffufrc

Scraper

The scrapper allows specific data to be extracted from HTTP responses either via regular expressions (regexp) or via CSS selectors.

Rules can be deployed in $XDG_CONFIG_HOME/ffuf/scraper/ as JSON files.

Note that ffuf rules are not the equivalent of nuclei templates. The ffuf rules are much simpler and only aim to extract information from one of the pages under test, whereas the nuclei templates allow for vulnerability scanning through an advanced DSL, multi-protocols support and advanced conditions.

Let’s imagine a use case where a bug hunter wants to retrieve the contact address from the security.txt file of all sites with a bug bounty or responsible disclosure program referenced on chaos from ProjectDiscovery.

To simulate this, let’s simply take the following list of hosts (hosts.txt):

www.google.com
cloudflare.com
github.com
nginx.com
discord.com
ovh.com

As a list of files to test, we will take the following list (security.txt):

/.well-known/security.txt
/security.txt

And in order to extract the contact address, we create the following ffuf rule in ~/.config/ffuf/scraper/security-txt.json :

{
    "active": false,
    "groupname": "security_txt",
    "rules": [
        {
            "name": "contact",
            "rule": "(?mi)Contact:(.*)",
            "target": "body",
            "type": "regexp",
            "onlymatched": true,
            "action": [
                "output"
            ]
        }
    ]
}

This gives the following answer:

$ ffuf -u https://HOST/SEC --scrapers "security_txt" -w ./security.txt:SEC -w ./hosts.txt:HOST -mc 200 -r
[...]
________________________________________________

 :: Method           : GET
 :: URL              : https://HOST/SEC
 :: Wordlist         : SEC: /home/noraj/R_D/articles/ffuf-2.0.wiki/security.txt
 :: Wordlist         : HOST: /home/noraj/R_D/articles/ffuf-2.0.wiki/hosts.txt
 :: Follow redirects : true
 :: Calibration      : false
 :: Timeout          : 10
 :: Threads          : 40
 :: Matcher          : Response status: 200
________________________________________________

[Status: 200, Size: 246, Words: 7, Lines: 7, Duration: 17ms]
    * HOST: www.google.com
    * SEC: /.well-known/security.txt
| SCR |
    * contact: Contact: https://g.co/vulnz
    * contact:  https://g.co/vulnz
    * contact: Contact: mailto:security@google.com
    * contact:  mailto:security@google.com

[Status: 200, Size: 169, Words: 6, Lines: 6, Duration: 45ms]
    * HOST: discord.com
    * SEC: /security.txt
| SCR |
    * contact: Contact: https://discord.com/security
    * contact:  https://discord.com/security
    * contact: Contact: mailto:security@discord.com
    * contact:  mailto:security@discord.com

[...]

[Status: 200, Size: 282, Words: 7, Lines: 7, Duration: 66ms]
    * HOST: ovh.com
    * SEC: /.well-known/security.txt
| SCR |
    * contact: Contact: csirt@ovhcloud.com
    * contact:  csirt@ovhcloud.com

:: Progress: [12/12] :: Job [1/1] :: 0 req/sec :: Duration: [0:00:00] :: Errors: 0 ::

In another scenario, let’s put ourselves in the shoes of an auditor who is eagerly scanning the ACCEIS new job offers, and wants to retrieve the titles of all available positions and the URLs of the offers. He then creates the following configuration file ~/.config/ffuf/scraper/acceis.json.

{
    "active": false,
    "groupname": "acceis",
    "rules": [
        {
            "name": "title",
            "rule": "p.post__title",
            "target": "body",
            "type": "query",
            "onlymatched": true,
            "action": [
                "output"
            ]
        },
        {
            "name": "url",
            "rule": "<a href=\"(.*)\" class=\"post__card \">",
            "target": "body",
            "type": "regexp",
            "onlymatched": true,
            "action": [
                "output"
            ]
        }
    ]
}

name is the label that will be displayed when data is retrieved, so we get the title with a CSS selector and use a regular expression to retrieve the URL.

Here is the result below. printf '/' | ... -w ... is used only to please ffuf because it is impossible not to specify a fuzzing keyword, so we create a virtual one from the standard input (STDIN).

$ printf '/' | ffuf -u https://www.acceis.fr/nos-offres-demploi/FUZZ -w - --scrapers "acceis"
[...]
________________________________________________

 :: Method           : GET
 :: URL              : https://www.acceis.fr/nos-offres-demploi/FUZZ
 :: Wordlist         : FUZZ: -
 :: Follow redirects : false
 :: Calibration      : false
 :: Timeout          : 10
 :: Threads          : 40
 :: Matcher          : Response status: 200,204,301,302,307,401,403,405,500
________________________________________________

[Status: 200, Size: 82501, Words: 6239, Lines: 573, Duration: 50ms]
    * FUZZ: /
| SCR |
    * title:
                Ingénieur d’Affaires en cybersécurité
    * title:
                Auditeur en cybersécurité
    * title:
                Consultant(e) cybersécurité
    * title:
                Ingénieur(e) évaluation en cybersécurité et cryptographie
    * url: <a href="https://www.acceis.fr/nos-offres-demploi/ingenieur-daffaires-en-cybersecurite/" class="post__card ">
    * url: https://www.acceis.fr/nos-offres-demploi/ingenieur-daffaires-en-cybersecurite/
    * url: <a href="https://www.acceis.fr/nos-offres-demploi/auditeur-en-cybersecurite/" class="post__card ">
    * url: https://www.acceis.fr/nos-offres-demploi/auditeur-en-cybersecurite/
    * url: <a href="https://www.acceis.fr/nos-offres-demploi/consultante-cybersecurite/" class="post__card ">
    * url: https://www.acceis.fr/nos-offres-demploi/consultante-cybersecurite/
    * url: <a href="https://www.acceis.fr/nos-offres-demploi/ingenieur-evaluation-en-cybersecurite-et-cryptographie/" class="post__card ">
    * url: https://www.acceis.fr/nos-offres-demploi/ingenieur-evaluation-en-cybersecurite-et-cryptographie/

:: Progress: [1/1] :: Job [1/1] :: 0 req/sec :: Duration: [0:00:00] :: Errors: 0 ::

A CTF player could just as well create a retrieval rule with a regular expression corresponding to the flag format in order to detect it while browsing the site. (We’ll see if this proves useful at BreizhCTF 🙃🏳️)

To retrieve the leaderboard from AttackerKB (or from a CTF or online challenge platform):

{
    "active": true,
    "groupname": "attackerkb",
    "rules": [
        {
            "name": "user",
            "rule": ".username>a",
            "target": "body",
            "type": "query",
            "onlymatched": true,
            "action": [
                "output"
            ]
        }
    ]
}

Results:

$ printf '/' | ffuf -u https://attackerkb.com/leaderboard -w - --scrapers "attackerkb" -H "X-Fake: FUZZ"
[...]
________________________________________________

 :: Method           : GET
 :: URL              : https://attackerkb.com/leaderboard
 :: Wordlist         : FUZZ: -
 :: Header           : X-Fake: FUZZ
 :: Follow redirects : false
 :: Calibration      : false
 :: Timeout          : 10
 :: Threads          : 40
 :: Matcher          : Response status: 200,204,301,302,307,401,403,405,500
________________________________________________

[Status: 200, Size: 22528, Words: 5990, Lines: 430, Duration: 941ms]
    * FUZZ: /
| SCR |
    * user: gwillcox-r7
    * user: wvu-r7
    * user: busterb
    * user: zeroSteiner
    * user: ccondon-r7
    * user: wchen-r7
    * user: bwatters-r7
    * user: nu11secur1ty
    * user: kevthehermit
    * user: space-r7

:: Progress: [1/1] :: Job [1/1] :: 0 req/sec :: Duration: [0:00:01] :: Errors: 0 ::

Or a knowledge-hungry person wanting to know about the latest episodes of Hack’n’Speak on Apple Podcast.

{
    "active": true,
    "groupname": "applepodcast",
    "rules": [
        {
            "name": "title",
            "rule": "ol.tracks.tracks--linear-show li.tracks__track.tracks__track--podcast a.link.tracks__track__link--block",
            "target": "body",
            "type": "query",
            "onlymatched": true,
            "action": [
                "output"
            ]
        }
    ]
}
$ printf '/' | ffuf -u https://podcasts.apple.com/fr/podcast/hackn-speak/id1548697084 -w - --scrapers "applepodcast" -H "X-Fake: FUZZ"
[...]
________________________________________________

 :: Method           : GET
 :: URL              : https://podcasts.apple.com/fr/podcast/hackn-speak/id1548697084
 :: Wordlist         : FUZZ: -
 :: Header           : X-Fake: FUZZ
 :: Follow redirects : false
 :: Calibration      : false
 :: Timeout          : 10
 :: Threads          : 40
 :: Matcher          : Response status: 200,204,301,302,307,401,403,405,500
________________________________________________

[Status: 200, Size: 349047, Words: 22795, Lines: 1651, Duration: 25ms]
    * FUZZ: /
| SCR |
    * title:
          0x1B - @M4yFly | Retour sur la création du lab GOAD et une RCE 9.8 sur GLPI CVE-2022-35914

    * title:
          0x1A - @g0h4n | Retour sur la création de RustHound, l'outil crossplateforme plus rapide que Sharphound !

    * title:
          0x19 - @rkvl | Retour sur la création de Sliver & le redteam aux US (gilet pare balles non obligatoire)

    * title:
          0x18 - @Swissky | Retour sur la création de PayloadsAllTheThings & SSRFmap !

    * title:
          0x17 - @_ZakSec | Retour sur la création de Masky et on parle purple team !

    * title:
          0x16 - @snyff | Retour sur la création et la philosophie de PentesterLab

:: Progress: [1/1] :: Job [1/1] :: 0 req/sec :: Duration: [0:00:00] :: Errors: 0 ::

As you will have understood, the use cases are numerous, so we will stop here for the examples.

Requests backtracking (Ffufhash)

In my previous article, the part about mutators explained how the variation number was stored in the FFUF_NUM environment variable so that it could be displayed and then used to find the mutator seed e.g. radamsa --seed 2.

This concept has now been generalised to the whole ffuf context with the FFUFHASH keyword which generates a hash for each payload to obtain a unique identifier for it.

This is particularly useful for blind or out of bound (OOB) queries as the payload will not be displayed on ffuf.

Let’s say you try to exploit an SSRF or RCE OOB that will attempt to make an HTTP connection to a domain you control. When you receive the connection, you know that the application is vulnerable. The problem is that you can’t tell which of the 200,000 payloads tested bypassed the WAF because it was filtered or contained invalid characters in the field where you could retrieve the information. You will have to use FFUFHASH as the canary.

Here I use an HTTP request collector to which I concatenate the canary in the url parameter and I use the validation code to which I concatenate the SQL payload for injection in the validation_code parameter.

ffuf -u https://example.org/endpoint?url=https://noraj.requestcatcher.com/FFUFHASH&validation_code=556cc23863fef20fab5c456db166bc6eFUZZ -w /usr/share/seclists/Fuzzing/SQLi/quick-SQLi.txt

For example, you will receive a request at https://noraj.requestcatcher.com/24e511. Then to search in ffuf for the payload that caused the request, you would use the -search option.

$ ffuf -search 24e511
Request candidate(s) for hash 24e511
-------------------------------------------
ffuf job started at: 2023-02-13T17:14:25+01:00

GET /24e511?payload=556cc23863fef20fab5c456db166bc6e%27%20OR%20%271%27=%271 HTTP/1.1
Host: noraj.requestcatcher.com
User-Agent: Fuzz Faster U Fool v2.0.0-dev
Accept-Encoding: gzip

This allows me to find that the payload that worked was ' OR '1'='1.

About the author

Article written by Alexandre ZANNI aka noraj, Penetration Testing Engineer at ACCEIS.