evil-calculator write up
Ans: AIS3{fake_flag}
進來網站後,會看到一個像計算機的東西。

這裡先隨意輸入 1+1,看他會做什麼事。


可以發現在我們做計算時,會送出一個 POST 請求,並且檔案是以 JSON 格式傳送,這裡我們攔截請求,先去看看他後端怎麼運作。

index.html 完整原始碼
在前端的程式碼中,前半段是一般計算機計算跟字元顯示的設定,但後半段傳送 POST 請求中,有一個問題。
這裡會把我們的輸入轉成 json 傳到後端做處理,但他這裡並沒有做任何的資料檢查或過濾,意味著我們輸什麼,他就送甚麼進去。
app.py 完整程式碼
這裡有一段程式碼是針對我們的輸入做處理,但沒有很嚴謹。
1. 先接收使用者的輸入
而使用者可以控制 data['expression'],這是用戶完全可控的輸入。
2. 接著對輸入做過濾
這只把空格和 _ 拿掉,但沒處理其他危險字符,例如:
.(用來存取屬性)。
__class__、__subclasses__()(物件鍊)。
[]、{}、()、'、" 都還可以用。
只要 payload 沒依賴 _,都能執行成功。
3. 再來是最重要的部分:eval()
這會把輸入的字串當成原生 Python 程式碼直接執行,例如:
執行起來就是:
- 讀檔。
- 反彈 shell、刪除檔案。
- 這是標準的 Remote Code Execution (RCE)。
4. 然後是例外處理
這裡只會把錯誤訊息包成字串,回傳給前端,不是保護,只是「讓伺服器不要炸掉」。
5. 最後回傳結果
他把 eval() 的結果送回給前端,這也代表可以用他來回傳敏感資料,例如:
攻擊
程式中的 /calculate 路由接收 JSON 格式的 POST 請求,然後從中提取 expression 參數。該值僅進行了 .replace(" ","").replace("_","") 的處理,接著就直接被傳入 eval()。由於 eval() 能執行任意 Python 程式碼,因此我可以構造惡意輸入,例如:

雖然是拿到 fake_flag,後來有嘗試 ls,這裡因為他鎖 _,我沒辦法用 import,所以直接用 chr 一個一個拼出來,但並沒有給其他有用的檔。

evil-calculator write up
進來網站後,會看到一個像計算機的東西。
這裡先隨意輸入 1+1,看他會做什麼事。
可以發現在我們做計算時,會送出一個 POST 請求,並且檔案是以 JSON 格式傳送,這裡我們攔截請求,先去看看他後端怎麼運作。
index.html 完整原始碼
<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 請求中,有一個問題。
const expression = expressionScreen.value; fetch('/calculate', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({expression: expression}), })這裡會把我們的輸入轉成 json 傳到後端做處理,但他這裡並沒有做任何的資料檢查或過濾,意味著我們輸什麼,他就送甚麼進去。
app.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)這裡有一段程式碼是針對我們的輸入做處理,但沒有很嚴謹。
@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'],這是用戶完全可控的輸入。data = request.json expression = data['expression']2. 接著對輸入做過濾
這只把空格和
_拿掉,但沒處理其他危險字符,例如:.(用來存取屬性)。__class__、__subclasses__()(物件鍊)。[]、{}、()、'、"都還可以用。只要 payload 沒依賴
_,都能執行成功。expression = data['expression'].replace(" ","").replace("_","")3. 再來是最重要的部分:
eval()這會把輸入的字串當成原生 Python 程式碼直接執行,例如:
{"expression":"open('/flag.txt').read()"}執行起來就是:
eval("open('/flag.txt').read()")result = eval(expression)4. 然後是例外處理
這裡只會把錯誤訊息包成字串,回傳給前端,不是保護,只是「讓伺服器不要炸掉」。
except Exception as e: result = str(e)5. 最後回傳結果
return jsonify(result=str(result))他把
eval()的結果送回給前端,這也代表可以用他來回傳敏感資料,例如:{"expression":"open('/etc/passwd').read()"}攻擊
程式中的
/calculate路由接收 JSON 格式的 POST 請求,然後從中提取expression參數。該值僅進行了.replace(" ","").replace("_","")的處理,接著就直接被傳入eval()。由於eval()能執行任意 Python 程式碼,因此我可以構造惡意輸入,例如:{"expression":"open('/flag.txt').read()"}雖然是拿到 fake_flag,後來有嘗試 ls,這裡因為他鎖
_,我沒辦法用 import,所以直接用 chr 一個一個拼出來,但並沒有給其他有用的檔。