{
	"id": "1712ea5d-4ee8-4981-880f-f73d5d339054",
	"created_at": "2026-04-06T00:12:22.325079Z",
	"updated_at": "2026-04-10T13:11:36.034683Z",
	"deleted_at": null,
	"sha1_hash": "7c095d6394414351dc72168eef29b792003f28b2",
	"title": "Backdooring an AWS account",
	"llm_title": "",
	"authors": "",
	"file_creation_date": "0001-01-01T00:00:00Z",
	"file_modification_date": "0001-01-01T00:00:00Z",
	"file_size": 654456,
	"plain_text": "Backdooring an AWS account\r\nBy Daniel Grzelak\r\nPublished: 2021-06-08 · Archived: 2026-04-05 22:01:35 UTC\r\n8 min read\r\nJul 9, 2016\r\nSo you’ve pwned an AWS account — congratulations — now what? You’re eager to get to the data theft, amirite?\r\nNot so fast whipper snapper, have you disrupted logging? Do you know what you have? Sweet! Time to get\r\nsettled in.\r\nMaintaining persistence in AWS is only limited by your imagination but there are few obvious and oft used\r\ntechniques everyone should know and watch for.\r\nNo one wants to get locked out before mid hack so grab yourself some temporary credentials.\r\naws sts get-session-token --duration-seconds 129600\r\nAcceptable durations for IAM user sessions range from 900 seconds (15 minutes) to 129600 seconds\r\n(36 hours), with 43200 seconds (12 hours) as the default. Sessions for AWS account owners are\r\nrestricted to a maximum of 3600 seconds (one hour). If the duration is longer than one hour, the session\r\nfor AWS account owners defaults to one hour.\r\nYou’ll want to setup a cron job to do this regularly from here on out. It might sound crazy, but it ain’t no lie. Baby,\r\nbye, bye, bye (Sorry got distracted). A sensible person might assume that deleting a compromised access key is a\r\nreasonable way to expunge an attacker. Alas, disabling or deleting the original access key does not kill any\r\ntemporary credentials created with the original. So if you find yourself ousted, you may still get somewhere\r\nbetween 0 and 36 hours to recover.\r\nThere are some limitations:\r\nYou cannot call any IAM APIs unless MFA authentication information is included in the request.\r\nYou cannot call any STS API except assume-role.\r\nThat does create an annoyance but an annoyance that’s trivially overcome. Assuming another role is an API call\r\naway. Spinning up compute running under another execution role or instance profile, that can call IAM, is almost\r\nas easy.\r\nhttps://medium.com/daniel-grzelak/backdooring-an-aws-account-da007d36f8f9\r\nPage 1 of 6\n\nThe best (worst?) part however, is that temporary session keys don’t show up anywhere. Checking the web\r\ninterface or running “aws iam list-access-keys” is ineffective. There’s no “list-session-tokens” or “delete-session-token” to go along with “get-session-token”. There have been more sitings of the Loch Ness Monster in the wild\r\nthan AWS session tokens.\r\nThis is the entire STS API at time of writing.\r\nPress enter or click to view image in full size\r\nI really do hope Amazon does something about this soon. Having someone use the force instead of the API within\r\nthe accounts I’m responsible for genuinely scares me.\r\nNow that you have insurance, it’s time to burrow in. If being loud and drunk is your cup of Malört, you could just\r\ncreate a new user and access key. Make it look like an existing user, kind of like typo-squatting, and you’ll have\r\nyourself a genuine lying-dormant cyber pathogen.\r\nBusting out a new user and key takes two one-liners. Some might call it a two-liner but I’m not into that kind of\r\nthing.\r\naws iam create-user --user-name [my-user]\r\naws iam create-access-key --user-name [my-user]\r\nIn response, you’ll receive an access key ID and a secret access key, which you’ll want to take note of.\r\n{\r\n \"AccessKey\": {\r\n \"UserName\": \"[my-user]\",\r\n \"Status\": \"Active\",\r\n \"SecretAccessKey\": \"hunter2\",\r\n \"AccessKeyId\": \"ABCDEFGHIJKLMNOPQRST\"\r\n }\r\n}\r\nThat approach is nice but it’s not the kind of persistent persistence you want. Should the user or access key get\r\ndiscovered, it will take half the API calls to kill them that it did to create them. You’ll be left with only stories\r\nhttps://medium.com/daniel-grzelak/backdooring-an-aws-account-da007d36f8f9\r\nPage 2 of 6\n\nabout how you used to hack things when you were young. I’ll be waiting for you there with my cup of washed-up\r\nsadness.\r\nInstead of creating a new account, it’s more effective to create a new access key for every user in bulk. Bonus\r\npoints to those who acquire temporary session tokens at the same time.\r\nThe code to do it is straightforward. Even a manager (like me) can write it.\r\nThe error handling is somewhat important here as the default key limit per user is two and you will bump up\r\nagainst it semi regularly. Additionally, all access keys have visible creation timestamps which make them easy to\r\nspot during a review. Another limitation is that federated (SAML authenticated) users won’t be affected as they\r\nintegrate with roles rather than user accounts.\r\nAt this point any good auditor would claim that this was merely a point-in-time activity, leaving potentially risky\r\ncompliance gaps when new accounts are created in the future. Alas feisty auditors, there is a solution!\r\nJust create a Lambda function that reacts to user creations via a CloudWatch Event Rule and automagically adds a\r\ndisaster recovery access key and posts it to a PCI-DSS compliant location of your choosing.\r\nAWS Lambda is a server-less compute thingy (only precise technical terms allowed) that runs a function\r\nimmediately in response to events and automatically manages the underlying infrastructure. CloudWatch Event\r\nRules are a mechanism for notifying other AWS services of state changes in resources in near real time. They have\r\na very natural relationship as CloudWatch provides the sub-system for monitoring AWS API calls and invoking\r\nLambda functions that execute self-contained business logic.\r\nThe API calls and deployment packaging required to setup a Lambda function are a bit convoluted but well\r\ndocumented. You can plough through manually and gain valuable plough experience or use a framework like\r\nServerless to avoid unnecessary wear on your delicate hands. Just ensure the function’s execute role has the\r\n“iam:CreateAccessKey” permission.\r\nUsers are so 90s though! Like the Backstreet Boys. Not like Michael Bolten. He’s timeless. I mean, how am I\r\nsupposed to live without him? Now that I’ve been lovin’ him so long.\r\nGet Daniel Grzelak’s stories in your inbox\r\nJoin Medium for free to get updates from this writer.\r\nRemember me for faster sign in\r\nThe AWS recommended ISO* compliant method for escalating privileges is to use the STS assume role API call.\r\nAmazon describes it so perfectly, I would be robbing you by not quoting it directly.\r\nFor cross-account access, imagine that you own multiple accounts and need to access resources in each\r\naccount. You could create long-term credentials in each account to access those resources. However,\r\nmanaging all those credentials and remembering which one can access which account can be time\r\nconsuming. Instead, you can create one set of long-term credentials in one account and then use\r\ntemporary security credentials to access all the other accounts by assuming roles in those accounts.\r\nhttps://medium.com/daniel-grzelak/backdooring-an-aws-account-da007d36f8f9\r\nPage 3 of 6\n\nSold! First, create the role.\r\naws iam create-role \\\r\n --role-name [my-role] \\\r\n --assume-role-policy-document [file://assume-role-policy.json]\r\nThe assume role policy document must include the ARN of the users, roles or accounts that will be accessing the\r\nbackdoored role. It’s best to specify “[account-id]:root”, which acts as a wild card for all users and roles in a given\r\naccount.\r\n{\r\n \"Version\": \"2012-10-17\",\r\n \"Statement\": [\r\n {\r\n \"Effect\": \"Allow\",\r\n \"Principal\": {\r\n \"AWS\": \"arn:aws:iam::[account-id]:root\"\r\n },\r\n \"Action\": \"sts:AssumeRole\"\r\n }\r\n ]\r\n}\r\nThen attach a policy to the backdoored role describing the actions it can perform. All of them, IMHO. The pre-canned “AdministratorAccess” policy works a treat as it is analogous to root.\r\naws iam attach-role-policy \\\r\n --policy-arn arn:aws:iam::aws:policy/AdministratorAccess \\\r\n --role-name [my-role]\r\nThere you have it, a freshly minted role to assume from your other pwned accounts without the hassle of all of\r\nmanaging those pesky extra credentials.\r\nWhile elegant, this approach does have its disadvantages. At some point in the chain of role assumptions, access\r\ncredentials are required. In the event those credentials or pwned accounts are discovered and purged, your access\r\nwill die with them.\r\nAs before, it’s more effective to backdoor the existing roles in an account than create new ones. The code is\r\ntrickier this time because it requires massaging of existing assume role policies and their structural edge cases.\r\nI’ve tried to comment them fully in the code below but edge cases may have been missed.\r\nWhile adding adding access keys to a user leaves a trail of recent creation timestamps, by default there is no easy\r\nway to identify which part of a policy has been modified. Defenders may be able to identify that a policy has been\r\nhttps://medium.com/daniel-grzelak/backdooring-an-aws-account-da007d36f8f9\r\nPage 4 of 6\n\nchanged, but without external record keeping of previous policy versions, they will be left to comb through each\r\npolicy to look for bad account trusts. This is made more difficult through randomisation of source account ARNs.\r\nFinally, to future proof it all, create a Lambda function that responds to role creations via a CloudWatch Event\r\nRule. As with the access key example, the below code posts the backdoored ARN to a location of your choosing.\r\nYou may also want to send the role’s permissions and source ARN.\r\nIf you were less lazy than me, you could make the code react to UpdateAssumeRolePolicy calls and reintroduce\r\nbackdoors that are removed.\r\nSometimes you’ll want to maintain access to live resources rather than the AWS API. For those situations there’s\r\none other basic access persistence tactic worth discussing in an introductory piece, security groups. Security\r\ngroups tend to get in the way of such things; SSH and database ports aren’t typically accessible to the Internet.\r\nA security group acts as a virtual firewall for your instance to control inbound and outbound traffic.\r\nWhen you launch an instance in a VPC, you can assign the instance to up to five security groups.\r\nSecurity groups act at the instance level, not the subnet level. Therefore, each instance in a subnet in\r\nyour VPC could be assigned to a different set of security groups.\r\nIn practice, “instances” is broader than just EC2. Security groups could be applied to Lambda functions, RDS\r\ndatabases, and other resources that support VPCs.\r\nBy now you know the drill. Creating a new security group or rule and applying it to one or two resources is okay\r\nbut let’s skip that step and just do all of them. Shockingly (can I be shocked by own set definitions?), “all of them”\r\nincludes the default security group. This is important because if a resource does not have a security group\r\nassociated with it, the default security group is implicitly associated.\r\nSome older accounts still have services running “EC2 Classic” mode, which means that modifying only EC2\r\nsecurity groups is not sufficient. Back in the day RDS, ElastiCache, and Redshift had their own implementations\r\nof security groups. Their relevant authorise functions would need to be called to get full security group coverage:\r\nauthorize_db_security_group_ingress\r\nauthorize_cache_security_group_ingress\r\nauthorize_cluster_security_group_ingress\r\nThis approach has been phased out. In fact, accounts created after 4th December 2013 cannot use EC2 Classic at\r\nall.\r\nFinally, complete the circle of life with a Lambda function that executes when create security group CloudWatch\r\nEvent Rules are fired.\r\nThe extra access rules are pretty easy to spot just by eyeballing the security group. However, the workflow for\r\ncreating a security group via the web console involves defining all the rules prior to actually calling the API.\r\nConsequently, unless someone returns to refine a security group, they are unlikely to notice the extra line item.\r\nBetween this and the other tactics, you should be well untruly entrenched in a pwned AWS account. You might not\r\nbe a devil worm but you are certainly a wombat. An AWS WOMBAT!\r\nhttps://medium.com/daniel-grzelak/backdooring-an-aws-account-da007d36f8f9\r\nPage 5 of 6\n\nIt is obvious that information could be used for good and evil. I used it to strengthen the security posture of\r\naccounts I am responsible for and make detection processes testable. Professional penetration testers will use it to\r\nmimic real world attackers in their engagements. Please do the same. Don’t be evil.\r\nWhatever your choice, none of this is unattainable to even the scriptiest (anyone know why there’s a red underline\r\nunder that word? hmmm) of script kiddies. It’s better for everyone to have access to the knowledge then just the\r\nbad guys.\r\n—\r\nWant to learn to hack AWS? I offer immersive online and in-person training to corporate teams at hackaws.cloud\r\nSource: https://medium.com/daniel-grzelak/backdooring-an-aws-account-da007d36f8f9\r\nhttps://medium.com/daniel-grzelak/backdooring-an-aws-account-da007d36f8f9\r\nPage 6 of 6",
	"extraction_quality": 1,
	"language": "EN",
	"sources": [
		"MITRE"
	],
	"origins": [
		"web"
	],
	"references": [
		"https://medium.com/daniel-grzelak/backdooring-an-aws-account-da007d36f8f9"
	],
	"report_names": [
		"backdooring-an-aws-account-da007d36f8f9"
	],
	"threat_actors": [
		{
			"id": "eb3f4e4d-2573-494d-9739-1be5141cf7b2",
			"created_at": "2022-10-25T16:07:24.471018Z",
			"updated_at": "2026-04-10T02:00:05.002374Z",
			"deleted_at": null,
			"main_name": "Cron",
			"aliases": [],
			"source_name": "ETDA:Cron",
			"tools": [
				"Catelites",
				"Catelites Bot",
				"CronBot",
				"TinyZBot"
			],
			"source_id": "ETDA",
			"reports": null
		}
	],
	"ts_created_at": 1775434342,
	"ts_updated_at": 1775826696,
	"ts_creation_date": 0,
	"ts_modification_date": 0,
	"files": {
		"pdf": "https://archive.orkl.eu/7c095d6394414351dc72168eef29b792003f28b2.pdf",
		"text": "https://archive.orkl.eu/7c095d6394414351dc72168eef29b792003f28b2.txt",
		"img": "https://archive.orkl.eu/7c095d6394414351dc72168eef29b792003f28b2.jpg"
	}
}