AWS Security13 min read

    Pentesting With Credentials

    Tarek Cheikh

    Founder & AWS Cloud Architect

    Pentesting with credentials in AWS Security Agent: authentication resources, the LOGIN agent, and 13 findings

    My first pentest found 5 findings (2 verified), and none of them were Critical. I ran the exact same test again with one change: I provided login credentials.

    The result was 13 verified findings, 6 of them Critical. Same application, same configuration. The only variable was a username and password. That application is the same vuln-catalog target from the previous post (../code/apps/vuln-catalog), deployed by the lab-core Terraform in ../code/infra/.

    This post covers how to configure authentication for a pentest, every authentication method the agent supports, where your credentials are actually stored, what the login agent does step by step, and why providing credentials more than doubled the findings.

    Configuring Authentication Resources

    Authentication is Step 3 of the four-step pentest creation wizard:

    1. Penetration test details (required)
    2. VPC Resources (optional)
    3. Authentication Resources (optional)
    4. Additional learning resources (optional)

    Step 3 is labeled optional. By default the page is empty with a single "Add credential" button. If you skip it and click Next, the pentest runs unauthenticated. That is what I did in run-001.

    For run-002, I clicked "Add credential." The wizard created a credential card named "Credential #1" (renamable with an "Edit name" button) with two radio options:

    • Input credentials (selected by default): "Enter your credentials directly. We'll securely store them in your AWS Secrets Manager on your behalf."
    • Advanced setting: "For sensitive credential information, we recommend using the advanced options."

    With "Input credentials" selected, the form shows five fields.

    Authentication Resources form with username, password, 2FA, Access URL, and login prompt fields
    FieldWhat it doesWhat I entered
    Username (required)The login usernameadmin
    Password (required)The login password (shown as dots)admin123
    2FA / TOTP (optional)TOTP secret or QR code upload for 2FA-protected apps(empty, the app has no 2FA)
    Access URLDropdown of target URLs from Step 1, maps this credential to a specific targethttps://vuln.lab.example.com
    Agent Space login prompt (optional)Natural-language instruction telling the agent how to log in"This actor should use the following domains for authentication: https://vuln.lab.example.com/login"

    The login prompt is a free-text field. The agent reads it as plain English and figures out the login flow. You could write "POST to /login with username and password form fields" or "This app uses OAuth, start at /auth/login." It is the agent's only instruction for how to authenticate, which is critical for non-standard login flows.

    The TOTP field is worth calling out. You can paste a TOTP secret or upload a QR code image, and the agent can then handle 2FA-protected applications. Most automated scanners cannot do this.

    Below the card, an "Add another credential" button lets you add multiple credential sets. You could provide one admin account and one regular user account, or credentials mapped to different target URLs, which enables testing as different user roles (admin vs regular user vs guest). I used a single credential set.

    Step 4 (Additional learning resources) lets you attach files, GitHub repositories, or S3 links for application context. I left it empty and clicked "Create penetration" (the alternative button is "Create and execute", which starts the run immediately), then started the run manually.

    The Full Set of Supported Authentication Methods

    The two radio options in the form are the front door, but the agent supports a broader set of authentication methods for authenticated pentests (AWS docs):

    • Static credentials: a username and password typed directly into the form (the "Input credentials" path).
    • IAM role assumption: the agent assumes an AWS IAM role, for applications using Cognito, API Gateway with IAM auth, or other AWS-native authentication.
    • AWS Secrets Manager: the agent reads a secret you already own by ARN. This is also how you supply an API key (store it as plaintext and instruct the agent to set it as a header).
    • Dynamically retrieved credentials via Lambda: a Lambda function runs at test time to generate fresh credentials, and it must complete within 30 seconds.

    For two-factor auth, only TOTP is supported. AWS's docs are explicit that SMS, email, push notifications, hardware keys, and OAuth are not.

    The "Input credentials" path is the simplest: the agent stores the static credentials in Secrets Manager for you. The "Advanced setting" path is where you bring your own secret or your own Lambda.

    Advanced credential setting showing Secrets Manager ARN field with IAM permission warning

    When you use the Advanced setting with your own secret, the console shows a warning: "This secret is not pre-configured in your agent space. To use it, you must manually grant the service role read permissions for this secret in IAM." This matters: the automatic credential storage only works with "Input credentials." The Advanced setting requires you to handle IAM yourself. The pentest service role needs read permission on the secret, or invoke permission on the Lambda, depending on which method you choose.

    Where the Credentials Go: Your Own Secrets Manager

    When you use "Input credentials," the UI says "We'll securely store them in your AWS Secrets Manager on your behalf." I checked, and it does. The credentials are not stored in AWS's account or inside the Security Agent service itself. They land in Secrets Manager in your own account.

    Secret name: secagent-lab-run-002-with-credentials-secret-XXXXXXXX
    Created: 2026-03-21T01:50:14 (local time, UTC+1, matching the CreatePentest row in the article on the agent's AWS footprint; same second as pentest creation)
    Content: {"username": "admin", "password": "admin123"}

    The naming pattern is secagent-lab-{pentest-name}-secret-{random}. The secagent-lab- prefix comes from the Agent Space name. The secret lives in your AWS account, so you own it and can see it in the Secrets Manager console.

    Three things to know:

    1. The secret persists after the pentest completes. It is not automatically cleaned up. If you used real credentials, go to Secrets Manager and delete the secret when you are done.
    2. The pentest service role needs both secretsmanager:GetSecretValue and secretsmanager:DescribeSecret to read it. The credential secret name starts with your Agent Space name (secagent-lab-*), not securityagent-*, and the pentest service role has the broader access needed to read it.
    3. One secret per pentest configuration. A second pentest with different credentials creates a second secret. They do not overwrite each other.

    What the LOGIN Agent Did

    When credentials are provided, a new agent type appears in the pentest pipeline: the LOGIN agent. This is a browser-based agent that handles authentication before any testing begins. It did not exist in run-001.

    LOGIN agent action showing Succeeded in the pentest monitor

    I downloaded the CloudWatch logs for the LOGIN agent stream and reconstructed every step. Here is the full sequence with actual log excerpts.

    Step 1: Initialize a browser session.

    {
      "interaction_number": 2,
      "agent_id": "00000000-0000-0000-0000-000000000000",
      "response": {
        "content": [
          {"type": "text", "text": "I'll help you authenticate to the service using the provided credentials. Let me start by initializing a browser session and navigating to the login page."},
          {"type": "tool_use", "name": "browse",
           "input": {"browser_input": {"action": {"type": "init_session",
           "session_name": "auth-session",
           "description": "Session to authenticate to vuln.lab.example.com"}}}}
        ]
      }
    }

    Step 2: Navigate to the login URL.

    {"type": "tool_use", "name": "browse",
     "input": {"browser_input": {"action": {
       "session_name": "auth-session",
       "type": "navigate",
       "url": "https://vuln.lab.example.com/login"}}}}

    Response: "Navigated to https://vuln.lab.example.com/login"

    Step 3: Take a screenshot of the login page.

    {"type": "tool_use", "name": "browse",
     "input": {"browser_input": {"action": {
       "session_name": "auth-session", "type": "screenshot"}}}}

    The agent took a screenshot, received it as an image, and said:

    "Good! I can see the login page with username and password fields. Now let me get the HTML to identify the correct selectors for the input fields."

    Step 4: Read the HTML to find form selectors.

    <form action="/login" method="POST">
        <label>username: <input type="text" name="username"></label><br>
        <label>password: <input type="password" name="password"></label><br>
        <button type="submit">Login</button>
    </form>

    Steps 5 and 6: Type credentials into the form.

    {"type": "tool_use", "name": "browse",
     "input": {"browser_input": {"action": {
       "session_name": "auth-session",
       "type": "type",
       "selector": "input[name='username']",
       "text": "[REDACTED]"}}}}

    Response: "Typed '[REDACTED]' into input[name='username']". The same followed for the password field with selector input[name='password'].

    Step 7: Click submit.

    {"type": "tool_use", "name": "browse",
     "input": {"browser_input": {"action": {
       "selector": "button[type='submit']",
       "session_name": "auth-session",
       "type": "click"}}}}

    Response: "Clicked element: button[type='submit']"

    Step 8: Post-login screenshot and verification. The agent took another screenshot and reported what it saw:

    "Excellent! The login was successful. I can see:

    • The page now shows "Welcome, [REDACTED]"
    • There's a token displayed on the page
    • The page shows various menu items (Dashboard, View Users, Search, Update Profile, Upload File)"

    Step 9: Extract the JWT token from the page HTML. The agent called get_html again on the post-login page and found the token in a <code> element:

    Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.REDACTED_ADMIN_JWT.REDACTED_SIG

    Step 10: Get browser cookies.

    {"type": "tool_use", "name": "browse",
     "input": {"browser_input": {"action": {
       "session_name": "auth-session", "type": "get_cookies"}}}}

    Step 11: Save credentials for other agents. This is the critical step. The agent called save_credentials, a tool that stores both cookies and HTTP headers so the Crawler, attack workers, and validators can all make authenticated requests:

    {"type": "tool_use", "name": "save_credentials",
     "input": {"save_credentials_input": {
       "description": "Authentication credentials for vuln.lab.example.com...",
       "actions": [
         {"type": "save_browser_cookies",
          "session_name": "auth-session",
          "cookie_names": ["session_token"]},
         {"type": "save_headers",
          "headers": ["Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."]}
       ]}}}

    Response: "done!"

    The save_credentials tool has two action types. save_browser_cookies extracts named cookies from the browser session, and save_headers stores HTTP headers like the Authorization Bearer token. Other agents reuse both. The entire login process took about 52 seconds across 24 interactions. Two LOGIN streams appeared in run-002, meaning the agent logged in twice, likely for different phases of the test.

    The 13 Findings

    Completed pentest run-002 showing 13 findings with JWT Critical finding detail

    Here is the full results table. CVSS (Common Vulnerability Scoring System) is a 0-to-10 scale measuring how dangerous a vulnerability is, where 10.0 is maximum severity.

    #FindingSeverityCVSSConfidenceRisk TypeNew?
    1Critical Command Injection in /cmdi/obvious via host parameterCritical9.9HighCommand InjectionYES
    2Critical Blind SSRF in /ssrf/hidden Webhook EndpointCritical9.9HighSSRFYES
    3JWT 'none' Algorithm Acceptance - Complete Auth BypassCritical10.0HighJWT VulnerabilitiesYES
    4SSTI with RCE in /ssti/obvious endpointCritical9.9HighSSTIYES
    5SSRF via URL Parameter (/ssrf/obvious)Critical9.1HighSSRFYES
    6JWT Signature Verification Not Implemented - Complete Auth BypassCritical9.8HighJWT VulnerabilitiesYES
    7Critical IDOR - Unauthenticated Access to All User DataHigh7.5HighIDORYES
    8LFI Filter Bypass via Incomplete Blacklist in /lfi/obviousHigh-HighLFIYES
    9File Existence Oracle via Path Traversal in lang ParameterMedium-HighPath TraversalYES
    10Critical XXE Injection in /xxe/obvious endpointMedium--XXEYES
    11File Upload MIME Type Validation BypassMedium--Arbitrary File UploadYES
    12Path Traversal in /path-traversal/obvious endpointMedium--Path TraversalNo
    13Broken RBAC on /clean/users - Regular Users Access Admin EndpointMedium--Privilege EscalationYES

    12 of the 13 findings are new relative to run-001; only the Medium path-traversal finding also surfaced there. All 6 Criticals appeared only in the credentialed run (run-001 produced zero, so 0 to 6). Most of the new findings needed a valid session to reach at all; three of them (#1, #4, #5) sit on public endpoints that run-001's crawl simply missed, so the credentialed run's broader discovery is what surfaced those.

    Findings #10 to #13 show a dash for risk score and confidence: the agent's exported report listed them as confirmed findings but did not populate those two fields. They still count among the 13, all of which appear in the confirmed report rather than the unverified, hidden-by-default bucket that run-001's extra findings fell into. That is why I describe all 13 as verified even though four of them carry no explicit confidence value.

    One more reading note: where a finding title begins with the word "Critical" (for example #7 and #10), that is the agent's own generated name for the finding, not its assigned severity. The Severity column is the authoritative rating, which is why finding #7 is titled "Critical IDOR" but scored High, and #10 is titled "Critical XXE" but scored Medium. The six findings that are actually Critical severity are #1 through #6.

    For the full machine-readable breakdown, see the data/pentest-reports/run-002-with-credentials-summary.md file in the companion repository.

    The JWT 'none' Algorithm Attack

    Finding 3 was scored at the maximum 10.0, reflecting unauthenticated, system-wide impact (a CVSS 3.1 base score reaches 10.0 only with Scope:Changed, where compromised authentication lets the attacker affect resources beyond the vulnerable component). Here is what happened.

    The JWT attack workers received a valid token from the LOGIN agent. They then tested whether the server validates token signatures. One test: forge a token with "alg": "none" (the "none" algorithm tells the server to skip signature verification) and set arbitrary claims.

    Forged token header:  {"alg": "none", "typ": "JWT"}
    Forged token payload: {"user_id": 999, "username": "hacker", "role": "superadmin"}
    Server response:      {"message":"Welcome hacker","role":"superadmin"}

    The server accepted an unsigned token from a nonexistent user with a made-up "superadmin" role. No secret key required. The agent also tested algorithm case variations (none, None, NONE) and the server accepted all of them.

    Finding 6 confirmed a related problem: the server does not verify JWT signatures at all. You can sign a token with any secret and the server will trust it. Two Critical JWT vulnerabilities that reached confirmed-Critical status only with credentials. (Without a session the agent could still brute-force the weak signing secret and forge tokens, as it did in run-001, but it found no exploitable role-based access to confirm, so the issue stayed an unverified finding.)

    Why the Credential-Gated Findings Needed Credentials

    Nine of the new findings required authentication to discover or to exploit, each for a specific reason. They group into eight themes below, with the two JWT findings (3 and 6) sharing one. The other three new findings (#1, #4, #5) are not in this list: they sit on public endpoints that run-001 never crawled, so credentials are not what unlocked them, broader discovery is.

    JWT attacks (3, 6): What credentials unlocked here was validator-confirmed impact, not the testing itself. In run-001 the agent already probed JWT unauthenticated, brute-forcing the weak secret and forging tokens, but it found no role-based access to exploit, so the finding stayed unverified. With a valid session the LOGIN agent supplied a real token and the attack workers then modified the algorithm, claims, and signature to probe the server's validation logic, which is what turned these into confirmed Criticals.

    IDOR (7): The vulnerable user-data endpoint was only linked from authenticated pages, so the unauthenticated Crawler never found it in run-001. With credentials, the Crawler discovered it, and the agent then logged in as admin (user_id=1) and tried accessing user_id=2 and user_id=3. Both returned data. It also tested without any token and still got data. The finding documents both an authorization bypass and an authentication bypass. The bug itself would respond to an unauthenticated request; it stayed invisible in run-001 because the endpoint was never discovered, not because there was no session to compare against.

    Blind SSRF in /ssrf/hidden (2): The /ssrf/hidden endpoint requires authentication, so the Crawler could not find it in run-001. With credentials, the Crawler discovered it and the SSRF worker tested 37+ payloads against internal services, AWS IMDS metadata at 169.254.169.254, the file protocol, and IP encoding bypasses (decimal, octal, hex, IPv6). The endpoint had a 100 percent acceptance rate with zero security controls.

    Broken RBAC (13): The agent tested /clean/users (an admin-only endpoint) with different role contexts and found regular users could access it. Testing role-based access control requires having a session with a known role.

    LFI filter bypass (8): The Crawler mapped more endpoints when authenticated, discovering /lfi/obvious and testing blacklist bypass techniques the unauthenticated Crawler never reached.

    XXE injection (10): The /xxe/obvious endpoint was only discovered through authenticated crawling.

    File upload bypass (11): Upload endpoints require authentication. The agent tested MIME type spoofing, extension manipulation, and path traversal in filenames.

    Path traversal oracle (9): The lang parameter on authenticated pages was probed for path traversal, revealing a file existence oracle that leaks whether files exist on the server.

    Run-001 vs Run-002

    MetricRun-001 (no credentials)Run-002 (with credentials)
    Findings513
    Critical findings06
    JWT findings02
    IDOR findings01
    RBAC findings01
    SSRF findings02
    Duration~2.5 hours2h 38m
    CloudWatch log streams4757
    CloudWatch log size7.1 MB8.6 MB
    LOGIN agents02
    Validator tasks718
    Cleanup agents45

    The validator count jumped from 7 to 18. More findings means more validators needed to independently confirm each one. The cleanup agent count went from 4 to 5 because the agent made more modifications during testing with authenticated access.

    The agent also chained attacks across findings:

    • SSTI to database extraction: used template injection at /ssti/obvious to run sqlite3 commands and dump the users table, extracting all credentials.
    • File upload to webshell: uploaded a PHP shell via MIME type spoofing at /upload/obvious, then accessed it through path traversal.
    • JWT forge to privilege escalation: forged tokens with role "superadmin" and the server accepted them.
    • SSRF to IMDS: reached 169.254.169.254 through the SSRF endpoint, but every response came back empty. The lab instance enforced IMDSv2, which requires a PUT request and a session token header that a GET-only SSRF cannot supply, so credential theft through this specific path was blocked. The article on the agent's AWS footprint covers this in full.

    The 10-Minute Setup That Changed Everything

    Configuring Authentication Resources took about 10 minutes. Fill in a username, a password, select the target URL, write a one-sentence login prompt. That was it.

    The result was a 160 percent increase in findings. That figure compares total findings (5) to verified findings (13); the verified-to-verified jump (2 to 13) is larger still. Five vulnerabilities became thirteen. Zero JWT findings became two Critical JWT findings. Zero authorization findings became an IDOR and a broken RBAC finding. Entire categories of vulnerabilities (authentication mechanisms, authorization controls, endpoints behind login) were invisible without credentials.

    Without credentials, the agent cannot test what it cannot reach. Providing credentials is not a nice-to-have. It is the difference between a partial scan and a real pentest.

    Next up: advanced pentesting against private targets, VPC resources, and cross-account shared VPCs.

    Go Deeper: The State of AWS Security 2026

    This article is just the start. Get the full picture with our free whitepaper - 8 chapters covering IAM, S3, VPC, monitoring, agentic AI security, compliance, and a prioritized action plan with 50+ CLI commands.

    AWS Security AgentPenetration TestingJWTAuthenticationAWS Secrets ManagerAppSecIDOR

    Toc Consulting: AWS Security & Cloud Architecture

    Want expert help with AWS Security?

    Our team helps engineering teams secure and architect AWS the right way: assessment in week one, a prioritized action plan in week two.