PTT Buffer overflow - HITCON ZeroDay

Vulnerability Detail Report

Vulnerability Overview

  • ZDID: ZD-2026-00047
  •  發信 Vendor: 批踢踢實業坊
  • Title: PTT Buffer overflow
  • Introduction: 將過長的字串複製至全域變數字串造成緩衝區溢出,進而可修改其他全域變數

處理狀態

目前狀態

公開
Last Update : 2026/03/26
  • 新提交
  • 已審核
  • 已通報
  • 未回報修補狀況
  • 未複測
  • 公開

處理歷程

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

參考資料

暫無資料
(本欄位資訊由系統根據漏洞類別自動產生,做為漏洞參考資料。)

相關網址

ptt.cc
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
所以只要我們建立出一個貼文符合

  1. owner 包含 '.'
  2. 第一行 "作者:" 後的長度超出 80

就可以利用回文+回信功能觸發

在支援匿名的看板中(如 ptt1 的某些檢舉板),可以讓 owner 變成 "Anonymous." 等包含 '.' 在末尾的 ID
一般情況下匿名文章無法編輯,也就無法讓處於 header 的 "作者:" 超出 80
此處的做法是讓使用者發布兩篇文章,一篇以真實ID發文,另一篇以匿名發文,並讓兩篇文章擁有相同的檔名
就可以藉由真實ID處編輯文章,最後以匿名ID處觸發漏洞

PTT的檔名組成為 M.<unix_time>.A.<random>
時間部分只要在同一秒內發文即可,隨機值部分,因為使用的是 random(),seed 總共只有 2^32 種,
可以在獲取若干隨機數後輕易反推出 seed ,進而控制讓兩篇文章隨機值相同

具體流程如下:

  1. (connection-0) 以真實 id 發布文章 A, 檔名 M.XXX.A.YYY
  2. (connection-0) 進入文章 A 編輯模式
  3. (connection-1) 刪除文章 A, 此時檔名 M.XXX.A.YYY 不存在
  4. (connection-2) 以匿名 "Anonymous." 發布文章 B,檔名 M.XXX.A.YYY
  5. (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

其他比較不嚴重、跟打譜相關的

  1. mbbsd/ch_gomo.c gomo_getstep: BRDSIZ 為 15, 但 Colname 只支援 14 個
  2. mbbsd/ch_gomo.c dirchk: 打譜模式五顆子連線後還可繼續下,pat_gomoku 沒考慮這點,若是 "BBBBB.BBBBB" (B=黑棋),下在正中間會讓 loc 過大而 OOB read
  3. mbbsd/chc.c chc_replay: 若棋譜第一步為 "1. C\n" 等不完整的會在 943~946 行 OOB read
  4. mbbsd/chess.c ChessReplayGame: 以 "<\n" 開頭時會在 1286 行 OOB read

3. 其他沒測過但覺得應該有問題的

  1. SIGWINCH 的 signal handler 是 sig_term_resize, 呼叫了 term_resize,進而呼叫 resizeterm_within,最終使用了 realloc/malloc,並非 async-signal-safe
    例如 CVE-2024-6387 便利用 signal handler 內的 malloc/free 達成 RCE
  2. mbbsd/kaede.c strip_ansi_movecmd: 行末以 ESC_CHR 結尾時會將 '\0' 覆蓋為 's' 進而 OOB read
  3. mbbsd/mbbsd.c user_login: 使用者登入流程為 pwcuLoginSave() -> restore_backup() -> check_BM()pwcuLoginSave() 更新完後,使用者可以在 restore_backup 等待使用者輸入時刻意斷線,下次登入時is_first_login_of_today 判斷會失效,便可永遠不用經過 check_BM
  4. common/sys/string.c str_decode_M3: 輸入是 "=??Q?" 時 str_iconv 使用未初始化的 charset[]
  5. common/sys/string.c str_decode_M3: mmdecode 有可能回傳 -1, str_iconv 的參數為 size_t, 會 underflow 後 buffer overread

擷圖

留言討論

聯絡組織

 發送私人訊息
您也可以透過私人訊息的方式與組織聯繫,討論有關於這個漏洞的相關資訊。
;