Dojo #41

Challenge "Ruby treasure" du dojo de YesWeHack (mai 2025)

Le

Le Dojo de YesWeHack est un espace d’entraînement pratique où les hunters peuvent développer leurs compétences en identifiant et exploitant des failles de sécurité à travers une série de défis dédiés au bug bounty. C'est pourquoi les articles de cette catégorie sont tournés comme des rapports de bug bounty.

The application

require 'cgi'
require 'erb'

Dir.chdir("/tmp/app/views")

# Validate the given filename
# We use a super strict regex, so it can't be bypassed, right?
def validateFile(filename)
    return !!/^[a-zA-Z0-9_-]+$/.match(filename) || filename.length == 0
end

filename = CGI.unescape("<user input>")
content = ""

if validateFile(filename)
    begin
        content = IO.read("#{filename}.erb")
    rescue
        content = IO.read("collection.erb")
    end
    
end

# Render the given page for our web application
puts ERB.new(IO.read("index.erb")).result_with_hash({page: content})

This Ruby application reads an .erb (HTML template) file based on a user-provided name, after validation, and injects its content into a main template called index.erb. It uses ERB to dynamically generate and render an HTML page.

📜 Description

Remote code execution allows an attacker to run any commands or code of the attacker's choice on a target machine or in a target process.

In our case, it is possible to execute commands by exploiting two vulnerabilities:

Regex Bypass

The first vulnerability lies in the validateFile function, specifically in the condition using the regular expression:

!!/^[a-zA-Z0-9_-]+$/.match(filename)

By inserting a newline character into the payload, we can bypass this regex. This happens because the regular expression matches each line individually, meaning if we send something like:

string_ok
`invalid string..`

the match will return true, since the first line satisfies the regex.

Remote Code Execution (RCE)

According to the Ruby documentation for the IO.read method (https://ruby-doc.org/core-2.5.5/IO.html#method-c-read):

If the name starts with a pipe character ("|"), a subprocess is created in the same way as Kernel#open, and its output is returned.

Therefore, we can execute arbitrary commands by prefixing the input with a pipe character and use backquotes (bash syntax) to execute system commands, like this:

|`ls`
string ok

🕵️ Proof of Concept

In the application, remote code execution occurs when a user send the payload below :

|`<command>`
ohohoh

For example :

|`id`
ohohoh

🚧 Impacts

A remote code execution could allow an attacker to :

  • Execute OS commands.
  • Read/Edit/Delete files.
  • Steal secrets.
  • Server takeover.
  • Perform malicious tasks from the victim server.
  • Perform scan on the internal network.

🔐 Mitigations

Input validation is a frequently-used technique for checking potentially dangerous inputs in order to ensure that the inputs are safe for processing within the code, or when communicating with other components. When software does not validate input properly, an attacker is able to craft the input in a form that is not expected by the rest of the application. This will lead to parts of the system receiving unintended input, which may result in altered control flow, arbitrary control of a resource, or arbitrary code execution.

Below is an example of a corrected version of the application.

require 'cgi'
require 'erb'

SAFE_VIEW_PATH = "/tmp/app/views"
DEFAULT_TEMPLATE = "collection.erb"
MAIN_TEMPLATE = "index.erb"

# Sanitize and validate filename strictly
def safe_filename(filename)
  return nil unless /^[a-zA-Z0-9_-]+$/.match?(filename)
  filename
end

# Sanitize input
raw_input = CGI.unescape(ARGV[0] || "")
filename = safe_filename(raw_input)

# Ensure we don't escape the safe directory
template_path = if filename
                  path = File.expand_path("#{filename}.erb", SAFE_VIEW_PATH)
                  path.start_with?(SAFE_VIEW_PATH) && File.exist?(path) ? path : nil
                end

template_path ||= File.join(SAFE_VIEW_PATH, DEFAULT_TEMPLATE)
main_template = File.join(SAFE_VIEW_PATH, MAIN_TEMPLATE)

# Load and escape content to avoid ERB injection
page_content = ERB::Util.html_escape(IO.read(template_path))

# Render final page with escaped content
puts ERB.new(IO.read(main_template)).result_with_hash({page: page_content})

📚 References

Accès Restreint

Cet article n'est pas (encore) publique.
Pour obtenir un accès anticipé, contactez-moi.

[[ passwordError ]]

[[ post.title ]]

[[ post.description ]]

Le [[ post.date ]]

[[ tag ]]
[[ currentMatchIndex + 1 ]] / [[ searchMatches.length ]] 0 / 0

Sommaire

Page principale