fromflaskimportFlask,Response,request,render_template,requestfromrandomimportchoice,randint#from string import lowercasefromfunctoolsimportwrapsapp=Flask(__name__)defcalc(recipe):globalgaragegarage={}print(recipe)try:exec(recipe,garage)except:passdefGCR(func):# Great Calculator of the observable universe and it's infinite timelines@wraps(func)deffederation(*args,**kwargs):ingredient=''recipe='%s = %s'%(ingredient,''.join(map(str,[randint(1,69),choice(['+','-','*']),randint(1,69)])))ifrequest.method=='POST':ingredient=request.form.get('ingredient','')recipe='%s = %s'%(ingredient,request.form.get('measurements',''))print(ingredient)calc(recipe)ifgarage.get(ingredient,''):returnrender_template('index.html',calculations=garage[ingredient])returnfunc(*args,**kwargs)returnfederation@app.route('/',methods=['GET','POST'])@GCRdefindex():returnrender_template('index.html')@app.route('/debug')defdebug():returnResponse(open(__file__).read(),mimetype='text/plain')if__name__=='__main__':app.run('0.0.0.0',port=1337)
The source code reveals that the website is actually a Flask app. The application defines two routes, / and /debug, of which / is of interest. This route accepts both GET and POST requests and calls a custom function (GCR) when invoked:
defGCR(func):# Great Calculator of the observable universe and it's infinite timelines@wraps(func)deffederation(*args,**kwargs):ingredient=''recipe='%s = %s'%(ingredient,''.join(map(str,[randint(1,69),choice(['+','-','*']),randint(1,69)])))ifrequest.method=='POST':ingredient=request.form.get('ingredient','')recipe='%s = %s'%(ingredient,request.form.get('measurements',''))calc(recipe)ifgarage.get(ingredient,''):returnrender_template('index.html',calculations=garage[ingredient])...
The number displayed on the page is indeed randomly generated and stored in the recipe variable. If the request method used is POST, the application also looks for the parameters ingredient and measurements. It then calls calc():
The critical line in the calc() function is the call to exec() on line 4. exec() allows running arbitrary Python code dynamically, which makes input validation critical. As this is lacking from the application, a user may execute arbitrary Python code by making a POST request to the application with the parameters ingredient and measurements.
object can be a string or a code object, globals and locals are dictionaries. Applies to the calc() function above, object is recipe, while garage is defined as a global dictionary.
The result shows that using exec() this way allows for arbitrary code execution. By modifying the payload, this can be abused for executing OS commands: