GACTF 2020 EZ FLASK (SSRF to SSTI)

web Aug 30, 2020

This challenge was the sequel to SimpleFlask.  This challenge felt far simpler even though it seemed like far fewer people were able to complete it.

Again we start by visiting the webpage.

We are immediately presented with a redacted python flask script.  We see a few immediately interesting things.  Ctfhint function, admin function, and eval function all stood out.

We first try ctfhint, what else could we really expect.  Moving on...

Based on the name of the page and variables we can assume eval passes arguments to the python eval function.  Instead of beating around the bush, we attempt to use eval to dump the constants for ctfhint to see what the hint could be...and we find the admin page url.  We also see "port : 5000" in the comments which was not open externally, maybe some sort of SSRF?

Visiting the admin page it appears we are in fact greeted with a page that can make web requests for us based on arguments we control.  So we try to access the internal port...

Blacklisted hmm.  Well since ctf.__code__.co_consts worked, how about we try dumping python bytecode by doing admin.__code__.co_code to analyze what exactly is going on here.  

And we have bytecode!  We copy the data to a file and use a simple python script to disassemble the bytecode....

	
with open("code.txt", "r") as myfile:
        dis.dis(myfile.read())

OUTPUT SNIPPET:
          0 SETUP_EXCEPT       43 (to 46)
          3 LOAD_GLOBAL         0 (0)
          6 LOAD_ATTR           1 (1)
          9 LOAD_CONST          1 (1)
         12 BINARY_SUBSCR  
         13 STORE_FAST          0 (0)
         16 LOAD_GLOBAL         0 (0)
         19 LOAD_ATTR           1 (1)
         22 LOAD_CONST          2 (2)
         25 BINARY_SUBSCR  
         26 STORE_FAST          1 (1)
         29 LOAD_GLOBAL         0 (0)
         32 LOAD_ATTR           1 (1)
         35 LOAD_CONST          3 (3)
         38 BINARY_SUBSCR  
         39 STORE_FAST          2 (2)
         42 POP_BLOCK      
         43 JUMP_FORWARD        8 (to 54)
    >>   46 POP_TOP  
    

We then also dumped names and constants...

	
INPUT: eval=admin.__code__.co_consts
OUTPUT:
(None, 'ip', 'port', 'path', 'post ip=x.x.x.x&port=xxxx&path=xxx => http://ip:port/path', 4, 'hacker?', 'http://{}:{}/{}', 'timeout', 2, 'requests error')
INPUT: eval=admin.__code__.co_names
OUTPUT: ('request', 'form', 'waf_ip', 'waf_path', 'len', 'requests', 'get', 'format', 'text')
    

Using these we manually decompile the bytecode back to it's python source and get...

	
try:
  myip = request.form["ip"]
  myport = request.form["port"]
  mypath = request.form["path"]
except:
  return "post ip=x.x.x.x&port=xxxx&path=xxx => http://ip:port/path"
try:
  if waf_ip(ip) and waf_path(path) and len(port) > 4:
    return "hacker?"
  myurl = "http://{}:{}/{}".format(myip, myport, mypath)
  r = requests.get(myurl, timeout=2)
  return r.text
except:
  return unknown_const???
return None

    

Two new interesting functions, waf_ip and waf_path have appeared.  We go ahead and repeat this process on those 2 as well. Unfortunately...waf_path can't dump anything as the blacklist catches us but we successfully recreate waf_ip.

	
//WAF_IP
blacklist = ["0.0","192","172","10.0","233.233"]
goodchars = "1234567890."

if len(arg) > 15
  return True

if len(arg.split(".")) != 4:
  return True

for c in arg0:
  if c not in goodchars:
    return True
    
for bad in blacklist:
  if bad in arg:
    return True
return False
    

These source codes that are obtained are approximations, but we can see that our 127.0.0.1 was blocked thanks to the 0.0 blacklist item.  Fortunately, I've messed with enough /etc/hosts files to know 127.0.1.1 is valid as well.

And we successfully access the resource.  It's another SSTI!  Unfortunately, since we can't dump the waf_path filter, we have to go blind, but at least we know the flag is in the config!

Dumping the config directly fails, too obvious I guess.

Looks like we can use url_for this time though!  Thankfully we can dump the config using url_for as well!

Searching for ctf gives us the flag so we don't get hit by the filter using .config as a key.  That completes this challenge.  Both challenges were quite fun and taught me quite a bit.