Skip to content

Instantly share code, notes, and snippets.

@0xKira
Created September 27, 2021 02:56
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save 0xKira/a00e2574a96c58c1408b93825d30b6b2 to your computer and use it in GitHub Desktop.
Save 0xKira/a00e2574a96c58c1408b93825d30b6b2 to your computer and use it in GitHub Desktop.
Solution for 0CTF/TCTF 2021 Final binlog

The challenge uses the Django framework to host a website, and a binary service written in C++ provides data access.

The web service only has one potential vulnerability. It uses Django-redis to store session data. This library uses pickle to serialize data. This is vulnerable if an attacker is able to control both the cache key and data.

Luckily the provided binary service can do them all. You are able to write a blog(cache content) controllable. So the only problem is about the cache key. One obvious difference between the binary and the common ones is, it's compiled with AddressSanitizer to detect memory corruption bugs. And we can check the binary service output from the web interface, even its stderr!

The idea is to retrieve sensitive data from ASAN's bug report. I leave an easy-to-find off by null vulnerability when you try to set suffix of the key. It's triggered if your input length is longer than 32. How ASAN can leak data? Let's check the code out.

// Check if the global is a zero-terminated ASCII string. If so, print it.
void PrintGlobalNameIfASCII(InternalScopedString *str, const __asan_global &g) {
  for (uptr p = g.beg; p < g.beg + g.size - 1; p++) {
    unsigned char c = *(unsigned char *)p;
    if (c == '\0' || !IsASCII(c)) return;
  }
  if (*(char *)(g.beg + g.size - 1) != '\0') return;
  str->append("  '%s' is ascii string '%s'\n", MaybeDemangleGlobalName(g.name),
              (char *)g.beg);
}

There are three requirements:

  1. The overflowed buffer is global buffer.
  2. The buffer is full.
  3. All the contents are ASCII.

The first one is already satisfied. For the second and third ones, you can register a user whose name is longer than 32 bytes(the max prefix length). Also send a 32 bytes ASCII suffix ends with '\x00' to fill the buffer.

Another issue I deliberately leave is the random string in the middle may have '\x00' inside, because the length is wrong passed to std::uniform_int_distribution. You have (62/63) ** (200 - 32 * 2) which is about 10% probability to leak an all ASCII key.

Sample output:

{"msg": "=================================================================\n==12==ERROR: AddressSanitizer: global-buffer-overflow on address 0x565007fb9028 at pc 0x565007fa39ef bp 0x7ffe0efcdfb0 sp 0x7ffe0efcdfa0\nWRITE of size 1 at 0x565007fb9028 thread T0\n    #0 0x565007fa39ee  (/home/ctf/blog_mgr+0x69ee)\n    #1 0x565007fa4918  (/home/ctf/blog_mgr+0x7918)\n    #2 0x565007fa4fe5  (/home/ctf/blog_mgr+0x7fe5)\n    #3 0x7f07c4bcd0b2 in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x270b2)\n    #4 0x565007fa2ccd  (/home/ctf/blog_mgr+0x5ccd)\n\n0x565007fb9028 is located 0 bytes to the right of global variable 'global_buf' defined in 'blog_mgr.cc:10:6' (0x565007fb8f60) of size 200\n  'global_buf' is ascii string ':1:django.contrib.sessions.cache21C4NGSrLOHBHuuDxN1WJNxXe8EDKyrDlavLVhHrLddvFtktTQrnOz12IP5WOiHBkN6YWUk5IWFrtZFejiIPP6LXB6EK79IQeBblu5dT2zYrKhWHF8dwrgwU59pUjW7uEwn6VWQK1234567890123456789012345678901'\nSUMMARY: AddressSanitizer: global-buffer-overflow (/home/ctf/blog_mgr+0x69ee) \nShadow bytes around the buggy address:\n  0x0aca80fef1b0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00\n  0x0aca80fef1c0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00\n  0x0aca80fef1d0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00\n  0x0aca80fef1e0: 00 00 00 00 01 f9 f9 f9 f9 f9 f9 f9 00 00 00 00\n  0x0aca80fef1f0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00\n=>0x0aca80fef200: 00 00 00 00 00[f9]f9 f9 f9 f9 f9 f9 00 00 00 00\n  0x0aca80fef210: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00\n  0x0aca80fef220: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00\n  0x0aca80fef230: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00\n  0x0aca80fef240: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00\n  0x0aca80fef250: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00\nShadow byte legend (one shadow byte represents 8 application bytes):\n  Addressable:           00\n  Partially addressable: 01 02 03 04 05 06 07 \n  Heap left redzone:       fa\n  Freed heap region:       fd\n  Stack left redzone:      f1\n  Stack mid redzone:       f2\n  Stack right redzone:     f3\n  Stack after return:      f5\n  Stack use after scope:   f8\n  Global redzone:          f9\n  Global init order:       f6\n  Poisoned by user:        f7\n  Container overflow:      fc\n  Array cookie:            ac\n  Intra object redzone:    bb\n  ASan internal:           fe\n  Left alloca redzone:     ca\n  Right alloca redzone:    cb\n  Shadow gap:              cc\n==12==ABORTING\n"}

Sum up the exploit steps:

  1. Register an user whose name starts with ":1:django.contrib.sessions.cache".
  2. Write a blog which content is pickle payload.
  3. Set suffix to trigger ASAN report.
  4. If the key is not leaked, repeat 2 && 3.
  5. Visit the website with the leaked session.
#include <iostream>
#include <random>
#include <string>
#include <cstdio>
#include <sw/redis++/redis++.h>
const size_t kMaxPostCount = 10;
const size_t kPreSuffixLen = 0x20;
const size_t kRedisKeyLen = 200;
char global_buf[kRedisKeyLen];
std::string random_string(size_t length) {
const char charset[] = "0123456789"
"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
"abcdefghijklmnopqrstuvwxyz";
thread_local static std::mt19937 rng{std::random_device{}()};
thread_local static std::uniform_int_distribution<> dist(0, sizeof(charset) - 1); // deliberately more 1, \x00 will include
std::string str(length, 0);
std::generate_n(str.begin(), length, [&]() { return charset[dist(rng)]; });
return str;
}
class BlogMgr {
public:
BlogMgr(const std::string &uri);
~BlogMgr();
// username as prefix
void SetPrefix(const std::string_view &prefix);
// user provided suffix
void SetSuffix(const std::string_view &suffix);
void WritePost(const std::string_view &content);
void ReadPost(size_t index);
void Loop();
private:
std::vector<std::string> keys_;
std::unique_ptr<sw::redis::Redis> redis_;
};
BlogMgr::BlogMgr(const std::string &uri) {
redis_ = std::make_unique<sw::redis::Redis>(uri);
std::cout << "Init OK." << std::endl;
}
BlogMgr::~BlogMgr() {
// clear redis
redis_->del(keys_.begin(), keys_.end());
}
void BlogMgr::SetPrefix(const std::string_view &prefix) {
std::cout << "Setting prefix: " << prefix << std::endl;
memcpy(global_buf, prefix.data(), std::min(prefix.length(), kPreSuffixLen));
std::cout << "Prefix set OK." << std::endl;
}
void BlogMgr::SetSuffix(const std::string_view &suffix) {
std::cout << "Setting suffix: " << suffix << std::endl;
const size_t copy_len = std::min(suffix.length(), kPreSuffixLen);
memcpy(global_buf + kRedisKeyLen - kPreSuffixLen, suffix.data(), copy_len);
// off by null
global_buf[kRedisKeyLen - kPreSuffixLen + copy_len] = 0;
std::cout << "Suffix set OK." << std::endl;
}
void BlogMgr::WritePost(const std::string_view &content) {
std::cout << "Writing post: " << content << std::endl;
if (keys_.size() >= kMaxPostCount) {
std::cerr << "Too many posts! Contact admin." << std::endl;
return;
}
std::string key, rand_str;
while (true) {
rand_str = random_string(kRedisKeyLen - 2 * kPreSuffixLen);
memcpy(global_buf + kPreSuffixLen, rand_str.data(), kRedisKeyLen - 2 * kPreSuffixLen);
key = std::string(global_buf, kRedisKeyLen);
if (!redis_->exists(key))
break;
}
if (redis_->set(key, content)) {
keys_.push_back(key);
std::cout << "Write succeed." << std::endl;
}
}
void BlogMgr::ReadPost(size_t index) {
std::cout << "Reading post: " << index << std::endl;
if (index >= keys_.size()) {
std::cerr << "Read post error!" << std::endl;
return;
}
const std::string_view &key = keys_[index];
auto content = redis_->get(key);
if (!content) {
std::cerr << "Read post error!" << std::endl;
return;
}
std::cout << *content << std::endl;
}
void BlogMgr::Loop() {
std::string input;
while (std::getline(std::cin, input)) {
std::istringstream ss(input);
int choice;
ss >> choice;
std::cout << "Choice: " << choice << std::endl;
switch (choice) {
case 0:
std::getline(std::cin, input); // username
SetPrefix(input);
break;
case 1:
std::getline(std::cin, input); // suffix
SetSuffix(input);
break;
case 2:
std::getline(std::cin, input); // content
WritePost(input);
break;
case 3:
int index;
std::getline(std::cin, input); // index
ss.str(input);
ss.clear();
ss >> index;
ReadPost(index);
break;
default:
std::cerr << "Critical error!" << std::endl;
return;
}
}
}
int main(int argc, char *argv[]) {
if (argc != 2) {
std::cerr << "Usage: ./blog_mgr redis_uri" << std::endl;
exit(-1);
}
std::setvbuf(stdout, NULL, _IONBF, 0);
std::setvbuf(stderr, NULL, _IONBF, 0);
BlogMgr mgr(argv[1]);
mgr.Loop();
return 1;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment