Logo of «2Captcha»To home page
Captcha bypass tutorials

Was this helpful?

How to bypass Amazon WAF captcha on portal-nc.tylertech.cloud

Jerry Slimane
Jerry Slimane

Technical engineer

portal-nc.tylertech.cloud is the North Carolina court portal built on the Tyler Technologies platform. Public case search on it is closed off by Amazon WAF captcha.

The front page of the portal carries no check. It switches on when you move into the sections with data: at /Portal/Home/Dashboard/29, for instance, you get a "Let's confirm you are human" page with a Begin button instead of the search form.

The page issues new iv and context values on every load. Storing them and reusing them in later tasks will not work: the service solves the captcha, but the site rejects that answer. They have to be read from the page every time.

The answer from the service consists of two values. The first one goes to a function built into the page, the second one goes into a cookie. The section on the token covers what to do with each.

The captcha script addresses belong to this portal alone, and the region in them is us-gov-west-1 rather than the us-east-1 you see in the documentation examples. Addresses copied from the examples will not work here.

Task Parameters

The task type is AmazonTaskProxyless if you solve through the service proxies, or AmazonTask if you supply your own.

Amazon WAF comes in two layouts: one with a pair of scripts, challenge.js and captcha.js, and one with a single jsapi.js script. The portal carries both scripts of the first pair, so everything below refers to that layout.

Parameter Type Required Description
type String Yes AmazonTaskProxyless or AmazonTask
websiteURL String Yes Address of the page where the check appeared: https://portal-nc.tylertech.cloud/Portal/Home/Dashboard/29
websiteKey String Yes Site key. It sits in the gokuProps object in the page code
iv String Yes The iv value from the same object. It changes on every page load
context String Yes The context value from the same object. It changes on every load as well
challengeScript String No Address of the challenge.js script. The field is optional, but for this portal it is better to pass it
captchaScript String No Address of the captcha.js script. The same applies
proxyType String Yes for AmazonTask Proxy type: http, socks4 or socks5
proxyAddress String Yes for AmazonTask Proxy IP address or hostname
proxyPort Number Yes for AmazonTask Proxy port
proxyLogin String No Login for proxy authentication
proxyPassword String No Password for proxy authentication

How to find parameters on portal-nc.tylertech.cloud

All three values sit right in the code of the verification page, in an inline script tag:

html Copy
<script type="text/javascript">
window.awsWafCookieDomainList = ['tylertech.cloud'];
window.gokuProps = {
  "key": "AQIDAHjArpIF94zAGfD1jHMK+X+fGwFDxU3Rah+BaNzPreei5wEQVIS0rYSlPDG5...OLB0Q==",
  "iv": "CgAHbUDiVwAAAamI",
  "context": "rM6MrRNd31WTMQScBddAj7jpVNT0PQkTDASLN6Veten7FZUGstY0kbodqvqT...g=="
};
</script>
<script src="https://e9b10f157f38.9a96e8b4.us-gov-west-1.token.awswaf.com/e9b10f157f38/6082afcd5143/bd1ef86d885e/challenge.js"></script>
<script src="https://e9b10f157f38.9a96e8b4.us-gov-west-1.captcha.awswaf.com/e9b10f157f38/6082afcd5143/bd1ef86d885e/captcha.js"></script>

The key and the context are shortened in this example so that they fit on a line: in full, each of these values runs to several hundred characters.

The quickest way to read them is one line in the browser console:

js Copy
window.gokuProps

The script addresses do not change from load to load:

At the time of writing, the site key is this:

Copy
AQIDAHjArpIF94zAGfD1jHMK+X+fGwFDxU3Rah+BaNzPreei5wEQVIS0rYSlPDG5aZ6OHq6HAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMmzDIFX2SSuNuEabWAgEQgDth9TJhevx6o2BPR3VuiAP5Rzf16WQp5RmonvmrCkOm4SY8uVNCeqgdXkz+qd49ACet3cA9ZPrx/OLB0Q==

It is tied to the protection settings on the site's own side, so it can change at any point. The iv and context values are definitely new on every load, and the documentation asks for fresh ones on every request. The safer approach is to read all three values from the page — that way the script survives the day the site rotates its key.

Code Examples

Python + requests (API v2)

Sending the task and polling for the result. Put in fresh key, iv and context values — the ones sitting on the page at the moment you run this.

python Copy
import time

import requests

API_KEY = "YOUR_API_KEY"
PAGE_URL = "https://portal-nc.tylertech.cloud/Portal/Home/Dashboard/29"
CHALLENGE_SCRIPT = ("https://e9b10f157f38.9a96e8b4.us-gov-west-1.token.awswaf.com"
                    "/e9b10f157f38/6082afcd5143/bd1ef86d885e/challenge.js")
CAPTCHA_SCRIPT = ("https://e9b10f157f38.9a96e8b4.us-gov-west-1.captcha.awswaf.com"
                  "/e9b10f157f38/6082afcd5143/bd1ef86d885e/captcha.js")

WEBSITE_KEY = "AQIDAHjArpIF94zAGfD1jHMK...OLB0Q=="
IV = "CgAHbUDiVwAAAamI"
CONTEXT = "rM6MrRNd31WTMQScBddAj7jpVNT0PQkTDASLN6Veten7...g=="

task = {
    "type": "AmazonTaskProxyless",
    "websiteURL": PAGE_URL,
    "websiteKey": WEBSITE_KEY,
    "iv": IV,
    "context": CONTEXT,
    "challengeScript": CHALLENGE_SCRIPT,
    "captchaScript": CAPTCHA_SCRIPT,
}

created = requests.post(
    "https://api.2captcha.com/createTask",
    json={"clientKey": API_KEY, "task": task},
    timeout=30,
).json()

if created.get("errorId") != 0:
    raise RuntimeError(f"Task was not created: {created}")

task_id = created["taskId"]
print("Task created:", task_id)

for _ in range(24):
    time.sleep(5)
    result = requests.post(
        "https://api.2captcha.com/getTaskResult",
        json={"clientKey": API_KEY, "taskId": task_id},
        timeout=30,
    ).json()

    if result.get("status") == "ready":
        voucher = result["solution"]["captcha_voucher"]
        existing_token = result["solution"]["existing_token"]
        print("Voucher received:", voucher[:20], "…")
        break

    if result.get("errorId") != 0:
        raise RuntimeError(f"Error: {result}")
else:
    raise RuntimeError("No solution arrived within the time limit")

Python + Playwright

The script opens the verification page, reads the parameters from the gokuProps object, sends the task and passes the voucher back to the page.

python Copy
import os
import time

import requests
from playwright.sync_api import sync_playwright

API_KEY = os.getenv("APIKEY_2CAPTCHA", "YOUR_API_KEY")
PAGE_URL = "https://portal-nc.tylertech.cloud/Portal/Home/Dashboard/29"


def solve(task):
    created = requests.post(
        "https://api.2captcha.com/createTask",
        json={"clientKey": API_KEY, "task": task},
        timeout=30,
    ).json()

    if created.get("errorId") != 0:
        raise RuntimeError(f"Task was not created: {created}")

    task_id = created["taskId"]

    for _ in range(24):
        time.sleep(5)
        result = requests.post(
            "https://api.2captcha.com/getTaskResult",
            json={"clientKey": API_KEY, "taskId": task_id},
            timeout=30,
        ).json()

        if result.get("status") == "ready":
            return result["solution"]

        if result.get("errorId") != 0:
            raise RuntimeError(f"Error: {result}")

    raise RuntimeError("No solution arrived within the time limit")


with sync_playwright() as p:
    browser = p.chromium.launch(headless=False)
    page = browser.new_page()

    try:
        page.goto(PAGE_URL)
        page.wait_for_function("window.gokuProps !== undefined", timeout=30000)

        params = page.evaluate("""() => {
            const scripts = [...document.querySelectorAll('script[src]')].map(s => s.src);
            return {
                key: window.gokuProps.key,
                iv: window.gokuProps.iv,
                context: window.gokuProps.context,
                challenge: scripts.find(s => s.includes('challenge.js')),
                captcha: scripts.find(s => s.includes('captcha.js')),
            };
        }""")

        solution = solve({
            "type": "AmazonTaskProxyless",
            "websiteURL": PAGE_URL,
            "websiteKey": params["key"],
            "iv": params["iv"],
            "context": params["context"],
            "challengeScript": params["challenge"],
            "captchaScript": params["captcha"],
        })

        voucher = solution["captcha_voucher"]
        print("Voucher received:", voucher[:20], "…")

        page.evaluate("""async (voucher) => {
            if (!window.ChallengeScript
                || typeof window.ChallengeScript.submitCaptcha !== 'function') {
                throw new Error('ChallengeScript.submitCaptcha was not found on the page');
            }
            await window.ChallengeScript.submitCaptcha(voucher);
        }""", voucher)

        page.reload()
        page.wait_for_load_state("networkidle")
        print("Page title after the check:", page.title())

    except Exception as e:
        print(f"Error: {e}")
    finally:
        browser.close()

If the page title still reads Human Verification after the reload, the check was not accepted. The table below covers the reasons.

If you prefer the Python SDK, the method is called amazon_waf and its signature is solver.amazon_waf(sitekey, iv, context, url, challenge_script=..., captcha_script=...). The SDK puts the service answer into a single result["code"] string.

How to use the received token on portal-nc.tylertech.cloud

The service returns two values:

json Copy
{
  "captcha_voucher": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...",
  "existing_token": "c06440ad-3f91-4e24-a13e-0aee760ececf:FgoAdy2S0xUFAAAA:eF1Y43dKAaLQ..."
}

captcha_voucher is the answer to the check itself. The page expects it in a function of its own. That code sits right on the page:

js Copy
window.addEventListener("load", function() {
  const container = document.querySelector("#captcha-container");
  CaptchaScript.renderCaptcha(container, async (voucher) => {
      await ChallengeScript.submitCaptcha(voucher);
      window.location.reload(true);
  });
});

When a person passes the check, the captcha widget itself hands the voucher to that function. A script has no widget, so call submitCaptcha yourself and give it the voucher from the service.

python Copy
page.evaluate("""async (voucher) => {
    if (!window.ChallengeScript
        || typeof window.ChallengeScript.submitCaptcha !== 'function') {
        throw new Error('ChallengeScript.submitCaptcha was not found on the page');
    }
    await window.ChallengeScript.submitCaptcha(voucher);
}""", voucher)
page.reload()

The reload is required: the call on its own only records the result of the check, and the page content refreshes only afterwards.

existing_token is a ready value for the aws-waf-token cookie. Its shape matches exactly what the portal sets on its own: a 36-character identifier, a 16-character service part and a base64 string, all separated by colons. This is the value to use when you work with plain requests rather than a browser — put it into the cookie and address the portal directly.

The cookie domain is set by the portal itself, in the awsWafCookieDomainList variable on the page: tylertech.cloud.

One note on lifetime: the voucher is a signed token with its own expiry inside, and you have only a few minutes to use it. Do not stockpile tasks, apply the answer right away.

Common Errors and Solutions

Error / Problem Cause Solution
The answer arrives, but the page shows the check again Stale iv and context were used Read them from the page on every run: they are new on every load
The task is created, but the answer is not accepted The root of the domain was passed in websiteURL Pass the address of the page where the check actually appeared, for example /Portal/Home/Dashboard/29
Error "ChallengeScript.submitCaptcha was not found on the page" The captcha scripts have not loaded yet, or the page is no longer a verification page Wait for window.gokuProps to appear and only then call the function
The voucher went through, but the page content did not change The reload was skipped Reload the page after submitCaptcha
The check passes, but the next request runs into it again The aws-waf-token cookie is not kept between requests Work in one browser context or carry the cookie over yourself
Script addresses were taken from the documentation example The example uses region us-east-1, the portal uses us-gov-west-1 Take the challenge.js and captcha.js addresses from the portal page
The answer arrives but the site rejects it Too much time passed between receiving the voucher and using it Apply the answer immediately after receiving it
The cookie is not accepted when working with requests The value was taken from captcha_voucher The cookie takes existing_token, not the voucher