PHDays 10 IDS Bypass contest: writeup and solutions

For the second time, the IDS Bypass contest was held at the Positive Hack Days conference. Just like last time (see blog.ptsecurity.com/2019/07/ids-bypass-contest-at-phdays-writeup.html), the players were supposed not only to find flaws in the six services and capture the flags, but also bypass the IDS, which would interfere with them. Alert messages about the facts of triggering IDS rules were supposed to help in bypassing them. And as you know from the last competition, there can be infinitely many solutions to tasks. Here we go.


192.168.30.10—Apache Tomcat

On port 8080, we can see Apache Tomcat version 9.0.17. The first search for an exploit for this version should lead to CVE-2019-0232.

This task was intended as an introductory one and was supposed to be the simplest (although some of the other tasks turned out to be simpler). In the exploit, we see a test URL with the command /cgi/test.bat?&dir. But with such a request, it just freezes, and the player sees the IDS alert:

ATTACK [PTsecurity] Apache Tomcat RCE on Windows (CVE-2019-0232)

It was intended so that the players would modify the URL in the same way as they would do it to bypass the WAF. The regular expression in the rule looks like this: pcre: "/\.(?:bat|cmd)\?\&/U"; and soon the IDS would submit to the player. In addition, some exploits already have an example of a URL with a bypass, for example: http://localhost:8080/cgi/test.bat%20%20?&dir. As a result, many easily completed the task. We have taken a look at the contest, let's move on.


192.168.30.20—PHP Bypass

On the main page, we can see an offer to test the ls command. It warns us that it may not be working.

And, as expected—it is not working. There is a message in the log:

ATTACK [PTsecurity] file_name parameter possible command injection

You might think that in the task you have to exploit RCE and get the flag, but the idea was different. In the summer of 2019, an author under the pseudonym "@Menin_TheMiddle" published an article (secjuice.com/abusing-php-query-string-parser-bypass-ids-ips-waf) about the IDS and WAF bypass. It said that a number of characters in the name of the GET parameter the PHP interpreter leads to an underscore ("_"). Our IDS, unlike PHP, does not do this. For example, the author used one of the public IDS rules of our AttackDetection team. And since the crucial part of the Suricata rule looked like this: pcre: "/file_name\s*=\s*[a-zA-Z\.]*[^a-zA-Z\.]/U";it was possible to bypass it simply by replacing the file_name parameter with, say, file[name. Due to an error in the Suricata rule, you could get the flag simply by sending file_name=.


192.168.30.30—He said yes

We see a form and instructions on how to get that flag. It is enough to give a simple answer "yes" to the HTTP request.


The players started a web server on their nodes, answered "yes" to incoming requests and saw the following line in the logs:

JOKE [PTsecurity] Sometimes Positive Technologies hurts! No 'yes' allowed

The rule checked all HTTP responses and did not allow those with the "yes" string inside. The game host also accepted the "yes" string in lowercase only, and this task received the largest number of different solutions!

The idea was to redirect the incoming HTTP request to the HTTPS protocol and answer "yes" in the new request. For this vector, the allow_redirects=True and verify=False parameters were specially used in the requests library. The solution looked like this:

echo -ne "HTTP/1.1 302 Redirect\r\nLocation: https://10.8.0.2/hi_there\r\nContent-Length: 0\r\n\r\n" | sudo nc -nkvlp 80

echo -ne "HTTP/1.1 200 OK\r\nContent-Length: 3\r\nContent-Type: text/html\r\n\r\nyes" | sudo ncat -nvklp 443 –ssl

The player @vos used a hundredfold nested gzip compression for the HTTP response, and the player @webr0ck used zero filling of almost 2 megabytes in size before the "yes" string. In both cases, Suricata turned out to be powerless. 

192.168.30.40—DCERPC

It was the most expensive task in the competition. The players were given the administrator account credentials and asked to show their knowledge of Windows protocols. To get the flag, it was necessary to extract the list of all the users on the device.


Those who are familiar with AD security can immediately think of the samrdump.py script from the impacket set, but the script addresses SMB port 445 that is closed on the host. The binding string in the script ncacn_np:*hostname*[\pipe\samr]is fixed and leads to the SMB pipe. In addition, of all open ports on the host, only port 135 is detected, the so-called Endpoint Mapper listens to it. EPM is responsible for resolving RPC interfaces.

Another script from the impacket set, rpcdump.py, uses EPM to obtain the list of currently active RPC interfaces.

> python rpcdump.py Administrator:[email protected]

Protocol: [MS-SAMR]: Security Account Manager (SAM) Remote Protocol

Provider: samsrv.dll

UUID    : 12345778-1234-ABCD-EF00-0123456789AC v1.0

Bindings:

          ncacn_ip_tcp:192.168.30.40[49668]

          ncacn_np:\\TASK4[\pipe\lsass]

Among all the interfaces, we can see the one we need, SAMR, which is responsible, among other things, for user management. Using the SAM interface, the samrdump.py script extracts the list of users. That is, in addition to the SMB pipe, we can directly connect to port 49668 and request the list of users via the DCERPC protocol. To do this, we need to patch the samrdump.py script so that it directly addresses the SAMR interface instead of the pipe. The players @vos and @Abr1k0s opted for a different solution. They used a ready-made walksam tool from the rpctools set, the only difficulty of which is using the flag RPC_C_AUTHN_LEVEL_PKT_PRIVACY instead of RPC_C_AUTHN_LEVEL_CALL. Alternative methods were to use atsvc, svcctl, dcom, and other interfaces. All of them allow arbitrary code execution and are closed by IDS rules. The shutdown interface was also closed. The most unusual way to solve this task involved remote search for user creation events using wevtutil: 


192.168.30.50—RDP me

The task is simple: to connect via RDP with a known account and read the flag from the desktop. The difficult part is that most of the known RDP clients are blocked by IDS rules. The players received one of the following messages every now and then:

  • TOOLS [PTsecurity] xfreerdp/vinagre/remmina RDP client
  • TOOLS [PTsecurity] xfreerdp/remmina RDP client
  • TOOLS [PTsecurity] MSTSC Win10 RDP client
  • TOOLS [PTsecurity] MSTSC Win7 RDP client
  • TOOLS [PTsecurity] Rdesktop RDP client

Sometimes different RDP clients behave the same way: for example, many Linux clients are built around the same library. The task has several solutions.

The first one, which was originally envisaged, is a head-on solution. The player goes through different launch options or tries different clients and sees different IDS alerts. After traffic analysis, it becomes clear which packet is blocked by the IDS. Based on this, the player can draw a conclusion about how the rules work. The rules are triggered on certain channel sequences (channelDef) in the ClientNetworkData field and on the header order itself. 


Going through the launch options of the xfreerdp client of the latest versions, the player may come across the echo option:

  • xfreerdp /v:192.168.30.50 /u:user /p:letmein +echo
  • And xfreerdp with exactly these parameters will slip past the IDS rules. 

Another way was demonstrated by @vos, @Abr1k0s, and @astalavista—they connected to the server using the Mocha RDP Lite mobile client. A fundamentally different solution using netsed was found by @webr0ck. Netsed, like a regular sed, is able to replace network data on the fly. The player simply zero-filled all the channel names in the ClientData RDP package.


192.168.30.60—LDAP

The description contains an IP address with open port 389. There are no credentials in the task, but the LDAP service supports anonymous connections (bind). However, with a simple connection using the Python library ldap3, we see the IDS alert in three lines.

server = ldap3.Server('192.168.30.60', port=389)

connection = ldap3.Connection(server)

connection.bind()

TEST [PTsecurity] LDAP ASN1 single byte length fields prohibited

We captured the dump of our traffic and we see that the bind itself is successful, but the searchRequest that the library sent after that remains unanswered. The IDS rule is triggered on it.

The Windows utilities ADSIEdit and ldp, as well as the Linux utility ldapsearch, give similar results, but with different alerts:

TEST [PTsecurity] LDAP ASN1 1-byte length encoded found

TEST [PTsecurity] LDAP ASN1 2-byte length encoded found

TEST [PTsecurity] LDAP ASN1 4-byte length encoded found

The whole point turns out to be how the lengths of individual fields in LDAP messages are encoded. The byte x in the byte sequence 30 8x yy yy yy is responsible for the length of the length field in bytes. For example, the sequence 30 82 00 02 encodes two bytes of the length field 00 02. Thus, the players were required to try fields of a different length and find that the IDS is not triggered on the field of 3 bytes long. The flag is in the response among the namingContexts fields. The task implied the only solution, and only two of the players managed to do the task.



Results:

1 place: @vos—Apple Watch Series 6 + backpack

2 place: @psih1337—cash reward + backpack

3 place: @Abr1k0s—backpack


Author: Kirill Shipulin, Positive Technologies


Article Link: Positive Technologies - learn and secure : PHDays 10 IDS Bypass contest: writeup and solutions