IDS Bypass contest at PHDays: writeup and solutions
7/5/2019
Positive Hack Days 2019 included our first-ever IDS Bypass competition. Participants had to study a network segment of five hosts, and then either exploit a service vulnerability or meet a particular criterion (for example, send a certain HTTP response) in order to get a flag. Finding an exploit was easy, but the IDS complicated things as it stood between the participants and the hosts, checking every network packet. When a signature blocked the connection, participants were informed via the dashboard. Here are details on the tasks and the ways to solve them.
100.64.0.11 — Struts
More participants solved the Struts task than any of the others. After a port scan with Nmap, you find Apache Struts on port 8080.
nmap -Pn -sV -p1-10000 100.64.0.11
631/tcp open ipp CUPS 2.1 8005/tcp open mxi? 8009/tcp open ajp13 Apache Jserv (Protocol v1.3) 8080/tcp open http Apache Tomcat/Coyote JSP engine 1.1
Using an Apache Struts vulnerability from 2017, an attacker could perform OGNL injection to obtain remote code execution (RCE). An exploit is available (such as on GitHub) but the IDS easily detects it:
[Drop] [] [1:1001:1] Apache Struts2 OGNL inj in header (CVE-2017-5638) []
The code of the signature is not available to participants. But the log messages make clear how it works. In this case, the signature detected OGNL injection into HTTP:
GET /showcase.action HTTP/1.1 Accept-Encoding: identity Host: 100.64.0.11:8080 Content-Type: %{(#_='multipart/form-data')...
Studying the behavior of the IDS, it becomes clear that the IDS is reacting to the combination %{ in the beginning of the Content-Type header. There are several ways around this:
- @empty_jack tried separating the %{ symbols using his own dictionary for fuzzing, arriving at a solution with the string Content-Type: %${.
- Fuzzing the HTTP request itself. @c00lhax0r found that a null symbol in the beginning of the header will also slip past the IDS: Content-Type: \0${.
- Most exploits for CVE-2017-5638 inject with a percent sign. However, some researchers studying this and previous Apache Struts vulnerabilities claim that injection can just as easily start with $. Therefore the combination ${ will bypass the IDS signature and execute code on the system. This was the solution we originally had in mind.
This task was the easiest one: eight participants found a solution.
100.64.0.10 — Solr
Port 8983 hosted an Apache Solr server (written in Java).
$ nmap -Pn -sV -p1-10000 100.64.0.10 22/tcp open ssh (protocol 2.0) 8983/tcp open http Jetty
Finding an exploit for Apache Solr 5.3.0 is easy: CVE-2019-0192. An attacker can spoof the address of the RMI server in a collection. Exploitation requires the ysoserial framework, which generates chains of Java objects (gadgets) and delivers them in various ways. For instance, from a JRMP server.
Of course, going ahead and using the exploit without finessing it first will just trigger the IDS:
[Drop] [] [1:10002700:3001] ATTACK [PTsecurity] Java Object Deserialization RCE POP Chain (ysoserial Jdk7u21) []
Jdk7u21 is just one of 30 possible payloads. The choice of payloads depends on the libraries used in the vulnerable service. The Jdk7u21 gadget chain uses only standard classes from Java Development Kit (JDK) version 7u21, while the CommonsCollections1 chain contains classes from the widely used Apache Commons Collections 3.1.
An attacker can replace the RMI server address in a Solr collection with a different one and then launch the JRMP server. Solr requests an object from the attacker-indicated address and receives a malicious Java object. After the object is deserialized, its code is executed on the server.
The signature is triggered by the sequence of classes in the serialized Java object. Sent from the attacker's computer, here is how the object starts in the traffic:
The solution to this task was simple. The signature explicitly names Jdk7u21. To bypass the signature, you had to try other gadget chains. One from CommonsCollections, say. The IDS had no signatures for other chains. The participant would then get a shell on the system and read the flag. Five participants succeeded in completing this task.
100.64.0.12 — SAMR
This was one of the trickiest and most interesting tasks. The target is a Windows computer with open port 445. The flag is split into two usernames, so completing the task required enumerating all Windows users.
Naturally, MS17-010 and other exploits did not work on this computer. The list of users could be obtained with scripts, such as those from Nmap or Impacket:
$ python samrdump.py 100.64.0.12 Impacket v0.9.15 - Copyright 2002-2016 Core Security Technologies
[] Retrieving endpoint list from 100.64.0.12 [] Trying protocol 445/SMB… Found domain(s): . SAMR . Builtin [] Looking up users in domain SAMR [-] The NETBIOS connection with the remote host timed out. [] No entries received.
Both scripts send DCERPC requests to the computer on port 445. But things weren't so simple. Some packets are blocked by the IDS, triggering not just one, but two signatures:
[] [1:2001:2] SAMR DCERPC Bind [] [Drop] [] [1:2002:2] SAMR EnumDomainUsers Request []
The first signature detects the connection to SAMR and flags the TCP connection. The second signature is triggered by the SAMR EnumDomainUsers request. SAMR provides other ways to get the list of users: QueryDisplayInfo, QueryDisplayInfo2, and QueryDisplayInfo3. All these, too, were blocked by signatures.
The DCERPC protocol and Windows services contain a large number of remote administration features. Most of the well-known tools, such as PsExec and BloodHound, use DCERPC. SAMR ("SAM Remote Protocol") allows working with accounts on a host, including enumeration of the user list.
To make an EnumDomainUsers request, here's what Impacket does:
A DCERPC connection to SAMR is established over SMB, and all subsequent requests are sent in the SAMR context. Signatures are triggered by the first and last packets in the screenshot.
In the contest, there were two clues given for this task:
- Your attempts cause IDS generate 2 alerts. Look closely at the first.
- Which connection commands for this protocol do you know?
The idea was to start thinking about DCERPC and different connection methods. In the list of available PDUs for connecting and changing context, we find the Bind and Alter Context commands. Alter Context allows changing the current context without interrupting the connection.
To get a solution, you needed to rework the samrdump script:
- Bind to a different service, such as with UUID 3919286a-b10c-11d0-9ba8-00c04fd92ef5.
- Use Alter Context to switch to SAMR.
- Make an EnumDomainUsers request.
All the changes fit in just three lines:
< dce.bind(samr.MSRPC_UUID_SAMR)
dce.bind(uuid.uuidtup_to_bin(("3919286a-b10c-11d0-9ba8-00c04fd92ef5", "0.0"))) dce.alter_ctx(samr.MSRPC_UUID_SAMR) dce._ctx = 1
There's also another solution proposed by contest winner @psih1337. EnumDomainUsers returned a list of users sorted by SID (Security ID) rather than by name. But the SID is not a random number. For instance, the SID of the LocalSystem account is S-1-5-18. For groups or users created manually, the SID is 1000 or greater.
So if you manually bruteforce SIDs between 1000 and 2000, you are very likely to find the accounts you're looking for. In our case, the SIDs were 1008 and 1009.
This task required an understanding of the DCERPC protocol and some experience in surveying Windows infrastructure. @psih1337 was the only person to solve this task.
100.64.0.13 — DNSCAT
Port 80 hosts a web page with a form for entering an IP address.
If you type in your own IP address, port 53 receives UDP like so:
17:40:45.501553 IP 100.64.0.13.38730 > 100.64.0.187: 61936+ CNAME? dnscat.d2bc039ce800000000d6eae8eae3bf81fd84d1695f5888aba8dcec06d071.a73b3f0561ca4906d268214f4b70da1bdb50f75739ae0577139096732bf8.0d0a987ce23408bac15426a22e. (173) 17:40:45.501639 IP 100.64.0.187 > 100.64.0.13: ICMP 100.64.0.187 udp port domain unreachable, length 209 17:40:46.520457 IP 100.64.0.13.38730 > 100.64.0.187: 21842+ TXT? dnscat.7f4e039ce800000000d6eae8eae3bf81fd84d1695f5888aba8dcec06d071.a73b3f0561ca4906d268214f4b70da1bdb50f75739ae0577139096732bf8.0d0a987ce23408bac15426a22e. (173) 17:40:46.520546 IP 100.64.0.187 > 100.64.0.13: ICMP 100.64.0.187 udp port domain unreachable, length 209
It's clearly DNSCAT, a tool for DNS tunneling. When you type an IP address in the form, the DNSCAT client attempts to connect to that address. If the attempt is successful, the server (in other words, the participant) gets a shell on the contest computer and collects the flag.
Naturally, if we simply try to raise the DNSCAT server and accept the connection, no such luck:
[Drop] [] [1:4001:1] 'dnscat' string found in DNS response []
The IDS signature is triggered by the string "dnscat" in the traffic from our server—this much is clear from the message. Obfuscating or encrypting traffic won't work.
But looking at the client code, we see that the checks are not too strict. The response need not contain the "dnscat" string at all! We only need to remove the string from the code or else replace it on the fly with the help of NetSED. It's much easier to swap it out on the fly, but here is the patch for server code just in case:
diff -r dnscat2/server/libs/dnser.rb dnscat2_bypass/server/libs/dnser.rb < segments << unpack("a#{len}")
segments << [unpack("a#{len}")[0].upcase]
< name.split(/./).each do |segment|
name.upcase.split(/\./).each do |segment|
diff -r dnscat2/server/tunnel_drivers/driver_dns.rb dnscat2_bypass/server/tunnel_drivers/driver_dns.rb < response = (response == "" ? "dnscat" : ("dnscat." + response))
response = (response == "" ? "dnsCat" : ("dnsCat." + response))
Five participants met the challenge.
100.64.0.14 — POST
No participants collected the flag for this task.
We see the now-familiar form for entering an IP address, inviting us to participate in testing new malware. One of its new tricks is bypassing the IDS in some unknown way. To get the flag, all you need to do is send the HTTP header "Server: ng1nx" in response. And the fun begins.
As expected, we get a GET request at our IP address and send a response, which gets blocked by the IDS.
[Drop] [] [1:5002:1] 'ng1nx' Server header found. Malware shall not pass []
Here's the hint given to participants:
Sometimes, tasks that look hard are the simplest. If nothing seems vulnerable, maybe you're missing something right under your nose?
That "something right under your nose" is the IDS. On the detections page, you can see that we're dealing with an unprotected Suricata IDS.
Search for "Suricata IDS Bypass" and the very first link you get points to CVE-2018-6794. This vulnerability allows you to bypass packet checks if the normal TCP handshake process is interrupted and the data is sent before the process is completed. It looks like this:
Client -> [SYN] [Seq=0 Ack=0] -> Evil Server # 1/2 Client <- [SYN, ACK] [Seq=0 Ack=1] <- Evil Server # 2/2 Client <- [PSH, ACK] [Seq=1 Ack=1] <- Evil Server # Data here Client <- [FIN, ACK] [Seq=83 Ack=1] <- Evil Server Client -> [ACK] [Seq=1 Ack=84] -> Evil Server # 3/2 Client -> [PSH, ACK] [Seq=1 Ack= 4] -> Evil Server
You download the exploit, change the string to "ng1nx", disable kernel reset (RST) packets, and run it.
As mentioned, nobody was able to get this flag, though a few participants were very close.
Conclusion
49 people signed up for the contest, of which 12 succeeded in collecting at least one flag. Part of the excitement comes from the fact that tasks can have multiple solutions, especially the tasks that involve SMB and DCERPC. Perhaps you have a few ideas of your own?
Winners:
- 1st place: @ psih1337
- 2nd place: @ webr0ck
- 3rd place: @empty_jack
Signature trigger statistics:
Thank you all for participating! Next year we'll have even more tasks of all levels of difficulty.