This is my first time making a website using Python!!!! :D
Author: daffainfo
Selain link menuju website, kita juga diberikan zip yang berisikan source code webapp nya.
Webapp ini dibuat menggunakan framework Flask dari Python, dengan kode main.py sebagai berikut.
from flask import Flask, request, render_template, redirect, session, abort
from flask_sqlalchemy import SQLAlchemy
import requests
import secrets
import bcrypt
app = Flask(__name__)
app.secret_key = secrets.token_hex(32)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///ctf.db'
db = SQLAlchemy(app)
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(50), unique=True, nullable=False)
password = db.Column(db.String(100), nullable=False)
is_admin = db.Column(db.String(1), default='0')
with app.app_context():
db.create_all()
def hash_password(password: str) -> str:
salt = bcrypt.gensalt()
return bcrypt.hashpw(password.encode('utf-8'), salt).decode('utf-8')
def check_password(stored_password: str, provided_password: str) -> bool:
return bcrypt.checkpw(provided_password.encode('utf-8'), stored_password.encode('utf-8'))
@app.route('/')
def index():
if 'user_id' not in session:
return redirect('/login')
user = db.session.get(User, session['user_id'])
if user.is_admin == '1':
return render_template('index.html', admin=True, username=user.username)
else:
return render_template('index.html', admin=False, username=user.username)
@app.route('/admin/fetch', methods=['GET', 'POST'])
def admin_fetch():
if 'user_id' not in session:
return redirect('/login')
user = db.session.get(User, session['user_id'])
if user.is_admin != '1':
return "You are not authorized.", 403
result = None
if request.method == 'POST':
url = request.form.get('url')
if 'daffainfo.com' not in url:
result = "Error: Only URLs with hostname 'daffainfo.com' are allowed."
else:
try:
resp = requests.get(url, timeout=5)
result = resp.text
except Exception as e:
result = f"Error fetching URL: {str(e)}"
return render_template('fetch.html', result=result)
@app.route('/register', methods=['GET', 'POST'])
def register():
if request.method == 'POST':
data = request.form.to_dict()
data['password'] = hash_password(data['password'])
user = User(**data)
db.session.add(user)
db.session.commit()
return redirect('/login')
return render_template('register.html')
@app.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'POST':
username = request.form['username']
password = request.form['password']
user = db.session.execute(
db.select(User).filter_by(username=username)
).scalar_one_or_none()
if user and check_password(user.password, password):
session['user_id'] = user.id
return redirect('/')
else:
return "Invalid credentials."
return render_template('login.html')
@app.route('/internal')
def internal():
if request.remote_addr != '127.0.0.1':
abort(403)
return "Flag: IFEST13{fake_flag}"
if __name__ == '__main__':
app.run(debug=False, host="0.0.0.0", port=1337)
Pada bagian "register", ada kode seperti ini.
data = request.form.to_dict()
###
user = User(**data)
Data dari POST request yang berbentuk username=username&password=password langsung dimasukkan begitu saja pada objek User , tanpa pengecekan sama sekali. Nah, di class User, ada atribut is_admin yang value-nya adalah 0 atau 1 (kalau 1, maka user tersebut adalah Admin).
Kalau begitu, kita bisa saja melakukan registrasi menggunakan data berikut pada POST request: username=username&password=password&is_admin=1 . Dengan begini, kita telah membuat sebuah akun Admin.
Berikutnya, ada sebuah fitur bernama Admin fetcher. Ini adalah setup klasik untuk serangan SSRF. Di sini, kita bisa masukkan URL apapun dan website akan menampilkan apapun data response dari request GET terhadap URL tersebut. Pada website ini, ada syarat unik yakni URL yang dimasukkan harus memiliki hostname "daffainfo.com".
Tapi, implementasinya benar-benar tidak aman. Selama ada string "daffainfo.com" pada URL, maka requestnya akan dijalankan. Untuk membypass aturan ini, kita bisa tambahkan ?url=daffainfo.com di akhir URL. Berikut adalah hasil untuk input URL http://example.com?url=daffainfo.com.
Nah, kembali ke source code. Untuk mendapatkan flag, kita perlu mengakses engpoint /internal . Akses menuju endpoint ini hanya dibolehkan dari 127.0.0.1 alias mesin lokal. Tapi tidak apa-apa, karena kita punya fitur Admin fetch. Fungsi fetching ini akan mengakses endpoint tadi sebagai mesin lokal.
@app.route('/internal')
def internal():
if request.remote_addr != '127.0.0.1':
abort(403)
return "Flag: IFEST13{fake_flag}"
if __name__ == '__main__':
app.run(debug=False, host="0.0.0.0", port=1337)
Hal penting lainnya adalah, sebagai mesin lokal, kita tidak mengunjungi website pada URL publiknya yakni http://103.163.139.198:12312/internal . Akan tetapi, pada mesin lokal, webapp ini dijalankan pada http://0.0.0.0:1337. Maka dari itu, payload akhir kita menjadi sebagai berikut.