From e286e2b6deb24787622713f0bcc3b0ffb32d94fe Mon Sep 17 00:00:00 2001 From: JJ-8 <34778827+JJ-8@users.noreply.github.com> Date: Mon, 5 Dec 2022 09:58:26 +0100 Subject: [PATCH 1/2] Add HTB parser This parses the tasks of the CTF website https://ctf.hackthebox.com/ The tasks can be copied from the network tab. A direct GET-request to the endpoint does not work, since the endpoint requires authentication with a bearer token in the Authorization header. The number to category mapping seems to be static and comes from the https://ctf.hackthebox.com/api/public/challengeCategories endpoint, which is publicly accessible. This parser also imports the challenge description. To allow this, the `ParsedTask` type is modified to include an optional `description` field. The object passed to `createTask` is also implicit, to include the description in the object. This commit also includes small code refactoring: - The `ParsedTask` type is not duplicated anymore, but imported from `parsers/index.ts`. - Because of this, the `keep` field is also added to fix typing in `TaskImportDialog.vue`. - The dialog now only hides when the importing is fully finished to prevent confusion with an empty screen after importing (due to delay of importing). The button displays the loading progress. --- .../components/Dialogs/TaskImportDialog.vue | 22 +++--- front/src/ctfnote/parsers/htb.ts | 79 +++++++++++++++++++ front/src/ctfnote/parsers/index.ts | 5 +- 3 files changed, 92 insertions(+), 14 deletions(-) create mode 100644 front/src/ctfnote/parsers/htb.ts diff --git a/front/src/components/Dialogs/TaskImportDialog.vue b/front/src/components/Dialogs/TaskImportDialog.vue index 6242abfbd..3dcf21c72 100644 --- a/front/src/components/Dialogs/TaskImportDialog.vue +++ b/front/src/components/Dialogs/TaskImportDialog.vue @@ -49,6 +49,7 @@ color="positive" :disable="btnDisable" :label="btnLabel" + :loading="loading" @click="btnClick" /> @@ -60,15 +61,9 @@ import { useDialogPluginComponent } from 'quasar'; import ctfnote from 'src/ctfnote'; import { Ctf } from 'src/ctfnote/models'; -import parsers from 'src/ctfnote/parsers'; +import parsers, { ParsedTask } from 'src/ctfnote/parsers'; import { defineComponent, ref } from 'vue'; -type ParsedTask = { - title: string; - category: string; - keep: boolean; -}; - export default defineComponent({ props: { ctf: { type: Object as () => Ctf, required: true }, @@ -96,6 +91,7 @@ export default defineComponent({ onDialogHide, onDialogOK, onDialogCancel, + loading: ref(false), }; }, computed: { @@ -151,19 +147,19 @@ export default defineComponent({ return { ...task, keep: !taskSet.has(hash) }; }); }, - btnClick() { + async btnClick() { if (this.tab == 'parse') { this.parsedTasks = this.parseTasks(this.model); this.tab = 'confirm'; } else { - this.parsedTasks + this.loading = true; + const result = this.parsedTasks .filter((t) => t.keep) .map((task) => { - void this.createTask(this.ctf.id, { - title: task.title, - category: task.category, - }); + return this.createTask(this.ctf.id, task); }); + await Promise.all(result); + this.loading = false; this.onDialogOK(); } }, diff --git a/front/src/ctfnote/parsers/htb.ts b/front/src/ctfnote/parsers/htb.ts new file mode 100644 index 000000000..cb8e62f74 --- /dev/null +++ b/front/src/ctfnote/parsers/htb.ts @@ -0,0 +1,79 @@ +import { ParsedTask, Parser } from '.'; +import { parseJson, parseJsonStrict } from '../utils'; + +// output of https://ctf.hackthebox.com/api/public/challengeCategories +const challengeCategories: { [index: number]: string } = { + 1: 'Fullpwn', + 2: 'Web', + 3: 'Pwn', + 4: 'Crypto', + 5: 'Reversing', + 6: 'Stego', + 7: 'Forensics', + 8: 'Misc', + 9: 'Start', + 10: 'PCAP', + 11: 'Coding', + 12: 'Mobile', + 13: 'OSINT', + 14: 'Blockchain', + 15: 'Hardware', + 16: 'Warmup', + 17: 'Attack', + 18: 'Defence', + 20: 'Cloud', + 21: 'Scada', +}; + +const HTBParser: Parser = { + name: 'HTB parser', + hint: 'paste https://ctf.hackthebox.com/api/ctf/ from the network tab', + + parse(s: string): ParsedTask[] { + const tasks = []; + const data = parseJsonStrict<{ + challenges: Array<{ + id: number; + name: string; + description: string; + challenge_category_id: number; + }>; + }>(s); + if (data == null) { + return []; + } + + for (const challenge of data.challenges) { + if ( + !challenge.description || + !challenge.name || + !challenge.challenge_category_id + ) { + continue; + } + + let category = challengeCategories[challenge.challenge_category_id]; + if (category == null) category = 'Unknown'; + + tasks.push({ + title: challenge.name, + category: category, + description: challenge.description, + }); + } + return tasks; + }, + isValid(s) { + const data = parseJson<{ + challenges: Array<{ + id: number; + name: string; + description: string; + challenge_category_id: number; + }>; + }>(s); + return data != null; + }, +}; + +export default HTBParser; diff --git a/front/src/ctfnote/parsers/index.ts b/front/src/ctfnote/parsers/index.ts index cf9181058..35bd25a38 100644 --- a/front/src/ctfnote/parsers/index.ts +++ b/front/src/ctfnote/parsers/index.ts @@ -1,10 +1,13 @@ import CTFDParser from './ctfd'; import ECSCParser from './ecsc'; import RawParser from './raw'; +import HTBParser from './htb'; export type ParsedTask = { title: string; category: string; + description?: string; + keep?: boolean; }; export type Parser = { @@ -14,4 +17,4 @@ export type Parser = { parse(s: string): ParsedTask[]; }; -export default [RawParser, CTFDParser, ECSCParser]; +export default [RawParser, CTFDParser, ECSCParser, HTBParser]; From 928497d04f23e3f19ab81b817da6def240dae5bb Mon Sep 17 00:00:00 2001 From: JJ-8 <34778827+JJ-8@users.noreply.github.com> Date: Sat, 10 Dec 2022 16:03:16 +0100 Subject: [PATCH 2/2] Fix HTB parser detection --- front/src/ctfnote/parsers/htb.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/front/src/ctfnote/parsers/htb.ts b/front/src/ctfnote/parsers/htb.ts index cb8e62f74..ca81b69a8 100644 --- a/front/src/ctfnote/parsers/htb.ts +++ b/front/src/ctfnote/parsers/htb.ts @@ -72,7 +72,7 @@ const HTBParser: Parser = { challenge_category_id: number; }>; }>(s); - return data != null; + return Array.isArray(data?.challenges); }, };