Skip to content

Instantly share code, notes, and snippets.

@tijldeneut
Created November 17, 2024 16:39
Show Gist options
  • Save tijldeneut/0244ff27d19f8c2520fe69cd1dd77894 to your computer and use it in GitHub Desktop.
Save tijldeneut/0244ff27d19f8c2520fe69cd1dd77894 to your computer and use it in GitHub Desktop.
Downloading CTF Callenges
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
r'''
Copyright 2024 Photubias(c)
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
Filename: ctfd-downloader-Photubias.py
This downloads challenges and files, full details will be in challs.json.
Supports unauthenticated, authenticated-creds and authenticated-Session token downloads.
The session cookie option is useful for MFA environments
-> The only requirement is the availability of the API:
https://ctf.example.com/api/v1/challenges
Just run python ctfd-downloader-Photubias.py https://ctf.example.com
and look at the output
'''
import requests, os, json, argparse, re
requests.packages.urllib3.disable_warnings()
def getAPI(sURL, sEndpoint, oSession, boolRedirect=True):
if sEndpoint[1] == '/': sEndpoint=sEndpoint[1:]
oResponse = oSession.get(f'{sURL}/api/v1/{sEndpoint}', allow_redirects=boolRedirect)
return oResponse
def errorStop(sMessage):
print(f'[-] Fatal error: {sMessage}')
exit(-1)
def sanitizeFileFolder(sOrg):
return sOrg.replace('<','').replace('>','').replace(':','').replace('"','').replace('/','').replace('\\','').replace('|','').replace('?','').replace('*','')
def downloadChalls(sURL, oSession, dctChallenges, sFolder):
if not os.path.isdir(f'{sFolder}'): os.mkdir(f'{sFolder}')
dctExported = {}
for dctChall in dctChallenges['data']:
print('[+] Downloading {} - {} [{} pts]'.format(dctChall['name'], dctChall['category'], dctChall['value']))
if dctChall['category'] not in dctExported: dctExported[dctChall['category']] = {}
if not os.path.isdir('{}/{}'.format(sFolder, dctChall['category'])): os.mkdir('{}/{}'.format(sFolder, dctChall['category']))
dctChallInfo = getAPI(sURL, 'challenges/{}'.format(dctChall['id']), oSession).json()['data']
sChallDir = '{}/{}/{} [{} pts]'.format(sFolder, sanitizeFileFolder(dctChall['category']), sanitizeFileFolder(dctChall['name']), dctChall['value'])
os.mkdir(sChallDir)
## README: Title, Category, Points, Description and Hints
sContent = 'Challenge:\n{}\n\nDescription:\n{}\n\nHints:\n'.format(sChallDir, dctChallInfo['description'])
if '"https://drive.google.com' in dctChallInfo['view']:
sGoogleURL = 'https://drive.google.com' + re.findall(r'\"https://drive.google.com(.*?)\"',dctChallInfo['view'])[0]
print(f' [!] Found a Google Drive link, please download manually:\n {sGoogleURL}')
for sHint in dctChallInfo:
if 'content' in sHint: sContent += '{}\n'.format(sHint.content)
open(f'{sChallDir}/README.md', 'w', encoding='utf-8').write(sContent)
## Challenge Files
sFileDir = f'{sChallDir}/files'
if len(dctChallInfo['files']) > 0: os.mkdir(sFileDir)
for sFileURL in dctChallInfo['files']:
sFilename = sFileURL.split('?token')[0].split('/')[-1]
print(f' [+] Downloading file: {sFilename}')
if sFileURL[:4] != 'http': sFileURL = sURL + sFileURL
bFileData = oSession.get(sFileURL).content
open(f'{sFileDir}/{sFilename}','wb').write(bFileData)
dctExported[dctChall['category']][dctChall['name']] = dctChallInfo
print('Writing challs.json')
sChallsFile = open(f'{sFolder}/challs.json','w')
json.dump(dctExported, sChallsFile, indent=4, sort_keys=True)
sChallsFile.close()
return
def verifyArgsAndAccess(dctArgs, oSession):
def loginWithCreds(sURL, sUsername, sPassword, oSession):
oResp = oSession.get(f'{sURL}/login') ## Get CSRF token
lstTokens = re.findall(r'<input type="hidden" name="nonce" value="(.*?)">', oResp.text)
if len(lstTokens)==0: lstTokens = re.findall(r'\'csrfNonce\': "(.*?)"',oResp.text)
sToken = lstTokens[0]
oResp = oSession.post(f'{sURL}/login',{"name":sUsername,"password":sPassword,"nonce":sToken}, allow_redirects=False)
if 'Your username or password is incorrect' in oResp.text: errorStop('Credentials provided are not correct.')
return oSession
def loginWithCookie(sURL, sSession, oSession):
oResp = oSession.get(f'{sURL}/login') ## Get correct headers
sCookieDomain = oSession.cookies.list_domains()[0]
oSession.cookies.set('session', sSession, domain=sCookieDomain)
oResp = oSession.get(f'{sURL}/challenges', allow_redirects=False)
if oResp.status_code == 302: errorStop('Error with the "session" cookie, please retry')
return oSession
sURL = dctArgs.url
oResp = getAPI(sURL, 'challenges', oSession, boolRedirect=False)
if oResp.status_code == 302:
## Authentication required
if not dctArgs.username and not dctArgs.session: errorStop(f' Authentication required, please specify credentials or session token')
if dctArgs.username and not dctArgs.password:
sPassword = input('[?] Please enter the password for user {}'.format(dctArgs.username))
oSession = loginWithCreds(sURL, dctArgs.username, sPassword, oSession)
else: oSession = loginWithCreds(sURL, dctArgs.username, dctArgs.password, oSession)
if dctArgs.session: loginWithCookie(sURL, dctArgs.session, oSession)
elif oResp.status_code >= 500: errorStop(f'URL {sURL}/api/v1/challenges not reachable')
oResp = getAPI(sURL, 'challenges', oSession)
if 'json' not in oResp.headers['Content-Type']: errorStop(f'URL {sURL}/api/v1/challenges is not JSON data')
if sURL[-1]=='/': sURL = sURL[:-1]
if sURL[:4].lower()!='http': sURL = f'https://{sURL}'
sFolder = sURL.replace('https://','').replace('http://','').replace('/','').replace('.','_')
return sURL, sFolder, oSession
def main():
## Banner
print(r'''
[*****************************************************************************]
--- CTFd Downloader ---
This script will try to download all available challenges & files
from a CTFd platform, no warranties
Unauthenticated, via credentials or the session header (e.g. to bypass MFA)
_______________________/-> Created By Tijl Deneut(c) <-\_______________________
[*****************************************************************************]
''')
## Defaults and parsing arguments
oParser = argparse.ArgumentParser()
oParser.add_argument('-u', '--username', help='Username for CTFd, optional')
oParser.add_argument('-p', '--password', help='Password for CTFd, optional, will be asked if username is provided')
oParser.add_argument('-s', '--session', help='Cookie value for "session", just specify the value. E.g. --session 22f3d655-0cd6-48e9-aaa2-b307ed6ef999.16vWw_3atI7ux6g9Vx8tjAAuyT0')
oParser.add_argument('-v', '--verbose', help='Verbosity; more info', action='store_true')
oParser.add_argument('--proxy', help='Default Burp proxy at 127.0.0.1:8080', action='store_true')
oParser.add_argument('url', help='Base URL for CTFd, e.g. https://ctf.sans.org. Hint: open challenges and remove "/challenges" from the URL')
dctArgs = oParser.parse_args()
dctProxy = {} if not dctArgs.proxy else {'http':'http://127.0.0.1:8080','https':'http://127.0.0.1:8080'}
## Set up variables
oSession = requests.Session()
oSession.verify = False
oSession.proxies = dctProxy
## Verification
sURL, sFolder, oSession = verifyArgsAndAccess(dctArgs, oSession)
## Get challenges
dctChallenges = getAPI(sURL, 'challenges', oSession).json()
if os.path.isdir(f'{sFolder}'): print(f'[!] Warning, folder {sFolder} found, will overwrite')
sAnswer = input('[+] Success, detected {} challenges, ready to create folder {} and start the download? [Y/n]: '.format(len(dctChallenges['data']), sFolder))
if 'n' in sAnswer: exit()
downloadChalls(sURL, oSession, dctChallenges, sFolder)
if __name__ == '__main__':
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment