[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(),
                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.