Vulnerability Detail Report
Vulnerability Overview
- ZDID: ZD-2026-00047
- Vendor: 批踢踢實業坊
- Title: PTT Buffer overflow
- Introduction: 將過長的字串複製至全域變數字串造成緩衝區溢出,進而可修改其他全域變數
處理狀態
目前狀態
-
新提交
-
已審核
-
已通報
-
未回報修補狀況
-
未複測
-
公開
處理歷程
- 2026/01/10 04:46:51 : 新提交 (由 藍富賢 更新此狀態)
- 2026/01/12 19:24:39 : 審核完成 (由 HITCON ZeroDay 服務團隊 更新此狀態)
- 2026/01/27 12:30:11 : 審核完成 (由 HITCON ZeroDay 服務團隊 更新此狀態)
- 2026/01/27 12:30:11 : 修補中 (由 HITCON ZeroDay 服務團隊 更新此狀態)
- 2026/01/27 12:30:12 : 修補中 (由 HITCON ZeroDay 服務團隊 更新此狀態)
- 2026/01/29 12:24:19 : 修補中 (由 HITCON ZeroDay 服務團隊 更新此狀態)
- 2026/03/23 15:13:22 : 修補中 (由 HITCON ZeroDay 服務團隊 更新此狀態)
- 2026/03/26 03:00:05 : 公開 (由 HITCON ZeroDay 平台自動更新)
詳細資料
- ZDID:ZD-2026-00047
- 通報者:bluerichwise (藍富賢)
- 風險:高
- 類型:溢位攻擊 (Overflow)
參考資料
相關網址
https://github.com/ptt/pttbbs
敘述
以下結果均僅在本地編譯後測試,並無實際攻擊站台
1. 引文時 Global buffer overflow
File: mbbsd/edit.c
1480: static void
1481: do_quote(void)
1482: {
...
1509: if ((curr_buf->flags & EDITFLAG_KIND_SENDMAIL) &&
1510: (curr_buf->flags & EDITFLAG_KIND_REPLYPOST) &&
1511: (str = strchr(quote_user, '.'))) {
1512: strcpy(++str, ptr);
1513: str = strchr(str, ' ');
1514: assert(str);
1515: str[0] = '\0';
1516: }
File: mbbsd/var.c
136: char quote_user[80] = "\0";
在第 1512 行中,只要 ptr 長度大於 sizeof(quote_user) (80) 就會造成 global buffer overflow
所以只要我們建立出一個貼文符合
- owner 包含 '.'
- 第一行 "作者:" 後的長度超出 80
就可以利用回文+回信功能觸發
在支援匿名的看板中(如 ptt1 的某些檢舉板),可以讓 owner 變成 "Anonymous." 等包含 '.' 在末尾的 ID
一般情況下匿名文章無法編輯,也就無法讓處於 header 的 "作者:" 超出 80
此處的做法是讓使用者發布兩篇文章,一篇以真實ID發文,另一篇以匿名發文,並讓兩篇文章擁有相同的檔名
就可以藉由真實ID處編輯文章,最後以匿名ID處觸發漏洞
PTT的檔名組成為 M.<unix_time>.A.<random>
時間部分只要在同一秒內發文即可,隨機值部分,因為使用的是 random(),seed 總共只有 2^32 種,
可以在獲取若干隨機數後輕易反推出 seed ,進而控制讓兩篇文章隨機值相同
具體流程如下:
- (connection-0) 以真實 id 發布文章 A, 檔名
M.XXX.A.YYY - (connection-0) 進入文章 A 編輯模式
- (connection-1) 刪除文章 A, 此時檔名
M.XXX.A.YYY不存在 - (connection-2) 以匿名 "Anonymous." 發布文章 B,檔名
M.XXX.A.YYY - (connection-0) 結束編輯,寫回
M.XXX.A.YYY
就可以讓 "Anonymous." 處的內文包含任意 header
若有開啟 EDITPOST_SMARTMERGE,則必須讓文章 A 和文章 B 的 partial hash 相同
但因為使用的是 FNV64,只有 2^64 種狀態,平均只需約 2^32 這個數量級的嘗試次數便可找出 collision
以下是測試成功後的 .DIR 內容 (我在本地創建 Anon 匿名看板,使用 "anon." 作為匿名ID)
$ xxd boards/A/Anon/.DIR
00000280: 2e64 656c 6574 6564 0032 3032 2e41 2e32 .deleted.202.A.2
00000290: 3334 0000 0000 0000 0000 0000 e9e4 5f69 34............_i
000002a0: 0000 2d00 6572 0000 0000 0000 0000 0000 ..-.er..........
000002b0: 2031 2f30 3800 28a5 bba4 e5a4 77b3 51a7 1/08.(.....w.Q.
000002c0: 52b0 a329 205b 7573 6572 5d00 0000 0000 R..) [user].....
000002d0: 0000 0000 0000 0000 0000 0000 0000 0000 ................
000002e0: 0000 0000 0000 0000 0000 0000 0000 0000 ................
000002f0: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000300: 4d2e 3137 3637 3839 3232 3032 2e41 2e32 M.1767892202.A.2
00000310: 3334 0000 0000 0000 0000 0000 0000 0000 34..............
00000320: 0000 616e 6f6e 2e00 0000 0000 0000 0000 ..anon..........
00000330: 2031 2f30 3800 616e 6f6e 0000 0000 0000 1/08.anon......
00000340: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000350: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000360: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000370: 0000 0000 0000 0000 0200 0000 8000 0000 ................
可以看到檔名欄分別為 ".deleted\0202.A.234" 以及 "M.1767892202.A.234"
原本檔名相同,".deleted\0" 是刪除時覆蓋的
此時在從匿名文章處回文並選擇同時回應看板並回信便可觸發
=================================================================
==846939==ERROR: AddressSanitizer: global-buffer-overflow on address 0x5615bc2b9d30 at pc 0x7f6b879ca6a1 bp 0x7ffe5b1c1ac0 sp 0x7ffe5b1c1280
WRITE of size 212 at 0x5615bc2b9d30 thread T0
#0 0x7f6b879ca6a0 in strcpy ../../../../src/libsanitizer/asan/asan_interceptors.cpp:563
#1 0x5615bc02a328 in do_quote /home/bbs/pttbbs/mbbsd/edit.c:1512
#2 0x5615bc0367d5 in vedit2 /home/bbs/pttbbs/mbbsd/edit.c:3591
#3 0x5615bc050939 in do_post_article /home/bbs/pttbbs/mbbsd/bbs.c:1413
#4 0x5615bc051ec3 in do_post /home/bbs/pttbbs/mbbsd/bbs.c:1637
#5 0x5615bc05220a in do_generalboardreply /home/bbs/pttbbs/mbbsd/bbs.c:1685
#6 0x5615bc052875 in do_reply /home/bbs/pttbbs/mbbsd/bbs.c:1743
#7 0x5615bc0528d9 in reply_post /home/bbs/pttbbs/mbbsd/bbs.c:1752
#8 0x5615bc074e37 in i_read_key /home/bbs/pttbbs/mbbsd/read.c:975
#9 0x5615bc0768ba in i_read /home/bbs/pttbbs/mbbsd/read.c:1259
#10 0x5615bc062dfb in Read /home/bbs/pttbbs/mbbsd/bbs.c:4582
#11 0x5615bc0850fe in choose_board /home/bbs/pttbbs/mbbsd/board.c:1954
#12 0x5615bc087b7d in Favorite /home/bbs/pttbbs/mbbsd/board.c:2320
#13 0x5615bc101cbf in domenu /home/bbs/pttbbs/mbbsd/menu.c:515
#14 0x5615bc103c7a in main_menu /home/bbs/pttbbs/mbbsd/menu.c:1011
#15 0x5615bc0cae02 in main /home/bbs/pttbbs/mbbsd/mbbsd.c:1840
#16 0x7f6b8775bd67 in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58
#17 0x7f6b8775be24 in __libc_start_main_impl ../csu/libc-start.c:360
#18 0x5615bc018dc0 in _start (/home/bbs/bin/mbbsd.202601081141+0x74dc0) (BuildId: 3551b7c758d48a705be810e02075cc0f86829d08)
0x5615bc2b9d30 is located 0 bytes after global variable 'quote_user' defined in 'var.c:136:17' (0x5615bc2b9ce0) of size 80
0x5615bc2b9d30 is located 48 bytes before global variable 'currtitle' defined in 'var.c:137:17' (0x5615bc2b9d60) of size 65
SUMMARY: AddressSanitizer: global-buffer-overflow ../../../../src/libsanitizer/asan/asan_interceptors.cpp:563 in strcpy
Shadow bytes around the buggy address:
0x5615bc2b9a80: f9 f9 f9 f9 f9 f9 f9 f9 00 00 00 00 00 00 00 00
0x5615bc2b9b00: 00 00 00 00 00 f9 f9 f9 f9 f9 f9 f9 04 f9 f9 f9
0x5615bc2b9b80: f9 f9 f9 f9 04 f9 f9 f9 f9 f9 f9 f9 04 f9 f9 f9
0x5615bc2b9c00: f9 f9 f9 f9 04 f9 f9 f9 f9 f9 f9 f9 00 00 00 00
0x5615bc2b9c80: 00 00 00 00 00 00 f9 f9 f9 f9 f9 f9 00 00 00 00
=>0x5615bc2b9d00: 00 00 00 00 00 00[f9]f9 f9 f9 f9 f9 00 00 00 00
0x5615bc2b9d80: 00 00 00 00 01 f9 f9 f9 f9 f9 f9 f9 00 00 00 00
0x5615bc2b9e00: 00 06 f9 f9 f9 f9 f9 f9 00 00 00 00 00 00 00 00
0x5615bc2b9e80: f9 f9 f9 f9 04 f9 f9 f9 f9 f9 f9 f9 04 f9 f9 f9
0x5615bc2b9f00: f9 f9 f9 f9 04 f9 f9 f9 f9 f9 f9 f9 04 f9 f9 f9
0x5615bc2b9f80: f9 f9 f9 f9 00 00 00 00 00 00 00 00 00 00 00 00
Shadow byte legend (one shadow byte represents 8 application bytes):
Addressable: 00
Partially addressable: 01 02 03 04 05 06 07
Heap left redzone: fa
Freed heap region: fd
Stack left redzone: f1
Stack mid redzone: f2
Stack right redzone: f3
Stack after return: f5
Stack use after scope: f8
Global redzone: f9
Global init order: f6
Poisoned by user: f7
Container overflow: fc
Array cookie: ac
Intra object redzone: bb
ASan internal: fe
Left alloca redzone: ca
Right alloca redzone: cb
==846939==ABORTING
2. 多處下棋重播模式未檢查輸入導致 OOB read/write
下棋結束時可以將棋譜寄回信箱,並在閱讀時按 z 進入打譜模式
但打譜模式同樣能播放使用者自行撰寫的棋譜
由於許多地方並未檢查棋譜的合法性導致多處 OOB read/write
gochess_replay 使用未初始化變數
File: mbbsd/ch_go.c
969: ChessInfo*
970: gochess_replay(FILE* fp)
...
978: go_step_t step;
...
1062: while ((ch = GETC()) != EOF && ch != ')') {
1063: if (ch == ';')
1064: ChessHistoryAppend(info, &step);
此處若第一個字元就是 ';' 便會 append 未初始化的 step,若能控制 step 的值,
便能在 go_getstep 處 OOB read/write
File: mbbsd/ch_go.c
416: static char*
417: go_getstep(const go_step_t* step, char buf[])
418: {
419: static const char* const ColName = "ABCDEFGHJKLMNOPQRST";
420: static const char* const RawName = "19181716151413121110987654321";
421: static const int ansi_length = sizeof(ANSI_COLOR(30;43)) - 1;
422:
423: strcpy(buf, turn_color[step->color]);
424: buf[ansi_length ] = ColName[step->loc.c * 2];
425: buf[ansi_length + 1] = ColName[step->loc.c * 2 + 1];
426: buf[ansi_length + 2] = RawName[step->loc.r * 2];
427: buf[ansi_length + 3] = RawName[step->loc.r * 2 + 1];
428: strcpy(buf + ansi_length + 4, ANSI_RESET " ");
429:
430: return buf;
431: }
能夠在 423 行處 OOB read/write
其他比較不嚴重、跟打譜相關的
mbbsd/ch_gomo.cgomo_getstep: BRDSIZ 為 15, 但 Colname 只支援 14 個mbbsd/ch_gomo.cdirchk: 打譜模式五顆子連線後還可繼續下,pat_gomoku沒考慮這點,若是 "BBBBB.BBBBB" (B=黑棋),下在正中間會讓 loc 過大而 OOB readmbbsd/chc.cchc_replay: 若棋譜第一步為 "1. C\n" 等不完整的會在 943~946 行 OOB readmbbsd/chess.cChessReplayGame: 以 "<\n" 開頭時會在 1286 行 OOB read
3. 其他沒測過但覺得應該有問題的
- SIGWINCH 的 signal handler 是
sig_term_resize, 呼叫了term_resize,進而呼叫resizeterm_within,最終使用了realloc/malloc,並非 async-signal-safe
例如 CVE-2024-6387 便利用 signal handler 內的 malloc/free 達成 RCE mbbsd/kaede.cstrip_ansi_movecmd: 行末以 ESC_CHR 結尾時會將 '\0' 覆蓋為 's' 進而 OOB readmbbsd/mbbsd.cuser_login: 使用者登入流程為pwcuLoginSave()->restore_backup()->check_BM()。pwcuLoginSave()更新完後,使用者可以在restore_backup等待使用者輸入時刻意斷線,下次登入時is_first_login_of_today判斷會失效,便可永遠不用經過check_BMcommon/sys/string.cstr_decode_M3: 輸入是 "=??Q?" 時 str_iconv 使用未初始化的 charset[]common/sys/string.cstr_decode_M3: mmdecode 有可能回傳 -1, str_iconv 的參數為 size_t, 會 underflow 後 buffer overread