iT邦幫忙

2025 iThome 鐵人賽

DAY 18
0
Security

Izumi從零開始的30日WEB馬拉松系列 第 18

Day-18 認識SQL Injection

  • 分享至 

  • xImage
  •  

今天我們來學SQL注入攻擊,這是Web最經典的攻擊方式之一。

SQLi是什麼?

SQL Injection(SQLi)是指攻擊者在使用者輸入處(例如表單、查詢字串、HTTP 標頭等)插入惡意的 SQL 片段,使得後端資料庫執行非預期的 SQL 語句。若程式把使用者輸入直接拼接到 SQL 字串中(而非使用參數化查詢),就可能被當成 SQL 語法執行,造成資料外洩、繞過驗證、資料刪改甚至取得系統權限。

最經典的例子:登入繞過

假設網站登入程式碼為

SELECT * FROM users
WHERE username = '使用者輸入的帳號'
  AND password = '使用者輸入的密碼';

若使用者輸入 username = izumi、password = 2006,實際執行:

SELECT * FROM users
WHERE username = 'izumi'
  AND password = '2006';

若攻擊者把帳號輸入為(密碼隨便填):

' OR 1=1--

則 SQL 會變成:

SELECT * FROM users
WHERE username = '' OR 1=1--'
  AND password = '任意';

很神奇吧!這樣就能繞過登入系統,可以直接登入了,那麼我們來解釋一下為什麼這樣能直接登入。同上,我們先假設網站程式碼長

SELECT * FROM users
WHERE username = '使用者輸入的帳號'
  AND password = '使用者輸入的密碼';

使用者輸入自己的帳號與密碼,伺服器就會尋找符合的資料,如果找到符合條件的資料就能登入成功。而惡意輸入會變成在使用者輸入' OR 1=1--,密碼則可以任意輸入,SQL會變成

SELECT * FROM users
WHERE username = '' OR 1=1--'
  AND password = '任意';

username = '',這條件不成立,因為資料表沒有空帳號,OR 1=1當中OR 的意思則是「或」而1=1 永遠為真,所以整個條件變成:username='' OR TRUE 整個條件永遠為真,--是SQL的註解符號,後面的AND password=就會被忽略導致密碼完全失效。

而除了示範的這種,SQLi還有好幾種類型,我們簡單來介紹一下:

1.錯誤型(Error-based SQLi)

利用資料庫回傳的錯誤訊息來推斷資料庫結構與內容,例如故意注入會造成語法錯誤或型別錯誤的 payload,從錯誤訊息中取得資訊。

2.盲注(Blind SQLi)

當應用不回傳錯誤或資料內容,但會根據查詢結果回傳不同的頁面/布林值(True/False)或行為(有/無),攻擊者透過一系列判定(例如逐位元測試)來推斷資料。

又可細分為:布林盲注(Boolean-based)與時間盲注(Time-based)。

3.聯合查詢(Union-based SQLi)

透過 UNION SELECT把攻擊者想要的資料合併到原本的查詢結果中,若應用將查詢結果直接顯示在頁面上,就能直接讀出資料。

4.時間盲注(Time-based SQLi)

透過觸發資料庫的延遲(SLEEP()、pg_sleep()等)來判斷某個條件是否為真,適用於無錯誤訊息、且頁面只回傳同一結果的情況。

5.堆疊查詢(Stacked queries/Multiple statements)

某些 DB(如MS SQL)允許同一請求中執行多條 SQL(例如 ; DROP TABLE users;),攻擊者可利用此特性直接執行破壞性的語句(現代應用常禁用或不支援多語句執行)。

6.基於檔案/存儲程序的攻擊(視DB與環境)

利用資料庫的檔案寫入/外部函數呼叫或存儲程序漏洞進一步攻擊系統(例如寫入 webshell)。

防禦方法:

  • 參數化查詢/Prepared Statements/Parameterized Queries:最常見的的防禦方式,把 SQL 與資料分離,資料會被視為純值,而不會被解讀為 SQL 語法片段

  • 最小權限原則:資料庫使用的帳號應只有必要權限(例如只允許SELECT而非DROP)

  • 輸入驗證(Input Validation):白名單檢查(例如長度、格式檢查),但單靠驗證不足以防 SQLi(仍應參數化)

  • 錯誤訊息處理:不要向使用者回傳詳細的 DB錯誤(會洩露結構),應記錄在伺服端但向使用者顯示泛化錯誤訊息。

今日實作

今天的實作目的是要讓大家能親自觀察「登入繞過」與「UNION-based資料洩漏」的SQLi運作過程,然後學習如何修復。

1.準備測試環境
先將SQLi.py存下來

# SQLi.py
from flask import Flask, request, g, render_template_string
import sqlite3
import os

DB = 'sqli_demo.db'
app = Flask(__name__)
app.config['SECRET_KEY'] = 'dev-key-only-for-lab'

# --- DB helper ---
def get_db():
    db = getattr(g, '_database', None)
    if db is None:
        db = g._database = sqlite3.connect(DB)
        db.row_factory = sqlite3.Row
    return db

@app.teardown_appcontext
def close_db(exc):
    db = getattr(g, '_database', None)
    if db is not None:
        db.close()

# --- setup (init) ---
def init_db():
    if os.path.exists(DB):
        os.remove(DB)
    conn = sqlite3.connect(DB)
    c = conn.cursor()
    # users table
    c.execute('CREATE TABLE users (id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT, password TEXT);')
    c.execute("INSERT INTO users (username, password) VALUES ('izumi', 'izumipw');")
    c.execute("INSERT INTO users (username, password) VALUES ('saber', 'saberpw');")
    # products table
    c.execute('CREATE TABLE products (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT);')
    c.execute("INSERT INTO products (name) VALUES ('Oriflamme');")
    c.execute("INSERT INTO products (name) VALUES ('Excalibur');")
    conn.commit()
    conn.close()

# --- templates (tiny) ---
form_tpl = """
<h2>{{title}}</h2>
<form method="post" action="{{action}}">
  username: <input name="username"><br>
  password: <input name="password"><br>
  <button type="submit">Login</button>
</form>
<pre>{{result}}</pre>
<hr>
<p>Note: Only for local testing.</p>
"""

search_tpl = """
<h2>Product Search (vulnerable)</h2>
<form method="get" action="/search">
  Search: <input name="q" value="{{q}}"><button type="submit">Search</button>
</form>
<pre>{{result}}</pre>
"""

# --- vulnerable login (unsafe string concatenation) ---
@app.route('/vuln_login', methods=['GET', 'POST'])
def vuln_login():
    result = ''
    if request.method == 'POST':
        u = request.form.get('username','')
        p = request.form.get('password','')
        db = get_db()
        # DANGEROUS: string formatting directly into SQL
        sql = "SELECT * FROM users WHERE username = '%s' AND password = '%s';" % (u, p)
        cur = db.execute(sql)
        rows = cur.fetchall()
        result = f"Executed SQL:\n{sql}\n\nRows returned: {len(rows)}\n"
        for r in rows:
            result += dict(r).__repr__() + "\n"
    return render_template_string(form_tpl, title='VULNERABLE LOGIN', action='/vuln_login', result=result)

# --- safe login (parameterized query) ---
@app.route('/safe_login', methods=['GET', 'POST'])
def safe_login():
    result = ''
    if request.method == 'POST':
        u = request.form.get('username','')
        p = request.form.get('password','')
        db = get_db()
        # SAFE: parameterized query (the ? placeholders)
        sql = "SELECT * FROM users WHERE username = ? AND password = ?;"
        cur = db.execute(sql, (u, p))
        rows = cur.fetchall()
        result = f"Executed SQL (parameterized):\n{sql}\n\nRows returned: {len(rows)}\n"
        for r in rows:
            result += dict(r).__repr__() + "\n"
    return render_template_string(form_tpl, title='SAFE LOGIN', action='/safe_login', result=result)

# --- vulnerable search demonstrating UNION-based exfil ---
@app.route('/search', methods=['GET'])
def search():
    q = request.args.get('q','')
    db = get_db()
    # Vulnerable: naive LIKE with direct interpolation
    sql = f"SELECT name FROM products WHERE name LIKE '%{q}%';"
    cur = db.execute(sql)
    rows = cur.fetchall()
    result = f"Executed SQL:\n{sql}\n\nRows:\n"
    for r in rows:
        result += str(dict(r)) + "\n"
    return render_template_string(search_tpl, q=q, result=result)

# --- index ---
@app.route('/')
def index():
    return "<h3>SQLi demo (local only)</h3><ul>" + \
           "<li><a href='/vuln_login'>VULNERABLE Login</a></li>" + \
           "<li><a href='/safe_login'>SAFE Login</a></li>" + \
           "<li><a href='/search'>Vulnerable Search (UNION demo)</a></li>" + \
           "</ul>"

if __name__ == '__main__':
    init_db()
    app.run(port=5000, debug=False)

2.執行並在瀏覽器輸入http://127.0.0.1:5000
應該會看到跟我一樣的畫面https://ithelp.ithome.com.tw/upload/images/20250928/20178008MZOSzKMVPj.png

3.測試脆弱版登入系統
3-1.
點擊第一個選項VULNERABLE LOGIN
會進到這個頁面https://ithelp.ithome.com.tw/upload/images/20250928/20178008vT1eKkghcg.png
3-2.輸入「' OR 1=1--」,密碼隨便都行
3-3.觀察結果,我這邊輸入的是「' OR 1=1--」跟0430,附圖如下
https://ithelp.ithome.com.tw/upload/images/20250928/20178008Cbtugfn8uv.png
可以看到我們成功登入系統,且回傳給我們所有使用者與他們的密碼,這表示我們攻擊成功(這邊是因為我們程式設定攻擊成功後顯示所有使用者,並非所有網站攻擊成功後都是這種輸出方式)

4.測試安全版
4-1.
點擊第二個選項
進入這個頁面
https://ithelp.ithome.com.tw/upload/images/20250928/20178008BsKn8fFO3M.png
4-2.輸入與剛才一模一樣的payload
4-3.觀察結果,發現不再顯示所有使用者資訊https://ithelp.ithome.com.tw/upload/images/20250928/20178008hfUZN9Nl3a.png
這是因為執行的是參數化查詢(SELECT ... WHERE username = ? AND password = ?),此時資料庫把 '? OR 1=1--' 視為純字串,比對失敗導致無法繞過

5.UNION-based資料洩漏(/search範例)
5-1.
點擊第三個選項
進入這個頁面
https://ithelp.ithome.com.tw/upload/images/20250928/201780087sqg0pCNoC.png
5-2.payload我們輸入' UNION SELECT username || ':' || password FROM users--
5-3.觀察結果,發現連使用者都跑出來了!如圖https://ithelp.ithome.com.tw/upload/images/20250928/20178008gdakjj2N0V.png
解說:透過 UNION SELECT,攻擊者把users表的資料「合併」到原本的查詢結果中,若應用直接把查詢結果顯示給使用者,就能看到敏感資料

由此我們可以發現,SQL注入很容易可以竊取資料,因此防禦很重要,通常一定會加上如第四步驟演示的參數化查詢,這大大減少了被SQL注入的機會

今日小節

今天我們學了基本的SQLi類以及實作,也了解到為什麼要防禦SQLi的重要性,明天我們將會介紹TLS


上一篇
Day17-認識TCP/IP及網路通訊基礎
下一篇
Day19 -認識TLS
系列文
Izumi從零開始的30日WEB馬拉松22
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言