CTF初心者のためのログ分析問題解法

:::note warn
本CTFの問題の原データは **現時点では**公開されておりません。
公開された場合にはリンク等を追加します。
:::

# はじまり
### この記事は
Sendai CTF 2022 での問題の一つである、**ログ分析問題**について、CTF初心者である私の<INS>復習を兼ねた</INS>いわゆるWrite UPです。一応復習を兼ねているのでかなり冗長に、かつハンズオンのように実行過程を辿れるよう書いています。

### 一応こんな人に
- これから知識ほぼ0でCTFに挑む人
- ログファイルの中身を見たことない人
- `grep` `awk`とかなにそれ？おいしいの？ってくらいの人
- 「はっきんぐたのしそー( ᐛ👐)」 みたいな人
- 少しだけプログラミングができる人

全て私のことです。~~(うわっ…私の技術力、低すぎ…？)~~

### 環境
```sh
ProductName:	macOS
ProductVersion:	12.5.1
BuildVersion:	21G83
```
実行はzshにて、Pythonは3系です。

# 問題
### ログ分析問題
この大問は小問5つから構成され、アクセスログについてそれぞれ解答するものです。
アクセスログは以下のような構成になっています。
```md:access_log
[2020-06-19 10:09:11+0900] XXX.XXX.XXX.XXX YYY.YYY.YYY.YYY:PP "GET /manager/html HTTP/1.1" 401 1032 R0VU************************************************************************************************************************
```

>問題データの公開は認められていないため、部分的に情報を隠しています。
>(当然ですが)CTF本番で配布されたデータでは具体的な値が記載されています。
>`XXX.XXX.XXX.XXX`と`YYY.YYY.YYY.YYY:PP`はIPアドレスやポート番号を、`R0VU*****...`はbase64でエンコードされた各種情報です。

このようなアクセスログが **36000行以上** あるファイル`access_log`を解析します。

## 問題1
### 問題
ログファイル中の /manager/html への偵察行為の対象となるアプリケーション名を答えてください。
### 解答例
Googleは偉大なり。
![https://www.google.com/search?q=%2Fmanager%2Fhtml](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569723/d1850b8a-c100-b1de-2da7-a81a9d0e99c6.png)
トップに「攻撃ログ」と気になる文字列があるので[開いて](https://harakuttui.hatenablog.jp/entry/2020/07/23/214609)みると、どうやら[**tomcat**](https://tomcat.apache.org/)というアプリケーションへのアクセスらしいです。

>tomcatとはWebサーバにおいて動かすソフトウェアみたいなものです。
>詳しくはGoogleなりYahooなりに聞いてください。

**FLAGは`tomcat`になります。**

## 問題2
### 問題
ログファイルから一番多く偵察を受けたアプリケーション名を答えてください。

### 解答例
問題1から類推して、HTTPリクエスト（ `"GET /manager/html ..."` ）に注目すると良さそうです。これをなんとかして36000行以上のデータに対して集計する必要があります。
`awk` `sort` `uniq`コマンドを使うと良いらしいようです。(私はCTFの途中までこのコマンドを知りませんでした…)

とりあえずこの問題ではHTTPリクエストの部分だけ抜きだせれば良いので
```zsh
% awk '{print $6}' access_log
```
を実行すると、出力(途中の抜粋)は
```
...
/TP/public/index.php?s=index/\think\app/invokefunction&function=call_user_func_array&vars[0]=phpinfo&vars[1][]=1
/TP/public/index.php?s=captcha
/users?page=&size=5
/
/
/hudson
/
/
/manager/html
/manager/html
...
```
こんな感じに出てきます。
> `awk`コマンドでは各行の6番目の要素(`$6`)を表示(`print`)しています。
> ```
> [2020-06-19 10:09:11+0900] XXX.XXX.XXX.XXX YYY.YYY.YYY.YYY:PP "GET /manager/html HTTP/1.1" 401 1032 R0VU************************************************************************************************************************
> 1           2              3               4                  5    6             7 ...
> ```
> といったような数え方です。(スペースで区切っています。区切り文字を変えるオプションもあるようです。)

次は重複をまとめた上で数えていきます。先ほどの出力に
```zsh
% awk '{print $6}' access_log | sort | uniq -c
```
を実行すると出力（最初と最後3行ずつ）は
```
   1 '/script1.sh'
 535 /
   3 /.git/config
...
   1 www.facebook.com:443
   2 www.google.com:443
   1 www.ipip.net:443
```
となります。行頭の値はそれぞれ重複する数です。
> `sort`では、それぞれの出力をソートします。
> `uniq`では入力で**隣り合う行が同じ内容の場合に**まとめます。直前で`srot`するのはこのためです。
> `uniq -c`ではその重複を数えて一緒に表示します。
> またコマンド`A | B`では`A`の出力を`B`の入力として実行するようです。（この辺は私は詳しくないので有識者の方教えてください。。。）

ただこの出力も膨大な行数になります。目視で最大値を見つけても良いのですがもう少し賢く見つけましょう。はい、`sort`します。
```zsh
% awk '{print $6}' access_log | sort | uniq -c | sort
```
この出力(最初と最後3行)はこんな感じです。
```
   1 '/script1.sh'
   1 /.well-known/security.txt
   1 /AuPV
...
   2311 /wp/wp-login.php
   2316 /cms/wp-login.php
   2326 /wp1/wp-login.php
```
これでほぼ答えに辿り着きました。あとは問題1同様['/wp1/wp-login.php'で検索すると](https://www.google.com/search?q=%2Fwp1%2Fwp-login.php)出てきます。
**FLAGは`WordPress`になります。**

## 問題3
### 問題
ログファイル中の2020/06/19～2020/06/20の/manager/html へのアクセス件数を答えてください。
### 解答例
とりあえず該当期間のデータを抽出する必要がありそうです。`grep`しましょう。

>`grep`コマンドとは入力された文字列を含む行を出力するコマンドです。例えば、
>```zsh
>% grep /wp1/wp-login.php access_log
>```
>を実行すると`/wp1/wp-login.php`へのアクセスログのみを全て出力します。

ただ、このときに日付は`2020-06-19` `2020-06-20`の2つあり、単純な`grep`では実現できません。
ちょっと工夫して
```zsh
% grep "2020-06-\(19\|20\)" access_log
```
とすると、最後の`(19|20)`は`19`または`20`のどちらかということになります。
`\`は`()`や`|`が文字列として認識されるのではなく、演算子として認識されるための記号です。[**正規表現**](https://www.google.com/search?q=%E6%AD%A3%E8%A6%8F%E8%A1%A8%E7%8F%BE)です。

> このコマンドの結果も膨大な量になります。確認するのが面倒なので
> ```zsh
> % grep "2020-06-\(19\|20\)" access_log | (head -1; tail -1)
> ```
> を実行すると最初と最後の1行だけを表示してくれます。[[参考文献](https://qiita.com/blueskyarea/items/67c40193abb6994628f7)]
> こうすると出力は、
> ```
> [2020-06-19 00:11:34+0900] xxx.xxx.xxx.xxx blank:80 "GET / HTTP/1.1" 200 False R0VU********************
> [2020-06-20 23:49:19+0900] yyy.yyy.yyy.yyy YYY.YYY.YYY.YYY:80 "GET / HTTP/1.0" 200 False R0VU****************************************************************************************************
> ```
> となります。問題なさそうです。

次は`/manager/html`へのアクセスを抽出に`grep`します。
```zsh
% grep "2020-06-\(19\|20\)" access_log | grep "/manager/html"
```
あとは数えるだけです。`wc`コマンドを使って
```zsh
% grep "2020-06-\(19\|20\)" access_log | grep "/manager/html" | wc
```
出力は
```
     676    6760  249955
```
となり、676(=アクセス数)であることがわかりました。
> `wc`コマンドでは入力に対して`line, word, byte`の順で出力されます。

**FLAGは`676`になります。**

### 懺悔
この記事の著者はこの問題を**力技で**解いたなどと供述しています。具体的には**テキストエディタにて手作業で**
1. 該当期間外のデータを削除し、(文字列`2020/06/19`を含む最初の行を検索してそれより前の行を全て削除、`2020/06/20`より後も同様)
902行目以前の行を全て削除して...↓
![grep_delete_m.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569723/a7c3d3af-eac4-4cae-f320-c2543041a157.jpeg)


2. `/manager/html`を検索して該当数を数えました。
![grep_search_m.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569723/2a08c862-ba07-66f9-c01d-dc7319de9c6c.jpeg)
![grep_lined_m.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569723/43302227-68dc-7855-b302-c181a8860637.jpeg)

非常に反省しています。

## 問題4
### 問題
ログファイル中の /manager/html へのアクセスで、一番多く試行されたパスワードを調べ、その試行回数を答えてください。
### 解答例

**パスワードどこ？？？？？？** って最初はなるかと思います。なりましたよね？
例として、一番最初に挙げたログをもう一度見てみます。
```
[2020-06-19 10:09:11+0900] XXX.XXX.XXX.XXX YYY.YYY.YYY.YYY:PP "GET /manager/html HTTP/1.1" 401 1032 R0VU************************************************************************************************************************
```
**どこ？？？？？？**
ただ、1箇所だけ内容がわからないところがあります。そうです、最後の``R0VU...``のところです。ここは一見するとランダムな文字列ですが、[**base64**](https://qiita.com/PlanetMeron/items/2905e2d0aa7fe46a36d4)と呼ばれる変換をした文字列になっています。
この部分を[デコード](https://tool-taro.com/base64_decode/)（変換を元に戻すこと）すると
```
GET /manager/html HTTP/1.1
Connection: close
Authorization: Basic dG9tY2F0OmJvdGg=
User-Agent: Java/1.8.0_131
Host: XXX.XXX.XXX.XXX
Accept: text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2
```
**意味のありそうな文字列が出てきます。**
(`XXX.XXX.XXX.XXX`のところは隠していますが具体的な値が入っています。)

この中にパスワードの隠し場所なりそうな箇所は... **`Authorization`** しかなさそうです。ここもbase64になってそうな雰囲気があるのでデコードすると
```
tomcat:both
```
**ｱｱｱｱｱｱｱｱ**ってなりましたか？私はなりました。
他のログについても同様に解析してみると、`admin:tomcat`のような出力があります。`admin`はユーザ名に使われることが多く、`:`の前に書いていることからも`ユーザ名:パスワード`の順になっている気がします。

これでやっとパスワードを見つけることができたのでFLAG発見 **...にはならず**、全てのログに対して同様の処理を施し、パスワードを見つけて、最も多いものを探さなければなりません。
ただこれまでの問題を解いてきたので簡単にできるはずです。
まずは全てのログをデコードするところから。
```zsh
% grep "/manager/html" access_log | awk '{print $10}' | nkf -mBW 
```
出力は長いので省略しますが、全ての該当するログに対してデコードできました。
>`nkf -mBW`はデコードするコマンドです。
> なお`nkf`はhomebrewなどでインストールが必要です。

ここから`Authorization`を抽出し、
```zsh
% grep "/manager/html" access_log | awk '{print $10}' | nkf -mBW | grep "Authorization"
```

```
Authorization: Basic YWRtaW46
Authorization: Basic YWRtaW46YWRtaW4=
Authorization: Basic YWRtaW46MTIzNDU=
...
Authorization: Basic dGVzdDphc2RmZ2hqa2w=
Authorization: Basic YWRtaW5pc3RyYXRvcjpxcTEyMzQ1Ng==
Authorization: Basic YWRtaW5pc3RyYXRvcjphc2RmZ2hqa2w=
```

この第3成分を抽出してデコードすると...
```zsh
% grep "/manager/html" access_log | awk '{print $10}' | nkf -mBW | grep "Authorization" | awk '{print $3}' | nkf -mBW
```
```
admin:admin:adminadmin:12345admin...(中略)...qq123456administrator:asdfghjkl%
```
**全て1行にまとめて出してきやがります。** そう簡単には思い通りになってくれないのがプログラミングです。このままでは最も多く試行されたパスワードは分かりません。

もうコマンドラインで無理に済ませようとするのは諦めてpythonに逃げましょう。
一度デコードすべきデータをテキストファイルに保存します。
```zsh
% grep "/manager/html" access_log | awk '{print $10}' | nkf -mBW | grep "Authorization" | awk '{print $3}' > userid_password_encoded.txt
```
>`COMMAND > FILENAME`を用いることで、`COMMAND`実行結果を`FILENAME`に記録することができます。
試しに
>```zsh
>% cat userid_password_encoded.txt
>```
>を実行すると、
>```
>YWRtaW46
>YWRtaW46YWRtaW4=
>YWRtaW46MTIzNDU=
>...
>dGVzdDphc2RmZ2hqa2w=
>YWRtaW5pc3RyYXRvcjpxcTEyMzQ1Ng==
>YWRtaW5pc3RyYXRvcjphc2RmZ2hqa2w=
>```
>確かに記録されていることがわかります。

あとはpythonのコードですが、
```python　decode_password.py
import base64

with open('./userid_password_decoded.txt', 'w', encoding='UTF-8') as w:
    with open('./userid_password_encoded.txt') as f:
        for line in f:
            w.write(base64.b64decode(line).decode()) # ここでデコードして
            w.write('\n') # ここで改行を入れている
```
こうすることで改行を入れます。
そろそろゴールです。pythonを実行し、出力ファイル`userid_password_decoded.txt`を集計して
```zsh
% python3 decode_password.py
% awk -F ':' '{print $2}' userid_password_decoded.txt | sort | uniq -c | sort
```
> `awk -F ':'`は`:`を区切り文字とする、という意味です。

出力は
```
   1 12345
   6 
  17 role1
...
  22 admin
  22 manager
  22 tomcat
```
長かったですがついにフラグに辿り着きました。
**FLAGは`22`になります。**

私はログ分析の問題群の中で唯一、この問題4をCTFの時間内に解くことができませんでした。

### 謎
:::note warn
```zsh
% grep "/manager/html" access_log | awk '{print $10}' | base64 -d
```
を実行すると途中までは良いのですが、あるところでなぜか
```
Invalid character in input stream.
```
となります。(この理由をご存知のつよつよエンジニアの方は教えて下さい。)
:::


## 問題5
### 問題
ログファイルから一日のアクセス件数が一番多い日を調べ、その日の中で一番アクセス数が多い送信元IPアドレスを答えてください。
### 解答例
これまでの問題を解いてきたのであれば、なんとなく解法が見えてきます。
まずはアクセス件数が一番多い日を調べます。
```zsh
% awk '{print $1}' access_log | sort | uniq -c | sort
```
```
  82 [2020-06-14
  95 [2020-06-15
  99 [2020-06-17
 116 [2020-06-22
 243 [2020-06-16
 383 [2020-06-18
 899 [2020-06-19
13896 [2020-06-20
20447 [2020-06-21
```
これより2020/06/21について調べると良さそうです。
次はこの日の送信元IPアドレスを調べます。ただそれぞれのログにはIPアドレスが2つあります。~~今回のCTFでは誤答しても何もないので両方試しても良いのですが~~どちらが送信元かを考えます。
私もアクセスログは初めて見たのでわからなかったのですが、後者のIPアドレスにはポート番号がついていることから、アクセス先アドレスではないかと推測しました（詳しい人教えて下さい）。
とりあえず前者のIPアドレスについて調べると
```zsh
% grep "2020-06-21" access_log | awk '{print $3}' | sort | uniq -c | sort
```
```
...
 351 185.128.41.50
```
これより**FLAGは`185.128.41.50`になります。**


# この記事は終了しました

### 扱ったコマンドの一覧
- 検索エンジン：偉大。
- `awk`：1行における任意のn番目の要素を抽出しました。
- `sort`：出力を順に並べました。
- `uniq`：重複を解消したり数えたりしました。
- `grep`：特定の文字列を含む行を抽出しました。
- `wc`：行を数えました。
- `nkf`：base64などの変換をしました。

### 最後に
筆者はアホなのでCTF中にメモを取るのを忘れており、そのためFLAGが違うかもしれません。
最後になりますがSendaiCTFの運営の方々ありがとうございました。
~~稚拙な文章と低すぎる技術力で構成されているこの記事が運営の方々に見つからないことを祈ります。~~