├── .gitignore ├── requirements.txt ├── example.png ├── install.sh ├── set_attr.py ├── README.md ├── board_configs ├── innoctf.py ├── m_star_ctf.py ├── ctforces.py ├── forkbomb.py └── ctfd.py ├── ls_tasks.py ├── cat_task.py ├── ctfd_formatter.py ├── utils.py └── dumper.py /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | termcolor 2 | lxml 3 | -------------------------------------------------------------------------------- /example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kochanac/ctf_thief_tools/HEAD/example.png -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" 4 | 5 | ln -sf $DIR/dumper.py /usr/local/bin/dumper 6 | ln -sf $DIR/cat_task.py /usr/local/bin/cattask 7 | ln -sf $DIR/ls_tasks.py /usr/local/bin/lstasks 8 | ln -sf $DIR/set_attr.py /usr/local/bin/setattr 9 | -------------------------------------------------------------------------------- /set_attr.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import sys, json, os 4 | from utils import * 5 | 6 | if '-h' in sys.argv or '--help' in sys.argv: 7 | print(f"""Usage: { sys.argv[0] } attr value [directory or json of task]""") 8 | exit(0) 9 | 10 | if len(sys.argv) == 3: 11 | file = 'info.json' 12 | elif os.path.isdir(sys.argv[3]): 13 | file = sys.argv[3]+'/info.json' 14 | else: 15 | file = sys.argv[3] 16 | 17 | 18 | data = json.load(open(file, 'r')) 19 | 20 | data[sys.argv[1]] = eval(sys.argv[2]) 21 | 22 | json.dump(data, open(file, 'w')) 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ctf_thief_tools 2 | 3 | ![img][example] 4 | 5 | [example]: ./example.png 6 | 7 | ## Usage: 8 | 9 | copy request to one of tasks json (RMB -> "copy request headers" in firefox) and save it to /tmp/request 10 | 11 | `install.sh` will link scripts to /usr/local/sbin 12 | 13 | `dumper ctfd 1-100` will dump/update tasks 14 | 15 | `cattask ./task1` 16 | 17 | `lstasks` 18 | 19 | `setattr Solved True task1` - mark task1 as solved 20 | 21 | ## Supported: 22 | * ctfd 23 | 24 | * ctforces (ctforces.com) 25 | 26 | * innoctf 27 | 28 | * forkbomb (forkbomb.ru) 29 | 30 | * m_star_ctf (mctf.online) 31 | 32 | -------------------------------------------------------------------------------- /board_configs/innoctf.py: -------------------------------------------------------------------------------- 1 | import json 2 | import re 3 | 4 | def getInfo(): 5 | return { 6 | "task_id_in_url": None, 7 | "url_to_files": "/{filename}", 8 | "http?": False, 9 | "custom_ids": False 10 | } 11 | 12 | def parse_task(data, **kwargs): 13 | try: 14 | data = json.loads(data)["tasks"][kwargs.get("id")] 15 | except IndexError: 16 | print("Fine.") 17 | return {} 18 | 19 | task = { 20 | "Title": data['title'], 21 | "Category": data['tags'], 22 | "Description": data['description'], 23 | "Files": list(), 24 | "Tags": list(), 25 | "Solved": data["solved"], 26 | "Value": data['points'] 27 | } 28 | 29 | task['Files'] = re.findall(r"files\/[0-9a-f]+\.?[a-z]*", task["Description"]) 30 | 31 | return task 32 | 33 | def get_meta(base, HEADERS): 34 | return {} -------------------------------------------------------------------------------- /board_configs/m_star_ctf.py: -------------------------------------------------------------------------------- 1 | import json 2 | import re 3 | 4 | def getInfo(): 5 | return { 6 | "task_id_in_url": None, 7 | "url_to_files": "/{filename}", 8 | "http?": False, 9 | "custom_ids": False 10 | } 11 | 12 | def parse_task(data, **kwargs): 13 | try: 14 | data = json.loads(data)["data"][kwargs.get("id")] 15 | except IndexError: 16 | print("No task.") 17 | return None 18 | 19 | task = { 20 | "Title": data['name'], 21 | "Category": data['category'], 22 | "Description": data['description'], 23 | "Files": data['file_set'], 24 | "Tags": list(), 25 | "Solved": data["already_solved"], 26 | "Value": data['score']/5 27 | } 28 | for i in range(len(task["Files"])): 29 | task["Files"][i] = task["Files"][i]["content"] 30 | 31 | return task 32 | 33 | def get_meta(base, HEADERS): 34 | return {} 35 | -------------------------------------------------------------------------------- /board_configs/ctforces.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | def getInfo(): 4 | return { 5 | "task_id_in_url": -2, 6 | "url_to_files": "media/{filename}", 7 | "http?": False, 8 | "custom_ids": False 9 | } 10 | 11 | def parse_task(data, **kwargs): 12 | data = json.loads(data) 13 | task = { 14 | "Title": data['name'], 15 | "Category": data['tags_details'][0]['name'], 16 | "Description": data['description'] + f"\n\n(from ctforces, author: { data['author_username'] })", 17 | "Files": list(), 18 | # "Tags": data['tags'], 19 | "Solved": data["is_solved_by_user"], 20 | "Value": data['contest_cost'] 21 | } 22 | 23 | task['Tags'] = list() 24 | for i in data['tags_details']: 25 | task['Tags'].append(i['name']) 26 | 27 | for i in data['files_details']: 28 | task['Files'].append(i['file_field']) 29 | 30 | return task 31 | 32 | def get_meta(base, HEADERS): 33 | return {} -------------------------------------------------------------------------------- /ls_tasks.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import sys, json, os 4 | from termcolor import colored as clr 5 | from utils import * 6 | 7 | form = "{name} - {category} {value} {solved}{files}" 8 | 9 | if '-h' in sys.argv or '--help' in sys.argv: 10 | print(f"""Usage: { sys.argv[0] } [ tag ]""") 11 | exit(0) 12 | 13 | args = sys.argv[1::] 14 | if len(args) == 0: 15 | args.append('') 16 | 17 | tag = args[0] 18 | 19 | tasks = os.listdir() 20 | for i in sorted(tasks): 21 | try: 22 | if not os.path.isdir(f'./{i}'): 23 | continue 24 | if 'info.json' not in os.listdir(f'./{i}'): 25 | continue 26 | 27 | task = json.load(open(f'./{i}/info.json')) 28 | 29 | if tag in task['Category']: 30 | print(form.format( name = process_title(task, True) 31 | , category = process_category(task) 32 | , value = process_value(task['Value']) 33 | , solved = process_solved(task) 34 | , files = red("[ FILES ]") if len(os.listdir(f'./{i}')) > len(task["Files"])+1 else '' 35 | ) 36 | ) 37 | 38 | except PermissionError: 39 | pass 40 | -------------------------------------------------------------------------------- /cat_task.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import sys, json, os 4 | from termcolor import colored as clr 5 | from utils import * 6 | 7 | form = """ 8 | {title} {solved} 9 | {category} - {value} 10 | 11 | {text} 12 | 13 | Files: 14 | {files} 15 | """ 16 | 17 | if '-h' in sys.argv or '--help' in sys.argv: 18 | print(f"""Usage: { sys.argv[0] } [directory or json of task]""") 19 | exit(0) 20 | 21 | if len(sys.argv) == 1: 22 | file = 'info.json' 23 | elif os.path.isdir(sys.argv[1]): 24 | file = sys.argv[1]+'/info.json' 25 | else: 26 | file = sys.argv[1] 27 | 28 | 29 | def fmt_file(x): 30 | if isinstance(x, str): 31 | return x 32 | if isinstance(x, dict): 33 | return x["name"] 34 | 35 | return x 36 | 37 | 38 | with open(file) as f: 39 | data = json.loads(f.read()) 40 | 41 | 42 | print(form.format(title = process_title(data) 43 | , category = process_category(data) 44 | , value = process_value(data["Value"]) 45 | , text = process_desc(data["Description"]) 46 | , files = ('\n'.join([fmt_file(file) for file in data['Files'] ]) if 'Files' in data else 'No files') 47 | , solved = process_solved(data) 48 | ) 49 | ) 50 | -------------------------------------------------------------------------------- /ctfd_formatter.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import json 4 | import sys 5 | 6 | if '-h' in sys.argv: 7 | print( 8 | f"""usage: {sys.argv[0]} info.json 9 | """) 10 | 11 | """ 12 | IFS=$" 13 | " 14 | 15 | for task in $(lstasks tag | cut -d'-' -f1); 16 | do task=$(echo "$task" | xargs); 17 | echo "$task" && cd "$task"; 18 | curl 'http://tasks.kochan.me/api/v1/challenges' -H 'User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:70.0) Gecko/20100101 Firefox/70.0' -H 'Accept: application/json' -H 'Accept-Language: en-US,en;q=0.5' --compressed -H 'Referer: http://tasks.kochan.me/admin/challenges/new' -H 'Content-Type: application/json' -H 'CSRF-Token: b8c51c8ea10b6e6a5f7eba85eb6534ed763e7403a2dbe00933c2fa95e5c8be9c' -H 'Origin: http://tasks.kochan.me' -H 'Connection: keep-alive' -H 'Cookie: session=sess' --data "$(~/pogroming/ctf_thief_tools/ctfd_formatter.py info.json)" && cd ..; 19 | done 20 | 21 | """ 22 | 23 | 24 | task = json.load(open(sys.argv[1])) 25 | 26 | print(json.dumps( 27 | { 28 | "category": task["Category"], 29 | "name": task["Title"], 30 | "description": task["Description"], 31 | "type": "standard", 32 | "state": "visible", 33 | "value": task["Value"] 34 | } 35 | )) -------------------------------------------------------------------------------- /board_configs/forkbomb.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | def getInfo(): 4 | return { 5 | "task_id_in_url": -1, 6 | "url_to_files": "", 7 | "http?": False, 8 | "custom_ids": True 9 | } 10 | 11 | def get_ids(url): 12 | import requests as req 13 | from lxml import etree 14 | 15 | url = '/'.join(url.split('/')[:-1]) 16 | 17 | urls = etree.HTML(req.get(url).text).xpath('//a[@class="tile"]/@href') 18 | 19 | return map(lambda x: x.split('/')[-1], urls) 20 | 21 | 22 | 23 | def parse_task(data, **kwargs): 24 | from lxml import etree 25 | 26 | tasktag = etree.HTML(data).xpath('//table[@id="flagtable"]')[0] 27 | 28 | author = tasktag.xpath('.//span[@class="author"]/text()')[0] 29 | 30 | task = { 31 | "Title": tasktag.xpath('.//span[@class="desc"]/text()')[0], 32 | "Category": tasktag.xpath('.//span[@class="cat"]/text()')[0], 33 | "Description": etree.tostring(tasktag.xpath('.//div[@class="fullDesc"]')[0]).decode() + f"\n\n(from forkbomb, { author })", 34 | "Files": list(), 35 | "Color": tasktag.xpath('//div[contains(@class,"fullTile")]/@style')[0].split(" ")[-1], 36 | "Solved": len(tasktag.xpath('//div[@class="fullTile solved"]')) == 1, 37 | "Value": int(tasktag.xpath('.//span[@class="cost"]/text()')[0]) 38 | } 39 | 40 | return task 41 | 42 | def get_meta(base, HEADERS): 43 | return {} -------------------------------------------------------------------------------- /board_configs/ctfd.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | def getInfo(): 4 | return { 5 | "task_id_in_url": -1, 6 | "url_to_files": "{filename}", 7 | "http?": False, 8 | "custom_ids": False 9 | } 10 | 11 | def parse_task(data, **kwargs): 12 | data = json.loads(data) 13 | 14 | if "data" in data: 15 | data = data["data"] 16 | 17 | data["files"] = [ 18 | {"name": f[:f.find("?")] if '?' in f else f, "url": f} 19 | for f in data["files"] 20 | ] 21 | 22 | desc = data['description'] 23 | if "connection_info" in data: 24 | desc += "\nConnection info: " 25 | desc += str(data["connection_info"]) 26 | 27 | return { 28 | "Title": data['name'], 29 | "Category": data['category'], 30 | "Description": desc, 31 | "Files": data['files'], 32 | "Tags": data['tags'], 33 | "Value": data['value'] 34 | } 35 | 36 | def get_meta(base, HEADERS): 37 | import requests as req 38 | 39 | solved = req.get(base + "api/v1/teams/me/solves", headers=HEADERS) 40 | if solved.status_code != 200: 41 | print("Can't get metadata. You can send me url which is smth like http://host/api/v1/smth/solves or just ignore it") 42 | url = input("URL (or nothing): ") 43 | solved = req.get(base + url, headers=HEADERS) 44 | 45 | try: 46 | solved = solved.json()["data"] 47 | 48 | data = {} 49 | for i in solved: 50 | data[int(i["challenge_id"])] = dict() 51 | data[int(i["challenge_id"])]["Solved"] = True 52 | 53 | except Exception as e: 54 | print("Something went wrong with meta") 55 | data = {} 56 | 57 | return data 58 | -------------------------------------------------------------------------------- /utils.py: -------------------------------------------------------------------------------- 1 | from termcolor import colored as clr 2 | 3 | category_to_color = { 4 | 'cry': 'green', 5 | 'pwn': 'magenta', 6 | 'rev': 'yellow', 7 | 'for': 'cyan', 8 | 'joy': 'red', 9 | 'web': 'blue' 10 | } 11 | 12 | def red(text): 13 | return clr(text, "red") 14 | 15 | def value_to_color(val): 16 | val = int(val) 17 | if val < 100: 18 | return "white" 19 | if val < 250: 20 | return "green" 21 | if val < 350: 22 | return "yellow" 23 | if val < 450: 24 | return "cyan" 25 | if val < 600: 26 | return "yellow" 27 | if val < 800: 28 | return "magenta" 29 | if val >= 800: 30 | return "red" 31 | 32 | def process_value(val): 33 | if int(val) >= 1000: 34 | return clr(f' {val} ', value_to_color(val), 'on_white', attrs=['bold']) 35 | elif int(val) >= 400: 36 | return clr(val, value_to_color(val), attrs=['bold']) 37 | else: 38 | return clr(val, value_to_color(val)) 39 | 40 | 41 | # TODO: clean these things 42 | 43 | def process_solved(data): 44 | if 'Solved' in data and data['Solved']: 45 | return clr('[ SOLVED ] ' 46 | , 'green' 47 | , attrs = ['bold']) 48 | else: 49 | return '' 50 | 51 | def process_category(data): 52 | if data["Category"][:3].lower() in category_to_color: 53 | return clr( data["Category"] 54 | , category_to_color[data["Category"][:3].lower()]) 55 | else: 56 | return data["Category"] 57 | 58 | def process_title(data, ls=False): 59 | if ls: 60 | return data["Title"] 61 | else: 62 | return clr(data["Title"] 63 | , value_to_color(data['Value']) 64 | , attrs=['bold']) 65 | 66 | 67 | def process_desc(text): 68 | return text 69 | 70 | def parse_request(http): 71 | # Now we need to parse request 72 | txt = open('/tmp/request', 'r').readlines() 73 | 74 | # http = (True if 'HTTPS/' not in txt[0] else False) 75 | 76 | headers = dict() 77 | for i in txt[1::]: 78 | hdr = i.replace('\n', '').split(': ') 79 | if len(hdr) > 2: 80 | print(i, i.split(': ')) 81 | raise TypeError('invalid request headers') 82 | 83 | headers[hdr[0]] = hdr[1] 84 | 85 | headers['Accept-Encoding'] = 'json' 86 | headers['If-Modified-Since'] = '0' 87 | url = ('https://' if not http else 'http://') + headers['Host'] + txt[0].split(' ')[1] 88 | 89 | return url, headers 90 | -------------------------------------------------------------------------------- /dumper.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import json 4 | import sys 5 | from os import listdir, makedirs, path 6 | 7 | import requests as req 8 | from utils import * 9 | 10 | DEBUG = False 11 | USE_HEADERS_FOR_FILES = False 12 | 13 | if '-h' in sys.argv: 14 | print( 15 | f"""~~ universal ctf dumper srcipt ~~ 16 | firstly, copy request to task json to /tmp/request 17 | 18 | usage: {sys.argv[0]} board_name [range default: 0-200] [http? default depends on board] [-om to get only metadata] 19 | results will be in ./task_name direcrory: 20 | 21 | example_task 22 | info.json 23 | file1.zip 24 | file2.png 25 | 26 | """) 27 | 28 | args = sys.argv[1::] 29 | 30 | if len(args) < 2: 31 | args.append('0-200') 32 | 33 | if len(args) < 3: 34 | args.append('from board') 35 | 36 | if len(args) < 4: 37 | args.append("False") 38 | 39 | 40 | if len(listdir("./")) != 0: 41 | print("This directory is not empty") 42 | act = input("Do you really want to continue? [y/N] ") 43 | if act != 'y': 44 | exit(0) 45 | 46 | 47 | def getTask(url, task_id): 48 | global files 49 | 50 | j = req.get(url, headers=HEADERS) 51 | 52 | if not j.ok: 53 | print('Now at id', task_id, end="\r") 54 | # print('Now at id', url.split('/')[board_info['task_id_in_url']], end="\r") 55 | return None 56 | 57 | print(f"[+] Got task with id { task_id }") 58 | 59 | # print(j.text) 60 | 61 | task = board.parse_task(j.text, id=task_id) 62 | if task is None: 63 | return None 64 | 65 | task["id"] = task_id 66 | 67 | try: 68 | print(f"[^] Title: { task['Title'] }, Category: { process_category(task) }, Value: { process_value(task['Value']) }") 69 | except KeyError: 70 | print(f"wtf {task=}") 71 | 72 | if not path.exists(task['Title']): 73 | makedirs(f'{task["Title"]}') 74 | else: 75 | print('Warning: Task is already downloaded') 76 | # print('rm -r * && !!') 77 | # exit(0) 78 | 79 | with open(f'./{task["Title"]}/info.json', 'w') as f: 80 | f.write(json.dumps(task)) 81 | 82 | for i in task['Files']: 83 | file_headers = HEADERS if USE_HEADERS_FOR_FILES else None 84 | 85 | if isinstance(i, str): 86 | name = i 87 | url = i 88 | if isinstance(i, dict): 89 | name = i["name"] 90 | url = i["url"] 91 | 92 | f = req.get(files.format(filename=url), headers=file_headers) 93 | 94 | while f.status_code != 200: 95 | print('Looks like there is something wrong with files url format string') 96 | print(f'{f.status_code=} {f.text=}') 97 | print(f"{f.history[0].url}") 98 | print(f"{f.url}") 99 | print('You can type it yourself, filename will be placed under {filename}') 100 | print('or type SKIP to skip') 101 | print(f'example filename: {url}') 102 | bs = base.strip("/") 103 | files = bs + input(f"{bs}") 104 | # if "SKIP" in files: 105 | # break 106 | 107 | ff = files.format(filename=url) 108 | print(f"getting {ff=}") 109 | f = req.get(ff, headers=file_headers, allow_redirects=True) 110 | 111 | with open(f'./{ task["Title"] }/{ name.split("/")[-1] }', 'wb') as file: 112 | file.write(f.content) 113 | 114 | 115 | if __name__ == '__main__': 116 | board_name = args[0] 117 | 118 | Range = list(map(int, args[1].split('-'))) 119 | Range[1] += 1 120 | 121 | board = __import__(f'board_configs.{ board_name }', fromlist=('wtf')) 122 | board_info = board.getInfo() 123 | 124 | http = args[2].lower() 125 | if http == 'from board': 126 | http = board_info['http?'] 127 | else: 128 | http = (True if args[2].lower() != 'false' else False) 129 | 130 | # parsed args, now parsing /tmp/request 131 | 132 | from utils import parse_request 133 | 134 | url, HEADERS = parse_request(http) 135 | url = url.split('/') 136 | if DEBUG: print(url) 137 | 138 | # /tmp/request parsed 139 | 140 | chals = url 141 | 142 | if board_info["task_id_in_url"] != None: 143 | chals[board_info["task_id_in_url"]] = "{id}" 144 | 145 | chals = '/'.join(chals) 146 | 147 | base = '/'.join(url[:3]) + '/' 148 | files = base + board_info["url_to_files"] # there should be {filename} in it 149 | 150 | if DEBUG: print(url, base, chals, files) 151 | 152 | if not board_info["custom_ids"]: 153 | ids = range(*Range) 154 | else: 155 | ids = board.get_ids(chals) 156 | 157 | urls = ((chals.format(id=str(Id)), Id) for Id in ids) 158 | 159 | if args[3] != "-om": 160 | for url, task_id in urls: 161 | getTask(url, task_id) 162 | 163 | print("getting metadata") 164 | meta = dict( board.get_meta(base, HEADERS) ) 165 | 166 | for task in listdir("./"): 167 | info = json.load(open(f"./{task}/info.json", 'r')) 168 | 169 | for key, val in meta.get(info["id"], {}).items(): 170 | info[key] = val 171 | 172 | json.dump(info, open(f"./{task}/info.json", 'w')) 173 | 174 | print("done.") 175 | 176 | 177 | --------------------------------------------------------------------------------