LoRexxar's Blog | 信息技术分享

Joern In RealWorld (2) - Jumpserver随机数种子泄露导致账户劫持漏洞(CVE-2023-42820)

2023/10/26

Jumpserver是一个开源的django架构的堡垒机系统,由lawliet & zhiniang peng(@edwardzpeng) with Sangfor在上个月报送了这个漏洞

漏洞原理其实比较神奇,一个常用的第三方组件库django-simple-captcha泄露随机数种子的问题,再配合Jumpserver使用了错误的随机数方案导致了最终的漏洞。

漏洞成因

这里我们的目标不是分析漏洞,所以这里简单快速的分析下漏洞的成因,具体的漏洞分析可以看下面两篇文章

在分析代码级的漏洞成因之前,我想作为计算机相关的工作者,我们应该都有一个共识,就是计算机中没有真正意义的伪随机,无论是任何语言的随机数生成函数几乎都是从类似 /dev/random的地方取值,这里我们不讨论随机数底层的问题。

在代码的上层,我们几乎可以认为如果你不知道随机数的种子,那么你就无法对随机数做出预测。换言之,如果我们知道随机数的种子,我们就有一定的概率预测随机数

django-simple-captcha是Django的相关组件中非常流行的验证码生成库,就像phith0n所说,在国内你几乎没有别的选择,引入的方式超级简单,只要在配置里引入对应库

1
2
3
INSTALLED_APPS = [
...
'captcha',

然后加入对应的验证码路由

1
2
3
urlpatterns += [
path('core/auth/captcha/', include('captcha.urls')),
]

最后只要在对应的form中加入验证码的字段就行了

1
2
class CaptchaMixin(forms.Form):
captcha = CaptchaField(widget=CustomCaptchaTextInput, label=_('Captcha'))

但事实上,看似简单的django-simple-captcha中实际包含着一个很大的问题。

django-simple-captcha 随机数种子泄露

这个问题在0.5.19版本中被修复

img

这里其实涉及到了django-simple-captcha的一个feature,在设计上其实是允许通过key来指定随机数种子的,这个feature是为了让同一个key可以对应同一个验证码,用来实现验证码的对应。

img

而这里的key是一个已知的值,就是用于生成验证码的参数

img

换言之,我们可以得知当前Random的随机数种子,甚至可以控制这个种子。

修复的方案也很简单粗暴,只要在生成结束之后用随机一个新种子就可以了

img

那么这对jumpserver又有什么影响呢?

JumpServer 密码重置漏洞

相比django-simple-captcha来说,JumpServer更像是一个受害者,虽然存在一些安全隐患但本身并不致命。我们可以猜想一下随机数在一般的系统里常用的场景。

  • 重置密码
  • 激活码、兑换码

相比激活码的场景来说,重置密码的常见程度更高,如果系统内没有刻意对管理员账号做限制,那么如果可以预测重置密码的验证结果,那么就可以获得一个超级管理员权限,而JumpServer的代码中是这样做的。

img

/apps/authentication/api/password.py

img

重置密码用的code使用了random_string来生成,然后看看random_string的定义

img

这个函数在jumpserver中重做过好几次,但大同小异,其实就是用random.choice从列表中选取随机字符,最终生成最后的验证码。

这里我们不去细纠这个利用方式中的细节点,这不是本篇文章的讨论重点,简单来说就是

  • 通过**django-simple-captcha泄露当前random的种子**
  • 通过种子推测有限次的random结果(其中不仅仅包括密码重置token,还有验证码噪点等)

这样我们就通过对随机数的预测实现进一步的漏洞利用,而修复的方案也很简单

在最初版本的修复方案中,Jumpserver在获取密码重置token时重置了当前随机数种子。这个修复方案也没什么问题。

img

在后来的改动中,可能是因为看了P牛的文章,Jumpserver把random换成了secrets,相对来说比前一个方案更稳定一些,也是相关文档中推荐的方案

img

For Joern

从源代码的角度来讲,这个漏洞成因可以分成两部分

  • 存在泄露随机数种子,或者可以控制随机数种子的位置
  • 在未显式重置随机数种子的基础上,引用了random来生成随机数

随机数种子泄露

其实这个漏洞用Joern来处理挺吃力的,首先是Joern只会处理目标目录下的源码,而在正常的环境下,python引入的包其实都在python的目录下,也就是说理论上我们无法在分析项目的时候,顺便分析django-simple-captcha。

这里我们强行引入下分析django-simple-captcha包

1
2
3
4
5
6
7
8
9
10
11
12
13
> .\joern

██╗ ██████╗ ███████╗██████╗ ███╗ ██╗
██║██╔═══██╗██╔════╝██╔══██╗████╗ ██║
██║██║ ██║█████╗ ██████╔╝██╔██╗ ██║
██ ██║██║ ██║██╔══╝ ██╔══██╗██║╚██╗██║
╚█████╔╝╚██████╔╝███████╗██║ ██║██║ ╚████║
╚════╝ ╚═════╝ ╚══════╝╚═╝ ╚═╝╚═╝ ╚═══╝
Version: 2.0.52
Type `help` to begin


joern> importCode("../captcha/")

找到random.seed调用的位置

img

先检查调用位置到函数定义位置是否有数据联通

1
2
3
4
def source = cpg.method("seed").caller.parameter
def sink = cpg.method("seed").callIn

sink.reachableByFlows(source).p

img

得到的答案是肯定的,seed的参数key可控,接着找对应的路由,在django里路由一般是用path,而这个组件使用re_path

1
2
from django.urls import path
from django.urls import re_path

我们可以直接快捷的查看相应的路由函数调用位置

img

然后通过对应的调用函数来获取指定的路由

1
cpg.method("re_path").callIn.filter(_._callViaAstOut.code.contains("views.captcha_image")).l

img

当然可能也不用这么麻烦,理论上来说直接设置source也是可以连起来的,只不过re_path读取key的方式是正则匹配,所以原装的reachableByFlows无法处理这种情况,我们只能强行做一些限制

1
cpg.method("re_path").callIn.filter(_._callViaAstOut.code.contains("views.captcha_image")).filterNot(_.argument.code.contains("<key>")).l

由于这条命令可以获取结果,所以代表着存在可以设置随机数种子的路由和对应的参数

当然,由于漏洞的特殊性不仅仅在于可控,还需要后续没有进一步重置随机数种子,所以我们还需要更多的条件来确认这一点。

其实要做到这点也并不复杂,只需要确认,在设置seed种子的方法中,没有调用过无参的seed方法即可。

1
cpg.method("seed").caller.filter(_._callViaContainsOut.filter(_.name.contains("seed")).filter(_.argument.size<2).size==0).l

上面这条命令的意思是

  • 寻找seed方法的调用方法
  • 寻找该方法中调用的方法中,名字为seed,并参数为0(joern中,参数index为0的位置表示为this,也就是当前方法所属的类)
  • 展示调用方法中,满足条件的调用数量为0的方法

img

如果返回结果,则证明该方法中没有重置新的随机数种子,当然,到这里并不能完全的验证这个结论,毕竟这里指处理了显式重置,如果是更严格的数据流分析,应该从重置随机数种子的位置入手,确认是否有数据流经过,但这种方案对于joern来说比较困难,这里先不深入到这个级别研究

JumpServer密码重置漏洞

这里分析JumpServer的时候遇到的最大的问题是JumpServer的代码量有点儿大,导入到Joern里有83万个节点:<

其实相比Django-simple-captcha的问题来说,JumpServer的问题在源代码的角度上来说更不像一个问题,只能算是一个使用错误的范例,有潜在的风险。我们需要用joern完成的工作包括两部分

  • 在获取随机数之前,没有重置过随机数种子
  • 在获取随机数之前,共执行了多少次随机操作

先找到对应调用random.choice方法的方法

img

调用过seed方法重置随机数种子的位置只有一个,看了一下没有相关的引用关系,看上去像是一段测试代码

img

由于场景特殊,这里我们用不到那么深入的数据流分析,只需要在对应重置密码的路由中确认是否调用random.choice方法就行了

img

这里直接用repeat untils来实现就可以

img

repeat…untils…还是那个老问题,容易递归爆炸,路径重复问题严重,我觉得这是joern实现里一个非常普遍的问题,但至少可以确定两个调用位置的连通性

接下来我们的问题变成了,我们如何知道在这条数据流中random调用了多少次

img

我尝试了几次之后发现,如果想要在语句上控制限制范围,以确认random的调用次数,会遇到比较多的问题正向分析的深入深度问题,以及循环分支的次数数据问题,问题比想象中的大,我暂且认为这不是joern的适用场景。

而相应的修复就更简单了,直接换用secrets替代random会直接影响到前面的方法发现,我们就无法获得对应的数据流了。

CATALOG
  1. 1. 漏洞成因
    1. 1.1. django-simple-captcha 随机数种子泄露
    2. 1.2. JumpServer 密码重置漏洞
  2. 2. For Joern
    1. 2.1. 随机数种子泄露
    2. 2.2. JumpServer密码重置漏洞