シェル芸スパルタン演習(第16回シェル芸勉強会編)
ここ最近、Unixシェルに触る機会が減っており、CLIを使う能力の低下を感じていた。
また、近日中に、第25回シェル芸勉強会の開催が予定されており、参加するにあたり予習をしたいとも考えていた。
そこで、過去に開催されたシェル芸勉強会の問題を活用し、シェル芸力をつける演習をおこなうことにした。
今回は、問題として、第16回シェル芸勉強会の設問を用いた。
実行環境
Arch Linux 4.8.4-1-ARCH
GNU bash 4.3.46
GNU coreutils 8.25-2
GNU diffutils 3.5-1
GNU findutils 4.6.0-2
util-linux 2.28.2-1
grep (GNU grep) 2.26
sed (GNU sed) 4.2.2
gawk 4.1.4
zcat (GNU gzip) 1.8
usp Personal Tukubai 20160402 ( Open usp Tukubaiで代用可能)
シェル芸演習
問題文は、以下のURLから。
http://blog.ueda.asia/?p=5851
模範解答は、こちらから。
http://blog.ueda.asia/?p=5644
前準備1
日時と時刻を正規化する問題。
まず、日時・時刻を正規表現で取り出し、行頭にくっつける。
その後、略称名になっている月名を、数字に変換すればOK。
01 #!/bin/sh
02
03 zcat access_log.nasa.gz |
04 sed 's;.*[[]\([0-3][0-9]\)/\([A-Z][a-z]*\)/\([1-9][0-9]\{0,3\}\):\([0-9]\{2\}\):\([0-9]\{2\}\):\([0-9]\{2\}\) .*;\3\2\1 \4\5\6 &;' |
05 awk 'sub(/Jan/, "01", $1); sub(/Feb/, "02", $1); sub(/Mar/, "03", $1); sub(/Apl/, "04", $1); sub(/May/, "05", $1); sub(/Jun/, "06", $1); sub(/Jul/, "07", $1); sub(/Aug/, "08", $1); sub(/Sep/, "09", $1); sub(/Oct/, "10", $1); sub(/Nov/, "11", $1); sub(/Dec/, "12", $1)'
以下、コピペ実行用のワンライナーコード。
zcat access_log.nasa.gz | sed 's;.*[[]\([0-3][0-9]\)/\([A-Z][a-z]*\)/\([1-9][0-9]\{0,3\}\):\([0-9]\{2\}\):\([0-9]\{2\}\):\([0-9]\{2\}\) .*;\3\2\1 \4\5\6 &;' | awk 'sub(/Jan/, "01", $1); sub(/Feb/, "02", $1); sub(/Mar/, "03", $1); sub(/Apl/, "04", $1); sub(/May/, "05", $1); sub(/Jun/, "06", $1); sub(/Jul/, "07", $1); sub(/Aug/, "08", $1); sub(/Sep/, "09", $1); sub(/Oct/, "10", $1); sub(/Nov/, "11", $1); sub(/Dec/, "12", $1)'
前準備2
ログの中身を日付ごとに分割する問題。
awkの特殊変数FILENAMEとリダイレクト機能を利用すれば、シンプルに実現できる。
01 #!/bin/sh
02
03 awk '{print $0 >> FILENAME"."$1}' nasa.log
Tukubai版。キー毎にファイルを分割するkeycutコマンドを使えば、一発。
01 #!/bin/sh
02
03 cat nasa.log |
04 sort -k 1,1n |
05 keycut nasa.log.%1 -
Tukubai版、その2。ソートの必要が無いsorterを使って分割。
01 #!/bin/sh
02
03 sorter nasa.log.%1 nasa.log
以下、コピペ実行用のワンライナーコード。
awk '{print $0 >> FILENAME"."$1}' nasa.log
cat nasa.log | sort -k 1,1n | keycut nasa.log.%1 -
sorter nasa.log.%1 nasa.log
問1
アクセスログを、ステータスコード毎に集計する問題。
awkの連想配列を使えば、簡単に集計できる。
01 #!/bin/sh
02
03 cat nasa.log |
04 awk '{stat[$(NF-1)]++} END{for(n in stat){print n,stat[n]}}'
以下、コピペ実行用のワンライナーコード。
cat nasa.log | awk '{stat[$(NF-1)]++} END{for(n in stat){print n,stat[n]}}'
問2
ファイルを開かずに、ログを日付毎に件数を調べる問題。
前準備2で、日付毎にファイルを分割した事を利用する。
01 #!/bin/sh
02
03 wc -l nasa.log.* |
04 sed '$d' |
05 sort -k 1,1nr |
06 head -n 1 |
07 awk '$0=$2'
以下、コピペ実行用のワンライナーコード。
wc -l nasa.log.* | sed '$d' | sort -k 1,1nr | head -n 1 | awk '$0=$2'
問3
本格的なログ解析をする問題。
問3-1
曜日毎にログ件数を求める問題。
前準備1で、日時を第1フィールドに追加しているので、それを利用すれば良い。
dateコマンドで対応する曜日を出力し、最後に曜日ごとに集計すればOK。
01 #!/bin/sh
02
03 cat nasa.log |
04 awk '$0=$1' |
05 xargs -I@ date -d @ +%A |
06 sort |
07 uniq -c
01 #!/bin/sh
02
03 wc -l nasa.log.* |
04 sed '$d' |
05 awk 'sub(/[^0-9]*/, "", $2)' |
06 awk '{print "echo",$1,"; date -d",$2,"+%A"}' |
07 sh |
08 xargs -n 2 |
09 awk '{days[$2]+=$1} END{for(n in days){print n,days[n]}}'
問3-2
今度は、時間帯ごとにログ件数を集計する問題。
これも、awkの連想配列を使えば、簡単に実現できる。
集計関係なら、awkを使えば大抵なんとかなる。
01 #!/bin/sh
02
03 cat nasa.log |
04 awk '{sub(/....$/, "", $2); hour[$2]++} END{for(n in hour){print n,hour[n]}}' |
05 sort -k 1,1n
以下、コピペ実行用のワンライナーコード。
問3-1
cat nasa.log | awk '$0=$1' | xargs -I@ date -d @ +%A | sort | uniq -c
wc -l nasa.log.* | sed '$d' | awk 'sub(/[^0-9]*/, "", $2)' | awk '{print "echo",$1,"; date -d",$2,"+%A"}' | sh | xargs -n 2 | awk '{days[$2]+=$1} END{for(n in days){print n,days[n]}}'
問3-2
cat nasa.log | awk '{sub(/....$/, "", $2); hour[$2]++} END{for(n in hour){print n,hour[n]}}' | sort -k 1,1n
問4
ログに含まれるIPを抽出し、全て192.168で始まっている事を調べる問題。
grep -oでIPアドレスを抽出した後、uniqすれば良い。
01 #!/bin/sh
02
03 zcat access.log.shellshock.gz |
04 grep -o '[0-9]\{1,3\}\.[0-9]\{1,3\}\.[0-9]\{1,3\}\.[0-9]\{1,3\}' |
05 awk -F. '{print $1"."$2}' |
06 sort |
07 uniq
以下、コピペ実行用のワンライナーコード。
zcat access.log.shellshock.gz | grep -o '[0-9]\{1,3\}\.[0-9]\{1,3\}\.[0-9]\{1,3\}\.[0-9]\{1,3\}' | awk -F. '{print $1"."$2}' | sort | uniq
問5
ログの中から、レスポンスヘッダの送信量が大きいもの上位10件を調べる問題。
今回のログでは、レスポンスヘッダはHTTPステータスコードの次フィールドにあるので、それを抽出して使う。
うまく抽出できたら、sortした後、上位10件を表示すればOK。
01 #!/bin/sh
02
03 zcat access.log_.shellshock.gz |
04 awk -F\" '{print $1,$3}' |
05 awk '{ip[$1]+=$NF} END{for(addr in ip){print addr,ip[addr]}}' |
06 sort -k 2,2nr |
07 head -n 10
以下、コピペ実行用のワンライナーコード。
zcat access.log_.shellshock.gz | awk -F\" '{print $1,$3}' | awk '{ip[$1]+=$NF} END{for(addr in ip){print addr,ip[addr]}}' | sort -k 2,2nr | head -n 10
問6
ログが0件の日を調べる問題。
前準備2で、ログを日付ごとに分けているので、それを利用する。
7月8月の全日付と、全ログファイル名の日付を比較し、差分を調べれば良い。
01 #!/bin/bash
02
03 diff <(echo 1995{07,08}{01..31} |awk -vRS=' ' '$1=$1' |sort) <(ls nasa.log.* |sed 's/[^0-9]*//' |sort) |
04 sed -n '/^< /{s///; p}'
Tukubai版。mdateを使って、7月8月の日付を全て列挙した。
7月8月は共に31日まであるので、今回の問題では上記解答でも大丈夫だったけれど、それ以外の場合だとmdateなどを使う必要がある。
01 #!/bin/bash
02
03 diff <(mdate -e 19950701 19950831 |tarr |sort) <(ls nasa.log.* |sed 's/[^0-9]*//' |sort) |
04 sed -n '/^< /{s///; p}'
以下、コピペ実行用のワンライナーコード。
diff <(echo 1995{07,08}{01..31} |awk -vRS=' ' '$1=$1' |sort) <(ls nasa.log.* |sed 's/[^0-9]*//' |sort) | sed -n '/^< /{s///; p}'
diff <(mdate -e 19950701 19950831 |tarr |sort) <(ls nasa.log.* |sed 's/[^0-9]*//' |sort) | sed -n '/^< /{s///; p}'
問7
ShellShockログを本格的に解析する問題。
問7-1
単純に、ログから該当するレコード、フィールドを抽出すれば良い。
抽出できたら、その後は集計する。
01 #!/bin/sh
02
03 zcat access.log_.shellshock.gz |
04 grep '() { :;};' |
05 awk -F\" '{for(i=6;i<=NF;i++){printf("%s ", $i)}; print ""}' |
06 sort |
07 uniq -c |
08 sort -k 1,1nr
ちなみに、実行結果は次の通り。
ざっと見た感じ、ShellShock脆弱性を悪用し、怪しげなperlスクリプトを落として実行させようとしている輩が多い印象。
問7-2
問題文の通り、エスケープコードを除去するだけ。
sedで地道に削っていく。
01 #!/bin/sh
02
03 zcat access.log_.shellshock.gz |
04 grep '() { :;};' |
05 awk -F\" '{for(i=6;i<=NF;i++){printf("%s ", $i)}; print ""}' |
06 sed 's;\\\\;\\;g' |
07 sed 's;\\ ;;g'
問7-3
ログに含まれるコマンドを実行する問題。
問7_1、7_2の結果を更に整形し、shに入力してやればOK。
01 #!/bin/sh
02
03 zcat access.log_.shellshock.gz |
04 grep '() { :;};' |
05 awk -F\" '{for(i=6;i<=NF;i++){printf("%s ", $i)}; print ""}' |
06 sed 's;\\\\;\\;g' |
07 sed 's;\\ ;;g' |
08 sort |
09 uniq |
10 sed 's/^() { :;}; *//' #| sh
以下、コピペ実行用のワンライナーコード。
問7-1
zcat access.log_.shellshock.gz | grep '() { :;};' | awk -F\" '{for(i=6;i<=NF;i++){printf("%s ", $i)}; print ""}' | sort | uniq -c | sort -k 1,1nr
問7-2
zcat access.log_.shellshock.gz | grep '() { :;};' | awk -F\" '{for(i=6;i<=NF;i++){printf("%s ", $i)}; print ""}' | sed 's;\\\\;\\;g' | sed 's;\\ ;;g'
問7-3
危険なので、最後のshのコメントを外す際は、くれぐれもご注意を。
zcat access.log_.shellshock.gz | grep '() { :;};' | awk -F\" '{for(i=6;i<=NF;i++){printf("%s ", $i)}; print ""}' | sed 's;\\\\;\\;g' | sed 's;\\ ;;g' | sort | uniq | sed 's/^() { :;}; *//' #| sh
雑記
にゃん。
久しぶりのダマイちゃん。東山のトラ舎付近は蚊が多いので、ここ最近は足が遠のいていました… 通りかかったお客さんからは、しょっちゅう「格好いい!」との声が上がります。 2016.09.14(水) #東山動物園 #スマトラトラ ダマイ pic.twitter.com/U2FlH06VrB
— ginjiro (@gin_135) 2016年9月19日