[UIUCTF 2023] Vimjail {1,2}{,.5}
Challenge Author: richard
Points: 50 + 50 + 57 + 91 (114 + 106 + 64 + 54 solves)
The challenge
While there are some differences across the different challenges, the
basic setup for all four challenges is the same: you’re stuck in a
restricted rvim
(even more restricted than
rvim
already is normally), and you want to get the flag.
The location of the flag is known and readable, so arbitrary read is
enough.
In all versions, we can’t properly get into normal mode due to the
set insertmode
in the used vimrc, and some
<c->
keybinds are blocked. In vimjail2, we
additionally have a given viminfo file being used (containing expression
history), and quitting vim is enough as the flag is printed upon exit;
but the entire alphabet and some special symbols are
mapped to _
in command mode. In the .5 revenge versions,
the entire keybind <c-\>
is blocked, rather than only
<c-\><c-n>
.
Previous convictions
Having some experience writing a vim jail before
myself1, my thoughts immediately went to
exploring how feasible it would be to just repurpose that exploit. And
indeed, hitting <c-r>=
in insert mode nicely drops us
into a command mode prompt where we can evaluate arbitrary viml
expressions. A simple execute(":e /flag.txt")
later, and we
see a nice and juicy flag pop up, for vimjail1 and vimjail1.5, that is.
The remapping for vimjail2 prevents us from typing the expression
directly.
With your arms tied behind your back
Now, to bypass those pesky underscores, it’s time to dig in our
extensive memory of vim weirdness a bit, that inevitably gets developed
when you use the editor for some amount of time. If you’re anything like
me, you’ve absolutely hit q:
by accident when trying to
quit your editor with :q
. What you get there, is
essentially the :
functionality, but in insert mode, which
doesn’t have the _
mapping! How could we leverage that in
the expression prompt though? It’s not like we can use q:
in normal mode either. Simple: :h q:
is this an
emoticon? will tell you that <c-f>
is the
default command-mode bind when nocompatible
is set. So a
full solution that works on all four jails:
<c-r>=<c-f>execute("e /flag.txt")<cr>
.2 The intended solution still goes
through the =
register, but uses the injected history to
recover the required characters to type execute(":q")
.
So that was what that file was for…
Reverse engineering revenges
There is one big disadvantage to making a revenge challenge available
to teams that haven’t solved the original:3 you
publicly disclose where the (unintended) weakness of the original is.
And indeed, looking at the diff, and exploring the keybinds beginning
with <c-\>
a bit, we find that going through
<c-\><c-o>:
brings us to the regular
:
command prompt, where we can type
:e /flag.txt
(or :________
if we’re unlucky).
If we just want to quit the editor, going for
<c-\><c-o>ZZ
is enough, and with just a smidge
more imagination, we can again abuse register insertion a bit, by
recording a macro with <c-\><c-o>qq
, typing the
wanted command in insert mode, and then
<c-\><c-o>:<c-r>q
to get text in there.
Another alternative to using <c-f>
or the history in
the expression prompt is to cycle through available functions with tab
completion and cobble together a complete expression in that way.
Becoming the warden?
As a bit of a bonus, let’s also consider what we can do with this (essentially unlimited) vim expression input. Could we succeed in getting a shell, elevating vim code execution into bash code execution? On this challenge, due to how it was setup, unfortunately not, but when building a similar setup against my local vim installation, I did manage to find a bypass.
The most important thing between us and our goal, is the used vim
command being rvim
, which, as documentation will tell us,
is equivalent to vim -Z
. Quickly checking out the gtfobins page gives
a few options, that – alas – don’t work out at all. Both
:py
and :lua
are completely blocked in rvim by
now, exactly to avoid this sort of scenario. So let’s have a look at
what :h -Z
can tell us then:
-Z restricted-mode E145 E981
-Z Restricted mode. All commands that make use of an external
shell are disabled. This includes suspending with CTRL-Z,
":sh", filtering, the system() function, backtick expansion
and libcall().
Also disallowed are delete(), rename(), mkdir(), job_start(),
etc.
Interfaces, such as Python, Ruby and Lua, are also disabled,
since they could be used to execute shell commands. Perl uses
the Safe module.
For Unix restricted mode is used when the last part of $SHELL
is "nologin" or "false".
Note that the user may still find a loophole to execute a
shell command, it has only been made difficult.
That’s pointing in an interesting direction, there’s such a thing as
:perl
and it’s less restricted than :py
or
:lua
. And to come to an anticlimactic finish:
:perl system sh
works without a flaw on my system, but the
challenge’s vim is compiled without perl support.