├── IMDS.go └── README.md /IMDS.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "log" 7 | "net/http" 8 | "os" 9 | "os/exec" 10 | "os/user" 11 | "time" 12 | 13 | "github.com/fatih/color" 14 | ) 15 | 16 | var Tick string = ("[" + color.GreenString("+") + ("]")) 17 | var TickError string = ("[" + color.RedString("!") + ("]")) 18 | 19 | // Change these variables to whatever you want returned 20 | var accessKey string = "HoneyToken" 21 | var secretAccessKey string = "HoneyToken" 22 | var token string = "IQoJb3Jpz2cXpQRkpVX3Uf////////////xMdLZHNjbmtGZ2NhL//////////wPbEVN6UGVwIgJ7I5bAOpTzLKpWxIb7sZR74Dq9MNYW/3kThIUWKqDNCoZP+iSbXHHTZuSILnIlFfnT+QcPnlS/tOzaGPxwhuFFnhpMKVtQqgfhWtdMFUPbUPxbtIIhVqPpueagIfbsAjbRRCvrLkRylooW+JDmiqymJQzeReoiWqCnSgzyvYnsSVZRHeNANqYFq/aMqTJ/KvXlbtbzjTPNHahpZXGamgvAtniqkJqhBYGPGQaGKi+cPqqEZdYIYPzMaYjtJgNtGmBoDxkKQeKRVlEtpkAdWMjXXWYm+BnddxxrNAnBcNwrjBtSp/OpQdjvhFfahhKxyEDinpjDkkRrfWTdkmwaMmOjDBHbtUotMJPekH+KtArZuX+HsSAoNfZlwHhnvFTC+jFqgwXelfAfOhrDxlEvadqCAGLOVKLBBtQjFwrXkDmHccVVdUZZEkzQqLuYRlMWVgUpJZQroHHK/uEBiYRYKrpdkEhcWwYPRkPagFLYzdWRTnhxtHGoNNTyq/EHBOKog+rtYUH+QJ+MBYf/ALKSUzIzij/WNH/bNfkVpqPdYPMYtmfk/CBpXoDgj+VweJZJGzHdXP/zBlvqvmHaswckLfSVWtoNLspdlNJUua+JMy/QxlEeghQiNPMmixPv+Ofn/IpLsHmhFYRceGt+EcVKKayGicwSiUXPFG/JafLNLNwQjMVbMb+WGm/CMsYfnNengS/XYYh/hRXNnSQzzcmscXjouqKhzmWhc/HGc+/wNRmrtFVwhTldmFAxiqmScziGDFvxlXeoEThIqKoVBqeqLiWNBeDzjKlwfVbiyFtQfrXWFwzVvTtJ+rDjLPk+SVgapQVRwpGlAUtjEkbuLyCYqLeO/uqGhKJhMZKjNTQ/aVPXkWR/CGxTmLWuEMZQFuSWlIFqYvyyfPHWQPCWDPwnjkGkkjNrJUhfkOXNpHAnBNHYpXUMidzsggFUccMzJIuqVLGAKgUENdRxsqqJiR+FbOgpnjaKEzyqWcLjiGDxMVpIdNqyWuJniNCRqyFKLkDsCi+MejhGVMVSr" 23 | 24 | type Credentials struct { 25 | Code string `json:"Code"` 26 | Message string `json:"Message"` 27 | LastUpdated string `json:LastUpdated"` 28 | Type string `json:"Type"` 29 | AccessKeyId string `json:"AccessKeyId"` 30 | SecretAccessKey string `json:"SecretAccessKey"` 31 | Token string `json:"Token"` 32 | Expiration string `json:"Expiration"` 33 | } 34 | 35 | func isRoot() { 36 | currentUser, err := user.Current() 37 | if err != nil { 38 | log.Fatalf("[isRoot] Unable to get current user: %s", err) 39 | } 40 | if currentUser.Username != "root" { 41 | fmt.Println(TickError, color.RedString("You must run this as root to manipulate iptables.")) 42 | os.Exit(1) 43 | 44 | } 45 | } 46 | 47 | func ipTables() { 48 | 49 | // Checks if the iptables rule already exists 50 | cmd := exec.Command("iptables", "-t", "nat", "-C", "OUTPUT", "-p", "tcp", "-d", "169.254.169.254", "--dport", "80", "-j", "DNAT", "--to-destination", "127.0.0.1:54321") 51 | if err := cmd.Start(); err != nil { 52 | fmt.Println(TickError, color.RedString("Encountered error while running iptables command. Are you root? Is iptables installed?\n"), err) 53 | os.Exit(1) 54 | } 55 | 56 | // If the rule does not exist, an error code is returned 57 | if err := cmd.Wait(); err != nil { 58 | if _, ok := err.(*exec.ExitError); ok { 59 | // Since the rule does not exist, add it 60 | cmd := exec.Command("iptables", "-t", "nat", "-A", "OUTPUT", "-p", "tcp", "-d", "169.254.169.254", "--dport", "80", "-j", "DNAT", "--to-destination", "127.0.0.1:54321") 61 | _, err := cmd.Output() 62 | if err != nil { 63 | fmt.Println(err) 64 | } 65 | } 66 | } 67 | 68 | fmt.Println(Tick, color.GreenString("Running command:"), cmd) 69 | fmt.Println(Tick, color.GreenString("Run the following command to revert IP tables rules change:"), "iptables -t nat -D OUTPUT -p tcp -d 169.254.169.254 --dport 80 -j DNAT --to-destination 127.0.0.1:54321") 70 | 71 | } 72 | 73 | // Returns JSON that looks like legitimate IMDS credentials 74 | func handler(w http.ResponseWriter, r *http.Request, accessKey string, secretAccessKey string, token string) { 75 | now := time.Now().UTC() 76 | later := now.Add(time.Hour * 6) 77 | expiredTime := later.Format("2006-01-02T15:04:05Z") 78 | formattedTime := now.Format("2006-01-02T15:04:05Z") 79 | 80 | credentials := Credentials{ 81 | Code: "Success", 82 | Message: "The request was successfully processed.", 83 | LastUpdated: formattedTime, 84 | Type: "AWS-HMAC", 85 | AccessKeyId: accessKey, 86 | SecretAccessKey: secretAccessKey, 87 | Token: token, 88 | Expiration: expiredTime, 89 | } 90 | 91 | jsonBytes, err := json.MarshalIndent(credentials, "", " ") 92 | if err != nil { 93 | http.Error(w, err.Error(), http.StatusInternalServerError) 94 | return 95 | } 96 | 97 | w.Header().Set("Content-Type", "application/json") 98 | w.Write(jsonBytes) 99 | } 100 | 101 | func index(w http.ResponseWriter, r *http.Request) { 102 | text := "1.0\n2007-01-192007-03-01\n2007-08-29\n2007-10-10\n2007-12-15\n2008-02-01\n2008-09-01\n2009-04-04\n2011-01-01\n2011-05-01\n2012-01-12\n2014-02-25\n2014-11-05\n2015-10-20\n2016-04-192016-06-30\n2016-09-022018-03-28\n2018-08-172018-09-24\n2019-10-01\n2020-10-27\n2021-01-03\n2021-03-23\n2021-07-15\n2022-07-09\n2022-09-24\n" 103 | directory := []byte(text) 104 | w.Header().Set("Content-Type", "text/plain") 105 | w.Write(directory) 106 | } 107 | func latest(w http.ResponseWriter, r *http.Request) { 108 | text := "dynamic\nmeta-data\nuser-data\n" 109 | directory := []byte(text) 110 | w.Header().Set("Content-Type", "text/plain") 111 | w.Write(directory) 112 | } 113 | 114 | func metadata(w http.ResponseWriter, r *http.Request) { 115 | text := "ami-id\nami-launch-index\nami-manifest-path\nblock-device-mapping/\nevents/\nhostname\niam/\nidentity-credentials/\ninstance-action\ninstance-id\ninstance-life-cycle\ninstance-type\nlocal-hostname\nlocal-ipv4\nmac\nmetrics/\nnetwork/\nplacement/\nprofile\npublic-hostname\npublic-ipv4\npublic-keys/\nreservation-id\nsecurity-groups\nservices/\nsystem\n" 116 | directory := []byte(text) 117 | w.Header().Set("Content-Type", "text/plain") 118 | w.Write(directory) 119 | } 120 | 121 | func iam(w http.ResponseWriter, r *http.Request) { 122 | text := "info\nsecurity-credentials/\n" 123 | directory := []byte(text) 124 | w.Header().Set("Content-Type", "text/plain") 125 | w.Write(directory) 126 | } 127 | func securitycredentials(w http.ResponseWriter, r *http.Request) { 128 | text := "ec2-admin\n" 129 | directory := []byte(text) 130 | w.Header().Set("Content-Type", "text/plain") 131 | w.Write(directory) 132 | } 133 | func main() { 134 | isRoot() 135 | ipTables() 136 | http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 137 | switch r.URL.Path { 138 | case "/": 139 | index(w, r) 140 | case "/latest": 141 | latest(w, r) 142 | case "/latest/": 143 | latest(w, r) 144 | case "/latest/meta-data": 145 | metadata(w, r) 146 | case "/latest/meta-data/": 147 | metadata(w, r) 148 | case "/latest/meta-data/iam": 149 | iam(w, r) 150 | case "/latest/meta-data/iam/": 151 | iam(w, r) 152 | case "/latest/meta-data/iam/security-credentials": 153 | securitycredentials(w, r) 154 | case "/latest/meta-data/iam/security-credentials/": 155 | securitycredentials(w, r) 156 | case "/latest/meta-data/iam/security-credentials/ec2-admin": 157 | handler(w, r, accessKey, secretAccessKey, token) 158 | case "/latest/meta-data/iam/security-credentials/ec2-admin/": 159 | handler(w, r, accessKey, secretAccessKey, token) 160 | default: 161 | http.NotFound(w, r) 162 | } 163 | }) 164 | fmt.Println(Tick, color.GreenString("IMDS Service Spoofing Enabled")) 165 | http.ListenAndServe(":54321", nil) 166 | 167 | } 168 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # IMDSPOOF 2 | IMDSPOOF is a cyber deception tool that spoofs an AWS IMDS service. One way that attackers are able to escalate privileges or move laterally in a cloud environment is by retrieving AWS Access keys from the [IMDS service](https://hackingthe.cloud/aws/exploitation/ec2-metadata-ssrf/) endpoint located at `http://169.254.169.254/latest/meta-data/iam/security-credentials/`. This tool spoofs that endpoint and redirects traffic sent to `169.254.169.254` to a local webserver that serves fake data. This can be leveraged for highly tuned detections by inserting [honey AWS tokens](https://canarytokens.org/generate#) into the response of the spoofed IMDS response. 3 | 4 | ![IMDSPOOF](https://github.com/grahamhelton/IMDSpoof/assets/19278569/334b4cf0-1563-441a-8281-7c03a5acfd4f) 5 | 6 | # Who is this for? 7 | This tool is intended to be used by blue teams on AWS instances that are *NOT* actively using the IMDS (version 1 or 2). 8 | 9 | From an attacker's perspective, they have no idea if the IMDS is being used on the EC2 instance they're in. The goal of IMDSPOOF is to trick an attacker who lands in your cloud environment into thinking that they're interacting with a legitimate IMDS service. 10 | 11 | # ⚠️ Warning ⚠️ 12 | Once again, if the applications running on your EC2 instance ARE using the IMDS service, this tool WILL cause issues! 13 | 14 | # Try it out 15 | To try out IMDSPOOF in a test environment, create an EC2 instance and make sure iptables is installed (`yum install iptables-services` if using amazon linux) 16 | - Compile and run `IMDS.go` ([How To Compile Go Code](https://go.dev/doc/tutorial/compile-install)) 17 | - To test out what the output looks like, run `curl http://169.254.169.254/latest/meta-data/iam/security-credentials/ec2-admin` 18 | - You should see the following output: 19 | ```bash 20 | { 21 | "Code": "Success", 22 | "Message": "The request was successfully processed.", 23 | "LastUpdated": "2023-11-22T03:33:51Z", 24 | "Type": "AWS-HMAC", 25 | "AccessKeyId": "InsertHoneyToken", 26 | "SecretAccessKey": "InsertHoneyToken", 27 | "Token": "HoneyToken", 28 | "Expiration": "2023-11-22T09:33:51Z" 29 | } 30 | 31 | ``` 32 | - To revert changes from the previous IP tables command run the following `iptables -t nat -D OUTPUT -p tcp -d 169.254.169.254 --dport 80 -j DNAT --to-destination 127.0.0.1:54321` 33 | 34 | 35 | # Customization 36 | In order for this to be useful, you must change the variables at the top of the `IMDS.go` file to be something more real looking. I *highly* recommend placing honey tokens in the file. You can leave the `token` variable alone or change it to a custom one. 37 | 38 | - Change these variables at the top of `IMDS.go` to whatever you want returned from accessing `http://169.254.169.254/latest/meta-data/iam/security-credentials/ec2-admin` 39 | ```go 40 | var accessKey string = "InsertHoneyToken" 41 | var secretAccessKey string = "InsertHoneyToken" 42 | var token string = "IQoJb3Jpz2cXpQRkpVX3Uf////////////xMdLZHNjb" 43 | ``` 44 | 45 | # Getting HoneyTokens 46 | The folks over at [Thinkst Canary](https://canary.tools/) have a [wonderful tool](https://canarytokens.org/generate#) that allows you to easily generate AWS credentials that will generate an alert on usage. 47 | 1. Visit [CanaryTokens](https://canarytokens.org/generate#) 48 | 2. Select *AWS Keys*, enter an email for the alert to go to and a note for what the alert is for 49 | 50 | ![Pasted image 20231124171011](https://github.com/grahamhelton/IMDSpoof/assets/19278569/2f141958-2cb2-4fda-849d-b5a16cd5b1c4) 51 | 52 | 53 | 3. Note down the honeytoken provided 54 | ```bash 55 | aws_access_key_id = AKIA.... 56 | aws_secret_access_key = uZF0y/l5X.... 57 | ``` 58 | 4. Within the IMDS.go source code, replace the `accessKey` and `secretAccessKey` variable's with the `aws_access_key_id` and `aws_secret_access_key` values given to you by CanaryTokens. 59 | 60 | ![Pasted image 20231124171422](https://github.com/grahamhelton/IMDSpoof/assets/19278569/4740c850-8bad-4c5f-a682-d8bbe91924a5) 61 | 62 | 5. Compile IMDS.go. IMDS will now return the honey tokens when the IMDS is queried. 63 | 64 | ![Pasted image 20231124172134](https://github.com/grahamhelton/IMDSpoof/assets/19278569/a8dcd1a0-7ae5-4e33-b39e-8d453aca4ae5) 65 | 66 | 67 | 6. Wait for an attacker to attempt to use the credentials 68 | 69 | ![Pasted image 20231124172935](https://github.com/grahamhelton/IMDSpoof/assets/19278569/096e510f-22d5-4b52-b32a-e4a5d849b110) 70 | 71 | 72 | 8. Email alert from Canary Token 73 | 74 | ![Pasted image 20231124173123](https://github.com/grahamhelton/IMDSpoof/assets/19278569/f2a17d6d-c835-48d2-9baa-a71c329dc5a6) 75 | 76 | 77 | # Run at startup 78 | Running IMDS spoof at startup can be done easily by create a systemd service and enabling the service. 79 | 80 | 81 | - Create a file at service file `sudo vim /etc/systemd/system/IMDS.service` (You can name this something else more stealthy if you wish but the following `systemctl` commands expect the service to be named `IMDS.service`) 82 | - Add the following to the `IMDS.service` file just created 83 | 84 | ```bash 85 | [Unit] 86 | # Change this to something else if you wish 87 | Description=IMDSPOOF 88 | 89 | # After dependencies are available 90 | After=multi-user.target 91 | 92 | [Service] 93 | # Type of service 94 | Type=simple 95 | 96 | # Command to execute the service program 97 | ExecStart=/bin/IMDS 98 | 99 | # User to run the service as 100 | User=root 101 | 102 | # Restart if the service crashes 103 | Restart=always 104 | 105 | # Restart delay in seconds 106 | RestartSec=10 107 | 108 | [Install] 109 | # Enable the service at boot 110 | WantedBy=multi-user.target 111 | ``` 112 | 113 | - Make sure the IMDS binary is in the `/bin/` directory: `sudo mv IMDS /bin/` 114 | - Enable the service to start at boot `sudo systemctl enable IMDS` 115 | - Start the service now `sudo systemctl start IMDS` 116 | - Ensure everything is running correctly with `sudo systemctl status IMDS` 117 | # Does this work with SSRF? 118 | Yes! Because of the way IMDSpoof manipulates the iptables on the ec2 instance, it doesn't matter where the traffic is coming from. This means that in addition to this working via the curl utilitiy on the EC2 instance, IMDSpoof also works if an SSRF vulnerability is found through a web application hosted on the EC2 instance. Using the following [vulnerable application from AlexanderHose's blog](https://alexanderhose.com/how-to-hack-aws-instances-with-the-metadata-service-enabled/) on IMDS pentesting, we can exploit the SSRF vulnerability which will also return the fake credentials from IMDSpoof 119 | 120 | ![Pasted image 20231124180610](https://github.com/grahamhelton/IMDSpoof/assets/19278569/dfb0c4c7-9eba-45a8-a155-72fabaece401) 121 | 122 | 123 | # Reverting 124 | - To stop the systemd service: `sudo systemctl stop IMDS` 125 | - To stop the systemd service from running at startup: `sudo systemctl disable IMDS` 126 | - To revert changes IMDSPOOF makes to iptables, run the following command: `iptables -t nat -D OUTPUT -p tcp -d 169.254.169.254 --dport 80 -j DNAT --to-destination 127.0.0.1:54321` 127 | --------------------------------------------------------------------------------