Building a Distributed Fuzzer with AWS Lambda Self-Invocation
Hook
A single AWS Lambda function can recursively spawn hundreds of copies of itself in seconds, creating a distributed fuzzing army that rivals botnets—all without managing a single server.
Context
Traditional web application security testing hits a fundamental bottleneck: concurrency. Tools like Burp Suite's Intruder module are powerful, but they're limited by your local machine's resources or the capacity of a single VPS. Want to test 10,000 payload variations against an endpoint? You're looking at sequential requests or modest parallelization that takes hours.
The conventional solution involves expensive infrastructure: spinning up dozens of EC2 instances, managing a cluster, dealing with IP rotation, and paying for idle time. Security professionals and bug bounty hunters needed a better way to achieve massive parallelization without the operational overhead. Lambda Intruder emerged as a clever exploitation of AWS Lambda's architecture—specifically, its ability to invoke itself recursively and scale to account-level concurrency limits (typically 1,000+ simultaneous executions). This creates a fan-out pattern where one Lambda spawns many, each processing a subset of payloads, achieving query-per-second rates that would otherwise require significant infrastructure investment.
Technical Insight
The genius of Lambda Intruder lies in its recursive fan-out pattern. When you invoke the Lambda function with a list of payloads, it doesn't process them sequentially. Instead, it splits the payload array into chunks and invokes new Lambda instances for each chunk, which can themselves split and invoke further instances. This creates an exponential scaling effect limited only by AWS's concurrency throttles.
Here's the core invocation pattern from the repository:
const AWS = require('aws-sdk');
const lambda = new AWS.Lambda();
const https = require('https');
exports.handler = async (event) => {
const { target, payloads, fanout } = event;
// Base case: if payload list is small enough, process directly
if (payloads.length <= fanout) {
const results = await Promise.all(
payloads.map(payload => makeRequest(target, payload))
);
return { success: true, results };
}
// Recursive case: split payloads and invoke child Lambdas
const chunkSize = Math.ceil(payloads.length / fanout);
const chunks = [];
for (let i = 0; i < payloads.length; i += chunkSize) {
chunks.push(payloads.slice(i, i + chunkSize));
}
const invocations = chunks.map(chunk => {
return lambda.invoke({
FunctionName: process.env.AWS_LAMBDA_FUNCTION_NAME,
InvocationType: 'Event', // Async invocation
Payload: JSON.stringify({
target,
payloads: chunk,
fanout
})
}).promise();
});
await Promise.all(invocations);
return { success: true, invoked: chunks.length };
};
function makeRequest(target, payload) {
return new Promise((resolve, reject) => {
const url = target.replace('FUZZ', payload);
https.get(url, (res) => {
let body = '';
res.on('data', chunk => body += chunk);
res.on('end', () => resolve({
payload,
status: res.statusCode,
length: body.length
}));
}).on('error', reject);
});
}
The key architectural decision is using InvocationType: 'Event' for asynchronous invocation. This means the parent Lambda doesn't wait for children to complete—it fires them off and terminates. This prevents timeout issues and maximizes parallelization, though it complicates result collection (you'll need to write results to S3 or a database).
The fanout parameter controls the branching factor. A fanout of 10 means each Lambda spawns 10 children. With 1,000 payloads and fanout of 10, you get 10 Lambdas in the first generation, each handling 100 payloads. If those 100 are still too many (above the fanout threshold), each spawns 10 more, giving you 100 Lambdas in generation two, each processing 10 payloads. This recursive pattern continues until chunks are small enough to process directly.
The beauty is in the speed: all generations happen nearly simultaneously due to asynchronous invocation. What would take hours sequentially happens in seconds. The practical QPS depends on your Lambda concurrency limits and the target's ability to handle load. With 1,000 concurrent Lambdas each making requests, you can theoretically sustain thousands of requests per second.
Deployment uses the Serverless Framework with a simple configuration:
service: lambda-intruder
provider:
name: aws
runtime: nodejs14.x
timeout: 900
memorySize: 256
iamRoleStatements:
- Effect: Allow
Action:
- lambda:InvokeFunction
Resource: "*"
functions:
intruder:
handler: handler.handler
The critical IAM permission is lambda:InvokeFunction, which allows the function to invoke itself. Without this, the recursive pattern fails. The 15-minute timeout (900 seconds) gives the function ample time to orchestrate child invocations, though actual processing happens asynchronously.
One architectural consideration often overlooked: Lambda's execution context reuse. AWS may reuse the same container for multiple invocations, meaning the AWS SDK client can be instantiated outside the handler for connection pooling. This micro-optimization reduces cold start overhead in recursive scenarios where the same Lambda instance might be invoked multiple times as it processes different payload chunks.
Gotcha
The most dangerous gotcha isn't technical—it's legal and financial. Lambda Intruder can easily violate AWS's Acceptable Use Policy if pointed at targets you don't own. Distributed denial-of-service attacks, even unintentional ones from testing, can result in account termination and legal liability. Always have explicit written authorization before using this tool.
Financially, recursive Lambda invocations can spiral out of control. A misconfigured fanout parameter or missing base case creates an invocation bomb that racks up costs rapidly. AWS Lambda charges per invocation and per compute time. With 1,000 payloads, fanout of 10, and two levels of recursion, you're looking at 111 invocations (1 parent + 10 children + 100 grandchildren). Scale that to 100,000 payloads and the math gets expensive quickly. Lambda costs $0.20 per million requests, but compute time ($0.0000166667 per GB-second) adds up with hundreds of concurrent executions. Always set billing alarms and use AWS Budgets to cap spending.
Technically, result aggregation is the tool's Achilles heel. Asynchronous invocations mean you can't return results to the caller—they're fire-and-forget. You must implement your own logging strategy, typically writing to CloudWatch Logs, S3, or DynamoDB. Parsing and analyzing results becomes a separate workflow. The repository doesn't include result collection mechanisms, leaving this critical functionality as an exercise for the user. Additionally, Lambda's 15-minute maximum execution time means extremely large payload sets need careful chunking to avoid timeouts in the orchestration layer, even with async invocations.
Verdict
Use if: You need to perform authorized, high-volume security testing against your own infrastructure or have explicit permission for penetration testing. This tool shines for one-off assessments where you need massive parallelization without infrastructure overhead—perfect for bug bounty hunters testing their own staging environments or security teams running authorized fuzzing campaigns. It's also valuable for learning about serverless architectures and distributed systems patterns. Skip if: You lack written authorization for testing, need sophisticated features like session handling or complex payload mutations, require built-in result analysis and reporting, or are uncomfortable with AWS billing complexity. Commercial tools like Burp Suite Pro offer legal protection, better controls, and professional reporting that justify their cost for production security work. Also skip if you're testing third-party systems—the legal and ethical risks far outweigh any technical benefits.