Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support TLS ClientHello randomization #3330

Closed
rpaul-ghostsec opened this issue Feb 16, 2023 · 5 comments · Fixed by #3844
Closed

Support TLS ClientHello randomization #3330

rpaul-ghostsec opened this issue Feb 16, 2023 · 5 comments · Fixed by #3844
Assignees
Labels
Status: Completed Nothing further to be done with this issue. Awaiting to be closed. Type: Enhancement Most issues will probably ask for additions or changes.
Milestone

Comments

@rpaul-ghostsec
Copy link

rpaul-ghostsec commented Feb 16, 2023

Feature

   -tlsi, -tls-impersonate  enable experimental client hello (ja3) tls randomization

See below for the implementation


I noticed that nuclei has the same JA3 hash across multiple different OS and environments (tested OSX, Ubuntu 20.04/22.04, Kali): 19e29534fd49dd27d09234e639c4057e
This makes it extremely easy to identify and block nuclei scans from a WAF perspective, without needing a reverse proxy to decrypt and inspect payloads at all because you only need to inspect the client hello coming from nuclei. See the JA3? Drop the request.

Here is an example of how to generate a random JA3 hash on every single request, which makes detecting scans more difficult and thus improves scan accuracy. Randomizing the cipher order on every single request might be expensive, but it could be done when a nuclei scan is launched so each individual scan has its own JA3, rather than the exact same JA3 every time for everyone using nuclei.

from requests import sessions
from fake_useragent import UserAgent
import random
from requests.adapters import HTTPAdapter
from requests.packages.urllib3.util.ssl_ import create_urllib3_context


class CryptoAdapter(HTTPAdapter):

    def get_randomized_ciphers(self):
        ciphers = [
            "ECDH+AESGCM",
            "DH+AESGCM",
            "ECDH+AES256",
            "DH+AES256",
            "ECDH+AES128",
            "DH+AES",
            "ECDH+HIGH",
            "DH+HIGH",
            "ECDH+3DES",
            "DH+3DES",
            "RSA+AESGCM",
            "RSA+AES",
            "RSA+HIGH",
            "RSA+3DES",
            "!aNULL",
            "!eNULL",
            "!MD5"
        ]
        random.shuffle(ciphers)
        ciphers = ciphers[:-1]
        ciphers = ':'.join(ciphers)

        print(f"[-] Using CIPHERS: {ciphers}")
        return ciphers

    def init_poolmanager(self, *args, **kwargs):
        context = create_urllib3_context(ciphers=self.get_randomized_ciphers())
        kwargs['ssl_context'] = context
        return super(CryptoAdapter, self).init_poolmanager(*args, **kwargs)

    def proxy_manager_for(self, *args, **kwargs):
        context = create_urllib3_context(ciphers=self.get_randomized_ciphers())
        kwargs['ssl_context'] = context
        return super(CryptoAdapter, self).proxy_manager_for(*args, **kwargs)


def main():
    ua = UserAgent()

    proxies = {
        "http": "http://127.0.0.1:8080",
        "https": "https://127.0.0.1:8080"
    }

    headers = {
        "accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9",
        # "accept-encoding": "gzip, deflate, br",
        "accept-language": "en-US,en;q=0.9",
        "cache-control": "max-age=0",
        "referer": f"https://google.com/",
        "sec-ch-ua": '"Not?A_Brand";v="8", "Chromium";v="108", "Google Chrome";v="108"',
        "sec-ch-ua-mobile": "?0",
        "sec-ch-ua-platform": '"macOS"',
        "sec-fetch-dest": "document",
        "sec-fetch-mode": "navigate",
        "sec-fetch-site": "same-origin",
        "sec-fetch-user": "?1",
        "upgrade-insecure-requests": "1",
        "user-agent": ua.chrome
    }

    s = sessions.Session()
    s.mount("https://www.google.com/", CryptoAdapter())  # Patch in the new ciphers for this domain.
    s.get("https://www.google.com/search?q=hello+world", headers=headers, proxies=proxies, verify=False)

	
if __name__ == "__main__":
    
    main()
@rpaul-ghostsec rpaul-ghostsec added the Type: Enhancement Most issues will probably ask for additions or changes. label Feb 16, 2023
@rpaul-ghostsec
Copy link
Author

rpaul-ghostsec commented Feb 16, 2023

I suspect the reason for the static JA3 hash is due to automatic cipher ordering in crypto/tls: https://go.dev/blog/tls-cipher-suites

https://github.com/projectdiscovery/nuclei/blob/195766e5fd37d617ec9c23a31ae9cd48407704ad/v2/pkg/protocols/headless/engine/http_client.go

tls.Config{
    Certificates: []tls.Certificate{cert},
    // shuffle this array here.
    CipherSuites: []uint16{
        tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
        tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
    },
    MinVersion:               tls.VersionTLS10,
    PreferServerCipherSuites: true,
    InsecureSkipVerify: true
}

From the blog:

Go 1.17, recently released, takes over cipher suite preference ordering for all Go users.
While Config.CipherSuites still controls which TLS 1.0–1.2 cipher suites are enabled, it is not used for ordering, and Config.PreferServerCipherSuites is now ignored.
Instead, crypto/tls makes all ordering decisions, based on the available cipher suites, the local hardware, and the inferred remote hardware capabilities.

Although I did make a quick script to perform a simple TLS request to catch the JA3 to see if the JA3 is static among golang packages and it is not. They had an entirely different JA3 hashes compared to nuclei.

Maybe one suggestion to randomize the JA3 is to enable and disable random ciphers since we cannot control the order of the ciphers but we can control what ciphers are used, which will cause the ciphers in the client hello message to be different, and thus change the outcome of the hash. I do something similar in the python script provided in the original post.

@ehsandeep ehsandeep added the Investigation Something to Investigate label Mar 10, 2023
@ehsandeep
Copy link
Member

@ehsandeep ehsandeep added the Status: Available No one has claimed responsibility for resolving this issue label Mar 10, 2023
@RamanaReddy0M RamanaReddy0M self-assigned this Apr 11, 2023
@Mzack9999 Mzack9999 self-assigned this May 26, 2023
@Mzack9999
Copy link
Member

Depends on projectdiscovery/fastdialer#123

@ehsandeep ehsandeep removed the Status: Available No one has claimed responsibility for resolving this issue label Jun 17, 2023
@ehsandeep ehsandeep changed the title Randomize Cipher Order to Create Random JA3 Hash Upon Scan Launch Support TLS ClientHello randomization Jun 17, 2023
@Mzack9999 Mzack9999 linked a pull request Jun 19, 2023 that will close this issue
4 tasks
@ehsandeep ehsandeep added Status: Completed Nothing further to be done with this issue. Awaiting to be closed. and removed Investigation Something to Investigate labels Jun 21, 2023
@ehsandeep
Copy link
Member

@rpaul-ghostsec this is now added in dev branch - #3844 (review)

@ehsandeep ehsandeep added this to the nuclei v2.9.7 milestone Jun 26, 2023
@ehsandeep
Copy link
Member

This is now added to the latest release.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Status: Completed Nothing further to be done with this issue. Awaiting to be closed. Type: Enhancement Most issues will probably ask for additions or changes.
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants