Dojo challenge #53 Hacker Club solution

August 26, 2026

Article hero image

The solution and the writeup provided were written by the hunter: address_below0

Description

The application is vulnerable to SSTI that can be done if an attacker can bypass the regex and email parser shenanigans. With a correct payload an attacker can exfiltrate the flag which is FLAG{Hack3rs_Wann4_Be_Hack3rs}

Exploitation

Reading and analyzing the code shows 3 ruby gem being used. The erb being used for template rendering the output, the mail being used for email parsing capability and uri being used for URL decoding. There's also a variable named RESERVED having dojo-yeswehack.com as the value
The main function first read the user inputed value which are the email address at this line of code

1email_address = URI.decode_www_form_component("")

And after that the msg, m = "", "" initialising both variables msg and m to empty strings
Then the function signup_allowed!(email_address) being called. The called function are like this, the function is doing regex check. If the email contains @dojo-yeswehack.com, reject it by raising an error.

1def signup_allowed!(addr_text)
2 domains = addr_text.scan(/@\s*([a-z0-9.-]+)/i).flatten.map(&:downcase)
3 raise "rejected: #{RESERVED} is reserved for VIP members" if domains.include?(RESERVED)
4 addr_text
5end

Next the function build_message(email_address) being called. The called function is using mail gem to parses it into structured fields

1def build_message(addr_text)
2 m = Mail.new
3 m.from = "info@#{RESERVED}"
4 m[:to] = addr_text
5 m.subject = "Invited to the Hacker Club"
6 m
7end

Then after that the routed_domain(m) being called. This called function checks if it equals dojo-yeswehack.co, if yes then render_invite() being called if no then render_denied() being called. The interesting part is render_invite() function because it's using #{name} which ruby string interpolation that basically If name contains ERB tags like this SSTI payload <%= 7*7 %> , those tags become part of the template that ERB will execute and will shows 49. Another one is .result(binding), this method executes the template using the local variable and method scope of where it is called meaning any injected ERB code can access the entire Ruby runtime and file system

1def render_invite(m)
2 name = m[:to].addrs.first.display_name || "guest"
3 ERB.new(
4 <<~MAIL
5 Hi,
6
7 Welcome to the Hacker Club #{name}!
8
9 Let's hack the planet together,
10
11 // Hackers Club
12 MAIL
13 ).result(binding)
14end

Moving on the next function in the main function are render_denied() which return a static message with no user input, then the last line of the main function at puts ERB.new(IO.read("views/index.erb")).result_with_hash({msg: msg, addr: m[:to].addrs.first}) are taking the msg and render it into HTML page template.

The results of the code analysis convinced me that I need to somehow bypass the email checking and put the ERB code to gain SSTI. Starting with bypassing the regex. The regex are possibly bypassed if a char being passed after @ is not in [a-z0-9.-]. And about the email parsing stuff when searching online I found this research by Gareth Heyes from Portswigger. The point is rfc2047 accepting encoded word in hex and base64. Meaning if i put =?utf-8?q?=41=42=43?=test@mail.com this one will turn into ABCtest@mail.com

Then after reading more from Gareth Heyes research I found the exact email parsing that worked <adb0@=?x?q?dojo-yeswehack.com?=> to reach the render_invite function

Now for the SSTI part, it's taking me so long but I found the working payload which are "<%= 7*7 %>" so the completed email would be "<%= 7*7 %>" <adb0@=?x?q?dojo-yeswehack.com?=> as we can see that 7*7 will shows 49 as the output

Confirming that SSTI successfully by 7*7, I test for Command Execution by puting backtick between the command itself because backticks Ruby's syntax for command execution. So now the payload becomes "<%= `id` %>" <adb0@=?x?q?dojo-yeswehack.com?=>

Final one is getting the flag.txt by putting "<%= `cat flag.txt` %>" <adb0@=?x?q?dojo-yeswehack.com?=>

Remediation

  1. Don't use #{} interpolation for user data in ERB templates in the render_invite function
  2. Limit scope to explicit variables only by changing .result(binding) to result_with_hash() so it's not using the full Ruby runtime in the render_invite function